From 1c184dd62cdc33d80b81ec3c09b79bb3c3736428 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 05:57:28 -0400 Subject: [PATCH 1/7] A call SLua only deprecates is written ll's, the one function llcompat's is Where SLua's ll deprecates a function and nothing else sets it apart -- no indexes from 0, no 1 or 0 for a boolean, still in ll -- llcompat's is the same function, and deprecated too, as every llcompat member is. The converter wrote llcompat's anyway, which said there was a difference where there was none, and its deprecation named nothing to use. It writes ll's now: ll.List2Json, whose deprecation names lljson.encode, in either style. The note saying so is the same. llcompat stays where it answers otherwise than ll's. SlCompatCall offers llcompat.List2Json as ll.List2Json for the same reason; it still offers nothing where ll's deprecates one that differs. allsltoslua 7, 66, 76; their check against deprecated ll calls lets through those ll's only deprecates. alscriptlintpass 3. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lint/alscriptlintpass.cpp | 10 ++- indra/alscript/lsl/allsltoslua.cpp | 89 +++++++++++-------- indra/alscript/tests/allsltoslua_test.cpp | 33 ++++--- .../alscript/tests/alscriptlintpass_test.cpp | 9 +- .../newview/skins/default/xui/en/strings.xml | 4 +- 5 files changed, 85 insertions(+), 60 deletions(-) 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/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index be7fb69c7c..800c0ba8c7 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -3220,24 +3220,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 +3310,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) { diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 39ffb51e17..05992b4d81 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); } @@ -2004,7 +2012,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 +2024,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 +2171,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 +2184,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")); 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/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml index c45766b0c8..e53d95b023 100644 --- a/indra/newview/skins/default/xui/en/strings.xml +++ b/indra/newview/skins/default/xui/en/strings.xml @@ -1160,8 +1160,8 @@ If you feel this is an error, please contact support@secondlife.com 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. From 2a09b68fedb2d6d23eb00893e2911cb1cd96a5f4 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 06:15:11 -0400 Subject: [PATCH 2/7] The grid's LSL compiler is read at its lines, and what names no line is at none The region compiles LSL for Luau's VM with Tailslide, which says `Line 320: WARN: words`, a line counted from 1. That form was neither Luau's nor LSL's Mono compiler's, so each such line was read as an error with no place. It is read in either language now, its level as it says it. The places its words name, `previously declared at (345, 1)`, are of the text that went up. They are written again as the source counts them, as the line is: past the envelope's lines, back through the preprocessor's map, and an include's as `(2, 9) in lib.lsl`. A problem that names no line kept line 0 all the same. It was listed at line 1, a double-click took the caret there, and the editor marked and squiggled the first word. That was true of a compiler line of no known form, a run-time error of no place, and the definitions and weight notes. Now such a problem is at line -1. The pane lists it with no line, marks nothing, F8 steps past it, an edit does not slide it, and choosing it opens its tab without moving the caret. alscriptmessages 11, alscriptrecompile 5, alscriptproblemspane 16. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/core/alscriptmessages.cpp | 22 +++++++ indra/alscript/core/alscriptmessages.h | 24 ++++++-- .../alscript/tests/alscriptmessages_test.cpp | 29 +++++++++ indra/newview/alfloaterscriptstudio.cpp | 12 +++- indra/newview/alscriptproblemspane.cpp | 25 ++++++-- indra/newview/alscriptrecompile.cpp | 6 +- indra/newview/alscriptstudiochecking.cpp | 2 +- indra/newview/alscriptstudiodoc.cpp | 34 ++++++++++- indra/newview/alscriptstudiodoc.h | 6 +- indra/newview/alscripttypes.h | 3 + indra/newview/alscriptworkspace.cpp | 1 + .../tests/alscriptproblemspane_test.cpp | 33 ++++++++++ .../newview/tests/alscriptrecompile_test.cpp | 60 +++++++++++++++++++ 13 files changed, 238 insertions(+), 19 deletions(-) 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/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/newview/alfloaterscriptstudio.cpp b/indra/newview/alfloaterscriptstudio.cpp index 3b50e1ce36..75a0ea205f 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) diff --git a/indra/newview/alscriptproblemspane.cpp b/indra/newview/alscriptproblemspane.cpp index dd6378dbdd..9a9cedfdaa 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) @@ -1183,7 +1196,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/tests/alscriptproblemspane_test.cpp b/indra/newview/tests/alscriptproblemspane_test.cpp index bb7552b7c0..0b019c304f 100644 --- a/indra/newview/tests/alscriptproblemspane_test.cpp +++ b/indra/newview/tests/alscriptproblemspane_test.cpp @@ -892,4 +892,37 @@ 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; 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); + } } 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); + } } From 28ac9b2804bd877d1f8a7a388a8f13bb2d789ab9 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 06:15:31 -0400 Subject: [PATCH 3/7] A list the LSL wrote over several lines is written over the same lines The converter wrote every list literal on one line, so a llList2Json of pairs, one to a line, with lists nested in it, came out as one line of three hundred characters. A list the LSL wrote over several lines is now written over the same ones. What was on a line together stays together, `{` is where `[` was, and `}` gets its own line where `]` had its own. A nested list goes a level further in, and a rule table in one does too. Each comment in such a list stays where it was: after its item on the line, or on a line of its own. A statement's comments inside it are put off to the first line written of it, where the list's marks place them; any the line has no place for go over it, as before, so none is lost or doubled. A comment after the statement trails its last line, not its first. A list in a line made inside an expression stays on one line. allsltoslua 82, and 37, whose list comments are now on their lines. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 267 +++++++++++++++++++++- indra/alscript/tests/allsltoslua_test.cpp | 73 +++++- 2 files changed, 331 insertions(+), 9 deletions(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 800c0ba8c7..3f765d922c 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) + ")" }; @@ -3842,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()) { @@ -3856,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) diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 05992b4d81..e45d8175a7 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -1271,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" @@ -1318,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")); @@ -2768,3 +2769,71 @@ 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); + } +} From ac330d2d75cee3e70d59ce2c72bb88f2589a12a6 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 06:19:14 -0400 Subject: [PATCH 4/7] The formatter breaks a line past a width at the commas of a bracket on it Formatting changed indentation and spacing only, so format on save left a line of three hundred characters as it was. A line wider than ALScriptFormatWidth (120 by default, as StyLua's; 0 for never, under Preferences > Editor > Formatting) is now broken at the commas of a bracket on it, each part on a line of its own a level further in, and the closing bracket back at the line's level. A part still too long is broken again. The bracket is the widest with commas, looked for inside the widest without. Where its last part is a table or list with commas of its own, that is broken instead and kept on the call's line -- `f(a, {` and `})` -- as Prettier hugs a last argument. Never broken: an LSL vector or rotation, a line with a comment inside it, a line a string or comment runs on past, a bracket left open on the line. A comment at a line's end goes after its last part. A tab is a level wide. Formatted again, the parts stay as they are, since the indentation rules put them where the break did. Format Document and Format Selection go through formatEach, one entry for each line of the text with a broken line's breaks in its entry, so the edits are still made line for line. formatLines keeps every line's number, as before. An LSL line that began with a vector was spaced `< 1, 2, 3 >`, in a list written over lines by hand as well; it is spaced as a vector now. alscriptformatter 10, 11. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/core/alscriptformatter.cpp | 343 +++++++++++++++++- indra/alscript/core/alscriptformatter.h | 18 +- .../alscript/tests/alscriptformatter_test.cpp | 99 +++++ indra/newview/alfloaterscriptpreferences.cpp | 2 +- indra/newview/alfloaterscriptstudio.cpp | 27 +- .../newview/app_settings/settings_alchemy.xml | 11 + .../xui/en/floater_script_studio_prefs.xml | 25 ++ 7 files changed, 495 insertions(+), 30 deletions(-) diff --git a/indra/alscript/core/alscriptformatter.cpp b/indra/alscript/core/alscriptformatter.cpp index d72ec4311c..8ffc987239 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,44 @@ 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); + const std::vector> each = writtenLines(lines, options, first, 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..1469efb7bd 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,11 @@ 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. + 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/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index eabdf46430..b9a8492f3c 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -278,4 +278,103 @@ 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"))); + } + + 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/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 75a0ea205f..8e51269a9f 100644 --- a/indra/newview/alfloaterscriptstudio.cpp +++ b/indra/newview/alfloaterscriptstudio.cpp @@ -7625,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/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: + + Date: Sat, 10 Oct 2026 06:54:00 -0400 Subject: [PATCH 5/7] The first error gone to is one at a line, and formatEach keeps every line from below the first Errors at no line sort first, so after a failed save selectFirstError chose one of them and went nowhere, though an error at a line was listed below it. It now goes to the first error at a line, and to one at no line only where no error names a line. formatEach given a `first` below nought asked writtenLines for the whole text's runs, which leaves out leading and surplus blank lines and so read an entry that was not there. It now starts from the first line, so every line keeps its entry. alscriptproblemspane 16, alscriptformatter 10. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/core/alscriptformatter.cpp | 4 +++- indra/alscript/core/alscriptformatter.h | 3 ++- .../alscript/tests/alscriptformatter_test.cpp | 5 +++++ indra/newview/alscriptproblemspane.cpp | 20 +++++++++++++++---- .../tests/alscriptproblemspane_test.cpp | 12 ++++++++++- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/indra/alscript/core/alscriptformatter.cpp b/indra/alscript/core/alscriptformatter.cpp index 8ffc987239..a5f4011979 100644 --- a/indra/alscript/core/alscriptformatter.cpp +++ b/indra/alscript/core/alscriptformatter.cpp @@ -1063,7 +1063,9 @@ std::vector ALScriptFormatter::formatEach(std::string_view text, co bool endsWithNewline = false; std::vector lines = linesOf(text, options.lua, endsWithNewline); decide(lines, options.lua); - const std::vector> each = writtenLines(lines, options, first, last, true); + // 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; diff --git a/indra/alscript/core/alscriptformatter.h b/indra/alscript/core/alscriptformatter.h index 1469efb7bd..a7db72496f 100644 --- a/indra/alscript/core/alscriptformatter.h +++ b/indra/alscript/core/alscriptformatter.h @@ -79,7 +79,8 @@ class ALScriptFormatter // 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. + // 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`: diff --git a/indra/alscript/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index b9a8492f3c..f79d411b57 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -331,6 +331,11 @@ namespace tut 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<> diff --git a/indra/newview/alscriptproblemspane.cpp b/indra/newview/alscriptproblemspane.cpp index 9a9cedfdaa..09f96259a5 100644 --- a/indra/newview/alscriptproblemspane.cpp +++ b/indra/newview/alscriptproblemspane.cpp @@ -1036,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 diff --git a/indra/newview/tests/alscriptproblemspane_test.cpp b/indra/newview/tests/alscriptproblemspane_test.cpp index 0b019c304f..f41eed5fce 100644 --- a/indra/newview/tests/alscriptproblemspane_test.cpp +++ b/indra/newview/tests/alscriptproblemspane_test.cpp @@ -898,7 +898,7 @@ namespace tut 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; one that names a line, at it"); + "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" }); @@ -924,5 +924,15 @@ namespace tut 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); } } From 310a8df95a19b2fe0f55c286f9fc41778d17efb4 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 07:20:45 -0400 Subject: [PATCH 6/7] The registry's latest, and Tailslide the one slua carries The alchemy-registry baseline moves to 33d9cfd: slua 0.732.16, cef-bin 154.0.8037.98 and faudio 26.10, and for the builds with them sentry-native 0.17.2, sentry-cocoa 9.30.1 and md4c 0.6.0. slua 0.732.16 has Tailslide in its own tree and installs it in its package as unofficial::slua::libtailslide, which its compiler links publicly; vcpkg will not install the tailslide port beside it. So the port leaves the manifest and the registry's package list, and ll::tailslide is the target slua exports. The headers are still . Its Tailslide is 1.5.15, which carries what the old port's seven patches did, and negates an integer wrapping without the overflow. Its builtins loaders now answer whether every definition was read: a definitions file with lines Tailslide skipped is said in the log, the rest loaded as before. MonoScriptCompiler::_mInGlobalInitializer is gone; nothing here used it. Tailslide's test scripts are copied again from the fork's Tailslide/tests/scripts: global_negation.lsl and vector_products.lsl are new, and constprop.lsl and num_literals.lsl have the 32-bit cases. check_script_strings.py finds Tailslide in the slua checkout, or a standalone one named with --tailslide. All 411 tests pass on macOS; check_script_strings.py agrees. Co-Authored-By: Claude Opus 5.5 --- doc/ARCHITECTURE.md | 2 +- indra/alscript/lsl/allslservice.cpp | 16 +++++++++---- indra/alscript/lsl/allslvalues.cpp | 10 ++++---- indra/alscript/tests/tailslide/README.md | 15 ++++++------ .../tests/tailslide/scripts/constprop.lsl | 12 ++++++++++ .../tailslide/scripts/global_negation.lsl | 13 ++++++++++ .../tests/tailslide/scripts/num_literals.lsl | 6 +++++ .../tailslide/scripts/vector_products.lsl | 14 +++++++++++ indra/cmake/Dependencies.cmake | 2 +- indra/vcpkg-configuration.json | 3 +-- indra/vcpkg.json | 1 - scripts/content_tools/check_script_strings.py | 24 ++++++++++++------- 12 files changed, 88 insertions(+), 30 deletions(-) create mode 100644 indra/alscript/tests/tailslide/scripts/global_negation.lsl create mode 100644 indra/alscript/tests/tailslide/scripts/vector_products.lsl 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/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/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/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/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() From 8c029778c0f7810b9b9169684be2c1cc979e0ade Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 09:19:49 -0400 Subject: [PATCH 7/7] A detected event's count is made of the table only where the handler names it `touch_start(integer num)` was written with `local num: number = #detected` whether the body read num or not, and Luau's lint then said num was never used. The count is now written only where the body names the parameter. An assignment to it counts, since it would otherwise set a global; a count only ever set is still unused, as the LSL had it. allsltoslua 83. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 9 ++++++- indra/alscript/tests/allsltoslua_test.cpp | 29 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 3f765d922c..061ead39e6 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -7151,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/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index e45d8175a7..a711f5cd2c 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -2836,4 +2836,33 @@ namespace tut 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); + } }