From 058b39afa2a408a723a5c07566da97553a236511 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 20:32:25 -0400 Subject: [PATCH 01/12] The timer's handler of a script with one state is a function statement A script with one state, its timer on LLTimers, had its timer handler written `timerHandler = function() ... end`, assigning the local that the timers preamble declares above setTimer. `function timerHandler()` assigns the same local, since a function statement of a local's name makes no global, and is how Lua writes it. With several states the handler is still a field of the state's table. allsltoslua 68, typed too. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 5 +++-- indra/alscript/tests/allsltoslua_test.cpp | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 1a999d7f1c..a7aeace61b 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -7249,14 +7249,15 @@ namespace std::vector lead; const std::string params = handlerParams(handler, lead); // The timer's handler, which LLTimers calls, where the script's - // timer is on LLTimers. + // timer is on LLTimers: the local timersPreamble declared, which + // a function statement of its name assigns. const bool timer = event == "timer" && mTimers; if (event == "timer" && !mTimers) { noteOnce(handler, "SluaTimer", "the timer event, set going by llcompat.SetTimerEvent; LLTimers:every is SLua's own."); } const bool field = mOptions.handlers == ALLSLToSLua::Options::Handlers::Field; - line(timer ? "timerHandler = function()" + line(timer ? "function timerHandler()" : field ? "function LLEvents." + event + "(" + params + ")" : "LLEvents:on(" + luaString(event) + ", function(" + params + ")"); trail(handler->getStatements()); diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 1a57c6da97..bf0a0341cb 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -2081,8 +2081,17 @@ namespace tut ensure("once: " + r.text, has(r, "timerHandle = LLTimers:once(seconds, function()") && !has(r, "LLTimers:every(seconds, function")); ensure("said so: " + r.text, has(r, "-- time it is set, as its handler turns it off before anything else.")); ensure("the handler's own off kept, and set again: " + r.text, - has(r, "timerHandler = function()\n setTimer(0.0)\n") && has(r, "setTimer(2)") && has(r, "setTimer(1)")); + has(r, "function timerHandler()\n setTimer(0.0)\n") && has(r, "setTimer(2)") && has(r, "setTimer(1)")); checksClean(r); + // Tapple's: the local the preamble declared, assigned by a function + // statement of its name, which types as it is declared. + ALLSLToSLua::Options typed; + typed.types = true; + const ALLSLToSLua::Result types = ALLSLToSLua::convert("default { state_entry() { llSetTimerEvent(5); } timer() { llOwnerSay(\"t\"); } }\n", typed); + ensure("a function statement, typed: " + types.text, + has(types, "local timerHandler: (() -> ())? = nil\n") && has(types, "\nfunction timerHandler()\n print(\"t\")\nend\n") && + !has(types, "timerHandler = function")); + checksClean(types); const ALLSLToSLua::Result states = convert("default {\n" " state_entry() { llSetTimerEvent(1); }\n" From 50f4191df2fd5f0272b68437b23c430367645b4c Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 20:33:38 -0400 Subject: [PATCH 02/12] setState stops at a state that is not there, before leaving the one it is in The converter's setState left the state it was in, running its state_exit and taking its handlers off, and then iterated states[name] as it was. A name with no state stopped the script with "attempt to iterate over a nil value", inside setState, with no handlers left on. LSL's compiler refused such a name, so converted code never does it, but an edit afterwards may. The state entered is now looked up first, and a name with none stops the script with "unknown state: ", said at the call that named it, before anything is left. allsltoslua 4, typed too. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 9 ++++++++- indra/alscript/tests/allsltoslua_test.cpp | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index a7aeace61b..55d73db23d 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -7305,9 +7305,17 @@ namespace // a field assigned nil. const std::string off = "LLEvents:off(event :: any, handler)"; const std::string on = field ? "(LLEvents :: any)[event] = handler" : "LLEvents:on(event :: any, handler)"; + // The state entered looked up before the one left is, so that a + // name with none -- which LSL's compiler refused, but an edit + // afterwards may write -- stops the script saying so, at the call + // that named it, and leaves it in the state it was in. mText += " if name == currentState then\n" " return\n" " end\n" + " local entering = states[name]\n" + " if not entering then\n" + " error(\"unknown state: \" .. name, 2)\n" + " end\n" " local leaving = currentState and states[currentState]\n" " if leaving then\n" " if leaving.state_exit then\n" @@ -7320,7 +7328,6 @@ namespace " end\n" " end\n" " currentState = name\n" - " local entering = states[name]\n" " for event, handler in entering do\n" " if " + own + " then\n" " " + on + "\n" diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index bf0a0341cb..3756286d9c 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -324,7 +324,14 @@ namespace tut ensure("calling the state's handler: " + r.text, has(r, "states[currentState].timer") && has(r, "event ~= \"timer\"")); ensure("state_exit a handler of its state: " + r.text, has(r, "state_exit = function()")); ensure("what is let go of noted: " + r.text, noted(r, "SluaStates")); + // Tapple's: a name with no state, which only an edit could write, + // said so at the call, before the state it is in is left. + const size_t looked = r.text.find(" local entering = states[name]\n if not entering then\n error(\"unknown state: \" .. name, 2)\n end\n"); + ensure("a state not there said: " + r.text, looked != std::string::npos && looked < r.text.find("local leaving")); checksClean(r); + ALLSLToSLua::Options typed; + typed.types = true; + checksClean(ALLSLToSLua::convert("default { touch_start(integer n) { state two; } }\nstate two { state_entry() { state default; } }\n", typed)); } template<> template<> From 1b67085dc6845fdba6446dd63b5e35cb27bd2b07 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 20:37:15 -0400 Subject: [PATCH 03/12] A find's place compared with a number is compared in SLua's count SLua's ll.SubStringIndex, and table.find for one item looked for in a list, count from 1 and answer nil for none. The converter makes that LSL's count, `(x or 0) - 1`, wherever the answer is used, and tests only whether anything was found against nil. A place compared with a number written out kept the count's arithmetic: llSubStringIndex(item, P) == 0 came out as (ll.SubStringIndex(item, P) or 0) - 1 == 0. Such a comparison is now made in SLua's count, the number moved on by one: ll.SubStringIndex(item, P) == 1, ~= 3, (x or 0) > 1 for an order, with the place on the left where the number was written first. It is the same test whatever the function answers. An == or ~= with a number below -1 is left as written, and a place read as a number keeps LSL's count. allsltoslua 86. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 58 +++++++++++++++++++++-- indra/alscript/tests/allsltoslua_test.cpp | 51 ++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 55d73db23d..0ab0bfbf7d 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -182,12 +182,15 @@ namespace }; // An expression as written: its text, how tightly it binds, and whether - // it is a Luau boolean where LSL has 1 or 0. + // it is a Luau boolean where LSL has 1 or 0. Where it is a find's + // place counted as LSL's, `(x or 0) - 1`: x, SLua's place, counted + // from 1 or nil. struct Expr { std::string text; int prec = PRIMARY; bool boolean = false; + std::string place; }; // Pairs of words, looked for by views of them without a copy. @@ -2550,6 +2553,47 @@ namespace return false; } + // A find's place counted as LSL's, compared with a whole number + // written out, on either side: compared in SLua's count instead, the + // number moved on by one -- `x == 1` -- or against nil where all it + // asks is whether anything was found. The same test whatever x is. + // Not an == or ~= with a number below -1, which no place is. + std::optional placeCompared(std::string_view word, const Expr& a, const Expr& b, LSLExpression* lhs, LSLExpression* rhs) + { + int v = 0; + const bool left = !a.place.empty() && wholeNumber(rhs, v); + const bool right = !left && !b.place.empty() && wholeNumber(lhs, v); + if (!left && !right) + { + return std::nullopt; + } + // With the place on the left. + std::string_view as = word; + if (right) + { + as = word == "<" ? ">" : word == ">" ? "<" : word == "<=" ? ">=" : word == ">=" ? "<=" : word; + } + const std::string& x = left ? a.place : b.place; + if ((as == "==" && v == -1) || (as == "<" && v == 0) || (as == "<=" && v == -1)) + { + return Expr{ x + " == nil", COMPARE, true }; + } + if ((as == "~=" && v == -1) || (as == ">=" && v == 0) || (as == ">" && v == -1)) + { + return Expr{ x + " ~= nil", COMPARE, true }; + } + const std::string n = std::to_string(static_cast(v) + 1); + if ((as == "==" || as == "~=") && v >= 0) + { + return Expr{ x + " " + std::string(as) + " " + n, COMPARE, true }; + } + if (as == "<" || as == ">" || as == "<=" || as == ">=") + { + return Expr{ "(" + x + " or 0) " + std::string(as) + " " + n, COMPARE, true }; + } + return std::nullopt; + } + // A number written out, an integer or a float, and whether it is // nought; or one with a minus before it. bool numberWritten(LSLExpression* e, bool& nought) @@ -3168,7 +3212,8 @@ namespace if (LSLASTNode* sought = soleItem(argumentAt(e, 1))) { called = "table.find"; - return Expr{ "(table.find(" + anyItems(argumentAt(e, 0), arg(0)) + ", " + itemText(sought) + ") or 0) - 1", ADD }; + const std::string found = "table.find(" + anyItems(argumentAt(e, 0), arg(0)) + ", " + itemText(sought) + ")"; + return Expr{ "(" + found + " or 0) - 1", ADD, false, found }; } } // A part of a list between two places written out, from its start @@ -3483,7 +3528,7 @@ namespace { const std::string args_text = (slua & ALLSLTraits::SluaIndexArgs) ? llArgs(e, indexes) : args(e->getArguments(), params, 0, -1, {}, true); called = "ll." + bare; - return { "(" + called + "(" + args_text + ") or 0) - 1", ADD }; + return { "(" + called + "(" + args_text + ") or 0) - 1", ADD, false, called + "(" + args_text + ")" }; } if (slua & ALLSLTraits::SluaRemoved) { @@ -3580,6 +3625,13 @@ namespace const auto infix = [&](const char* word, int prec, bool boolean = false, bool right_assoc = false) -> Expr { const Expr a = value(lhs); const Expr b = value(rhs); + if (prec == COMPARE && boolean) + { + if (std::optional compared = placeCompared(word, a, b, lhs, rhs)) + { + return *compared; + } + } return { bracketed(a, right_assoc ? prec + 1 : prec) + " " + word + " " + bracketed(b, right_assoc ? prec : prec + 1), prec, boolean }; }; // A find's answer asked only whether it found: against nil, as ll diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 3756286d9c..c505c7d6d8 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -2923,4 +2923,55 @@ namespace tut ensure("llcompat's the same: " + close.text, close.converted && close.text.find(":: any)") != std::string::npos); checksClean(close); } + + template<> template<> + void allsltoslua_object::test<86>() + { + set_test_name("a find's place compared with a whole number written out is compared in SLua's count, from 1 or nil, on either side: " + "x == 1 where LSL said == 0; a place below -1 left as written, and one read as a number kept LSL's"); + const std::string lsl = "string P = \"cfg\";\n" + "list L;\n" + "check(string item) {\n" + " if (P != \"\" && llSubStringIndex(item, P) == 0) llOwnerSay(\"a\");\n" + " if (0 == llSubStringIndex(item, P)) llOwnerSay(\"b\");\n" + " if (llSubStringIndex(item, P) != 2) llOwnerSay(\"c\");\n" + " if (llSubStringIndex(item, P) > 0) llOwnerSay(\"d\");\n" + " if (3 < llSubStringIndex(item, P)) llOwnerSay(\"e\");\n" + " if (llSubStringIndex(item, P) == -2) llOwnerSay(\"f\");\n" + " if (!(llSubStringIndex(item, P) == 0)) llOwnerSay(\"g\");\n" + " if (llListFindList(L, [item]) == 0) llOwnerSay(\"h\");\n" + " integer at = llSubStringIndex(item, P);\n" + " llOwnerSay((string)at);\n" + "}\n" + "default { touch_start(integer n) { check(\"x\"); } }\n"; + const ALLSLToSLua::Result r = convert(lsl); + ensure("Tapple's: " + r.text, has(r, "if P ~= \"\" and ll.SubStringIndex(item, P) == 1 then")); + ensure("the number on the left: " + r.text, has(r, "if ll.SubStringIndex(item, P) == 1 then print(\"b\")")); + ensure("~=: " + r.text, has(r, "if ll.SubStringIndex(item, P) ~= 3 then")); + ensure("an order, nil read as LSL's -1: " + r.text, has(r, "if (ll.SubStringIndex(item, P) or 0) > 1 then")); + ensure("an order the other way round: " + r.text, has(r, "if (ll.SubStringIndex(item, P) or 0) > 4 then")); + ensure("below -1, as written: " + r.text, has(r, "if (ll.SubStringIndex(item, P) or 0) - 1 == -2 then")); + ensure("under a not: " + r.text, has(r, "if ll.SubStringIndex(item, P) ~= 1 then print(\"g\")")); + ensure("table.find's: " + r.text, has(r, "if table.find(L, item) == 1 then")); + ensure("a number, LSL's: " + r.text, has(r, "local at = (ll.SubStringIndex(item, P) or 0) - 1")); + checksClean(r); + + // table.find's alone, without SLua's ll: whether it found, against nil. + ALLSLToSLua::Options idioms = ALLSLToSLua::Options::closeToLSL(); + idioms.idioms = true; + const ALLSLToSLua::Result found = + ALLSLToSLua::convert("list L;\ndefault { touch_start(integer n) {\n" + " if (llListFindList(L, [n]) == -1) llOwnerSay(\"none\");\n" + " if (llListFindList(L, [n]) >= 0) llOwnerSay(\"some\");\n" + " if (llListFindList(L, [n]) == 2) llOwnerSay(\"third\");\n" + "} }\n", + idioms); + ensure("found, against nil: " + found.text, found.converted && has(found, "if table.find(L, n) == nil then") && + has(found, "if table.find(L, n) ~= nil then") && has(found, "if table.find(L, n) == 3 then")); + checksClean(found); + + const ALLSLToSLua::Result close = ALLSLToSLua::convert(lsl, ALLSLToSLua::Options::closeToLSL()); + ensure("llcompat's, LSL's count: " + close.text, close.converted && has(close, "llcompat.SubStringIndex(item, P) == 0")); + checksClean(close); + } } From 6b99cf4ae6982a56ed3b427d355f0f997b2d4d21 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 20:41:11 -0400 Subject: [PATCH 04/12] A blank line the LSL had between two statements is kept The converter kept a blank line written over a comment, but dropped one between two statements with no comment between them. So the author's grouping survived only where a comment marked it: a function's `if (...) return FALSE;`, a blank line, and `return TRUE;` came out with the two lines together. The same went for globals. A statement or global with one or more blank lines between it and the one written before it now has one blank line over it. A global after a function the LSL had between them counts as apart, since the function is written later. None is written at a block's start, nor a second where a comment over the statement keeps its own. A comment kept apart as a block's first line no longer leaves a blank line under the line that opens the block. allsltoslua 87. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 67 ++++++++++++++++++++++- indra/alscript/tests/allsltoslua_test.cpp | 39 +++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 0ab0bfbf7d..09725276f2 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -585,6 +585,9 @@ namespace // Where each of the LSL's lines starts, from the first. const std::vector& lineStarts(); std::vector mLineStarts; + // Whether a line of the LSL lies blank between two others, counted + // from one, neither of them itself. + bool blankBetween(S32 after, S32 before); Expr lvalue(LSLLValueExpression* e); Expr call(LSLFunctionExpression* e); // The call as written, and the name SLua calls it by where it is @@ -668,6 +671,12 @@ namespace void statement(LSLASTNode* s, bool last); void statementBody(LSLASTNode* s, bool last); void block(LSLASTNode* s); + // A blank line over a statement or a global, where the LSL had one + // or more between it and the one written before it, which grouped + // them: one, and not where nothing has been written since `opened` + // -- a block's start -- nor where a comment over it keeps the + // blank line over itself (Comment::apart). + void keepApart(LSLASTNode* before, LSLASTNode* node, size_t opened); // An if written from `at` in the text, `lines` lines down, its // stretches from `spans`, put on one line where it is one statement // and nothing said over it: if c then s end. Not called where a @@ -907,6 +916,9 @@ namespace // What each part of the script may change, for LSL's order. ALLSLEffects mEffects; std::string mText; + // Where the block being written began in mText: no blank line kept + // over what is written first in it. + size_t mBlockStart = std::string::npos; // The LSL's stretches beside the SLua's made of them, the SLua's // counted in mText until the head goes over it, and its last lines // worked out once all is written (endSpans); the LSL lines of the @@ -1694,7 +1706,7 @@ namespace putTrailing(mText.size() - 1, indent() + c.text + "\n"); return; } - if (c.apart && !mText.empty() && !blank_over) + if (c.apart && !mText.empty() && !blank_over && mText.size() != mBlockStart) { mText += "\n"; } @@ -2020,6 +2032,21 @@ namespace return mLineStarts; } + bool Writer::blankBetween(S32 after, S32 before) + { + const std::vector& starts = lineStarts(); + for (S32 line = std::max(after + 1, 1); line < before && static_cast(line) <= starts.size(); ++line) + { + const size_t from = starts[static_cast(line) - 1]; + const size_t to = std::min(mSource.find('\n', from), mSource.size()); + if (mSource.substr(from, to - from).find_first_not_of(" \t\r") == std::string_view::npos) + { + return true; + } + } + return false; + } + std::vector> Writer::numbersNear(LSLConstant* c) { std::vector> found; @@ -4491,19 +4518,49 @@ namespace if (s->getNodeSubType() == NODE_COMPOUND_STATEMENT) { // What was after its brace that no line opening it took. + mBlockStart = mText.size(); commentsOpening(s); + const size_t opened = mText.size(); + LSLASTNode* before = nullptr; for (LSLASTNode* child = s->getChild(0); child; child = child->getNext()) { + keepApart(before, child, opened); statement(child, !child->getNext()); + before = isNull(child) || !child->getLoc() ? before : child; } lineUp(); commentsAtEnd(s); return; } + mBlockStart = mText.size(); statement(s, true); lineUp(); } + void Writer::keepApart(LSLASTNode* before, LSLASTNode* node, size_t opened) + { + if (!before || isNull(node) || !node->getLoc() || mText.size() == opened || + (mText.size() >= 2 && mText.compare(mText.size() - 2, 2, "\n\n") == 0)) + { + return; + } + const Pos begins = { node->getLoc()->first_line, node->getLoc()->first_column }; + if (const auto found = mCommentsBefore.find(node); found != mCommentsBefore.end()) + { + for (size_t k : found->second) + { + if (mComments[k].at < begins) + { + return; + } + } + } + if (blankBetween(before->getLoc()->last_line, begins.line)) + { + mText += "\n"; + } + } + void Writer::statement(LSLASTNode* s, bool last) { if (isNull(s)) @@ -7117,7 +7174,9 @@ namespace void Writer::globals() { - bool any = false; + bool any = false; + const size_t opened = mText.size(); + LSLASTNode* before = nullptr; for (LSLASTNode* g = mScript->getGlobals()->getChild(0); g; g = g->getNext()) { if (g->getNodeType() != NODE_GLOBAL_VARIABLE) @@ -7127,6 +7186,10 @@ namespace auto* global = static_cast(g); LSLIdentifier* id = global->getIdentifier(); LSLExpression* init = global->getInitializer(); + // Apart from the one before it as the LSL had them, or a + // function the LSL had between them, which is written later. + keepApart(before, global, opened); + before = global; commentsBefore(global); const size_t from = mText.size(); if (boolean(id->getSymbol())) diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index c505c7d6d8..1adebd2725 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -2974,4 +2974,43 @@ namespace tut ensure("llcompat's, LSL's count: " + close.text, close.converted && has(close, "llcompat.SubStringIndex(item, P) == 0")); checksClean(close); } + + template<> template<> + void allsltoslua_object::test<87>() + { + set_test_name("a blank line between two statements or globals is kept, one for a run of them; none at a block's start or end, " + "nor a second where a comment over the statement keeps its own"); + const ALLSLToSLua::Result r = convert("integer a = 1;\n" + "integer b = 2;\n" + "\n" + "\n" + "integer c = 3;\n" + "integer preCheck(key avatar) {\n" + "\n" + " // only allow my owner to request contents\n" + " if (avatar != llGetOwner()) return FALSE;\n" + "\n" + " return TRUE;\n" + "}\n" + "default {\n" + " touch_start(integer n) {\n" + " if (preCheck(llDetectedKey(0))) {\n" + " llOwnerSay((string)a);\n" + "\n" + " llOwnerSay((string)b);\n" + "\n" + " }\n" + "\n" + " // said apart\n" + " llOwnerSay((string)c);\n" + " }\n" + "}\n"); + ensure("the globals: " + r.text, has(r, "local a = 1\nlocal b = 2\n\nlocal c = 3\n")); + ensure("Tapple's: " + r.text, has(r, " -- only allow my owner to request contents\n if avatar ~= ll.GetOwner() then return false end\n\n return true\nend")); + ensure("none at a block's start: " + r.text, has(r, "(avatar)\n -- only allow")); + ensure("in a block, none at its end: " + r.text, has(r, " print(tostring(a))\n\n print(tostring(b))\n end\n")); + ensure("one over a comment kept apart: " + r.text, has(r, " end\n\n -- said apart\n print(tostring(c))\n")); + ensure("never two: " + r.text, !has(r, "\n\n\n")); + checksClean(r); + } } From 5744150d65fc7f9671a50b79cad8034d3bf5224e Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 21:06:21 -0400 Subject: [PATCH 05/12] The formatter spaces the inside of SLua's braces as a setting asks The formatter put a space inside Luau's braces, as StyLua does, but only where a name, number or string stood next to them. A brace, a bracket or a sign did not get one, so the converter's own state table type, `{ [string]: { [string]: (...any) -> () } }`, came out as `{[string]: {[string]:(...any) -> () } }`, and `{{1}, {2}}` as `{{ 1 }, { 2 } }`. A bracket after a colon lost its space too, a rule for a method call that no Luau call has. Inside a table's braces, or a table type's, the first part and the closing brace are now spaced alike whatever they are, and a bracket after a colon is spaced as written. Whether there is a space at all is a setting, ALScriptFormatBraceSpaces, on by default: StyLua writes { 1, 2 }, and Luau's own documentation and SLua's tests write {1, 2}. It is under Preferences > Editor > Formatting. {} and an interpolated string's braces are never spaced, and LSL's braces are as they were. alscriptformatter 12. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/core/alscriptformatter.cpp | 27 ++++++++++----- indra/alscript/core/alscriptformatter.h | 6 +++- .../alscript/tests/alscriptformatter_test.cpp | 33 +++++++++++++++++++ indra/newview/alfloaterscriptpreferences.cpp | 2 +- indra/newview/alfloaterscriptstudio.cpp | 1 + .../newview/app_settings/settings_alchemy.xml | 11 +++++++ .../xui/en/floater_script_studio_prefs.xml | 11 +++++++ 7 files changed, 80 insertions(+), 11 deletions(-) diff --git a/indra/alscript/core/alscriptformatter.cpp b/indra/alscript/core/alscriptformatter.cpp index a5f4011979..1e7016d8ad 100644 --- a/indra/alscript/core/alscriptformatter.cpp +++ b/indra/alscript/core/alscriptformatter.cpp @@ -140,10 +140,13 @@ namespace // The line's tokens spaced afresh, after its indentation: one space // around a binary operator, none around a unary one or inside - // brackets, one after a comma and none before; a trailing comment, - // and anything the rules do not name, spaced as written. - std::string spaced(const Line& line, bool lua) + // brackets, one after a comma and none before; inside Luau's braces + // as Options::braceSpaces asks, whatever stands next to them; a + // trailing comment, and anything the rules do not name, spaced as + // written. + std::string spaced(const Line& line, const ALScriptFormatter::Options& options) { + const bool lua = options.lua; std::string out; const Token* prev = nullptr; // The last token before prev that is neither blank nor a comment: @@ -205,7 +208,13 @@ namespace else if (t.kind == Kind::Punct) { const std::string& p = t.text; - if (p == "," || p == ";") + if (lua && a.kind == Kind::Punct && a.text == "{" && p != "}") + { + // A table's first part, or a table type's, whatever + // begins it: { { 1 } }, { [k] = v }, { -1 }. + say(options.braceSpaces); + } + else if (p == "," || p == ";") { say(false); } @@ -219,7 +228,7 @@ namespace { say(isKeyword(lua, a) && !(lua && a.text == "function")); } - else if (a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || a.text == ")" || a.text == "]" || a.text == "." || a.text == ":")) + else if (a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || a.text == ")" || a.text == "]" || a.text == ".")) { say(false); } @@ -249,7 +258,7 @@ namespace } else if (p == "}") { - say(!(a.kind == Kind::Punct && a.text == "{")); + say(!(a.kind == Kind::Punct && a.text == "{") && (!lua || options.braceSpaces)); } else if (p == "." || p == "++" || p == "--") { @@ -317,7 +326,7 @@ namespace } else if (p == "{") { - say(true); + say(!lua || options.braceSpaces); } else if (p == "," || p == ";") { @@ -793,7 +802,7 @@ namespace { Line line; line.tokens = tokens; - return indentText(options, indent) + spaced(line, options.lua); + return indentText(options, indent) + spaced(line, options); } // The tokens as pieces no wider than the width where a bracket's commas @@ -975,7 +984,7 @@ namespace text = indentText(options, line.indent); if (options.spacing) { - text += spaced(line, options.lua); + text += spaced(line, options); } else { diff --git a/indra/alscript/core/alscriptformatter.h b/indra/alscript/core/alscriptformatter.h index a7db72496f..a3a70d1c6b 100644 --- a/indra/alscript/core/alscriptformatter.h +++ b/indra/alscript/core/alscriptformatter.h @@ -39,7 +39,7 @@ // or `else` without braces -- and within a line the spaces are put // where the language reads best: one around a binary operator, none // around a unary one or inside brackets, one after a comma, none -// before it. What the author spaced deliberately is left alone: the +// before it, and inside Luau's braces as asked. What the author spaced deliberately is left alone: the // 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 @@ -59,6 +59,10 @@ class ALScriptFormatter // Whether the spaces within a line are touched at all, or only // the indentation. bool spacing = true; + // Luau: a space inside a table's braces, and a table type's, as + // StyLua writes them -- { 1, 2 }, { any } -- or none, {1, 2}. + // Never inside {}, nor an interpolated string's. + bool braceSpaces = 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 diff --git a/indra/alscript/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index f79d411b57..83158b03ea 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -382,4 +382,37 @@ namespace tut tabs.width = 99; ensure("does not", ALScriptFormatter::format(tabbed, tabs).find("\t\tllSay(\n\t\t\t0,\n") != std::string::npos); } + + template<> template<> + void alscriptformatter_object::test<12>() + { + set_test_name("Luau's braces spaced inside as asked, whatever stands next to them, a table type's too; never {} nor an " + "interpolated string's; a type's colon before a bracket spaced as written; LSL's braces as they were"); + const std::string in = "local states: {[string]: {[string]:(...any) -> ()}} = {}\n" + "local t = {{1}, {2, 3}, [k] = v, -1, #t}\n" + "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})\n" + "print(`a {b} c {{d}}`, `{ {1} }`)\n" + "local f: (number) -> () = obj:method(1)\n"; + const std::string want = "local states: { [string]: { [string]:(...any) -> () } } = {}\n" + "local t = { { 1 }, { 2, 3 }, [k] = v, -1, #t }\n" + "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})\n" + "print(`a {b} c {{d}}`, `{ {1} }`)\n" + "local f: (number) -> () = obj:method(1)\n"; + ensure_equals("StyLua's, by default", lua(in), want); + ALScriptFormatter::Options none; + none.lua = true; + none.braceSpaces = false; + ensure_equals("none", ALScriptFormatter::format(in, none), + std::string("local states: {[string]: {[string]:(...any) -> ()}} = {}\n" + "local t = {{1}, {2, 3}, [k] = v, -1, #t}\n" + "local params: {any} = ll.ParseStringKeepNulls(msg, {\"\\n\"}, {})\n" + "print(`a {b} c {{d}}`, `{ {1} }`)\n" + "local f: (number) -> () = obj:method(1)\n")); + ensure_equals("a type's colon, spaced", lua("local states: {[string]: (...any) -> ()} = {}\n"), + std::string("local states: { [string]: (...any) -> () } = {}\n")); + ALScriptFormatter::Options lsl_none; + lsl_none.braceSpaces = false; + ensure_equals("LSL's braces", ALScriptFormatter::format("default { state_entry() { llSay(0, \"a\"); } }\n", lsl_none), + std::string("default { state_entry() { llSay(0, \"a\"); } }\n")); + } } diff --git a/indra/newview/alfloaterscriptpreferences.cpp b/indra/newview/alfloaterscriptpreferences.cpp index badf86605a..f8adbc6bc3 100644 --- a/indra/newview/alfloaterscriptpreferences.cpp +++ b/indra/newview/alfloaterscriptpreferences.cpp @@ -91,7 +91,7 @@ namespace "ALScriptStudioAutoClose", "ALScriptStudioCaretStyle", "ALScriptStudioCaretBlink", "ALScriptStudioHoverCards", "ALScriptStudioHoverDelay", "ALScriptStudioVimClipboard", - "ALScriptFormatBlankLines", "ALScriptFormatSpacing", "ALScriptFormatWidth", "ALScriptFormatOnSave", "ALScriptTrimOnSave", "ALScriptFixOnSave", + "ALScriptFormatBlankLines", "ALScriptFormatSpacing", "ALScriptFormatBraceSpaces", "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 8e51269a9f..8f9c55aaa1 100644 --- a/indra/newview/alfloaterscriptstudio.cpp +++ b/indra/newview/alfloaterscriptstudio.cpp @@ -7625,6 +7625,7 @@ 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"); + options.braceSpaces = gSavedSettings.getBOOL("ALScriptFormatBraceSpaces"); 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 diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 63cb616c64..df436de314 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -882,6 +882,17 @@ Value 1 + ALScriptFormatBraceSpaces + + Comment + Script Studio writes a space inside SLua's braces, { 1, 2 } and { any }, as StyLua does: its formatter, and Convert to SLua; off, {1, 2}. + Persist + 1 + Type + Boolean + Value + 1 + ALScriptFormatWidth 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 18651c94e5..cd8a3aefc5 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 @@ -575,6 +575,17 @@ end tool_tip="Formatting breaks a line with more columns than this at the commas of a bracket on it, each part on a line of its own; 0 never breaks a line" top_delta="-2" width="60" /> + Date: Sat, 10 Oct 2026 21:08:04 -0400 Subject: [PATCH 06/12] Convert to SLua spaces its tables' braces as the formatter does The converter wrote a list as {"\n"}, with no spaces, but a list's type as { any } and a rule table as { auto_play = true }. Tapple's converted line, `local params: { any } = ll.ParseStringKeepNulls(msg, {"\n"}, {})`, held both, and format on save then changed every list. Tables and table types are now written by ALScriptFormatBraceSpaces, the formatter's setting: { "\n" } and { any } by default, as StyLua and the formatter write them, and {"\n"} and {any} with it off. A list over several lines takes the spaces where an item shares a line with a brace. An interpolated string's braces and {} are as they were. Converted text's braces are then as format on save would leave them. allsltoslua 88, and the expectations of sixteen others. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 49 ++++---- indra/alscript/lsl/allsltoslua.h | 4 + indra/alscript/tests/allsltoslua_test.cpp | 136 +++++++++++++++------- indra/newview/alfloaterscriptstudio.cpp | 2 + 4 files changed, 126 insertions(+), 65 deletions(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 09725276f2..cb12c62f34 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -665,6 +665,9 @@ namespace std::string llArgs(LSLFunctionExpression* e, U16 indexes); // `: type` for an LSL type, where types are written. std::string typed(LSLIType type) const; + // A table's braces round what it holds, or a table type's: spaced + // inside or not as Options::braceSpaces asks; {} round nothing. + std::string braced(const std::string& inside) const; // --- statements ------------------------------------------------------------- @@ -2009,7 +2012,7 @@ namespace { out += (out.empty() ? "" : ", ") + constant(item).text; } - return { "{" + out + "}" }; + return { braced(out) }; } default: return { "nil" }; @@ -2674,6 +2677,11 @@ namespace return static_cast(arg); } + std::string Writer::braced(const std::string& inside) const + { + return inside.empty() ? "{}" : mOptions.braceSpaces ? "{ " + inside + " }" : "{" + inside + "}"; + } + std::string Writer::typed(LSLIType type) const { if (!mOptions.types) @@ -2688,7 +2696,7 @@ namespace case LST_KEY: return ": uuid"; case LST_VECTOR: return ": vector"; case LST_QUATERNION: return ": quaternion"; - case LST_LIST: return ": { any }"; + case LST_LIST: return ": " + braced("any"); default: return std::string(); } } @@ -3166,11 +3174,12 @@ namespace // rule for a loop's range keeps alive, and not every // compiler has that rule. const std::optional> pieces = whitelistPieces(*textWritten(v)); + std::string each; for (std::string_view piece : *pieces) { - text += (text.empty() ? "{ " : ", ") + luaString(piece); + each += (each.empty() ? "" : ", ") + luaString(piece); } - text += " }"; + text = braced(each); break; } case Takes::Headers: @@ -3195,7 +3204,7 @@ namespace } for (const Gathered& g : gathered) { - fields[g.at] += "{ " + g.all + " }"; + fields[g.at] += braced(g.all); } // The serializer sends headers sorted by name: said where that is // not the order they were written in. @@ -3207,12 +3216,12 @@ namespace } // A key to a line where the LSL's list had more than one. const bool lines = list->getLoc()->first_line != list->getLoc()->last_line; - std::string table = lines ? "{\n" : "{ "; + std::string table; for (size_t i = 0; i < fields.size(); ++i) { table += lines ? listIndent(1) + fields[i] + ",\n" : (i ? ", " : "") + fields[i]; } - table += lines ? listIndent(0) + "}" : " }"; + table = lines ? "{\n" + table + listIndent(0) + "}" : braced(table); called = "ll." + lsl.substr(2); LSLParamList* params = e->getIdentifier()->getSymbol()->getFunctionDecl(); return Expr{ called + "(" + args(e->getArguments(), params, 0, call->at, table, true) + ")" }; @@ -3762,13 +3771,13 @@ namespace std::string first = madeHere(lhs) ? anyItems(lhs, a) : "table.clone(" + a.text + ")"; if (lt != LST_LIST) { - first = "{" + a.text + "} :: { any }"; + first = braced(a.text) + " :: " + braced("any"); } else if (left->getNodeSubType() == NODE_BINARY_EXPRESSION && left->getOperation() == OP_PLUS) { first = a.text; } - return { "table.extend(" + first + ", " + (rt == LST_LIST ? b.text : "{" + b.text + "}") + ")" }; + return { "table.extend(" + first + ", " + (rt == LST_LIST ? b.text : braced(b.text)) + ")" }; } if (lt == LST_STRING || lt == LST_KEY || rt == LST_STRING || rt == LST_KEY) { @@ -4008,7 +4017,7 @@ namespace noteOnce(e, "SluaVectorText", "ll.DumpList2String writes a vector's or a rotation's parts as LSL did, with five places; SLua's tostring " "writes as few as each needs, <1, 2, 3>, which tovector reads back the same."); - return { "ll.DumpList2String({" + v.text + "}, \"\")" }; + return { "ll.DumpList2String(" + braced(v.text) + ", \"\")" }; default: return { "tostring(" + v.text + ")" }; } case LST_KEY: @@ -4028,7 +4037,7 @@ namespace // only where an operator around it binds more tightly than or. case LST_VECTOR: return { "tovector(" + v.text + ") or ZERO_VECTOR", OR }; case LST_QUATERNION: return { "toquaternion(" + v.text + ") or ZERO_ROTATION", OR }; - case LST_LIST: return { "{" + v.text + "}" }; + case LST_LIST: return { braced(v.text) }; default: return v; } } @@ -4127,7 +4136,7 @@ namespace auto* each = static_cast(item); out += (out.empty() ? "" : ", ") + coerced(each, each->getIType()).text; } - return { "{" + out + "}" }; + return { braced(out) }; } default: return { "nil" }; @@ -4204,12 +4213,12 @@ namespace { out += ","; } - to(begins(item), listIndent(0), i > 0); + to(begins(item), listIndent(0), i > 0 || mOptions.braceSpaces); // 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); + to(close, listIndent(-1), mOptions.braceSpaces); out += "}"; --mListDepth; return out; @@ -4241,7 +4250,7 @@ namespace if (t == LST_LIST) { // Something else may hold the list: a new one. - return "table.extend(table.clone(" + old + "), " + (rhs && rhs->getIType() == LST_LIST ? v.text : "{" + v.text + "}") + ")"; + return "table.extend(table.clone(" + old + "), " + (rhs && rhs->getIType() == LST_LIST ? v.text : braced(v.text)) + ")"; } return old + " + " + bracketed(v, ADD + 1); case OP_SUB_ASSIGN: @@ -6019,7 +6028,7 @@ namespace std::string Writer::anyItems(LSLExpression* e, const Expr& written) const { - return madeHere(e) ? bracketed(written, PRIMARY) + " :: { any }" : written.text; + return madeHere(e) ? bracketed(written, PRIMARY) + " :: " + braced("any") : written.text; } LSLASTNode* Writer::soleItem(LSLExpression* e) const @@ -6415,7 +6424,7 @@ namespace } else if (values.size() > 32) { - line("table.extend(" + name + ", {" + all + "})"); + line("table.extend(" + name + ", " + braced(all) + ")"); } else if (!values.empty()) { @@ -6657,7 +6666,7 @@ namespace { const std::string name = mNames.contains(var) ? mNames[var] : nameOf(var->getName()); const std::string parts = freshName(name + "Parts"); - line("local " + parts + (mOptions.types ? ": { string }" : "") + " = {}"); + line("local " + parts + (mOptions.types ? ": " + braced("string") : "") + " = {}"); mBuilding[var] = parts; joins.push_back({ name, parts, mDeclaredAtJoin.contains({ loop, var }) }); } @@ -7304,7 +7313,7 @@ namespace { lead.push_back("local " + nameOf(static_cast(first)) + typed(LST_INTEGER) + " = #detected"); } - return mOptions.types ? "detected: { DetectedEvent }" : "detected"; + return mOptions.types ? "detected: " + braced("DetectedEvent") : "detected"; } int at = 0; for (LSLASTNode* p = first; p; p = p->getNext(), ++at) @@ -7407,7 +7416,7 @@ namespace note(nullptr, "SluaStates", "LSL let go of a state's listens, sensor repeats and targets as it left the state; setState does not. Remove them " "yourself where the script relied on it."); - line("local states: { [string]: { [string]: (...any) -> () } } = {}"); + line("local states: " + braced("[string]: " + braced("[string]: (...any) -> ()")) + " = {}"); line("local currentState: string? = nil"); line(""); line("local function setState(name: string)"); diff --git a/indra/alscript/lsl/allsltoslua.h b/indra/alscript/lsl/allsltoslua.h index d902dfe113..af32f50d77 100644 --- a/indra/alscript/lsl/allsltoslua.h +++ b/indra/alscript/lsl/allsltoslua.h @@ -87,6 +87,10 @@ class ALLSLToSLua // Each place noted as a "-- LSL:" comment over its line, as well as // in the notes. bool comments = true; + // A space inside a table's braces, and a table type's, as StyLua + // and the studio's formatter write them by default -- { "a", 1 }, + // { any } -- or none: {"a", 1}. Nothing inside, {} either way. + bool braceSpaces = true; // The script's own comments, each over what it stood over, after it // where it was after it on its line, or at the end of what held it: // // as --, and /* */ as --[[ ]]; a rule of stars or slashes as one of diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 1adebd2725..6ac5ca29bb 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -25,6 +25,7 @@ #include "linden_common.h" #include "../lsl/allsltoslua.h" +#include "../core/alscriptformatter.h" #include "../lint/alscriptfixes.h" #include "../lsl/allslservice.h" #include "../lsl/allsltraits.h" @@ -264,12 +265,12 @@ namespace tut ensure("a float as LSL writes it: " + r.text, has(r, "string.format(\"%.6f\", f)")); ensure("a string as an integer, as a list's item converts it: " + r.text, has(r, "lslInteger(\"12abc\")") && has(r, "return llcompat.List2Integer({ s }, 0)")); - ensure("lists compared by length: " + r.text, has(r, "#l == #{1}") && noted(r, "SluaListCompare")); + ensure("lists compared by length: " + r.text, has(r, "#l == #{ 1 }") && noted(r, "SluaListCompare")); // bit32 answers 0 to 4294967295: made LSL's signed integer, but where // bit32 takes it again. ensure("bits: " + r.text, has(r, "bit32.s32(bit32.bor(bit32.band(a, b), 4))") && !has(r, "local function int32")); ensure("a vector's part set: " + r.text, has(r, "v = vector(4, v.y, v.z)") && has(r, "v = vector(v.x, v.y, v.z + 1)")); - ensure("a vector as LSL writes it: " + r.text, has(r, "ll.DumpList2String({v}, \"\")")); + ensure("a vector as LSL writes it: " + r.text, has(r, "ll.DumpList2String({ v }, \"\")")); checksClean(r); } @@ -407,7 +408,7 @@ namespace tut ensure("one thing found in a list: " + r.text, has(r, "if table.find(l, \"a\") ~= nil then")); ensure("~ of a find: " + r.text, has(r, "if string.find(s, \"c\", 1, true) ~= nil then")); ensure("< 0 of one: " + r.text, has(r, "if string.find(s, \"z\", 1, true) == nil then")); - ensure("a find's index as a number, through llcompat: " + r.text, has(r, "llcompat.ListFindList(l, {\"a\", \"b\"})")); + ensure("a find's index as a number, through llcompat: " + r.text, has(r, "llcompat.ListFindList(l, { \"a\", \"b\" })")); ensure("^, math and vector: " + r.text, has(r, "2.0 ^ 3.0") && has(r, "math.abs(-1.0)") && has(r, "vector.magnitude(vector(1, 2, 3) - ZERO_VECTOR)")); ensure("half up, as LSL rounds, not math.round: " + r.text, has(r, "math.floor(2.5 + 0.5)")); ensure("os.time and print: " + r.text, has(r, "os.time()") && has(r, "print(")); @@ -535,7 +536,7 @@ namespace tut ensure("a global toggled: " + r.text, has(r, "local gOn = false") && has(r, "gOn = not gOn") && has(r, "if gOn then")); ensure("compared with FALSE: " + r.text, has(r, "local gSeen = false") && has(r, "if not gSeen and isOwner(") && has(r, "gSeen = true")); ensure("a function that answers one: " + r.text, has(r, "return k == ll.GetOwner()")); - ensure("a length, as a truth: " + r.text, has(r, "return #l ~= 0") && has(r, "local busy = anyOf({1})") && has(r, "while not busy do")); + ensure("a length, as a truth: " + r.text, has(r, "return #l ~= 0") && has(r, "local busy = anyOf({ 1 })") && has(r, "while not busy do")); ensure("stepped: " + r.text, has(r, "local count = 0") && has(r, "count += 1")); ensure("printed: " + r.text, has(r, "local shown = 1")); ensure("more than TRUE, compared with it: " + r.text, has(r, "local loose = 5") && has(r, "if loose == 1 then")); @@ -629,19 +630,19 @@ namespace tut " show(llListInsertList(gGrown, [addOne()], 0));\n" " show(kept() + b + a + gAll + gStored);\n" "} }\n"); - ensure("a local returned, grown in place: " + r.text, has(r, "local out = ll.ParseString2List(s, {\",\"}, {})") && has(r, "table.insert(out, \"end\")")); + ensure("a local returned, grown in place: " + r.text, has(r, "local out = ll.ParseString2List(s, { \",\" }, {})") && has(r, "table.insert(out, \"end\")")); ensure("a call's list, put on the end: " + r.text, has(r, "gAll = parts(\"a,b\")") && has(r, "table.extend(gAll, parts(\"c\"))")); ensure("prepended: " + r.text, has(r, "table.insert(gAll, 1, \"first\")")); ensure("a list by its name: " + r.text, has(r, "table.extend(gAll, more)")); ensure("several that do not run apart, each had before any is added: " + r.text, has(r, "table.append(gAll, ll.GetTime(), ll.Frand(1.0))")); ensure("passed to a function that only reads it: " + r.text, has(r, "table.insert(gSeen, n)")); ensure("passed to one that keeps it: " + r.text, - has(r, "gPassed = table.extend(table.clone(gPassed), {n})") && + has(r, "gPassed = table.extend(table.clone(gPassed), { n })") && has(r, "-- LSL: LSL's lists were values, and gPassed is passed to a function of the script's that keeps it")); - ensure("returned: " + r.text, has(r, "gKept = table.extend(table.clone(gKept), {2})") && has(r, "gKept is returned by a function")); - ensure("given to another: " + r.text, has(r, "a = table.extend(table.clone(a), {2})") && has(r, "a is given to another variable")); + ensure("returned: " + r.text, has(r, "gKept = table.extend(table.clone(gKept), { 2 })") && has(r, "gKept is returned by a function")); + ensure("given to another: " + r.text, has(r, "a = table.extend(table.clone(a), { 2 })") && has(r, "a is given to another variable")); ensure("read where a call in the statement grows it: " + r.text, - has(r, "gGrown = table.extend(table.clone(gGrown), {1})") && + has(r, "gGrown = table.extend(table.clone(gGrown), { 1 })") && has(r, "gGrown is read where a call of the script's in the same statement changes it")); ensure("said once for each: " + r.text, count(r, "and gPassed is") == 1); checksClean(r); @@ -712,7 +713,7 @@ namespace tut ensure("not where the left of the operator is more than x: " + r.text, has(r, "b = b - c - 1") && has(r, "c = c * 2 + b")); ensure("a string's pieces, joined: " + r.text, has(r, "s ..= `y{b}`")); ensure("a float's and a vector's as LSL writes them, in one interpolated string: " + r.text, - has(r, "print(`{a}{string.format(\"%.6f\", f)}{ll.DumpList2String({v}, \"\")}{s}{b + c}`)")); + has(r, "print(`{a}{string.format(\"%.6f\", f)}{ll.DumpList2String({ v }, \"\")}{s}{b + c}`)")); checksClean(r); } @@ -749,7 +750,7 @@ namespace tut "} }\n"); ensure("left out: " + r.text, !has(r, "PointAt(") && has(r, "-- llPointAt, left out") && noted(r, "SluaAbsent", "llPointAt")); ensure("its empty value where read: " + r.text, has(r, "local e = {}") && noted(r, "SluaAbsent", "llGetExperienceList")); - ensure("llcompat, said: " + r.text, has(r, "llcompat.GetPrimitiveParams({PRIM_FULLBRIGHT, 0})") && noted(r, "SluaBoolList", "GetPrimitiveParams")); + ensure("llcompat, said: " + r.text, has(r, "llcompat.GetPrimitiveParams({ PRIM_FULLBRIGHT, 0 })") && noted(r, "SluaBoolList", "GetPrimitiveParams")); ensure("not a truth: " + r.text, !has(r, "if llcompat.GetPrimitiveParams")); checksClean(r); } @@ -770,7 +771,7 @@ namespace tut "} }\n"); ensure("a key against a key: " + r.text, has(r, "if k ~= NULL_KEY then")); ensure("a string where LSL's string was: " + r.text, has(r, "tostring(NULL_KEY) ~= \"\"") && has(r, "ll.GetSubString(tostring(NULL_KEY), 1, 8)")); - ensure("in a list, as LSL had it: " + r.text, has(r, "{tostring(NULL_KEY), tostring(TEXTURE_BLANK)}")); + ensure("in a list, as LSL had it: " + r.text, has(r, "{ tostring(NULL_KEY), tostring(TEXTURE_BLANK) }")); ensure("a string's truth: " + r.text, has(r, "if tostring(NULL_KEY) ~= \"\" then print(")); ensure("given to a string and a key: " + r.text, has(r, "local gName = tostring(NULL_KEY)") && has(r, "local gKey = NULL_KEY")); ensure("returned as a string function's: " + r.text, has(r, "return tostring(NULL_KEY)")); @@ -1332,7 +1333,7 @@ namespace tut 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")); ensure("over a statement: " + r.text, has(r, " -- the rotation it turns by\n local r = ")); - ensure("a trailing one, after its statement: " + r.text, has(r, " ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_ROT_LOCAL, r * ll.GetLocalRot()}) -- turn\n")); + ensure("a trailing one, after its statement: " + r.text, has(r, " ll.SetLinkPrimitiveParamsFast(LINK_THIS, { PRIM_ROT_LOCAL, r * ll.GetLocalRot() }) -- turn\n")); ensure("between branches, over the elseif: " + r.text, has(r, " -- when it shuts\n elseif by < 0 then\n")); ensure("an empty branch's, in it: " + r.text, has(r, " if false then\n --[[ left off ]]\n elseif by == 0 then\n")); ensure("at a block's end: " + r.text, has(r, " -- done\nend\n")); @@ -1634,8 +1635,8 @@ namespace tut " llOwnerSay((string)ZERO_ROTATION);\n" "} }\n"); ensure("converted", r.converted); - ensure("LSL's text kept: " + r.text, has(r, "set(7, `{SEAT_NUM};{ll.DumpList2String({offset}, \"\")}`)")); - ensure("a rotation's too: " + r.text, has(r, "ll.DumpList2String({ZERO_ROTATION}, \"\")")); + ensure("LSL's text kept: " + r.text, has(r, "set(7, `{SEAT_NUM};{ll.DumpList2String({ offset }, \"\")}`)")); + ensure("a rotation's too: " + r.text, has(r, "ll.DumpList2String({ ZERO_ROTATION }, \"\")")); ensure("said why, once: " + r.text, noted(r, "SluaVectorText") && r.text.find("-- LSL: ll.DumpList2String writes") != std::string::npos && r.text.find("-- LSL: ll.DumpList2String writes") == r.text.rfind("-- LSL: ll.DumpList2String writes")); checksClean(r); @@ -2433,15 +2434,15 @@ namespace tut ensure("a list by its name: " + r.text, has(r, "table.extend(all, more)")); ensure("itself: " + r.text, has(r, "table.extend(all, all)")); ensure("a call's, run once: " + r.text, has(r, "table.extend(all, llcompat.List2List(more, 0, n))") && !has(r, "for _, item in")); - ensure("many values: " + r.text, has(r, "table.extend(all, {0, 1, 2,") && has(r, "32, \"x\"})")); + ensure("many values: " + r.text, has(r, "table.extend(all, { 0, 1, 2,") && has(r, "32, \"x\" })")); ensure("one copy, of the variable a call answers: " + r.text, has(r, "show(table.extend(table.extend(table.clone(kept()), more), all))")); - ensure("a variable copied: " + r.text, has(r, "show(table.extend(table.clone(more), {n}))")); - ensure("a value: " + r.text, has(r, "show(table.extend({n} :: { any }, more))")); - ensure("written out: " + r.text, has(r, "show(table.extend({PRIM_NAME, \"x\"} :: { any }, more))") && - has(r, "table.extend({PRIM_COLOR, ALL_SIDES, vector(1, 1, 1), 1.0} :: { any }, more)")); + ensure("a variable copied: " + r.text, has(r, "show(table.extend(table.clone(more), { n }))")); + ensure("a value: " + r.text, has(r, "show(table.extend({ n } :: { any }, more))")); + ensure("written out: " + r.text, has(r, "show(table.extend({ PRIM_NAME, \"x\" } :: { any }, more))") && + has(r, "table.extend({ PRIM_COLOR, ALL_SIDES, vector(1, 1, 1), 1.0 } :: { any }, more)")); ensure("a library call's: " + r.text, - has(r, "show(table.extend(table.extend(ll.ParseString2List(\"c\", {}, {}) :: { any }, {1}), {vector(1, 2, 3)}))")); + has(r, "show(table.extend(table.extend(ll.ParseString2List(\"c\", {}, {}) :: { any }, { 1 }), { vector(1, 2, 3) }))")); ensure("no helper, nor a move: " + r.text, !has(r, "joinLists") && !has(r, "table.move(")); checksClean(r); } @@ -2487,22 +2488,22 @@ namespace tut "} }\n"); ensure("replaced in place, in Luau's numeric for: " + r.text, has(r, "for i = 0, #l - 1 do\n l[i + 1] = l[i + 1] * 2\n")); ensure("a list held elsewhere, replaced as LSL's: " + r.text, - has(r, "for j = 0, #held - 1 do\n") && has(r, " held = llcompat.ListReplaceList(held, {j}, j, j)\n")); + has(r, "for j = 0, #held - 1 do\n") && has(r, " held = llcompat.ListReplaceList(held, { j }, j, j)\n")); ensure("a global a call of the script's could change: " + r.text, has(r, "while k < #gL do") && - has(r, "gL = llcompat.ListReplaceList(gL, {bump()}, k, k)")); - ensure("the last of a list that could be empty: " + r.text, has(r, "l = llcompat.ListReplaceList(l, {7}, -1, -1)")); + has(r, "gL = llcompat.ListReplaceList(gL, { bump() }, k, k)")); + ensure("the last of a list that could be empty: " + r.text, has(r, "l = llcompat.ListReplaceList(l, { 7 }, -1, -1)")); ensure("on the end: " + r.text, has(r, "table.insert(l, 9)")); ensure("a part, and all of it: " + r.text, has(r, "local part = table.move(l, 2, 3, 1, {})") && has(r, "local all = table.clone(l)")); ensure("to the end, and the part left out, as LSL's: " + r.text, has(r, "local rest = llcompat.List2List(l, 2, -1)") && has(r, "local wrapped = llcompat.List2List(l, 3, 1)")); ensure("whole numbers joined: " + r.text, has(r, "print(table.concat(l, \",\"))") && has(r, "print(table.concat(l))")); - ensure("and text: " + r.text, has(r, "print(table.concat(table.extend(table.clone(gNames), {n}), \", \"))")); - ensure("a float as LSL writes one: " + r.text, has(r, "print(ll.DumpList2String(table.extend(table.clone(l), {1.5}), \",\"))")); + ensure("and text: " + r.text, has(r, "print(table.concat(table.extend(table.clone(gNames), { n }), \", \"))")); + ensure("a float as LSL writes one: " + r.text, has(r, "print(ll.DumpList2String(table.extend(table.clone(l), { 1.5 }), \",\"))")); ensure("found, its place: " + r.text, has(r, "local at = (table.find(gNames, \"b\") or 0) - 1")); ensure("NULL_KEY as LSL's string, as its place and found: " + r.text, has(r, "local none = (table.find(gNames, tostring(NULL_KEY)) or 0) - 1") && has(r, "if table.find(gNames, tostring(NULL_KEY)) ~= nil then")); - ensure("in a library call's list: " + r.text, has(r, "local parsed = (table.find(ll.ParseString2List(\"a,b\", {\",\"}, {}) :: { any }, \"b\") or 0) - 1")); + ensure("in a library call's list: " + r.text, has(r, "local parsed = (table.find(ll.ParseString2List(\"a,b\", { \",\" }, {}) :: { any }, \"b\") or 0) - 1")); ensure("no llcompat but those: " + r.text, count(r, "llcompat.") == 5); checksClean(r); } @@ -2602,10 +2603,10 @@ namespace tut // The join leaves a first piece of none out, escapes a \, and sends // no rule at all for no text. ensure("a whitelist the join would change, a list: " + r.text, - has(r, "ll.SetPrimMediaParams(1, {PRIM_MEDIA_WHITELIST, \",a.com\"})") && has(r, "ll.SetPrimMediaParams(2, {PRIM_MEDIA_WHITELIST, \"a") && - has(r, "ll.SetPrimMediaParams(3, {PRIM_MEDIA_WHITELIST, \"\"})")); + has(r, "ll.SetPrimMediaParams(1, { PRIM_MEDIA_WHITELIST, \",a.com\" })") && has(r, "ll.SetPrimMediaParams(2, { PRIM_MEDIA_WHITELIST, \"a") && + has(r, "ll.SetPrimMediaParams(3, { PRIM_MEDIA_WHITELIST, \"\" })")); ensure("a truth not written out as one, a list: " + r.text, - has(r, "ll.SetPrimMediaParams(4, {PRIM_MEDIA_AUTO_PLAY, on})") && has(r, "ll.SetPrimMediaParams(5, {PRIM_MEDIA_AUTO_ZOOM, 2})")); + has(r, "ll.SetPrimMediaParams(4, { PRIM_MEDIA_AUTO_PLAY, on })") && has(r, "ll.SetPrimMediaParams(5, { PRIM_MEDIA_AUTO_ZOOM, 2 })")); checksClean(r); } @@ -2650,9 +2651,9 @@ namespace tut " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted()], \"\");\n" " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_CUSTOM_HEADER, \"B\", counted()], \"\");\n" "} }\n"); - ensure("a name twice, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", {HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"A\", \"2\"}, \"\")")); - ensure("a name not written out, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", {HTTP_CUSTOM_HEADER, token, \"1\"}, \"\")")); - ensure("moved past another, a list: " + lists.text, has(lists, "{HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted()}")); + ensure("a name twice, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", { HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"A\", \"2\" }, \"\")")); + ensure("a name not written out, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", { HTTP_CUSTOM_HEADER, token, \"1\" }, \"\")")); + ensure("moved past another, a list: " + lists.text, has(lists, "{ HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted() }")); ensure("one after another, run in their order: " + lists.text, has(lists, "{ custom_header = { [\"A\"] = counted(), [\"B\"] = counted() } }")); checksClean(lists); } @@ -2677,18 +2678,18 @@ namespace tut "} }\n"; const ALLSLToSLua::Result r = convert(script); ensure("not written out: " + r.text, has(r, "ll.ParticleSystem(gRules)")); - ensure("a key twice: " + r.text, has(r, "ll.ParticleSystem({PSYS_PART_MAX_AGE, 2.0, PSYS_PART_MAX_AGE, 3.0})")); - ensure("another table's rule: " + r.text, has(r, "ll.ParticleSystem({PRIM_MEDIA_AUTO_PLAY, 1})")); - ensure("a number for a rule: " + r.text, has(r, "ll.ParticleSystem({7, 2.0})")); - ensure("a value of another shape: " + r.text, has(r, "ll.ParticleSystem({PSYS_PART_START_ALPHA, vector(1, 0, 0)})")); - ensure("no value: " + r.text, has(r, "ll.ParticleSystem({PSYS_PART_MAX_AGE})")); + ensure("a key twice: " + r.text, has(r, "ll.ParticleSystem({ PSYS_PART_MAX_AGE, 2.0, PSYS_PART_MAX_AGE, 3.0 })")); + ensure("another table's rule: " + r.text, has(r, "ll.ParticleSystem({ PRIM_MEDIA_AUTO_PLAY, 1 })")); + ensure("a number for a rule: " + r.text, has(r, "ll.ParticleSystem({ 7, 2.0 })")); + ensure("a value of another shape: " + r.text, has(r, "ll.ParticleSystem({ PSYS_PART_START_ALPHA, vector(1, 0, 0) })")); + ensure("no value: " + r.text, has(r, "ll.ParticleSystem({ PSYS_PART_MAX_AGE })")); ensure("flags as they are: " + r.text, has(r, "ll.LinkParticleSystem(1, { flags = PSYS_PART_WIND_MASK + PSYS_PART_BOUNCE_MASK })") && has(r, "ll.LinkParticleSystem(2, { flags = bit32.bor(PSYS_PART_WIND_MASK, PSYS_PART_WIND_MASK) })")); ensure("masks bracketed, each true: " + r.text, has(r, "ll.LinkParticleSystem(3, { wind = true, bounce = true, ribbon = true })")); ALLSLToSLua::Options plain; plain.idioms = false; const ALLSLToSLua::Result lists = ALLSLToSLua::convert(script, plain); - ensure("no idioms, no tables: " + lists.text, has(lists, "ll.LinkParticleSystem(3, {PSYS_PART_FLAGS, ") && !has(lists, "wind = true")); + ensure("no idioms, no tables: " + lists.text, has(lists, "ll.LinkParticleSystem(3, { PSYS_PART_FLAGS, ") && !has(lists, "wind = true")); } template<> template<> @@ -2832,12 +2833,12 @@ namespace tut 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")); + 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" + " \"ima\", ll.List2Json(JSON_ARRAY, { \"Ungulate\" }),\n" " \"ownerOnly\", 1, -- only the owner\n" " \"toucher\", ll.GetOwner(),\n" " \"requestor\", ll.List2Json(JSON_OBJECT, {\n" @@ -2848,8 +2849,8 @@ namespace tut " \"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("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); } @@ -2897,10 +2898,10 @@ namespace tut " if (mine(llJsonGetValue(gJson, [\"owner\"]))) llOwnerSay(\"mine\");\n" "} }\n"; const ALLSLToSLua::Result r = convert(lsl); - ensure("Tapple's, as it is: " + r.text, has(r, "ll.Key2Name(ll.JsonGetValue(gJson, {\"selected\"}) :: any)")); + ensure("Tapple's, as it is: " + r.text, has(r, "ll.Key2Name(ll.JsonGetValue(gJson, { \"selected\" }) :: any)")); ensure("bracketed where it must be: " + r.text, has(r, "ll.Key2Name((\"a2e76fcd-9360-4f6d-a924-\" .. \"938f923df11d\") :: any)")); ensure("a UUID written out, a uuid: " + r.text, has(r, "ll.Key2Name(uuid(\"a2e76fcd-9360-4f6d-a924-938f923df11d\"))")); - ensure("the script's own function's key, uuid(): " + r.text, has(r, "mine(uuid(ll.JsonGetValue(gJson, {\"owner\"})))") && + ensure("the script's own function's key, uuid(): " + r.text, has(r, "mine(uuid(ll.JsonGetValue(gJson, { \"owner\" })))") && noted(r, "SluaUuidText")); ensure("said once: " + r.text, count(r, "-- LSL: SLua's ll takes a key as text") == 1); checksClean(r); @@ -3013,4 +3014,49 @@ namespace tut ensure("never two: " + r.text, !has(r, "\n\n\n")); checksClean(r); } + + template<> template<> + void allsltoslua_object::test<88>() + { + set_test_name("tables and table types spaced inside their braces as asked, as the formatter spaces them: StyLua's by default, " + "Tapple's line; none where asked; an interpolated string's braces and {} never"); + const std::string lsl = "list gTwo = [1, 2,\n 3];\n" + "default {\n" + " state_entry() { state two; }\n" + "}\n" + "state two {\n" + " listen(integer c, string name, key id, string msg) {\n" + " list params = llParseStringKeepNulls(msg, [\"\\n\"], []);\n" + " llSetPrimMediaParams(0, [PRIM_MEDIA_AUTO_PLAY, TRUE, PRIM_MEDIA_WHITELIST, \"a.com,b.com\"]);\n" + " llOwnerSay(\"n=\" + (string)llGetListLength(params) + \" \" + (string)<1, 2, 3>);\n" + " }\n" + "}\n"; + ALLSLToSLua::Options typed; + typed.types = true; + const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, typed); + ensure("Tapple's: " + r.text, r.converted && has(r, "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})")); + ensure("a rule table: " + r.text, has(r, "ll.SetPrimMediaParams(0, { auto_play = true, whitelist = { \"a.com\", \"b.com\" } })")); + ensure("the states' type: " + r.text, has(r, "local states: { [string]: { [string]: (...any) -> () } } = {}")); + ensure("over lines: " + r.text, has(r, "local gTwo: { any } = { 1, 2,\n 3 }\n")); + ensure("an interpolated string's: " + r.text, has(r, "`n={#params} {ll.DumpList2String({ vector(1, 2, 3) }, \"\")}`")); + checksClean(r); + + typed.braceSpaces = false; + const ALLSLToSLua::Result none = ALLSLToSLua::convert(lsl, typed); + ensure("none: " + none.text, none.converted && has(none, "local params: {any} = ll.ParseStringKeepNulls(msg, {\"\\n\"}, {})") && + has(none, "ll.SetPrimMediaParams(0, {auto_play = true, whitelist = {\"a.com\", \"b.com\"}})") && + has(none, "local states: {[string]: {[string]: (...any) -> ()}} = {}") && + has(none, "local gTwo: {any} = {1, 2,\n 3}\n") && !has(none, "{ ")); + checksClean(none); + + // As the formatter would leave them, either way. + for (bool spaces : { true, false }) + { + ALScriptFormatter::Options formatter; + formatter.lua = true; + formatter.braceSpaces = spaces; + const ALLSLToSLua::Result same = spaces ? r : none; + ensure_equals(spaces ? "formatted, spaced" : "formatted, none", ALScriptFormatter::format(same.text, formatter), same.text); + } + } } diff --git a/indra/newview/alfloaterscriptstudio.cpp b/indra/newview/alfloaterscriptstudio.cpp index 8f9c55aaa1..e16fff6e7f 100644 --- a/indra/newview/alfloaterscriptstudio.cpp +++ b/indra/newview/alfloaterscriptstudio.cpp @@ -8194,6 +8194,8 @@ void ALFloaterScriptStudio::convertToSLua(Doc& doc) options.types = gSavedSettings.getBOOL("ALScriptConvertTypes"); options.comments = gSavedSettings.getBOOL("ALScriptConvertComments"); options.keepComments = gSavedSettings.getBOOL("ALScriptConvertKeepComments"); + // Its tables' braces as the formatter writes them. + options.braceSpaces = gSavedSettings.getBOOL("ALScriptFormatBraceSpaces"); // Its notes, and its comments, in the skin's words. options.words = alScriptKeyedWords; const std::string source = doc.editor->wholeText(); From ee9af67ea1825d42c3c39ebb47c451243b8b4e98 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 21:19:01 -0400 Subject: [PATCH 07/12] The formatter keeps a sign against what it signs, and a comparison spaced Three slips in the formatter's spacing, found formatting converted SLua. A sign before a bracket or a brace was spaced from it: -(a) came out as - (a), #{1, 2} as # { 1, 2 }, and LSL's !(b) as ! (b). A sign after a < was always put against it, since LSL's < opens a vector there, but a < that compares does not: Luau's i < -1 came out as i <-1, i < #t as i <#t, and LSL's if (i < -1) as i <-1. And a sign after a minus was put against it as if the minus were a sign too, so x - #t came out as x -#t. A sign standing as one, where nothing it could be an operator of comes before it, is now written against the bracket or brace after it. After a <, it is only where that < opened an LSL vector, and after a minus only where the minus is a sign itself. alscriptformatter 13. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/core/alscriptformatter.cpp | 23 ++++++++++++++++--- .../alscript/tests/alscriptformatter_test.cpp | 14 +++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/indra/alscript/core/alscriptformatter.cpp b/indra/alscript/core/alscriptformatter.cpp index 1e7016d8ad..3245a31bbe 100644 --- a/indra/alscript/core/alscriptformatter.cpp +++ b/indra/alscript/core/alscriptformatter.cpp @@ -160,6 +160,9 @@ namespace // statement, spaced from it; what follows a cast's is its operand. std::vector brackets; bool conditionClosed = false; + // Whether the token before is a < that opened an LSL vector, not + // a comparison: a sign after it is the first part's. + bool vectorOpened = false; for (const Token& t : line.tokens) { if (t.kind == Kind::Space) @@ -169,6 +172,8 @@ namespace } const bool closedCondition = conditionClosed; conditionClosed = false; + const bool openedVector = vectorOpened; + vectorOpened = false; if (t.kind == Kind::Punct && t.text == "(") { brackets.push_back(prev && prev->kind == Kind::Ident && (prev->text == "if" || prev->text == "while" || prev->text == "for")); @@ -185,6 +190,7 @@ namespace if (!lua && t.kind == Kind::Punct && t.text == "<") { ++vector; + vectorOpened = true; } out += t.text; prev = &t; @@ -234,7 +240,8 @@ namespace } else if (a.kind == Kind::Punct && isOperator(lua, a.text)) { - say(true); + // After a sign, nothing: -(a), #(t), !(b). + say(!(isUnary(lua, a.text) && (!earlier || !operand(lua, *earlier)))); } } else if (p == "[") @@ -251,6 +258,11 @@ namespace { say(false); } + else if (a.kind == Kind::Punct && isUnary(lua, a.text) && (!earlier || !operand(lua, *earlier))) + { + // A length of a table written out: #{1, 2}. + say(false); + } else { say(true); @@ -271,6 +283,7 @@ namespace if (!aOperand) { ++vector; + vectorOpened = true; say(!(a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || a.text == "<" || a.text == "{"))); } else @@ -290,7 +303,9 @@ namespace // A sign or a not: spaced before as its neighbour // asks, and nothing after, which the next token's // turn will see to. - if (a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || a.text == "<" || a.text == "{" || a.text == "!" || a.text == "~" || a.text == "#")) + // Not after a < that compares: LSL's where an operand + // stood before it, and Luau's everywhere. + if (a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || (a.text == "<" && openedVector) || a.text == "{" || a.text == "!" || a.text == "~" || a.text == "#")) { say(false); } @@ -298,8 +313,10 @@ namespace { say(true); } - else if (a.kind == Kind::Punct && isUnary(lua, a.text)) + else if (a.kind == Kind::Punct && isUnary(lua, a.text) && (!earlier || !operand(lua, *earlier))) { + // A sign after a sign: -#t. Not after a minus + // that subtracts, x - #t. say(false); } else diff --git a/indra/alscript/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index 83158b03ea..153b34e975 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -415,4 +415,18 @@ namespace tut ensure_equals("LSL's braces", ALScriptFormatter::format("default { state_entry() { llSay(0, \"a\"); } }\n", lsl_none), std::string("default { state_entry() { llSay(0, \"a\"); } }\n")); } + + template<> template<> + void alscriptformatter_object::test<13>() + { + set_test_name("a sign against the bracket or brace after it, and after a < that compares or a minus that subtracts, spaced " + "from it: -(a), #{ 1 }, i < -1, x - #t, in Luau and in LSL; LSL's vector's first sign still against its <"); + ensure_equals("Luau", lua("local x = - (a)\nlocal y = # (t)\nlocal z = # {1}\nif i <#t and i <-1 and i > - (2) then end\n" + "local w = not (a)\nlocal q = a - (b)\nreturn -(x)\nlocal d = #{1} -#{2} - -#t + -#t\n"), + std::string("local x = -(a)\nlocal y = #(t)\nlocal z = #{ 1 }\nif i < #t and i < -1 and i > -(2) then end\n" + "local w = not (a)\nlocal q = a - (b)\nreturn -(x)\nlocal d = #{ 1 } - #{ 2 } - -#t + -#t\n")); + ensure_equals("LSL", lsl("default { state_entry() { integer x = - (a); vector v = < -1, 0, 0 >; if (i <-1) x = ! (b); } }\n"), + std::string("default { state_entry() { integer x = -(a); vector v = <-1, 0, 0>; if (i < -1) x = !(b); } }\n")); + ensure_equals("a list of vectors", lsl("list l = [<-1, 0, 0>, < -2, 0, 0 >];\n"), std::string("list l = [<-1, 0, 0>, <-2, 0, 0>];\n")); + } } From 495c3ac368e7fd075d4118ba6b10d36a5b8fd45d Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 22:02:26 -0400 Subject: [PATCH 08/12] The formatter keeps a comment written over an elseif at the elseif's level A comment on a line of its own was indented as far as the block it stood in. One the author wrote over an `elseif`, `else`, `until`, `end` or `}`, at that line's level, was pushed into the block before it. The converter writes its notes on an elseif's or an until's condition that way, over the line they are about, so format on save moved them into the wrong block. Such a comment now keeps the level of the line under it where the author wrote it out of the block: no further in than that line, and less far than the code over it in the block. In a block with no code before it, the author's indentation decides, wherever the text is indented at all. A comment written as far in as the block, or in text with no indentation to tell, is the block's end and stays in it. alscriptformatter 15. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/core/alscriptformatter.cpp | 87 +++++++++++++++++++ .../alscript/tests/alscriptformatter_test.cpp | 26 ++++++ 2 files changed, 113 insertions(+) diff --git a/indra/alscript/core/alscriptformatter.cpp b/indra/alscript/core/alscriptformatter.cpp index 3245a31bbe..306b5de016 100644 --- a/indra/alscript/core/alscriptformatter.cpp +++ b/indra/alscript/core/alscriptformatter.cpp @@ -954,6 +954,90 @@ namespace return out; } + // How far in the author wrote a line, tabs to their stops. + S32 writtenIndent(const Line& line, S32 tab) + { + S32 column = 0; + if (!line.tokens.empty() && line.tokens.front().kind == Kind::Space) + { + for (const char c : line.tokens.front().text) + { + column = c == '\t' ? (column / tab + 1) * tab : column + 1; + } + } + return column; + } + + // Whether a line holds a comment and nothing else. + bool commentOnly(const Line& line) + { + bool comment = false; + for (const Token& t : line.tokens) + { + if (t.kind == Kind::Comment) + { + comment = true; + } + else if (t.kind != Kind::Space) + { + return false; + } + } + return comment && !line.directive && line.first == line.last; + } + + // A comment over a line that goes back out -- `elseif`, `else`, + // `until`, `end`, `}` -- is about that line where the author wrote it + // out of the block: no further in than that line, and less far than + // the code over it in the block, or, in a block with none, in text + // the author indented. It goes in as far as the line. One written as + // far in as the block, or in text with no indentation to tell, is the + // end of the block, and stays in it. + void overWhatFollows(std::vector& lines, const ALScriptFormatter::Options& options) + { + const S32 tab = options.indent > 0 ? options.indent : 4; + const auto code = [&](const Line& line) { return !line.blank && !line.directive && !commentOnly(line); }; + const bool indented = std::any_of(lines.begin(), lines.end(), + [&](const Line& line) { return code(line) && line.indent > 0 && writtenIndent(line, tab) > 0; }); + for (size_t i = 1; i < lines.size(); ++i) + { + const Line& next = lines[i]; + if (!code(next)) + { + continue; + } + size_t top = i; + while (top > 0 && commentOnly(lines[top - 1])) + { + --top; + } + if (top == i) + { + continue; + } + size_t over = top; + while (over > 0 && !code(lines[over - 1])) + { + --over; + } + if (over == 0) + { + continue; + } + // The code over them, in the block or the line that opens it. + const Line& above = lines[over - 1]; + const S32 written = writtenIndent(next, tab); + const auto out = [&](const Line& comment) { + const S32 at = writtenIndent(comment, tab); + return at <= written && (above.indent < comment.indent ? indented : at < writtenIndent(above, tab)); + }; + for (size_t j = i; j > top && lines[j - 1].indent > next.indent && out(lines[j - 1]); --j) + { + lines[j - 1].indent = next.indent; + } + } + } + // 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`. @@ -1069,6 +1153,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); + overWhatFollows(lines, options); return emit(lines, options, endsWithNewline, -1, -1, true); } @@ -1079,6 +1164,7 @@ 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); + overWhatFollows(lines, options); return emit(lines, options, endsWithNewline, first, last, false); } @@ -1089,6 +1175,7 @@ std::vector ALScriptFormatter::formatEach(std::string_view text, co bool endsWithNewline = false; std::vector lines = linesOf(text, options.lua, endsWithNewline); decide(lines, options.lua); + overWhatFollows(lines, options); // 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); diff --git a/indra/alscript/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index 153b34e975..68fb3904e0 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -429,4 +429,30 @@ namespace tut std::string("default { state_entry() { integer x = -(a); vector v = <-1, 0, 0>; if (i < -1) x = !(b); } }\n")); ensure_equals("a list of vectors", lsl("list l = [<-1, 0, 0>, < -2, 0, 0 >];\n"), std::string("list l = [<-1, 0, 0>, <-2, 0, 0>];\n")); } + + template<> template<> + void alscriptformatter_object::test<15>() + { + set_test_name("a comment the author wrote out of the block over an elseif, else, until or end stays as far in as that line; one " + "written in the block stays in it; in an empty block, as written where the text is indented; flat text, in"); + const std::string chain = "if a then\n" + " x()\n" + "-- about b\n" + "elseif b then\n" + " y()\n" + " -- the end of y\n" + "else\n" + " z()\n" + "end\n"; + ensure_equals("as written", lua(chain), chain); + const std::string repeat = "local function f()\n" + " repeat\n" + " -- why\n" + " until x\n" + "end\n"; + ensure_equals("an empty block, indented", lua(repeat), repeat); + ensure_equals("flat", lua("if a then\nx()\n-- note\nend\n"), std::string("if a then\n x()\n -- note\nend\n")); + ensure_equals("LSL", lsl("default\n{\n touch_start(integer n)\n {\n x();\n // about the end\n }\n}\n"), + std::string("default\n{\n touch_start(integer n)\n {\n x();\n // about the end\n }\n}\n")); + } } From 50c315087d54c3b407141c268a9cddc7416f0754 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 22:02:53 -0400 Subject: [PATCH 09/12] The formatter keeps what the author lined up The formatter put one space around every `=`, so a run of declarations the author had lined up came apart, and comments lined up after them came apart with the code. The converter lines up again what the LSL lined up (QA3, 91b94b5a95), so format on save undid it in converted scripts too. A run is lines one after another at one depth whose `=`, its first outside brackets, or whose trailing comments the author put at one column, with more than one space before at least one of them to show it was meant. A comment on a line of its own at that depth does not break a run. Once each line is spaced afresh, the run's `=` or comments go to that column again, or just past the longest line where it no longer fits. A run of comments whose code did not change keeps its blanks as written. Columns are measured with tabs to their stops, so LSL lined up with tabs is kept, in spaces. alscriptformatter 14. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/core/alscriptformatter.cpp | 283 ++++++++++++++++-- .../alscript/tests/alscriptformatter_test.cpp | 25 ++ 2 files changed, 290 insertions(+), 18 deletions(-) diff --git a/indra/alscript/core/alscriptformatter.cpp b/indra/alscript/core/alscriptformatter.cpp index 306b5de016..634a3a53d9 100644 --- a/indra/alscript/core/alscriptformatter.cpp +++ b/indra/alscript/core/alscriptformatter.cpp @@ -1038,6 +1038,236 @@ namespace } } + // Where the author put a line's `=` -- its first outside brackets -- + // or its trailing comment: the token, and its column from where the + // line's code starts, tabs to their stops; and whether the blank + // before it is wider than one space, which is what says it was put + // there. + struct Mark + { + size_t token = std::string::npos; + S32 column = 0; + bool padded = false; + }; + + struct Marks + { + Mark equals; + Mark comment; + // A comment and nothing else, which a run goes on past. + bool through = false; + }; + + Marks marksOf(const Line& line, S32 tab) + { + Marks marks; + marks.through = commentOnly(line); + if (line.blank || line.directive || line.first != line.last || marks.through) + { + return marks; + } + S32 column = 0; + S32 start = -1; + S32 depth = 0; + bool code = false; + for (size_t i = 0; i < line.tokens.size(); ++i) + { + const Token& t = line.tokens[i]; + if (t.kind != Kind::Space && start < 0) + { + start = column; + } + const bool padded = i > 0 && line.tokens[i - 1].kind == Kind::Space && line.tokens[i - 1].text != " "; + if (t.kind == Kind::Punct) + { + if (t.text == "(" || t.text == "[" || t.text == "{") + { + ++depth; + } + else if (t.text == ")" || t.text == "]" || t.text == "}") + { + // Closing what an earlier line opened: no statement's =. + depth = depth > 0 ? depth - 1 : -1000; + } + else if (t.text == "=" && depth == 0 && code && marks.equals.token == std::string::npos) + { + marks.equals = { i, column - start, padded }; + } + } + if (t.kind == Kind::Comment && code) + { + marks.comment = { i, column - start, padded }; + } + else if (t.kind != Kind::Space) + { + // Anything after a comment is no trailing one. + marks.comment = Mark(); + code = true; + } + for (const char c : t.text) + { + column = c == '\t' ? (column / tab + 1) * tab : column + ((static_cast(c) & 0xC0) != 0x80 ? 1 : 0); + } + } + if (marks.comment.token != std::string::npos && marks.equals.token > marks.comment.token) + { + marks.equals = Mark(); + } + return marks; + } + + // What the author lined up, lined up again once each line is spaced + // afresh: a run of lines one after another, as deep as each other, + // whose `=`, or whose trailing comments, the author put at one column + // and spaced wider than one somewhere to get there. Each goes to that + // column again, or just past the longest line where it no longer + // fits. A run of comments none of whose lines' code changed is left + // as it was, its blanks as written. `code` holds each line written + // out where it is the formatter's to write. + void lineUp(const std::vector& lines, const ALScriptFormatter::Options& options, std::vector>& code) + { + const S32 tab = options.indent > 0 ? options.indent : 4; + std::vector marks; + marks.reserve(lines.size()); + for (const Line& line : lines) + { + marks.push_back(marksOf(line, tab)); + } + // Each run of the marks `of` picks, from `from` to `to`, one past, + // a comment as far in on a line of its own going on past; each + // line of it given `each` has its mark. + const auto runs = [&](auto of, auto&& each) { + for (size_t from = 0; from < lines.size();) + { + const Mark first = of(marks[from]); + if (first.token == std::string::npos) + { + ++from; + continue; + } + size_t to = from; + size_t marked = 0; + bool padded = false; + for (size_t at = from; at < lines.size() && lines[at].indent == lines[from].indent; ++at) + { + if (marks[at].through) + { + continue; + } + if (of(marks[at]).token == std::string::npos || of(marks[at]).column != first.column) + { + break; + } + padded = padded || of(marks[at]).padded; + ++marked; + to = at + 1; + } + if (marked >= 2 && padded) + { + each(from, to, first.column); + } + from = to; + } + }; + const auto lead = [&](size_t i) { return indentText(options, lines[i].indent).size(); }; + runs([](const Marks& m) { return m.equals; }, + [&](size_t from, size_t to, S32 column) { + // Where each = is now: past its code as spaced, which is + // what the line written out begins with, and one space. + std::vector> at(to - from, { std::string::npos, 0 }); + S32 most = column; + for (size_t i = from; i < to; ++i) + { + if (!code[i] || marks[i].through) + { + continue; + } + Line before; + before.tokens.assign(lines[i].tokens.begin(), lines[i].tokens.begin() + static_cast(marks[i].equals.token)); + std::string prefix = spaced(before, options); + while (!prefix.empty() && (prefix.back() == ' ' || prefix.back() == '\t')) + { + prefix.pop_back(); + } + const size_t equals = lead(i) + prefix.size() + 1; + if (equals < code[i]->size() && (*code[i])[equals] == '=' && (*code[i])[equals - 1] == ' ') + { + at[i - from] = { equals, columns(prefix, 1) + 1 }; + most = std::max(most, at[i - from].second); + } + } + for (size_t i = from; i < to; ++i) + { + if (at[i - from].first != std::string::npos) + { + code[i]->insert(at[i - from].first, static_cast(most - at[i - from].second), ' '); + } + } + }); + runs([](const Marks& m) { return m.comment; }, + [&](size_t from, size_t to, S32 column) { + // Each line's code, as written out now, and its comment. + struct Split + { + size_t end = std::string::npos; + S32 columns = 0; + }; + std::vector at(to - from); + S32 most = column; + bool changed = false; + for (size_t i = from; i < to; ++i) + { + if (!code[i] || marks[i].through) + { + continue; + } + std::string comment = lines[i].tokens[marks[i].comment.token].text; + while (!comment.empty() && (comment.back() == ' ' || comment.back() == '\t' || comment.back() == '\r')) + { + comment.pop_back(); + } + const std::string& text = *code[i]; + if (comment.empty() || text.size() < comment.size() + lead(i) || + text.compare(text.size() - comment.size(), comment.size(), comment) != 0) + { + continue; + } + size_t end = text.size() - comment.size(); + while (end > lead(i) && (text[end - 1] == ' ' || text[end - 1] == '\t')) + { + --end; + } + const std::string_view now = std::string_view(text).substr(lead(i), end - lead(i)); + std::string was; + for (size_t t = 0; t < marks[i].comment.token; ++t) + { + was += was.empty() && lines[i].tokens[t].kind == Kind::Space ? std::string() : lines[i].tokens[t].text; + } + while (!was.empty() && (was.back() == ' ' || was.back() == '\t')) + { + was.pop_back(); + } + changed = changed || now != was; + at[i - from] = { end, columns(now, 1) }; + most = std::max(most, at[i - from].columns + 1); + } + if (!changed) + { + return; + } + for (size_t i = from; i < to; ++i) + { + if (at[i - from].end != std::string::npos) + { + std::string& text = *code[i]; + const size_t comment = text.find_first_not_of(" \t", at[i - from].end); + text = text.substr(0, at[i - from].end) + + std::string(static_cast(most - at[i - from].columns), ' ') + text.substr(comment); + } + } + }); + } + // 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`. @@ -1048,15 +1278,46 @@ namespace S32 blanks = 0; size_t kept = 0; const bool whole = first < 0; + const auto ours = [&](const Line& line) { return whole || (line.first >= first && line.last <= last); }; + // Each line of code the formatter writes, spaced, and lined up + // where the author lined them up, before any is broken. + std::vector> code(lines.size()); + for (size_t i = 0; i < lines.size(); ++i) + { + const Line& line = lines[i]; + if (!ours(line) || line.directive || line.blank) + { + continue; + } + std::string text = indentText(options, line.indent); + if (options.spacing) + { + text += spaced(line, options); + } + else + { + std::string body = asWritten(line); + size_t at = body.find_first_not_of(" \t"); + text += at == std::string::npos ? std::string() : body.substr(at); + } + while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) + { + text.pop_back(); + } + code[i] = std::move(text); + } + if (options.spacing) + { + lineUp(lines, options, code); + } for (size_t i = 0; i < lines.size(); ++i) { const Line& line = lines[i]; - const bool ours = whole || (line.first >= first && line.last <= last); std::string text; - if (!ours || line.directive) + if (!ours(line) || line.directive) { text = asWritten(line); - if (ours) + if (ours(line)) { while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) { @@ -1082,21 +1343,7 @@ namespace } else { - text = indentText(options, line.indent); - if (options.spacing) - { - text += spaced(line, options); - } - else - { - std::string body = asWritten(line); - size_t at = body.find_first_not_of(" \t"); - text += at == std::string::npos ? std::string() : body.substr(at); - } - while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) - { - text.pop_back(); - } + text = std::move(*code[i]); if (wrap) { text = wrapped(line, options, text); diff --git a/indra/alscript/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index 68fb3904e0..f74d8ef06e 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -430,6 +430,31 @@ namespace tut ensure_equals("a list of vectors", lsl("list l = [<-1, 0, 0>, < -2, 0, 0 >];\n"), std::string("list l = [<-1, 0, 0>, <-2, 0, 0>];\n")); } + template<> template<> + void alscriptformatter_object::test<14>() + { + set_test_name("what the author lined up stays lined up: a run's =, at the author's column or past the longest, a comment on a line " + "of its own going on past; a run's trailing comments where its code changed; not equal columns by chance; tabs to " + "their stops"); + const std::string lined = "local CHANNEL = 1 -- channel\n" + "local OWNER_ONLY = true -- owner\n" + "-- a note over the next\n" + "local USE_OWNER_SAY = true -- say\n"; + ensure_equals("as it was", lua(lined), lined); + ensure_equals("spaced afresh, lined up again", lua("local a =1 -- one\nlocal bbb = {2,3} -- two\n"), + std::string("local a = 1 -- one\nlocal bbb = { 2, 3 } -- two\n")); + ensure_equals("the author's column", lua("local a = 1\nlocal bb = 2\n"), std::string("local a = 1\nlocal bb = 2\n")); + ensure_equals("past the longest", lua("t[i+1] = 1\nt[ii] = 2\n"), std::string("t[i + 1] = 1\nt[ii] = 2\n")); + ensure_equals("not by chance", lua("ab=1\nc = 2\n"), std::string("ab = 1\nc = 2\n")); + ensure_equals("not across a blank line", lua("local a = 1\n\nlocal bb = 2\n"), std::string("local a = 1\n\nlocal bb = 2\n")); + const std::string fields = "local t = {\n alpha = 1,\n b = 2,\n}\n"; + ensure_equals("a table's fields", lua(fields), fields); + ensure_equals("LSL, by tabs", lsl("integer a\t= 1;\ninteger bb\t= 2;\n"), std::string("integer a = 1;\ninteger bb = 2;\n")); + // The formatter's own, again: the same. + ensure_equals("again", lua(lua("local a =1 -- one\nlocal bbb = {2,3} -- two\n")), + std::string("local a = 1 -- one\nlocal bbb = { 2, 3 } -- two\n")); + } + template<> template<> void alscriptformatter_object::test<15>() { From 4fe80f72818410c4dfbdd8f1443a0364e4a59309 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 22:03:33 -0400 Subject: [PATCH 10/12] A comment after an unbraced body trails the statement it was after A comment at the end of the line an unbraced body ends was given to the wrong statement. After an if's `then` body it went over the `else`: `llDialog(...); // present submenu` came out on a line of its own before `elseif`, at the if's level, or as the first line of the else branch. After the last branch of an if, or a loop's body, it went over the whole if or loop, since a comment after one of them would trail its first line. Such a comment now trails the statement that ends its line, found down the if's last branch or the loop's body: `print("a") -- after a`. Where that is an empty body, `if (c); // why`, it trails the line that opens the body, `if c then -- why`. An if written on one line, with no else, still takes it after its `end`. allsltoslua 95. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 91 ++++++++++++++++++++--- indra/alscript/tests/allsltoslua_test.cpp | 34 +++++++++ 2 files changed, 114 insertions(+), 11 deletions(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index cb12c62f34..8653430683 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -455,6 +455,12 @@ namespace void findComments(); void place(size_t k, const std::vector& children, LSLASTNode* holder); bool placeInside(size_t k, LSLASTNode* node); + // A comment of one line after a bare statement, on the line it + // ends: given to the statement that ends that line (trailedBare), + // after it, or, where that is an empty body, after the line that + // opens it. Not where that is an if or a loop over lines, whose + // own line is its first. Whether it was. + bool trailBare(size_t k, LSLASTNode* statement, bool body); 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. Those @@ -1539,6 +1545,69 @@ namespace default: return true; } } + + // The statement that ends a bare one's last line: down an if's last + // branch, a while's or a for's body. Not into an if with no else on + // one line, which is written on one (Writer::onOneLine), but for an + // else's, which is an elseif. + LSLASTNode* trailedBare(LSLASTNode* node) + { + bool elseif = false; + while (!isNull(node) && node->getNodeType() == NODE_STATEMENT) + { + switch (node->getNodeSubType()) + { + case NODE_IF_STATEMENT: + { + LSLASTNode* no = node->getChild(2); + if (isNull(no) && !elseif && node->getLoc()->first_line == node->getLoc()->last_line) + { + return node; + } + elseif = !isNull(no) && no->getNodeSubType() == NODE_IF_STATEMENT; + node = isNull(no) ? node->getChild(1) : no; + continue; + } + case NODE_WHILE_STATEMENT: + elseif = false; + node = node->getChild(1); + continue; + case NODE_FOR_STATEMENT: + elseif = false; + node = node->getChild(3); + continue; + default: return node; + } + } + return node; + } + } + + bool Writer::trailBare(size_t k, LSLASTNode* statement, bool body) + { + const Pos at = mComments[k].at; + if (isNull(statement) || !endsBare(statement) || mComments[k].endLine != at.line || at.line != statement->getLoc()->last_line) + { + return false; + } + LSLASTNode* trailed = trailedBare(statement); + const LSLNodeSubType type = trailed->getNodeSubType(); + const bool control = type == NODE_IF_STATEMENT || type == NODE_WHILE_STATEMENT || type == NODE_FOR_STATEMENT || type == NODE_DO_STATEMENT; + if (control && trailed->getLoc()->first_line != trailed->getLoc()->last_line) + { + return false; + } + // An empty body writes no line of its own: the comment trails the + // one that opens it, `if c then -- why`. + if (type == NODE_NOP_STATEMENT && (body || trailed != statement)) + { + mCommentsOpening[trailed].push_back(k); + } + else + { + mCommentsAfter[trailed].push_back(k); + } + return true; } void Writer::place(size_t k, const std::vector& children, LSLASTNode* holder) @@ -1567,19 +1636,11 @@ namespace break; } // On the line a bare statement ends, after it: about that statement, - // and after it too, where it is one line of the comment's and the - // statement one line, or no if or loop, whose first line is not - // where the comment was. + // and after the one that ends the line (trailBare), or over it where + // it does not trail one. if (prev && endsBare(prev) && at.line == prev->getLoc()->last_line) { - const LSLNodeSubType type = prev->getNodeSubType(); - const bool control = type == NODE_IF_STATEMENT || type == NODE_WHILE_STATEMENT || type == NODE_FOR_STATEMENT || - type == NODE_DO_STATEMENT; - if (mComments[k].endLine == at.line && (!control || prev->getLoc()->first_line == prev->getLoc()->last_line)) - { - mCommentsAfter[prev].push_back(k); - } - else + if (!trailBare(k, prev, false)) { mCommentsBefore[prev].push_back(k); } @@ -1666,6 +1727,14 @@ namespace { return true; } + // On the line a bare branch ends, after it: after + // what ends that line, not over the else. + if (LSLASTNode* yes = node->getChild(1); !isNull(yes) && + !(at < Pos{ yes->getLoc()->last_line, yes->getLoc()->last_column }) && + trailBare(k, yes, true)) + { + return true; + } // Between the branches: over what the else runs -- an // elseif's line, or the else's first statement. LSLASTNode* no = node->getChild(2); diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 6ac5ca29bb..8b95e310b9 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -3059,4 +3059,38 @@ namespace tut ensure_equals(spaces ? "formatted, spaced" : "formatted, none", ALScriptFormatter::format(same.text, formatter), same.text); } } + + template<> template<> + void allsltoslua_object::test<95>() + { + set_test_name("a comment after an unbraced body, on the line it ends, trails the statement that ends that line: a then's, an " + "elseif's, an else's, a loop's; an empty body's the line that opens it; a one-line if's, the whole if"); + const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" + " integer a; integer b;\n" + " if (n == 1)\n" + " llOwnerSay(\"a\"); // after a\n" + " else if (n == 2)\n" + " llOwnerSay(\"b\"); // after b\n" + " else\n" + " llOwnerSay(\"c\"); // after c\n" + " if ( (b = 1) == 2 ); // empty\n" + " else if ( b == 2 ); // empty too\n" + " else a = 2; // after a = 2\n" + " if (a)\n" + " if (b) llOwnerSay(\"x\"); // inner, one line\n" + " while (n > 5)\n" + " n = n - 1; // after the step\n" + " while (a) ; // an empty loop\n" + " if (n == 5) llOwnerSay(\"h\"); // after h\n" + "} }\n"); + ensure("a then's, an elseif's, an else's: " + r.text, + has(r, " if n == 1 then\n print(\"a\") -- after a\n elseif n == 2 then\n print(\"b\") -- after b\n" + " else\n print(\"c\") -- after c\n end\n")); + ensure("an empty body's, the line that opens it: " + r.text, + has(r, " == 2 then -- empty\n elseif b == 2 then -- empty too\n else\n a = true -- after a = 2\n end\n")); + ensure("an if on one line inside another: " + r.text, has(r, " if b ~= 0 then print(\"x\") end -- inner, one line\n")); + ensure("a loop's body's: " + r.text, has(r, " n -= 1 -- after the step\n end\n") && has(r, " while a do -- an empty loop\n end\n")); + ensure("a one-line if, as before: " + r.text, has(r, " if n == 5 then print(\"h\") end -- after h\n")); + checksClean(r); + } } From b2253f94c0faa7de4dc39a88cb5c05a49e641bd4 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 22:04:17 -0400 Subject: [PATCH 11/12] Converted SLua is as format on save leaves it Two things were still changed when converted SLua was formatted. The text ended with a blank line, which the formatter drops. And the lslInteger and lslFloat helpers wrote `{ s }` whatever the brace setting asked, so with ALScriptFormatBraceSpaces off the formatter changed them to `{s}`. The text now ends with one line break, and the helpers' braces follow the setting. Over Tailslide's 73 test scripts, formatting the converted SLua now changes nothing, with either brace setting and with types on or off, but where the width rule breaks a line longer than the width. allsltoslua 89, which converts a script with what used to change and checks that formatting it changes nothing, either brace spacing. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 9 +++- indra/alscript/tests/allsltoslua_test.cpp | 54 +++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 8653430683..53c27cbe8c 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -7709,7 +7709,7 @@ end out += R"LUA(-- LSL's (integer) of a string: the whole number it starts with, in -- decimal or 0x hexadecimal, after any spaces; 0 where it starts with none. local function lslInteger(s: string): number - return llcompat.List2Integer({ s }, 0) + return llcompat.List2Integer()LUA" + braced("s") + R"LUA(, 0) end )LUA"; @@ -7719,7 +7719,7 @@ end out += R"LUA(-- LSL's (float) of a string: the number it starts with; 0 where it -- starts with none. local function lslFloat(s: string): number - return llcompat.List2Float({ s }, 0) + return llcompat.List2Float()LUA" + braced("s") + R"LUA(, 0) end )LUA"; @@ -7801,6 +7801,11 @@ end { writeComment(c); } + // Ending with one line break, as format on save leaves it. + while (mText.size() >= 2 && mText.compare(mText.size() - 2, 2, "\n\n") == 0) + { + mText.pop_back(); + } // Each of its lines a comment, whatever language the studio says // it in. const std::string head = mOptions.comments diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 8b95e310b9..957f6c4109 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -3060,6 +3060,60 @@ namespace tut } } + template<> template<> + void allsltoslua_object::test<89>() + { + set_test_name("converted SLua is as format on save leaves it: a comment after an unbraced body trails it, after the line that " + "opens it where the body is empty; what the LSL lined up, lined up; one line break at the end; either brace spacing"); + const std::string lsl = "integer CHANNEL = 1; // channel\n" + "integer OWNER_ONLY = 0; // owner\n" + "float TIMEOUT = 2.5; // seconds\n" + "integer asInt(string s) { return (integer)s; }\n" + "default {\n" + " touch_start(integer n) {\n" + " integer b;\n" + " if (n == 1)\n" + " llOwnerSay(\"a\"); // after a\n" + " else if (n == 2)\n" + " llOwnerSay(\"b\"); // after b\n" + " else\n" + " llOwnerSay(\"c\"); // after c\n" + " if ((b = 1) == 2); // an empty body\n" + " else llOwnerSay(\"f\"); // after f\n" + " while (n > 5)\n" + " n = n - 1; // after the step\n" + " if (n) {\n" + " llOwnerSay(\"d\");\n" + " }\n" + " // over the else\n" + " else {\n" + " llOwnerSay(\"e\");\n" + " }\n" + " do ; while ((b = b - 1) > 0);\n" + " llOwnerSay((string)asInt(\"3\") + (string)CHANNEL + (string)OWNER_ONLY + (string)TIMEOUT);\n" + " }\n" + "}\n"; + for (bool spaces : { true, false }) + { + ALLSLToSLua::Options options; + options.braceSpaces = spaces; + const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, options); + ensure("converted", r.converted); + ensure("each after its own: " + r.text, has(r, " print(\"a\") -- after a\n elseif") && has(r, " print(\"b\") -- after b\n else") && + has(r, " print(\"c\") -- after c\n end")); + ensure("an empty body's, after the line that opens it: " + r.text, has(r, " == 2 then -- an empty body\n else\n print(\"f\") -- after f\n")); + ensure("a loop's body's: " + r.text, has(r, " n -= 1 -- after the step\n end")); + ensure("between a block and its else, over what the else runs: " + r.text, has(r, " else\n -- over the else\n print(\"e\")\n")); + ensure("lined up: " + r.text, has(r, "local CHANNEL = 1 -- channel\nlocal OWNER_ONLY = 0 -- owner\nlocal TIMEOUT = 2.5 -- seconds\n")); + ensure("one line break at the end: " + r.text, r.text.size() > 2 && r.text.compare(r.text.size() - 2, 2, "\n\n") != 0 && r.text.back() == '\n'); + ALScriptFormatter::Options formatter; + formatter.lua = true; + formatter.braceSpaces = spaces; + ensure_equals(spaces ? "formatted, spaced" : "formatted, none", ALScriptFormatter::format(r.text, formatter), r.text); + checksClean(r); + } + } + template<> template<> void allsltoslua_object::test<95>() { From 268b39436c0f72f836da6b9357a5231551f5017b Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 02:32:08 -0400 Subject: [PATCH 12/12] A float cut to an integer is math.modf bare where one value is read anyway LSL's (integer) of a float is written as math.modf's whole part, always as (math.modf(x)). The brackets keep a call that answers two values to its first, which matters only where its values would spread: a call's last argument, a table's last item, a return. As an operand they did nothing, so `(integer)llFrand(1000) - 10000` came out as `(math.modf(ll.Frand(1000))) - 10000`. The call is now bare where only its first value is read anyway: an operator's operand, a compound assignment's value, and all of the value given a variable or a local, `x = math.modf(f) - 10000`. The brackets stay where the values would spread, including a vector's part assigned, which is made an argument of vector(). allsltoslua 96. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 41 ++++++++++++++++------- indra/alscript/tests/allsltoslua_test.cpp | 33 ++++++++++++++++++ 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 53c27cbe8c..d90b8fc889 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -184,15 +184,26 @@ namespace // An expression as written: its text, how tightly it binds, and whether // it is a Luau boolean where LSL has 1 or 0. Where it is a find's // place counted as LSL's, `(x or 0) - 1`: x, SLua's place, counted - // from 1 or nil. + // from 1 or nil. Where it is a call that answers more than one value, + // kept to its first by brackets, `(math.modf(x))`: the call bare. struct Expr { std::string text; int prec = PRIMARY; bool boolean = false; std::string place; + std::string bare; }; + // As one value, where only its first is read anyway -- an operator's + // operand, the one value assigned -- a call without the brackets that + // keep it to one. Where its values would spread -- a call's last + // argument, a table's last item, a return -- the brackets stay. + Expr oneValue(const Expr& e) + { + return e.bare.empty() ? e : Expr{ e.bare, PRIMARY, e.boolean }; + } + // Pairs of words, looked for by views of them without a copy. struct PairLess { @@ -3728,8 +3739,8 @@ namespace const LSLIType lt = slType(lhs); const LSLIType rt = slType(rhs); const auto infix = [&](const char* word, int prec, bool boolean = false, bool right_assoc = false) -> Expr { - const Expr a = value(lhs); - const Expr b = value(rhs); + const Expr a = oneValue(value(lhs)); + const Expr b = oneValue(value(rhs)); if (prec == COMPARE && boolean) { if (std::optional compared = placeCompared(word, a, b, lhs, rhs)) @@ -3981,7 +3992,7 @@ namespace { // Bracketed where it begins with a minus of its own, which // after this one would be a comment. - const Expr v = value(child); + const Expr v = oneValue(value(child)); return { "-" + (!v.text.empty() && v.text.front() == '-' ? "(" + v.text + ")" : bracketed(v, UNARY)), UNARY }; } case OP_BOOLEAN_NOT: @@ -4050,8 +4061,9 @@ namespace case LST_INTEGER: if (from == LST_FLOATINGPOINT) { - // Toward zero, as LSL cuts a float. - return { "(math.modf(" + v.text + "))" }; + // Toward zero, as LSL cuts a float: math.modf's first + // value, its whole part. + return { "(math.modf(" + v.text + "))", PRIMARY, false, {}, "math.modf(" + v.text + ")" }; } if (from == LST_STRING) { @@ -4303,11 +4315,12 @@ namespace LSLASTNode* member = target->getMember(); // What the new value is, from the old one where the operator makes // it so. - const auto made = [&](const std::string& old, LSLIType t) -> std::string { + // `whole` where it is all of the value assigned, not a part of one. + const auto made = [&](const std::string& old, LSLIType t, bool whole = false) -> std::string { const Expr v = rhs ? value(rhs) : Expr{ "1" }; switch (op) { - case OP_ASSIGN: return rhs ? coerced(rhs, t).text : v.text; + case OP_ASSIGN: return rhs ? (whole ? oneValue(coerced(rhs, t)) : coerced(rhs, t)).text : v.text; case OP_ADD_ASSIGN: case OP_PRE_INCR: case OP_POST_INCR: @@ -4437,7 +4450,9 @@ namespace line(name + " ..= " + joinedPiece(rhs).text); return; } - const Expr v = rhs ? value(rhs) : Expr{ "1" }; + // One value, but where it is math.fmod's last argument. + const Expr whole = rhs ? value(rhs) : Expr{ "1" }; + const Expr v = oneValue(whole); switch (op) { case OP_ADD_ASSIGN: @@ -4463,7 +4478,7 @@ namespace case OP_MOD_ASSIGN: if (type == LST_INTEGER) { - line(name + " = math.fmod(" + name + ", " + v.text + ")"); + line(name + " = math.fmod(" + name + ", " + whole.text + ")"); return; } break; @@ -4475,7 +4490,7 @@ namespace line(name + " = " + truthOf(rhs)); return; } - line(name + " = " + made(name, type)); + line(name + " = " + made(name, type, true)); } void Writer::effect(LSLExpression* e) @@ -4912,7 +4927,7 @@ namespace } else { - line("local " + name + typed(type) + " = " + (isNull(init) ? defaultOf(type) : coerced(init, type).text)); + line("local " + name + typed(type) + " = " + (isNull(init) ? defaultOf(type) : oneValue(coerced(init, type)).text)); } writeSteps(steps.after); return; @@ -7277,7 +7292,7 @@ namespace else { const LSLIType type = varType(id->getSymbol(), id->getIType()); - line("local " + nameOf(id) + typed(type) + " = " + (isNull(init) ? defaultOf(type) : coerced(init, type).text)); + line("local " + nameOf(id) + typed(type) + " = " + (isNull(init) ? defaultOf(type) : oneValue(coerced(init, type)).text)); } commentsAfter(global, from); lined(global, from); diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 957f6c4109..066e7955b7 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -3147,4 +3147,37 @@ namespace tut ensure("a one-line if, as before: " + r.text, has(r, " if n == 5 then print(\"h\") end -- after h\n")); checksClean(r); } + + template<> template<> + void allsltoslua_object::test<96>() + { + set_test_name("a float cut to an integer is math.modf's first value: bare where only its first is read anyway -- an operand, all " + "of a value assigned -- and kept to it by brackets where its values would spread"); + const ALLSLToSLua::Result r = convert("integer menuChannel;\n" + "vector gV;\n" + "integer cut(float f) { return (integer)f; }\n" + "default { touch_start(integer n) {\n" + " float f = llFrand(10);\n" + " menuChannel = (integer)llFrand(1000) - 10000;\n" + " integer x = (integer)f;\n" + " x = (integer)f;\n" + " x += (integer)f;\n" + " x = -(integer)f;\n" + " if ((integer)f == 3) llOwnerSay((string)((integer)f));\n" + " llOwnerSay(llList2CSV([(integer)f]));\n" + " gV.z = (integer)f;\n" + " llOwnerSay((string)(cut(f) + x + menuChannel) + (string)gV);\n" + "} }\n"); + ensure("Tapple's: " + r.text, has(r, "menuChannel = math.modf(ll.Frand(1000)) - 10000\n")); + ensure("a local: " + r.text, has(r, "local x = math.modf(f)\n")); + ensure("assigned: " + r.text, has(r, " x = math.modf(f)\n")); + ensure("added: " + r.text, has(r, " x += math.modf(f)\n")); + ensure("its negative: " + r.text, has(r, " x = -math.modf(f)\n")); + ensure("compared: " + r.text, has(r, "if math.modf(f) == 3 then")); + ensure("an argument: " + r.text, has(r, "print(tostring((math.modf(f))))")); + ensure("a list's item: " + r.text, has(r, "{ (math.modf(f)) }")); + ensure("returned: " + r.text, has(r, "return (math.modf(f))")); + ensure("a vector's part, an argument: " + r.text, has(r, "gV = vector(gV.x, gV.y, (math.modf(f)))")); + checksClean(r); + } }