diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 1de066ac34..6045dfbffd 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -34,7 +34,7 @@ - **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), `masters/` (files on disk as the masters of scripts in the world: the links and their index, a master found from a script's `@file` hint or its name under the blessed folders, and what a send does) and `preprocessor/` (the preprocessor and its includes) +- **alscript** - Script Studio's language services, free of UI (depends on llcommon, llmath, and SLua's Luau with the Tailslide it carries). 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), `masters/` (files on disk as the masters of scripts in the world: the links and their index, a master found from a script's `@file` hint or its name under the blessed folders, and what a send does) 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 - **rust** - The Rust the viewer builds: one Cargo workspace and lock file, imported by Corrosion, a vcpkg port. It holds velopack_libc, Velopack's C API over the `velopack` crate from crates.io, its sources and headers vendored from the release with one buffer fix marked Alchemy (`cmake/Velopack.cmake`): a DLL on Windows, a static library on macOS and Linux, the release build for every configuration - **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 diff --git a/indra/alscript/core/alscriptformatter.cpp b/indra/alscript/core/alscriptformatter.cpp index d72ec4311c..a5f4011979 100644 --- a/indra/alscript/core/alscriptformatter.cpp +++ b/indra/alscript/core/alscriptformatter.cpp @@ -30,7 +30,9 @@ #include "alscriptlexicon.h" #include +#include #include +#include #include namespace @@ -175,6 +177,12 @@ namespace } if (!prev) { + // A vector or rotation opens where nothing stands before + // it: a line of a list may begin with one. + if (!lua && t.kind == Kind::Punct && t.text == "<") + { + ++vector; + } out += t.text; prev = &t; gap.clear(); @@ -666,11 +674,270 @@ namespace } } - std::string emit(const std::vector& lines, const ALScriptFormatter::Options& options, bool endsWithNewline, S32 first, S32 last) + // How many columns text takes: a tab a level's width, a character of + // several bytes one. + S32 columns(std::string_view text, S32 tab) { + S32 n = 0; + for (const char c : text) + { + n += c == '\t' ? tab : (static_cast(c) & 0xC0) != 0x80 ? 1 : 0; + } + return n; + } + + // A bracket of a line that closes on it, by its tokens' indexes: the + // commas directly in it, and the bracket it is in. An LSL vector or + // rotation is one too, which nothing breaks. + struct Group + { + size_t open = 0; + size_t close = 0; + size_t parent = std::string::npos; + size_t width = 0; + bool vector = false; + std::vector commas; + }; + + // What the tokens' brackets are; none where they do not match, or one + // is left open: a line that runs on over the next is not broken. + std::vector groupsOf(const std::vector& tokens, bool lua) + { + std::vector groups; + std::vector open; + const Token* before = nullptr; + for (size_t i = 0; i < tokens.size(); ++i) + { + const Token& t = tokens[i]; + if (!significant(t)) + { + continue; + } + if (t.kind == Kind::Punct) + { + // A vector where no operand stands before its <, as spaced + // reads one; one that never closes was a comparison. + const bool vector = !lua && t.text == "<" && !(before && operand(lua, *before)); + if (t.text == "(" || t.text == "[" || t.text == "{" || vector) + { + Group group; + group.open = i; + group.close = std::string::npos; + group.parent = open.empty() ? std::string::npos : open.back(); + group.vector = vector; + open.push_back(groups.size()); + groups.push_back(std::move(group)); + } + else if (t.text == ")" || t.text == "]" || t.text == "}") + { + while (!open.empty() && groups[open.back()].vector) + { + open.pop_back(); + } + const std::string_view opener = t.text == ")" ? "(" : t.text == "]" ? "[" : "{"; + if (open.empty() || tokens[groups[open.back()].open].text != opener) + { + return {}; + } + groups[open.back()].close = i; + open.pop_back(); + } + else if (t.text == ">" && !lua && !open.empty() && groups[open.back()].vector) + { + groups[open.back()].close = i; + open.pop_back(); + } + else if (t.text == "," && !open.empty() && !groups[open.back()].vector) + { + groups[open.back()].commas.push_back(i); + } + } + before = &t; + } + if (!open.empty() && std::any_of(open.begin(), open.end(), [&groups](size_t g) { return !groups[g].vector; })) + { + return {}; + } + for (Group& group : groups) + { + for (size_t i = group.open; group.close != std::string::npos && i <= group.close; ++i) + { + group.width += tokens[i].text.size(); + } + } + return groups; + } + + // A line's tokens as lines of their own, each with how far in it is. + struct Piece + { + std::vector tokens; + S32 indent = 0; + }; + + // The tokens from `from` to `to`, the blanks at either end left off. + std::vector slice(const std::vector& tokens, size_t from, size_t to) + { + while (from < to && tokens[from].kind == Kind::Space) + { + ++from; + } + while (to > from && tokens[to - 1].kind == Kind::Space) + { + --to; + } + return std::vector(tokens.begin() + static_cast(from), tokens.begin() + static_cast(to)); + } + + std::string written(const std::vector& tokens, S32 indent, const ALScriptFormatter::Options& options) + { + Line line; + line.tokens = tokens; + return indentText(options, indent) + spaced(line, options.lua); + } + + // The tokens as pieces no wider than the width where a bracket's commas + // let them be: the bracket broken is the widest with commas, looked + // for in the widest without, down; or, where its last part is a table + // or a list with commas of its own, that, which stays on the line + // that opens the bracket where that fits. + void wrap(const std::vector& tokens, S32 indent, const ALScriptFormatter::Options& options, std::vector& out) + { + const S32 tab = options.tabs ? options.indent : 1; + if (columns(written(tokens, indent, options), tab) <= options.width) + { + out.push_back({ tokens, indent }); + return; + } + const std::vector groups = groupsOf(tokens, options.lua); + const auto pick = [&groups](size_t parent, auto&& self) -> std::optional { + std::vector in; + for (size_t g = 0; g < groups.size(); ++g) + { + if (groups[g].parent == parent && !groups[g].vector && groups[g].close != std::string::npos) + { + in.push_back(g); + } + } + std::stable_sort(in.begin(), in.end(), [&groups](size_t a, size_t b) { return groups[a].width > groups[b].width; }); + for (size_t g : in) + { + if (!groups[g].commas.empty()) + { + return g; + } + if (const std::optional inner = self(g, self)) + { + return inner; + } + } + return std::nullopt; + }; + std::optional chosen = pick(std::string::npos, pick); + if (!chosen) + { + out.push_back({ tokens, indent }); + return; + } + // Its last part, where that is a table or a list of its own with + // commas, and the line to its opening fits. + const Group& group = groups[*chosen]; + size_t last = group.commas.back() + 1; + while (last < group.close && !significant(tokens[last])) + { + ++last; + } + for (size_t g = 0; g < groups.size(); ++g) + { + const Group& inner = groups[g]; + if (inner.parent == *chosen && inner.open == last && !inner.commas.empty() && tokens[inner.open].text != "(") + { + size_t end = inner.close + 1; + while (end < group.close && !significant(tokens[end])) + { + ++end; + } + if (end == group.close && columns(written(slice(tokens, 0, inner.open + 1), indent, options), tab) <= options.width) + { + chosen = g; + } + break; + } + } + const Group& broken = groups[*chosen]; + wrap(slice(tokens, 0, broken.open + 1), indent, options, out); + size_t from = broken.open + 1; + for (size_t comma : broken.commas) + { + wrap(slice(tokens, from, comma + 1), indent + 1, options, out); + from = comma + 1; + } + const std::vector rest = slice(tokens, from, broken.close); + if (!rest.empty()) + { + wrap(rest, indent + 1, options, out); + } + wrap(slice(tokens, broken.close, tokens.size()), indent, options, out); + } + + // A line past the width broken where it can be: its comment after its + // code, where it has one, after the last piece. As it is where it has + // a comment inside it, or a string or comment runs on past it. + std::string wrapped(const Line& line, const ALScriptFormatter::Options& options, const std::string& text) + { + const S32 tab = options.tabs ? options.indent : 1; + if (options.width <= 0 || line.first != line.last || columns(text, tab) <= options.width) + { + return text; + } + std::vector tokens = slice(line.tokens, 0, line.tokens.size()); + std::vector after; + if (!tokens.empty() && tokens.back().kind == Kind::Comment) + { + after.push_back(tokens.back()); + tokens = slice(tokens, 0, tokens.size() - 1); + } + if (std::any_of(tokens.begin(), tokens.end(), [](const Token& t) { return t.kind == Kind::Comment; })) + { + return text; + } + std::vector pieces; + wrap(tokens, line.indent, options, pieces); + if (pieces.size() < 2) + { + return text; + } + if (!after.empty()) + { + Token gap; + gap.kind = Kind::Space; + gap.text = " "; + pieces.back().tokens.push_back(gap); + pieces.back().tokens.push_back(after.front()); + } std::string out; - S32 blanks = 0; - const bool whole = first < 0; + for (const Piece& piece : pieces) + { + std::string one = options.spacing ? written(piece.tokens, piece.indent, options) : indentText(options, piece.indent) + asWritten(Line{ piece.tokens }); + while (!one.empty() && (one.back() == ' ' || one.back() == '\t')) + { + one.pop_back(); + } + out += (out.empty() ? "" : "\n") + one; + } + return out; + } + + // Each line as written out, or none for a blank line past a run's + // length where the whole text is asked for; past the width broken + // where `wrap`. + std::vector> writtenLines(const std::vector& lines, const ALScriptFormatter::Options& options, S32 first, + S32 last, bool wrap) + { + std::vector> out; + S32 blanks = 0; + size_t kept = 0; + const bool whole = first < 0; for (size_t i = 0; i < lines.size(); ++i) { const Line& line = lines[i]; @@ -695,8 +962,9 @@ namespace if (whole) { ++blanks; - if (blanks > options.maxBlankLines || out.empty()) + if (blanks > options.maxBlankLines || kept == 0) { + out.emplace_back(); continue; } } @@ -719,13 +987,35 @@ namespace { text.pop_back(); } + if (wrap) + { + text = wrapped(line, options, text); + } } if (!line.blank) { blanks = 0; } - out += text; - if (i + 1 < lines.size() || endsWithNewline) + ++kept; + out.push_back(std::move(text)); + } + return out; + } + + std::string emit(const std::vector& lines, const ALScriptFormatter::Options& options, bool endsWithNewline, S32 first, S32 last, + bool wrap) + { + const std::vector> each = writtenLines(lines, options, first, last, wrap); + const bool whole = first < 0; + std::string out; + for (size_t i = 0; i < each.size(); ++i) + { + if (!each[i]) + { + continue; + } + out += *each[i]; + if (i + 1 < each.size() || endsWithNewline) { out += '\n'; } @@ -753,7 +1043,7 @@ std::string ALScriptFormatter::format(std::string_view text, const Options& opti bool endsWithNewline = false; std::vector lines = linesOf(text, options.lua, endsWithNewline); decide(lines, options.lua); - return emit(lines, options, endsWithNewline, -1, -1); + return emit(lines, options, endsWithNewline, -1, -1, true); } // static @@ -763,7 +1053,46 @@ std::string ALScriptFormatter::formatLines(std::string_view text, const Options& bool endsWithNewline = false; std::vector lines = linesOf(text, options.lua, endsWithNewline); decide(lines, options.lua); - return emit(lines, options, endsWithNewline, first, last); + return emit(lines, options, endsWithNewline, first, last, false); +} + +// static +std::vector ALScriptFormatter::formatEach(std::string_view text, const Options& options, S32 first, S32 last) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + bool endsWithNewline = false; + std::vector lines = linesOf(text, options.lua, endsWithNewline); + decide(lines, options.lua); + // Every line kept, blank or not: from the first where `first` is + // below it, which would otherwise ask for the whole text's runs. + const std::vector> each = writtenLines(lines, options, llmax(first, 0), last, true); + // A line a string or a comment runs on over is as many of the text's; + // a line broken at the width is still one. + std::vector out; + for (size_t i = 0; i < each.size(); ++i) + { + const std::string& one = *each[i]; + if (lines[i].first == lines[i].last) + { + out.push_back(one); + continue; + } + for (size_t at = 0;;) + { + const size_t nl = one.find('\n', at); + out.push_back(one.substr(at, nl == std::string::npos ? std::string::npos : nl - at)); + if (nl == std::string::npos) + { + break; + } + at = nl + 1; + } + } + if (endsWithNewline) + { + out.emplace_back(); + } + return out; } // static diff --git a/indra/alscript/core/alscriptformatter.h b/indra/alscript/core/alscriptformatter.h index a064eae9e3..a7db72496f 100644 --- a/indra/alscript/core/alscriptformatter.h +++ b/indra/alscript/core/alscriptformatter.h @@ -43,7 +43,8 @@ // gap before a trailing comment, a preprocessor line, anything inside // a string or a comment. Runs of blank lines are shortened and trailing // whitespace dropped, unless only some lines are asked for, in which -// case every line keeps its number. +// case every line keeps its number. A line too long may be broken at +// the commas of a bracket on it; no other break is added. class ALScriptFormatter { public: @@ -58,6 +59,16 @@ class ALScriptFormatter // Whether the spaces within a line are touched at all, or only // the indentation. bool spacing = true; + // How many columns a line may take before it is broken, a tab as + // wide as a level: at the commas of the widest bracket on it that + // has any, or of the table or list a call ends with, which stays + // on the call's line -- `f(a, {` and `})` -- each part on a line + // of its own, a level further in, and broken again where it is + // still too long. Never a vector or rotation, nor a line with a + // comment inside it or a string or comment running on past it. + // Nought for never. Not where only some lines are asked for of + // formatLines, whose lines keep their numbers. + S32 width = 0; }; // The whole text. @@ -65,6 +76,12 @@ class ALScriptFormatter // Only the lines from `first` to `last`, zero-based and inclusive, // changed; the rest as it was, and every line where it was. static std::string formatLines(std::string_view text, const Options& options, S32 first, S32 last); + // The same, as each line of the text in order, one entry a line -- + // and an entry for the empty line after a final newline -- each of + // those from `first` to `last` broken past the width, its entry + // holding the breaks. A `first` below nought is the first line: no + // line is taken out. + static std::vector formatEach(std::string_view text, const Options& options, S32 first, S32 last); // Which line breaks stand inside a string, one for each line of `text`: // true where the break that ends the line is part of a string that runs diff --git a/indra/alscript/core/alscriptmessages.cpp b/indra/alscript/core/alscriptmessages.cpp index cce3602ae1..90d162620e 100644 --- a/indra/alscript/core/alscriptmessages.cpp +++ b/indra/alscript/core/alscriptmessages.cpp @@ -38,6 +38,11 @@ namespace // as the viewer scrolls to them. const ALRegex LUAU_LOCATION(R"(^([^:]*):([0-9]+):\s*(.*)$)"); const ALRegex LSL_LOCATION(R"(\((\d+), (\d+)\) : ([^:]+) : (.+))"); + // Tailslide's, as the grid's LSL compiler for Luau's VM gives it: a + // one-based line, the level, the words; and the places they name, + // one-based line and column. + const ALRegex TAILSLIDE_LOCATION(R"(^\s*Line (\d+):\s*([A-Za-z]+):\s*(.*)$)"); + const ALRegex TAILSLIDE_MENTION(R"(\((\d+), (\d+)\))"); const ALRegex DEFAULT_STATE(R"(\s*default\s*\{)"); // A frame of a Luau traceback: the chunk, bare -- a script's name, // spaces and all -- or as Lua quotes one, and its one-based line, then @@ -82,6 +87,23 @@ namespace ALScriptMessages return place; } } + if (TAILSLIDE_LOCATION.match(line, &found)) + { + place.line = llmax(0, std::atoi(found.str(1).c_str()) - 1); + place.hasLine = true; + place.level = found.str(2); + place.message = found.str(3); + TAILSLIDE_MENTION.forEach(place.message, [&place](const ALRegexMatch& named) { + Mention mention; + mention.at = named.begin(); + mention.length = named.length(); + mention.line = llmax(0, std::atoi(named.str(1).c_str()) - 1); + mention.column = llmax(0, std::atoi(named.str(2).c_str()) - 1); + place.mentions.push_back(mention); + return true; + }); + return place; + } place.level = "ERROR"; place.message = line; return place; diff --git a/indra/alscript/core/alscriptmessages.h b/indra/alscript/core/alscriptmessages.h index 135a1a1293..b97d469560 100644 --- a/indra/alscript/core/alscriptmessages.h +++ b/indra/alscript/core/alscriptmessages.h @@ -39,23 +39,39 @@ // can hold them still. namespace ALScriptMessages { + // A place the words of a message name, as `previously declared at + // (345, 1)`: where it stands in them, and the line and column it + // names, zero-based. Written again as the source counts, where the + // text the compiler had is not the source's. + struct Mention + { + size_t at = 0; + size_t length = 0; + S32 line = 0; + S32 column = 0; + }; + // Zero-based line and column, as everything in the studio counts. struct Place { S32 line = 0; S32 column = 0; bool hasColumn = false; - // A line named at all: not by what matched neither compiler's form. + // A line named at all: not by what matched no compiler's form. bool hasLine = false; - // The compiler's own word: ERROR or WARNING. + // The compiler's own word: ERROR, WARNING, or WARN. std::string level; std::string message; + std::vector mentions; }; // One line of what a compiler said. Luau names the chunk and a // one-based line; LSL gives a zero-based line and column in - // brackets with its level between colons; anything else is an error - // with no place. The language's form is tried first, then the other's. + // brackets with its level between colons; the grid's LSL compiler + // for Luau's VM, which is Tailslide, says `Line 320: WARN: words`, + // its line and the places its words name, `(345, 1)`, one-based; + // anything else is an error with no place. The language's form is + // tried first, then the other's, then Tailslide's. Place readDiagnostic(const std::string& line, bool lua); // Every line of them. std::vector readDiagnostics(const LLSD& errors, bool lua); diff --git a/indra/alscript/lint/alscriptlintpass.cpp b/indra/alscript/lint/alscriptlintpass.cpp index 54826b252e..77fe7683de 100644 --- a/indra/alscript/lint/alscriptlintpass.cpp +++ b/indra/alscript/lint/alscriptlintpass.cpp @@ -1850,9 +1850,11 @@ namespace // What ll's row says of an llcompat call, and the call written as // ll's: llcompat put as ll, each index from 0 up moved by one, as - // the assistant moves them. Nothing where ll lacks the function or - // deprecates it, gives booleans in a list, or an index is not a - // number. + // the assistant moves them. Nothing where ll lacks the function, + // deprecates it and differs from llcompat's too, gives booleans in + // a list, or an index is not a number. One ll's only deprecates is + // llcompat's, deprecated as every llcompat function is; ll's names + // what SLua would use. struct Compat { const ALLSLTraits::Trait* row = nullptr; @@ -1877,7 +1879,7 @@ namespace out.lsl = "ll" + out.name; out.row = ALLSLTraits::of(out.lsl.c_str()); constexpr U8 lacking = ALLSLTraits::SluaRemoved | ALLSLTraits::SluaAbsent | ALLSLTraits::SluaDeprecated | ALLSLTraits::SluaBoolList; - if (!out.row || (out.row->slua & lacking)) + if (!out.row || ((out.row->slua & lacking) && out.row->slua != ALLSLTraits::SluaDeprecated)) { return std::nullopt; } diff --git a/indra/alscript/lsl/allslservice.cpp b/indra/alscript/lsl/allslservice.cpp index d22592379b..95a75403d5 100644 --- a/indra/alscript/lsl/allslservice.cpp +++ b/indra/alscript/lsl/allslservice.cpp @@ -1364,7 +1364,10 @@ bool ALLSLService::loadBuiltins(const std::string& path, std::string& error) { return true; } - Tailslide::tailslide_init_builtins_from_data(added.c_str()); + if (!Tailslide::tailslide_init_builtins_from_data(added.c_str())) + { + LL_WARNS("ScriptAnalysis") << "Some of the LSL definitions " << path << " adds could not be read and were left out" << LL_ENDL; + } // 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; @@ -1396,9 +1399,9 @@ bool ALLSLService::loadBuiltins(const std::string& path, std::string& error) // 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. + // cannot parse -- it says so on stderr and skips, and its load then + // answers false, which the log is told of: the file is the grid's, and + // what it could read is loaded all the same. std::string text; { llifstream in(path, std::ios::in | std::ios::binary); @@ -1411,7 +1414,10 @@ bool ALLSLService::loadBuiltins(const std::string& path, std::string& error) all << in.rdbuf(); text = all.str(); } - Tailslide::tailslide_init_builtins_from_data(text.c_str()); + if (!Tailslide::tailslide_init_builtins_from_data(text.c_str())) + { + LL_WARNS("ScriptAnalysis") << "Some of the LSL definitions in " << path << " could not be read and were left out" << LL_ENDL; + } // 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/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index be7fb69c7c..061ead39e6 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -454,8 +454,28 @@ namespace 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. + // with any of what it held that nothing wrote, at its end. Those + // inside the node are put off to the first line written of it, + // which may be a list's over several lines that has them in its + // lines (Writer::listLines), and are over it where not. void commentsBefore(LSLASTNode* node); + std::vector mDeferred; + // Where a list over lines has a comment, in its text: marked with a + // byte no expression's text has (strings escape it) and the + // comment's index, and how it goes -- after what is on the line + // (After), on a line of its own (Own), or before what follows on + // the line (Before). Written by Writer::line, which writes each + // comment once: a mark of one written is taken out, its line with + // it if it had one. + static constexpr char COMMENT_MARK = '\x7f'; + enum class Marked : char + { + After = 'a', + Own = 'o', + Before = 'b' + }; + static std::string marked(size_t k, Marked how); + std::string withComments(const std::string& text); void commentsAtEnd(LSLASTNode* holder); void writeComment(Comment& c); // Those after a node on the line it ends, where it is all on one @@ -463,6 +483,10 @@ namespace // 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); + // Where the line of code that begins at `from` in the text ends: + // its break, or that of the last line a bracket it opens runs on + // over. + size_t codeLineEnd(size_t from) const; // How long what commentsAfter put after each node's line was. boost::unordered_flat_map mTrailed; // Those on the line a block's brace opens, before anything in it: @@ -504,10 +528,20 @@ namespace }; std::vector mRun; std::string indent() const { return std::string(static_cast(mDepth) * 4, ' '); } + // How far in what a list over lines (Writer::listLines) or a rule + // table holds goes: a level past the line it opens on, and a level + // more for each it is in. + S32 mListDepth = 0; + std::string listIndent(S32 more) const { return indent() + std::string(static_cast(mListDepth + more) * 4, ' '); } // --- expressions ------------------------------------------------------------ Expr expr(LSLExpression* e); + // A list literal the LSL wrote over several lines, over the same + // lines: what it put on a line together on one, `{` where its `[` + // was and `}` on a line of its own where its `]` was, each comment + // in it where it was. + std::string listLines(LSLASTNode* list); // As a number where LSL has one, a boolean made 1 or 0. Expr value(LSLExpression* e); // As a boolean, as LSL reads a value in a condition. @@ -1082,9 +1116,28 @@ namespace void Writer::line(const std::string& text) { // What was noted of it, over it -- over the statement a line made - // inside an expression stands in, where it is one. + // inside an expression stands in, where it is one -- under the + // comments put off to it that it has no place for. if (!mInline) { + const auto has_mark = [&text](size_t k) { + const std::string head = COMMENT_MARK + std::to_string(k); + for (size_t at = text.find(head); at != std::string::npos; at = text.find(head, at + 1)) + { + if (at + head.size() < text.size() && std::isalpha(static_cast(text[at + head.size()]))) + { + return true; + } + } + return false; + }; + for (size_t k : std::exchange(mDeferred, {})) + { + if (!has_mark(k)) + { + writeComment(mComments[k]); + } + } for (const std::string& said : mPending) { if (mOptions.comments) @@ -1114,7 +1167,50 @@ namespace { sameAs("}", "end"); } - mText += text.empty() ? std::string("\n") : indent() + text + "\n"; + mText += text.empty() ? std::string("\n") : indent() + withComments(text) + "\n"; + } + + // static + std::string Writer::marked(size_t k, Marked how) + { + return COMMENT_MARK + std::to_string(k) + static_cast(how) + COMMENT_MARK; + } + + std::string Writer::withComments(const std::string& text) + { + if (text.find(COMMENT_MARK) == std::string::npos) + { + return text; + } + std::string out; + size_t at = 0; + while (at < text.size()) + { + const size_t mark = text.find(COMMENT_MARK, at); + if (mark == std::string::npos) + { + out.append(text, at, std::string::npos); + break; + } + out.append(text, at, mark - at); + const size_t close = text.find(COMMENT_MARK, mark + 1); + const size_t k = std::strtoul(text.c_str() + mark + 1, nullptr, 10); + const Marked how = static_cast(text[close - 1]); + at = close + 1; + Comment& c = mComments[k]; + if (!c.written) + { + c.written = true; + out += how == Marked::After ? " " + c.text : how == Marked::Before ? c.text + " " : c.text; + } + else if (how == Marked::Own) + { + // Its line too, break and all. + const size_t line = out.rfind('\n'); + out.erase(line == std::string::npos ? 0 : line); + } + } + return out; } S32 Writer::linesWritten() @@ -1599,11 +1695,24 @@ namespace // Every node written is through here first: the first line of code // written after its comments is the one made of it. mAnchorPending.reset(); + // Any put off that no line took, over what follows. + for (size_t k : std::exchange(mDeferred, {})) + { + writeComment(mComments[k]); + } if (const auto found = mCommentsBefore.find(node); found != mCommentsBefore.end()) { + const Pos begins = isNull(node) ? Pos{} : Pos{ node->getLoc()->first_line, node->getLoc()->first_column }; for (size_t k : found->second) { - writeComment(mComments[k]); + if (!isNull(node) && begins < mComments[k].at) + { + mDeferred.push_back(k); + } + else + { + writeComment(mComments[k]); + } } } if (!isNull(node) && node->getLoc()) @@ -1636,7 +1745,7 @@ namespace const size_t first = mText.find_first_not_of(' ', at); if (first < end && mText.compare(first, 2, "--") != 0) { - putTrailing(end, after); + putTrailing(codeLineEnd(at), after); mTrailed[node] = after.size(); return; } @@ -1645,6 +1754,63 @@ namespace mText += indent() + after.substr(1) + "\n"; } + size_t Writer::codeLineEnd(size_t from) const + { + // Past strings and comments, counting brackets: Luau's, as written + // here. + S32 open = 0; + size_t i = from; + // A long bracket's level at `at`, `[==[`, or -1 where there is none. + const auto level = [this](size_t at) { + size_t n = at + 1; + while (n < mText.size() && mText[n] == '=') + { + ++n; + } + return n < mText.size() && mText[n] == '[' ? static_cast(n - at - 1) : -1; + }; + const auto pastLong = [this](size_t at, S32 equals) { + const std::string closer = "]" + std::string(static_cast(equals), '=') + "]"; + const size_t found = mText.find(closer, at); + return found == std::string::npos ? mText.size() : found + closer.size(); + }; + while (i < mText.size()) + { + const char c = mText[i]; + if (c == '\n') + { + if (open <= 0) + { + return i; + } + ++i; + } + else if (c == '"' || c == '\'' || c == '`') + { + for (++i; i < mText.size() && mText[i] != c && mText[i] != '\n'; ++i) + { + i += mText[i] == '\\' ? 1 : 0; + } + ++i; + } + else if (c == '-' && i + 1 < mText.size() && mText[i + 1] == '-') + { + const S32 equals = i + 2 < mText.size() && mText[i + 2] == '[' ? level(i + 2) : -1; + i = equals >= 0 ? pastLong(i + 2, equals) : std::min(mText.find('\n', i), mText.size()); + } + else if (c == '[' && level(i) >= 0) + { + i = pastLong(i, level(i)); + } + else + { + open += c == '(' || c == '{' || c == '[' ? 1 : c == ')' || c == '}' || c == ']' ? -1 : 0; + ++i; + } + } + return std::min(mText.find('\n', from), mText.size()); + } + bool Writer::trail(LSLASTNode* body) { const auto found = mCommentsOpening.find(body); @@ -2946,9 +3112,9 @@ namespace std::string table = lines ? "{\n" : "{ "; for (size_t i = 0; i < fields.size(); ++i) { - table += lines ? indent() + " " + fields[i] + ",\n" : (i ? ", " : "") + fields[i]; + table += lines ? listIndent(1) + fields[i] + ",\n" : (i ? ", " : "") + fields[i]; } - table += lines ? indent() + "}" : " }"; + table += lines ? listIndent(0) + "}" : " }"; called = "ll." + lsl.substr(2); LSLParamList* params = e->getIdentifier()->getSymbol()->getFunctionDecl(); return Expr{ called + "(" + args(e->getArguments(), params, 0, call->at, table) + ")" }; @@ -3220,24 +3386,67 @@ namespace noteOnce(e, "SluaAbsent", "SLua has no [1], in ll or in llcompat: it is left out.", { lsl }); return { defaultOf(e->getIType()) }; } + // SLua's word on one it deprecates, what it would use and why: which + // says more than how its indexes count. The why is the definitions' + // own. But the two XorBase64Strings: SLua names ll.XorBase64 for + // both, with no why, and it answers otherwise than either -- said + // how, since LSL's is kept. + const auto deprecated = [&]() { + if (lsl == "llXorBase64Strings") + { + noteOnce(e, "SluaXorBase64Wrong", + "SLua deprecates ll.XorBase64Strings, for ll.XorBase64, which XORs correctly where this did not, and so answers " + "otherwise: ll.XorBase64Strings is LSL's."); + } + else if (lsl == "llXorBase64StringsCorrect") + { + noteOnce(e, "SluaXorBase64Nul", + "SLua deprecates ll.XorBase64StringsCorrect, for ll.XorBase64, which answers otherwise where the second string " + "holds a NUL, which ended it here: ll.XorBase64StringsCorrect is LSL's."); + } + else if (trait->sluaUse && trait->sluaReason) + { + noteOnce(e, "SluaDeprecatedForWhy", "SLua deprecates ll.[1], for [2]: [3]", { bare, trait->sluaUse, trait->sluaReason }); + } + else if (trait->sluaUse) + { + noteOnce(e, "SluaDeprecatedFor", "SLua deprecates ll.[1], for [2].", { bare, trait->sluaUse }); + } + else if (trait->sluaReason) + { + noteOnce(e, "SluaDeprecatedWhy", "SLua deprecates ll.[1]: [2]", { bare, trait->sluaReason }); + } + else + { + noteOnce(e, "SluaDeprecated", "SLua deprecates ll.[1].", { bare }); + } + }; // SLua's ll where it means the same: a boolean answer, which a // condition reads as it is and a number takes as 1 or 0; index // arguments written out, moved on by one. Not where the list it // answers has booleans in LSL's 1 and 0's places, nor one SLua - // deprecates, which llcompat keeps LSL's where no way of SLua's - // own (Writer::idiom) is sure to mean the same. + // deprecates that differs from llcompat's, which keeps LSL's where + // no way of SLua's own (Writer::idiom) is sure to mean the same. + // One SLua deprecates and nothing else is the one function in both, + // deprecated in both, as every llcompat function is: ll's, whose + // deprecation names what SLua would use. const U8 compat_only = ALLSLTraits::SluaRemoved | ALLSLTraits::SluaIndexResult | ALLSLTraits::SluaBoolList | ALLSLTraits::SluaDeprecated; - const bool ll_indexes = !(slua & ALLSLTraits::SluaIndexArgs) || constantIndexes(e, indexes); - if (slua == 0 || (mOptions.sluaCalls && !(slua & compat_only) && ll_indexes)) + const bool ll_indexes = !(slua & ALLSLTraits::SluaIndexArgs) || constantIndexes(e, indexes); + const bool only_deprecated = slua == ALLSLTraits::SluaDeprecated; + if (slua == 0 || only_deprecated || (mOptions.sluaCalls && !(slua & compat_only) && ll_indexes)) { - if (trait && trait->sluaUse) + called = "ll." + bare; + if (only_deprecated) + { + deprecated(); + } + else if (trait && trait->sluaUse) { noteOnce(e, "SluaUse", "SLua would use [1] for [2].", { trait->sluaUse, lsl }); } const std::string args_text = (slua & ALLSLTraits::SluaIndexArgs) ? llArgs(e, indexes) : args(e->getArguments(), params, trait ? trait->sluaTextArgs : 0); - called = "ll." + bare; return { called + "(" + args_text + ")", PRIMARY, (slua & ALLSLTraits::SluaBool) != 0 }; } // An index SLua's ll counts from 1, or nil for none, read as LSL's: @@ -3267,39 +3476,7 @@ namespace } else if (slua & ALLSLTraits::SluaDeprecated) { - // SLua's word on it, what it would use and why: which says more - // than how its indexes count. The why is the definitions' own. - // But the two XorBase64Strings: SLua names ll.XorBase64 for both, - // with no why, and it answers otherwise than either -- said how, - // since llcompat's is kept. - if (lsl == "llXorBase64Strings") - { - noteOnce(e, "SluaXorBase64Wrong", - "SLua deprecates ll.XorBase64Strings, for ll.XorBase64, which XORs correctly where this did not, and so answers " - "otherwise: llcompat's is LSL's."); - } - else if (lsl == "llXorBase64StringsCorrect") - { - noteOnce(e, "SluaXorBase64Nul", - "SLua deprecates ll.XorBase64StringsCorrect, for ll.XorBase64, which answers otherwise where the second string " - "holds a NUL, which ended it here: llcompat's is LSL's."); - } - else if (trait->sluaUse && trait->sluaReason) - { - noteOnce(e, "SluaDeprecatedForWhy", "SLua deprecates ll.[1], for [2]: [3]", { bare, trait->sluaUse, trait->sluaReason }); - } - else if (trait->sluaUse) - { - noteOnce(e, "SluaDeprecatedFor", "SLua deprecates ll.[1], for [2].", { bare, trait->sluaUse }); - } - else if (trait->sluaReason) - { - noteOnce(e, "SluaDeprecatedWhy", "SLua deprecates ll.[1]: [2]", { bare, trait->sluaReason }); - } - else - { - noteOnce(e, "SluaDeprecated", "SLua deprecates ll.[1].", { bare }); - } + deprecated(); } else if (slua & ALLSLTraits::SluaIndexResult) { @@ -3831,6 +4008,12 @@ namespace } case NODE_LIST_EXPRESSION: { + // Over the lines the LSL wrote it over; but not in a line + // made inside an expression, which is made one line. + if (!mInline && e->getChild(0) && !isNull(e->getChild(0)) && e->getLoc()->first_line != e->getLoc()->last_line) + { + return { listLines(e) }; + } std::string out; for (LSLASTNode* item = e->getChild(0); item; item = item->getNext()) { @@ -3845,6 +4028,87 @@ namespace } } + std::string Writer::listLines(LSLASTNode* list) + { + const auto begins = [](LSLASTNode* n) { return Pos{ n->getLoc()->first_line, n->getLoc()->first_column }; }; + const auto ends = [](LSLASTNode* n) { return Pos{ n->getLoc()->last_line, n->getLoc()->last_column }; }; + std::vector items; + for (LSLASTNode* item = list->getChild(0); item; item = item->getNext()) + { + items.push_back(static_cast(item)); + } + // Its comments, in order: those in no item of it, an item's being + // its own to place. Not a toggle's, which is about the code over or + // under it. + const Pos open = begins(list); + const Pos close = ends(list); + std::vector comments; + const auto from = std::lower_bound(mComments.begin(), mComments.end(), open, [](const Comment& c, const Pos& p) { return c.at < p; }); + for (auto it = from; it != mComments.end() && it->at < close; ++it) + { + const Comment& c = *it; + if (c.written || c.closer || c.text.rfind("---[", 0) == 0 || + std::any_of(items.begin(), items.end(), [&](LSLExpression* item) { return !(c.at < begins(item)) && c.at < ends(item); })) + { + continue; + } + comments.push_back(static_cast(it - mComments.begin())); + } + ++mListDepth; + std::string out = "{"; + // The LSL line what is written ends on, and the comment next. + S32 line = open.line; + size_t next = 0; + // Those before `upto`: after what is on the line so far, on lines of + // their own, then before what is on the line `upto` is on; a break + // to that line, written at `in`, or a space where there was one. + const auto to = [&](const Pos& upto, const std::string& in, bool spaced) { + std::string before; + for (; next < comments.size() && mComments[comments[next]].at < upto; ++next) + { + const size_t k = comments[next]; + const S32 at = mComments[k].at.line; + if (at == line) + { + out += marked(k, Marked::After); + } + else if (at < upto.line) + { + out += "\n" + listIndent(0) + marked(k, Marked::Own); + } + else + { + before += marked(k, Marked::Before); + } + } + if (upto.line > line) + { + out += "\n" + in; + } + else if (spaced) + { + out += " "; + } + out += before; + }; + for (size_t i = 0; i < items.size(); ++i) + { + LSLExpression* item = items[i]; + if (i) + { + out += ","; + } + to(begins(item), listIndent(0), i > 0); + // As LSL typed it: NULL_KEY in a list is LSL's string. + out += coerced(item, item->getIType()).text; + line = item->getLoc()->last_line; + } + to(close, listIndent(-1), false); + out += "}"; + --mListDepth; + return out; + } + // --- statements ------------------------------------------------------------------- void Writer::assign(LSLLValueExpression* target, LSLOperator op, LSLExpression* rhs) @@ -6887,7 +7151,14 @@ namespace LSLASTNode* first = handler->getArguments() ? handler->getArguments()->getChild(0) : nullptr; if (detectedEvent(event)) { - if (first) + // Only where the body names it: a local nothing reads is what a + // lint says to take out. + LSLSymbol* count = first ? static_cast(first)->getSymbol() : nullptr; + bool named = false; + walk(handler->getStatements(), [&](LSLASTNode* node) { + named = named || (count && node->getNodeType() == NODE_IDENTIFIER && static_cast(node)->getSymbol() == count); + }); + if (named) { lead.push_back("local " + nameOf(static_cast(first)) + typed(LST_INTEGER) + " = #detected"); } diff --git a/indra/alscript/lsl/allslvalues.cpp b/indra/alscript/lsl/allslvalues.cpp index 57502e5dd4..b9b5de1ed7 100644 --- a/indra/alscript/lsl/allslvalues.cpp +++ b/indra/alscript/lsl/allslvalues.cpp @@ -108,11 +108,11 @@ LSLConstant* ALLSLArithmetic::operation(LSLOperator op, LSLConstant* cv, LSLCons } // 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. + // two agree: LSO works in singles, and Mono's sums are not known to. + // Tailslide's arithmetic has them as singles since 1.5.15 (before, 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(); diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 39ffb51e17..a711f5cd2c 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -27,6 +27,7 @@ #include "../lsl/allsltoslua.h" #include "../lint/alscriptfixes.h" #include "../lsl/allslservice.h" +#include "../lsl/allsltraits.h" #include "../luau/alluauservice.h" #include "../test/lltut.h" @@ -92,13 +93,19 @@ namespace tut // What the converter must not write: an error, what the studio's own // lints would have written SLua's way, or a deprecated ll function -- - // SLua's way where it means the same, llcompat's where not. + // SLua's way where it means the same, llcompat's where not. But one + // SLua only deprecates, the one function llcompat's is: ll's. static bool unwanted(const ALScriptProblem& p) { static const std::vector OWN = { "SlCompoundAssign", "SlNumberTruth", "SlZeroIndex", "SlParenCondition", "SlGeneralizedFor", "SlIndexDivision", "SlVectorProduct" }; + const auto only_deprecated = [](const std::string& member) { + const ALLSLTraits::Trait* trait = ALLSLTraits::of(("ll" + member.substr(3)).c_str()); + return trait && trait->slua == ALLSLTraits::SluaDeprecated; + }; return p.severity == ALScriptProblem::Severity::Error || std::find(OWN.begin(), OWN.end(), p.code) != OWN.end() || - (p.key.rfind("LuauLintDeprecatedMember", 0) == 0 && !p.args.empty() && p.args[0].rfind("ll.", 0) == 0); + (p.key.rfind("LuauLintDeprecatedMember", 0) == 0 && !p.args.empty() && p.args[0].rfind("ll.", 0) == 0 && + !only_deprecated(p.args[0])); } // What SLua's check says of it: nothing unwanted. @@ -351,7 +358,8 @@ namespace tut template<> template<> void allsltoslua_object::test<7>() { - set_test_name("close to LSL: llcompat's Detected functions and timer event, as LSL had them, noted with SLua's ways"); + set_test_name("close to LSL: llcompat's Detected functions and timer event, as LSL had them, noted with SLua's ways; ll's " + "OwnerSay, which only SLua's deprecating sets apart"); const ALLSLToSLua::Options close = ALLSLToSLua::Options::closeToLSL(); const ALLSLToSLua::Result r = ALLSLToSLua::convert("default {\n" " state_entry() { llSetTimerEvent(2.0); }\n" @@ -362,8 +370,8 @@ namespace tut ensure("converted", r.converted); ensure("llcompat's Detected: " + r.text, has(r, "llcompat.DetectedName(0)") && noted(r, "SluaDetected")); ensure("the timer event: " + r.text, has(r, "llcompat.SetTimerEvent(2") && has(r, "LLEvents:on(\"timer\"") && noted(r, "SluaTimer")); - ensure("llcompat.OwnerSay, SLua deprecating ll's for print: " + r.text, - has(r, "llcompat.OwnerSay(\"tick\")") && noted(r, "SluaDeprecatedFor", "OwnerSay")); + ensure("ll.OwnerSay, SLua deprecating it for print: " + r.text, + has(r, "ll.OwnerSay(\"tick\")") && !has(r, "llcompat.OwnerSay") && noted(r, "SluaDeprecatedFor", "OwnerSay")); checksClean(r); } @@ -1263,7 +1271,7 @@ namespace tut 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, " + 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 on their lines, " "a trailing one, a brace's after its opening line, 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" @@ -1310,7 +1318,8 @@ namespace tut 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("those inside a list, on their lines: " + r.text, + has(r, "local NAMES = {\n \"open\", -- when it is open\n \"closed\" -- when it is shut\n}\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, after the line that opens it: " + r.text, has(r, "-- Turns the door.\nlocal function swing(by) -- by degrees\n")); @@ -2004,7 +2013,7 @@ namespace tut void allsltoslua_object::test<66>() { set_test_name("Base64 through llbase64: encode as it is, the same bytes as LSL's; decode noted once, as it keeps a NUL and what is " - "not UTF-8; close to LSL, llcompat's"); + "not UTF-8; close to LSL, ll's, which only SLua's deprecating sets apart from llcompat's"); const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" " key k = llDetectedKey(0);\n" " string s = llStringToBase64(\"hello\");\n" @@ -2016,14 +2025,15 @@ namespace tut ensure("encode: " + r.text, has(r, "local s = llbase64.encode(\"hello\")") && has(r, "local t = llbase64.encode(tostring(k))")); ensure("decode: " + r.text, has(r, "local d = llbase64.decode(s)") && has(r, "local e = llbase64.decode(t .. \"=\")")); ensure("decode noted once: " + r.text, noted(r, "SluaBase64Decode") && count(r, "-- LSL: llbase64.decode keeps every byte") == 1); - ensure("nothing of llcompat's: " + r.text, !has(r, "llcompat.") && !noted(r, "SluaDeprecatedFor", "StringToBase64") && - !noted(r, "SluaDeprecatedFor", "Base64ToString")); + ensure("nothing of llcompat's or ll's: " + r.text, !has(r, "llcompat.") && !has(r, "ll.StringToBase64") && !has(r, "ll.Base64ToString") && + !noted(r, "SluaDeprecatedFor", "StringToBase64") && + !noted(r, "SluaDeprecatedFor", "Base64ToString")); checksClean(r); const ALLSLToSLua::Result close = ALLSLToSLua::convert("default { state_entry() { llOwnerSay(llBase64ToString(llStringToBase64(\"x\"))); } }\n", ALLSLToSLua::Options::closeToLSL()); ensure("close to LSL: " + close.text, - has(close, "llcompat.Base64ToString(llcompat.StringToBase64(\"x\"))") && !has(close, "llbase64.decode(") && + has(close, "ll.Base64ToString(ll.StringToBase64(\"x\"))") && !has(close, "llcompat.") && !has(close, "llbase64.decode(") && !has(close, "llbase64.encode(") && !noted(close, "SluaBase64Decode")); } @@ -2162,7 +2172,7 @@ namespace tut void allsltoslua_object::test<76>() { set_test_name("a key of no text said to be NULL_KEY, and one with capitals said to be written small, cast or given; other text as " - "it was said; the two XorBase64Strings said to be what ll.XorBase64 is not, llcompat's kept"); + "it was said; the two XorBase64Strings said to be what ll.XorBase64 is not, LSL's kept"); const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" " key a = (key)\"\"; key c = (key)\"A2E76FCD-9360-4F6D-A924-938F923DF11D\";\n" " if ((key)\"\" == NULL_KEY) llOwnerSay(\"same\");\n" @@ -2175,8 +2185,8 @@ namespace tut ensure("capitals, small: " + r.text, has(r, "uuid(\"A2E76FCD-9360-4F6D-A924-938F923DF11D\")") && noted(r, "SluaKeyCase")); ensure("other text as it was: " + r.text, has(r, "local d = uuid(s)") && noted(r, "SluaUuid")); ensure("ll.XorBase64 as it is: " + r.text, has(r, "ll.XorBase64(\"a\", \"b\")")); - ensure("the two, llcompat's: " + r.text, - has(r, "llcompat.XorBase64Strings(\"a\", \"b\")") && has(r, "llcompat.XorBase64StringsCorrect(\"a\", \"b\")")); + ensure("the two, LSL's: " + r.text, + has(r, "ll.XorBase64Strings(\"a\", \"b\")") && has(r, "ll.XorBase64StringsCorrect(\"a\", \"b\")") && !has(r, "llcompat.")); ensure("said what ll.XorBase64 is not: " + r.text, noted(r, "SluaXorBase64Wrong") && noted(r, "SluaXorBase64Nul") && !noted(r, "SluaDeprecated", "XorBase64Strings") && !noted(r, "SluaDeprecated", "XorBase64StringsCorrect")); @@ -2759,3 +2769,100 @@ namespace tut } } } + +namespace tut +{ + template<> template<> + void allsltoslua_object::test<82>() + { + set_test_name("a list the LSL wrote over several lines is written over the same lines, nested ones a level further in, each " + "comment in it where it was; a comment after it on the line it ends trails that line; a list on one line, or in " + "a line made inside an expression, stays on one"); + const ALLSLToSLua::Result r = convert("list gColours = [\n" + " // warm\n" + " \"red\", \"orange\",\n" + " /* cool */ \"blue\" // last\n" + "];\n" + "list gTwo = [1, 2,\n" + " 3]; // three\n" + "default { touch_start(integer n) {\n" + " string activeRequest = llList2Json(JSON_OBJECT, [\n" + " \"fn\", \"SELECT\",\n" + " \"radius\", 10,\n" + " \"ima\", llList2Json(JSON_ARRAY, [\"Ungulate\"]),\n" + " \"ownerOnly\", TRUE, // only the owner\n" + " \"toucher\", llGetOwner(),\n" + " \"requestor\", llList2Json(JSON_OBJECT, [\n" + " \"id\", llGetKey(),\n" + " \"ima\", \"[]\",\n" + " \"playsWith\", \"[]\"\n" + " ]),\n" + " \"responders\", \"[]\"\n" + " ]); // sent\n" + " llSay(0, activeRequest + llList2CSV(gColours + gTwo + [n, 1]));\n" + " list m;\n" + " llSay(0, (string)llGetListLength(m = [1,\n" + " 2]));\n" + " while (llGetListLength(m = [3,\n" + " 4]) < 0) {}\n" + " if (~llListFindList([\"a\", // first\n" + " \"b\"], [activeRequest])) llSay(0, \"found\");\n" + " llParticleSystem([\n" + " PSYS_PART_FLAGS, 0, // none\n" + " PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_DROP\n" + " ]);\n" + "} }\n"); + ensure("a global's, its comments where they were: " + r.text, + has(r, "local gColours = {\n -- warm\n \"red\", \"orange\",\n --[[ cool ]] \"blue\" -- last\n}\n")); + ensure("closed on the line it closed on, a comment after it trailing that: " + r.text, + has(r, "local gTwo = {1, 2,\n 3} -- three\n")); + ensure("Tapple's, as the LSL had it: " + r.text, + has(r, " local activeRequest = ll.List2Json(JSON_OBJECT, {\n" + " \"fn\", \"SELECT\",\n" + " \"radius\", 10,\n" + " \"ima\", ll.List2Json(JSON_ARRAY, {\"Ungulate\"}),\n" + " \"ownerOnly\", 1, -- only the owner\n" + " \"toucher\", ll.GetOwner(),\n" + " \"requestor\", ll.List2Json(JSON_OBJECT, {\n" + " \"id\", ll.GetKey(),\n" + " \"ima\", \"[]\",\n" + " \"playsWith\", \"[]\"\n" + " }),\n" + " \"responders\", \"[]\"\n" + " }) -- sent\n")); + ensure("each comment once: " + r.text, count(r, "only the owner") == 1 && count(r, "-- sent") == 1 && count(r, "-- first") == 1); + ensure("in a line made inside an expression, one line: " + r.text, has(r, "(function() m = {3, 4} return m end)()")); + ensure("in a condition: " + r.text, has(r, " if table.find({\"a\", -- first\n \"b\"}")); + ensure("one written another way, its comment over it: " + r.text, has(r, " -- none\n ll.ParticleSystem({\n")); + checksClean(r); + } + + template<> template<> + void allsltoslua_object::test<83>() + { + set_test_name("a detected event's count made of the detected table only where the body names it: none that nothing reads, which " + "a lint would ask to take out"); + const ALLSLToSLua::Result r = convert("selectHorse() {}\n" + "default {\n" + " touch_start (integer num) {\n" + " if (llDetectedKey(0) == llGetOwner()) {\n" + " selectHorse();\n" + " }\n" + " }\n" + " collision_start(integer n) { llOwnerSay((string)n); }\n" + " sensor(integer found) { found = 0; }\n" + "}\n"); + ensure("none that nothing reads: " + r.text, + has(r, "LLEvents:on(\"touch_start\", function(detected)\n -- LSL: detected[n]") && !has(r, "local num")); + ensure("one read: " + r.text, has(r, "function(detected)\n local n = #detected\n print(tostring(n))\n")); + ensure("one set, which needs it there: " + r.text, has(r, " local found = #detected\n found = 0\n")); + // Unused, of what Luau says: only what the LSL itself never read. + std::string unused; + for (const ALScriptProblem& p : service.check(r.text)) + { + unused += p.code == "LocalUnused" ? p.message + "\n" : std::string(); + } + ensure_equals("unused", unused, std::string("Variable 'found' is never used; prefix with '_' to silence\n")); + checksClean(r); + } +} diff --git a/indra/alscript/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index eabdf46430..f79d411b57 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -278,4 +278,108 @@ namespace tut options.lua = true; ensure_equals("Luau, by lines", ALScriptFormatter::formatLines("do\n-- note\nend\n", options, 0, 3), std::string("do\n -- note\nend\n")); } + + template<> template<> + void alscriptformatter_object::test<10>() + { + set_test_name("a line past the width broken at the commas of its widest bracket, or of the table a call ends with on the call's " + "line, each part a level further in and broken again where still too long, its comment after the last; formatted " + "again, the same; not where no width is set, nor by formatLines; formatEach gives it as one line's"); + ALScriptFormatter::Options options; + options.lua = true; + options.width = 100; + const std::string in = "local function f()\n" + " local r: string = llcompat.List2Json(JSON_OBJECT, {\"fn\", \"SELECT\", \"radius\", 10, \"ima\", " + "llcompat.List2Json(JSON_ARRAY, {\"Ungulate\"}), \"requestor\", llcompat.List2Json(JSON_OBJECT, {\"id\", " + "ll.GetKey(), \"ima\", \"[]\", \"playsWith\", \"[]\"}), \"responders\", \"[]\"}) -- sent\n" + " print(string.format(\"%s and %s and %s\", tostring(aVeryLongName), tostring(anotherVeryLongName), " + "tostring(yetAnotherName)))\n" + " print(\"a string with no comma in it that is longer than the width of one hundred columns, and so on\")\n" + "end\n"; + const std::string want = "local function f()\n" + " local r: string = llcompat.List2Json(JSON_OBJECT, {\n" + " \"fn\",\n" + " \"SELECT\",\n" + " \"radius\",\n" + " 10,\n" + " \"ima\",\n" + " llcompat.List2Json(JSON_ARRAY, { \"Ungulate\" }),\n" + " \"requestor\",\n" + " llcompat.List2Json(JSON_OBJECT, { \"id\", ll.GetKey(), \"ima\", \"[]\", \"playsWith\", \"[]\" }),\n" + " \"responders\",\n" + " \"[]\"\n" + " }) -- sent\n" + " print(string.format(\n" + " \"%s and %s and %s\",\n" + " tostring(aVeryLongName),\n" + " tostring(anotherVeryLongName),\n" + " tostring(yetAnotherName)\n" + " ))\n" + " print(\"a string with no comma in it that is longer than the width of one hundred columns, and so on\")\n" + "end\n"; + const std::string out = ALScriptFormatter::format(in, options); + ensure_equals("broken", out, want); + ensure_equals("the same again", ALScriptFormatter::format(out, options), want); + + ALScriptFormatter::Options none = options; + none.width = 0; + ensure("no width, no break", ALScriptFormatter::format(in, none).find("{\n") == std::string::npos); + const std::string lines = ALScriptFormatter::formatLines(in, options, 0, 5); + ensure_equals("formatLines keeps every line's number", std::count(lines.begin(), lines.end(), '\n'), std::count(in.begin(), in.end(), '\n')); + + const std::vector each = ALScriptFormatter::formatEach(in, options, 0, 5); + ensure_equals("one a line, and the empty one after the last break", each.size(), size_t(6)); + ensure_equals("the broken line's, breaks and all", each[1] + "\n", want.substr(want.find(" local r"), want.find(" print(string") - want.find(" local r"))); + ensure_equals("a line asked for", ALScriptFormatter::formatEach(in, options, 2, 2)[1], in.substr(in.find(" local r"), in.find(" -- sent") + 8 - in.find(" local r"))); + // From below the first line: every line still one of its own, the + // blank ones that the whole text's runs would take out too. + const std::vector blanks = ALScriptFormatter::formatEach("\n\n\n\n\nprint(1)\n", options, -1, 5); + ensure_equals("a line each, blank or not", blanks.size(), size_t(7)); + ensure_equals("the code where it was", blanks[5], std::string("print(1)")); + } + + template<> template<> + void alscriptformatter_object::test<11>() + { + set_test_name("LSL past the width: a vector or rotation never broken, nor spaced inside where a line begins with one; a call's list " + "on the call's line; tabs a level wide; not a line with a comment inside it"); + ALScriptFormatter::Options options; + options.width = 100; + const std::string in = "default\n{\n state_entry()\n {\n" + " llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_POSITION, <1.0, 2.0, 3.0>, PRIM_ROTATION, " + "<0.0, 0.0, 0.0, 1.0>, PRIM_SIZE, <0.5, 0.5, 0.5>, PRIM_COLOR, ALL_SIDES, <1, 1, 1>, 1.0]);\n" + " llSetText(\"a\", <1, 1, 1>, 1.0); /* inside */ llSetText(\"and a longer piece of text than fits\", " + "<1, 1, 1>, 1.0);\n" + " }\n}\n"; + const std::string want = "default\n{\n state_entry()\n {\n" + " llSetLinkPrimitiveParamsFast(LINK_THIS, [\n" + " PRIM_POSITION,\n" + " <1.0, 2.0, 3.0>,\n" + " PRIM_ROTATION,\n" + " <0.0, 0.0, 0.0, 1.0>,\n" + " PRIM_SIZE,\n" + " <0.5, 0.5, 0.5>,\n" + " PRIM_COLOR,\n" + " ALL_SIDES,\n" + " <1, 1, 1>,\n" + " 1.0\n" + " ]);\n" + " llSetText(\"a\", <1, 1, 1>, 1.0); /* inside */ llSetText(\"and a longer piece of text than fits\", " + "<1, 1, 1>, 1.0);\n" + " }\n}\n"; + const std::string out = ALScriptFormatter::format(in, options); + ensure_equals("broken", out, want); + ensure_equals("the same again", ALScriptFormatter::format(out, options), want); + + // With tabs, each a level's width: 2 levels of 4 and the call's 92 + // are 100, which fits; at 99 it does not. + ALScriptFormatter::Options tabs = options; + tabs.tabs = true; + const std::string call = "llSay(0, \"" + std::string(92 - 13, 'x') + "\");"; + ensure_equals("a call of 92", call.size(), size_t(92)); + const std::string tabbed = "default\n{\n state_entry()\n {\n " + call + "\n }\n}\n"; + ensure("fits", ALScriptFormatter::format(tabbed, tabs).find("\t\t" + call + "\n") != std::string::npos); + tabs.width = 99; + ensure("does not", ALScriptFormatter::format(tabbed, tabs).find("\t\tllSay(\n\t\t\t0,\n") != std::string::npos); + } } diff --git a/indra/alscript/tests/alscriptlintpass_test.cpp b/indra/alscript/tests/alscriptlintpass_test.cpp index cf9840b4ad..8dc9a1060f 100644 --- a/indra/alscript/tests/alscriptlintpass_test.cpp +++ b/indra/alscript/tests/alscriptlintpass_test.cpp @@ -232,7 +232,8 @@ namespace tut void object::test<3>() { set_test_name("SlCompatCall: llcompat's where ll's means the same -- the same function, indexes that are numbers, a boolean against 1 " - "or 0, a find against -1 or 0, an answer unread; not where ll deprecates it, nor an answer read otherwise"); + "or 0, a find against -1 or 0, an answer unread, one ll's only deprecates; not where ll deprecates one that differs, nor an " + "answer read otherwise"); ensure("definitions: " + error, loaded); const std::string said = found("local s = \"abc\"\n" "llcompat.Say(0, s)\n" @@ -245,7 +246,8 @@ namespace tut "llcompat.SameGroup(ll.GetOwner())\n" "local n = llcompat.SubStringIndex(s, \"b\")\n" "local g = llcompat.SameGroup(ll.GetOwner()) + 1\n" - "print(n, g, llcompat.ListFindList({1}, {1}) == -1)\n", + "print(n, g, llcompat.ListFindList({1}, {1}) == -1)\n" + "print(llcompat.List2Json(JSON_ARRAY, {s}))\n", "SlCompatCall", ALScriptProblem::Severity::Note); ensure_equals("each", said, std::string("1 LuauLintSlCompatCall|llcompat.Say|ll.Say(...)\n" @@ -254,7 +256,8 @@ namespace tut "5 LuauLintSlCompatCall|llcompat.SameGroup|not ll.SameGroup(...)\n" "6 LuauLintSlCompatCall|llcompat.SubStringIndex|ll.SubStringIndex(...) ~= nil\n" "7 LuauLintSlCompatCall|llcompat.SubStringIndex|ll.SubStringIndex(...) == nil\n" - "8 LuauLintSlCompatCall|llcompat.SameGroup|ll.SameGroup(...)\n")); + "8 LuauLintSlCompatCall|llcompat.SameGroup|ll.SameGroup(...)\n" + "12 LuauLintSlCompatCall|llcompat.List2Json|ll.List2Json(...)\n")); ensure_equals("the same", fixed("llcompat.Say(0, \"hi\")\n", "LuauLintSlCompatCall", "Write it ll.Say(...)", true), std::string("ll.Say(0, \"hi\")\n")); ensure_equals("indexes", fixed("print(llcompat.GetSubString(\"abc\", 0, 1))\n", "LuauLintSlCompatCall", "Write it ll.GetSubString(\"abc\", 1, 2)", true), diff --git a/indra/alscript/tests/alscriptmessages_test.cpp b/indra/alscript/tests/alscriptmessages_test.cpp index 8d076183c1..c43ad4e4a9 100644 --- a/indra/alscript/tests/alscriptmessages_test.cpp +++ b/indra/alscript/tests/alscriptmessages_test.cpp @@ -218,4 +218,33 @@ namespace tut const ALScriptMessages::Place lsl = ALScriptMessages::readDiagnostic("(4, 2) : ERROR : Syntax error", true); ensure("LSL's, for SLua", lsl.hasLine && lsl.line == 4 && lsl.hasColumn && lsl.column == 2 && lsl.message == "Syntax error"); } + + template<> template<> + void alscriptmessages_object::test<11>() + { + set_test_name("Tailslide's line, as the grid's LSL compiler for Luau's VM says it: its line and the places its words name " + "count from one, which the viewer counts from zero; its level as it says it"); + const std::string said = "Line 320: WARN: Declaration of `activeRequest' in this scope shadows previous declaration at (345, 1)"; + for (const bool lua : { false, true }) + { + const ALScriptMessages::Place place = ALScriptMessages::readDiagnostic(said, lua); + ensure("a line named", place.hasLine && !place.hasColumn); + ensure_equals("one off the line", place.line, 319); + ensure_equals("its level", place.level, std::string("WARN")); + ensure_equals("the words past the level", + place.message, std::string("Declaration of `activeRequest' in this scope shadows previous declaration at (345, 1)")); + ensure_equals("the place the words name", place.mentions.size(), size_t(1)); + const ALScriptMessages::Mention& named = place.mentions.front(); + ensure_equals("where it stands", place.message.substr(named.at, named.length), std::string("(345, 1)")); + ensure("one off each", named.line == 344 && named.column == 0); + } + const ALScriptMessages::Place error = ALScriptMessages::readDiagnostic("Line 1: ERROR: Duplicate declaration of `x'; previously " + "declared at (1, 9). And (2, 3).", false); + ensure("an error on the first line", error.hasLine && error.line == 0 && error.level == "ERROR"); + ensure_equals("each place named", error.mentions.size(), size_t(2)); + ensure("in order", error.mentions[0].line == 0 && error.mentions[0].column == 8 && error.mentions[1].line == 1 && + error.mentions[1].column == 2); + // Words with a place in them, in LSL's own form, are LSL's. + ensure("no Tailslide in LSL's", ALScriptMessages::readDiagnostic("(4, 2) : ERROR : at (1, 1)", false).mentions.empty()); + } } diff --git a/indra/alscript/tests/tailslide/README.md b/indra/alscript/tests/tailslide/README.md index 02733f4d3b..be64f95610 100644 --- a/indra/alscript/tests/tailslide/README.md +++ b/indra/alscript/tests/tailslide/README.md @@ -1,9 +1,10 @@ # Tailslide's test scripts -The LSL scripts from Tailslide's `tests/scripts` (v1.5.12), under its MIT -license (LICENSE here), which the LSL to SLua assistant's tests run through -the converter for their breadth of LSL's corners: every one Tailslide takes -must convert and check clean as SLua, and each one it refuses -- listed in -`allsltoslua_test.cpp` -- must stay refused. Tailslide's expected outputs are -not needed and not copied. Copied again, as they are, when the port moves to -a Tailslide with new ones. +The LSL scripts from Tailslide's `tests/scripts` (1.5.15, as SLua v0.732.16 +carries it in its `Tailslide` folder), under its MIT license (LICENSE here), +which the LSL to SLua assistant's tests run through the converter for their +breadth of LSL's corners: every one Tailslide takes must convert and check +clean as SLua, and each one it refuses -- listed in `allsltoslua_test.cpp` -- +must stay refused. Tailslide's expected outputs are not needed and not +copied. Copied again, as they are, when the slua port moves to a Tailslide +with new ones. diff --git a/indra/alscript/tests/tailslide/scripts/constprop.lsl b/indra/alscript/tests/tailslide/scripts/constprop.lsl index d43e6e084b..59de308cbc 100644 --- a/indra/alscript/tests/tailslide/scripts/constprop.lsl +++ b/indra/alscript/tests/tailslide/scripts/constprop.lsl @@ -63,6 +63,18 @@ default { // read past its terminator while sniffing for the prefix if ( (integer)"0X1F" == 31 ) return; // $[E20012] if ( (integer)"0" == 0 ) return; // $[E20012] + // string -> integer behaves like a 32-bit strtoul(): anything past 0xFFFFFFFF + // saturates to -1 rather than wrapping, whether or not there's a sign. + if ( (integer)"4294967295" == -1 ) return; // $[E20012] + if ( (integer)"4294967296" == -1 ) return; // $[E20012] + if ( (integer)"4294967297" == -1 ) return; // $[E20012] + if ( (integer)"0x100000000" == -1 ) return; // $[E20012] + if ( (integer)"-4294967296" == -1 ) return; // $[E20012] + if ( (integer)"99999999999999999999" == -1 ) return; // $[E20012] + // but in-range negatives are negated in unsigned space + if ( (integer)"-4294967295" == 1 ) return; // $[E20012] + if ( (integer)" -5xyz" == -5 ) return; // $[E20012] + if ( (integer)"-" == 0 ) return; // $[E20012] // dividing by -1 negates, and INT_MIN / -1 wraps back to INT_MIN if ( 5 / -1 == -5 ) return; // $[E20012] if ( 0x80000000 / -1 == 0x80000000 ) return; // $[E20012] diff --git a/indra/alscript/tests/tailslide/scripts/global_negation.lsl b/indra/alscript/tests/tailslide/scripts/global_negation.lsl new file mode 100644 index 0000000000..4b5d3c1a6e --- /dev/null +++ b/indra/alscript/tests/tailslide/scripts/global_negation.lsl @@ -0,0 +1,13 @@ +// A negated number in a global initializer is a single constant to LL's compiler, +// not a negation, while in an expression it's a negation of the positive literal. +integer gi = -1; // $[E20009] +integer gbig = -2147483648; // $[E20009] +float gf = -1.0; // $[E20009] +integer gs = -ALL_SIDES; // $[E20009] + +default { + state_entry() { + integer li = -1; // $[E20009] + float lf = -1.0; // $[E20009] + } +} diff --git a/indra/alscript/tests/tailslide/scripts/num_literals.lsl b/indra/alscript/tests/tailslide/scripts/num_literals.lsl index 1f9e312fc3..9fd7b8311b 100644 --- a/indra/alscript/tests/tailslide/scripts/num_literals.lsl +++ b/indra/alscript/tests/tailslide/scripts/num_literals.lsl @@ -4,5 +4,11 @@ default { llOwnerSay((string)(-(-2147483648))); llOwnerSay((string)(-(2147483648))); llOwnerSay((string)(-2147483648)); + // literals past 0xFFFFFFFF saturate to -1 rather than wrapping + llOwnerSay((string)4294967295); + llOwnerSay((string)4294967296); + llOwnerSay((string)0xFFFFFFFF); + llOwnerSay((string)0x100000000); + llOwnerSay((string)99999999999999999999); } } diff --git a/indra/alscript/tests/tailslide/scripts/vector_products.lsl b/indra/alscript/tests/tailslide/scripts/vector_products.lsl new file mode 100644 index 0000000000..b958435317 --- /dev/null +++ b/indra/alscript/tests/tailslide/scripts/vector_products.lsl @@ -0,0 +1,14 @@ +default { + state_entry() { + // dot product pairs each component with the same component of the other vector + float d1 = <1.0, 0.0, 0.0> * <1.0, 0.0, 0.0>; // $[E20009] + float d2 = <1.0, 0.0, 0.0> * <0.0, 0.0, 1.0>; // $[E20009] + float d3 = <1.0, 2.0, 3.0> * <4.0, 5.0, 6.0>; // $[E20009] + vector c1 = <1.0, 0.0, 0.0> % <0.0, 1.0, 0.0>; // $[E20009] + vector c2 = <1.0, 2.0, 3.0> % <4.0, 5.0, 6.0>; // $[E20009] + // A vector crossed with itself must be exactly zero. If the compiler fuses the + // multiply and subtract this comes out as the rounding error of the products instead. + // Basically, check if we de-fused the fused multiply-sub we didn't want to happen :) + vector c3 = <0.1, 0.2, 0.3> % <0.1, 0.2, 0.3>; // $[E20009] + } +} diff --git a/indra/cmake/Dependencies.cmake b/indra/cmake/Dependencies.cmake index 45e630e8db..18a9f6ce52 100644 --- a/indra/cmake/Dependencies.cmake +++ b/indra/cmake/Dependencies.cmake @@ -91,7 +91,7 @@ al_import(ll::re2 PACKAGE re2 CONFIG TARGETS re2::re2) al_import(ll::simdjson PACKAGE simdjson CONFIG TARGETS simdjson::simdjson) al_import(ll::simdutf PACKAGE simdutf TARGETS simdutf::simdutf) al_import(ll::slua PACKAGE unofficial-slua CONFIG TARGETS unofficial::slua::Luau.Analysis unofficial::slua::Luau.Compiler unofficial::slua::Luau.Require unofficial::slua::Luau.VM) -al_import(ll::tailslide PACKAGE unofficial-tailslide CONFIG TARGETS unofficial::tailslide::libtailslide) +al_import(ll::tailslide PACKAGE unofficial-slua CONFIG TARGETS unofficial::slua::libtailslide) al_import(ll::tinyexr PACKAGE tinyexr CONFIG TARGETS unofficial::tinyexr::tinyexr) al_import(ll::vorbis PACKAGE Vorbis CONFIG TARGETS Vorbis::vorbisfile Vorbis::vorbisenc Vorbis::vorbis) al_import(ll::xxhash PACKAGE xxHash CONFIG TARGETS xxHash::xxhash) diff --git a/indra/newview/alfloaterscriptpreferences.cpp b/indra/newview/alfloaterscriptpreferences.cpp index f82e5662da..badf86605a 100644 --- a/indra/newview/alfloaterscriptpreferences.cpp +++ b/indra/newview/alfloaterscriptpreferences.cpp @@ -91,7 +91,7 @@ namespace "ALScriptStudioAutoClose", "ALScriptStudioCaretStyle", "ALScriptStudioCaretBlink", "ALScriptStudioHoverCards", "ALScriptStudioHoverDelay", "ALScriptStudioVimClipboard", - "ALScriptFormatBlankLines", "ALScriptFormatSpacing", "ALScriptFormatOnSave", "ALScriptTrimOnSave", "ALScriptFixOnSave", + "ALScriptFormatBlankLines", "ALScriptFormatSpacing", "ALScriptFormatWidth", "ALScriptFormatOnSave", "ALScriptTrimOnSave", "ALScriptFixOnSave", "ALScriptTemplateLSL", "ALScriptTemplateSLua", "ALScriptPreprocDefines", "ExternalEditor", "ALScriptConvertLLTimers", "ALScriptConvertDetectedTable", "ALScriptConvertSLuaCalls", "ALScriptConvertIdioms", "ALScriptConvertHandlerFields", "ALScriptConvertTypes", "ALScriptConvertComments", "ALScriptConvertKeepComments", diff --git a/indra/newview/alfloaterscriptstudio.cpp b/indra/newview/alfloaterscriptstudio.cpp index 3b50e1ce36..8e51269a9f 100644 --- a/indra/newview/alfloaterscriptstudio.cpp +++ b/indra/newview/alfloaterscriptstudio.cpp @@ -4240,7 +4240,7 @@ std::vector ALFloaterScriptStudio::problemPlaces(const Doc& doc, bool std::vector places; for (const Doc::Shown& row : doc.shown()) { - if (row.file.empty() && row.origin != definitions && row.origin != weight && (!migration || row.migration)) + if (row.file.empty() && row.line >= 0 && row.origin != definitions && row.origin != weight && (!migration || row.migration)) { places.push_back(doc.editor->document().clamp(ALTextPos(row.line, row.hasColumn ? row.column : 0))); } @@ -4518,7 +4518,7 @@ void ALFloaterScriptStudio::problemChosen(const ALScriptProblemsPane::Place& pla } else { - const bool at = place.file.empty(); + const bool at = place.file.empty() && place.line >= 0; goToPlace(place.ref, place.name, at ? place.line : 0, at && place.hasColumn ? place.column : 0, 0); } --mHoldPanes; @@ -4552,6 +4552,14 @@ void ALFloaterScriptStudio::problemChosen(const ALScriptProblemsPane::Place& pla // or a notecard in the world, or a file on disk. openIncludeAt(place.file, place.fileName, line, has_column ? column : -1, 0); } + else if (line < 0) + { + // About no place in it: its tab, the caret where it was. + if (index != mActive) + { + activate(index); + } + } else { if (index != mActive) @@ -7617,26 +7625,13 @@ void ALFloaterScriptStudio::format(Doc& doc, bool selection_only) options.tabs = !doc.editor->getSoftTabs(); options.maxBlankLines = llclamp(gSavedSettings.getS32("ALScriptFormatBlankLines"), 0, 10); options.spacing = gSavedSettings.getBOOL("ALScriptFormatSpacing"); - const std::string text = document.text(); - const std::string formatted = ALScriptFormatter::formatLines(text, options, first, last); - // Line for line, since only some lines were asked for and the - // formatter keeps every line's number that way; each line that - // changed is one edit, and the whole one step. - std::vector lines; - { - size_t at = 0; - while (at <= formatted.size()) - { - const size_t nl = formatted.find('\n', at); - if (nl == std::string::npos) - { - lines.push_back(formatted.substr(at)); - break; - } - lines.push_back(formatted.substr(at, nl - at)); - at = nl + 1; - } - } + options.width = llmax(0, gSavedSettings.getS32("ALScriptFormatWidth")); + const std::string text = document.text(); + // Line for line, since only some lines may be asked for and the + // formatter gives each line's as one, a line broken at the width + // and all; each line that changed is one edit, and the whole one + // step. + const std::vector lines = ALScriptFormatter::formatEach(text, options, first, last); if (static_cast(lines.size()) != count) { // Said, rather than the command doing nothing where it was asked. diff --git a/indra/newview/alscriptproblemspane.cpp b/indra/newview/alscriptproblemspane.cpp index dd6378dbdd..09f96259a5 100644 --- a/indra/newview/alscriptproblemspane.cpp +++ b/indra/newview/alscriptproblemspane.cpp @@ -84,6 +84,11 @@ ALScriptProblemsPane::Made ALScriptProblemsPane::make(const Doc& doc, const ALSc return; } made.rows.push_back(std::move(row)); + // At no line: listed, and marked nowhere. + if (line < 0) + { + return; + } made.marks.emplace_back(line, mark); ALCodeEditor::Decoration decoration; const ALTextPos begin = text.clamp(ALTextPos(line, has_column ? column : 0)); @@ -113,7 +118,7 @@ ALScriptProblemsPane::Made ALScriptProblemsPane::make(const Doc& doc, const ALSc auto analysed_error_near = [&](S32 line) { for (const ALScriptProblem& problem : doc.check->analysis) { - if (problem.severity == ALScriptProblem::Severity::Error && problem.file.empty() && std::abs(problem.line - line) <= 1) + if (line >= 0 && problem.severity == ALScriptProblem::Severity::Error && problem.file.empty() && std::abs(problem.line - line) <= 1) { return true; } @@ -201,7 +206,7 @@ ALScriptProblemsPane::Made ALScriptProblemsPane::make(const Doc& doc, const ALSc } for (const Doc::RuntimeProblem& problem : doc.runtime) { - const S32 line = llmax(0, problem.line); + const S32 line = problem.line; const S32 column = llmax(0, problem.column); // Said again and again, it is one problem, with how many times. std::string message = problem.message; @@ -217,6 +222,7 @@ ALScriptProblemsPane::Made ALScriptProblemsPane::make(const Doc& doc, const ALSc if (!doc.check->definitionsError.empty()) { Doc::Shown row; + row.line = -1; row.level = Doc::Level::Note; row.origin = services.words("OriginDefinitions"); row.message = doc.check->definitionsError; @@ -242,6 +248,7 @@ ALScriptProblemsPane::Made ALScriptProblemsPane::make(const Doc& doc, const ALSc args["[LEFT]"] = nearing ? fmt::format("{:f}", (F64)(weight.limit - weight.total) / 1024.0) : std::string(); args["[TARGET]"] = ALScriptWeight::nameOf(weight.target); Doc::Shown row; + row.line = -1; row.level = Doc::Level::Warning; row.origin = services.words("OriginWeight"); const char* said = nearing ? (weight.estimate ? "WeightNearEstimate" : "WeightNear") @@ -934,6 +941,11 @@ std::string ALScriptProblemsPane::whereOf(const Doc* doc, const Doc::Shown& prob ? editor->document().displayColumn(ALTextPos(problem.line, problem.column), editor->getTabWidth()) : problem.column; // The line as the tab's gutter counts it: a notecard's may be from 0. + // Nothing where it names none. + if (problem.line < 0) + { + return std::string(); + } const S32 line = mServices->shownLine(problem.line, doc && doc->itemNotecard() && problem.file.empty()); return problem.hasColumn ? fmt::format("{}:{}", line, column + 1) : std::to_string(line); } @@ -969,8 +981,9 @@ std::string ALScriptProblemsPane::tipOf(const LLScrollListItem* item) const const std::string level = mServices->words(problem->level == Doc::Level::Error ? "LevelError" : problem->level == Doc::Level::Warning ? "LevelWarning" : "LevelNote"); - std::string tip = level + " \xC2\xB7 " + (!problem->file.empty() ? problem->fileName : doc ? doc->name : one ? one->name : std::string()) + ":" + - whereOf(doc, *problem) + "\n" + problem->message; + const std::string where = whereOf(doc, *problem); + std::string tip = level + " \xC2\xB7 " + (!problem->file.empty() ? problem->fileName : doc ? doc->name : one ? one->name : std::string()) + + (where.empty() ? std::string() : ":" + where) + "\n" + problem->message; for (const ALScriptFix& fix : problem->fixes) { if (fix.kind == ALScriptFix::Kind::Fix) @@ -1023,17 +1036,29 @@ void ALScriptProblemsPane::selectFirstError(bool checkers_only) mList->updateSort(); const std::vector rows = mList->getAllData(); const std::string compiler = mServices->words("OriginCompiler"); + // One at a line first, which there is somewhere to go to; one at none, + // listed first, only where no error names a line. + std::optional chosen; for (size_t i = 0; i < rows.size(); ++i) { const Doc::Shown* problem = shownOf(rows[i]->getValue()); if (problem && problem->level == Doc::Level::Error && (!checkers_only || problem->origin != compiler)) { - mList->selectNthItem(static_cast(i)); - mList->scrollToShowSelected(); - choose(true); - return; + if (problem->line < 0) + { + chosen = chosen ? chosen : i; + continue; + } + chosen = i; + break; } } + if (chosen) + { + mList->selectNthItem(static_cast(*chosen)); + mList->scrollToShowSelected(); + choose(true); + } } ALScriptProblemsPane::Doc* ALScriptProblemsPane::chosenDoc() const @@ -1183,7 +1208,9 @@ void ALScriptProblemsPane::act(const std::string& action) const auto as_text = [this](const Doc& whose, const Doc::Shown& one) { const std::string name = !one.fileName.empty() ? one.fileName : whose.name; const S32 line = mServices->shownLine(one.line, whose.itemNotecard() && one.file.empty()); - const std::string where = one.hasColumn ? fmt::format("{}:{}:{}", name, line, one.column + 1) : fmt::format("{}:{}", name, line); + const std::string where = one.line < 0 ? name + : one.hasColumn ? fmt::format("{}:{}:{}", name, line, one.column + 1) + : fmt::format("{}:{}", name, line); const std::string level = one.level == Doc::Level::Error ? "error" : one.level == Doc::Level::Warning ? "warning" : "note"; return where + ": " + level + ": " + one.message + " [" + one.origin + "]"; }; diff --git a/indra/newview/alscriptrecompile.cpp b/indra/newview/alscriptrecompile.cpp index e414215861..6cdd0657b8 100644 --- a/indra/newview/alscriptrecompile.cpp +++ b/indra/newview/alscriptrecompile.cpp @@ -201,13 +201,13 @@ std::vector ALScriptRecompile::rowsOf(const ALScr Doc::Shown row; row.level = Doc::levelOf(said.level); row.origin = origin; - row.message = said.message; - row.line = said.line; + row.message = Doc::mentionsMapped(said, map, result.codeLine); + row.line = said.hasLine ? said.line : -1; row.column = said.column; row.hasColumn = said.hasColumn; // The region counts the envelope's lines above the code; the map // is of the code. - if (map) + if (map && said.hasLine) { const ALSourceMap::Loc loc = map->toSource(said.line - result.codeLine, said.column); if (loc.found()) diff --git a/indra/newview/alscriptstudiochecking.cpp b/indra/newview/alscriptstudiochecking.cpp index 1b19d99b53..d733848b4e 100644 --- a/indra/newview/alscriptstudiochecking.cpp +++ b/indra/newview/alscriptstudiochecking.cpp @@ -1422,7 +1422,7 @@ void ALScriptStudioChecking::slideProblems(Doc& doc, const ALTextDocument::Edit& auto slide = [&edit](auto& list) { list.erase(std::remove_if(list.begin(), list.end(), [&edit](auto& problem) { - if (!problem.file.empty()) + if (!problem.file.empty() || problem.line < 0) { return false; } diff --git a/indra/newview/alscriptstudiodoc.cpp b/indra/newview/alscriptstudiodoc.cpp index c5d9388db9..f59f71d2bf 100644 --- a/indra/newview/alscriptstudiodoc.cpp +++ b/indra/newview/alscriptstudiodoc.cpp @@ -46,6 +46,8 @@ #include "lldate.h" #include "llfocusmgr.h" +#include + #include #include @@ -244,12 +246,12 @@ std::vector ALScriptStudioDoc::compiledOf(const std for (const ALScriptDiagnostic& each : said) { Compiled one; - one.line = each.line; + one.line = each.hasLine ? each.line : -1; one.column = each.column; one.hasColumn = each.hasColumn; one.level = each.level; - one.message = each.message; - if (map) + one.message = mentionsMapped(each, map, under); + if (map && each.hasLine) { const ALSourceMap::Loc loc = map->toSource(each.line - under, each.column); if (loc.found()) @@ -273,6 +275,32 @@ std::vector ALScriptStudioDoc::compiledOf(const std return out; } +// static +std::string ALScriptStudioDoc::mentionsMapped(const ALScriptDiagnostic& said, const ALSourceMap* map, S32 under) +{ + std::string message = said.message; + if (!map) + { + return message; + } + // From the last, so that each before it is still where it was. + for (auto it = said.mentions.rbegin(); it != said.mentions.rend(); ++it) + { + const ALSourceMap::Loc loc = map->toSource(it->line - under, it->column); + if (!loc.found() || it->at + it->length > message.size()) + { + continue; + } + std::string place = fmt::format("({}, {})", loc.line + 1, loc.column + 1); + if (loc.file > 0) + { + place += " in " + map->files()[loc.file].name; + } + message.replace(it->at, it->length, place); + } + return message; +} + std::string ALScriptStudioDoc::headerFor(const std::string& expanded_text, const Header& header) const { if (!header.on) diff --git a/indra/newview/alscriptstudiodoc.h b/indra/newview/alscriptstudiodoc.h index a7e71d1343..800a05ffaa 100644 --- a/indra/newview/alscriptstudiodoc.h +++ b/indra/newview/alscriptstudiodoc.h @@ -407,7 +407,7 @@ struct ALScriptStudioDoc bool preprocessing = false; // What the compiler said of the last save, in the source's places // -- back through the preprocessor's map, where it ran -- or an - // include's, by its path. + // include's, by its path; its line -1 where it named none. struct Compiled { S32 line = 0; @@ -423,6 +423,10 @@ struct ALScriptStudioDoc // to the source and its includes, or at the expansion's line where the // preprocessor made the code; as said where there is no map. static std::vector compiledOf(const std::vector& said, const ALSourceMap* map, S32 under); + // What a compiler said, with each place its words name in the + // source's count, as the map reads it back: `(343, 1)`, or an + // include's `(12, 5) in name`; as said where nothing maps it. + static std::string mentionsMapped(const ALScriptDiagnostic& said, const ALSourceMap* map, S32 under); // What the script said as it ran, since it was last saved: a // run-time error's place, or -1 for none, the include it is in, // and its words. diff --git a/indra/newview/alscripttypes.h b/indra/newview/alscripttypes.h index 3cb67a10e1..da80ebd36f 100644 --- a/indra/newview/alscripttypes.h +++ b/indra/newview/alscripttypes.h @@ -24,6 +24,7 @@ #pragma once +#include "alscriptmessages.h" #include "llinventory.h" #include "llpointer.h" #include "lluuid.h" @@ -157,6 +158,8 @@ struct ALScriptDiagnostic // It names a line at all: a line the compiler said nothing of the // place of does not, nor an include that never came. bool hasLine = true; + // The places its words name, in the compiler's count. + std::vector mentions; }; struct ALScriptCompileResult diff --git a/indra/newview/alscriptworkspace.cpp b/indra/newview/alscriptworkspace.cpp index 95e881279b..2de3afaae3 100644 --- a/indra/newview/alscriptworkspace.cpp +++ b/indra/newview/alscriptworkspace.cpp @@ -659,6 +659,7 @@ std::vector ALScriptWorkspace::parseDiagnostics(const LLSD& diagnostic.hasLine = place.hasLine; diagnostic.level = place.level; diagnostic.message = place.message; + diagnostic.mentions = place.mentions; out.push_back(std::move(diagnostic)); } return out; diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index c41d3051e2..63cb616c64 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -882,6 +882,17 @@ Value 1 + ALScriptFormatWidth + + Comment + How many columns a line may take before Script Studio's formatter breaks it, at the commas of a bracket on it; 0 never breaks a line. + Persist + 1 + Type + S32 + Value + 120 + ALScriptFixOnSave Comment diff --git a/indra/newview/skins/default/xui/en/floater_script_studio_prefs.xml b/indra/newview/skins/default/xui/en/floater_script_studio_prefs.xml index f327e45340..18651c94e5 100644 --- a/indra/newview/skins/default/xui/en/floater_script_studio_prefs.xml +++ b/indra/newview/skins/default/xui/en/floater_script_studio_prefs.xml @@ -550,6 +550,31 @@ end tool_tip="Formatting puts one space around operators and after commas; off, it only fixes indentation" top_pad="6" width="370" /> + + Break lines longer than: + + SLua deprecates ll.[1], for [2]. SLua deprecates ll.[1]: [2] SLua deprecates ll.[1]. - SLua deprecates ll.XorBase64Strings, for ll.XorBase64, which XORs correctly where this did not, and so answers otherwise: llcompat's is LSL's. - SLua deprecates ll.XorBase64StringsCorrect, for ll.XorBase64, which answers otherwise where the second string holds a NUL, which ended it here: llcompat's is LSL's. + SLua deprecates ll.XorBase64Strings, for ll.XorBase64, which XORs correctly where this did not, and so answers otherwise: ll.XorBase64Strings is LSL's. + SLua deprecates ll.XorBase64StringsCorrect, for ll.XorBase64, which answers otherwise where the second string holds a NUL, which ended it here: ll.XorBase64StringsCorrect is LSL's. llcompat.[1] counts from 0 and says -1 for none, as LSL did; ll.[1] counts from 1 and says nil. llcompat.[1] takes indexes from 0, as LSL did; ll.[1] takes them from 1. llcompat.[1] answers 1 or 0, as LSL did; ll.[1] answers true or false. diff --git a/indra/newview/tests/alscriptproblemspane_test.cpp b/indra/newview/tests/alscriptproblemspane_test.cpp index bb7552b7c0..f41eed5fce 100644 --- a/indra/newview/tests/alscriptproblemspane_test.cpp +++ b/indra/newview/tests/alscriptproblemspane_test.cpp @@ -892,4 +892,47 @@ namespace tut out.act("fix_migration"); ensure("asked", !studio.kinds.empty() && studio.kinds.back() == "door:from LSL"); } + + template <> + template <> + void alscriptproblemspane_object::test<16>() + { + set_test_name("what names no line -- a compiler's line of no form it has, a run-time error of no place -- is listed at none, marked " + "nowhere, and chosen at none, and gone to as the first error only where no error names a line; one that names a line, at it"); + ALScriptProblemsPane& out = make(); + Doc& d = doc("door"); + d.problems.push_back({ -1, 0, false, std::string(), "ERROR", "the compiler's, nowhere" }); + d.problems.push_back({ 3, 0, false, std::string(), "WARN", "the compiler's, at 3" }); + d.runtime.push_back({ -1, -1, std::string(), "Stack-Heap Collision", 1 }); + const ALScriptProblemsPane::Made made = gather(d); + ensure_equals("all three", made.rows.size(), size_t(3)); + ensure("at none, first", made.rows[0].line == -1 && made.rows[1].line == -1 && made.rows[2].line == 3); + ensure("a warning said WARN", made.rows[2].level == Doc::Level::Warning); + ensure("marked at its line alone", made.marks.size() == 1 && made.marks[0].first == 3); + ensure("waved at its line alone", made.decorations.size() == 1 && made.decorations[0].range.begin.line == 3); + out.fill(&d); + list()->updateSort(); + for (const LLScrollListItem* item : list()->getAllData()) + { + const std::string message = item->getColumn(1)->getValue().asString(); + const std::string where = item->getColumn(3)->getValue().asString(); + ensure("where: " + message + " " + where, message == "the compiler's, at 3" ? where == "4" : where.empty()); + } + choose("the compiler's, nowhere"); + out.choose(false); + ensure_equals("chosen at none", studio.chosen.back().line, -1); + choose("the compiler's, at 3"); + out.choose(false); + ensure_equals("chosen at its line", studio.chosen.back().line, 3); + + // The first error gone to: one at none, listed first, only where no + // error names a line. + out.selectFirstError(false); + ensure_equals("one at none, where no error is at a line", studio.chosen.back().line, -1); + d.problems.push_back({ 5, 0, false, std::string(), "ERROR", "the compiler's, at 5" }); + gather(d); + out.fill(&d); + out.selectFirstError(false); + ensure_equals("one at a line, past those at none", studio.chosen.back().line, 5); + } } diff --git a/indra/newview/tests/alscriptrecompile_test.cpp b/indra/newview/tests/alscriptrecompile_test.cpp index d73067dbfa..b2e39c738d 100644 --- a/indra/newview/tests/alscriptrecompile_test.cpp +++ b/indra/newview/tests/alscriptrecompile_test.cpp @@ -277,4 +277,64 @@ namespace tut run.recompile({}, {}, "auto"); ensure("nothing: done at once", window.done.size() == 2 && window.done[1].compiled == 0 && !run.running()); } + + template<> template<> + void alscriptrecompile_object::test<5>() + { + set_test_name("the places a compiler's words name are read back as its line is, the script's own or an include's by name; one " + "that names no line is at none"); + ALPreprocessor::Options options; + options.fileName = "main.lsl"; + options.resolve = [](const ALPreprocessor::Ask& ask, ALPreprocessor::Include& out) { + out.name = ask.name; + out.text = "integer a;\ninteger b;\n"; + return ALPreprocessor::Found::Yes; + }; + const ALPreprocessor::Result expanded = ALPreprocessor::run("#include \"lib.lsl\"\ndefault\n{\nstate_entry() { x; }\n}\n", options); + ensure("expanded", expanded.problems.empty()); + S32 entry = -1, second = -1, line = 0; + for (size_t at = 0, next; at < expanded.text.size(); at = next + 1, ++line) + { + next = expanded.text.find('\n', at); + const std::string piece = expanded.text.substr(at, next == std::string::npos ? std::string::npos : next - at); + entry = piece.find("state_entry") != std::string::npos ? line : entry; + second = piece.find("integer b") != std::string::npos ? line : second; + if (next == std::string::npos) + { + break; + } + } + ensure("found in the expansion", entry >= 0 && second >= 0); + + // As Tailslide says it, one-based, under the envelope's seven lines. + LLSD said; + said.append(llformat("Line %d: WARN: x shadows previous declaration at (%d, 1)", entry + 8, entry + 8)); + said.append(llformat("Line %d: ERROR: b previously declared at (%d, 9). Again at (5000, 1).", entry + 8, second + 8)); + said.append("Math Error"); + Result result; + result.codeLine = 7; + result.sourceMap = std::make_shared(expanded.map); + for (const ALScriptMessages::Place& place : ALScriptMessages::readDiagnostics(said, false)) + { + ALScriptDiagnostic one; + one.line = place.line; + one.column = place.column; + one.hasLine = place.hasLine; + one.level = place.level; + one.message = place.message; + one.mentions = place.mentions; + result.diagnostics.push_back(one); + } + const std::vector rows = Recompile::rowsOf(result, "Compiler", "made"); + ensure_equals("three", rows.size(), size_t(3)); + ensure("the line, the source's", rows[0].line == 3 && rows[0].level == ALScriptStudioDoc::Level::Warning); + ensure_equals("the place named, the source's", rows[0].message, std::string("x shadows previous declaration at (4, 1)")); + ensure_equals("an include's, by its name; one made, as said", rows[1].message, + std::string("b previously declared at (2, 9) in lib.lsl. Again at (5000, 1).")); + ensure("no line named, none", rows[2].line == -1 && rows[2].file.empty() && rows[2].message == "Math Error"); + + // A tab's, the same. + const std::vector tab = ALScriptStudioDoc::compiledOf(result.diagnostics, result.sourceMap.get(), 7); + ensure("a tab's", tab.size() == 3 && tab[0].line == 3 && tab[0].message == rows[0].message && tab[2].line == -1); + } } diff --git a/indra/vcpkg-configuration.json b/indra/vcpkg-configuration.json index a12a6c6871..3c915aec0a 100644 --- a/indra/vcpkg-configuration.json +++ b/indra/vcpkg-configuration.json @@ -2,7 +2,7 @@ "registries": [ { "kind": "git", - "baseline": "a495fafffb6ec53f5220d0eca0801643eb8eb77e", + "baseline": "33d9cfd5018d515bb933a5933e5175de82394313", "reference": "main", "repository": "https://github.com/AlchemyViewer/alchemy-registry", "packages": [ @@ -34,7 +34,6 @@ "sentry-cocoa", "sentry-native", "slua", - "tailslide", "tinygltf", "tracy", "v-hacd", diff --git a/indra/vcpkg.json b/indra/vcpkg.json index fa178389d0..2b48f5dee4 100644 --- a/indra/vcpkg.json +++ b/indra/vcpkg.json @@ -146,7 +146,6 @@ "simdjson", "simdutf", "slua", - "tailslide", "tinyexr", "tinygltf", "v-hacd", diff --git a/scripts/content_tools/check_script_strings.py b/scripts/content_tools/check_script_strings.py index 882c6365cd..54ed127394 100644 --- a/scripts/content_tools/check_script_strings.py +++ b/scripts/content_tools/check_script_strings.py @@ -41,8 +41,9 @@ loops that register several; one built in a way this cannot read is reported, not passed over. -Tailslide and Luau are found under vcpkg/buildtrees, the newest checkout -of each, unless named: +Luau is found under vcpkg/buildtrees, the newest checkout of slua, and +Tailslide in its Tailslide folder, where the fork has carried it since +0.732.16, unless named: python scripts/content_tools/check_script_strings.py python scripts/content_tools/check_script_strings.py --tailslide DIR --luau DIR @@ -566,8 +567,13 @@ def read_code_pairs(): def read_tailslide(folder): """Tailslide's message tables, by code: the errors from E_ERROR, the - warnings from W_WARNING, each table's rows in the enum's order.""" - src = open(os.path.join(folder, "libtailslide", "logger.cc"), encoding="utf-8").read() + warnings from W_WARNING, each table's rows in the enum's order. The + library's folder is tailslide/ in the fork's tree and libtailslide/ in + Tailslide's own.""" + logger = os.path.join(folder, "tailslide", "logger.cc") + if not os.path.exists(logger): + logger = os.path.join(folder, "libtailslide", "logger.cc") + src = open(logger, encoding="utf-8").read() out = {} for name, base in (("_sErrorMessages", 10000), ("_sWarningMessages", 20000)): m = re.search(r"%s\[[^\]]*\]\s*=\s*\{(.*?)\};" % name, src, re.S) @@ -622,14 +628,16 @@ def buildable(piece, literals): def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) - ap.add_argument("--tailslide", help="a Tailslide checkout (with libtailslide/logger.cc)") + ap.add_argument("--tailslide", help="a Tailslide checkout (with tailslide/logger.cc or libtailslide/logger.cc)") ap.add_argument("--luau", help="a Luau checkout (with Analysis/src/Linter.cpp)") ap.add_argument("--list-luau", action="store_true", help="list the lint templates the map does not have") args = ap.parse_args() - tailslide = args.tailslide or newest(os.path.join(ROOT, "vcpkg", "buildtrees", "tailslide", "src", "*")) - luau = args.luau or newest(os.path.join(ROOT, "vcpkg", "buildtrees", "slua", "src", "*")) \ - or newest(os.path.join(ROOT, "vcpkg", "buildtrees", "luau", "src", "*")) + slua = newest(os.path.join(ROOT, "vcpkg", "buildtrees", "slua", "src", "*")) + tailslide = args.tailslide + if not tailslide and slua and os.path.isdir(os.path.join(slua, "Tailslide")): + tailslide = os.path.join(slua, "Tailslide") + luau = args.luau or slua or newest(os.path.join(ROOT, "vcpkg", "buildtrees", "luau", "src", "*")) strings = read_strings() lsl_rows, lint_rows, shape_rows, err_rows = read_map()