From d5c96631562621b0f649f43c83bb969e87ef27b5 Mon Sep 17 00:00:00 2001 From: bjcscat Date: Tue, 26 Aug 2025 18:27:18 -0700 Subject: [PATCH 001/642] Refactor base64 battery lib (#365) Uses a different LUT method to provide a general speed up for encoding operations --- batteries/base64.luau | 235 ++++++++++++++++-------------------------- 1 file changed, 89 insertions(+), 146 deletions(-) diff --git a/batteries/base64.luau b/batteries/base64.luau index b582b764f..3dbca38d8 100644 --- a/batteries/base64.luau +++ b/batteries/base64.luau @@ -1,141 +1,105 @@ ---[[ - Pulled from https://github.com/Reselim/Base64 - - Copyright (c) 2020 Reselim - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -]] - --!native --!optimize 2 +--!strict + +local BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +local BASE64_ENCODING_LUT = table.create(4096) +local BASE64_DECODING_LUT = table.create(255, 0) + +do + for i = 0, 4095 do + local hi = bit32.rshift(i, 6) + 1 + local lo = bit32.band(i, 0x3F) + 1 + BASE64_ENCODING_LUT[i + 1] = + bit32.bor(string.byte(BASE64_ALPHABET, hi), bit32.lshift(string.byte(BASE64_ALPHABET, lo), 8)) + end -local lookupValueToCharacter = buffer.create(64) -local lookupCharacterToValue = buffer.create(256) + for i = 1, #BASE64_ALPHABET do + BASE64_DECODING_LUT[string.byte(BASE64_ALPHABET, i)] = i - 1 + end +end -local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -local padding = string.byte("=") +local function encode(input_buffer: buffer): buffer + assert(typeof(input_buffer) == "buffer", "Expected input to be a buffer") -for index = 1, 64 do - local value = index - 1 - local character = string.byte(alphabet, index) + local input_length = buffer.len(input_buffer) - buffer.writeu8(lookupValueToCharacter, value, character) - buffer.writeu8(lookupCharacterToValue, character, value) -end + if input_length == 0 then + return buffer.create(0) + end -local function encode(input: buffer): buffer - local inputLength = buffer.len(input) - local inputChunks = math.ceil(inputLength / 3) + local output = buffer.create(((input_length + 2) // 3) * 4) + local output_idx = 0 + local i = 0 - local outputLength = inputChunks * 4 - local output = buffer.create(outputLength) + while i + 3 <= input_length do + local triple = bit32.bor( + bit32.lshift(buffer.readu8(input_buffer, i), 16), + bit32.lshift(buffer.readu8(input_buffer, i + 1), 8), + buffer.readu8(input_buffer, i + 2) + ) - -- Since we use readu32 and chunks are 3 bytes large, we can't read the last chunk here - for chunkIndex = 1, inputChunks - 1 do - local inputIndex = (chunkIndex - 1) * 3 - local outputIndex = (chunkIndex - 1) * 4 + local high = bit32.band(bit32.rshift(triple, 12), 0xFFF) + 1 + local low = bit32.band(triple, 0xFFF) + 1 - local chunk = bit32.byteswap(buffer.readu32(input, inputIndex)) + local pair0 = BASE64_ENCODING_LUT[high] + local pair1 = BASE64_ENCODING_LUT[low] - -- 8 + 24 - (6 * index) - local value1 = bit32.rshift(chunk, 26) - local value2 = bit32.band(bit32.rshift(chunk, 20), 0b111111) - local value3 = bit32.band(bit32.rshift(chunk, 14), 0b111111) - local value4 = bit32.band(bit32.rshift(chunk, 8), 0b111111) + buffer.writeu32(output, output_idx, bit32.bor(pair0, bit32.lshift(pair1, 16))) - buffer.writeu8(output, outputIndex, buffer.readu8(lookupValueToCharacter, value1)) - buffer.writeu8(output, outputIndex + 1, buffer.readu8(lookupValueToCharacter, value2)) - buffer.writeu8(output, outputIndex + 2, buffer.readu8(lookupValueToCharacter, value3)) - buffer.writeu8(output, outputIndex + 3, buffer.readu8(lookupValueToCharacter, value4)) + i += 3 + output_idx += 4 end - local inputRemainder = inputLength % 3 - - if inputRemainder == 1 then - local chunk = buffer.readu8(input, inputLength - 1) - - local value1 = bit32.rshift(chunk, 2) - local value2 = bit32.band(bit32.lshift(chunk, 4), 0b111111) - - buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) - buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) - buffer.writeu8(output, outputLength - 2, padding) - buffer.writeu8(output, outputLength - 1, padding) - elseif inputRemainder == 2 then - local chunk = - bit32.bor(bit32.lshift(buffer.readu8(input, inputLength - 2), 8), buffer.readu8(input, inputLength - 1)) - - local value1 = bit32.rshift(chunk, 10) - local value2 = bit32.band(bit32.rshift(chunk, 4), 0b111111) - local value3 = bit32.band(bit32.lshift(chunk, 2), 0b111111) - - buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) - buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) - buffer.writeu8(output, outputLength - 2, buffer.readu8(lookupValueToCharacter, value3)) - buffer.writeu8(output, outputLength - 1, padding) - elseif inputRemainder == 0 and inputLength ~= 0 then - local chunk = bit32.bor( - bit32.lshift(buffer.readu8(input, inputLength - 3), 16), - bit32.lshift(buffer.readu8(input, inputLength - 2), 8), - buffer.readu8(input, inputLength - 1) - ) + local rem = input_length - i + + if rem == 1 then + local high = bit32.band(bit32.lshift(buffer.readu8(input_buffer, i), 4), 0xFF0) - local value1 = bit32.rshift(chunk, 18) - local value2 = bit32.band(bit32.rshift(chunk, 12), 0b111111) - local value3 = bit32.band(bit32.rshift(chunk, 6), 0b111111) - local value4 = bit32.band(chunk, 0b111111) + local TWO_EQUALS = 0x3D3D + buffer.writeu32(output, output_idx, bit32.bor(BASE64_ENCODING_LUT[high + 1], bit32.lshift(TWO_EQUALS, 16))) + elseif rem == 2 then + local first = buffer.readu8(input_buffer, i) + local second = buffer.readu8(input_buffer, i + 1) + local high = bit32.bor(bit32.lshift(first, 4), bit32.rshift(second, 4)) + local low_idx = bit32.lshift(bit32.band(second, 0x0F), 2) + local low_equals = bit32.bor(string.byte(BASE64_ALPHABET, low_idx + 1), bit32.lshift(0x3D, 8)) - buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) - buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) - buffer.writeu8(output, outputLength - 2, buffer.readu8(lookupValueToCharacter, value3)) - buffer.writeu8(output, outputLength - 1, buffer.readu8(lookupValueToCharacter, value4)) + buffer.writeu32(output, output_idx, bit32.bor(BASE64_ENCODING_LUT[high + 1], bit32.lshift(low_equals, 16))) end return output end -local function decode(input: buffer): buffer - local inputLength = buffer.len(input) - local inputChunks = math.ceil(inputLength / 4) +local function decode(input_buffer: buffer): buffer + assert(typeof(input_buffer) == "buffer", "Expected input to be a buffer") - -- TODO: Support input without padding - local inputPadding = 0 - if inputLength ~= 0 then - if buffer.readu8(input, inputLength - 1) == padding then - inputPadding += 1 - end - if buffer.readu8(input, inputLength - 2) == padding then - inputPadding += 1 - end + local input_length = buffer.len(input_buffer) + + if input_length == 0 then + return buffer.create(0) + end + + local padding_size = 0 + if input_length >= 2 and buffer.readu16(input_buffer, input_length - 2) == 0x3D3D then + padding_size = 2 + elseif input_length >= 1 and buffer.readu8(input_buffer, input_length - 1) == 0x3D then + padding_size = 1 end - local outputLength = inputChunks * 3 - inputPadding - local output = buffer.create(outputLength) + -- get correct output size + local output_length = ((input_length / 4) * 3) - padding_size + local output = buffer.create(output_length) + local chunks = input_length // 4 - for chunkIndex = 1, inputChunks - 1 do - local inputIndex = (chunkIndex - 1) * 4 - local outputIndex = (chunkIndex - 1) * 3 + for chunk_idx = 1, chunks do + local index = (chunk_idx - 1) * 4 + local out_index = (chunk_idx - 1) * 3 - local value1 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex)) - local value2 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 1)) - local value3 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 2)) - local value4 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 3)) + local value1 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index)] + local value2 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index + 1)] + local value3 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index + 2)] + local value4 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index + 3)] local chunk = bit32.bor(bit32.lshift(value1, 18), bit32.lshift(value2, 12), bit32.lshift(value3, 6), value4) @@ -143,47 +107,26 @@ local function decode(input: buffer): buffer local character2 = bit32.band(bit32.rshift(chunk, 8), 0b11111111) local character3 = bit32.band(chunk, 0b11111111) - buffer.writeu8(output, outputIndex, character1) - buffer.writeu8(output, outputIndex + 1, character2) - buffer.writeu8(output, outputIndex + 2, character3) - end - - if inputLength ~= 0 then - local lastInputIndex = (inputChunks - 1) * 4 - local lastOutputIndex = (inputChunks - 1) * 3 - - local lastValue1 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex)) - local lastValue2 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 1)) - local lastValue3 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 2)) - local lastValue4 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 3)) - - local lastChunk = bit32.bor( - bit32.lshift(lastValue1, 18), - bit32.lshift(lastValue2, 12), - bit32.lshift(lastValue3, 6), - lastValue4 - ) - - if inputPadding <= 2 then - local lastCharacter1 = bit32.rshift(lastChunk, 16) - buffer.writeu8(output, lastOutputIndex, lastCharacter1) + -- always write the first byte + if out_index < output_length then + buffer.writeu8(output, out_index, character1) + end - if inputPadding <= 1 then - local lastCharacter2 = bit32.band(bit32.rshift(lastChunk, 8), 0b11111111) - buffer.writeu8(output, lastOutputIndex + 1, lastCharacter2) + -- write second byte if have space (+padding) + if out_index + 1 < output_length then + buffer.writeu8(output, out_index + 1, character2) + end - if inputPadding == 0 then - local lastCharacter3 = bit32.band(lastChunk, 0b11111111) - buffer.writeu8(output, lastOutputIndex + 2, lastCharacter3) - end - end + -- Write third byte if we have space (+padding) + if out_index + 2 < output_length then + buffer.writeu8(output, out_index + 2, character3) end end return output end -return { +return table.freeze({ encode = encode, decode = decode, -} +}) From 2de18011011584de19a0d543587f68657f13115e Mon Sep 17 00:00:00 2001 From: alexis <71101894+alexzinyew@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:00:12 -0400 Subject: [PATCH 002/642] Add lute setup to cli help (#368) The setup options text might be redundant --- lute/cli/src/climain.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index f190f69ed..909a38d34 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -150,6 +150,7 @@ static void displayHelp(const char* argv0) printf(" run (default) Run a Luau script.\n"); printf(" check Type check Luau files.\n"); printf(" compile Compile a Luau script into the executable.\n"); + printf(" setup Generate type definition files for the language server."); printf("\n"); printf("Run Options (when using 'run' or no command):\n"); printf(" lute [run] [args...]\n"); @@ -163,6 +164,10 @@ static void displayHelp(const char* argv0) printf(" lute compile [output_executable]\n"); printf(" Compiles the script, embedding it into a new executable.\n"); printf("\n"); + printf("Setup Options:\n"); + printf(" lute setup"); + printf(" Generates type definition files for the language server.\n"); + printf("\n"); printf("General Options:\n"); printf(" -h, --help Display this usage message.\n"); } From ab41047266f8058f314d18834812a9061cf162c2 Mon Sep 17 00:00:00 2001 From: checkraisefold Date: Mon, 1 Sep 2025 16:00:41 -0700 Subject: [PATCH 003/642] Fix luau.load type signature (#366) fifteen billionth iteration of fixing this later, these should be optional --- definitions/luau.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 2318ae9f6..cf1a8036e 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -570,7 +570,7 @@ function luau.compile(source: string): Bytecode error("not implemented") end -function luau.load(bytecode: Bytecode, chunkname: string, env: { [any]: any }): (...any) -> ...any +function luau.load(bytecode: Bytecode, chunkname: string?, env: { [any]: any }?): (...any) -> ...any error("not implemented") end From c489e33cf7a595a4dd04c05281b00dadb2e01ebe Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:05:56 -0400 Subject: [PATCH 004/642] Add printStatement func to printer (#361) There are print cases not covered by `printBlock` and `printExpr`, so I figure this is useful to have If anything, we can replace `printBlock` with this function and export it as `print` instead, [since`visitStatement` encompasses blocks](https://github.com/luau-lang/lute/blob/primary/lute/std/libs/syntax/visitor.luau#L796), in addition to many statement types --------- Co-authored-by: ariel --- lute/std/libs/syntax/printer.luau | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index ada74933e..90840268f 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -123,17 +123,17 @@ local function printVisitor() return printer end ---- Returns a string representation of an AstStatBlock -local function printBlock(block: luau.AstStatBlock): string +--- Returns a string representation of an AstExpr +function printExpr(block: luau.AstExpr): string local printer = printVisitor() - visitor.visitBlock(block, printer) + visitor.visitExpression(block, printer) return buffer.readstring(printer.result, 0, printer.cursor) end ---- Returns a string representation of an AstExpr -function printExpr(block: luau.AstExpr): string +--- Returns a string representation of an AstStat +local function printStatement(statement: luau.AstStat): string local printer = printVisitor() - visitor.visitExpression(block, printer) + visitor.visitStatement(statement, printer) return buffer.readstring(printer.result, 0, printer.cursor) end @@ -145,7 +145,7 @@ function printFile(result: { root: luau.AstStatBlock, eof: luau.Eof }): string end return { - print = printBlock, printexpr = printExpr, + printstatement = printStatement, printfile = printFile, } From 99bd39409b3ebf878a2d6e2ad2c1fb19c6b17d52 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:18:36 -0700 Subject: [PATCH 005/642] RequireVfs: Remove all `default` blocks in favor of being explicit (#371) When first implementing this, I didn't realize that the compiler checks for exhaustivity when switching over an enum. This is an improvement that helps us catch errors at compile-time by being more explicit. --- lute/require/src/requirevfs.cpp | 87 ++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 83b3e0cc0..921bc1692 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -63,18 +63,23 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) return NavigationStatus::Success; } + NavigationStatus status = NavigationStatus::NotFound; switch (vfsType) { case VFSType::Disk: - return fileVfs.resetToPath(std::string(path)); + status = fileVfs.resetToPath(std::string(path)); + break; case VFSType::Std: - return stdLibVfs.resetToPath(std::string(path)); + status = stdLibVfs.resetToPath(std::string(path)); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - return cliVfs->resetToPath(std::string(path)); - default: - return NavigationStatus::NotFound; + status = cliVfs->resetToPath(std::string(path)); + break; + case VFSType::Lute: + break; } + return status; } NavigationStatus RequireVfs::toParent(lua_State* L) @@ -96,8 +101,6 @@ NavigationStatus RequireVfs::toParent(lua_State* L) case VFSType::Lute: luaL_error(L, "cannot get the parent of @lute"); break; - default: - return NavigationStatus::NotFound; } if (status == NavigationStatus::NotFound) @@ -128,8 +131,6 @@ NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) case VFSType::Lute: luaL_error(L, "'%s' is not a lute library", std::string(name).c_str()); break; - default: - break; } return NavigationStatus::NotFound; @@ -149,8 +150,6 @@ bool RequireVfs::isModulePresent(lua_State* L) const case VFSType::Lute: luaL_error(L, "@lute is not requirable"); break; - default: - break; } return false; @@ -172,7 +171,7 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c LUAU_ASSERT(cliVfs); contents = cliVfs->getContents(loadname); break; - default: + case VFSType::Lute: break; } @@ -181,50 +180,65 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c std::string RequireVfs::getChunkname(lua_State* L) const { + std::string chunkname; switch (vfsType) { case VFSType::Disk: - return "@" + fileVfs.getFilePath(); + chunkname = "@" + fileVfs.getFilePath(); + break; case VFSType::Std: - return "@" + stdLibVfs.getIdentifier(); + chunkname = "@" + stdLibVfs.getIdentifier(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - return "@" + cliVfs->getIdentifier(); - default: - return ""; + chunkname = "@" + cliVfs->getIdentifier(); + break; + case VFSType::Lute: + break; } + return chunkname; } std::string RequireVfs::getLoadname(lua_State* L) const { + std::string loadname; switch (vfsType) { case VFSType::Disk: - return fileVfs.getAbsoluteFilePath(); + loadname = fileVfs.getAbsoluteFilePath(); + break; case VFSType::Std: - return stdLibVfs.getIdentifier(); + loadname = stdLibVfs.getIdentifier(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - return cliVfs->getIdentifier(); - default: - return ""; + loadname = cliVfs->getIdentifier(); + break; + case VFSType::Lute: + break; } + return loadname; } std::string RequireVfs::getCacheKey(lua_State* L) const { + std::string cacheKey; switch (vfsType) { case VFSType::Disk: - return fileVfs.getAbsoluteFilePath(); + cacheKey = fileVfs.getAbsoluteFilePath(); + break; case VFSType::Std: - return stdLibVfs.getIdentifier(); + cacheKey = stdLibVfs.getIdentifier(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - return cliVfs->getIdentifier(); - default: - return ""; + cacheKey = cliVfs->getIdentifier(); + break; + case VFSType::Lute: + break; } + return cacheKey; } bool RequireVfs::isConfigPresent(lua_State* L) const @@ -232,18 +246,23 @@ bool RequireVfs::isConfigPresent(lua_State* L) const if (atFakeRoot) return true; + bool isPresent = false; switch (vfsType) { case VFSType::Disk: - return fileVfs.isConfigPresent(); + isPresent = fileVfs.isConfigPresent(); + break; case VFSType::Std: - return stdLibVfs.isConfigPresent(); + isPresent = stdLibVfs.isConfigPresent(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - return cliVfs->isConfigPresent(); - default: - return false; + isPresent = cliVfs->isConfigPresent(); + break; + case VFSType::Lute: + break; } + return isPresent; } std::string RequireVfs::getConfig(lua_State* L) const @@ -260,7 +279,6 @@ std::string RequireVfs::getConfig(lua_State* L) const } std::optional configContents; - switch (vfsType) { case VFSType::Disk: @@ -273,9 +291,8 @@ std::string RequireVfs::getConfig(lua_State* L) const LUAU_ASSERT(cliVfs); configContents = cliVfs->getConfig(); break; - default: + case VFSType::Lute: break; } - return configContents ? *configContents : ""; } From a479ae725c9ce01a79553ab2d5a5b0d074eae780 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:54:17 -0700 Subject: [PATCH 006/642] Allow required modules to return string and numeric values (#373) Based on this PR in the Luau repository: https://github.com/luau-lang/luau/pull/1967. I also spent a while trying to get newlines in error messages to play nicely, but we still have some issues that we'll need to address later. Main tension is that `lua_debugtrace` automatically puts a newline at the end of the trace, but a "regular" error on a thread is usually just a string with no newline. --- definitions/luau.luau | 90 ++++++++++--------- lute/require/src/require.cpp | 11 +-- tests/src/require.test.cpp | 14 ++- .../require/without_config/types/boolean.luau | 1 + .../require/without_config/types/buffer.luau | 1 + .../without_config/types/function.luau | 3 + .../src/require/without_config/types/nil.luau | 1 + .../require/without_config/types/number.luau | 1 + .../require/without_config/types/string.luau | 1 + .../require/without_config/types/table.luau | 1 + .../require/without_config/types/tester.luau | 10 +++ .../require/without_config/types/thread.luau | 3 + .../without_config/types/userdata.luau | 1 + .../require/without_config/types/vector.luau | 1 + 14 files changed, 91 insertions(+), 48 deletions(-) create mode 100644 tests/src/require/without_config/types/boolean.luau create mode 100644 tests/src/require/without_config/types/buffer.luau create mode 100644 tests/src/require/without_config/types/function.luau create mode 100644 tests/src/require/without_config/types/nil.luau create mode 100644 tests/src/require/without_config/types/number.luau create mode 100644 tests/src/require/without_config/types/string.luau create mode 100644 tests/src/require/without_config/types/table.luau create mode 100644 tests/src/require/without_config/types/tester.luau create mode 100644 tests/src/require/without_config/types/thread.luau create mode 100644 tests/src/require/without_config/types/userdata.luau create mode 100644 tests/src/require/without_config/types/vector.luau diff --git a/definitions/luau.luau b/definitions/luau.luau index cf1a8036e..78950109c 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -134,21 +134,24 @@ export type AstExprAnonymousFunction = { body: AstFunctionBody, } -export type AstExprTableItem = | { kind: "list", value: AstExpr, separator: Token<"," | ";">? } | { - kind: "record", - key: Token, - equals: Token<"=">, - value: AstExpr, - separator: Token<"," | ";">?, -} | { - kind: "general", - indexerOpen: Token<"[">, - key: AstExpr, - indexerClose: Token<"]">, - equals: Token<"=">, - value: AstExpr, - separator: Token<"," | ";">?, -} +export type AstExprTableItem = + | { kind: "list", value: AstExpr, separator: Token<"," | ";">? } + | { + kind: "record", + key: Token, + equals: Token<"=">, + value: AstExpr, + separator: Token<"," | ";">?, + } + | { + kind: "general", + indexerOpen: Token<"[">, + key: AstExpr, + indexerClose: Token<"]">, + equals: Token<"=">, + value: AstExpr, + separator: Token<"," | ";">?, + } export type AstExprTable = { tag: "table", @@ -458,33 +461,36 @@ export type AstTypeArray = { closeBrace: Token<"}">, } -export type AstTypeTableItem = { - kind: "indexer", - access: Token<"read" | "write">?, - indexerOpen: Token<"[">, - key: AstType, - indexerClose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, -} | { - kind: "stringproperty", - access: Token<"read" | "write">?, - indexerOpen: Token<"[">, - key: AstTypeSingletonString, - indexerClose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, -} | { - kind: "property", - access: Token<"read" | "write">?, - key: AstTypeSingletonString, - indexerClose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, -} +export type AstTypeTableItem = + { + kind: "indexer", + access: Token<"read" | "write">?, + indexerOpen: Token<"[">, + key: AstType, + indexerClose: Token<"]">, + colon: Token<":">, + value: AstType, + separator: Token<"," | ";">?, + } + | { + kind: "stringproperty", + access: Token<"read" | "write">?, + indexerOpen: Token<"[">, + key: AstTypeSingletonString, + indexerClose: Token<"]">, + colon: Token<":">, + value: AstType, + separator: Token<"," | ";">?, + } + | { + kind: "property", + access: Token<"read" | "write">?, + key: AstTypeSingletonString, + indexerClose: Token<"]">, + colon: Token<":">, + value: AstType, + separator: Token<"," | ";">?, + } export type AstTypeTable = { tag: "table", diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index 35e5fd3a4..6c9938673 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -126,6 +126,7 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname // now we can compile & run module on the new thread std::string bytecode = Luau::compile(*contents, copts()); + bool errored = true; if (luau_load(ML, chunkname, bytecode.data(), bytecode.size(), 0) == 0) { if (getCodegenEnabled()) @@ -138,10 +139,10 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname if (status == 0) { - const std::string prefix = "module " + std::string(path) + " must"; - - if (lua_gettop(ML) == 0) - lua_pushstring(ML, (prefix + " return a value, if it has no return value, you should explicitly return `nil`\n").c_str()); + if (lua_gettop(ML) == 1) + errored = false; + else + lua_pushfstring(ML, "module %s must return a single value, if it has no return value, you should explicitly return `nil`\n", path); } else if (status == LUA_YIELD) { @@ -155,7 +156,7 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname // add ML result to L stack lua_xmove(ML, L, 1); - if (lua_isstring(L, -1)) + if (errored && lua_isstring(L, -1)) { lua_pushstring(L, lua_debugtrace(ML)); lua_concat(L, 2); diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index 16d3af0ea..72f13a958 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -86,7 +86,7 @@ TEST_CASE("require_modules") { doPassingSubcase(argv, {"./without_config/luau"}, {"result from init.luau"}); } - + SUBCASE("init_lua") { doPassingSubcase(argv, {"./without_config/lua"}, {"result from init.lua"}); @@ -151,3 +151,15 @@ TEST_CASE("require_modules") } } } + +TEST_CASE("require_types") +{ + char executablePlaceholder[] = "lute"; + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string requirer = joinPaths(luteProjectRoot, "tests/src/require/without_config/types/tester.luau"); + std::vector argv = {executablePlaceholder, requirer.data()}; + + CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + } +} diff --git a/tests/src/require/without_config/types/boolean.luau b/tests/src/require/without_config/types/boolean.luau new file mode 100644 index 000000000..585a20e04 --- /dev/null +++ b/tests/src/require/without_config/types/boolean.luau @@ -0,0 +1 @@ +return false diff --git a/tests/src/require/without_config/types/buffer.luau b/tests/src/require/without_config/types/buffer.luau new file mode 100644 index 000000000..47869c5e5 --- /dev/null +++ b/tests/src/require/without_config/types/buffer.luau @@ -0,0 +1 @@ +return buffer.create(16) diff --git a/tests/src/require/without_config/types/function.luau b/tests/src/require/without_config/types/function.luau new file mode 100644 index 000000000..599ceec9d --- /dev/null +++ b/tests/src/require/without_config/types/function.luau @@ -0,0 +1,3 @@ +return function() + return 1 + 1 +end diff --git a/tests/src/require/without_config/types/nil.luau b/tests/src/require/without_config/types/nil.luau new file mode 100644 index 000000000..15e53c4a8 --- /dev/null +++ b/tests/src/require/without_config/types/nil.luau @@ -0,0 +1 @@ +return nil diff --git a/tests/src/require/without_config/types/number.luau b/tests/src/require/without_config/types/number.luau new file mode 100644 index 000000000..eadc91cf6 --- /dev/null +++ b/tests/src/require/without_config/types/number.luau @@ -0,0 +1 @@ +return 12345 diff --git a/tests/src/require/without_config/types/string.luau b/tests/src/require/without_config/types/string.luau new file mode 100644 index 000000000..bc39fbe82 --- /dev/null +++ b/tests/src/require/without_config/types/string.luau @@ -0,0 +1 @@ +return "foo" diff --git a/tests/src/require/without_config/types/table.luau b/tests/src/require/without_config/types/table.luau new file mode 100644 index 000000000..00aef0c9e --- /dev/null +++ b/tests/src/require/without_config/types/table.luau @@ -0,0 +1 @@ +return { "foo", "bar" } diff --git a/tests/src/require/without_config/types/tester.luau b/tests/src/require/without_config/types/tester.luau new file mode 100644 index 000000000..d51b333d6 --- /dev/null +++ b/tests/src/require/without_config/types/tester.luau @@ -0,0 +1,10 @@ +local _ = require("./boolean") +local _ = require("./buffer") +local _ = require("./function") +local _ = require("./nil") +local _ = require("./number") +local _ = require("./string") +local _ = require("./table") +local _ = require("./thread") +local _ = require("./userdata") +local _ = require("./vector") diff --git a/tests/src/require/without_config/types/thread.luau b/tests/src/require/without_config/types/thread.luau new file mode 100644 index 000000000..1661e039c --- /dev/null +++ b/tests/src/require/without_config/types/thread.luau @@ -0,0 +1,3 @@ +return coroutine.create(function() + return "foo" +end) diff --git a/tests/src/require/without_config/types/userdata.luau b/tests/src/require/without_config/types/userdata.luau new file mode 100644 index 000000000..a11620833 --- /dev/null +++ b/tests/src/require/without_config/types/userdata.luau @@ -0,0 +1 @@ +return newproxy() diff --git a/tests/src/require/without_config/types/vector.luau b/tests/src/require/without_config/types/vector.luau new file mode 100644 index 000000000..14210acb7 --- /dev/null +++ b/tests/src/require/without_config/types/vector.luau @@ -0,0 +1 @@ +return vector.create(1, 2, 3) From 774eba75caea4e21c4d254784f2762d2b415ecc1 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:35:52 -0400 Subject: [PATCH 007/642] Add printType function (#377) --- lute/std/libs/syntax/printer.luau | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 90840268f..f34ba3534 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -123,18 +123,25 @@ local function printVisitor() return printer end ---- Returns a string representation of an AstExpr -function printExpr(block: luau.AstExpr): string +function printNode(node: T, visit: (node: T, visitor: visitor.Visitor) -> ()): string local printer = printVisitor() - visitor.visitExpression(block, printer) + visit(node, printer) return buffer.readstring(printer.result, 0, printer.cursor) end +--- Returns a string representation of an AstExpr +function printExpr(expr: luau.AstExpr): string + return printNode(expr, visitor.visitExpression) +end + --- Returns a string representation of an AstStat local function printStatement(statement: luau.AstStat): string - local printer = printVisitor() - visitor.visitStatement(statement, printer) - return buffer.readstring(printer.result, 0, printer.cursor) + return printNode(statement, visitor.visitStatement) +end + +-- Returns a string representation of an AstType +function printType(type: luau.AstType): string + return printNode(type, visitor.visitType) end function printFile(result: { root: luau.AstStatBlock, eof: luau.Eof }): string @@ -147,5 +154,6 @@ end return { printexpr = printExpr, printstatement = printStatement, + printtype = printType, printfile = printFile, } From 331a6c6ac2b20f09c3100cbf6d89afeb577cf833 Mon Sep 17 00:00:00 2001 From: ffrostfall <80861876+ffrostfall@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:58:18 -0400 Subject: [PATCH 008/642] Implement task.delay (#372) This PR implements task.delay in the form of `(dur: number | time.Duration, routine: (T...) -> U..., ...: T...)`. `yieldLuaStateFor` was also changed to accept an `nargs` parameter, to support using that function for `task.delay`. Closes #260 --------- Co-authored-by: ariel --- definitions/task.luau | 10 +++-- examples/task-delay.luau | 5 +++ lute/task/include/lute/task.h | 2 + lute/task/src/task.cpp | 79 ++++++++++++++++++++++++++++++++--- 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 examples/task-delay.luau diff --git a/definitions/task.luau b/definitions/task.luau index 5b280aea2..3b80c6f34 100644 --- a/definitions/task.luau +++ b/definitions/task.luau @@ -6,15 +6,19 @@ function task.defer() error("unimplemented") end -function task.wait(dur: (number | time.Duration)?) +function task.wait(dur: (number | time.Duration)?): number error("unimplemented") end -function task.spawn(routine: ((T...) -> U...) | thread, ...: T...) +function task.spawn(routine: ((T...) -> U...) | thread, ...: T...): thread error("unimplemented") end -function task.resume(thread: thread) +function task.resume(thread: thread): thread + error("unimplemented") +end + +function task.delay(dur: number | time.Duration, routine: (T...) -> U..., ...: T...): thread error("unimplemented") end diff --git a/examples/task-delay.luau b/examples/task-delay.luau new file mode 100644 index 000000000..baed15a1a --- /dev/null +++ b/examples/task-delay.luau @@ -0,0 +1,5 @@ +local task = require("@lute/task") + +task.delay(1, coroutine.create(print), vector.one) + +task.delay(1, print, vector.one) diff --git a/lute/task/include/lute/task.h b/lute/task/include/lute/task.h index 99b60f6f3..5b2b69d22 100644 --- a/lute/task/include/lute/task.h +++ b/lute/task/include/lute/task.h @@ -15,11 +15,13 @@ int lua_defer(lua_State* L); int lua_wait(lua_State* L); int lua_spawn(lua_State* L); int lute_resume(lua_State* L); +int lua_delay(lua_State* L); static const luaL_Reg lib[] = { {"defer", lua_defer}, {"wait", lua_wait}, {"spawn", lua_spawn}, + {"delay", lua_delay}, {"resume", lute_resume}, diff --git a/lute/task/src/task.cpp b/lute/task/src/task.cpp index b4338ca76..e235992b2 100644 --- a/lute/task/src/task.cpp +++ b/lute/task/src/task.cpp @@ -11,6 +11,7 @@ #include #include + // taken from extern/luau/VM/lcorolib.cpp static const char* const statnames[] = {"running", "suspended", "normal", "dead", "dead"}; @@ -23,10 +24,10 @@ struct WaitData uint64_t startedAtMs; bool putDeltaTimeOnStack; + int nargs; }; - -static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaTimeOnStack) +static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaTimeOnStack, int nargs) { WaitData* yield = new WaitData(); uv_timer_init(uv_default_loop(), &yield->uvTimer); @@ -35,6 +36,7 @@ static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaT yield->startedAtMs = uv_now(uv_default_loop()); yield->uvTimer.data = yield; yield->putDeltaTimeOnStack = putDeltaTimeOnStack; + yield->nargs = nargs; uv_timer_start( &yield->uvTimer, @@ -45,8 +47,9 @@ static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaT yield->resumptionToken->complete( [yield](lua_State* L) { - int stackReturnAmount = yield->putDeltaTimeOnStack ? 1 : 0; - if (stackReturnAmount) + int stackReturnAmount = yield->putDeltaTimeOnStack ? yield->nargs + 1 : yield->nargs; + + if (yield->putDeltaTimeOnStack) lua_pushnumber(L, static_cast(uv_now(uv_default_loop()) - yield->startedAtMs) / 1000.0); delete yield; @@ -71,16 +74,80 @@ int lua_defer(lua_State* L) return lua_yield(L, 0); }; + +int lua_delay(lua_State* L) +{ + int type = lua_type(L, 1); + uint64_t milliseconds = 0; + + // Handle overloads + switch (type) + { + case LUA_TNUMBER: + milliseconds = static_cast(lua_tonumber(L, 1) * 1000); + break; + + case LUA_TUSERDATA: + { + double seconds = getSecondsFromTimespec(getTimespecFromDuration(L, 1)); + milliseconds = static_cast(seconds * 1000); + break; + } + + default: + luaL_errorL(L, "invalid type %s passed into task.delay", lua_typename(L, lua_type(L, 1))); + break; + }; + + // remove the wait time + lua_remove(L, 1); + + lua_State* passedLuaState; + + if (lua_isfunction(L, 1)) + { + lua_State* NL = lua_newthread(L); + lua_pop(L, 1); + + passedLuaState = NL; + + // get the function + lua_xpush(L, NL, 1); + + // remove the function + lua_remove(L, 1); + } + else if (lua_isthread(L, 1)) + { + passedLuaState = lua_tothread(L, 1); + lua_remove(L, 1); + } + else + { + luaL_error(L, "can only pass threads or functions to task.spawn"); + }; + + int nargs = lua_gettop(L); + lua_xmove(L, passedLuaState, nargs); + + yieldLuaStateFor(passedLuaState, milliseconds, false, nargs); + + return 1; +} + int lua_spawn(lua_State* L) { if (lua_isfunction(L, 1)) { lua_State* NL = lua_newthread(L); + // transfer arguments from other lua_State to the called lua_State lua_xpush(L, NL, 1); + // remove the function arg lua_remove(L, 1); + // insert the thread lua_insert(L, 1); } else if (!lua_isthread(L, 1)) @@ -89,6 +156,8 @@ int lua_spawn(lua_State* L) } lute_resume(L); + + // return the thread return 1; } @@ -116,7 +185,7 @@ int lua_wait(lua_State* L) break; }; - yieldLuaStateFor(L, milliseconds, true); + yieldLuaStateFor(L, milliseconds, true, 0); return lua_yield(L, 0); } From dda7f0eb23df29546147ef3ed9f9336fca7e9ed0 Mon Sep 17 00:00:00 2001 From: bjcscat Date: Mon, 29 Sep 2025 14:14:19 -0500 Subject: [PATCH 009/642] Switch fs.exists to use uv_fs_access (#376) This is more correct behavior, fs.exist currently uses stat and checks for an error --------- Co-authored-by: ariel --- lute/fs/src/fs.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index c257fdbf2..2220464ea 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -12,6 +12,8 @@ #include #ifdef _WIN32 #include +#else +#include #endif #include #include @@ -659,10 +661,11 @@ int fs_exists(lua_State* L) auto* req = new uv_fs_t(); req->data = new ResumeToken(getResumeToken(L)); - int err = uv_fs_stat( + int err = uv_fs_access( uv_default_loop(), req, path, + F_OK, [](uv_fs_t* req) { auto* request_state = static_cast(req->data); @@ -670,15 +673,8 @@ int fs_exists(lua_State* L) request_state->get()->complete( [req](lua_State* L) { - if (req->result == UV_ENOENT) - { - lua_pushboolean(L, false); // does not exist - } - else - { - lua_pushboolean(L, true); - } - + lua_pushboolean(L, req->result == 0); + uv_fs_req_cleanup(req); delete req; From ce7f89922dfc7ccb39bf52440b438a9f38f3c6fe Mon Sep 17 00:00:00 2001 From: Carlos Sobrinho <1511758+csobrinho@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:15:17 -0700 Subject: [PATCH 010/642] Escape json strings with `"` (#381) Fixes #380 --- batteries/json.luau | 1 + 1 file changed, 1 insertion(+) diff --git a/batteries/json.luau b/batteries/json.luau index 36eaf5521..33c41ffd5 100644 --- a/batteries/json.luau +++ b/batteries/json.luau @@ -77,6 +77,7 @@ local ESCAPE_MAP = { [0x0A] = string.byte("n"), [0x0D] = string.byte("r"), [0x09] = string.byte("t"), + [0x22] = string.byte('"'), } local function serializeUnicode(codepoint: number) From 9f92dcdcc1ee4b61c8ccfc233ad7e13c1f1f0e6a Mon Sep 17 00:00:00 2001 From: Carlos Sobrinho <1511758+csobrinho@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:16:27 -0700 Subject: [PATCH 011/642] Manually get the full body including any \0 instead of relying on the string length (#379) Without this we would get a truncated body and curl would hang waiting for the missing bytes to send. Test: ```luau local net = require("@lute/net") local pp = require("@batteries/pp") local function test(payload: string) local response = net.request("https://httpbin.org/post", { method = "POST", headers = { ["Content-Type"] = "application/octet-stream", }, body = payload, }) print(response.status, pp(response.headers), response.ok, pp(response.body)) end test("Hello World") -- works test("Hello\0World") -- hangs ``` `Content-Length` is 11 for both. --- lute/net/src/net.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 6f8faa077..520b4de1a 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -66,8 +66,11 @@ static CurlResponse requestData( curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str()); if (!body.empty()) + { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); + } + if (!headers.empty()) { for (const auto& header_pair : headers) @@ -133,7 +136,11 @@ int request(lua_State* L) lua_getfield(L, 2, "body"); if (lua_isstring(L, -1)) - body = lua_tostring(L, -1); + { + size_t len; + const char* data = lua_tolstring(L, -1, &len); + body.assign(data, data + len); + } lua_pop(L, 1); lua_getfield(L, 2, "headers"); From 1542e4fa5b04c1b823ee09e42cb7d3fa22804609 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 2 Oct 2025 11:42:32 -0700 Subject: [PATCH 012/642] infra: Bootstrap shell script for downloading lute dependencies on macOS/linux (#383) Resolves #363 . This PR adds a bootstrap script for fetching the dependencies needed to built lute from scratch, which removes the requirement of needing a `lute` binary. --- README.md | 4 ++++ tools/bootstrap.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100755 tools/bootstrap.sh diff --git a/README.md b/README.md index ef1376b32..65a0c336d 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,7 @@ The Lute repository fundamentally contains three sets of libraries. These are as - `batteries`: A collection of useful, standalone Luau libraries that do not depend on `lute`. Contributions to any of these libraries are welcome, and we encourage you to open issues or pull requests if you have any feedback or contributions to make. +### Building Lute without a `lute` executable +- From the root directory, run `./tools/bootstrap.sh` to get lute dependencies +- Configure with `cmake -G=Ninja -B {MacOS: build/xcode/debug | Linux: build/debug | Windows: build/vs2022/debug} -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1` +- Run `ninja -C {MacOS: build/xcode/debug | Linux: build/debug | Windows: build/vs2022/debug} {lute/cli/lute | tests/lute-tests}` diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh new file mode 100755 index 000000000..86f5208c5 --- /dev/null +++ b/tools/bootstrap.sh @@ -0,0 +1,51 @@ +fetch_dep() { + local dep_file="$1" + + # Ensure the file exists + if [[ ! -f "$dep_file" ]]; then + echo "Dependency file not found: $dep_file" + return 1 + fi + + # Default values + local name="" + local remote="" + local branch="" + local revision="" + + # Parse the file + while IFS='=' read -r key value; do + key=$(echo "$key" | xargs) + value=$(echo "$value" | sed 's/"//g' | xargs) + + case "$key" in + name) name="$value" ;; + remote) remote="$value" ;; + branch) branch="$value" ;; + revision) revision="$value" ;; + esac + done < "$dep_file" + + local target_dir="extern/$name" + + # Clone if not already present + if [[ -d "$target_dir" ]]; then + echo "Dependency '$name' already exists at $target_dir. Skipping clone." + else + echo "Cloning $name from $remote (branch: $branch) into $target_dir..." + git clone --branch "$branch" --single-branch "$remote" "$target_dir" || return 1 + fi + + # Checkout specific revision if provided + if [[ -n "$revision" ]]; then + echo "Checking out revision $revision in $target_dir" + git -C "$target_dir" checkout "$revision" || return 1 + fi +} + +for file in extern/*.tune; do + if [[ -f "$file" ]]; then + echo "Fetching dependency from: $(basename "$file")" + fetch_dep "extern/$(basename "$file")" + fi +done From ac91f91ddc7ffba0543ed37058b67f553eb6cf65 Mon Sep 17 00:00:00 2001 From: othomson-roblox <64273528+othomson-roblox@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:50:12 -0700 Subject: [PATCH 013/642] Fix issue where the body in a response issued by the local server is truncated on a null terminator (#385) Similar to #379, a response returned by the request handler in `net.serve` would have its body truncated on the first null terminator. This PR repeats the fix in #379 to apply to http responses as well. Test ```luau --test-vm.luau local net = require("@lute/net") local self = {} function self.createServer(port: number, body: string) net.serve({ port = port, hostname = "localhost", reuseport = true, handler = function(request) return { status = 200, body = body, } end }) end return self ``` ```luau --test.luau local net = require("@lute/net") local vm = require("@lute/vm") local function test(body: string) local serverController = vm.create("./test-vm") serverController.createServer(8081, body) local response = net.request("http://localhost:8081") assert(body == response.body, `{#body} != {#response.body}`) print("good!") end test("Hello World") -- good! test("Hello\0World") -- 11 != 5 (before) good! (after) ``` --- lute/net/src/net.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 520b4de1a..7fc90bd8f 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -347,7 +347,11 @@ static void handleResponse(auto* res, lua_State* L, int responseIndex) lua_pop(L, 1); lua_getfield(L, responseIndex, "body"); - std::string body = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + + std::string body = ""; + size_t bodyLength; + const char* bodyData = lua_tolstring(L, -1, &bodyLength); + body.assign(bodyData, bodyData + bodyLength); lua_pop(L, 1); res->end(body); From 1d0129ccaf3bda4d7eb9e1e209d24063423c0892 Mon Sep 17 00:00:00 2001 From: ariel Date: Thu, 2 Oct 2025 15:48:32 -0700 Subject: [PATCH 014/642] infra: copy compile_commands to `build/` in the configure step (#387) Fixes #348 by copying `compile_commands.json` to `build/compile_commands.json` during the configure step. --- tools/luthier.luau | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/luthier.luau b/tools/luthier.luau index 9ccd67ea3..30be0ee51 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -693,7 +693,10 @@ local function configure() table.move(arguments, 1, #arguments, 2, cmd) check(fetchDependenciesIfNeeded()) - return call(cmd) + + local result = call(cmd) + fs.copy(joinPath(getProjectPath(), "compile_commands.json"), projectRelative(BUILD_DIR, "compile_commands.json")) + return result end local function run() From 93cdd017eda0a43d02d915a270d9da0c6d390f87 Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Fri, 3 Oct 2025 09:09:30 -0700 Subject: [PATCH 015/642] Bump lute in toolchain managers (#388) #387 broke luthier when using lute via toolchain managers since the old version they use doesn't have `fs.copy`. --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 822ead819..def302598 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.0.2" } -lute = { github = "luau-lang/lute", version = "0.1.0-nightly.20250606" } +lute = { github = "luau-lang/lute", version = "0.1.0-nightly.20251003" } diff --git a/rokit.toml b/rokit.toml index cf1a46d09..5855d9112 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.0.2" -lute = "luau-lang/lute@0.1.0-nightly.20250606" +lute = "luau-lang/lute@0.1.0-nightly.20251003" From 9c4b28ef792ff5004c31e58955effc6558e41edd Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 3 Oct 2025 14:43:32 -0700 Subject: [PATCH 016/642] Update the bootstrap script to actually build two copies of lute (#386) The bootstrap script didn't correctly generate the lute standard library headers that the `luthier` script did. This PR: - generates a minimal header and implementation for the lute standard libraries during the bootstrap phase - uses these headers to build a `bootstrapped-lute` binary - uses that `bootstrapped-lute` to run `luthier.luau` which builds the actual `lute` executable with the `std` library correctly generated. - accepts an `--install` argument to install this `lute` binary to `$HOME/.lute/bin`. There are some additional improvements that have been made: 1. General README fixes 2. This script can now be run multiple times in sequence, and it correctly deletes artifacts generated by the bootstrapping process each time (e.g. generated std headers, dependencies). 3. Uses git commands to `clone` only the particular revision of the `extern` dependencies we care about, not the entire history. --------- Co-authored-by: ariel --- .github/actions/setup-and-build/action.yml | 26 ++++- README.md | 34 +++++- tools/bootstrap.sh | 129 ++++++++++++++++----- tools/templates/cli_header.h | 9 ++ tools/templates/cli_impl.cpp | 8 ++ tools/templates/std_header.h | 9 ++ tools/templates/std_impl.cpp | 8 ++ 7 files changed, 187 insertions(+), 36 deletions(-) create mode 100644 tools/templates/cli_header.h create mode 100644 tools/templates/cli_impl.cpp create mode 100644 tools/templates/std_header.h create mode 100644 tools/templates/std_impl.cpp diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index def71560d..cad6f5d9b 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -60,16 +60,33 @@ runs: path: extern key: extern-${{ hashFiles('extern/*.tune') }} + # We can currently only bootstrap correctly in ci on mac/linux + - name: Bootstrap Lute (Darwin/Linux only) + if: runner.os != 'Windows' + shell: bash + run: | + ./tools/bootstrap.sh + correct_lute=$(./build/bootstrapped-lute tools/luthier.luau build lute --config debug --which) + mv $correct_lute ./build/lute + echo "luthier_lute=./build/lute" >> $GITHUB_ENV + + - name: Build ${{ inputs.target }} ${{ runner.os }} + if: runner.os != 'Windows' + shell: bash + run: $luthier_lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config}} ${{ inputs.options }} + - name: Fetch ${{ inputs.target }} - if: steps.cache-extern.outputs.cache-hit != 'true' + if: steps.cache-extern.outputs.cache-hit != 'true' && runner.os == 'Windows' shell: bash run: lute ./tools/luthier.luau fetch ${{ inputs.target }} - name: Configure ${{ inputs.target }} + if: runner.os == 'Windows' shell: bash run: lute ./tools/luthier.luau configure ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} - name: Build ${{ inputs.target }} + if: runner.os == 'Windows' shell: bash run: lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} @@ -77,6 +94,11 @@ runs: id: artifact_path shell: bash run: | - exe_path=$(lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} --which) + if [[ ${{ runner.os }} == "Windows" ]]; then + LUTE="lute" + else + LUTE="$luthier_lute" + fi + exe_path=$($LUTE ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} --which) echo "Executable path: $exe_path" # Debug print echo "exe_path=$exe_path" >> "$GITHUB_OUTPUT" diff --git a/README.md b/README.md index 65a0c336d..016d72027 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,33 @@ The Lute repository fundamentally contains three sets of libraries. These are as - `batteries`: A collection of useful, standalone Luau libraries that do not depend on `lute`. Contributions to any of these libraries are welcome, and we encourage you to open issues or pull requests if you have any feedback or contributions to make. -### Building Lute without a `lute` executable -- From the root directory, run `./tools/bootstrap.sh` to get lute dependencies -- Configure with `cmake -G=Ninja -B {MacOS: build/xcode/debug | Linux: build/debug | Windows: build/vs2022/debug} -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1` -- Run `ninja -C {MacOS: build/xcode/debug | Linux: build/debug | Windows: build/vs2022/debug} {lute/cli/lute | tests/lute-tests}` + +### Building Lute from scratch + +Lute uses a script built in Luau to handle pulling in vendored dependencies and embedding our Luau standard library into the `lute` executable. As a result, you typically need a `lute` executable available already in order to build it. As a result, you'd like to build `lute` entirely from scratch, you'll have to bootstrap it. +From the root directory, run `./tools/bootstrap.sh` to build a `lute` executable. If you pass the `--install` flag, this will be +installed to `$HOME/.lute/bin/lute` by default, with a prompt provided to select an alternative install location. Make sure to add this `lute` to your `$PATH` if you would like to have `lute` resolve to it in general. + +### Building Lute with `lute` installed + +After either bootstrapping `lute` per above or installing it using `foreman`, `rokit`, or manually from Releases, you can build modified local versions of `lute` using our build script, `luthier`, located at `./tools/luthier.luau`. To perform a clean build of Lute, you can run: +```bash +/path/to/lute tools/luthier.luau build --clean {lute | Lute.CLI | Lute.tests} +``` + +## Manually building Lute + +Outside of resolving the dependencies, Lute's build system is implemented today with `cmake`, meaning you can fairly easily pull external dependencies into `extern`, and then perform your own manual build with the following steps: + +- Copy all of the templates from `./tools/templates` into the right locations to support building a version with no embedded Luau functionality, e.g. + ```bash + mkdir -p ./lute/std/src/generated + cp ./tools/templates/std_impl.cpp ./lute/std/src/generated/modules.cpp + cp ./tools/templates/std_header.h ./lute/std/src/generated/modules.h + mkdir -p ./lute/cli/generated + cp ./tools/templates/cli_impl.cpp ./lute/cli/generated/commands.cpp + cp ./tools/templates/cli_header.h ./lute/cli/generated/commands.h + ``` +- Configure with `cmake -G=Ninja -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1` +- Build a `lute` executable with `ninja -C build lute/cli/lute` or our test suite with `ninja -C build tests/lute-tests`. +- Optionally, use this version to run `luthier generate` to generate the files for embedded Luau functionality. diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh index 86f5208c5..6f7c2a6f7 100755 --- a/tools/bootstrap.sh +++ b/tools/bootstrap.sh @@ -1,3 +1,5 @@ +set -e + fetch_dep() { local dep_file="$1" @@ -7,45 +9,112 @@ fetch_dep() { return 1 fi - # Default values - local name="" - local remote="" - local branch="" - local revision="" - - # Parse the file - while IFS='=' read -r key value; do - key=$(echo "$key" | xargs) - value=$(echo "$value" | sed 's/"//g' | xargs) - - case "$key" in - name) name="$value" ;; - remote) remote="$value" ;; - branch) branch="$value" ;; - revision) revision="$value" ;; - esac - done < "$dep_file" + # Extract each field by key + name=$(grep '^name' "$file" | sed -E 's/^name *= *"(.*)"/\1/') + remote=$(grep '^remote' "$file" | sed -E 's/^remote *= *"(.*)"/\1/') + branch=$(grep '^branch' "$file" | sed -E 's/^branch *= *"(.*)"/\1/') + revision=$(grep '^revision' "$file" | sed -E 's/^revision *= *"(.*)"/\1/') + # Output the parsed variables local target_dir="extern/$name" + # Get version string (e.g., "git version 2.50.1") + version_str=$(git --version) - # Clone if not already present - if [[ -d "$target_dir" ]]; then - echo "Dependency '$name' already exists at $target_dir. Skipping clone." - else - echo "Cloning $name from $remote (branch: $branch) into $target_dir..." - git clone --branch "$branch" --single-branch "$remote" "$target_dir" || return 1 - fi + # Extract the major and minor version numbers using regex + # e.g., 2 and 50 from "git version 2.50.1" + if [[ $version_str =~ ([0-9]+)\.([0-9]+)\.[0-9]+ ]]; then + gitMajor="${BASH_REMATCH[1]}" + gitMinor="${BASH_REMATCH[2]}" - # Checkout specific revision if provided - if [[ -n "$revision" ]]; then - echo "Checking out revision $revision in $target_dir" - git -C "$target_dir" checkout "$revision" || return 1 + # Now perform the version check + if [[ "$gitMajor" -ge 3 || ( "$gitMajor" -eq 2 && "$gitMinor" -ge 49 ) ]]; then + git clone --depth=1 --revision $revision $remote $target_dir + else + git clone --depth=1 --branch $branch $remote $target_dir + fi + else + echo "Could not parse Git version from: $version_str" + exit 1 fi } +# Download dependencies from the tune files +find "extern" -mindepth 1 ! -name "*.tune" -prune -exec rm -rf {} + + for file in extern/*.tune; do - if [[ -f "$file" ]]; then + if [[ -f "$file" ]]; then echo "Fetching dependency from: $(basename "$file")" fetch_dep "extern/$(basename "$file")" fi done + +# Generate the stdlib modules.cpp file +rm -rf lute/std/src/generated +mkdir -p lute/std/src/generated + +cp ./tools/templates/std_impl.cpp ./lute/std/src/generated/modules.cpp +cp ./tools/templates/std_header.h ./lute/std/src/generated/modules.h + +# Generate the clicommands modules.cpp file +rm -rf lute/cli/generated +mkdir -p lute/cli/generated + +cp ./tools/templates/cli_impl.cpp ./lute/cli/generated/commands.cpp +cp ./tools/templates/cli_header.h ./lute/cli/generated/commands.h + +## Configure bootstrap lute - lute stdlib +os_type="$(uname)" +BUILD_PATH="" +EXE_PATH=lute/cli/lute +OUT_BINARY=./build/bootstrapped-lute +if [[ "$os_type" == "Linux" ]]; then + BUILD_PATH=build/debug +elif [[ "$os_type" == "Darwin" ]]; then + BUILD_PATH=build/xcode/debug +elif [[ "$os_type" == MINGW* || "$os_type" == MSYS* || "$os_type" == CYGWIN* ]]; then + BUILD_PATH=build/vs2022/debug + EXE_PATH+=".exe" + OUT_BINARY+=.exe +else + echo "Unknown OS: $os_type, cannot bootstrap" + return 1 +fi + +rm -rf build && mkdir build +cmake -G=Ninja -B $BUILD_PATH -DCMAKE_BUILD_TYPE=Debug + +# Compile bootstrapping lute +ninja -C $BUILD_PATH $EXE_PATH +echo "" +echo "Successfully built the bootstrapped lute - std" + +# Use bootstrapped lute to build lute with standard libraries included +BOOTSTRAPPED_LUTE=./$BUILD_PATH/$EXE_PATH + +mv $BOOTSTRAPPED_LUTE $OUT_BINARY +$OUT_BINARY tools/luthier.luau build --clean lute + +# optionally install bootstrapped lute +install_requested=false +# Parse flags +# Loop through all command-line arguments +for arg in "$@"; do + if [[ "$arg" == "--install" ]]; then + install_requested=true + break + fi +done + +INSTALL_DIR=$HOME/.lute/bin +if $install_requested; then + read -p "Enter the path where you would like to install lute to (default: $INSTALL_DIR): " USER_PATH + + # If user_input is empty, keep default_path; else update it + if [[ -n "$USER_PATH" ]]; then + INSTALL_DIR=$USER_PATH + fi + mkdir -p $INSTALL_DIR + cp $BOOTSTRAPPED_LUTE $INSTALL_DIR + echo "" + echo "Installed lute to $INSTALL_DIR/lute. Make sure to add this to your PATH variable." +fi diff --git a/tools/templates/cli_header.h b/tools/templates/cli_header.h new file mode 100644 index 000000000..5ff503f74 --- /dev/null +++ b/tools/templates/cli_header.h @@ -0,0 +1,9 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `std/libs`. +#pragma once + +#include +#include + +extern const std::pair luteclicommands[1]; + diff --git a/tools/templates/cli_impl.cpp b/tools/templates/cli_impl.cpp new file mode 100644 index 000000000..03f2de9d1 --- /dev/null +++ b/tools/templates/cli_impl.cpp @@ -0,0 +1,8 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `std/libs`. + +#include +#include "commands.h" + +const std::pair luteclicommands[] = {{"", ""}}; + diff --git a/tools/templates/std_header.h b/tools/templates/std_header.h new file mode 100644 index 000000000..da5a7ca7d --- /dev/null +++ b/tools/templates/std_header.h @@ -0,0 +1,9 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `std/libs`. +#pragma once + +#include +#include + +extern const std::pair lutestdlib[1]; + diff --git a/tools/templates/std_impl.cpp b/tools/templates/std_impl.cpp new file mode 100644 index 000000000..fd88f5c16 --- /dev/null +++ b/tools/templates/std_impl.cpp @@ -0,0 +1,8 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `std/libs`. + +#include +#include "modules.h" + +constexpr std::pair lutestdlib[] = {{"", ""}}; + From 92379b8eea09fa56491a8fd00f1dae09bc47cc35 Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Fri, 3 Oct 2025 18:18:34 -0700 Subject: [PATCH 017/642] Upgrade Luau to 0.690 (#391) --- extern/luau.tune | 4 ++-- lute/luau/src/luau.cpp | 17 ----------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 7073c6944..669ad5ee3 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.675" -revision = "59658182835dc39f598a4fc16ba0b177b8e6f55b" +branch = "0.690" +revision = "fb63edcceadafc7144d3de7a6ccb0fee83978cf0" diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index d17ab6d1e..efb2069d0 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -20,11 +20,6 @@ const char* COMPILE_RESULT_TYPE = "CompileResult"; -LUAU_FASTFLAG(LuauStoreCSTData2) -LUAU_FASTFLAG(LuauStoreReturnTypesAsPackOnAst) -LUAU_FASTFLAG(LuauStoreLocalAnnotationColonPositions) -LUAU_FASTFLAG(LuauCSTForReturnTypeFunctionTail) - namespace luau { @@ -38,12 +33,6 @@ struct StatResult static StatResult parse(std::string& source) { - // TODO: this is very bad, fix it! - FFlag::LuauStoreCSTData2.value = true; - FFlag::LuauStoreReturnTypesAsPackOnAst.value = true; - FFlag::LuauStoreLocalAnnotationColonPositions.value = true; - FFlag::LuauCSTForReturnTypeFunctionTail.value = true; - auto allocator = std::make_shared(); auto names = std::make_shared(*allocator); @@ -67,12 +56,6 @@ struct ExprResult static ExprResult parseExpr(std::string& source) { - // TODO: this is very bad, fix it! - FFlag::LuauStoreCSTData2.value = true; - FFlag::LuauStoreReturnTypesAsPackOnAst.value = true; - FFlag::LuauStoreLocalAnnotationColonPositions.value = true; - FFlag::LuauCSTForReturnTypeFunctionTail.value = true; - auto allocator = std::make_shared(); auto names = std::make_shared(*allocator); From 3d94c93a13c09163f5f913732ed44b37f1ee9f77 Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Fri, 3 Oct 2025 19:11:24 -0700 Subject: [PATCH 018/642] Create lute --version command (#390) --- .github/workflows/release.yml | 20 +++++++++++++++----- CMakeBuild/get_version.cmake | 13 ++++++++++++- lute/cli/CMakeLists.txt | 10 +++++++++- lute/cli/include/lute/version.h.in | 5 +++++ lute/cli/src/climain.cpp | 12 ++++++++++++ 5 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 lute/cli/include/lute/version.h.in diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a52f96517..466adb12b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ jobs: runs-on: ubuntu-latest outputs: proceed: ${{ steps.check.outputs.proceed }} + nightly_tag: ${{ steps.set_nightly.outputs.nightly_tag }} permissions: contents: read @@ -58,6 +59,13 @@ jobs: echo "proceed=$PROCEED" >> $GITHUB_OUTPUT + - name: Set nightly version suffix + id: set_nightly + if: github.event_name == 'schedule' || github.event.inputs.nightly + run: | + DATE=$(date +%Y%m%d) + echo "nightly_tag=nightly.$DATE" >> "$GITHUB_OUTPUT" + build: strategy: matrix: @@ -82,6 +90,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set nightly version suffix + if: needs.check.outputs.nightly_tag + run: echo "LUTE_VERSION_SUFFIX=${{ needs.check.outputs.nightly_tag }}" >> "$GITHUB_ENV" + - name: Setup and Build Lute id: build_lute uses: ./.github/actions/setup-and-build @@ -98,7 +110,7 @@ jobs: name: ${{ matrix.artifact_name }} release: - needs: build + needs: [check, build] runs-on: ubuntu-latest permissions: @@ -130,11 +142,9 @@ jobs: - name: Create Nightly Tag id: tag_step - if: github.event_name == 'schedule' || github.event.inputs.nightly + if: needs.check.outputs.nightly_tag run: | - DATE=$(date +%Y%m%d) - TAG="-nightly.$DATE" - echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "tag=-${{ needs.check.outputs.nightly_tag }}" >> $GITHUB_OUTPUT - name: Create Release Tag id: tag_release diff --git a/CMakeBuild/get_version.cmake b/CMakeBuild/get_version.cmake index 4463e578a..6700ed106 100644 --- a/CMakeBuild/get_version.cmake +++ b/CMakeBuild/get_version.cmake @@ -1,4 +1,15 @@ cmake_minimum_required(VERSION 3.13) set(LUTE_VERSION 0.1.0) -message("${LUTE_VERSION}") + +set(LUTE_VERSION_SUFFIX_DEFAULT "$ENV{LUTE_VERSION_SUFFIX}") +if(NOT DEFINED LUTE_VERSION_SUFFIX AND NOT "${LUTE_VERSION_SUFFIX_DEFAULT}" STREQUAL "") + set(LUTE_VERSION_SUFFIX "${LUTE_VERSION_SUFFIX_DEFAULT}") +endif() +set(LUTE_VERSION_SUFFIX "${LUTE_VERSION_SUFFIX}" CACHE STRING "Optional suffix appended to the Lute version string") +set(LUTE_VERSION_FULL "${LUTE_VERSION}") +if(LUTE_VERSION_SUFFIX) + set(LUTE_VERSION_FULL "${LUTE_VERSION_FULL}-${LUTE_VERSION_SUFFIX}") +endif() + +message("${LUTE_VERSION_FULL}") diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index b0b8e88e3..8ab56ea90 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -12,6 +12,14 @@ target_compile_features(Lute.CLI.Commands PUBLIC cxx_std_17) target_include_directories(Lute.CLI.Commands PUBLIC include generated) target_compile_options(Lute.CLI.Commands PRIVATE ${LUTE_OPTIONS}) +set(CLI_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") +file(MAKE_DIRECTORY "${CLI_GENERATED_INCLUDE_DIR}/lute") +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/include/lute/version.h.in + ${CLI_GENERATED_INCLUDE_DIR}/lute/version.h + @ONLY +) + add_library(Lute.CLI.lib STATIC) target_sources(Lute.CLI.lib PRIVATE @@ -25,7 +33,7 @@ target_sources(Lute.CLI.lib PRIVATE ) target_compile_features(Lute.CLI.lib PUBLIC cxx_std_17) -target_include_directories(Lute.CLI.lib PUBLIC include) +target_include_directories(Lute.CLI.lib PUBLIC include ${CLI_GENERATED_INCLUDE_DIR}) target_link_libraries(Lute.CLI.lib PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Require Lute.Runtime Luau.CLI.lib) target_compile_options(Lute.CLI.lib PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/cli/include/lute/version.h.in b/lute/cli/include/lute/version.h.in new file mode 100644 index 000000000..0334b964a --- /dev/null +++ b/lute/cli/include/lute/version.h.in @@ -0,0 +1,5 @@ +#pragma once + +#define LUTE_VERSION "@LUTE_VERSION@" +#define LUTE_VERSION_SUFFIX "@LUTE_VERSION_SUFFIX@" +#define LUTE_VERSION_FULL "@LUTE_VERSION_FULL@" diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 909a38d34..721faed14 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -17,6 +17,7 @@ #include "lute/require.h" #include "lute/runtime.h" #include "lute/tc.h" +#include "lute/version.h" #ifdef _WIN32 #include @@ -170,6 +171,12 @@ static void displayHelp(const char* argv0) printf("\n"); printf("General Options:\n"); printf(" -h, --help Display this usage message.\n"); + printf(" --version Show the lute version.\n"); +} + +static void displayVersion() +{ + printf("%s\n", LUTE_VERSION_FULL); } static void displayRunHelp(const char* argv0) @@ -439,6 +446,11 @@ int cliMain(int argc, char** argv) displayHelp(argv[0]); return 0; } + else if (strcmp(command, "--version") == 0) + { + displayVersion(); + return 0; + } else if (std::optional result = getCliCommand(command); result) { return handleCliCommand(*result); From f28ca52dc07fba7e44104a1706925a39d8ab05af Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 7 Oct 2025 10:59:39 -0700 Subject: [PATCH 019/642] stdlib: Introduce a small standard library utility for writing tests in lute (#393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on [frktest](https://github.com/itsfrank/frktest) This testing utility needs a bunch of assertions to be more useful, but this PR just scopes out what a simple, builtin testing api in Luau could look like. For some tests that look like: ``` local test = require("@std/test") local check = test.assert test.case("foo", function() check.eq(3, 3) check.eq(6, 1) end) test.case("bar", function() check.eq("a", "b") end) test.case("baz", function() check.neq("a", "b") end) test.suite("MySuite", function() test.case("subcase", function() check.eq(5 * 5, 3) end) end) test.run(); ``` this will render as: ``` ❯ ./build/xcode/debug/lute/cli/lute run examples/testing.luau ================================================== TEST RESULTS ================================================== Failed Tests (3): ❌ foo ./examples/testing.luau:5 eq: 6 ~= 1 ❌ bar ./examples/testing.luau:10 eq: a ~= b ❌ MySuite.subcase ./examples/testing.luau:20 eq: 25 ~= 3 -------------------------------------------------- Total: 4 Passed: 1 ✓ Failed: 3 ✗ ================================================== ``` Currently reports: - [x] Test suites and cases - [x] Line numbers - [x] Stack traces - [x] The expression that failed. --- examples/testing.luau | 22 ++++++ lute/std/libs/test.luau | 166 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 examples/testing.luau create mode 100644 lute/std/libs/test.luau diff --git a/examples/testing.luau b/examples/testing.luau new file mode 100644 index 000000000..06adde5cc --- /dev/null +++ b/examples/testing.luau @@ -0,0 +1,22 @@ +local test = require("@std/test") +local check = test.assert +test.case("foo", function() + check.eq(3, 3) + check.eq(6, 1) +end) + +test.case("bar", function() + check.eq("a", "b") +end) + +test.case("baz", function() + check.neq("a", "b") +end) + +test.suite("MySuite", function() + test.case("subcase", function() + check.eq(5 * 5, 3) + end) +end) + +test.run() diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau new file mode 100644 index 000000000..51c9a2dd8 --- /dev/null +++ b/lute/std/libs/test.luau @@ -0,0 +1,166 @@ +--!strict + +type Test = () -> () +type TestCase = { name: string, case: Test } +type TestCases = { TestCase } +type TestSuite = { name: string, cases: TestCases } + +type FailedTest = { + test: string, -- name of the test case that failed + assertion: string, -- name of the assertion that failed (e.g., "assert.eq") + error: string, -- the error message from the assertion + filename: string, -- source file where the failure occurred + linenumber: number, -- line number of the failure +} + +type TestRunResult = { + failures: { FailedTest }, + total: number, + failed: number, + passed: number, +} + +type TestReporter = (TestRunResult) -> () + +local function printFailedTest(failed: FailedTest) + print(`❌ {failed.test}`) + print(` {failed.filename}:{failed.linenumber}`) + print(` {failed.assertion}: {failed.error}`) + print() +end + +local function simpleReporter(result: TestRunResult) + print("\n" .. string.rep("=", 50)) + print("TEST RESULTS") + print(string.rep("=", 50)) + + if #result.failures > 0 then + print(`\nFailed Tests ({#result.failures}):\n`) + for _, failed in result.failures do + printFailedTest(failed) + end + end + + print(string.rep("-", 50)) + print(`Total: {result.total}`) + print(`Passed: {result.failed} ✓`) + print(`Failed: {result.passed} ✗`) + print(string.rep("=", 50) .. "\n") +end + +type TestEnvironment = { + anonymous: TestCases, + suites: { TestSuite }, + reporter: TestReporter, + active_suite: TestSuite?, +} + +local env: TestEnvironment = { anonymous = {}, suites = {}, reporter = simpleReporter } + +local test = {} +test.assert = {} +local assert = test.assert +-- todo: we should add some more assertions for diffing tables! +function assert.eq(lhs: T, rhs: T) + if lhs ~= rhs then + error({ msg = `{lhs} ~= {rhs}` }) + end +end + +function assert.neq(lhs: T, rhs: T) + if lhs == rhs then + error({ msg = `{lhs} == {rhs}` }) + end +end + +-- register a test case +function test.case(name: string, case: () -> ()) + local testcase = { name = name, case = case } + if env.active_suite then + local _, cases = env.active_suite.name, env.active_suite.cases + table.insert(cases, testcase) + else + table.insert(env.anonymous, testcase) + end +end + +-- register a test suite +-- takes name and a function to register +function test.suite(name: string, registerSuite: () -> ()) + local testsuite: TestSuite = { name = name, cases = {} } + table.insert(env.suites, testsuite) + env.active_suite = testsuite + registerSuite() + env.active_suite = nil +end + +function test.setreporter(reporter: TestReporter) + test.reporter = reporter +end + +function test.run() + local failures: { FailedTest } = {} + local total = 0 + local failed = 0 + local passed = 0 + + -- Run anonymous tests + for _, tc in env.anonymous do + total += 1 + local function handlefailure(err) + local fileName, lineNumber = debug.info(4, "sl") + local assertName = debug.info(3, "n") + + local failedtest: FailedTest = { + test = tc.name, + assertion = assertName, + error = err.msg, + filename = fileName, + linenumber = lineNumber, + } + table.insert(failures, failedtest) + failed += 1 + end + + local success = xpcall(tc.case, handlefailure) + if success then + passed += 1 + end + end + + -- Run tests from suites + for _, suite in env.suites do + for _, tc in suite.cases do + total += 1 + local function handlefailure(err) + local fileName, lineNumber = debug.info(4, "sl") + local assertName = debug.info(3, "n") + + local failedtest: FailedTest = { + test = `{suite.name}.{tc.name}`, + assertion = assertName, + error = err.msg, + filename = fileName, + linenumber = lineNumber, + } + table.insert(failures, failedtest) + failed += 1 + end + + local success = xpcall(tc.case, handlefailure) + if success then + passed += 1 + end + end + end + + local result: TestRunResult = { + failures = failures, + total = total, + failed = failed, + passed = passed, + } + env.reporter(result) +end + +return table.freeze(test) From 7f9cbc2843d8d4f97c77290788baea7e440a2175 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 8 Oct 2025 10:58:02 -0700 Subject: [PATCH 020/642] clicommands: typedef setup command should try to create the full path where the typedefs are stored (#397) I ran into a small bug with this script where I had `~/.lute` on my device, but not the rest of the path. The setup script only checks if this directory exists and assumes that the rest of the path is there if `~/.lute` is there. This can cause errors with writing the type def files because the subpaths won't exist. This change also refactors the code that makes the subdirs by letting you pass the full path and then making the subdirectories as you go. --- lute/cli/commands/setup/init.luau | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 3b4a0056f..21f02dc4c 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -4,14 +4,19 @@ local process = require("@lute/process") local homedir = process.homedir() --- Already exists +function mkdirdashp(home: string, subpath: string) + local subdir = home + for _, part in string.split(subpath, "/") do + subdir = subdir .. "/" .. part + if not fs.exists(subdir) then + fs.mkdir(subdir) + end + end +end + -- TODO we're assuming the files are completely unmodified, but we really shouldn't do that. print("Found existing /.lute/ directory! Overwriting type definitions.") -if not fs.exists(homedir .. "/.lute") then - fs.mkdir(homedir .. "/.lute") - fs.mkdir(homedir .. "/.lute/typedefs") - fs.mkdir(homedir .. "/.lute/typedefs/0.1.0") -end +mkdirdashp(homedir, ".lute/typedefs/0.1.0") for key, value in definitions :: { [string]: string } do fs.writestringtofile(homedir .. "/.lute/typedefs/0.1.0/" .. key, value) From 696b3e7bcbe0c202d8f86171feb84a3c0413f991 Mon Sep 17 00:00:00 2001 From: ariel Date: Wed, 8 Oct 2025 11:02:20 -0700 Subject: [PATCH 021/642] infra: make some minor improvements to the bootstrap script and the build system (#396) This PR is a bit of a grab bag of small fixes to both the build system and the bootstrap script. The important fix here is that we force `CURL_ZSTD` off so that we won't have linking failures if you _happen_ to have zstd present on your system for other reasons. Otherwise, we tweak the message a bit when printing the Lute version in CMake, and tweak the bootstrap script to echo the key commands it's running to make the whole output from it easier to interpret. We also change the default behavior for the bootstrap script to assume something vaguely POSIX-y without specialized needs, which makes the script work on additional Unix platforms like FreeBSD. Finally, we change `bootstrapped-lute` to `lute0` for the initial version of lute generated without the standard library embedded in it because the term `bootstrapped` here feels confusing as it reads like it's the _final_ version, rather than an intermediary. `lute0` is "the first build" and, god help us, if our process is ever multistaged for bootstrapping, it has an obvious extension to `lute1`, `lute2`, etc. This may be reasonable to do later for validation if only to make sure that `lute1` and `lute2` are the same executable, byte for byte. --- CMakeBuild/get_version.cmake | 2 +- CMakeLists.txt | 2 +- README.md | 30 ++++++--- tools/bootstrap.sh | 116 ++++++++++++++++++----------------- 4 files changed, 82 insertions(+), 68 deletions(-) diff --git a/CMakeBuild/get_version.cmake b/CMakeBuild/get_version.cmake index 6700ed106..6a49057ed 100644 --- a/CMakeBuild/get_version.cmake +++ b/CMakeBuild/get_version.cmake @@ -12,4 +12,4 @@ if(LUTE_VERSION_SUFFIX) set(LUTE_VERSION_FULL "${LUTE_VERSION_FULL}-${LUTE_VERSION_SUFFIX}") endif() -message("${LUTE_VERSION_FULL}") +message("Lute Version: ${LUTE_VERSION_FULL}") diff --git a/CMakeLists.txt b/CMakeLists.txt index 78547e596..8cdeda91a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,7 @@ set(USE_NGHTTP2 OFF) set(CURL_USE_LIBPSL OFF) set(CURL_USE_LIBSSH2 OFF) set(CURL_ZLIB ON) +SET(CURL_ZSTD OFF CACHE STRING "Disable ZSTD" FORCE) set(CURL_BROTLI OFF CACHE STRING "Disable brotli support" FORCE) set(CURL_ENABLE_SSL ON) set(CURL_USE_OPENSSL ON) @@ -90,7 +91,6 @@ include(uWebSockets) # Define include directories for uWebSockets and uSockets set(UWEBSOCKETS_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/extern/uWebSockets/src) - # libsodium for `pwhash` include(libsodium) diff --git a/README.md b/README.md index 016d72027..2eacddd8e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Lute [![CI](https://github.com/luau-lang/lute/actions/workflows/ci.yml/badge.svg)](https://github.com/luau-lang/lute/actions/workflows/ci.yml) -==== +# Lute [![CI](https://github.com/luau-lang/lute/actions/workflows/ci.yml/badge.svg)](https://github.com/luau-lang/lute/actions/workflows/ci.yml) Lute is a standalone runtime for general-purpose programming in [Luau](https://luau.org), and a collection of optional extension libraries for Luau embedders to include to expand the capabilities of the Luau scripts in their software. It is designed to make it readily feasible to use Luau to write any sort of general-purpose programs, including manipulating files, making network requests, opening sockets, and even making tooling that directly manipulates Luau scripts. @@ -11,28 +10,39 @@ We would love to hear from you about your experiences working with other Luau or ### Lute Libraries The Lute repository fundamentally contains three sets of libraries. These are as follows: + - `lute`: The core runtime libraries in C++, which provides the basic functionality for general-purpose Luau programming. - `std`: The standard library, which extends those core C++ libraries with additional functionality in Luau. - `batteries`: A collection of useful, standalone Luau libraries that do not depend on `lute`. Contributions to any of these libraries are welcome, and we encourage you to open issues or pull requests if you have any feedback or contributions to make. -### Building Lute from scratch +## Building Lute -Lute uses a script built in Luau to handle pulling in vendored dependencies and embedding our Luau standard library into the `lute` executable. As a result, you typically need a `lute` executable available already in order to build it. As a result, you'd like to build `lute` entirely from scratch, you'll have to bootstrap it. -From the root directory, run `./tools/bootstrap.sh` to build a `lute` executable. If you pass the `--install` flag, this will be -installed to `$HOME/.lute/bin/lute` by default, with a prompt provided to select an alternative install location. Make sure to add this `lute` to your `$PATH` if you would like to have `lute` resolve to it in general. +Lute has a fairly conventional C++ build system built atop CMake. However, in the interest of dogfooding Lute itself, and avoiding the trap of shipping elaborate, difficult-to-maintain CMake configurations that attempt to perform dependency resolution and code generation, we've written a build tool called `luthier` (located at `./tools/luthier.luau`). +`luthier` is written to appropriately run or re-run any of the steps in the build process as needed based on local changes, which affords a more pleasant developer experience for folks working on `lute` than invoking each step manually. Some +`luthier` subcommands like `configure` and `build` just wrap the standard CMake configuration and ninja invocations. Other commands include `fetch` which implements the logic to parse dependency information from the TOML files (named `extern/\*.tune`) and resolve them efficiently using `git`, and `generate` which performs the code generation steps necessary to embed both Lute's CLI frontend commands and Lute's standard library into the executable. +The `generate` step in particular is necessary to producing a full `lute` executable, but we provide empty versions of both embeddings (in `./tools/templates`) to be used for any builds that do _not_ embed the standard library. Since you'll need `lute` to execute `luthier` and you'll need `luthier` to run the code generation step in particular, there are a few different paths to building Lute. ### Building Lute with `lute` installed -After either bootstrapping `lute` per above or installing it using `foreman`, `rokit`, or manually from Releases, you can build modified local versions of `lute` using our build script, `luthier`, located at `./tools/luthier.luau`. To perform a clean build of Lute, you can run: +The most straightforward, and generally recommended, way to build a local version of `lute` is to have already installed an existing version of `lute`. Today, you can do that using a toolchain manager like [`foreman`](https://github.com/Roblox/foreman), [`rokit`](https://github.com/rojo-rbx/rokit), and [mise-en-place](https://mise.jdx.dev/), or by manually installing a prebuilt binary from our [Releases](https://github.com/luau-lang/lute/releases). With a copy of `lute` present on your system, you can then run the following to perform a clean build: + ```bash -/path/to/lute tools/luthier.luau build --clean {lute | Lute.CLI | Lute.tests} +# with `lute` on your path... +lute tools/luthier.luau build --clean {lute | Lute.CLI | Lute.Test} +# or referring directly to a specific location... +/path/to/lute tools/luthier.luau build --clean {lute | Lute.CLI | Lute.Test} +# you can also use `run`, instead of `build`, to also invoke the appropriate executable afterwards! ``` -## Manually building Lute +### Building Lute from scratch + +If you wish to build `lute` from scratch without a version of `lute` already present on your machine, this is still possible! We've provided a small, easily auditable shell script `./tools/bootstrap.sh` that will perform the entire build process in sequence for a totally fresh build. This entails first building a debug version of `lute` called `lute0` without any of the CLI commands implemented in Luau, and without the standard library embedded into the executable. We can then use `lute0` to run `luthier`, perform the requisite code generation step, and then build a fresh release version of `lute`. The script supports a single command-line option `--install` which can be used to install this release executable to a desired location on your machine. By default, this is `$HOME/.lute/bin/lute`, but the script will provide a prompt during its execution about where `lute` should be placed. In order to then use this `lute` executable, please ensure that it is accessible on your `$PATH`. + +### Manually building Lute -Outside of resolving the dependencies, Lute's build system is implemented today with `cmake`, meaning you can fairly easily pull external dependencies into `extern`, and then perform your own manual build with the following steps: +If you wish to work very manually with the build system, you can, of course, still invoke `cmake` and `ninja` directly after pulling external dependencies into `extern` by hand. In order for the build to succeed, you'll have to provide _some_ version of a handful of generated files, but we provide empty versions to use in `./tools/templates`. A manual build therefore would look something like: - Copy all of the templates from `./tools/templates` into the right locations to support building a version with no embedded Luau functionality, e.g. ```bash diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh index 6f7c2a6f7..42b5a3cb7 100755 --- a/tools/bootstrap.sh +++ b/tools/bootstrap.sh @@ -1,120 +1,124 @@ +#!/usr/bin/env bash + set -e +install_requested=false -fetch_dep() { +# simple argument parsing +for arg in "$@"; do + if [[ "$arg" == "--install" ]]; then + install_requested=true + break + fi +done + +# function to display commands before running them +exe() { echo ": $@" ; "$@" ; } + +fetch_dependency() { local dep_file="$1" - # Ensure the file exists + # check the file exists if [[ ! -f "$dep_file" ]]; then - echo "Dependency file not found: $dep_file" + echo "missing dependency file: $dep_file" return 1 fi - # Extract each field by key + # extract each field by key name=$(grep '^name' "$file" | sed -E 's/^name *= *"(.*)"/\1/') remote=$(grep '^remote' "$file" | sed -E 's/^remote *= *"(.*)"/\1/') branch=$(grep '^branch' "$file" | sed -E 's/^branch *= *"(.*)"/\1/') revision=$(grep '^revision' "$file" | sed -E 's/^revision *= *"(.*)"/\1/') - # Output the parsed variables + # output the parsed variables local target_dir="extern/$name" - # Get version string (e.g., "git version 2.50.1") - version_str=$(git --version) + # get git's version string to determine whether to use revision or branch clones (e.g., "git version 2.50.1") + version_str=$(git --version) # Extract the major and minor version numbers using regex # e.g., 2 and 50 from "git version 2.50.1" if [[ $version_str =~ ([0-9]+)\.([0-9]+)\.[0-9]+ ]]; then gitMajor="${BASH_REMATCH[1]}" gitMinor="${BASH_REMATCH[2]}" - # Now perform the version check + # git 2.49 added support for `--revision` to `git clone` if [[ "$gitMajor" -ge 3 || ( "$gitMajor" -eq 2 && "$gitMinor" -ge 49 ) ]]; then - git clone --depth=1 --revision $revision $remote $target_dir + exe git clone --depth=1 --revision $revision $remote $target_dir else - git clone --depth=1 --branch $branch $remote $target_dir + exe git clone --depth=1 --branch $branch $remote $target_dir fi else - echo "Could not parse Git version from: $version_str" + echo "ill-formed git version string: $version_str" exit 1 fi } -# Download dependencies from the tune files +# download dependencies from the tune files find "extern" -mindepth 1 ! -name "*.tune" -prune -exec rm -rf {} + - for file in extern/*.tune; do if [[ -f "$file" ]]; then - echo "Fetching dependency from: $(basename "$file")" - fetch_dep "extern/$(basename "$file")" + echo "fetching $(basename "$file")" + fetch_dependency "extern/$(basename "$file")" fi done -# Generate the stdlib modules.cpp file +# place empty versions of the standard library rm -rf lute/std/src/generated mkdir -p lute/std/src/generated -cp ./tools/templates/std_impl.cpp ./lute/std/src/generated/modules.cpp -cp ./tools/templates/std_header.h ./lute/std/src/generated/modules.h +exe cp ./tools/templates/std_impl.cpp ./lute/std/src/generated/modules.cpp +exe cp ./tools/templates/std_header.h ./lute/std/src/generated/modules.h -# Generate the clicommands modules.cpp file +# place empty versions of the luau-based lute subcommands for the cli rm -rf lute/cli/generated mkdir -p lute/cli/generated -cp ./tools/templates/cli_impl.cpp ./lute/cli/generated/commands.cpp -cp ./tools/templates/cli_header.h ./lute/cli/generated/commands.h +exe cp ./tools/templates/cli_impl.cpp ./lute/cli/generated/commands.cpp +exe cp ./tools/templates/cli_header.h ./lute/cli/generated/commands.h -## Configure bootstrap lute - lute stdlib +## configure the build system for lute0 os_type="$(uname)" -BUILD_PATH="" +BUILD_ROOT="" EXE_PATH=lute/cli/lute -OUT_BINARY=./build/bootstrapped-lute -if [[ "$os_type" == "Linux" ]]; then - BUILD_PATH=build/debug -elif [[ "$os_type" == "Darwin" ]]; then - BUILD_PATH=build/xcode/debug +OUT_BINARY=./build/lute0 +if [[ "$os_type" == "Darwin" ]]; then + BUILD_ROOT=build/xcode elif [[ "$os_type" == MINGW* || "$os_type" == MSYS* || "$os_type" == CYGWIN* ]]; then - BUILD_PATH=build/vs2022/debug - EXE_PATH+=".exe" - OUT_BINARY+=.exe + BUILD_ROOT=build/vs2022 + EXE_PATH+=".exe" + OUT_BINARY+=.exe else - echo "Unknown OS: $os_type, cannot bootstrap" - return 1 + BUILD_ROOT=build fi +BUILD_PATH=$BUILD_ROOT/debug rm -rf build && mkdir build -cmake -G=Ninja -B $BUILD_PATH -DCMAKE_BUILD_TYPE=Debug +exe cmake -G=Ninja -B $BUILD_PATH -DCMAKE_BUILD_TYPE=Debug -# Compile bootstrapping lute -ninja -C $BUILD_PATH $EXE_PATH +# build lute0 +exe ninja -C $BUILD_PATH $EXE_PATH echo "" -echo "Successfully built the bootstrapped lute - std" - -# Use bootstrapped lute to build lute with standard libraries included -BOOTSTRAPPED_LUTE=./$BUILD_PATH/$EXE_PATH +echo "built lute0 at $BUILD_PATH/$EXE_PATH" -mv $BOOTSTRAPPED_LUTE $OUT_BINARY -$OUT_BINARY tools/luthier.luau build --clean lute - -# optionally install bootstrapped lute -install_requested=false -# Parse flags -# Loop through all command-line arguments -for arg in "$@"; do - if [[ "$arg" == "--install" ]]; then - install_requested=true - break - fi -done +# use lute0 to build lute with the standard library included. +LUTESTRAP=./$BUILD_PATH/$EXE_PATH +mv $LUTESTRAP $OUT_BINARY +exe $OUT_BINARY tools/luthier.luau build --config release --clean lute +# optionally install the final built lute version INSTALL_DIR=$HOME/.lute/bin +BUILD_PATH=$BUILD_ROOT/release +LUTESTRAP=./$BUILD_PATH/$EXE_PATH if $install_requested; then - read -p "Enter the path where you would like to install lute to (default: $INSTALL_DIR): " USER_PATH + # TODO: give the lute executable a self-install subcommand and just invoke that here instead + read -p "where would you like to install lute? (default: $INSTALL_DIR): " USER_PATH # If user_input is empty, keep default_path; else update it if [[ -n "$USER_PATH" ]]; then INSTALL_DIR=$USER_PATH fi mkdir -p $INSTALL_DIR - cp $BOOTSTRAPPED_LUTE $INSTALL_DIR + cp $LUTESTRAP $INSTALL_DIR echo "" - echo "Installed lute to $INSTALL_DIR/lute. Make sure to add this to your PATH variable." + echo "installed lute to $INSTALL_DIR/lute" + echo "please ensure this is accessible on your \$PATH" fi From a89822ac438ec7fe02c3453d74bdee85ae3b5d2e Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 8 Oct 2025 13:30:40 -0700 Subject: [PATCH 022/642] Add longer lute command delimiter (#398) This is needed to add in the codemod source code to lute commands, because it contains the character sequence `")`, which current early terminates the raw string literal. --- tools/luthier.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/luthier.luau b/tools/luthier.luau index 30be0ee51..8b0119fd2 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -472,7 +472,7 @@ local function generateCliCommandsFilesIfNeeded() end fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) else - fs.write(cpp, `\n \{"{aliasedPath}", R"({libSrc})"},\n`) + fs.write(cpp, `\n \{"{aliasedPath}", R"LUTE_CMD({libSrc})LUTE_CMD"},\n`) end end From 0940db6fa345273e6dd8d8e5a8ff7fa7de32d655 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:32:12 -0700 Subject: [PATCH 023/642] Run configure explicitly in bootstrap.sh (#399) In order to have `compile_commands.json` in place, we want to rerun `configure` before doing the clean build during bootstrapping. This should generally be a noop, besides copying the file. --- tools/bootstrap.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh index 42b5a3cb7..6dd717cb0 100755 --- a/tools/bootstrap.sh +++ b/tools/bootstrap.sh @@ -102,6 +102,7 @@ echo "built lute0 at $BUILD_PATH/$EXE_PATH" # use lute0 to build lute with the standard library included. LUTESTRAP=./$BUILD_PATH/$EXE_PATH mv $LUTESTRAP $OUT_BINARY +exe $OUT_BINARY tools/luthier.luau configure --config release --clean lute exe $OUT_BINARY tools/luthier.luau build --config release --clean lute # optionally install the final built lute version From a745c291fe1a3ae743a7c2d58ec1de99a786874e Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:48:56 -0700 Subject: [PATCH 024/642] Fix incorrect bootstrapped executable name in setup-and-build workflow (#400) In #396, we renamed `bootstrapped-lute` to `lute0`. This PR updates the relevant GitHub workflow file with the same change. --- .github/actions/setup-and-build/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index cad6f5d9b..1af2775d6 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -66,7 +66,7 @@ runs: shell: bash run: | ./tools/bootstrap.sh - correct_lute=$(./build/bootstrapped-lute tools/luthier.luau build lute --config debug --which) + correct_lute=$(./build/lute0 tools/luthier.luau build lute --config debug --which) mv $correct_lute ./build/lute echo "luthier_lute=./build/lute" >> $GITHUB_ENV From dbf53864be4f7195ce7aab768b79f8b68fce0dff Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:33:35 -0700 Subject: [PATCH 025/642] Prevent merging failing PRs by making CI aggregator fail when `build`, `test`, or `check-format` jobs fail (#401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem Our CI aggregator job was unconditionally running and passing. This wasn't too much of an issue because contributors weren't merging their PRs until all checks passed, but PRs set to auto-merge were being merged automatically. Because of this, some PRs introduced bugs that got merged in: #386 and #396. ### Solution The aggregator job's now depends on the success of the `build`, `test`, and `check-format` jobs. If any job fails, the aggregator fails, which will block PRs from being merged. The bug in #396 was fixed in #400. To fix the bug introduced in #386, this PR also reverts that PR's changes to our GitHub Actions setup (using the bootstrap script to create a `lute` binary). We already have Foreman installed on these runners and there's no need to use bootstrapping here—in fact, it makes each CI job quite a bit slower since two fresh builds are needed in order to run anything. --- .github/actions/setup-and-build/action.yml | 26 ++-------------------- .github/workflows/ci.yml | 14 +++++++----- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index 1af2775d6..def71560d 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -60,33 +60,16 @@ runs: path: extern key: extern-${{ hashFiles('extern/*.tune') }} - # We can currently only bootstrap correctly in ci on mac/linux - - name: Bootstrap Lute (Darwin/Linux only) - if: runner.os != 'Windows' - shell: bash - run: | - ./tools/bootstrap.sh - correct_lute=$(./build/lute0 tools/luthier.luau build lute --config debug --which) - mv $correct_lute ./build/lute - echo "luthier_lute=./build/lute" >> $GITHUB_ENV - - - name: Build ${{ inputs.target }} ${{ runner.os }} - if: runner.os != 'Windows' - shell: bash - run: $luthier_lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config}} ${{ inputs.options }} - - name: Fetch ${{ inputs.target }} - if: steps.cache-extern.outputs.cache-hit != 'true' && runner.os == 'Windows' + if: steps.cache-extern.outputs.cache-hit != 'true' shell: bash run: lute ./tools/luthier.luau fetch ${{ inputs.target }} - name: Configure ${{ inputs.target }} - if: runner.os == 'Windows' shell: bash run: lute ./tools/luthier.luau configure ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} - name: Build ${{ inputs.target }} - if: runner.os == 'Windows' shell: bash run: lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} @@ -94,11 +77,6 @@ runs: id: artifact_path shell: bash run: | - if [[ ${{ runner.os }} == "Windows" ]]; then - LUTE="lute" - else - LUTE="$luthier_lute" - fi - exe_path=$($LUTE ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} --which) + exe_path=$(lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} --which) echo "Executable path: $exe_path" # Debug print echo "exe_path=$exe_path" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dde0f7a06..5e2242c15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: build +name: Gated Commits on: push: @@ -85,10 +85,14 @@ jobs: aggregator: name: Gated Commits runs-on: ubuntu-latest - needs: - - build + needs: [build, test, check-format] if: ${{ always() }} steps: - - name: Aggregate + - name: Aggregator run: | - echo "All required jobs completed" + if [ "${{ needs.build.result }}" == "success" ] && [ "${{ needs.test.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ]; then + echo "All jobs succeeded" + else + echo "One or more jobs failed" + exit 1 + fi From 83dbed3f7f015c150bbcd44bd7a7ed850e453674 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:12:19 -0700 Subject: [PATCH 026/642] Infra: stop running in-progress CI workflow when a new commit is pushed (#402) We use [concurrency groups](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency) to cancel in-progress CI runs when they're invalidated by a new commit. --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e2242c15..9555d98eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: pull_request: branches: ["primary"] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ${{ matrix.os }} From 77b2591ffc2ebb8b558b8b9d325458d1ef448b6f Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 9 Oct 2025 10:32:41 -0700 Subject: [PATCH 027/642] bugfix: Lute's testing framework swaps tests passed and tests failed (#406) --- lute/std/libs/test.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index 51c9a2dd8..a3a3a8714 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -43,8 +43,8 @@ local function simpleReporter(result: TestRunResult) print(string.rep("-", 50)) print(`Total: {result.total}`) - print(`Passed: {result.failed} ✓`) - print(`Failed: {result.passed} ✗`) + print(`Passed: {result.passed} ✓`) + print(`Failed: {result.failed} ✗`) print(string.rep("=", 50) .. "\n") end From f23a9460f1115faa30898b8468998373441e0c01 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:33:46 -0700 Subject: [PATCH 028/642] Enable alias discovery even when ancestors in VFS are technically ambiguous (#405) Resolves #332. If navigating up a virtual filesystem yields `Ambiguous` during the alias discovery phase, the Luau.Require library currently fails silently. This is an issue, but Lute could also do better here. In the following filesystem, `main.luau`'s parent is _technically_ ambiguous, but because filesystems are trees, it's not actually ambiguous whether we should navigate to the file `folder.luau` or the directory `folder` (the latter is correct): - folder.luau - folder - main.luau Solution: in the case of parent operations in the VFS, ambiguity is overridden to be treated as a success. I'll also be putting up a separate PR to fix the silent error in the Luau.Require library. --- lute/require/src/modulepath.cpp | 6 ++++-- tests/src/require.test.cpp | 15 +++++++++++++++ .../with_config/src/parent_ambiguity/.luaurc | 5 +++++ .../with_config/src/parent_ambiguity/folder.luau | 0 .../src/parent_ambiguity/folder/requirer.luau | 1 + .../with_config/src/parent_ambiguity/foo.luau | 1 + 6 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 tests/src/require/with_config/src/parent_ambiguity/.luaurc create mode 100644 tests/src/require/with_config/src/parent_ambiguity/folder.luau create mode 100644 tests/src/require/with_config/src/parent_ambiguity/folder/requirer.luau create mode 100644 tests/src/require/with_config/src/parent_ambiguity/foo.luau diff --git a/lute/require/src/modulepath.cpp b/lute/require/src/modulepath.cpp index 059fde5c8..62cc3d332 100644 --- a/lute/require/src/modulepath.cpp +++ b/lute/require/src/modulepath.cpp @@ -151,7 +151,7 @@ std::string ModulePath::getPotentialLuaurcPath() const ResolvedRealPath result = getRealPath(); // No navigation has been performed; we should already be in a valid state. - assert(result.status == NavigationStatus::Success); + assert(result.status != NavigationStatus::NotFound); std::string_view directory = result.realPath; @@ -188,7 +188,9 @@ NavigationStatus ModulePath::toParent() if (relativePathToTrack) relativePathToTrack = normalizePath(joinPaths(*relativePathToTrack, "..")); - return getRealPath().status; + // There is no ambiguity when navigating up in a tree. + NavigationStatus status = getRealPath().status; + return status == NavigationStatus::Ambiguous ? NavigationStatus::Success : status; } NavigationStatus ModulePath::toChild(const std::string& name) diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index 72f13a958..20d3ddec1 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -152,6 +152,21 @@ TEST_CASE("require_modules") } } +TEST_CASE("require_with_parent_ambiguity") +{ + // This test case cannot be included in the general "require_modules" test + // because ambiguity prevents the test's requirer.luau from navigating to + // this test's entry point. Instead, we manually start the entry point here. + + char executablePlaceholder[] = "lute"; + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string requirer = joinPaths(luteProjectRoot, "tests/src/require/with_config/src/parent_ambiguity/folder/requirer.luau"); + std::vector argv = {executablePlaceholder, requirer.data()}; + CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + } +} + TEST_CASE("require_types") { char executablePlaceholder[] = "lute"; diff --git a/tests/src/require/with_config/src/parent_ambiguity/.luaurc b/tests/src/require/with_config/src/parent_ambiguity/.luaurc new file mode 100644 index 000000000..24f8e9ff1 --- /dev/null +++ b/tests/src/require/with_config/src/parent_ambiguity/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "foo": "./foo", + } +} diff --git a/tests/src/require/with_config/src/parent_ambiguity/folder.luau b/tests/src/require/with_config/src/parent_ambiguity/folder.luau new file mode 100644 index 000000000..e69de29bb diff --git a/tests/src/require/with_config/src/parent_ambiguity/folder/requirer.luau b/tests/src/require/with_config/src/parent_ambiguity/folder/requirer.luau new file mode 100644 index 000000000..2f613057d --- /dev/null +++ b/tests/src/require/with_config/src/parent_ambiguity/folder/requirer.luau @@ -0,0 +1 @@ +return require("@foo") diff --git a/tests/src/require/with_config/src/parent_ambiguity/foo.luau b/tests/src/require/with_config/src/parent_ambiguity/foo.luau new file mode 100644 index 000000000..aa00aca1e --- /dev/null +++ b/tests/src/require/with_config/src/parent_ambiguity/foo.luau @@ -0,0 +1 @@ +return { "result from foo" } From 392899ad852a71b69d46c63cb8d95a6f07b7ac1d Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 9 Oct 2025 10:34:24 -0700 Subject: [PATCH 029/642] Register argc and argv for cli commands (#403) This lets Luau subcommands see command line arguments Plus a clang-format change --- lute/cli/src/climain.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 721faed14..fbb0c0e53 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -219,7 +219,8 @@ static bool checkValidPath(std::filesystem::path& filePath) } // if the file has an explicit extension, dont do a fallback - if (filePath.has_extension()) { + if (filePath.has_extension()) + { return false; } @@ -453,6 +454,8 @@ int cliMain(int argc, char** argv) } else if (std::optional result = getCliCommand(command); result) { + program_argc = argc - argOffset; + program_argv = &argv[argOffset]; return handleCliCommand(*result); } else From 325f74508b825e76a8c0d6c41f8a90014cff8d0e Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:38:33 -0700 Subject: [PATCH 030/642] [net.serve] Integrate app with main libuv event loop to prevent blocking the task scheduling system (#404) Resolves #384. ### Problem The current `net.serve` logic is to schedule a continuation that runs every task scheduler step, which (1) explicitly calls `run()` on our `uWS::TemplatedApp`, and (2) schedules itself to run the next frame. This would work if `run()` were actually `step()`, but uWS doesn't provide an easy way to step an app. ### Solution After some investigation, because uWS is configured to use libuv under the hood, we can patch in the libuv event loop that our task system is managing by calling `uWS::Loop::get(uv_default_loop())` before creating the app object. We get many things for free here (e.g. reference counting for keeping the server alive, managed by libuv), and it's simpler to reason about. This also means we don't need to explicitly "run" the app; it simply gets registered to run on the same event loop. To ensure this works, some minor changes were made to the task scheduler logic to ensure that we are consuming events from the libuv event loop if it is live and to ensure we aren't blocking on I/O. --- lute/net/src/net.cpp | 25 ++++++------------------- lute/runtime/src/runtime.cpp | 8 ++++++-- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 7fc90bd8f..2c3fafeb4 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -4,12 +4,16 @@ #include "curl/curl.h" #include "App.h" +#include "Loop.h" #include "Luau/DenseHash.h" #include "Luau/Variant.h" #include "lua.h" #include "lualib.h" +#include "uv.h" +#include +#include #include #include #include @@ -519,6 +523,8 @@ bool closeServer(int serverId) int lua_serve(lua_State* L) { + uWS::Loop::get(uv_default_loop()); + std::string hostname = "0.0.0.0"; int port = 3000; bool reusePort = false; @@ -642,28 +648,9 @@ int lua_serve(lua_State* L) return 0; } - state->loopFunction = [state]() - { - if (!state->running) - { - return; - } - Luau::visit( - [](auto* appPtr) - { - if (appPtr) - appPtr->run(); - }, - state->app - ); - state->runtime->schedule(state->loopFunction); - }; - serverInstances[serverId] = std::move(app); serverStates[serverId] = state; - runtime->schedule(state->loopFunction); - lua_createtable(L, 0, 3); lua_pushstring(L, "hostname"); diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 5dac64d64..5650b922a 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -50,12 +50,16 @@ Runtime::~Runtime() bool Runtime::hasWork() { - return hasContinuations() || hasThreads() || activeTokens.load() != 0; + // TODO: activeTokens and uv_loop_alive have a decent amount of overlap. + // Unfortunately, we do currently have some places where we add/release + // tokens that don't correspond to libuv activity, so for now we keep both. + // uv_ref/unref could be used to patch tokens into the libuv loop itself. + return hasContinuations() || hasThreads() || activeTokens.load() != 0 || uv_loop_alive(uv_default_loop()); } RuntimeStep Runtime::runOnce() { - uv_run(uv_default_loop(), UV_RUN_DEFAULT); + uv_run(uv_default_loop(), UV_RUN_NOWAIT); // Complete all C++ continuations std::vector> copy; From fd2c7cab235fabb2dcb6ea70cc66533e3aa97e7d Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:42:45 -0700 Subject: [PATCH 031/642] [CLI] Use require-by-string semantics during file discovery (#407) Resolves #306. Enables discovering files to run by using require-by-string semantics. The bulk of the changes here enable creating a `ModulePath` object, which handles all the require-by-string semantics for us (with some minor changes made to `ModulePath`'s API to easily extract whether we're pointing at a file or directory). Other than this, I removed `#include ` to remain consistent with how the rest of Lute handles these operations. I also changed up some function signatures: ```cpp // old (mutates filePath directly) bool checkValidPath(std::filesystem::path& filePath) // new (returns an optional with the new value) std::optional getValidPath(std::string filePath) ``` If we are unable to find a file to run using require-by-string semantics, we still fallback to checking `./.lute/file.luau` and `./.lute/file.lua`. See test for expected behavior: `require_by_string_semantics_in_cli`. --- lute/cli/src/climain.cpp | 67 +++++++++++++++++--------- lute/require/include/lute/modulepath.h | 7 +++ lute/require/src/modulepath.cpp | 19 ++++---- tests/src/require.test.cpp | 30 ++++++++++++ 4 files changed, 91 insertions(+), 32 deletions(-) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index fbb0c0e53..e55f59f6d 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -25,7 +25,6 @@ #include #include -#include #include static int program_argc = 0; @@ -211,38 +210,59 @@ static int assertionHandler(const char* expr, const char* file, int line, const return 1; } -static bool checkValidPath(std::filesystem::path& filePath) +static std::optional getWithRequireByStringSemantics(std::string filePath) { - if (std::filesystem::exists(filePath)) - { - return true; - } + std::string normalized = normalizePath(std::move(filePath)); - // if the file has an explicit extension, dont do a fallback - if (filePath.has_extension()) + std::string rootOfPath; + std::string restOfPath = normalized; + if (size_t firstSlash = normalized.find_first_of("\\/"); firstSlash != std::string::npos) { - return false; + rootOfPath = normalized.substr(0, firstSlash); + restOfPath = normalized.substr(firstSlash + 1); } - std::filesystem::path fallbackPath = ".lute" / filePath; + std::optional mp = ModulePath::create(std::move(rootOfPath), std::move(restOfPath), isFile, isDirectory); + if (!mp) + return std::nullopt; + + ResolvedRealPath resolved = mp->getRealPath(); + if (resolved.status != NavigationStatus::Success) + return std::nullopt; + + if (resolved.type == ResolvedRealPath::PathType::File) + return resolved.realPath; + + return std::nullopt; +}; + +static std::optional getValidPath(std::string filePath) +{ + if (std::optional path = getWithRequireByStringSemantics(filePath)) + return *path; + + // Only fallback to checking .lute/* if the original path has no extension. + if (filePath.find('.') != std::string::npos) + return std::nullopt; + + std::string fallbackPath = joinPaths(".lute", filePath); + size_t fallbackSize = fallbackPath.size(); for (const auto& ext : {".luau", ".lua"}) { - fallbackPath.replace_extension(ext); + fallbackPath.resize(fallbackSize); + fallbackPath += ext; - if (std::filesystem::exists(fallbackPath)) - { - filePath = std::move(fallbackPath); - return true; - } + if (isFile(fallbackPath)) + return fallbackPath; } - return false; + return std::nullopt; } int handleRunCommand(int argc, char** argv, int argOffset) { - std::optional filePath; + std::string filePath; for (int i = argOffset; i < argc; ++i) { @@ -261,14 +281,14 @@ int handleRunCommand(int argc, char** argv, int argOffset) } else { - filePath.emplace(currentArg); + filePath = currentArg; program_argc = argc - i; program_argv = &argv[i]; break; } } - if (!filePath) + if (filePath.empty()) { fprintf(stderr, "Error: No file specified for 'run' command.\n\n"); displayRunHelp(argv[0]); @@ -278,13 +298,14 @@ int handleRunCommand(int argc, char** argv, int argOffset) Runtime runtime; lua_State* L = setupCliState(runtime); - if (!checkValidPath(filePath.value())) + std::optional validPath = getValidPath(filePath); + if (!validPath) { - std::cerr << "Error: File '" << filePath->string() << "' does not exist.\n"; + std::cerr << "Error: File '" << filePath << "' does not exist.\n"; return 1; } - bool success = runFile(runtime, filePath->string().c_str(), L); + bool success = runFile(runtime, validPath->c_str(), L); return success ? 0 : 1; } diff --git a/lute/require/include/lute/modulepath.h b/lute/require/include/lute/modulepath.h index c6c0ca667..92660d63b 100644 --- a/lute/require/include/lute/modulepath.h +++ b/lute/require/include/lute/modulepath.h @@ -12,9 +12,16 @@ enum class NavigationStatus struct ResolvedRealPath { + enum class PathType + { + File, + Directory + }; + NavigationStatus status; std::string realPath; std::optional relativePath; + PathType type; }; class ModulePath diff --git a/lute/require/src/modulepath.cpp b/lute/require/src/modulepath.cpp index 62cc3d332..4301cccb1 100644 --- a/lute/require/src/modulepath.cpp +++ b/lute/require/src/modulepath.cpp @@ -87,7 +87,7 @@ ModulePath::ModulePath( ResolvedRealPath ModulePath::getRealPath() const { - bool found = false; + std::optional resolvedType; std::string suffix; std::string lastComponent; @@ -107,43 +107,44 @@ ResolvedRealPath ModulePath::getRealPath() const { if (isAFile(partialRealPath + std::string(potentialSuffix))) { - if (found) + if (resolvedType) return {NavigationStatus::Ambiguous}; + resolvedType = ResolvedRealPath::PathType::File; suffix = potentialSuffix; - found = true; } } } if (isADirectory(partialRealPath)) { - if (found) + if (resolvedType) return {NavigationStatus::Ambiguous}; for (std::string_view potentialSuffix : kInitSuffixes) { if (isAFile(partialRealPath + std::string(potentialSuffix))) { - if (found) + if (resolvedType) return {NavigationStatus::Ambiguous}; + resolvedType = ResolvedRealPath::PathType::File; suffix = potentialSuffix; - found = true; } } - found = true; + if (!resolvedType) + resolvedType = ResolvedRealPath::PathType::Directory; } - if (!found) + if (!resolvedType) return {NavigationStatus::NotFound}; std::optional relativePathWithSuffix; if (relativePathToTrack) relativePathWithSuffix = *relativePathToTrack + suffix; - return {NavigationStatus::Success, partialRealPath + suffix, relativePathWithSuffix}; + return {NavigationStatus::Success, partialRealPath + suffix, relativePathWithSuffix, *resolvedType}; } std::string ModulePath::getPotentialLuaurcPath() const diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index 20d3ddec1..e50898573 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -178,3 +178,33 @@ TEST_CASE("require_types") CHECK_EQ(cliMain(argv.size(), argv.data()), 0); } } + +TEST_CASE("require_by_string_semantics_in_cli") +{ + char executablePlaceholder[] = "lute"; + + // Expected to pass + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::vector inputPaths = { + joinPaths(luteProjectRoot, "tests/src/require/without_config/nested"), + joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/init.luau"), + joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/submodule"), + joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/submodule.luau"), + }; + + for (std::string& inputPath : inputPaths) + { + std::vector argv = {executablePlaceholder, inputPath.data()}; + CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + } + } + + // Expected to fail + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string inputPath = joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/init"); + std::vector argv = {executablePlaceholder, inputPath.data()}; + CHECK_NE(cliMain(argv.size(), argv.data()), 0); + } +} From cb6fc4103a29b932742f6a43d3eb8db6701166d1 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 10 Oct 2025 19:36:20 -0700 Subject: [PATCH 032/642] infra: Set up the ability to write .test.luau files that get automatically executed by our CI (#417) This implements something like what is described here: https://github.com/luau-lang/lute/issues/410 , which allows us to write test cases in luau using the std/test library. We can build lute for all the different platforms we support and then run the lute tests as part of a separate pass. Test Failure documented here: https://github.com/luau-lang/lute/actions/runs/18421725097/job/52496749349?pr=417 Resolves #410. --- .github/workflows/ci.yml | 20 +++++++++++++------- lute/std/libs/test.luau | 6 +++++- tests/smoke.test.luau | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 tests/smoke.test.luau diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9555d98eb..84757cc5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ concurrency: cancel-in-progress: true jobs: - build: + run-lutecli-luau-tests: runs-on: ${{ matrix.os }} strategy: @@ -30,6 +30,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup and Build Lute + id: build_lute uses: ./.github/actions/setup-and-build with: target: Lute.CLI @@ -37,7 +38,13 @@ jobs: options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} - test: + - name: Remove .luaurc file + run: rm .luaurc + + - name: Run Luau Tests + run: find tests -name "*.test.luau" | xargs -I {} ${{ steps.build_lute.outputs.exe_path }} run {} + + run-lute-cxx-unittests: runs-on: ${{ matrix.os }} strategy: @@ -56,7 +63,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup and Build Tests + - name: Setup and Build C++ Unit Tests id: build_tests uses: ./.github/actions/setup-and-build with: @@ -64,11 +71,10 @@ jobs: config: debug options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} - - name: Remove .luaurc file run: rm .luaurc - - name: Run Tests + - name: Run C++ Tests run: ${{ steps.build_tests.outputs.exe_path }} check-format: @@ -89,12 +95,12 @@ jobs: aggregator: name: Gated Commits runs-on: ubuntu-latest - needs: [build, test, check-format] + needs: [run-lutecli-luau-tests, run-lute-cxx-unittests, check-format] if: ${{ always() }} steps: - name: Aggregator run: | - if [ "${{ needs.build.result }}" == "success" ] && [ "${{ needs.test.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ]; then + if [ "${{ needs.run-lutecli-luau-tests.result }}" == "success" ] && [ "${{ needs.run-lute-cxx-unittests.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ]; then echo "All jobs succeeded" else echo "One or more jobs failed" diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index a3a3a8714..736453022 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -1,5 +1,5 @@ --!strict - +local ps = require("@lute/process") type Test = () -> () type TestCase = { name: string, case: Test } type TestCases = { TestCase } @@ -161,6 +161,10 @@ function test.run() passed = passed, } env.reporter(result) + if failed ~= 0 then + ps.exit(1) + end + ps.exit(0) end return table.freeze(test) diff --git a/tests/smoke.test.luau b/tests/smoke.test.luau new file mode 100644 index 000000000..7ace4d649 --- /dev/null +++ b/tests/smoke.test.luau @@ -0,0 +1,15 @@ +local test = require("@std/test") + +test.suite("SmokeSuite", function() + test.case("make_fail", function() + -- test.assert.eq(1, 2) + test.assert.eq(1, 1) -- comment this and uncomment above to make the test fail + end) + test.case("make_pass", function() + test.assert.eq(1, 1) + end) +end) + +test.case("Passing", function() end) + +test.run() From 63019277d4b7c61b2b5089740a12521eb704181b Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Sat, 11 Oct 2025 08:09:59 -0700 Subject: [PATCH 033/642] Fix release workflow by stripping away extraneous information from `get_version.cmake` output (#419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our release workflow depends on the output of `get_version.cmake` being the raw version. We added some extra text in #396 that is currently causing errors during the release tag generation step, so this PR reverts that change. Someone reached out about recent changes not being "released" yet, which was surprising because we're on a nightly release schedule—this was the root cause. --- CMakeBuild/get_version.cmake | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeBuild/get_version.cmake b/CMakeBuild/get_version.cmake index 6a49057ed..fb7f2109b 100644 --- a/CMakeBuild/get_version.cmake +++ b/CMakeBuild/get_version.cmake @@ -12,4 +12,6 @@ if(LUTE_VERSION_SUFFIX) set(LUTE_VERSION_FULL "${LUTE_VERSION_FULL}-${LUTE_VERSION_SUFFIX}") endif() -message("Lute Version: ${LUTE_VERSION_FULL}") +# The release workflow depends on this exact message format to extract the +# version. If making changes, update the workflow accordingly. +message("${LUTE_VERSION_FULL}") From 92e85d4455b14a178a33a334b436a911d4b45de2 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Sat, 11 Oct 2025 08:24:34 -0700 Subject: [PATCH 034/642] stdlib: Make the json library part of the lute std library (#418) This PR comes with a small change to tools/luthier.luau to pick a different delimiter string. --- examples/json.luau | 2 +- {batteries => lute/std/libs}/json.luau | 2 -- tools/luthier.luau | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) rename {batteries => lute/std/libs}/json.luau (99%) diff --git a/examples/json.luau b/examples/json.luau index 8d5e71d2d..1ff086df3 100644 --- a/examples/json.luau +++ b/examples/json.luau @@ -1,4 +1,4 @@ -local json = require("@batteries/json") +local json = require("@std/json") local serialize = json.serialize({ hello = "world", diff --git a/batteries/json.luau b/lute/std/libs/json.luau similarity index 99% rename from batteries/json.luau rename to lute/std/libs/json.luau index 33c41ffd5..377c60ddc 100644 --- a/batteries/json.luau +++ b/lute/std/libs/json.luau @@ -1,5 +1,3 @@ ---!strict - local json = { --- Not actually a nil value, a newproxy stand-in for a null value since Luau has no actual representation of `null` NULL = newproxy() :: nil, diff --git a/tools/luthier.luau b/tools/luthier.luau index 8b0119fd2..0cd3f6fac 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -303,7 +303,7 @@ local function generateStdLibFilesIfNeeded() end fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) else - fs.write(cpp, `\n \{"{aliasedPath}", R"({libSrc})"},\n`) + fs.write(cpp, `\n \{"{aliasedPath}", R"LUTE_STDLIB({libSrc})LUTE_STDLIB"},\n`) end end From 385e190fefdf7ade49318286ce9ba70c9b86d711 Mon Sep 17 00:00:00 2001 From: checkraisefold Date: Sun, 12 Oct 2025 12:30:26 -0700 Subject: [PATCH 035/642] Use system CA store on net.request (#422) tin makes net.request work with https on windows. should also work on linux according to the [docs](https://curl.se/libcurl/c/CURLOPT_SSL_OPTIONS.html) --- lute/net/src/net.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 2c3fafeb4..e666e1a68 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -65,6 +65,7 @@ static CurlResponse requestData( curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); if (method != "GET") curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str()); From 9dfa2218b1558e46bb34966d5ca56fbe62881789 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Sun, 12 Oct 2025 12:31:30 -0700 Subject: [PATCH 036/642] Fix use-after-free bug in `process.cpp` (#421) ### Problem The issue can be replicated with a simple script that calls `process.run` twice, like this: ```luau local process = require("@lute/process") process.run({"echo", "hello!"}) process.run({"echo", "goodbye!"}) ``` Very rarely, some runs throw unexpected errors, crash, or corrupt Lute output with random bytes. The problem is a subtle UAF bug in `process.cpp`. Despite the small PR diff, this was a very tricky bug to nail down. Had to go digging pretty deep into the libuv source to understand what was going on. Essentially, it's not correct to check if the `stdout` and `stderr` pipes are active before closing them. Since these pipes are used as read-only data streams, they're automatically made inactive by libuv upon reaching EOF. In this situation, we don't need to call `uv_read_stop` on these streams because they're already stopped (although it is fine to do it; `uv_read_stop` is [idempotent](https://docs.libuv.org/en/v1.x/stream.html#c.uv_read_stop)), but we do still need to close the underlying pipe handle. We're currently in a situation where calling `process.run` might immediately close either the `stdout` or `stderr` streams (at EOF), causing the `uv_is_active` check to fail, preventing us from calling `uv_close` on the handle and from incrementing `pendingCloses`. Failing to close these handles means that libuv retains data internally associated with them, and by not incrementing `pendingCloses` (essentially a refcount), we end up destroying the owning `ProcessHandle` of these handles before libuv has been able to clean them up internally. Then, when we go to initialize new handles for the next `process.run` call, we have a UAF manifest deep inside of libuv's circular doubly linked queue code: adding a new handle to the data structure requires adjusting pointers on the last-added handle, which happens to be a handle created from the previous `process.run` call. At this point, it is freed memory that was supposed to be removed from libuv's internals before being freed (using `uv_close`). ### Solution Check instead if the handles still need closing with `!uv_is_closing` since `uv_is_active` is not relevant to whether `uv_close` needs to be called in this case. --- lute/process/src/process.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 7ec3cdcec..57bf96bd3 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -42,19 +42,19 @@ struct ProcessHandle } }; - if (stdoutPipe.loop && uv_is_active((uv_handle_t*)&stdoutPipe)) + if (!uv_is_closing((uv_handle_t*)&stdoutPipe)) { pendingCloses++; uv_read_stop((uv_stream_t*)&stdoutPipe); uv_close((uv_handle_t*)&stdoutPipe, closeCb); } - if (stderrPipe.loop && uv_is_active((uv_handle_t*)&stderrPipe)) + if (!uv_is_closing((uv_handle_t*)&stderrPipe)) { pendingCloses++; uv_read_stop((uv_stream_t*)&stderrPipe); uv_close((uv_handle_t*)&stderrPipe, closeCb); } - if (process.loop) + if (!uv_is_closing((uv_handle_t*)&process)) { pendingCloses++; uv_close((uv_handle_t*)&process, closeCb); From 4eca5c73edb56d6acab71b116e01a152e756ff41 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 13 Oct 2025 09:27:17 -0700 Subject: [PATCH 037/642] Add draft release step to release workflow for uploading artifacts (#423) --- .github/workflows/release.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 466adb12b..c27c13582 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -154,11 +154,17 @@ jobs: RELEASE_TAG="$VERSION$NIGHTLY_TAG" echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT - - name: Create Release + - name: Create Draft Release uses: luau-lang/action-gh-release@v2 with: tag_name: ${{ steps.tag_release.outputs.tag }} name: ${{ steps.tag_release.outputs.tag }} - draft: false + draft: true prerelease: ${{ github.event.inputs.nightly || github.event_name == 'schedule' }} files: release/*.zip + + - name: Publish Release + uses: luau-lang/action-gh-release@v2 + with: + tag_name: ${{ steps.tag_release.outputs.tag }} + draft: false From 491e05dc8a513191b13816ec76fc3a924e5224d0 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:00:37 -0700 Subject: [PATCH 038/642] stdlib: add `system` and boolean environment checks (#416) ### Problem There was no std lib for `system` related functionalities. ### Solution We introduce a `std` library analogue for `system`, where we can now check the environment with boolean flags. Closes #167 To test (with huge help from @vrn-sn), we wanted to write a C++ file that compares the "os" captured by the C++ executable and compares to the `@std/system` OS. To write that (and other similar output comparison test cases), we exposed the `CliRuntimeFixture` and a `runCode()` function that was used by `require.test.cpp` before but we added a `capture()` function based on the `Luau/ReplFixture` class. Then, we changed some function signatures to ensure the program args (`argc` and `argv`) are not statically passed to avoid some tests failing. ### Example: ```lua local system = require("@std/system") if system.linux then print("Running on Linux") end ``` See `examples/system_environment_check.luau` for example use case --- examples/system_environment_check.luau | 13 +++++ lute/cli/include/lute/climain.h | 5 +- lute/cli/src/climain.cpp | 36 ++++++------ lute/runtime/include/lute/runtime.h | 2 +- lute/runtime/src/runtime.cpp | 2 +- lute/std/libs/system.luau | 32 +++++++++++ tests/CMakeLists.txt | 6 +- tests/src/cliruntimefixture.h | 56 +++++++++++++++++++ tests/src/require.test.cpp | 24 +------- tests/src/require/lute/std.luau | 2 + tests/src/stdsystem.test.cpp | 44 +++++++++++++++ ....spec.luau => testAstSerializer.test.luau} | 0 12 files changed, 175 insertions(+), 47 deletions(-) create mode 100644 examples/system_environment_check.luau create mode 100644 lute/std/libs/system.luau create mode 100644 tests/src/cliruntimefixture.h create mode 100644 tests/src/stdsystem.test.cpp rename tests/{testAstSerializer.spec.luau => testAstSerializer.test.luau} (100%) diff --git a/examples/system_environment_check.luau b/examples/system_environment_check.luau new file mode 100644 index 000000000..c061986b8 --- /dev/null +++ b/examples/system_environment_check.luau @@ -0,0 +1,13 @@ +local system = require("@std/system") + +if system.win32 then + print("Running on Windows") +elseif system.linux then + print("Running on Linux") +elseif system.macos then + print("Running on macOS") +elseif system.unix then + print("Running on Unix") +else + print("Unknown operating system") +end diff --git a/lute/cli/include/lute/climain.h b/lute/cli/include/lute/climain.h index d2bc33aae..4841d7ab3 100644 --- a/lute/cli/include/lute/climain.h +++ b/lute/cli/include/lute/climain.h @@ -1,7 +1,10 @@ #pragma once +#include +#include struct lua_State; struct Runtime; -lua_State* setupCliState(Runtime& runtime); +lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit = nullptr); int cliMain(int argc, char** argv); +bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::string& chunkname, lua_State* GL, int program_argc, char** program_argv); \ No newline at end of file diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index e55f59f6d..8277fb7de 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -1,13 +1,13 @@ +#include "lute/climain.h" + #include "Luau/Common.h" #include "Luau/CodeGen.h" #include "Luau/Compiler.h" #include "Luau/FileUtils.h" -#include "Luau/Parser.h" #include "Luau/Require.h" #include "lua.h" #include "lualib.h" -#include "uv.h" #include "lute/clicommands.h" #include "lute/clivfs.h" @@ -27,9 +27,6 @@ #include #include -static int program_argc = 0; -static char** program_argv = nullptr; - void* createCliRequireContext(lua_State* L) { void* ctx = lua_newuserdatadtor( @@ -55,16 +52,18 @@ void* createCliRequireContext(lua_State* L) return ctx; } -lua_State* setupCliState(Runtime& runtime) +lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit) { return setupState( runtime, - [](lua_State* L) + [preSandboxInit = std::move(preSandboxInit)](lua_State* L) { if (Luau::CodeGen::isSupported()) Luau::CodeGen::create(L); luaopen_require(L, requireConfigInit, createCliRequireContext(L)); + if (preSandboxInit) + preSandboxInit(L); } ); } @@ -80,7 +79,7 @@ bool setupArguments(lua_State* L, int argc, char** argv) return true; } -static bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::string& chunkname, lua_State* GL) +bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::string& chunkname, lua_State* GL, int program_argc, char** program_argv) { // module needs to run in a new thread, isolated from the rest lua_State* L = lua_newthread(GL); @@ -120,7 +119,7 @@ static bool runBytecode(Runtime& runtime, const std::string& bytecode, const std return runtime.runToCompletion(); } -static bool runFile(Runtime& runtime, const char* name, lua_State* GL) +static bool runFile(Runtime& runtime, const char* name, lua_State* GL, int program_argc, char** program_argv) { if (isDirectory(name)) { @@ -139,7 +138,7 @@ static bool runFile(Runtime& runtime, const char* name, lua_State* GL) std::string bytecode = Luau::compile(*source, copts()); - return runBytecode(runtime, bytecode, chunkname, GL); + return runBytecode(runtime, bytecode, chunkname, GL, program_argc, program_argv); } static void displayHelp(const char* argv0) @@ -263,6 +262,8 @@ static std::optional getValidPath(std::string filePath) int handleRunCommand(int argc, char** argv, int argOffset) { std::string filePath; + int program_argc = 0; + char** program_argv = nullptr; for (int i = argOffset; i < argc; ++i) { @@ -305,7 +306,7 @@ int handleRunCommand(int argc, char** argv, int argOffset) return 1; } - bool success = runFile(runtime, validPath->c_str(), L); + bool success = runFile(runtime, validPath->c_str(), L, program_argc, program_argv); return success ? 0 : 1; } @@ -410,13 +411,13 @@ int handleCompileCommand(int argc, char** argv, int argOffset) return compileScript(inputFilePath, outputFilePath, argv[0]); } -int handleCliCommand(CliCommandResult result) +int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv) { Runtime runtime; lua_State* L = setupCliState(runtime); std::string bytecode = Luau::compile(std::string(result.contents), copts()); - return runBytecode(runtime, bytecode, "@" + result.path, L) ? 0 : 1; + return runBytecode(runtime, bytecode, "@" + result.path, L, program_argc, program_argv) ? 0 : 1; } int cliMain(int argc, char** argv) @@ -429,10 +430,7 @@ int cliMain(int argc, char** argv) Runtime runtime; lua_State* GL = setupCliState(runtime); - program_argc = argc; - program_argv = argv; - - bool success = runBytecode(runtime, embedded.BytecodeData, "=__EMBEDDED__", GL); + bool success = runBytecode(runtime, embedded.BytecodeData, "=__EMBEDDED__", GL, argc, argv); return success ? 0 : 1; } @@ -475,9 +473,7 @@ int cliMain(int argc, char** argv) } else if (std::optional result = getCliCommand(command); result) { - program_argc = argc - argOffset; - program_argv = &argv[argOffset]; - return handleCliCommand(*result); + return handleCliCommand(*result, argc - argOffset, &argv[argOffset]); } else { diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index 8b8d94e29..a4d417173 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -112,4 +112,4 @@ struct ResumeTokenData ResumeToken getResumeToken(lua_State* L); -lua_State* setupState(Runtime& runtime, void (*doBeforeSandbox)(lua_State*)); +lua_State* setupState(Runtime& runtime, std::function doBeforeSandbox); diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 5650b922a..6e1818681 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -334,7 +334,7 @@ static void luteopen_libs(lua_State* L) } } -lua_State* setupState(Runtime& runtime, void (*doBeforeSandbox)(lua_State*)) +lua_State* setupState(Runtime& runtime, std::function doBeforeSandbox) { // Separate VM for data copies runtime.dataCopy.reset(luaL_newstate()); diff --git a/lute/std/libs/system.luau b/lute/std/libs/system.luau new file mode 100644 index 000000000..21287647a --- /dev/null +++ b/lute/std/libs/system.luau @@ -0,0 +1,32 @@ +--!strict +-- @std/system +-- stdlib for `@lute/system` +-- Provides re-exports of `@lute/system` and boolean properties for OS detection + +local system = require("@lute/system") + +local osName = string.lower(system.os) + +local win32 = osName == "windows_nt" +local linux = osName == "linux" +local macos = osName == "darwin" +local unix = macos or linux or osName == "unix" or osName == "posix" + +return table.freeze({ + os = system.os, + arch = system.arch, + + -- function re-exports + threadcount = system.threadcount, + hostname = system.hostname, + totalmemory = system.totalmemory, + freememory = system.freememory, + uptime = system.uptime, + cpus = system.cpus, + + -- boolean properties + win32 = win32, + linux = linux, + macos = macos, + unix = unix, +}) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 796571aac..172ee49c6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,14 +3,16 @@ add_executable(Lute.Test) target_sources(Lute.Test PRIVATE src/doctest.h src/main.cpp + src/cliruntimefixture.h src/luteprojectroot.h src/luteprojectroot.cpp src/modulepath.test.cpp - src/require.test.cpp) + src/require.test.cpp + src/stdsystem.test.cpp) set_target_properties(Lute.Test PROPERTIES OUTPUT_NAME lute-tests) target_compile_features(Lute.Test PUBLIC cxx_std_17) -target_link_libraries(Lute.Test PRIVATE Lute.CLI.lib Lute.Require Lute.Runtime Luau.CLI.lib) +target_link_libraries(Lute.Test PRIVATE Lute.CLI.lib Lute.Require Lute.Runtime Luau.CLI.lib Luau.Compiler Luau.VM) target_compile_options(Lute.Test PRIVATE ${LUTE_OPTIONS}) diff --git a/tests/src/cliruntimefixture.h b/tests/src/cliruntimefixture.h new file mode 100644 index 000000000..94422b281 --- /dev/null +++ b/tests/src/cliruntimefixture.h @@ -0,0 +1,56 @@ +#pragma once +#include "doctest.h" + +#include "lute/climain.h" +#include "lute/runtime.h" + +#include "lua.h" +#include "lualib.h" + +#include "Luau/Compiler.h" + +#include + +static int capture(lua_State* L) +{ + const char* str = luaL_tolstring(L, 1, nullptr); + lua_pushstring(L, "capturedoutput"); + lua_pushstring(L, str ? str : ""); + lua_settable(L, LUA_REGISTRYINDEX); + return 0; +} + +class CliRuntimeFixture +{ +public: + CliRuntimeFixture() + : runtime(std::make_unique()) + { + L = setupCliState(*runtime, [](lua_State* L) { + lua_pushstring(L, "capturedoutput"); + lua_pushstring(L, ""); + lua_settable(L, LUA_REGISTRYINDEX); + lua_pushcfunction(L, capture, "capture"); + lua_setglobal(L, "capture"); + }); + } + + std::string getCapturedOutput() + { + lua_getfield(L, LUA_REGISTRYINDEX, "capturedoutput"); + const char* output = lua_tostring(L, -1); + lua_pop(L, 1); + return output ? output : ""; + } + + bool runCode(const std::string& source) + { + std::string bytecode = Luau::compile(source, Luau::CompileOptions()); + return runBytecode(*runtime, bytecode, "=stdin", L, 0, nullptr); + } + + lua_State* L; + +private: + std::unique_ptr runtime; +}; \ No newline at end of file diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index e50898573..410a25494 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -2,31 +2,11 @@ #include "doctest.h" #include "lute/climain.h" -#include "lute/require.h" -#include "lute/runtime.h" - -#include "Luau/Require.h" #include "lua.h" -#include "lualib.h" -#include "luteprojectroot.h" - -#include -class CliRuntimeFixture -{ -public: - CliRuntimeFixture() - : runtime(std::make_unique()) - { - L = setupCliState(*runtime); - } - - lua_State* L; - -private: - std::unique_ptr runtime; -}; +#include "cliruntimefixture.h" +#include "luteprojectroot.h" TEST_CASE_FIXTURE(CliRuntimeFixture, "require_exists") { diff --git a/tests/src/require/lute/std.luau b/tests/src/require/lute/std.luau index 0b208e162..c3624f856 100644 --- a/tests/src/require/lute/std.luau +++ b/tests/src/require/lute/std.luau @@ -2,10 +2,12 @@ local table = require("@std/table") local task = require("@std/task") local time = require("@std/time") local vector = require("@std/vector") +local system = require("@std/system") assert(type(table) == "table") assert(type(task) == "table") assert(type(time) == "table") assert(type(vector) == "table") +assert(type(system) == "table") return { "successfully required @std modules" } diff --git a/tests/src/stdsystem.test.cpp b/tests/src/stdsystem.test.cpp new file mode 100644 index 000000000..55e4fe30e --- /dev/null +++ b/tests/src/stdsystem.test.cpp @@ -0,0 +1,44 @@ +#include "doctest.h" + +#include "cliruntimefixture.h" + +#include + +std::string getHostOS() +{ +#if defined(__linux__) + return "Linux"; +#elif defined(__APPLE__) + return "Darwin"; +#elif defined(_WIN32) + return "Windows_NT"; +#else + return "unknown"; +#endif +} + +TEST_CASE_FIXTURE(CliRuntimeFixture, "std_system_os_matches_host_os") +{ + runCode(R"( + local system = require("@std/system") + capture(system.os) + )"); + CHECK(getCapturedOutput() == getHostOS()); +} + +TEST_CASE_FIXTURE(CliRuntimeFixture, "check_std_system_env_bools") +{ + std::string os = getHostOS(); + + auto checkBool = [&](const std::string& field, bool expected) { + runCode("local system = require(\"@std/system\")\n" + "capture(system." + field + ")\n"); + std::string output = getCapturedOutput(); + CHECK(output == (expected ? "true" : "false")); + }; + + checkBool("win32", os == "Windows_NT"); + checkBool("linux", os == "Linux"); + checkBool("macos", os == "Darwin"); + checkBool("unix", os == "Linux" || os == "Darwin"); +} diff --git a/tests/testAstSerializer.spec.luau b/tests/testAstSerializer.test.luau similarity index 100% rename from tests/testAstSerializer.spec.luau rename to tests/testAstSerializer.test.luau From 551c4e87bd7a726f039d3f447690bced1937aa22 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:13:01 -0700 Subject: [PATCH 039/642] refactor: split `cliruntimefixture` into .h and .cpp (#426) continuation of https://github.com/luau-lang/lute/pull/416#issuecomment-3403638037 to avoid duplicating a `static` function each time the header is included --------- Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- tests/CMakeLists.txt | 1 + tests/src/cliruntimefixture.cpp | 41 +++++++++++++++++++++++++++++++ tests/src/cliruntimefixture.h | 43 +++++---------------------------- 3 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 tests/src/cliruntimefixture.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 172ee49c6..82a5500bd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(Lute.Test PRIVATE src/doctest.h src/main.cpp src/cliruntimefixture.h + src/cliruntimefixture.cpp src/luteprojectroot.h src/luteprojectroot.cpp diff --git a/tests/src/cliruntimefixture.cpp b/tests/src/cliruntimefixture.cpp new file mode 100644 index 000000000..9a2baf32f --- /dev/null +++ b/tests/src/cliruntimefixture.cpp @@ -0,0 +1,41 @@ +#include "cliruntimefixture.h" + +#include "lua.h" +#include "lualib.h" + +#include "Luau/Compiler.h" + +static int capture(lua_State* L) +{ + const char* str = luaL_tolstring(L, 1, nullptr); + lua_pushstring(L, "capturedoutput"); + lua_pushstring(L, str ? str : ""); + lua_settable(L, LUA_REGISTRYINDEX); + return 0; +} + +CliRuntimeFixture::CliRuntimeFixture() + : runtime(std::make_unique()) +{ + L = setupCliState(*runtime, [](lua_State* L) { + lua_pushstring(L, "capturedoutput"); + lua_pushstring(L, ""); + lua_settable(L, LUA_REGISTRYINDEX); + lua_pushcfunction(L, capture, "capture"); + lua_setglobal(L, "capture"); + }); +} + +std::string CliRuntimeFixture::getCapturedOutput() +{ + lua_getfield(L, LUA_REGISTRYINDEX, "capturedoutput"); + const char* output = lua_tostring(L, -1); + lua_pop(L, 1); + return output ? output : ""; +} + +bool CliRuntimeFixture::runCode(const std::string& source) +{ + std::string bytecode = Luau::compile(source, Luau::CompileOptions()); + return runBytecode(*runtime, bytecode, "=stdin", L, 0, nullptr); +} diff --git a/tests/src/cliruntimefixture.h b/tests/src/cliruntimefixture.h index 94422b281..0801a6d1e 100644 --- a/tests/src/cliruntimefixture.h +++ b/tests/src/cliruntimefixture.h @@ -5,49 +5,18 @@ #include "lute/runtime.h" #include "lua.h" -#include "lualib.h" - -#include "Luau/Compiler.h" #include - -static int capture(lua_State* L) -{ - const char* str = luaL_tolstring(L, 1, nullptr); - lua_pushstring(L, "capturedoutput"); - lua_pushstring(L, str ? str : ""); - lua_settable(L, LUA_REGISTRYINDEX); - return 0; -} +#include class CliRuntimeFixture { public: - CliRuntimeFixture() - : runtime(std::make_unique()) - { - L = setupCliState(*runtime, [](lua_State* L) { - lua_pushstring(L, "capturedoutput"); - lua_pushstring(L, ""); - lua_settable(L, LUA_REGISTRYINDEX); - lua_pushcfunction(L, capture, "capture"); - lua_setglobal(L, "capture"); - }); - } - - std::string getCapturedOutput() - { - lua_getfield(L, LUA_REGISTRYINDEX, "capturedoutput"); - const char* output = lua_tostring(L, -1); - lua_pop(L, 1); - return output ? output : ""; - } - - bool runCode(const std::string& source) - { - std::string bytecode = Luau::compile(source, Luau::CompileOptions()); - return runBytecode(*runtime, bytecode, "=stdin", L, 0, nullptr); - } + CliRuntimeFixture(); + + std::string getCapturedOutput(); + + bool runCode(const std::string& source); lua_State* L; From 7b4e269ba4652442900875f1085150569d38388b Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 15 Oct 2025 02:40:39 +0200 Subject: [PATCH 040/642] fix: cleanup/free filesystem requests and data (#425) Some filesystem operations didn't clean up and free all their data. This PR attempts to fix this. Some general observations: - `uv_fs_req_cleanup` has to be called on all requests (sync/async). The documentation doesn't really mention this in much detail. For some operations, this might be a noop, but that's not part of the API and libuv is free to change this afaik. - When handling errors, the request wasn't always free'd. - The data associated with requests wasn't always free'd. - Some code was unreachable due to a Lua error thrown before. These errors seem hard to detect statically. Maybe there could be some RAII type for typed requests? --- lute/fs/src/fs.cpp | 84 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 2220464ea..4fce71228 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -12,7 +12,7 @@ #include #ifdef _WIN32 #include -#else +#else #include #endif #include @@ -212,6 +212,7 @@ int close(lua_State* L) uv_fs_t closeReq; uv_fs_close(uv_default_loop(), &closeReq, file.fileDescriptor, nullptr); + uv_fs_req_cleanup(&closeReq); return 0; } @@ -233,11 +234,12 @@ int read(lua_State* L) uv_fs_read(uv_default_loop(), &readReq, file.fileDescriptor, &iov, 1, -1, nullptr); numBytesRead = readReq.result; + uv_fs_req_cleanup(&readReq); if (numBytesRead < 0) { - luaL_errorL(L, "Error reading: %s. Closing file.\n", uv_err_name(numBytesRead)); memset(readBuffer, 0, sizeof(readBuffer)); + luaL_errorL(L, "Error reading: %s. Closing file.\n", uv_err_name(numBytesRead)); return 0; } @@ -279,12 +281,13 @@ int write(lua_State* L) int bytesWritten = 0; uv_fs_write(uv_default_loop(), &writeReq, file.fileDescriptor, &iov, 1, -1, nullptr); bytesWritten = writeReq.result; + uv_fs_req_cleanup(&writeReq); if (bytesWritten < 0) { // Error case. - luaL_errorHandle(L, file); memset(writeBuffer, 0, sizeof(writeBuffer)); + luaL_errorHandle(L, file); return 0; } @@ -304,13 +307,16 @@ std::optional openHelper(lua_State* L, const char* path, const char* uv_fs_t openReq; int errcode = uv_fs_open(uv_default_loop(), &openReq, path, *openFlags, *modeFlags, nullptr); - if (openReq.result < 0) + auto result = openReq.result; + uv_fs_req_cleanup(&openReq); + + if (result < 0) { luaL_errorL(L, "Error opening file %s\n", path); return std::nullopt; } - return FileHandle{openReq.result, errcode}; + return FileHandle{result, errcode}; } int open(lua_State* L) @@ -345,12 +351,14 @@ void cleanup(char* buffer, int size, const FileHandle& handle) memset(buffer, 0, size); uv_fs_t closeReq; uv_fs_close(uv_default_loop(), &closeReq, handle.fileDescriptor, nullptr); + uv_fs_req_cleanup(&closeReq); } int fs_remove(lua_State* L) { uv_fs_t unlink_req; int err = uv_fs_unlink(uv_default_loop(), &unlink_req, luaL_checkstring(L, 1), nullptr); + uv_fs_req_cleanup(&unlink_req); if (err) luaL_errorL(L, "%s", uv_strerror(err)); @@ -365,6 +373,7 @@ int fs_mkdir(lua_State* L) uv_fs_t req; int err = uv_fs_mkdir(uv_default_loop(), &req, path, mode, nullptr); + uv_fs_req_cleanup(&req); if (err) luaL_errorL(L, "%s", uv_strerror(err)); @@ -378,6 +387,7 @@ int fs_rmdir(lua_State* L) uv_fs_t rmdir_req; int err = uv_fs_rmdir(uv_default_loop(), &rmdir_req, path, nullptr); + uv_fs_req_cleanup(&rmdir_req); if (err) luaL_errorL(L, "%s", uv_strerror(err)); @@ -393,7 +403,10 @@ int fs_stat(lua_State* L) int err = uv_fs_stat(uv_default_loop(), &stat_req, path, nullptr); if (err) + { + uv_fs_req_cleanup(&stat_req); luaL_errorL(L, "%s", uv_strerror(err)); + } lua_createtable(L, 0, 6); @@ -426,28 +439,31 @@ int fs_stat(lua_State* L) lua_setfield(L, -2, "permissions"); + uv_fs_req_cleanup(&stat_req); + return 1; } static void defaultCallback(uv_fs_t* req) { auto* request_state = static_cast(req->data); + auto token = std::move(*request_state); + delete request_state; - if (req->result) + auto err = req->result; + + uv_fs_req_cleanup(req); + delete req; + + if (err) { - request_state->get()->fail(uv_strerror(req->result)); - uv_fs_req_cleanup(req); - delete req; + token->fail(uv_strerror(req->result)); return; } - request_state->get()->complete( - [req](lua_State* L) + token->complete( + [](lua_State* L) { - uv_fs_req_cleanup(req); - - delete req; - return 0; } ); @@ -465,6 +481,8 @@ int fs_copy(lua_State* L) if (err) { + delete static_cast(req->data); + delete req; luaL_errorL(L, "%s", uv_strerror(err)); } @@ -483,6 +501,8 @@ int fs_link(lua_State* L) if (err) { + delete static_cast(req->data); + delete req; luaL_errorL(L, "%s", uv_strerror(err)); } @@ -510,6 +530,8 @@ int fs_symlink(lua_State* L) if (err) { + delete static_cast(req->data); + delete req; luaL_errorL(L, "%s", uv_strerror(err)); } @@ -674,7 +696,6 @@ int fs_exists(lua_State* L) [req](lua_State* L) { lua_pushboolean(L, req->result == 0); - uv_fs_req_cleanup(req); delete req; @@ -682,11 +703,14 @@ int fs_exists(lua_State* L) return 1; } ); + delete request_state; } ); if (err) { + delete static_cast(req->data); + delete req; luaL_errorL(L, "%s", uv_strerror(err)); } @@ -702,7 +726,10 @@ int type(lua_State* L) int err = uv_fs_stat(uv_default_loop(), &req, path, nullptr); if (err) + { + uv_fs_req_cleanup(&req); luaL_errorL(L, "%s", uv_strerror(err)); + } auto type = fileModeToType(req.statbuf.st_mode); lua_pushstring(L, type); @@ -752,6 +779,7 @@ int listdir(lua_State* L) uv_fs_req_cleanup(req); + delete static_cast(req->data); delete req; if (err != UV_EOF) @@ -764,7 +792,11 @@ int listdir(lua_State* L) ); if (err) + { + delete static_cast(req->data); + delete req; luaL_errorL(L, "%s", uv_strerror(err)); + } return lua_yield(L, 0); } @@ -795,11 +827,12 @@ int readfiletostring(lua_State* L) uv_fs_read(uv_default_loop(), &readReq, handle->fileDescriptor, &iov, 1, -1, nullptr); numBytesRead = readReq.result; + uv_fs_req_cleanup(&readReq); if (numBytesRead < 0) { - luaL_errorL(L, "Error reading: %s. Closing file.\n", uv_err_name(numBytesRead)); cleanup(readBuffer, sizeof(readBuffer), *handle); + luaL_errorL(L, "Error reading: %s. Closing file.\n", uv_err_name(numBytesRead)); return 0; } @@ -850,12 +883,13 @@ int writestringtofile(lua_State* L) int bytesWritten = 0; uv_fs_write(uv_default_loop(), &writeReq, handle->fileDescriptor, &iov, 1, -1, nullptr); bytesWritten = writeReq.result; + uv_fs_req_cleanup(&writeReq); if (bytesWritten < 0) { // Error case. - luaL_errorHandle(L, *handle); cleanup(writeBuffer, sizeof(writeBuffer), *handle); + luaL_errorHandle(L, *handle); return 0; } @@ -895,7 +929,7 @@ int readasync(lua_State* L) const char* path = luaL_checkstring(L, 1); uv_fs_t* openReq = createRequest(L); - uv_fs_open( + int err = uv_fs_open( uv_default_loop(), openReq, path, @@ -911,6 +945,7 @@ int readasync(lua_State* L) info->token->fail("Error opening file"); uv_fs_t closeReq; uv_fs_close(uv_default_loop(), &closeReq, fd, nullptr); + uv_fs_req_cleanup(&closeReq); uv_fs_req_cleanup(req); delete (ResumeCaptureInformation*)req->data; delete req; @@ -931,11 +966,13 @@ int readasync(lua_State* L) { uv_fs_read(uv_default_loop(), &readReq, fd, &iov, 1, -1, nullptr); numBytesRead = readReq.result; + uv_fs_req_cleanup(&readReq); if (numBytesRead < 0) { uv_fs_t closeReq; uv_fs_close(uv_default_loop(), &closeReq, fd, nullptr); + uv_fs_req_cleanup(&closeReq); // Schedule error; // Also, we should free the original request. We don't have to do this for the read req since it's sycnrhonous info->token->fail("Error reading file"); @@ -961,6 +998,8 @@ int readasync(lua_State* L) uv_fs_t closeReq; uv_fs_close(uv_default_loop(), &closeReq, fd, nullptr); + uv_fs_req_cleanup(&closeReq); + uv_fs_req_cleanup(req); // free the read buffer as well as the resume information and the request delete (ResumeCaptureInformation*)req->data; delete req; @@ -968,6 +1007,13 @@ int readasync(lua_State* L) } ); + if (err) + { + delete static_cast(openReq->data); + delete openReq; + luaL_errorL(L, "%s", uv_strerror(err)); + } + return lua_yield(L, 0); } From b299d8b7538194830d54acd88b392224058b4369 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 15 Oct 2025 10:29:36 -0700 Subject: [PATCH 041/642] Add a typedef for process.exit() (#428) This should now provide exit as an option when invoking process. + provide better in editor typechecking. --- definitions/process.luau | 4 ++++ examples/process.luau | 2 ++ 2 files changed, 6 insertions(+) diff --git a/definitions/process.luau b/definitions/process.luau index b839ecd9c..46c48ef5f 100644 --- a/definitions/process.luau +++ b/definitions/process.luau @@ -33,4 +33,8 @@ function process.run(args: string | { string }, options: ProcessRunOptions?): Pr error("not implemented") end +function process.exit(exitcode: number): never + error("not implemented") +end + return process diff --git a/examples/process.luau b/examples/process.luau index abc801c0c..aa1bc46a7 100644 --- a/examples/process.luau +++ b/examples/process.luau @@ -30,3 +30,5 @@ print(r7.stdout) local r8 = process.run("echo $0", { shell = "/bin/sh" }) print(r8.stdout) + +process.exit(0) From 3d84ba537cf6cda33859594888ac9449a12b81bd Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:58:02 -0700 Subject: [PATCH 042/642] Replace unnecessary `reinterpret_cast`s with `static_cast`s (#429) Static casting `void*` to `T*` is always allowed, so it's better to use the narrower cast when possible. --- lute/fs/src/fs.cpp | 2 +- lute/runtime/src/runtime.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 4fce71228..931fcb6b6 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -921,7 +921,7 @@ uv_fs_t* createRequest(lua_State* L) ResumeCaptureInformation* getResumeInformation(uv_fs_t* req) { - return reinterpret_cast(req->data); + return static_cast(req->data); } int readasync(lua_State* L) diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 6e1818681..475a6c563 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -278,7 +278,7 @@ void Runtime::releasePendingToken() Runtime* getRuntime(lua_State* L) { - return reinterpret_cast(lua_getthreaddata(lua_mainthread(L))); + return static_cast(lua_getthreaddata(lua_mainthread(L))); } void ResumeTokenData::fail(std::string error) From a70354de11446cc95b65f00d853b0c3d0b0faf84 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 15 Oct 2025 15:50:30 -0700 Subject: [PATCH 043/642] luacase field names in lute's APIs (#430) Refactoring to make lute's API formatting more consistent --- definitions/fs.luau | 6 +- definitions/luau.luau | 172 ++++++++++++------------- lute/fs/src/fs.cpp | 6 +- lute/luau/src/luau.cpp | 178 +++++++++++++------------- lute/std/libs/syntax/printer.luau | 24 ++-- lute/std/libs/syntax/visitor.luau | 202 +++++++++++++++--------------- tests/testAstSerializer.test.luau | 108 ++++++++-------- 7 files changed, 348 insertions(+), 348 deletions(-) diff --git a/definitions/fs.luau b/definitions/fs.luau index 4dbfe83ed..0e00b1a46 100644 --- a/definitions/fs.luau +++ b/definitions/fs.luau @@ -12,9 +12,9 @@ export type FileMetadata = { type: FileType, permissions: { readonly: boolean }, size: number, - created_at: any, - accessed_at: any, - modified_at: any, + created: any, + accessed: any, + modified: any, } export type DirectoryEntry = { diff --git a/definitions/luau.luau b/definitions/luau.luau index 78950109c..a672ac3de 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -32,10 +32,10 @@ export type MultiLineComment = { export type Trivia = Whitespace | SingleLineComment | MultiLineComment export type Token = { - leadingTrivia: { Trivia }, + leadingtrivia: { Trivia }, position: Position, text: Kind, - trailingTrivia: { Trivia }, + trailingtrivia: { Trivia }, } export type Eof = Token<""> & { tag: "eof" } @@ -52,9 +52,9 @@ export type AstLocal = { export type AstExprGroup = { tag: "group", - openParens: Token<"(">, + openparens: Token<"(">, expression: AstExpr, - closeParens: Token<")">, + closeparens: Token<")">, } export type AstExprConstantNil = Token<"nil"> & { tag: "nil" } @@ -65,8 +65,8 @@ export type AstExprConstantNumber = Token & { tag: "number", value: numb export type AstExprConstantString = Token & { tag: "string", - quoteStyle: "single" | "double" | "block" | "interp", - blockDepth: number, + quotestyle: "single" | "double" | "block" | "interp", + blockdepth: number, } export type AstExprLocal = { @@ -86,9 +86,9 @@ export type AstExprVarargs = Token<"..."> & { tag: "vararg" } export type AstExprCall = { tag: "call", func: AstExpr, - openParens: Token<"(">?, + openparens: Token<"(">?, arguments: Punctuated, - closeParens: Token<")">?, + closeparens: Token<")">?, self: boolean, argLocation: Location, } @@ -98,39 +98,39 @@ export type AstExprIndexName = { expression: AstExpr, accessor: Token<"." | ":">, index: Token, - indexLocation: Location, + indexlocation: Location, } export type AstExprIndexExpr = { tag: "index", expression: AstExpr, - openBrackets: Token<"[">, + openbrackets: Token<"[">, index: AstExpr, - closeBrackets: Token<"]">, + closebrackets: Token<"]">, } export type AstFunctionBody = { - openGenerics: Token<"<">?, + opengenerics: Token<"<">?, generics: Punctuated?, - genericPacks: Punctuated?, - closeGenerics: Token<">">?, + genericpacks: Punctuated?, + closegenerics: Token<">">?, self: AstLocal?, - openParens: Token<"(">, + openparens: Token<"(">, parameters: Punctuated, vararg: Token<"...">?, - varargColon: Token<":">?, - varargAnnotation: AstTypePack?, - closeParens: Token<")">, - returnSpecifier: Token<":">?, - returnAnnotation: AstTypePack?, + varargcolon: Token<":">?, + varargannotation: AstTypePack?, + closeparens: Token<")">, + returnspecifier: Token<":">?, + returnannotation: AstTypePack?, body: AstStatBlock, - endKeyword: Token<"end">, + endkeyword: Token<"end">, } export type AstExprAnonymousFunction = { tag: "function", attributes: { AstAttribute }, - functionKeyword: Token<"function">, + functionkeyword: Token<"function">, body: AstFunctionBody, } @@ -145,9 +145,9 @@ export type AstExprTableItem = } | { kind: "general", - indexerOpen: Token<"[">, + indexeropen: Token<"[">, key: AstExpr, - indexerClose: Token<"]">, + indexerclose: Token<"]">, equals: Token<"=">, value: AstExpr, separator: Token<"," | ";">?, @@ -155,9 +155,9 @@ export type AstExprTableItem = export type AstExprTable = { tag: "table", - openBrace: Token<"{">, + openbrace: Token<"{">, entries: { AstExprTableItem }, - closeBrace: Token<"}">, + closebrace: Token<"}">, } export type AstExprUnary = { @@ -187,20 +187,20 @@ export type AstExprTypeAssertion = { } export type AstExprIfElseIfs = { - elseifKeyword: Token<"elseif">, + elseifkeyword: Token<"elseif">, condition: AstExpr, - thenKeyword: Token<"then">, + thenkeyword: Token<"then">, consequent: AstExpr, } export type AstExprIfElse = { tag: "conditional", - ifKeyword: Token<"if">, + ifkeyword: Token<"if">, condition: AstExpr, - thenKeyword: Token<"then">, + thenkeyword: Token<"then">, consequent: AstExpr, elseifs: { AstExprIfElseIfs }, - elseKeyword: Token<"else">, + elsekeyword: Token<"else">, antecedent: AstExpr, } @@ -230,31 +230,31 @@ export type AstStatBlock = { } export type AstStatElseIf = { - elseifKeyword: Token<"elseif">, + elseifkeyword: Token<"elseif">, condition: AstExpr, - thenKeyword: Token<"then">, + thenkeyword: Token<"then">, consequent: AstStatBlock, } export type AstStatIf = { tag: "conditional", - ifKeyword: Token<"if">, + ifkeyword: Token<"if">, condition: AstExpr, - thenKeyword: Token<"then">, + thenkeyword: Token<"then">, consequent: AstStatBlock, elseifs: { AstStatElseIf }, - elseKeyword: Token<"else">?, -- TODO: this could be elseif! + elsekeyword: Token<"else">?, -- TODO: this could be elseif! antecedent: AstStatBlock?, - endKeyword: Token<"end">, + endkeyword: Token<"end">, } export type AstStatWhile = { tag: "while", - whileKeyword: Token<"while">, + whilekeyword: Token<"while">, condition: AstExpr, - doKeyword: Token<"do">, + dokeyword: Token<"do">, body: AstStatBlock, - endKeyword: Token<"end">, + endkeyword: Token<"end">, } export type AstStatRepeat = { @@ -271,7 +271,7 @@ export type AstStatContinue = Token<"continue"> & { tag: "continue" } export type AstStatReturn = { tag: "return", - returnKeyword: Token<"return">, + returnkeyword: Token<"return">, expressions: Punctuated, } @@ -282,7 +282,7 @@ export type AstStatExpr = { export type AstStatLocal = { tag: "local", - localKeyword: Token<"local">, + localkeyword: Token<"local">, variables: Punctuated, equals: Token<"=">?, values: Punctuated, @@ -290,28 +290,28 @@ export type AstStatLocal = { export type AstStatFor = { tag: "for", - forKeyword: Token<"for">, + forkeyword: Token<"for">, variable: AstLocal, equals: Token<"=">, from: AstExpr, - toComma: Token<",">, + tocomma: Token<",">, to: AstExpr, - stepComma: Token<",">?, + stepcomma: Token<",">?, step: AstExpr?, - doKeyword: Token<"do">, + dokeyword: Token<"do">, body: AstStatBlock, - endKeyword: Token<"end">, + endkeyword: Token<"end">, } export type AstStatForIn = { tag: "forin", - forKeyword: Token<"for">, + forkeyword: Token<"for">, variables: Punctuated>, - inKeyword: Token<"in">, + inkeyword: Token<"in">, values: Punctuated, - doKeyword: Token<"do">, + dokeyword: Token<"do">, body: AstStatBlock, - endKeyword: Token<"end">, + endkeyword: Token<"end">, } export type AstStatAssign = { @@ -333,7 +333,7 @@ export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { tag export type AstStatFunction = { tag: "function", attributes: { AstAttribute }, - functionKeyword: Token<"function">, + functionkeyword: Token<"function">, name: AstExpr, body: AstFunctionBody, } @@ -341,8 +341,8 @@ export type AstStatFunction = { export type AstStatLocalFunction = { tag: "localfunction", attributes: { AstAttribute }, - localKeyword: Token<"local">, - functionKeyword: Token<"function">, + localkeyword: Token<"local">, + functionkeyword: Token<"function">, name: AstLocal, body: AstFunctionBody, } @@ -352,10 +352,10 @@ export type AstStatTypeAlias = { export: Token<"export">?, typeToken: Token<"type">, name: Token, - openGenerics: Token<"<">?, + opengenerics: Token<"<">?, generics: Punctuated?, - genericPacks: Punctuated?, - closeGenerics: Token<">">?, + genericpacks: Punctuated?, + closegenerics: Token<">">?, equals: Token<"=">, type: AstType, } @@ -364,7 +364,7 @@ export type AstStatTypeFunction = { tag: "typefunction", export: Token<"export">?, type: Token<"type">, - functionKeyword: Token<"function">, + functionkeyword: Token<"function">, name: Token, body: AstFunctionBody, } @@ -406,11 +406,11 @@ export type AstGenericTypePack = { export type AstTypeReference = { tag: "reference", prefix: Token?, - prefixPoint: Token<".">?, + prefixpoint: Token<".">?, name: Token, - openParameters: Token<"<">?, + openparameters: Token<"<">?, parameters: Punctuated?, - closeParameters: Token<">">?, + closeparameters: Token<">">?, } export type AstTypeSingletonBool = Token<"true" | "false"> & { @@ -420,22 +420,22 @@ export type AstTypeSingletonBool = Token<"true" | "false"> & { export type AstTypeSingletonString = Token & { tag: "string", - quoteStyle: "single" | "double", + quotestyle: "single" | "double", } export type AstTypeTypeof = { tag: "typeof", typeof: Token<"typeof">, - openParens: Token<"(">, + openparens: Token<"(">, expression: AstExpr, - closeParens: Token<")">, + closeparens: Token<")">, } export type AstTypeGroup = { tag: "group", - openParens: Token<"(">, + openparens: Token<"(">, type: AstType, - closeParens: Token<">">, + closeparens: Token<">">, } export type AstTypeOptional = Token<"?"> & { tag: "optional" } @@ -455,19 +455,19 @@ export type AstTypeIntersection = { export type AstTypeArray = { tag: "array", - openBrace: Token<"{">, + openbrace: Token<"{">, access: Token<"read" | "write">?, type: AstType, - closeBrace: Token<"}">, + closebrace: Token<"}">, } export type AstTypeTableItem = { kind: "indexer", access: Token<"read" | "write">?, - indexerOpen: Token<"[">, + indexeropen: Token<"[">, key: AstType, - indexerClose: Token<"]">, + indexerclose: Token<"]">, colon: Token<":">, value: AstType, separator: Token<"," | ";">?, @@ -475,9 +475,9 @@ export type AstTypeTableItem = | { kind: "stringproperty", access: Token<"read" | "write">?, - indexerOpen: Token<"[">, + indexeropen: Token<"[">, key: AstTypeSingletonString, - indexerClose: Token<"]">, + indexerclose: Token<"]">, colon: Token<":">, value: AstType, separator: Token<"," | ";">?, @@ -486,7 +486,7 @@ export type AstTypeTableItem = kind: "property", access: Token<"read" | "write">?, key: AstTypeSingletonString, - indexerClose: Token<"]">, + indexerclose: Token<"]">, colon: Token<":">, value: AstType, separator: Token<"," | ";">?, @@ -494,9 +494,9 @@ export type AstTypeTableItem = export type AstTypeTable = { tag: "table", - openBrace: Token<"{">, + openbrace: Token<"{">, entries: { AstTypeTableItem }, - closeBrace: Token<"}">, + closebrace: Token<"}">, } export type AstTypeFunctionParameter = { @@ -507,16 +507,16 @@ export type AstTypeFunctionParameter = { export type AstTypeFunction = { tag: "function", - openGenerics: Token<"<">?, + opengenerics: Token<"<">?, generics: Punctuated?, - genericPacks: Punctuated?, - closeGenerics: Token<">">?, - openParens: Token<"(">, + genericpacks: Punctuated?, + closegenerics: Token<">">?, + openparens: Token<"(">, parameters: Punctuated, vararg: AstTypePack?, - closeParens: Token<")">, - returnArrow: Token<"->">, - returnTypes: AstTypePack, + closeparens: Token<")">, + returnarrow: Token<"->">, + returntypes: AstTypePack, } export type AstType = @@ -534,10 +534,10 @@ export type AstType = export type AstTypePackExplicit = { tag: "explicit", - openParens: Token<"(">?, + openparens: Token<"(">?, types: Punctuated, - tailType: AstTypePack?, - closeParens: Token<")">?, + tailtype: AstTypePack?, + closeparens: Token<")">?, } export type AstTypePackGeneric = { @@ -559,7 +559,7 @@ export type ParseResult = { root: AstStatBlock, eof: Eof, lines: number, - lineOffsets: { number }, + lineoffsets: { number }, } function luau.parse(source: string): ParseResult diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 931fcb6b6..39e48d7e7 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -421,13 +421,13 @@ int fs_stat(lua_State* L) lua_setfield(L, -2, "size"); createDurationFromTimespec32(L, stat.st_birthtim); - lua_setfield(L, -2, "created_at"); + lua_setfield(L, -2, "created"); createDurationFromTimespec32(L, stat.st_atim); - lua_setfield(L, -2, "accessed_at"); + lua_setfield(L, -2, "accessed"); createDurationFromTimespec32(L, stat.st_mtim); - lua_setfield(L, -2, "modified_at"); + lua_setfield(L, -2, "modified"); // permissions lua_createtable(L, 0, 2); diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index efb2069d0..b0735ce32 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -393,14 +393,14 @@ struct AstSerialize : public Luau::AstVisitor LUAU_ASSERT(cstNode->indexerOpenPosition); serializeToken(*cstNode->indexerOpenPosition, "["); - lua_setfield(L, -2, "indexerOpen"); + lua_setfield(L, -2, "indexeropen"); visit(item.key); lua_setfield(L, -2, "key"); LUAU_ASSERT(cstNode->indexerClosePosition); serializeToken(*cstNode->indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerClose"); + lua_setfield(L, -2, "indexerclose"); LUAU_ASSERT(cstNode->equalsPosition); serializeToken(*cstNode->equalsPosition, "="); @@ -491,7 +491,7 @@ struct AstSerialize : public Luau::AstVisitor LUAU_ASSERT(lua_istable(L, -1)); serializeTrivia(trailingTrivia); - lua_setfield(L, -2, "trailingTrivia"); + lua_setfield(L, -2, "trailingtrivia"); lua_pop(L, 1); lua_unref(L, lastTokenRef); lastTokenRef = LUA_NOREF; @@ -503,7 +503,7 @@ struct AstSerialize : public Luau::AstVisitor serializeTrivia(trivia); } LUAU_ASSERT(lua_istable(L, -2)); - lua_setfield(L, -2, "leadingTrivia"); + lua_setfield(L, -2, "leadingtrivia"); serialize(position); lua_setfield(L, -2, "position"); @@ -513,7 +513,7 @@ struct AstSerialize : public Luau::AstVisitor advancePosition(text); lua_createtable(L, 0, 0); - lua_setfield(L, -2, "trailingTrivia"); + lua_setfield(L, -2, "trailingtrivia"); lastTokenRef = lua_ref(L, -1); } @@ -662,13 +662,13 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "group"); serializeToken(node->location.begin, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); node->expr->visit(this); lua_setfield(L, -2, "expression"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serialize(Luau::AstExprConstantNil* node) @@ -718,10 +718,10 @@ struct AstSerialize : public Luau::AstVisitor lua_pushstring(L, "interp"); break; } - lua_setfield(L, -2, "quoteStyle"); + lua_setfield(L, -2, "quotestyle"); lua_pushnumber(L, cstNode->blockDepth); - lua_setfield(L, -2, "blockDepth"); + lua_setfield(L, -2, "blockdepth"); // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually @@ -779,7 +779,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(*cstNode->openParens, "("); else lua_pushnil(L); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); serializePunctuated(node->args, cstNode->commaPositions, ","); lua_setfield(L, -2, "arguments"); @@ -794,7 +794,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(*cstNode->closeParens, ")"); else lua_pushnil(L); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serialize(Luau::AstExprIndexName* node) @@ -813,7 +813,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->indexLocation.begin, node->index.value); lua_setfield(L, -2, "index"); serialize(node->indexLocation); - lua_setfield(L, -2, "indexLocation"); + lua_setfield(L, -2, "indexlocation"); } void serialize(Luau::AstExprIndexExpr* node) @@ -829,13 +829,13 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "expression"); serializeToken(cstNode->openBracketPosition, "["); - lua_setfield(L, -2, "openBrackets"); + lua_setfield(L, -2, "openbrackets"); node->index->visit(this); lua_setfield(L, -2, "index"); serializeToken(cstNode->closeBracketPosition, "]"); - lua_setfield(L, -2, "closeBrackets"); + lua_setfield(L, -2, "closebrackets"); } void serializeFunctionBody(Luau::AstExprFunction* node) @@ -848,17 +848,17 @@ struct AstSerialize : public Luau::AstVisitor if (node->generics.size > 0 || node->genericPacks.size > 0) { serializeToken(cstNode->openGenericsPosition, "<"); - lua_setfield(L, -2, "openGenerics"); + lua_setfield(L, -2, "opengenerics"); auto commas = cstNode->genericsCommaPositions; serializePunctuated(node->generics, commas, ","); lua_setfield(L, -2, "generics"); serializePunctuated(node->genericPacks, splitArray(commas, node->generics.size), ","); - lua_setfield(L, -2, "genericPacks"); + lua_setfield(L, -2, "genericpacks"); serializeToken(cstNode->closeGenericsPosition, ">"); - lua_setfield(L, -2, "closeGenerics"); + lua_setfield(L, -2, "closegenerics"); } if (node->self) @@ -870,7 +870,7 @@ struct AstSerialize : public Luau::AstVisitor if (node->argLocation) { serializeToken(node->argLocation->begin, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); } serializePunctuated(node->args, cstNode->argsCommaPositions, ",", cstNode->argsAnnotationColonPositions); @@ -886,7 +886,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(cstNode->varargAnnotationColonPosition, ":"); else lua_pushnil(L); - lua_setfield(L, -2, "varargColon"); + lua_setfield(L, -2, "varargcolon"); if (node->varargAnnotation) { @@ -897,31 +897,31 @@ struct AstSerialize : public Luau::AstVisitor } else lua_pushnil(L); - lua_setfield(L, -2, "varargAnnotation"); + lua_setfield(L, -2, "varargannotation"); if (node->argLocation) { serializeToken(Luau::Position{node->argLocation->end.line, node->argLocation->end.column - 1}, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } if (node->returnAnnotation) serializeToken(cstNode->returnSpecifierPosition, ":"); else lua_pushnil(L); - lua_setfield(L, -2, "returnSpecifier"); + lua_setfield(L, -2, "returnspecifier"); if (node->returnAnnotation) node->returnAnnotation->visit(this); else lua_pushnil(L); - lua_setfield(L, -2, "returnAnnotation"); + lua_setfield(L, -2, "returnannotation"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } void serialize(Luau::AstExprFunction* node) @@ -937,7 +937,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); + lua_setfield(L, -2, "functionkeyword"); serializeFunctionBody(node); lua_setfield(L, -2, "body"); @@ -953,7 +953,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "table"); serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openBrace"); + lua_setfield(L, -2, "openbrace"); lua_createtable(L, node->items.size, 0); for (size_t i = 0; i < node->items.size; i++) @@ -964,7 +964,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "entries"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closeBrace"); + lua_setfield(L, -2, "closebrace"); } void serialize(Luau::AstExprUnary* node) @@ -1028,7 +1028,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "conditional"); serializeToken(node->location.begin, "if"); - lua_setfield(L, -2, "ifKeyword"); + lua_setfield(L, -2, "ifkeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1036,7 +1036,7 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasThen) { serializeToken(cstNode->thenPosition, "then"); - lua_setfield(L, -2, "thenKeyword"); + lua_setfield(L, -2, "thenkeyword"); node->trueExpr->visit(this); } @@ -1054,7 +1054,7 @@ struct AstSerialize : public Luau::AstVisitor cstNode = lookupCstNode(node); serializeToken(node->location.begin, "elseif"); - lua_setfield(L, -2, "elseifKeyword"); + lua_setfield(L, -2, "elseifkeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1062,7 +1062,7 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasThen) { serializeToken(cstNode->thenPosition, "then"); - lua_setfield(L, -2, "thenKeyword"); + lua_setfield(L, -2, "thenkeyword"); node->trueExpr->visit(this); } else @@ -1077,7 +1077,7 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasElse) { serializeToken(cstNode->elsePosition, "else"); - lua_setfield(L, -2, "elseKeyword"); + lua_setfield(L, -2, "elsekeyword"); node->falseExpr->visit(this); } else @@ -1154,13 +1154,13 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "conditional"); serializeToken(node->location.begin, "if"); - lua_setfield(L, -2, "ifKeyword"); + lua_setfield(L, -2, "ifkeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); serializeToken(node->thenLocation->begin, "then"); - lua_setfield(L, -2, "thenKeyword"); + lua_setfield(L, -2, "thenkeyword"); node->thenbody->visit(this); lua_setfield(L, -2, "consequent"); @@ -1173,13 +1173,13 @@ struct AstSerialize : public Luau::AstVisitor auto elseif = node->elsebody->as(); serializeToken(elseif->location.begin, "elseif"); - lua_setfield(L, -2, "elseifKeyword"); + lua_setfield(L, -2, "elseifkeyword"); elseif->condition->visit(this); lua_setfield(L, -2, "condition"); serializeToken(elseif->thenLocation->begin, "then"); - lua_setfield(L, -2, "thenKeyword"); + lua_setfield(L, -2, "thenkeyword"); elseif->thenbody->visit(this); lua_setfield(L, -2, "consequent"); @@ -1194,24 +1194,24 @@ struct AstSerialize : public Luau::AstVisitor { LUAU_ASSERT(node->elseLocation); serializeToken(node->elseLocation->begin, "else"); - lua_setfield(L, -2, "elseKeyword"); + lua_setfield(L, -2, "elsekeyword"); node->elsebody->visit(this); lua_setfield(L, -2, "antecedent"); serializeToken(node->elsebody->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } else { lua_pushnil(L); - lua_setfield(L, -2, "elseKeyword"); + lua_setfield(L, -2, "elsekeyword"); lua_pushnil(L); lua_setfield(L, -2, "antecedent"); serializeToken(node->thenbody->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } } @@ -1223,7 +1223,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "while"); serializeToken(node->location.begin, "while"); - lua_setfield(L, -2, "whileKeyword"); + lua_setfield(L, -2, "whilekeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1232,13 +1232,13 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->doLocation.begin, "do"); else lua_pushnil(L); - lua_setfield(L, -2, "doKeyword"); + lua_setfield(L, -2, "dokeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } void serializeStat(Luau::AstStatRepeat* node) @@ -1284,7 +1284,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "return"); serializeToken(node->location.begin, "return"); - lua_setfield(L, -2, "returnKeyword"); + lua_setfield(L, -2, "returnkeyword"); const auto cstNode = lookupCstNode(node); serializePunctuated(node->list, cstNode->commaPositions, ","); @@ -1310,7 +1310,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "local"); serializeToken(node->location.begin, "local"); - lua_setfield(L, -2, "localKeyword"); + lua_setfield(L, -2, "localkeyword"); const auto cstNode = lookupCstNode(node); serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); @@ -1336,7 +1336,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "for"); serializeToken(node->location.begin, "for"); - lua_setfield(L, -2, "forKeyword"); + lua_setfield(L, -2, "forkeyword"); serialize(node->var, /* createToken= */ true, std::make_optional(cstNode->annotationColonPosition)); lua_setfield(L, -2, "variable"); @@ -1348,7 +1348,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "from"); serializeToken(cstNode->endCommaPosition, ","); - lua_setfield(L, -2, "toComma"); + lua_setfield(L, -2, "tocomma"); node->to->visit(this); lua_setfield(L, -2, "to"); @@ -1356,7 +1356,7 @@ struct AstSerialize : public Luau::AstVisitor if (cstNode->stepCommaPosition) { serializeToken(*cstNode->stepCommaPosition, ","); - lua_setfield(L, -2, "stepComma"); + lua_setfield(L, -2, "stepcomma"); } if (node->step) @@ -1369,13 +1369,13 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->doLocation.begin, "do"); else lua_pushnil(L); - lua_setfield(L, -2, "doKeyword"); + lua_setfield(L, -2, "dokeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } void serializeStat(Luau::AstStatForIn* node) @@ -1388,7 +1388,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "forin"); serializeToken(node->location.begin, "for"); - lua_setfield(L, -2, "forKeyword"); + lua_setfield(L, -2, "forkeyword"); serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); lua_setfield(L, -2, "variables"); @@ -1397,7 +1397,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->inLocation.begin, "in"); else lua_pushnil(L); - lua_setfield(L, -2, "inKeyword"); + lua_setfield(L, -2, "inkeyword"); serializePunctuated(node->values, cstNode->valuesCommaPositions, ","); lua_setfield(L, -2, "values"); @@ -1406,13 +1406,13 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->doLocation.begin, "do"); else lua_pushnil(L); - lua_setfield(L, -2, "doKeyword"); + lua_setfield(L, -2, "dokeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endKeyword"); + lua_setfield(L, -2, "endkeyword"); } void serializeStat(Luau::AstStatAssign* node) @@ -1465,7 +1465,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "attributes"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); + lua_setfield(L, -2, "functionkeyword"); node->name->visit(this); lua_setfield(L, -2, "name"); @@ -1487,10 +1487,10 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); serializeToken(cstNode->localKeywordPosition, "local"); - lua_setfield(L, -2, "localKeyword"); + lua_setfield(L, -2, "localkeyword"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); + lua_setfield(L, -2, "functionkeyword"); serialize(node->name); lua_setfield(L, -2, "name"); @@ -1530,17 +1530,17 @@ struct AstSerialize : public Luau::AstVisitor if (node->generics.size > 0 || node->genericPacks.size > 0) { serializeToken(cstNode->genericsOpenPosition, "<"); - lua_setfield(L, -2, "openGenerics"); + lua_setfield(L, -2, "opengenerics"); auto commas = cstNode->genericsCommaPositions; serializePunctuated(node->generics, commas, ","); lua_setfield(L, -2, "generics"); serializePunctuated(node->genericPacks, splitArray(commas, node->generics.size), ","); - lua_setfield(L, -2, "genericPacks"); + lua_setfield(L, -2, "genericpacks"); serializeToken(cstNode->genericsClosePosition, ">"); - lua_setfield(L, -2, "closeGenerics"); + lua_setfield(L, -2, "closegenerics"); } serializeToken(cstNode->equalsPosition, "="); @@ -1569,7 +1569,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "type"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionKeyword"); + lua_setfield(L, -2, "functionkeyword"); serializeToken(node->nameLocation.begin, node->name.value); lua_setfield(L, -2, "name"); @@ -1627,7 +1627,7 @@ struct AstSerialize : public Luau::AstVisitor LUAU_ASSERT(cstNode); LUAU_ASSERT(cstNode->prefixPointPosition); serializeToken(*cstNode->prefixPointPosition, "."); - lua_setfield(L, -2, "prefixPoint"); + lua_setfield(L, -2, "prefixpoint"); } serializeToken(node->nameLocation.begin, node->name.value); @@ -1637,13 +1637,13 @@ struct AstSerialize : public Luau::AstVisitor { LUAU_ASSERT(cstNode); serializeToken(cstNode->openParametersPosition, "<"); - lua_setfield(L, -2, "openParameters"); + lua_setfield(L, -2, "openparameters"); serializePunctuated(node->parameters, cstNode->parametersCommaPositions, ","); lua_setfield(L, -2, "parameters"); serializeToken(cstNode->closeParametersPosition, ">"); - lua_setfield(L, -2, "closeParameters"); + lua_setfield(L, -2, "closeparameters"); } } @@ -1659,7 +1659,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "array"); serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openBrace"); + lua_setfield(L, -2, "openbrace"); if (node->indexer->accessLocation) { @@ -1674,7 +1674,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "type"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closeBrace"); + lua_setfield(L, -2, "closebrace"); return; } @@ -1685,7 +1685,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "table"); serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openBrace"); + lua_setfield(L, -2, "openbrace"); lua_createtable(L, cstNode->items.size, 0); const Luau::AstTableProp* prop = node->props.begin(); @@ -1713,13 +1713,13 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "access"); serializeToken(item.indexerOpenPosition, "["); - lua_setfield(L, -2, "indexerOpen"); + lua_setfield(L, -2, "indexeropen"); node->indexer->indexType->visit(this); lua_setfield(L, -2, "key"); serializeToken(item.indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerClose"); + lua_setfield(L, -2, "indexerclose"); serializeToken(item.colonPosition, ":"); lua_setfield(L, -2, "colon"); @@ -1758,7 +1758,7 @@ struct AstSerialize : public Luau::AstVisitor if (item.kind == Luau::CstTypeTable::Item::Kind::StringProperty) { serializeToken(item.indexerOpenPosition, "["); - lua_setfield(L, -2, "indexerOpen"); + lua_setfield(L, -2, "indexeropen"); { auto initialPosition = item.stringPosition; @@ -1775,7 +1775,7 @@ struct AstSerialize : public Luau::AstVisitor default: LUAU_ASSERT(false); } - lua_setfield(L, -2, "quoteStyle"); + lua_setfield(L, -2, "quotestyle"); // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually @@ -1789,7 +1789,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "key"); serializeToken(item.indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerClose"); + lua_setfield(L, -2, "indexerclose"); } else { @@ -1816,7 +1816,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "entries"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closeBrace"); + lua_setfield(L, -2, "closebrace"); } void serializeType(Luau::AstTypeFunction* node) @@ -1831,21 +1831,21 @@ struct AstSerialize : public Luau::AstVisitor if (node->generics.size > 0 || node->genericPacks.size > 0) { serializeToken(cstNode->openGenericsPosition, "<"); - lua_setfield(L, -2, "openGenerics"); + lua_setfield(L, -2, "opengenerics"); auto commas = cstNode->genericsCommaPositions; serializePunctuated(node->generics, commas, ","); lua_setfield(L, -2, "generics"); serializePunctuated(node->genericPacks, splitArray(commas, node->generics.size), ","); - lua_setfield(L, -2, "genericPacks"); + lua_setfield(L, -2, "genericpacks"); serializeToken(cstNode->closeGenericsPosition, ">"); - lua_setfield(L, -2, "closeGenerics"); + lua_setfield(L, -2, "closegenerics"); } serializeToken(cstNode->openArgsPosition, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); lua_createtable(L, node->argTypes.types.size, 0); for (size_t i = 0; i < node->argTypes.types.size; i++) @@ -1890,13 +1890,13 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "vararg"); serializeToken(cstNode->closeArgsPosition, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); serializeToken(cstNode->returnArrowPosition, "->"); - lua_setfield(L, -2, "returnArrow"); + lua_setfield(L, -2, "returnarrow"); node->returnTypes->visit(this); - lua_setfield(L, -2, "returnTypes"); + lua_setfield(L, -2, "returntypes"); } void serializeType(Luau::AstTypeTypeof* node) @@ -1911,13 +1911,13 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); serializeToken(cstNode->openPosition, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); node->expr->visit(this); lua_setfield(L, -2, "expression"); serializeToken(cstNode->closePosition, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serializeType(Luau::AstTypeUnion* node) @@ -2016,7 +2016,7 @@ struct AstSerialize : public Luau::AstVisitor default: LUAU_ASSERT(false); } - lua_setfield(L, -2, "quoteStyle"); + lua_setfield(L, -2, "quotestyle"); // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually @@ -2032,13 +2032,13 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "group"); serializeToken(node->location.begin, "("); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); node->type->visit(this); lua_setfield(L, -2, "type"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, ")"); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serializeType(Luau::AstGenericType* node) @@ -2112,7 +2112,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(cstNode->openParenthesesPosition, "("); else lua_pushnil(L); - lua_setfield(L, -2, "openParens"); + lua_setfield(L, -2, "openparens"); serializePunctuated(node->typeList.types, cstNode->commaPositions, ","); lua_setfield(L, -2, "types"); @@ -2121,13 +2121,13 @@ struct AstSerialize : public Luau::AstVisitor node->typeList.tailType->visit(this); else lua_pushnil(L); - lua_setfield(L, -2, "tailType"); + lua_setfield(L, -2, "tailtype"); if (cstNode->hasParentheses) serializeToken(cstNode->closeParenthesesPosition, ")"); else lua_pushnil(L); - lua_setfield(L, -2, "closeParens"); + lua_setfield(L, -2, "closeparens"); } void serializeTypePack(Luau::AstTypePackGeneric* node) @@ -2569,7 +2569,7 @@ int luau_parse(lua_State* L) lua_pushnumber(L, serializer.lineOffsets[i]); lua_settable(L, -3); } - lua_setfield(L, -2, "lineOffsets"); + lua_setfield(L, -2, "lineoffsets"); return 1; } diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index f34ba3534..50706e4ee 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -23,26 +23,26 @@ local function printTriviaList(trivia: { luau.Trivia }) end local function printToken(token: luau.Token): string - return printTriviaList(token.leadingTrivia) .. token.text .. printTriviaList(token.trailingTrivia) + return printTriviaList(token.leadingtrivia) .. token.text .. printTriviaList(token.trailingtrivia) end local function printString(expr: luau.AstExprConstantString): string - local result = printTriviaList(expr.leadingTrivia) + local result = printTriviaList(expr.leadingtrivia) - if expr.quoteStyle == "single" then + if expr.quotestyle == "single" then result ..= `'{expr.text}'` - elseif expr.quoteStyle == "double" then + elseif expr.quotestyle == "double" then result ..= `"{expr.text}"` - elseif expr.quoteStyle == "block" then - local equals = string.rep("=", expr.blockDepth) + elseif expr.quotestyle == "block" then + local equals = string.rep("=", expr.blockdepth) result ..= `[{equals}[{expr.text}]{equals}]` - elseif expr.quoteStyle == "interp" then + elseif expr.quotestyle == "interp" then result ..= "`" .. expr.text .. "`" else - return exhaustiveMatch(expr.quoteStyle) + return exhaustiveMatch(expr.quotestyle) end - result ..= printTriviaList(expr.trailingTrivia) + result ..= printTriviaList(expr.trailingtrivia) return result end @@ -50,7 +50,7 @@ local function printInterpolatedString(expr: luau.AstExprInterpString): string local result = "" for i = 1, #expr.strings do - result ..= printTriviaList(expr.strings[i].leadingTrivia) + result ..= printTriviaList(expr.strings[i].leadingtrivia) if i == 1 then result ..= "`" else @@ -60,10 +60,10 @@ local function printInterpolatedString(expr: luau.AstExprInterpString): string if i == #expr.strings then result ..= "`" - result ..= printTriviaList(expr.strings[i].trailingTrivia) + result ..= printTriviaList(expr.strings[i].trailingtrivia) else result ..= "{" - result ..= printTriviaList(expr.strings[i].trailingTrivia) + result ..= printTriviaList(expr.strings[i].trailingtrivia) result ..= printExpr(expr.expressions[i]) end end diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index f0f163ebd..513b2c53a 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -165,33 +165,33 @@ end local function visitIf(node: luau.AstStatIf, visitor: Visitor) if visitor.visitIf(node) then - visitToken(node.ifKeyword, visitor) + visitToken(node.ifkeyword, visitor) visitExpression(node.condition, visitor) - visitToken(node.thenKeyword, visitor) + visitToken(node.thenkeyword, visitor) visitBlock(node.consequent, visitor) for _, elseifNode in node.elseifs do - visitToken(elseifNode.elseifKeyword, visitor) + visitToken(elseifNode.elseifkeyword, visitor) visitExpression(elseifNode.condition, visitor) - visitToken(elseifNode.thenKeyword, visitor) + visitToken(elseifNode.thenkeyword, visitor) visitBlock(elseifNode.consequent, visitor) end - if node.elseKeyword then - visitToken(node.elseKeyword, visitor) + if node.elsekeyword then + visitToken(node.elsekeyword, visitor) end if node.antecedent then visitBlock(node.antecedent, visitor) end - visitToken(node.endKeyword, visitor) + visitToken(node.endkeyword, visitor) end end local function visitWhile(node: luau.AstStatWhile, visitor: Visitor) if visitor.visitWhile(node) then - visitToken(node.whileKeyword, visitor) + visitToken(node.whilekeyword, visitor) visitExpression(node.condition, visitor) - visitToken(node.doKeyword, visitor) + visitToken(node.dokeyword, visitor) visitBlock(node.body, visitor) - visitToken(node.endKeyword, visitor) + visitToken(node.endkeyword, visitor) end end @@ -206,14 +206,14 @@ end local function visitReturn(node: luau.AstStatReturn, visitor: Visitor) if visitor.visitReturn(node) then - visitToken(node.returnKeyword, visitor) + visitToken(node.returnkeyword, visitor) visitPunctuated(node.expressions, visitor, visitExpression) end end local function visitLocalStatement(node: luau.AstStatLocal, visitor: Visitor) if visitor.visitLocalDeclaration(node) then - visitToken(node.localKeyword, visitor) + visitToken(node.localkeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) if node.equals then visitToken(node.equals, visitor) @@ -226,33 +226,33 @@ end local function visitFor(node: luau.AstStatFor, visitor: Visitor) if visitor.visitFor(node) then - visitToken(node.forKeyword, visitor) + visitToken(node.forkeyword, visitor) visitLocal(node.variable, visitor) visitToken(node.equals, visitor) visitExpression(node.from, visitor) - visitToken(node.toComma, visitor) + visitToken(node.tocomma, visitor) visitExpression(node.to, visitor) - if node.stepComma then - visitToken(node.stepComma, visitor) + if node.stepcomma then + visitToken(node.stepcomma, visitor) end if node.step then visitExpression(node.step, visitor) end - visitToken(node.doKeyword, visitor) + visitToken(node.dokeyword, visitor) visitBlock(node.body, visitor) - visitToken(node.endKeyword, visitor) + visitToken(node.endkeyword, visitor) end end local function visitForIn(node: luau.AstStatForIn, visitor: Visitor) if visitor.visitForIn(node) then - visitToken(node.forKeyword, visitor) + visitToken(node.forkeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) - visitToken(node.inKeyword, visitor) + visitToken(node.inkeyword, visitor) visitPunctuated(node.values, visitor, visitExpression) - visitToken(node.doKeyword, visitor) + visitToken(node.dokeyword, visitor) visitBlock(node.body, visitor) - visitToken(node.endKeyword, visitor) + visitToken(node.endkeyword, visitor) end end @@ -300,17 +300,17 @@ local function visitTypeAlias(node: luau.AstStatTypeAlias, visitor: Visitor) end visitToken(node.typeToken, visitor) visitToken(node.name, visitor) - if node.openGenerics then - visitToken(node.openGenerics, visitor) + if node.opengenerics then + visitToken(node.opengenerics, visitor) end if node.generics then visitPunctuated(node.generics, visitor, visitGeneric) end - if node.genericPacks then - visitPunctuated(node.genericPacks, visitor, visitGenericPack) + if node.genericpacks then + visitPunctuated(node.genericpacks, visitor, visitGenericPack) end - if node.closeGenerics then - visitToken(node.closeGenerics, visitor) + if node.closegenerics then + visitToken(node.closegenerics, visitor) end visitToken(node.equals, visitor) visitType(node.type, visitor) @@ -362,12 +362,12 @@ end local function visitCall(node: luau.AstExprCall, visitor: Visitor) if visitor.visitCall(node) then visitExpression(node.func, visitor) - if node.openParens then - visitToken(node.openParens, visitor) + if node.openparens then + visitToken(node.openparens, visitor) end visitPunctuated(node.arguments, visitor, visitExpression) - if node.closeParens then - visitToken(node.closeParens, visitor) + if node.closeparens then + visitToken(node.closeparens, visitor) end end end @@ -388,38 +388,38 @@ local function visitBinary(node: luau.AstExprBinary, visitor: Visitor) end local function visitFunctionBody(node: luau.AstFunctionBody, visitor: Visitor) - if node.openGenerics then - visitToken(node.openGenerics, visitor) + if node.opengenerics then + visitToken(node.opengenerics, visitor) end if node.generics then visitPunctuated(node.generics, visitor, visitGeneric) end - if node.genericPacks then - visitPunctuated(node.genericPacks, visitor, visitGenericPack) + if node.genericpacks then + visitPunctuated(node.genericpacks, visitor, visitGenericPack) end - if node.closeGenerics then - visitToken(node.closeGenerics, visitor) + if node.closegenerics then + visitToken(node.closegenerics, visitor) end - visitToken(node.openParens, visitor) + visitToken(node.openparens, visitor) visitPunctuated(node.parameters, visitor, visitLocal) if node.vararg then visitToken(node.vararg, visitor) end - if node.varargColon then - visitToken(node.varargColon, visitor) + if node.varargcolon then + visitToken(node.varargcolon, visitor) end - if node.varargAnnotation then - visitTypePack(node.varargAnnotation, visitor) + if node.varargannotation then + visitTypePack(node.varargannotation, visitor) end - visitToken(node.closeParens, visitor) - if node.returnSpecifier then - visitToken(node.returnSpecifier, visitor) + visitToken(node.closeparens, visitor) + if node.returnspecifier then + visitToken(node.returnspecifier, visitor) end - if node.returnAnnotation then - visitTypePack(node.returnAnnotation, visitor) + if node.returnannotation then + visitTypePack(node.returnannotation, visitor) end visitBlock(node.body, visitor) - visitToken(node.endKeyword, visitor) + visitToken(node.endkeyword, visitor) end local function visitAttribute(node: luau.AstAttribute, visitor) @@ -431,7 +431,7 @@ local function visitAnonymousFunction(node: luau.AstExprAnonymousFunction, visit for _, attribute in node.attributes do visitAttribute(attribute, visitor) end - visitToken(node.functionKeyword, visitor) + visitToken(node.functionkeyword, visitor) visitFunctionBody(node.body, visitor) end end @@ -441,7 +441,7 @@ local function visitFunction(node: luau.AstStatFunction, visitor: Visitor) for _, attribute in node.attributes do visitAttribute(attribute, visitor) end - visitToken(node.functionKeyword, visitor) + visitToken(node.functionkeyword, visitor) visitExpression(node.name, visitor) visitFunctionBody(node.body, visitor) end @@ -452,8 +452,8 @@ local function visitLocalFunction(node: luau.AstStatLocalFunction, visitor: Visi for _, attribute in node.attributes do visitAttribute(attribute, visitor) end - visitToken(node.localKeyword, visitor) - visitToken(node.functionKeyword, visitor) + visitToken(node.localkeyword, visitor) + visitToken(node.functionkeyword, visitor) visitLocal(node.name, visitor) visitFunctionBody(node.body, visitor) end @@ -465,7 +465,7 @@ local function visitStatTypeFunction(node: luau.AstStatTypeFunction, visitor: Vi visitToken(node.export, visitor) end visitToken(node.type, visitor) - visitToken(node.functionKeyword, visitor) + visitToken(node.functionkeyword, visitor) visitToken(node.name, visitor) visitFunctionBody(node.body, visitor) end @@ -480,9 +480,9 @@ local function visitTableItem(node: luau.AstExprTableItem, visitor: Visitor) visitToken(node.equals, visitor) visitExpression(node.value, visitor) elseif node.kind == "general" then - visitToken(node.indexerOpen, visitor) + visitToken(node.indexeropen, visitor) visitExpression(node.key, visitor) - visitToken(node.indexerClose, visitor) + visitToken(node.indexerclose, visitor) visitToken(node.equals, visitor) visitExpression(node.value, visitor) else @@ -497,11 +497,11 @@ end local function visitTable(node: luau.AstExprTable, visitor: Visitor) if visitor.visitTable(node) then - visitToken(node.openBrace, visitor) + visitToken(node.openbrace, visitor) for _, item in node.entries do visitTableItem(item, visitor) end - visitToken(node.closeBrace, visitor) + visitToken(node.closebrace, visitor) end end @@ -516,17 +516,17 @@ end local function visitIndexExpr(node: luau.AstExprIndexExpr, visitor: Visitor) if visitor.visitIndexExpr(node) then visitExpression(node.expression, visitor) - visitToken(node.openBrackets, visitor) + visitToken(node.openbrackets, visitor) visitExpression(node.index, visitor) - visitToken(node.closeBrackets, visitor) + visitToken(node.closebrackets, visitor) end end local function visitGroup(node: luau.AstExprGroup, visitor: Visitor) if visitor.visitGroup(node) then - visitToken(node.openParens, visitor) + visitToken(node.openparens, visitor) visitExpression(node.expression, visitor) - visitToken(node.closeParens, visitor) + visitToken(node.closeparens, visitor) end end @@ -551,17 +551,17 @@ end local function visitIfExpression(node: luau.AstExprIfElse, visitor: Visitor) if visitor.visitIfExpression(node) then - visitToken(node.ifKeyword, visitor) + visitToken(node.ifkeyword, visitor) visitExpression(node.condition, visitor) - visitToken(node.thenKeyword, visitor) + visitToken(node.thenkeyword, visitor) visitExpression(node.consequent, visitor) for _, elseifs in node.elseifs do - visitToken(elseifs.elseifKeyword, visitor) + visitToken(elseifs.elseifkeyword, visitor) visitExpression(elseifs.condition, visitor) - visitToken(elseifs.thenKeyword, visitor) + visitToken(elseifs.thenkeyword, visitor) visitExpression(elseifs.consequent, visitor) end - visitToken(node.elseKeyword, visitor) + visitToken(node.elsekeyword, visitor) visitExpression(node.antecedent, visitor) end end @@ -579,18 +579,18 @@ local function visitTypeReference(node: luau.AstTypeReference, visitor: Visitor) if node.prefix then visitToken(node.prefix, visitor) end - if node.prefixPoint then - visitToken(node.prefixPoint, visitor) + if node.prefixpoint then + visitToken(node.prefixpoint, visitor) end visitToken(node.name, visitor) - if node.openParameters then - visitToken(node.openParameters, visitor) + if node.openparameters then + visitToken(node.openparameters, visitor) end if node.parameters then visitPunctuated(node.parameters, visitor, visitTypeOrPack) end - if node.closeParameters then - visitToken(node.closeParameters, visitor) + if node.closeparameters then + visitToken(node.closeparameters, visitor) end end end @@ -610,17 +610,17 @@ end local function visitTypeTypeof(node: luau.AstTypeTypeof, visitor: Visitor) if visitor.visitTypeTypeof(node) then visitToken(node.typeof, visitor) - visitToken(node.openParens, visitor) + visitToken(node.openparens, visitor) visitExpression(node.expression, visitor) - visitToken(node.closeParens, visitor) + visitToken(node.closeparens, visitor) end end local function visitTypeGroup(node: luau.AstTypeGroup, visitor: Visitor) if visitor.visitTypeGroup(node) then - visitToken(node.openParens, visitor) + visitToken(node.openparens, visitor) visitType(node.type, visitor) - visitToken(node.closeParens, visitor) + visitToken(node.closeparens, visitor) end end @@ -644,30 +644,30 @@ end local function visitTypeArray(node: luau.AstTypeArray, visitor: Visitor) if visitor.visitTypeArray(node) then - visitToken(node.openBrace, visitor) + visitToken(node.openbrace, visitor) if node.access then visitToken(node.access, visitor) end visitType(node.type, visitor) - visitToken(node.closeBrace, visitor) + visitToken(node.closebrace, visitor) end end local function visitTypeTable(node: luau.AstTypeTable, visitor: Visitor) if visitor.visitTypeTable(node) then - visitToken(node.openBrace, visitor) + visitToken(node.openbrace, visitor) for _, entry in node.entries do if entry.access then visitToken(entry.access, visitor) end if entry.kind == "indexer" then - visitToken(entry.indexerOpen, visitor) + visitToken(entry.indexeropen, visitor) visitType(entry.key, visitor) - visitToken(entry.indexerClose, visitor) + visitToken(entry.indexerclose, visitor) elseif entry.kind == "stringproperty" then - visitToken(entry.indexerOpen, visitor) + visitToken(entry.indexeropen, visitor) visitTypeString(entry.key, visitor) - visitToken(entry.indexerClose, visitor) + visitToken(entry.indexerclose, visitor) else visitToken(entry.key, visitor) end @@ -677,7 +677,7 @@ local function visitTypeTable(node: luau.AstTypeTable, visitor: Visitor) visitToken(entry.separator, visitor) end end - visitToken(node.closeBrace, visitor) + visitToken(node.closebrace, visitor) end end @@ -693,40 +693,40 @@ end local function visitTypeFunction(node: luau.AstTypeFunction, visitor: Visitor) if visitor.visitTypeFunction(node) then - if node.openGenerics then - visitToken(node.openGenerics, visitor) + if node.opengenerics then + visitToken(node.opengenerics, visitor) end if node.generics then visitPunctuated(node.generics, visitor, visitGeneric) end - if node.genericPacks then - visitPunctuated(node.genericPacks, visitor, visitGenericPack) + if node.genericpacks then + visitPunctuated(node.genericpacks, visitor, visitGenericPack) end - if node.closeGenerics then - visitToken(node.closeGenerics, visitor) + if node.closegenerics then + visitToken(node.closegenerics, visitor) end - visitToken(node.openParens, visitor) + visitToken(node.openparens, visitor) visitPunctuated(node.parameters, visitor, visitTypeFunctionParameter) if node.vararg then visitTypePack(node.vararg, visitor) end - visitToken(node.closeParens, visitor) - visitToken(node.returnArrow, visitor) - visitTypePack(node.returnTypes, visitor) + visitToken(node.closeparens, visitor) + visitToken(node.returnarrow, visitor) + visitTypePack(node.returntypes, visitor) end end local function visitTypePackExplicit(node: luau.AstTypePackExplicit, visitor: Visitor) if visitor.visitTypePackExplicit(node) then - if node.openParens then - visitToken(node.openParens, visitor) + if node.openparens then + visitToken(node.openparens, visitor) end visitPunctuated(node.types, visitor, visitType) - if node.tailType then - visitTypePack(node.tailType, visitor) + if node.tailtype then + visitTypePack(node.tailtype, visitor) end - if node.closeParens then - visitToken(node.closeParens, visitor) + if node.closeparens then + visitToken(node.closeparens, visitor) end end end diff --git a/tests/testAstSerializer.test.luau b/tests/testAstSerializer.test.luau index ccec3bfec..c411125f8 100644 --- a/tests/testAstSerializer.test.luau +++ b/tests/testAstSerializer.test.luau @@ -25,11 +25,11 @@ local function test_tokenContainsLeadingSpaces() local l = block.statements[1] assert(l.tag == "local") - local token = l.localKeyword - assert(#token.leadingTrivia == 1) - assert(token.leadingTrivia[1].tag == "whitespace") - assert(token.leadingTrivia[1].text == " ") - assertEqualsLocation(token.leadingTrivia[1].location, 0, 0, 0, 2) + local token = l.localkeyword + assert(#token.leadingtrivia == 1) + assert(token.leadingtrivia[1].tag == "whitespace") + assert(token.leadingtrivia[1].text == " ") + assertEqualsLocation(token.leadingtrivia[1].location, 0, 0, 0, 2) end local function test_tokenContainsLeadingNewline() @@ -39,11 +39,11 @@ local function test_tokenContainsLeadingNewline() local l = block.statements[1] assert(l.tag == "local") - local token = l.localKeyword - assert(#token.leadingTrivia == 1) - assert(token.leadingTrivia[1].tag == "whitespace") - assert(token.leadingTrivia[1].text == "\n") - assertEqualsLocation(token.leadingTrivia[1].location, 0, 0, 1, 0) + local token = l.localkeyword + assert(#token.leadingtrivia == 1) + assert(token.leadingtrivia[1].tag == "whitespace") + assert(token.leadingtrivia[1].text == "\n") + assertEqualsLocation(token.leadingtrivia[1].location, 0, 0, 1, 0) end local function test_tokenContainsLeadingSingleLineComment() @@ -53,14 +53,14 @@ local function test_tokenContainsLeadingSingleLineComment() local l = block.statements[1] assert(l.tag == "local") - local token = l.localKeyword - assert(#token.leadingTrivia == 2) - assert(token.leadingTrivia[1].tag == "comment") - assert(token.leadingTrivia[1].text == "-- comment") - assertEqualsLocation(token.leadingTrivia[1].location, 0, 0, 0, 10) - assert(token.leadingTrivia[2].tag == "whitespace") - assert(token.leadingTrivia[2].text == "\n") - assertEqualsLocation(token.leadingTrivia[2].location, 0, 10, 1, 0) + local token = l.localkeyword + assert(#token.leadingtrivia == 2) + assert(token.leadingtrivia[1].tag == "comment") + assert(token.leadingtrivia[1].text == "-- comment") + assertEqualsLocation(token.leadingtrivia[1].location, 0, 0, 0, 10) + assert(token.leadingtrivia[2].tag == "whitespace") + assert(token.leadingtrivia[2].text == "\n") + assertEqualsLocation(token.leadingtrivia[2].location, 0, 10, 1, 0) end local function test_tokenContainsLeadingBlockComment() @@ -70,14 +70,14 @@ local function test_tokenContainsLeadingBlockComment() local l = block.statements[1] assert(l.tag == "local") - local token = l.localKeyword - assert(#token.leadingTrivia == 2) - assert(token.leadingTrivia[1].tag == "blockcomment") - assert(token.leadingTrivia[1].text == "--[[ comment ]]") - assertEqualsLocation(token.leadingTrivia[1].location, 0, 0, 0, 15) - assert(token.leadingTrivia[2].tag == "whitespace") - assert(token.leadingTrivia[2].text == " ") - assertEqualsLocation(token.leadingTrivia[2].location, 0, 15, 0, 16) + local token = l.localkeyword + assert(#token.leadingtrivia == 2) + assert(token.leadingtrivia[1].tag == "blockcomment") + assert(token.leadingtrivia[1].text == "--[[ comment ]]") + assertEqualsLocation(token.leadingtrivia[1].location, 0, 0, 0, 15) + assert(token.leadingtrivia[2].tag == "whitespace") + assert(token.leadingtrivia[2].text == " ") + assertEqualsLocation(token.leadingtrivia[2].location, 0, 15, 0, 16) end local function test_tokenizeWhitespace() @@ -87,11 +87,11 @@ local function test_tokenizeWhitespace() local l = block.statements[1] assert(l.tag == "local") - local token = l.localKeyword - assert(#token.leadingTrivia == 3) - assert(token.leadingTrivia[1].text == " \n") - assert(token.leadingTrivia[2].text == "\t\t\n") - assert(token.leadingTrivia[3].text == "\n") + local token = l.localkeyword + assert(#token.leadingtrivia == 3) + assert(token.leadingtrivia[1].text == " \n") + assert(token.leadingtrivia[2].text == "\t\t\n") + assert(token.leadingtrivia[3].text == "\n") end local function test_triviaSplitBetweenLeadingAndTrailing() @@ -103,18 +103,18 @@ local function test_triviaSplitBetweenLeadingAndTrailing() local trailingToken = firstStmt.values[1].node assert(trailingToken.tag == "string") - assert(#trailingToken.trailingTrivia == 3) - assert(trailingToken.trailingTrivia[1].text == " ") - assert(trailingToken.trailingTrivia[2].text == "-- comment") - assert(trailingToken.trailingTrivia[3].text == "\n") + assert(#trailingToken.trailingtrivia == 3) + assert(trailingToken.trailingtrivia[1].text == " ") + assert(trailingToken.trailingtrivia[2].text == "-- comment") + assert(trailingToken.trailingtrivia[3].text == "\n") local secondStmt = block.statements[2] assert(secondStmt.tag == "local") - local leadingToken = secondStmt.localKeyword - assert(#leadingToken.leadingTrivia == 2) - assert(leadingToken.leadingTrivia[1].text == "-- comment 2") - assert(leadingToken.leadingTrivia[2].text == "\n") + local leadingToken = secondStmt.localkeyword + assert(#leadingToken.leadingtrivia == 2) + assert(leadingToken.leadingtrivia[1].text == "-- comment 2") + assert(leadingToken.leadingtrivia[2].text == "\n") end local function test_roundtrippableAst() @@ -138,29 +138,29 @@ local function test_lineOffsetsField() local singleLineCode = "local x = 1" local result = luau.parse(singleLineCode) - assert(result.lineOffsets) - assert(type(result.lineOffsets) == "table", "lineOffsets should be a table") - assert(#result.lineOffsets == 1, "Should have 1 line offset for single line code") -- only line 0 - assert(result.lineOffsets[1] == 0, "First line should start at offset 0") + assert(result.lineoffsets) + assert(type(result.lineoffsets) == "table", "lineoffsets should be a table") + assert(#result.lineoffsets == 1, "Should have 1 line offset for single line code") -- only line 0 + assert(result.lineoffsets[1] == 0, "First line should start at offset 0") local multiLineCode = "local x = 1\nlocal y = 2\nlocal z = 3" local multiResult = luau.parse(multiLineCode) - assert(multiResult.lineOffsets) - assert(type(multiResult.lineOffsets) == "table", "lineOffsets should be a table for multiline") - assert(#multiResult.lineOffsets == 3, "Should have 3 line offsets for 3-line code") -- lines 0, 1, 2 - assert(multiResult.lineOffsets[1] == 0, "First line should start at offset 0") - assert(multiResult.lineOffsets[2] == 12, "Second line should start after 'local x = 1\\n'") - assert(multiResult.lineOffsets[3] == 24, "Third line should start after 'local x = 1\\nlocal y = 2\\n'") + assert(multiResult.lineoffsets) + assert(type(multiResult.lineoffsets) == "table", "lineoffsets should be a table for multiline") + assert(#multiResult.lineoffsets == 3, "Should have 3 line offsets for 3-line code") -- lines 0, 1, 2 + assert(multiResult.lineoffsets[1] == 0, "First line should start at offset 0") + assert(multiResult.lineoffsets[2] == 12, "Second line should start after 'local x = 1\\n'") + assert(multiResult.lineoffsets[3] == 24, "Third line should start after 'local x = 1\\nlocal y = 2\\n'") local emptyLineCode = "local x = 1\n\nlocal y = 2" local emptyResult = luau.parse(emptyLineCode) - assert(emptyResult.lineOffsets) - assert(#emptyResult.lineOffsets == 3, "Should have 3 line offsets including empty line") - assert(emptyResult.lineOffsets[1] == 0, "First line should start at offset 0") - assert(emptyResult.lineOffsets[2] == 12, "Second line (empty) should start after 'local x = 1\\n'") - assert(emptyResult.lineOffsets[3] == 13, "Third line should start after empty line") + assert(emptyResult.lineoffsets) + assert(#emptyResult.lineoffsets == 3, "Should have 3 line offsets including empty line") + assert(emptyResult.lineoffsets[1] == 0, "First line should start at offset 0") + assert(emptyResult.lineoffsets[2] == 12, "Second line (empty) should start after 'local x = 1\\n'") + assert(emptyResult.lineoffsets[3] == 13, "Third line should start after empty line") end test_tokenContainsLeadingSpaces() From 2cabbe6773e3f52f27aa69844af61c4f29d3f324 Mon Sep 17 00:00:00 2001 From: nerix Date: Thu, 16 Oct 2025 23:43:38 +0200 Subject: [PATCH 044/642] fix: reserve stack slot for thread (#432) When running a script with `LUA_MINSTACK - 1` (19) or more arguments, Luau fails with `L->top < L->ci->top`, because `getRefForThread` tries to push the current thread onto a full stack [in `runBytecode`](https://github.com/luau-lang/lute/blob/a70354de11446cc95b65f00d853b0c3d0b0faf84/lute/cli/src/climain.cpp#L115). The stack only had `argc` slots, because it was resized in `setupArguments`. With this PR, `setupArguments` reserves one more slot (an alternative would be to reserve space in `getRefForThread`). `setupArguments` should probably be declared `static` as well, since it's not declared in any header. --- lute/cli/src/climain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 8277fb7de..6af5f23b0 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -70,7 +70,7 @@ lua_State* setupCliState(Runtime& runtime, std::function preSa bool setupArguments(lua_State* L, int argc, char** argv) { - if (!lua_checkstack(L, argc)) + if (!lua_checkstack(L, argc + 1)) return false; for (int i = 0; i < argc; ++i) From b564ea0fec65c2a2dc581643431bdb2291db9f3e Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Fri, 17 Oct 2025 08:44:28 -0700 Subject: [PATCH 045/642] Reserve stack space for creating thread refs in `getRefForThread` itself (#436) --- lute/cli/src/climain.cpp | 4 ++-- lute/runtime/src/ref.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 6af5f23b0..eb0235dba 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -68,9 +68,9 @@ lua_State* setupCliState(Runtime& runtime, std::function preSa ); } -bool setupArguments(lua_State* L, int argc, char** argv) +static bool setupArguments(lua_State* L, int argc, char** argv) { - if (!lua_checkstack(L, argc + 1)) + if (!lua_checkstack(L, argc)) return false; for (int i = 0; i < argc; ++i) diff --git a/lute/runtime/src/ref.cpp b/lute/runtime/src/ref.cpp index be4e0f60f..60e13baba 100644 --- a/lute/runtime/src/ref.cpp +++ b/lute/runtime/src/ref.cpp @@ -23,6 +23,7 @@ void Ref::push(lua_State* L) const std::shared_ptr getRefForThread(lua_State* L) { + lua_rawcheckstack(L, 1); lua_pushthread(L); std::shared_ptr ref = std::make_shared(L, -1); lua_pop(L, 1); From 8079e83717dd9a031b2349106a235d9af8a93b2f Mon Sep 17 00:00:00 2001 From: ariel Date: Fri, 17 Oct 2025 10:15:57 -0700 Subject: [PATCH 046/642] std: adopt underscore prefixes for internal data in std/time (#435) This PR takes a different approach to #434 by saying "yes, the data will be visible, and we'll communicate intent for it to generally be undisturbed with an underscore prefix." This is generally accepted as the idiom for information hiding in Luau today, and I don't feel like this is worth making a point of difference. --- lute/std/libs/time.luau | 54 ++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lute/std/libs/time.luau b/lute/std/libs/time.luau index bbf1e5a8b..ee2e63b99 100644 --- a/lute/std/libs/time.luau +++ b/lute/std/libs/time.luau @@ -9,8 +9,8 @@ local HOURS_PER_DAY = 24 local DAYS_PER_WEEK = 7 type durationdata = { - seconds: number, - nanoseconds: number, + _seconds: number, + _nanoseconds: number, } local duration = {} @@ -21,8 +21,8 @@ export type duration = setmetatable function duration.create(seconds: number, nanoseconds: number): duration local self = { - seconds = seconds, - nanoseconds = nanoseconds, + _seconds = seconds, + _nanoseconds = nanoseconds, } return setmetatable(self, duration) @@ -70,36 +70,36 @@ function duration.weeks(weeks: number): duration end function duration.subsecmillis(self: duration): number - return math.floor(self.nanoseconds / NANOS_PER_MILLI) + return math.floor(self._nanoseconds / NANOS_PER_MILLI) end function duration.subsecmicros(self: duration): number - return math.floor(self.nanoseconds / NANOS_PER_MICRO) + return math.floor(self._nanoseconds / NANOS_PER_MICRO) end function duration.subsecnanos(self: duration): number - return self.nanoseconds + return self._nanoseconds end function duration.toseconds(self: duration): number - return self.seconds + self.nanoseconds / NANOS_PER_SEC + return self._seconds + self._nanoseconds / NANOS_PER_SEC end function duration.tomilliseconds(self: duration): number - return self.seconds * MILLIS_PER_SEC + self:subsecmillis() + return self._seconds * MILLIS_PER_SEC + self:subsecmillis() end function duration.tomicroseconds(self: duration): number - return self.seconds * MICROS_PER_SEC + self:subsecmicros() + return self._seconds * MICROS_PER_SEC + self:subsecmicros() end function duration.tonanoseconds(self: duration): number - return self.seconds * NANOS_PER_SEC + self:subsecnanos() + return self._seconds * NANOS_PER_SEC + self:subsecnanos() end function duration.__add(a: duration, b: duration): duration - local seconds = a.seconds + b.seconds - local nanoseconds = a.nanoseconds + b.nanoseconds + local seconds = a._seconds + b._seconds + local nanoseconds = a._nanoseconds + b._nanoseconds if nanoseconds >= NANOS_PER_SEC then seconds += 1 @@ -110,8 +110,8 @@ function duration.__add(a: duration, b: duration): duration end function duration.__sub(a: duration, b: duration): duration - local seconds = a.seconds - b.seconds - local nanoseconds = a.nanoseconds - b.nanoseconds + local seconds = a._seconds - b._seconds + local nanoseconds = a._nanoseconds - b._nanoseconds if nanoseconds < 0 then seconds -= 1 @@ -122,8 +122,8 @@ function duration.__sub(a: duration, b: duration): duration end function duration.__mul(a: duration, b: number): duration - local seconds = a.seconds * b - local nanoseconds = a.nanoseconds * b + local seconds = a._seconds * b + local nanoseconds = a._nanoseconds * b seconds += math.floor(nanoseconds / NANOS_PER_SEC) nanoseconds %= NANOS_PER_SEC @@ -132,8 +132,8 @@ function duration.__mul(a: duration, b: number): duration end function duration.__div(a: duration, b: number): duration - local seconds, extraSeconds = a.seconds / b, a.seconds % b - local nanoseconds, extraNanos = a.nanoseconds / b, a.nanoseconds % b + local seconds, extraSeconds = a._seconds / b, a._seconds % b + local nanoseconds, extraNanos = a._nanoseconds / b, a._nanoseconds % b nanoseconds += (extraSeconds * NANOS_PER_SEC + extraNanos) / b @@ -141,27 +141,27 @@ function duration.__div(a: duration, b: number): duration end function duration.__eq(a: duration, b: duration): boolean - return a.seconds == b.seconds and a.nanoseconds == b.nanoseconds + return a._seconds == b._seconds and a._nanoseconds == b._nanoseconds end function duration.__lt(a: duration, b: duration): boolean - if a.seconds == b.seconds then - return a.nanoseconds < b.nanoseconds + if a._seconds == b._seconds then + return a._nanoseconds < b._nanoseconds end - return a.seconds < b.seconds + return a._seconds < b._seconds end function duration.__le(a: duration, b: duration): boolean - if a.seconds == b.seconds then - return a.nanoseconds <= b.nanoseconds + if a._seconds == b._seconds then + return a._nanoseconds <= b._nanoseconds end - return a.seconds <= b.seconds + return a._seconds <= b._seconds end function duration.__tostring(self: duration): string - return string.format("%d.%09d", self.seconds, self.nanoseconds) + return string.format("%d.%09d", self._seconds, self._nanoseconds) end return table.freeze({ From 3d063d501ffd7a02b5d10c33292c4c3ed7d21162 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 17 Oct 2025 10:39:48 -0700 Subject: [PATCH 047/642] stdlib: Update the testing api with some more test lifecycle methods + testing assertions (#411) This PR comes with some changes to the testing api to reduce the amount of global state we depend on - when making subcases, we now pass in the suite object on which the user can operate. Additionally, assertions are passed in with the test case itself, making them more accessible and reducing an additional line of code needed to start writing tests. - [x] {before,after}x{all,each} lifecycle methods - [x] Table assertions For example: image --- examples/testing.luau | 55 ++++++++-- lute/std/libs/assert.luau | 75 +++++++++++++ lute/std/libs/test.luau | 214 ++++++++++++++++++++++++++++++-------- tests/smoke.test.luau | 14 +-- 4 files changed, 298 insertions(+), 60 deletions(-) create mode 100644 lute/std/libs/assert.luau diff --git a/examples/testing.luau b/examples/testing.luau index 06adde5cc..ccc0efc31 100644 --- a/examples/testing.luau +++ b/examples/testing.luau @@ -1,21 +1,54 @@ local test = require("@std/test") -local check = test.assert -test.case("foo", function() - check.eq(3, 3) - check.eq(6, 1) + +test.case("foo", function(assert) + assert.eq(3, 3) + assert.eq(6, 1) end) -test.case("bar", function() - check.eq("a", "b") +test.case("bar", function(assert) + assert.eq("a", "b") end) -test.case("baz", function() - check.neq("a", "b") +test.case("baz", function(assert) + assert.neq("a", "b") end) -test.suite("MySuite", function() - test.case("subcase", function() - check.eq(5 * 5, 3) +test.suite("MySuite", function(suite) + -- beforeall runs once before all tests in the suite + suite:beforeall(function() + print("Setting up MySuite - runs once before all tests") + end) + + -- beforeeach runs before each individual test + suite:beforeeach(function() + print("Running before each test") + end) + + -- aftereach runs after each individual test + suite:aftereach(function() + print("Cleaning up after each test") + end) + + -- afterall runs once after all tests in the suite + suite:afterall(function() + print("Tearing down MySuite - runs once after all tests") + end) + + suite:case("subsuite", function(assert) + assert.neq(1, 2) + assert.eq(1, 2) + end) + + suite:case("another_test", function(assert) + assert.eq(1, 1) + end) + + -- Example of table_eq assertion - demonstrates error reporting + suite:case("table_comparison", function(assert) + local table1 = { a = 1, b = { c = 2, d = 3 } } + local table2 = { a = 1, b = { c = 2, d = 3 } } + assert.tableeq(table1, table2) + assert.tableeq(table1, { a = 1 }) end) end) diff --git a/lute/std/libs/assert.luau b/lute/std/libs/assert.luau new file mode 100644 index 000000000..813894f2e --- /dev/null +++ b/lute/std/libs/assert.luau @@ -0,0 +1,75 @@ +local assertions = {} + +function assertions.eq(lhs: T, rhs: T) + if lhs ~= rhs then + error({ msg = `{lhs} ~= {rhs}` }) + end +end + +function assertions.neq(lhs: T, rhs: T) + if lhs == rhs then + error({ msg = `{lhs} == {rhs}` }) + end +end + +-- Helper to format values for error messages +local function formatValue(val: any): string + if type(val) == "nil" then + return "nil" + elseif type(val) == "string" then + return `"{val}"` + elseif type(val) == "table" then + return "table" + else + return tostring(val) + end +end + +-- Structural equality for two tables +local function tablesEqual(t1: unknown, t2: unknown, path: string): (boolean, string?) + if typeof(t1) ~= "table" or typeof(t2) ~= "table" then + if t1 ~= t2 then + return false, `lhs{path} = {formatValue(t1)} but rhs{path} = {formatValue(t2)}` + end + return true, nil + end + + -- Check all keys in t1 + for k, v1 in t1 do + local v2 = (t2 :: any)[k] + local newPath = if path == "" then `[{k}]` else `{path}[{k}]` + + -- Check if key exists in t2 + if v2 == nil then + return false, `lhs{newPath} = {formatValue(v1)} but rhs{newPath} does not exist` + end + + local equal, msg = tablesEqual(v1, v2, newPath) + if not equal then + return false, msg + end + end + + -- Check for keys in t2 that aren't in t1 + for k, v in t2 do + local v1 = (t1 :: any)[k] + if v1 == nil then + local newPath = if path == "" then `[{k}]` else `{path}[{k}]` + return false, `rhs{newPath} = {formatValue(v)} but lhs{newPath} does not exist` + end + end + + return true, nil +end + +-- Table equality assertionsion with deep comparison +function assertions.tableeq(lhs: { [unknown]: unknown }, rhs: { [unknown]: unknown }) + local equal, msg = tablesEqual(lhs, rhs, "") + if not equal then + error({ msg = msg or "tables are not equal" }) + end +end + +export type Asserts = typeof(assertions) + +return table.freeze(assertions) diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index 736453022..d643a51b0 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -1,10 +1,60 @@ --!strict local ps = require("@lute/process") -type Test = () -> () +local assert = require("./assert") +-- Test Types +type Test = (assert.Asserts) -> () type TestCase = { name: string, case: Test } -type TestCases = { TestCase } -type TestSuite = { name: string, cases: TestCases } +type TestSuite = { + name: string, + cases: { TestCase }, + _beforeeach: (() -> ())?, + _beforeall: (() -> ())?, + _aftereach: (() -> ())?, + _afterall: (() -> ())?, + case: (self: TestSuite, string, Test) -> (), + beforeeach: (self: TestSuite, () -> ()) -> (), + beforeall: (self: TestSuite, () -> ()) -> (), + aftereach: (self: TestSuite, () -> ()) -> (), + afterall: (self: TestSuite, () -> ()) -> (), +} + +local TestSuite = {} +TestSuite.__index = TestSuite + +function TestSuite.new(name: string): TestSuite + local self = setmetatable({ + name = name, + cases = {}, + _beforeeach = nil, + _beforeall = nil, + _aftereach = nil, + _afterall = nil, + }, TestSuite) + return self :: TestSuite +end + +function TestSuite:case(name: string, testCase: Test) + table.insert(self.cases, { name = name, case = testCase }) +end + +function TestSuite:beforeeach(hook: () -> ()) + self._beforeeach = hook +end + +function TestSuite:beforeall(hook: () -> ()) + self._beforeall = hook +end + +function TestSuite:aftereach(hook: () -> ()) + self._aftereach = hook +end + +function TestSuite:afterall(hook: () -> ()) + self._afterall = hook +end + +-- Test Reporting type FailedTest = { test: string, -- name of the test case that failed assertion: string, -- name of the assertion that failed (e.g., "assert.eq") @@ -20,8 +70,6 @@ type TestRunResult = { passed: number, } -type TestReporter = (TestRunResult) -> () - local function printFailedTest(failed: FailedTest) print(`❌ {failed.test}`) print(` {failed.filename}:{failed.linenumber}`) @@ -48,62 +96,111 @@ local function simpleReporter(result: TestRunResult) print(string.rep("=", 50) .. "\n") end +export type TestReporter = (TestRunResult) -> () + +-- Testing Environment type TestEnvironment = { - anonymous: TestCases, + anonymous: { TestCase }, suites: { TestSuite }, reporter: TestReporter, - active_suite: TestSuite?, } local env: TestEnvironment = { anonymous = {}, suites = {}, reporter = simpleReporter } +-- Test library export surface local test = {} -test.assert = {} -local assert = test.assert --- todo: we should add some more assertions for diffing tables! -function assert.eq(lhs: T, rhs: T) - if lhs ~= rhs then - error({ msg = `{lhs} ~= {rhs}` }) - end -end -function assert.neq(lhs: T, rhs: T) - if lhs == rhs then - error({ msg = `{lhs} == {rhs}` }) - end +function test.setreporter(reporter: TestReporter) + test.reporter = reporter end --- register a test case -function test.case(name: string, case: () -> ()) +-- register an anonymous test case +function test.case(name: string, case: Test) local testcase = { name = name, case = case } - if env.active_suite then - local _, cases = env.active_suite.name, env.active_suite.cases - table.insert(cases, testcase) - else - table.insert(env.anonymous, testcase) - end + table.insert(env.anonymous, testcase) end -- register a test suite --- takes name and a function to register -function test.suite(name: string, registerSuite: () -> ()) - local testsuite: TestSuite = { name = name, cases = {} } - table.insert(env.suites, testsuite) - env.active_suite = testsuite - registerSuite() - env.active_suite = nil -end - -function test.setreporter(reporter: TestReporter) - test.reporter = reporter +function test.suite(name: string, registerFn: (TestSuite) -> ()) + local suite = TestSuite.new(name) + registerFn(suite) + table.insert(env.suites, suite) end +-- run all the tests function test.run() local failures: { FailedTest } = {} local total = 0 local failed = 0 local passed = 0 + -- Error handler for beforeall hook + local function beforeallErrorHandler(suite: TestSuiteData) + return function(err) + -- If beforeall fails, skip all tests in the suite + total += #suite.cases + failed += #suite.cases + for _, tc in suite.cases do + local failedtest: FailedTest = { + test = `{suite.name}.{tc.name}`, + assertion = "beforeall", + error = `beforeall hook failed: {err}`, + filename = "", + linenumber = 0, + } + table.insert(failures, failedtest) + end + return tostring(err) + end + end + + -- Error handler for beforeeach hook + local function beforeeachErrorHandler(testFullName: string) + return function(err) + failed += 1 + local failedtest: FailedTest = { + test = testFullName, + assertion = "beforeeach", + error = `beforeeach hook failed: {err}`, + filename = "", + linenumber = 0, + } + table.insert(failures, failedtest) + return tostring(err) + end + end + + -- Error handler for aftereach hook + local function afterachErrorHandler(testFullName: string) + return function(err) + local failedtest: FailedTest = { + test = testFullName, + assertion = "aftereach", + error = `aftereach hook failed: {err}`, + filename = "", + linenumber = 0, + } + table.insert(failures, failedtest) + return tostring(err) + end + end + + -- Error handler for afterall hook + local function afterallErrorHandler(suiteName: string) + return function(err) + failed += 1 + local failedtest: FailedTest = { + test = suiteName, + assertion = "afterall", + error = `afterall hook failed: {err}`, + filename = "", + linenumber = 0, + } + table.insert(failures, failedtest) + return tostring(err) + end + end + -- Run anonymous tests for _, tc in env.anonymous do total += 1 @@ -122,7 +219,7 @@ function test.run() failed += 1 end - local success = xpcall(tc.case, handlefailure) + local success = xpcall(tc.case, handlefailure, assert) if success then passed += 1 end @@ -130,28 +227,61 @@ function test.run() -- Run tests from suites for _, suite in env.suites do + -- Run beforeall hook once per suite + if suite._beforeall then + local beforeallSuccess = xpcall(suite._beforeall, beforeallErrorHandler(suite)) + if not beforeallSuccess then + -- If beforeall fails, skip all tests in the suite + continue + end + end + for _, tc in suite.cases do total += 1 + local testFullName = `{suite.name}.{tc.name}` local function handlefailure(err) local fileName, lineNumber = debug.info(4, "sl") local assertName = debug.info(3, "n") local failedtest: FailedTest = { - test = `{suite.name}.{tc.name}`, + test = testFullName, assertion = assertName, error = err.msg, filename = fileName, linenumber = lineNumber, } table.insert(failures, failedtest) - failed += 1 end - local success = xpcall(tc.case, handlefailure) + -- Run beforeeach hook if it exists + if suite._beforeeach then + local beforeSuccess = xpcall(suite._beforeeach, beforeeachErrorHandler(testFullName)) + if not beforeSuccess then + continue + end + end + + local success = xpcall(tc.case, handlefailure, assert) + -- Run aftereach hook if it exists (runs even if test failed) + if suite._aftereach then + local afterSuccess = xpcall(suite._aftereach, afterachErrorHandler(testFullName)) + if not afterSuccess then + -- If test passed but aftereach failed, mark as failed + success = false + end + end + -- if the test and the teardown method passed, the test passes if success then passed += 1 + else + failed += 1 end end + + -- Run afterall hook once per suite (runs even if tests failed) + if suite._afterall then + xpcall(suite._afterall, afterallErrorHandler(suite.name)) + end end local result: TestRunResult = { diff --git a/tests/smoke.test.luau b/tests/smoke.test.luau index 7ace4d649..84f6d32b5 100644 --- a/tests/smoke.test.luau +++ b/tests/smoke.test.luau @@ -1,15 +1,15 @@ local test = require("@std/test") -test.suite("SmokeSuite", function() - test.case("make_fail", function() - -- test.assert.eq(1, 2) - test.assert.eq(1, 1) -- comment this and uncomment above to make the test fail +test.suite("SmokeSuite", function(suite) + suite:case("make_fail", function(assert) + -- assert.eq(1, 2) + assert.eq(1, 1) -- comment this and uncomment above to make the test fail end) - test.case("make_pass", function() - test.assert.eq(1, 1) + suite:case("make_pass", function(assert) + assert.eq(1, 1) end) end) -test.case("Passing", function() end) +test.case("Passing", function(assert) end) test.run() From 56ae06a5fe1ceb860228d5322442d1edf31e2c0b Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:05:33 -0700 Subject: [PATCH 048/642] [Infra] Enable AddressSanitizer in CI builds (#438) --- .github/workflows/ci.yml | 10 ++++++---- CMakeLists.txt | 8 ++++++++ tools/luthier.luau | 5 +++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84757cc5f..77085d505 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,11 @@ jobs: - os: windows-latest options: --c-compiler cl.exe --cxx-compiler cl.exe - os: ubuntu-latest - options: --c-compiler gcc --cxx-compiler g++ + options: --c-compiler gcc --cxx-compiler g++ --enable-asan - os: ubuntu-latest - options: --c-compiler clang --cxx-compiler clang++ + options: --c-compiler clang --cxx-compiler clang++ --enable-asan - os: macos-latest + options: --enable-asan fail-fast: false steps: @@ -53,10 +54,11 @@ jobs: - os: windows-latest options: --c-compiler cl.exe --cxx-compiler cl.exe - os: ubuntu-latest - options: --c-compiler gcc --cxx-compiler g++ + options: --c-compiler gcc --cxx-compiler g++ --enable-asan - os: ubuntu-latest - options: --c-compiler clang --cxx-compiler clang++ + options: --c-compiler clang --cxx-compiler clang++ --enable-asan - os: macos-latest + options: --enable-asan fail-fast: false steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cdeda91a..c334ca92e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,6 +114,14 @@ else() list(APPEND LUTE_OPTIONS -Werror) # Warnings are errors endif() +option(LUTE_ENABLE_ASAN "Enable AddressSanitizer" OFF) +if (LUTE_ENABLE_ASAN) + if(NOT MSVC) + add_compile_options(-fsanitize=address -fno-omit-frame-pointer) + add_link_options(-fsanitize=address) + endif() +endif() + # libraries add_subdirectory(lute/require) add_subdirectory(lute/runtime) diff --git a/tools/luthier.luau b/tools/luthier.luau index 0cd3f6fac..a59725c9b 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -66,6 +66,7 @@ args:add("clean", "flag", { help = "perform a clean build" }) args:add("which", "flag", { help = "print out the path to the compiled binary and exit", aliases = { "w" } }) args:add("cxx-compiler", "option", { help = "C++ compiler to use" }) args:add("c-compiler", "option", { help = "C compiler to use" }) +args:add("enable-asan", "flag", { help = "enable AddressSanitizer" }) if not isWindows and not isMac and not isLinux then error("Unknown platform " .. os) @@ -566,6 +567,10 @@ local function getConfigureArguments() table.insert(configArgs, "-DCMAKE_C_COMPILER=" .. args:get("c-compiler")) end + if args:has("enable-asan") then + table.insert(configArgs, "-DLUTE_ENABLE_ASAN=ON") + end + return configArgs end From 8ca39638bf7563b761bb1268294da1f6f122d96f Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 17 Oct 2025 21:09:46 -0700 Subject: [PATCH 049/642] stdlib: filepath library (#427) --- lute/std/libs/path/init.luau | 96 +++++ lute/std/libs/path/posix.luau | 227 +++++++++++ lute/std/libs/path/win32.luau | 282 ++++++++++++++ tests/path.posix.test.luau | 534 ++++++++++++++++++++++++++ tests/path.win32.test.luau | 695 ++++++++++++++++++++++++++++++++++ 5 files changed, 1834 insertions(+) create mode 100644 lute/std/libs/path/init.luau create mode 100644 lute/std/libs/path/posix.luau create mode 100644 lute/std/libs/path/win32.luau create mode 100644 tests/path.posix.test.luau create mode 100644 tests/path.win32.test.luau diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau new file mode 100644 index 000000000..0fbff765c --- /dev/null +++ b/lute/std/libs/path/init.luau @@ -0,0 +1,96 @@ +local system = require("@lute/system") + +local posix = require("@self/posix") +local win32 = require("@self/win32") + +export type path = posix.path | win32.path + +export type pathlike = string | path + +local onWindows = system.os == "windows" + +local function basename(path: pathlike): string? + if onWindows then + return win32.basename(path :: win32.pathlike) + else + return posix.basename(path :: posix.pathlike) + end +end + +local function dirname(path: pathlike): string + if onWindows then + return win32.dirname(path :: win32.pathlike) + else + return posix.dirname(path :: posix.pathlike) + end +end + +local function extname(path: pathlike): string + if onWindows then + return win32.extname(path :: win32.pathlike) + else + return posix.extname(path :: posix.pathlike) + end +end + +local function format(path: pathlike): string + if onWindows then + return win32.format(path :: win32.pathlike) + else + return posix.format(path :: posix.pathlike) + end +end + +local function isabsolute(path: pathlike): boolean + if onWindows then + return win32.isabsolute(path :: win32.pathlike) + else + return posix.isabsolute(path :: posix.pathlike) + end +end + +local function join(...: pathlike): path + if onWindows then + return win32.join(...) + else + return posix.join(...) + end +end + +local function normalize(path: pathlike): path + if onWindows then + return win32.normalize(path :: win32.pathlike) + else + return posix.normalize(path :: posix.pathlike) + end +end + +local function parse(path: pathlike): path + if onWindows then + return win32.parse(path :: win32.pathlike) + else + return posix.parse(path :: posix.pathlike) + end +end + +function resolve(...: pathlike): path + if onWindows then + return win32.resolve(...) + else + return posix.resolve(...) + end +end + +return table.freeze({ + win32 = win32, + posix = posix, + basename = basename, + dirname = dirname, + extname = extname, + format = format, + isabsolute = isabsolute, + join = join, + normalize = normalize, + parse = parse, + resolve = resolve, +}) diff --git a/lute/std/libs/path/posix.luau b/lute/std/libs/path/posix.luau new file mode 100644 index 000000000..f3f04f4c1 --- /dev/null +++ b/lute/std/libs/path/posix.luau @@ -0,0 +1,227 @@ +local process = require("@lute/process") + +export type path = { + parts: { string }, + absolute: boolean, +} + +export type pathlike = string | path + +local posix = {} + +function posix.basename(path: pathlike): string? + if typeof(path) == "string" then + path = posix.parse(path) + end + path = path :: path + return if #path.parts > 0 then path.parts[#path.parts] else nil +end + +function posix.dirname(path: pathlike): string + if typeof(path) == "string" then + path = posix.parse(path) + end + + path = path :: path + + if #path.parts == 0 then + return posix.format(path) + else + return posix.format({ + parts = { table.unpack(path.parts, 1, #path.parts - 1) }, + absolute = path.absolute, + }) + end +end + +function posix.extname(path: pathlike): string + if typeof(path) == "string" then + path = posix.parse(path) + end + + path = path :: path + + if #path.parts == 0 then + return "" + end + + local filename = path.parts[#path.parts] + + if filename == "." or filename == ".." then + return "" + end + + local dotIndex, _ = filename:find("%.[^%.]*$") + if dotIndex == nil or dotIndex == 1 then + return "" + end + + return filename:sub(dotIndex) +end + +function posix.format(path: pathlike): string + if typeof(path) == "string" then + return path + end + + path = path :: path + + if #path.parts == 0 then + return if path.absolute then "/" else "." + else + return (if path.absolute then "/" else "") .. table.concat(path.parts, "/") + end +end + +function posix.isabsolute(path: pathlike): boolean + if typeof(path) == "string" then + path = posix.parse(path) + end + path = path :: path + return path.absolute +end + +-- In place extends path with addend +local function joinHelper(path: path, addend: pathlike): () + if typeof(addend) == "string" then + addend = posix.parse(addend) + end + addend = addend :: path + + if addend.absolute then + local stringPath = posix.format(path) + local stringAddend = posix.format(addend) + error(table.concat({ + "Could not join ", + (if path.absolute then "absolute" else "relative"), + " path ", + stringPath, + " and absolute path ", + stringAddend, + })) + end + + table.move(addend.parts, 1, #addend.parts, #path.parts + 1, path.parts) +end + +function posix.join(...: pathlike): path + local parts = { ... } + if #parts == 0 then + return { + parts = {}, + absolute = false, + } + end + + local path = posix.parse(parts[1]) + + for i = 2, #parts do + joinHelper(path, parts[i]) + end + + return path +end + +function posix.normalize(path: pathlike): path + if typeof(path) == "string" then + path = posix.parse(path) + end + path = path :: path + + local newParts = {} + for _, part in path.parts do + if part == "" or part == "." then + continue + elseif part == ".." then + if path.absolute then + if #newParts > 0 then -- if absolute, only pop if we're not at the root + table.remove(newParts, #newParts) + end + else -- if a relative path, we keep .. prefixes + if #newParts > 0 and newParts[#newParts] ~= ".." then + table.remove(newParts, #newParts) + else + table.insert(newParts, "..") + end + end + else + table.insert(newParts, part) + end + end + + return { + parts = newParts, + absolute = path.absolute, + } +end + +function posix.parse(path: pathlike): path + if typeof(path) == "table" then + return path + end + if typeof(path) ~= "string" then + error("Expected string or path") + end + + local isAbs = false + + -- Check if path is absolute + isAbs = string.sub(path, 1, 1) == "/" + + local parts = {} + if isAbs then + -- Strip out the leading / for absolute paths + parts = path:sub(2):split("/") + else + parts = path:split("/") + end + + -- Filter out empty parts (can happen with leading/trailing separators) + if #parts > 0 then + if parts[#parts] == "" then + table.remove(parts, #parts) + end + if #parts > 0 and parts[1] == "" then + table.remove(parts, 1) + end + end + + return { + parts = parts, + absolute = isAbs, + } +end + +function posix.resolve(...: pathlike): path + local parts = { ... } + if #parts == 0 then + return posix.parse(process.cwd()) + end + + local path = posix.parse(parts[#parts]) + + for i = #parts - 1, 1, -1 do + if posix.isabsolute(path) then + break + end + + local segment = posix.parse(parts[i]) + + if #segment.parts == 0 then + continue + end + + joinHelper(segment, path) + path = segment + end + + if not posix.isabsolute(path) then + local cwd = posix.parse(process.cwd()) + joinHelper(cwd, path) + path = cwd + end + + return posix.normalize(path) +end + +return table.freeze(posix) diff --git a/lute/std/libs/path/win32.luau b/lute/std/libs/path/win32.luau new file mode 100644 index 000000000..deda728ce --- /dev/null +++ b/lute/std/libs/path/win32.luau @@ -0,0 +1,282 @@ +local process = require("@lute/process") + +export type pathkind = "unc" | "absolute" | "relative" + +export type path = { + parts: { string }, + kind: pathkind, + driveLetter: string?, +} + +export type pathlike = string | path + +local win32 = {} + +function win32.basename(path: pathlike): string? + if typeof(path) == "string" then + path = win32.parse(path) + end + path = path :: path + return if #path.parts > 0 then path.parts[#path.parts] else nil +end + +function win32.dirname(path: pathlike): string + if typeof(path) == "string" then + path = win32.parse(path) + end + + path = path :: path + + if #path.parts == 0 then + return win32.format(path) + else + return win32.format({ + parts = { table.unpack(path.parts, 1, #path.parts - 1) }, + kind = path.kind, + driveLetter = path.driveLetter, + }) + end +end + +function win32.extname(path: pathlike): string + if typeof(path) == "string" then + path = win32.parse(path) + end + + path = path :: path + + if #path.parts == 0 then + return "" + end + + local filename = path.parts[#path.parts] + + if filename == "." or filename == ".." then + return "" + end + + local dotIndex, _ = filename:find("%.[^%.]*$") + if dotIndex == nil or dotIndex == 1 then + return "" + end + + return filename:sub(dotIndex) +end + +function win32.format(path: pathlike): string + if typeof(path) == "string" then + return path + end + + path = path :: path + + if #path.parts == 0 then + if path.kind == "unc" then + return "\\\\" + elseif path.driveLetter ~= nil then + return table.concat({ path.driveLetter, ":", (if path.kind == "absolute" then "\\" else "") }) + else + return "." + end + else + local parts = table.concat(path.parts, "\\") + if path.kind == "unc" then + return "\\\\" .. parts + elseif path.driveLetter ~= nil then + return table.concat({ path.driveLetter, ":", (if path.kind == "absolute" then "\\" else ""), parts }) + else + return parts + end + end +end + +function win32.isabsolute(path: pathlike): boolean + if typeof(path) == "string" then + path = win32.parse(path) + end + path = path :: path + return path.kind == "absolute" or path.kind == "unc" +end + +-- In place extends path with addend +local function joinHelper(path: path, addend: pathlike): () + if typeof(addend) == "string" then + addend = win32.parse(addend) + end + addend = addend :: path + + if addend.kind == "absolute" or addend.kind == "unc" then + local stringPath = win32.format(path) + local stringAddend = win32.format(addend) + error(table.concat({ + "Could not join ", + path.kind, + " path ", + stringPath, + " and ", + addend.kind, + " path ", + stringAddend, + })) + end + + local driveLettersIncompatible = path.driveLetter ~= nil + and addend.driveLetter ~= nil + and addend.driveLetter:lower() ~= path.driveLetter:lower() + + if driveLettersIncompatible then + local stringPath = win32.format(path) + local stringAddend = win32.format(addend) + error( + table.concat({ "Could not join paths with incompatible drive letters: ", stringPath, " and ", stringAddend }) + ) + end + + table.move(addend.parts, 1, #addend.parts, #path.parts + 1, path.parts) + + if path.driveLetter == nil and addend.driveLetter ~= nil then + path.driveLetter = addend.driveLetter + end +end + +function win32.join(...: pathlike): path + local parts = { ... } + if #parts == 0 then + return { + parts = {}, + kind = "relative", + driveLetter = nil, + } + end + + local path = win32.parse(parts[1]) + + for i = 2, #parts do + joinHelper(path, parts[i]) + end + + return path +end + +function win32.normalize(path: pathlike): path + if typeof(path) == "string" then + path = win32.parse(path) + end + path = path :: path + + local newParts = {} + for _, part in path.parts do + if part == "" or part == "." then + continue + elseif part == ".." then + if path.kind == "unc" or path.kind == "absolute" then + if #newParts > 0 then -- if absolute, only pop if we're not at the root + table.remove(newParts, #newParts) + end + else -- if a relative path, we keep .. prefixes + if #newParts > 0 and newParts[#newParts] ~= ".." then + table.remove(newParts, #newParts) + else + table.insert(newParts, "..") + end + end + else + table.insert(newParts, part) + end + end + + return { + parts = newParts, + kind = path.kind, + driveLetter = path.driveLetter, + } +end + +function win32.parse(path: pathlike): path + if typeof(path) == "table" then + return path + end + if typeof(path) ~= "string" then + error("Expected string or path") + end + + local kind: pathkind = "relative" + local driveLetter = nil + + -- Check if path is absolute and if it's a unc path on Windows + if string.sub(path, 1, 2) == "\\\\" then + -- unc path + kind = "unc" + elseif string.match(path, "^%a:") ~= nil then + driveLetter = string.sub(path, 1, 1) + -- Windows: starts with drive letter + backslash (C:\) + kind = if string.sub(path, 3, 3) == "\\" then "absolute" else "relative" + end + + local parts = {} + -- Windows supports mixed separators, so sub forward slashes for backslashes first + path = path:gsub("/", "\\") + + if kind == "unc" then + -- Strip out the leading \\ for unc paths + parts = path:sub(3):split("\\") + elseif kind == "absolute" then + -- Strip out the drive letter, colon, and backslash (C:\) + parts = path:sub(4):split("\\") + elseif driveLetter ~= nil then + -- Strip out the drive letter and colon (C:) + parts = path:sub(3):split("\\") + else + parts = path:split("\\") + end + + -- Filter out empty parts (can happen with leading/trailing separators) + if #parts > 0 then + if parts[#parts] == "" then + table.remove(parts, #parts) + end + if #parts > 0 and parts[1] == "" then + table.remove(parts, 1) + end + end + + return { + parts = parts, + kind = kind, + driveLetter = driveLetter, + } +end + +function win32.resolve(...: pathlike): path + local parts = { ... } + if #parts == 0 then + return win32.parse(process.cwd()) + end + + local path = win32.parse(parts[#parts]) + + for i = #parts - 1, 1, -1 do + if win32.isabsolute(path) then + break + end + + local segment = win32.parse(parts[i]) + + if #segment.parts == 0 then + continue + end + + joinHelper(segment, path) + path = segment + end + + if not win32.isabsolute(path) then + local cwd = win32.parse(process.cwd()) + joinHelper(cwd, path) + path = cwd + end + + return win32.normalize(path) +end + +return table.freeze(win32) diff --git a/tests/path.posix.test.luau b/tests/path.posix.test.luau new file mode 100644 index 000000000..3ba105120 --- /dev/null +++ b/tests/path.posix.test.luau @@ -0,0 +1,534 @@ +local system = require("@std/system") +local test = require("@std/test") + +local path = require("@std/path") +local posix = require("@std/path/posix") + +test.suite("PathPosixParseSuite", function(suite) + suite:case("parse_posix_absolute_path", function(assert) + local result = path.posix.parse("/home/user/documents/file.txt") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("parse_posix_relative_path", function(assert) + local result = path.posix.parse("documents/file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("parse_empty_string", function(assert) + local result = path.posix.parse("") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("parse_root_path", function(assert) + local result = path.posix.parse("/") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 0) + end) + + suite:case("parse_single_file", function(assert) + local result = path.posix.parse("file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "file.txt") + end) + + suite:case("parse_pathobj_returns_same", function(assert) + local originalPath: posix.path = { + parts = { "folder", "file.txt" }, + absolute = false, + } + local result = path.posix.parse(originalPath) + assert.eq(result, originalPath) + end) +end) + +test.suite("PathPosixBasenameSuite", function(suite) + suite:case("basename_posix_absolute_path", function(assert) + local result = path.posix.basename("/home/user/documents/file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_posix_relative_path", function(assert) + local result = path.posix.basename("documents/file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_single_file", function(assert) + local result = path.posix.basename("file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_directory_no_extension", function(assert) + local result = path.posix.basename("/home/user/documents") + assert.eq(result, "documents") + end) + + suite:case("basename_empty_path", function(assert) + local result = path.posix.basename("") + assert.eq(result, nil) + end) + + suite:case("basename_root_path", function(assert) + local result = path.posix.basename("/") + assert.eq(result, nil) + end) + + suite:case("basename_pathobj", function(assert) + local pathobj: posix.path = { + parts = { "folder", "subfolder", "file.txt" }, + absolute = false, + } + local result = path.posix.basename(pathobj) + assert.eq(result, "file.txt") + end) +end) + +test.suite("PathPosixFormatSuite", function(suite) + suite:case("format_posix_absolute_path", function(assert) + local pathobj: posix.path = { + parts = { "home", "user", "documents", "file.txt" }, + absolute = true, + } + local result = path.posix.format(pathobj) + assert.eq(result, "/home/user/documents/file.txt") + end) + + suite:case("format_posix_relative_path", function(assert) + local pathobj: posix.path = { + parts = { "documents", "file.txt" }, + absolute = false, + } + local result = path.posix.format(pathobj) + assert.eq(result, "documents/file.txt") + end) + + suite:case("format_empty_relative_path", function(assert) + local pathobj: posix.path = { + parts = {}, + absolute = false, + } + local result = path.posix.format(pathobj) + assert.eq(result, ".") + end) + + suite:case("format_root_path", function(assert) + local pathobj: posix.path = { + parts = {}, + absolute = true, + } + local result = path.posix.format(pathobj) + assert.eq(result, "/") + end) + + suite:case("format_string_returns_same", function(assert) + local result = path.posix.format("/home/user/file.txt") + assert.eq(result, "/home/user/file.txt") + end) +end) + +test.suite("PathPosixDirnameSuite", function(suite) + suite:case("dirname_posix_absolute_path", function(assert) + local result = path.posix.dirname("/home/user/documents/file.txt") + assert.eq(result, "/home/user/documents") + end) + + suite:case("dirname_posix_relative_path", function(assert) + local result = path.posix.dirname("documents/file.txt") + assert.eq(result, "documents") + end) + + suite:case("dirname_single_file", function(assert) + local result = path.posix.dirname("file.txt") + assert.eq(result, ".") + end) + + suite:case("dirname_nested_directory", function(assert) + local result = path.posix.dirname("/home/user/documents/subfolder") + assert.eq(result, "/home/user/documents") + end) + + suite:case("dirname_empty_path", function(assert) + local result = path.posix.dirname("") + assert.eq(result, ".") + end) + + suite:case("dirname_root_path", function(assert) + local result = path.posix.dirname("/") + assert.eq(result, "/") + end) + + suite:case("dirname_pathobj", function(assert) + local pathobj: posix.path = { + parts = { "folder", "subfolder", "file.txt" }, + absolute = false, + } + local result = path.posix.dirname(pathobj) + assert.eq(result, "folder/subfolder") + end) +end) + +test.suite("PathPosixExtnameSuite", function(suite) + suite:case("extname_simple_extension", function(assert) + local result = path.posix.extname("/home/user/file.txt") + assert.eq(result, ".txt") + end) + + suite:case("extname_multiple_dots", function(assert) + local result = path.posix.extname("/home/user/archive.tar.gz") + assert.eq(result, ".gz") + end) + + suite:case("extname_no_extension", function(assert) + local result = path.posix.extname("/home/user/README") + assert.eq(result, "") + end) + + suite:case("extname_hidden_file_with_extension", function(assert) + local result = path.posix.extname("/home/user/.bashrc") + assert.eq(result, "") + end) + + suite:case("extname_hidden_file_with_real_extension", function(assert) + local result = path.posix.extname("/home/user/.config.json") + assert.eq(result, ".json") + end) + + suite:case("extname_directory_path", function(assert) + local result = path.posix.extname("/home/user/documents") + assert.eq(result, "") + end) + + suite:case("extname_empty_path", function(assert) + local result = path.posix.extname("") + assert.eq(result, "") + end) + + suite:case("extname_root_path", function(assert) + local result = path.posix.extname("/") + assert.eq(result, "") + end) + + suite:case("extname_pathobj", function(assert) + local pathobj: posix.path = { + parts = { "folder", "file.pdf" }, + absolute = false, + } + local result = path.posix.extname(pathobj) + assert.eq(result, ".pdf") + end) + + suite:case("extname_dot_only", function(assert) + local result = path.posix.extname("/home/user/file.") + assert.eq(result, ".") + end) + + suite:case("extname_filename_is_dot", function(assert) + local result = path.posix.extname("/home/user/.") + assert.eq(result, "") + end) + + suite:case("extname_filename_is_dotdot", function(assert) + local result = path.posix.extname("/home/user/..") + assert.eq(result, "") + end) +end) + +test.suite("PathPosixIsAbsoluteSuite", function(suite) + suite:case("absolute_posix_absolute_path", function(assert) + local result = path.posix.isabsolute("/home/user/documents/file.txt") + assert.eq(result, true) + end) + + suite:case("absolute_posix_relative_path", function(assert) + local result = path.posix.isabsolute("documents/file.txt") + assert.eq(result, false) + end) + + suite:case("absolute_root_path", function(assert) + local result = path.posix.isabsolute("/") + assert.eq(result, true) + end) + + suite:case("absolute_empty_path", function(assert) + local result = path.posix.isabsolute("") + assert.eq(result, false) + end) + + suite:case("absolute_pathobj_absolute", function(assert) + local pathobj: posix.path = { + parts = { "home", "user", "file.txt" }, + absolute = true, + } + local result = path.posix.isabsolute(pathobj) + assert.eq(result, true) + end) + + suite:case("absolute_pathobj_relative", function(assert) + local pathobj: posix.path = { + parts = { "folder", "file.txt" }, + absolute = false, + } + local result = path.posix.isabsolute(pathobj) + assert.eq(result, false) + end) +end) + +test.suite("PathPosixNormalizeSuite", function(suite) + suite:case("normalize_removes_current_directory", function(assert) + local result = path.posix.normalize("/home/./user/documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + end) + + suite:case("normalize_resolves_parent_directory", function(assert) + local result = path.posix.normalize("/home/user/../documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "documents") + end) + + suite:case("normalize_multiple_dots", function(assert) + local result = path.posix.normalize("/home/user/./documents/../files/./") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "files") + end) + + suite:case("normalize_relative_path", function(assert) + local result = path.posix.normalize("documents/./subfolder/../file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("normalize_removes_empty_segments", function(assert) + local result = path.posix.normalize("/home//user///documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + end) + + suite:case("normalize_parent_beyond_root", function(assert) + local result = path.posix.normalize("/home/../..") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 0) + end) + + suite:case("normalize_empty_path", function(assert) + local result = path.posix.normalize("") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("normalize_only_dots", function(assert) + local result = path.posix.normalize("./././.") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("normalize_pathobj", function(assert) + local pathobj: posix.path = { + parts = { "home", ".", "user", "..", "documents" }, + absolute = true, + } + local result = path.posix.normalize(pathobj) + assert.eq(result.absolute, true) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "documents") + end) + + suite:case("normalize_parent_directory_only", function(assert) + local result = path.posix.normalize("..") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "..") + end) + + suite:case("normalize_multiple_parent_directories", function(assert) + local result = path.posix.normalize("../../..") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "..") + assert.eq(result.parts[3], "..") + end) + + suite:case("normalize_parent_then_folder", function(assert) + local result = path.posix.normalize("../folder/file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "folder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("normalize_multiple_parents_then_folder", function(assert) + local result = path.posix.normalize("../../folder/subfolder") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "..") + assert.eq(result.parts[3], "folder") + assert.eq(result.parts[4], "subfolder") + end) + + suite:case("normalize_folder_then_multiple_parents", function(assert) + local result = path.posix.normalize("folder/subfolder/../../other") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "other") + end) +end) + +test.suite("PathPosixJoinSuite", function(suite) + suite:case("join_absolute_and_relative", function(assert) + local result = path.posix.join("/home/user", "documents", "file.txt") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("join_relative_paths", function(assert) + local result = path.posix.join("documents", "subfolder", "file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "documents") + assert.eq(result.parts[2], "subfolder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("join_empty_parts", function(assert) + local result = path.posix.join("folder", "", "file.txt") + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "folder") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("join_no_arguments", function(assert) + local result = path.posix.join() + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("join_single_argument", function(assert) + local result = path.posix.join("/home/user") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + end) + + suite:case("join_pathobj_and_string", function(assert) + local pathobj: posix.path = { + parts = { "home", "user" }, + absolute = true, + } + local result = path.posix.join(pathobj, "documents/file.txt") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("join_error_on_absolute_addend", function(assert) + local success, _ = pcall(function() + return path.posix.join("/home/user", "/absolute/path") + end) + assert.eq(success, false) + end) +end) + +test.suite("PathPosixResolveSuite", function(suite) + suite:case("resolve_absolute_path", function(assert) + local result = path.posix.resolve("/home/user/documents/file.txt") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("resolve_relative_path_from_cwd", function(assert) + -- This test assumes we're in some working directory + local result = path.posix.resolve("documents/file.txt") + -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / + assert.eq(result.absolute, system.os ~= "windows") + -- The exact parts will depend on the current working directory + -- But we can check that it ends with our relative path + local endIndex = #result.parts + assert.eq(result.parts[endIndex - 1], "documents") + assert.eq(result.parts[endIndex], "file.txt") + end) + + suite:case("resolve_multiple_paths", function(assert) + local result = path.posix.resolve("/home", "user", "../admin", "documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "admin") + assert.eq(result.parts[3], "documents") + end) + + suite:case("resolve_with_absolute_in_middle", function(assert) + local result = path.posix.resolve("relative", "/absolute/path", "more") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "absolute") + assert.eq(result.parts[2], "path") + assert.eq(result.parts[3], "more") + end) + + suite:case("resolve_no_arguments", function(assert) + -- Should return normalized current working directory + local result = path.posix.resolve() + -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / + assert.eq(result.absolute, system.os ~= "windows") + -- Can't test exact parts since cwd varies + end) + + suite:case("resolve_empty_strings", function(assert) + local result = path.posix.resolve("", "", "/home/user") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + end) + + suite:case("resolve_with_dot_segments", function(assert) + local result = path.posix.resolve("/home/user", "../admin/./documents") + assert.eq(result.absolute, true) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "admin") + assert.eq(result.parts[3], "documents") + end) +end) + +test.run() diff --git a/tests/path.win32.test.luau b/tests/path.win32.test.luau new file mode 100644 index 000000000..af4e02f21 --- /dev/null +++ b/tests/path.win32.test.luau @@ -0,0 +1,695 @@ +local system = require("@std/system") +local test = require("@std/test") + +local path = require("@std/path") +local win32 = require("@std/path/win32") + +test.suite("PathWin32ParseSuite", function(suite) + suite:case("parse_windows_absolute_path", function(assert) + local result: win32.path = path.win32.parse("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("parse_windows_unc_path", function(assert) + local result: win32.path = path.win32.parse("\\\\server\\share\\folder\\file.txt") + assert.eq(result.kind, "unc") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "server") + assert.eq(result.parts[2], "share") + assert.eq(result.parts[3], "folder") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("parse_empty_string", function(assert) + local result: win32.path = path.win32.parse("") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 0) + end) + + suite:case("parse_single_file", function(assert) + local result: win32.path = path.win32.parse("file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "file.txt") + end) + + suite:case("parse_pathobj_returns_same", function(assert) + local originalPath: win32.path = { + parts = { "folder", "file.txt" }, + kind = "relative", + driveLetter = nil, + } + local result = path.win32.parse(originalPath) + assert.eq(result, originalPath) + end) + + suite:case("parse_mixed_separators", function(assert) + local result: win32.path = path.win32.parse("/home/user\\documents/file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "home") + assert.eq(result.parts[2], "user") + assert.eq(result.parts[3], "documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("parse_windows_relative_with_drive", function(assert) + local result: win32.path = path.win32.parse("C:Documents\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "file.txt") + end) +end) + +test.suite("PathWin32BasenameSuite", function(suite) + suite:case("basename_windows_absolute_path", function(assert) + local result = path.win32.basename("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_windows_unc_path", function(assert) + local result = path.win32.basename("\\\\server\\share\\folder\\file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_single_file", function(assert) + local result = path.win32.basename("file.txt") + assert.eq(result, "file.txt") + end) + + suite:case("basename_directory_no_extension", function(assert) + local result = path.win32.basename("C:\\Users\\username\\Documents") + assert.eq(result, "Documents") + end) + + suite:case("basename_empty_path", function(assert) + local result = path.win32.basename("") + assert.eq(result, nil) + end) + + suite:case("basename_root_path", function(assert) + local result = path.win32.basename("C:\\") + assert.eq(result, nil) + end) + + suite:case("basename_pathobj", function(assert) + local pathobj: win32.path = { + parts = { "folder", "subfolder", "file.txt" }, + kind = "relative", + driveLetter = nil, + } + local result = path.win32.basename(pathobj) + assert.eq(result, "file.txt") + end) + + suite:case("basename_mixed_separators", function(assert) + local result = path.win32.basename("/home/user\\documents/file.txt") + assert.eq(result, "file.txt") + end) +end) + +test.suite("PathWin32FormatSuite", function(suite) + suite:case("format_windows_absolute_path", function(assert) + local pathobj: win32.path = { + parts = { "Users", "username", "Documents", "file.txt" }, + kind = "absolute", + driveLetter = "C", + } + local result = path.win32.format(pathobj) + assert.eq(result, "C:\\Users\\username\\Documents\\file.txt") + end) + + suite:case("format_windows_relative_with_drive", function(assert) + local pathobj: win32.path = { + parts = { "Documents", "file.txt" }, + kind = "relative", + driveLetter = "C", + } + local result = path.win32.format(pathobj) + assert.eq(result, "C:Documents\\file.txt") + end) + + suite:case("format_windows_unc_path", function(assert) + local pathobj: win32.path = { + parts = { "server", "share", "folder", "file.txt" }, + kind = "unc", + driveLetter = nil, + } + local result = path.win32.format(pathobj) + assert.eq(result, "\\\\server\\share\\folder\\file.txt") + end) + + suite:case("format_empty_relative_path", function(assert) + local pathobj: win32.path = { + parts = {}, + kind = "relative", + driveLetter = nil, + } + local result = path.win32.format(pathobj) + assert.eq(result, ".") + end) + + suite:case("format_empty_relative_path_with_drive", function(assert) + local pathobj: win32.path = { + parts = {}, + kind = "relative", + driveLetter = "C", + } + local result = path.win32.format(pathobj) + assert.eq(result, "C:") + end) + + suite:case("format_root_path_with_drive", function(assert) + local pathobj: win32.path = { + parts = {}, + kind = "absolute", + driveLetter = "C", + } + local result = path.win32.format(pathobj) + assert.eq(result, "C:\\") + end) + + suite:case("format_string_returns_same", function(assert) + local result = path.win32.format("/home/user/file.txt") + assert.eq(result, "/home/user/file.txt") + end) +end) + +test.suite("PathWin32DirnameSuite", function(suite) + suite:case("dirname_windows_absolute_path", function(assert) + local result = path.win32.dirname("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result, "C:\\Users\\username\\Documents") + end) + + suite:case("dirname_windows_unc_path", function(assert) + local result = path.win32.dirname("\\\\server\\share\\folder\\file.txt") + assert.eq(result, "\\\\server\\share\\folder") + end) + + suite:case("dirname_single_file", function(assert) + local result = path.win32.dirname("file.txt") + assert.eq(result, ".") + end) + + suite:case("dirname_empty_path", function(assert) + local result = path.win32.dirname("") + assert.eq(result, ".") + end) + + suite:case("dirname_windows_root", function(assert) + local result = path.win32.dirname("C:\\") + assert.eq(result, "C:\\") + end) + + suite:case("dirname_pathobj", function(assert) + local pathobj: win32.path = { + parts = { "folder", "subfolder", "file.txt" }, + kind = "relative", + driveLetter = nil, + } + local result = path.win32.dirname(pathobj) + assert.eq(result, "folder\\subfolder") + end) + + suite:case("dirname_windows_relative_with_drive", function(assert) + local result = path.win32.dirname("C:Documents\\file.txt") + assert.eq(result, "C:Documents") + end) +end) + +test.suite("PathWin32ExtnameSuite", function(suite) + suite:case("extname_simple_extension", function(assert) + local result = path.win32.extname("C:\\Users\\username\\file.txt") + assert.eq(result, ".txt") + end) + + suite:case("extname_multiple_dots", function(assert) + local result = path.win32.extname("C:\\Users\\username\\archive.tar.gz") + assert.eq(result, ".gz") + end) + + suite:case("extname_no_extension", function(assert) + local result = path.win32.extname("C:\\Users\\username\\README") + assert.eq(result, "") + end) + + suite:case("extname_hidden_file_with_extension", function(assert) + local result = path.win32.extname("C:\\Users\\username\\.bashrc") + assert.eq(result, "") + end) + + suite:case("extname_hidden_file_with_real_extension", function(assert) + local result = path.win32.extname("C:\\Users\\username\\.config.json") + assert.eq(result, ".json") + end) + + suite:case("extname_windows_path", function(assert) + local result = path.win32.extname("C:\\Users\\username\\document.docx") + assert.eq(result, ".docx") + end) + + suite:case("extname_directory_path", function(assert) + local result = path.win32.extname("C:\\Users\\username\\documents") + assert.eq(result, "") + end) + + suite:case("extname_empty_path", function(assert) + local result = path.win32.extname("") + assert.eq(result, "") + end) + + suite:case("extname_root_path", function(assert) + local result = path.win32.extname("C:\\") + assert.eq(result, "") + end) + + suite:case("extname_pathobj", function(assert) + local pathobj: win32.path = { + parts = { "folder", "file.pdf" }, + kind = "relative", + driveLetter = nil, + } + local result = path.win32.extname(pathobj) + assert.eq(result, ".pdf") + end) + + suite:case("extname_dot_only", function(assert) + local result = path.win32.extname("C:\\Users\\username\\file.") + assert.eq(result, ".") + end) + + suite:case("extname_filename_is_dot", function(assert) + local result = path.win32.extname("C:\\Users\\username\\.") + assert.eq(result, "") + end) + + suite:case("extname_filename_is_dotdot", function(assert) + local result = path.win32.extname("C:\\Users\\username\\..") + assert.eq(result, "") + end) +end) + +test.suite("PathWin32IsAbsoluteSuite", function(suite) + suite:case("isAbsolute_windows_absolute_path", function(assert) + local result = path.win32.isabsolute("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result, true) + end) + + suite:case("isAbsolute_windows_relative_path", function(assert) + local result = path.win32.isabsolute("Documents\\file.txt") + assert.eq(result, false) + end) + + suite:case("isAbsolute_windows_relative_with_drive", function(assert) + local result = path.win32.isabsolute("C:Documents\\file.txt") + assert.eq(result, false) + end) + + suite:case("isAbsolute_windows_unc_path", function(assert) + local result = path.win32.isabsolute("\\\\server\\share\\folder\\file.txt") + assert.eq(result, true) + end) + + suite:case("isAbsolute_root_path", function(assert) + local result = path.win32.isabsolute("C:\\") + assert.eq(result, true) + end) + + suite:case("isAbsolute_empty_path", function(assert) + local result = path.win32.isabsolute("") + assert.eq(result, false) + end) + + suite:case("isAbsolute_pathobj_absolute", function(assert) + local pathobj: win32.path = { + parts = { "home", "user", "file.txt" }, + kind = "absolute", + driveLetter = "C", + } + local result = path.win32.isabsolute(pathobj) + assert.eq(result, true) + end) + + suite:case("isAbsolute_pathobj_relative", function(assert) + local pathobj: win32.path = { + parts = { "folder", "file.txt" }, + kind = "relative", + driveLetter = nil, + } + local result = path.win32.isabsolute(pathobj) + assert.eq(result, false) + end) +end) + +test.suite("PathWin32NormalizeSuite", function(suite) + suite:case("normalize_removes_current_directory", function(assert) + local result = path.win32.normalize("C:\\Users\\.\\username\\Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + end) + + suite:case("normalize_resolves_parent_directory", function(assert) + local result = path.win32.normalize("C:\\Users\\username\\..\\Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "Documents") + end) + + suite:case("normalize_multiple_dots", function(assert) + local result = path.win32.normalize("C:\\Users\\username\\.\\Documents\\..\\files\\.\\") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "files") + end) + + suite:case("normalize_relative_path", function(assert) + local result = path.win32.normalize("Documents\\.\\subfolder\\..\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("normalize_removes_empty_segments", function(assert) + local result = path.win32.normalize("C:\\Users\\\\username\\\\\\Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + end) + + suite:case("normalize_parent_beyond_root", function(assert) + local result = path.win32.normalize("C:\\Users\\..\\..\\") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 0) + end) + + suite:case("normalize_unc_path", function(assert) + local result = path.win32.normalize("\\\\server\\share\\.\\folder\\..\\files") + assert.eq(result.kind, "unc") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "server") + assert.eq(result.parts[2], "share") + assert.eq(result.parts[3], "files") + end) + + suite:case("normalize_relative_with_drive", function(assert) + local result = path.win32.normalize("C:Documents\\.\\subfolder\\..\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("normalize_parent_directory_only", function(assert) + local result = path.win32.normalize("..") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "..") + end) + + suite:case("normalize_multiple_parent_directories", function(assert) + local result = path.win32.normalize("..\\..\\..") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "..") + assert.eq(result.parts[3], "..") + end) + + suite:case("normalize_parent_then_folder", function(assert) + local result = path.win32.normalize("..\\folder\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "folder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("normalize_folder_then_multiple_parents", function(assert) + local result = path.win32.normalize("folder\\subfolder\\..\\..\\other") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 1) + assert.eq(result.parts[1], "other") + end) + + suite:case("normalize_pathobj", function(assert) + local pathobj: win32.path = { + parts = { "Users", ".", "username", "..", "Documents" }, + kind = "absolute", + driveLetter = "C", + } + local result = path.win32.normalize(pathobj) + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "Documents") + end) + + suite:case("normalize_relative_with_drive_and_parent", function(assert) + local result = path.win32.normalize("C:..\\folder\\file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "folder") + assert.eq(result.parts[3], "file.txt") + end) +end) + +test.suite("PathWin32JoinSuite", function(suite) + suite:case("join_absolute_and_relative", function(assert) + local result = path.win32.join("C:\\Users\\username", "Documents", "file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("join_relative_paths", function(assert) + local result = path.win32.join("Documents", "subfolder", "file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "subfolder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("join_drive_relative_paths", function(assert) + local result = path.win32.join("C:Documents", "subfolder", "file.txt") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "subfolder") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("join_unc_and_relative", function(assert) + local result = path.win32.join("\\\\server\\share", "folder", "file.txt") + assert.eq(result.kind, "unc") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "server") + assert.eq(result.parts[2], "share") + assert.eq(result.parts[3], "folder") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("join_no_arguments", function(assert) + local result = path.win32.join() + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 0) + end) + + suite:case("join_pathobj_and_string", function(assert) + local pathobj: win32.path = { + parts = { "Users", "username" }, + kind = "absolute", + driveLetter = "C", + } + local result = path.win32.join(pathobj, "Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("join_error_on_absolute_addend", function(assert) + local success, _ = pcall(function() + return path.win32.join("C:\\Users", "D:\\absolute\\path") + end) + assert.eq(success, false) + end) + + suite:case("join_error_on_unc_addend", function(assert) + local success, _ = pcall(function() + return path.win32.join("C:\\Users", "\\\\server\\share") + end) + assert.eq(success, false) + end) + + suite:case("join_error_on_incompatible_drives", function(assert) + local success, _ = pcall(function() + return path.win32.join("C:Documents", "D:other") + end) + assert.eq(success, false) + end) + + suite:case("join_compatible_drives", function(assert) + local result = path.win32.join("C:Documents", "C:subfolder") + assert.eq(result.kind, "relative") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "subfolder") + end) +end) + +test.suite("PathWin32ResolveSuite", function(suite) + suite:case("resolve_absolute_path", function(assert) + local result = path.win32.resolve("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + assert.eq(result.parts[4], "file.txt") + end) + + suite:case("resolve_relative_path_from_cwd", function(assert) + -- This test assumes we're in some working directory + local result = path.win32.resolve("Documents\\file.txt") + -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters + if system.os == "windows" then + assert.eq(result.kind, "absolute") + else + assert.eq(result.kind, "relative") + end + -- The exact parts will depend on the current working directory + -- But we can check that it ends with our relative path + -- In an ideal world, we'd mock `process.cwd` + local endIndex = #result.parts + assert.eq(result.parts[endIndex - 1], "Documents") + assert.eq(result.parts[endIndex], "file.txt") + end) + + suite:case("resolve_multiple_paths", function(assert) + local result = path.win32.resolve("C:\\Users", "username", "..\\admin", "Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "admin") + assert.eq(result.parts[3], "Documents") + end) + + suite:case("resolve_with_absolute_in_middle", function(assert) + local result = path.win32.resolve("relative", "D:\\absolute\\path", "more") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "D") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "absolute") + assert.eq(result.parts[2], "path") + assert.eq(result.parts[3], "more") + end) + + suite:case("resolve_unc_path", function(assert) + local result = path.win32.resolve("\\\\server\\share\\folder") + assert.eq(result.kind, "unc") + assert.eq(result.driveLetter, nil) + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "server") + assert.eq(result.parts[2], "share") + assert.eq(result.parts[3], "folder") + end) + + suite:case("resolve_no_arguments", function(assert) + -- Should return normalized current working directory + local result = path.win32.resolve() + -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters + if system.os == "windows" then + assert.eq(result.kind, "absolute") + else + assert.eq(result.kind, "relative") + end + -- Can't test exact parts since cwd varies + end) + + suite:case("resolve_drive_relative_path", function(assert) + local result = path.win32.resolve("C:Documents\\file.txt") + -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters + if system.os == "windows" then + assert.eq(result.kind, "absolute") + else + assert.eq(result.kind, "relative") + end + assert.eq(result.driveLetter, "C") + -- The exact parts depend on cwd, but should be absolute + local endIndex = #result.parts + assert.eq(result.parts[endIndex - 1], "Documents") + assert.eq(result.parts[endIndex], "file.txt") + end) + + suite:case("resolve_with_dot_segments", function(assert) + local result = path.win32.resolve("C:\\Users\\username", "..\\admin\\.\\Documents") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "admin") + assert.eq(result.parts[3], "Documents") + end) + + suite:case("resolve_empty_strings", function(assert) + local result = path.win32.resolve("", "", "C:\\Users\\username") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + end) +end) + +test.run() From cb0af2952d0885875617b778e8f0b23c31b94c1c Mon Sep 17 00:00:00 2001 From: checkraisefold Date: Sat, 18 Oct 2025 19:46:09 -0700 Subject: [PATCH 050/642] Fix new path lib incorrectly checking OS (#445) --- lute/std/libs/path/init.luau | 4 ++-- lute/std/libs/test.luau | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau index 0fbff765c..df0fd6ca9 100644 --- a/lute/std/libs/path/init.luau +++ b/lute/std/libs/path/init.luau @@ -1,4 +1,4 @@ -local system = require("@lute/system") +local system = require("@std/system") local posix = require("@self/posix") local win32 = require("@self/win32") @@ -7,7 +7,7 @@ export type path = posix.path | win32.path export type pathlike = string | path -local onWindows = system.os == "windows" +local onWindows = system.win32 local function basename(path: pathlike): string? if onWindows then diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index d643a51b0..0b6c184fe 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -1,6 +1,6 @@ --!strict local ps = require("@lute/process") -local assert = require("./assert") +local assert = require("@std/assert") -- Test Types type Test = (assert.Asserts) -> () type TestCase = { name: string, case: Test } From 49f4be5263aeda72091d4056206066e0566b1423 Mon Sep 17 00:00:00 2001 From: checkraisefold Date: Sat, 18 Oct 2025 21:16:08 -0700 Subject: [PATCH 051/642] Fix 4 extra spaces on first level on json pretty serialize (#446) --- lute/std/libs/json.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 377c60ddc..89da1dabe 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -47,7 +47,7 @@ local function writeSpaces(state: SerializerState) if state.prettyPrint then checkState(state, state.depth * 4) - for i = 0, state.depth do + for i = 1, state.depth do buffer.writeu32(state.buf, state.cursor, 0x_20_20_20_20) state.cursor += 4 end From 5d95623a2606d1bc982e8147d243c6ec388141e0 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Sat, 18 Oct 2025 21:21:05 -0700 Subject: [PATCH 052/642] Break bidirectional dependencies between Lute.Runtime and every runtime library (#442) --- CMakeLists.txt | 16 ++++++++-------- lute/cli/CMakeLists.txt | 2 +- lute/cli/src/climain.cpp | 34 ++++++++++++++++++++++++++++++++++ lute/runtime/CMakeLists.txt | 2 +- lute/runtime/src/runtime.cpp | 35 ----------------------------------- 5 files changed, 44 insertions(+), 45 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c334ca92e..c629ae082 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,14 @@ project( LANGUAGES CXX C ) +option(LUTE_ENABLE_ASAN "Enable AddressSanitizer" OFF) +if (LUTE_ENABLE_ASAN) + if(NOT MSVC) + add_compile_options(-fsanitize=address -fno-omit-frame-pointer) + add_link_options(-fsanitize=address) + endif() +endif() + # luau setup set(LUAU_BUILD_CLI OFF) set(LUAU_BUILD_TESTS OFF) @@ -114,14 +122,6 @@ else() list(APPEND LUTE_OPTIONS -Werror) # Warnings are errors endif() -option(LUTE_ENABLE_ASAN "Enable AddressSanitizer" OFF) -if (LUTE_ENABLE_ASAN) - if(NOT MSVC) - add_compile_options(-fsanitize=address -fno-omit-frame-pointer) - add_link_options(-fsanitize=address) - endif() -endif() - # libraries add_subdirectory(lute/require) add_subdirectory(lute/runtime) diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 8ab56ea90..257098dd0 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -34,7 +34,7 @@ target_sources(Lute.CLI.lib PRIVATE target_compile_features(Lute.CLI.lib PUBLIC cxx_std_17) target_include_directories(Lute.CLI.lib PUBLIC include ${CLI_GENERATED_INCLUDE_DIR}) -target_link_libraries(Lute.CLI.lib PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Require Lute.Runtime Luau.CLI.lib) +target_link_libraries(Lute.CLI.lib PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Require Lute.Runtime Luau.CLI.lib) target_compile_options(Lute.CLI.lib PRIVATE ${LUTE_OPTIONS}) add_executable(Lute.CLI) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index eb0235dba..d27361550 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -12,12 +12,21 @@ #include "lute/clicommands.h" #include "lute/clivfs.h" #include "lute/compile.h" +#include "lute/crypto.h" +#include "lute/fs.h" +#include "lute/luau.h" +#include "lute/net.h" #include "lute/options.h" +#include "lute/process.h" #include "lute/ref.h" #include "lute/require.h" #include "lute/runtime.h" +#include "lute/system.h" +#include "lute/task.h" #include "lute/tc.h" +#include "lute/time.h" #include "lute/version.h" +#include "lute/vm.h" #ifdef _WIN32 #include @@ -52,12 +61,37 @@ void* createCliRequireContext(lua_State* L) return ctx; } +static void luteopen_libs(lua_State* L) +{ + std::vector> libs = {{ + {"@lute/crypto", luteopen_crypto}, + {"@lute/fs", luteopen_fs}, + {"@lute/luau", luteopen_luau}, + {"@lute/net", luteopen_net}, + {"@lute/process", luteopen_process}, + {"@lute/task", luteopen_task}, + {"@lute/vm", luteopen_vm}, + {"@lute/system", luteopen_system}, + {"@lute/time", luteopen_time}, + }}; + + for (const auto& [name, func] : libs) + { + lua_pushcfunction(L, luarequire_registermodule, nullptr); + lua_pushstring(L, name); + func(L); + lua_call(L, 2, 0); + } +} + lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit) { return setupState( runtime, [preSandboxInit = std::move(preSandboxInit)](lua_State* L) { + luteopen_libs(L); + if (Luau::CodeGen::isSupported()) Luau::CodeGen::create(L); diff --git a/lute/runtime/CMakeLists.txt b/lute/runtime/CMakeLists.txt index 632d89ffd..0b4cfb7f7 100644 --- a/lute/runtime/CMakeLists.txt +++ b/lute/runtime/CMakeLists.txt @@ -11,5 +11,5 @@ target_sources(Lute.Runtime PRIVATE target_compile_features(Lute.Runtime PUBLIC cxx_std_17) target_include_directories(Lute.Runtime PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Runtime PRIVATE Lute.Crypto Lute.Fs Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Luau.Common Luau.Require Luau.VM uv_a) +target_link_libraries(Lute.Runtime PRIVATE Luau.Common Luau.Require Luau.VM uv_a) target_compile_options(Lute.Runtime PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 475a6c563..737e44b4f 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -1,15 +1,5 @@ #include "lute/runtime.h" -#include "lute/crypto.h" -#include "lute/fs.h" -#include "lute/luau.h" -#include "lute/net.h" -#include "lute/process.h" -#include "lute/system.h" -#include "lute/task.h" -#include "lute/vm.h" -#include "lute/time.h" - #include "Luau/Require.h" #include "lua.h" @@ -311,29 +301,6 @@ ResumeToken getResumeToken(lua_State* L) return token; } -static void luteopen_libs(lua_State* L) -{ - std::vector> libs = {{ - {"@lute/crypto", luteopen_crypto}, - {"@lute/fs", luteopen_fs}, - {"@lute/luau", luteopen_luau}, - {"@lute/net", luteopen_net}, - {"@lute/process", luteopen_process}, - {"@lute/task", luteopen_task}, - {"@lute/vm", luteopen_vm}, - {"@lute/system", luteopen_system}, - {"@lute/time", luteopen_time}, - }}; - - for (const auto& [name, func] : libs) - { - lua_pushcfunction(L, luarequire_registermodule, nullptr); - lua_pushstring(L, name); - func(L); - lua_call(L, 2, 0); - } -} - lua_State* setupState(Runtime& runtime, std::function doBeforeSandbox) { // Separate VM for data copies @@ -350,8 +317,6 @@ lua_State* setupState(Runtime& runtime, std::function doBefore // register the builtin tables luaL_openlibs(L); - luteopen_libs(L); - lua_pushnil(L); lua_setglobal(L, "setfenv"); From eeaa35ac20cc1a61b55df864d1809726fcc34130 Mon Sep 17 00:00:00 2001 From: checkraisefold Date: Sat, 18 Oct 2025 21:29:14 -0700 Subject: [PATCH 053/642] Fix AstStatForIn type definition (#448) --- definitions/luau.luau | 2 +- foreman.toml | 2 +- rokit.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index a672ac3de..efee44c1f 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -306,7 +306,7 @@ export type AstStatFor = { export type AstStatForIn = { tag: "forin", forkeyword: Token<"for">, - variables: Punctuated>, + variables: Punctuated, inkeyword: Token<"in">, values: Punctuated, dokeyword: Token<"do">, diff --git a/foreman.toml b/foreman.toml index def302598..77b192ea6 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] -stylua = { github = "JohnnyMorganz/StyLua", version = "2.0.2" } +stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } lute = { github = "luau-lang/lute", version = "0.1.0-nightly.20251003" } diff --git a/rokit.toml b/rokit.toml index 5855d9112..cd6590579 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] -stylua = "JohnnyMorganz/StyLua@2.0.2" +stylua = "JohnnyMorganz/StyLua@2.3.0" lute = "luau-lang/lute@0.1.0-nightly.20251003" From 29d70016f7835186e2cf6a0a6100fa801debd51f Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 20 Oct 2025 09:36:07 -0700 Subject: [PATCH 054/642] refactor: Modulepath takes std::function instead of a function pointer (#449) This is a pre-req for a future PR for Multi-File bytecode compilation. Doing this lets me use the module path interface more flexibly. --- lute/require/include/lute/modulepath.h | 13 +++++++------ lute/require/src/modulepath.cpp | 12 ++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lute/require/include/lute/modulepath.h b/lute/require/include/lute/modulepath.h index 92660d63b..232ca8329 100644 --- a/lute/require/include/lute/modulepath.h +++ b/lute/require/include/lute/modulepath.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -36,8 +37,8 @@ class ModulePath static std::optional create( std::string rootDirectory, std::string filePath, - bool (*isAFile)(const std::string&), - bool (*isADirectory)(const std::string&), + std::function isAFile, + std::function isADirectory, std::optional relativePathToTrack = std::nullopt ); @@ -51,13 +52,13 @@ class ModulePath ModulePath( std::string realPathPrefix, std::string modulePath, - bool (*isAFile)(const std::string&), - bool (*isADirectory)(const std::string&), + std::function isAFile, + std::function isADirectory, std::optional relativePathToTrack = std::nullopt ); - bool (*isAFile)(const std::string&); - bool (*isADirectory)(const std::string&); + std::function isAFile; + std::function isADirectory; std::string realPathPrefix; std::string modulePath; diff --git a/lute/require/src/modulepath.cpp b/lute/require/src/modulepath.cpp index 4301cccb1..0e7e94dc7 100644 --- a/lute/require/src/modulepath.cpp +++ b/lute/require/src/modulepath.cpp @@ -39,8 +39,8 @@ static std::string_view removeExtension(std::string_view path) std::optional ModulePath::create( std::string rootDirectory, std::string filePath, - bool (*isAFile)(const std::string&), - bool (*isADirectory)(const std::string&), + std::function isAFile, + std::function isADirectory, std::optional relativePathToTrack ) { @@ -73,12 +73,12 @@ std::optional ModulePath::create( ModulePath::ModulePath( std::string realPathPrefix, std::string modulePath, - bool (*isAFile)(const std::string&), - bool (*isADirectory)(const std::string&), + std::function isAFile, + std::function isADirectory, std::optional relativePathToTrack ) - : isAFile(isAFile) - , isADirectory(isADirectory) + : isAFile(std::move(isAFile)) + , isADirectory(std::move(isADirectory)) , realPathPrefix(std::move(realPathPrefix)) , modulePath(std::move(modulePath)) , relativePathToTrack(std::move(relativePathToTrack)) From 59b1beb10f08562b757b296d2a5fd36f106d229b Mon Sep 17 00:00:00 2001 From: nerix Date: Mon, 20 Oct 2025 18:55:19 +0200 Subject: [PATCH 055/642] feat: add `process.execpath` to get the current lute executable (#431) I was trying to get the code generation to run through CMake (which would run lute in the build). When integrating this into luthier, I needed a way to identify the current lute executable to pass to CMake. This adds `execpath` to `@lute/process`, which mirrors Node.js' [process.execPath](https://nodejs.org/docs/latest/api/process.html#processexecpath) to get the lute executable path - this is immune to `exec -a foo lute` having `foo` as `argv[0]`. [`uv_setup_args`](https://docs.libuv.org/en/v1.x/misc.html#c.uv_setup_args) can allocate on some platforms. That memory should be free'd with [`uv_library_shutdown`](https://docs.libuv.org/en/v1.x/misc.html#c.uv_library_shutdown). I'm not sure where it's appropriate to call this. Passing around the `argv0` also seems a bit wrong. Maybe this path shouldn't be stored in `Runtime`? --- definitions/process.luau | 4 ++++ lute/cli/src/climain.cpp | 17 +++++++++-------- lute/process/include/lute/process.h | 3 +++ lute/process/src/process.cpp | 6 ++++++ lute/runtime/include/lute/runtime.h | 4 +++- lute/runtime/src/runtime.cpp | 28 +++++++++++++++++++++++++++- lute/vm/src/spawn.cpp | 2 +- 7 files changed, 53 insertions(+), 11 deletions(-) diff --git a/definitions/process.luau b/definitions/process.luau index 46c48ef5f..a2f1069ee 100644 --- a/definitions/process.luau +++ b/definitions/process.luau @@ -37,4 +37,8 @@ function process.exit(exitcode: number): never error("not implemented") end +function process.execpath(): string + error("not implemented") +end + return process diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index d27361550..31a0a003a 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -27,6 +27,7 @@ #include "lute/time.h" #include "lute/version.h" #include "lute/vm.h" +#include "uv.h" #ifdef _WIN32 #include @@ -293,7 +294,7 @@ static std::optional getValidPath(std::string filePath) return std::nullopt; } -int handleRunCommand(int argc, char** argv, int argOffset) +int handleRunCommand(int argc, char** argv, char* argv0, int argOffset) { std::string filePath; int program_argc = 0; @@ -330,7 +331,7 @@ int handleRunCommand(int argc, char** argv, int argOffset) return 1; } - Runtime runtime; + Runtime runtime(argv0); lua_State* L = setupCliState(runtime); std::optional validPath = getValidPath(filePath); @@ -445,9 +446,9 @@ int handleCompileCommand(int argc, char** argv, int argOffset) return compileScript(inputFilePath, outputFilePath, argv[0]); } -int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv) +int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv, char* argv0) { - Runtime runtime; + Runtime runtime(argv0); lua_State* L = setupCliState(runtime); std::string bytecode = Luau::compile(std::string(result.contents), copts()); @@ -461,7 +462,7 @@ int cliMain(int argc, char** argv) AppendedBytecodeResult embedded = checkForAppendedBytecode(argv[0]); if (embedded.found) { - Runtime runtime; + Runtime runtime(argv[0]); lua_State* GL = setupCliState(runtime); bool success = runBytecode(runtime, embedded.BytecodeData, "=__EMBEDDED__", GL, argc, argv); @@ -485,7 +486,7 @@ int cliMain(int argc, char** argv) if (strcmp(command, "run") == 0) { - return handleRunCommand(argc, argv, argOffset); + return handleRunCommand(argc, argv, argv[0], argOffset); } else if (strcmp(command, "check") == 0) { @@ -507,12 +508,12 @@ int cliMain(int argc, char** argv) } else if (std::optional result = getCliCommand(command); result) { - return handleCliCommand(*result, argc - argOffset, &argv[argOffset]); + return handleCliCommand(*result, argc - argOffset, &argv[argOffset], argv[0]); } else { // Default to 'run' command argOffset = 1; - return handleRunCommand(argc, argv, argOffset); + return handleRunCommand(argc, argv, argv[0], argOffset); } } diff --git a/lute/process/include/lute/process.h b/lute/process/include/lute/process.h index 88c888fd7..be4c2268b 100644 --- a/lute/process/include/lute/process.h +++ b/lute/process/include/lute/process.h @@ -18,11 +18,14 @@ int cwd(lua_State* L); int exitFunc(lua_State* L); +int execpath(lua_State* L); + static const luaL_Reg lib[] = { {"run", run}, {"homedir", homedir}, {"cwd", cwd}, {"exit", exitFunc}, + {"execpath", execpath}, {nullptr, nullptr} }; diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 57bf96bd3..e24b849f8 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -474,6 +474,12 @@ int cwd(lua_State* L) return 1; }; +int execpath(lua_State* L) +{ + lua_pushstring(L, getRuntime(L)->execPath.c_str()); + return 1; +} + static int envIndex(lua_State* L) { const char* key = luaL_checkstring(L, 2); diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index a4d417173..0426f24b1 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -40,7 +40,7 @@ using RuntimeStep = Luau::Variant; struct Runtime { - Runtime(); + Runtime(const char* argv0 = nullptr); ~Runtime(); bool runToCompletion(); @@ -81,6 +81,8 @@ struct Runtime std::vector runningThreads; + std::string execPath; + private: std::mutex continuationMutex; std::vector> continuations; diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 737e44b4f..02e5f77b9 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -9,6 +9,13 @@ #include #include +#include // IWYU pragma: keep + +#ifdef PATH_MAX +#define LUTE_PATH_MAX PATH_MAX +#else +#define LUTE_PATH_MAX 8192 +#endif static void lua_close_checked(lua_State* L) { @@ -16,9 +23,28 @@ static void lua_close_checked(lua_State* L) lua_close(L); } -Runtime::Runtime() +static std::string getExecPath(const char* argv0) +{ + // FIXME: uv_exepath requires that uv_setup_args(argc, argv) was called. + // This function can allocate on some platforms. However, the state is + // persisted for the duration of the program and cleaning it up with + // uv_library_shutdown will only delete it once. This is a problem in unit + // tests that call cliMain multiple times. There, the function would leak + // memory. On the three main platforms supported by Lute however, uv doesn't + // require uv_setup_args to be called, so it's currently left out. + char buf[LUTE_PATH_MAX]; + size_t len = sizeof(buf); + if (uv_exepath(buf, &len) == 0) + return {buf, len}; + if (argv0) + return argv0; + return {}; +} + +Runtime::Runtime(const char* argv0) : globalState(nullptr, lua_close_checked) , dataCopy(nullptr, lua_close_checked) + , execPath(getExecPath(argv0)) { stop.store(false); activeTokens.store(0); diff --git a/lute/vm/src/spawn.cpp b/lute/vm/src/spawn.cpp index 26bd7a369..bcd8944b5 100644 --- a/lute/vm/src/spawn.cpp +++ b/lute/vm/src/spawn.cpp @@ -200,7 +200,7 @@ int lua_spawn(lua_State* L) { const char* file = luaL_checkstring(L, 1); - auto child = std::make_shared(); + auto child = std::make_shared(getRuntime(L)->execPath.c_str()); setupState( *child, From 93c35ac890067bb0ad62ce80c788a68a110bdd52 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:01:16 -0700 Subject: [PATCH 056/642] Rename "Gated Commits" to "CI" (#453) Small change, but "CI" makes more sense than "Gated Commits" for the README icon: image --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77085d505..d0b1c8a19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Gated Commits +name: CI on: push: From 1934ced2d0073d5e86150ba1b1dbfd2dd6eb6650 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:01:57 -0400 Subject: [PATCH 057/642] Add ``errors`` assertion (#444) This is a super common assertion and something I wanted to use in a project I was working on so figure'd I would add it. Not sure what the larger vision is for testing API, but hopefully this fits somewhere. Can also add an optional `expectedError` param --- examples/testing.luau | 15 +++++++++++++++ lute/std/libs/assert.luau | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/examples/testing.luau b/examples/testing.luau index ccc0efc31..66e658cc2 100644 --- a/examples/testing.luau +++ b/examples/testing.luau @@ -50,6 +50,21 @@ test.suite("MySuite", function(suite) assert.tableeq(table1, table2) assert.tableeq(table1, { a = 1 }) end) + + suite:case("expect_error", function(assert) + local function foo(v: boolean) + if v then + error("error") + end + end + + assert.errors(function() -- demonstrates success (callback throws err) + foo(true) + end) + assert.errors(function() -- demonstrates failure (callback doesn't throw) + foo(false) + end) + end) end) test.run() diff --git a/lute/std/libs/assert.luau b/lute/std/libs/assert.luau index 813894f2e..684df4807 100644 --- a/lute/std/libs/assert.luau +++ b/lute/std/libs/assert.luau @@ -12,6 +12,13 @@ function assertions.neq(lhs: T, rhs: T) end end +function assertions.errors(callback: () -> ()) + local success = pcall(callback) + if success then + error({ msg = `{callback} did not throw error.` }) + end +end + -- Helper to format values for error messages local function formatValue(val: any): string if type(val) == "nil" then From 5e7322b073743befeb154dfd652973da0d1409b2 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:39:41 -0700 Subject: [PATCH 058/642] Add a simple CONTRIBUTING.md file (#443) [Rendered](https://github.com/luau-lang/lute/blob/vrn-sn/contributing-md/CONTRIBUTING.md). Feel free to commit to this branch directly. --------- Co-authored-by: ariel --- CONTRIBUTING.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..6e1650632 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Contributing to Lute + +## Getting Started + +First off, thank you for your interest in contributing to Lute! +We're really excited about building a powerful standalone runtime for Luau, and we're glad that you are too! +Whether you're fixing bugs, improving our documentation, or posting feature requests, we value your contributions! +If you're interested in contributing code in particular, please continue reading this document and +check out the [open issues](https://github.com/luau-lang/lute/issues) on our issue tracker for possible avenues to contribute! + +## Ways to Contribute + +- **Bug Reports:** Please open a [GitHub Issue](https://github.com/luau-lang/lute/issues/new) with a clear description, label, and reproduction steps. +- **Feature Requests:** Open an issue with your idea and rationale. +- **Code Contributions:** Fork this repository, create a branch, and submit a PR (see "Code Guidelines" below). + +## Repository Structure + +Lute as a project is organized to use [GitHub](https://github.com/luau-lang/lute) for issue tracking, project milestones, managing contributions, and so forth. +Lute's [documentation site](https://luau-lang.github.io/lute) is also built from this repository, and accepts pull requests just like any other code does. + +Navigating the repository benefits from knowing its structure, namely: +- Source code lives in the [`/lute/`](./lute) directory, +- type definitions are in [`/definitions/`](./definitions), +- user-facing documentation is defined in [`/docs/`](./docs), +- supporting tooling for working on lute is provided in [`/tools/`](./tools), and finally, +- tests can be found in the [`/tests/`](./tests/) directory. + +As a codebase, Lute consists of three components: +1. The runtime itself, written in C++, defined in various modules under `/lute/` and accessible from Luau scripts via `require("@lute/module")`. + These runtime libraries provide core functionality that extends the expressivity of Luau itself, and allows programmers to write general-purpose programs at all. +2. The standard library, written in Luau, defined under `/lute/std/libs` and _embedded into the Lute executable_. + The standard library provides a general interface for programming in Luau, one that we strive to make as delightful as it can be. + It includes both wrappers of runtime functionality that allow runtime authors to support cross-runtime code, as well as + general Luau libraries that improve the developer experience and increase developer productivity on new projects. +3. batteries, written in Luau, defined in various modules under `/batteries/` which are not included in any formal distribution of Lute. + A goal of Lute as a project is to build the foundations for a healthy open source ecosystem for Luau, and achieving that means building + core developer tooling like _a package manager_. In the mean time, we need to write some Luau code to help us build the future, and we + need a place for it to live. batteries are exactly that! Useful Luau code that should run everywhere, and that we can use to build our tooling. + Once Luau has a proper package manager built on Lute, batteries will be pulled out of the repository and made available as a separate package. + +## Code Guidelines + +In the interest of having a consistent standard for code, we expect that incoming contributions will follow existing code styling. +In support of this, we've provided appropriate configuration for autoformatter tools to help enforce this. +For C++ code, we provide a `.clang-format` file; please make sure to format your code before opening a PR. +All Luau scripts should be formatted with [StyLua](https://github.com/JohnnyMorganz/StyLua). + +There are some additional style considerations that we do not have automated enforcement for today, but that we will address in reviews: + +1. All Luau APIs exposed publically by Lute in both the runtime and the standard library must have identifiers written in `luacase`. + This means module names, function names, table fields, exported types, etc. should be written in all lowercase, and should be named ideally with + one or two words succinctly. We know this style is not everyone's favorite, and we do not advocate for external software to use it, but we would like + to retain consistency with Luau's builtin library which inherits this convention from Lua. +2. All other Luau code, including the internals of those Luau APIs, should be written using `camelCase` for identifiers and table fields, and `PascalCase` for type names. +3. Every functionality change should come with tests that express the desired behavior of the code being added. +4. Tests should be placed along side existing tests, and use the appropriate testing tools provided for Lute. + C++ code should be tested using `doctest` and linked into a test executable. + Luau code should be tested using Lute's testing framework, and should be written in a `.test.luau` file. +5. Small, incremental contributions are always preferred over sweeping changes. You should expect that any large sweeping change will be rejected summarily without review. + If you're interested in working on something that _requires_ such a change, you should open an issue first to discuss the idea and get buy-in from the team. + +## Licensing + +By providing code in an issue or opening a pull request, you agree to license that code under the MIT License, and indicate that you have the legal right to do so. From bf5d1d40c6e221b7dbd851c3c4a1d4ad2364a4bf Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:20:13 -0700 Subject: [PATCH 059/642] Feature: add support for user input in `@std/io` (#437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem Lute did not support user input yet. ### Solution We introduce user input support to the Lute and the Lute standard library! Users can use the `input()` function found in`@std/io` to process and store user input from command line, piped input, or file redirection. See examples below and in `examples/user_input.luau`. Functionality was implemented using `libuv`'s library and can be found in `lute/io` ### Examples #### Scripts Get user input ```lua local io = require("@std/io") local input = io.input() print(input) ``` Get user input with prompt ```lua local io = require("@std/io") local name = io.input("Please enter your name: ") print(name) ``` #### Ways to execute the script Command line ```bash $ lute user_input.luau > [will wait for user input in command line] ``` Piped input ```bash $ echo hello | lute user_input.luau ``` Input redirected from file ```bash $ lute user_input.luau < input_text.txt ``` ### Future Work - Tests - `stdout` and `stderr` functionality and other I/O utilities. - Currently, providing a prompt to the user with `.input()` will produce a new line between said prompt and the user input, this will most likely be resolved once `stdout` is implemented Thank you @vrn-sn for massive help (esp with `libuv`) 🙏 Resolves #360 --- CMakeLists.txt | 1 + definitions/io.luau | 7 ++ examples/user_input_no_prompt.luau | 10 ++ examples/user_input_with_prompt.luau | 10 ++ lute/cli/CMakeLists.txt | 2 +- lute/cli/src/climain.cpp | 2 + lute/io/CMakeLists.txt | 12 +++ lute/io/include/lute/io.h | 22 +++++ lute/io/src/io.cpp | 134 +++++++++++++++++++++++++++ lute/std/libs/io.luau | 19 ++++ 10 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 definitions/io.luau create mode 100644 examples/user_input_no_prompt.luau create mode 100644 examples/user_input_with_prompt.luau create mode 100644 lute/io/CMakeLists.txt create mode 100644 lute/io/include/lute/io.h create mode 100644 lute/io/src/io.cpp create mode 100644 lute/std/libs/io.luau diff --git a/CMakeLists.txt b/CMakeLists.txt index c629ae082..b56cea21d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,7 @@ add_subdirectory(lute/vm) add_subdirectory(lute/process) add_subdirectory(lute/system) add_subdirectory(lute/time) +add_subdirectory(lute/io) # executables add_subdirectory(lute/cli) diff --git a/definitions/io.luau b/definitions/io.luau new file mode 100644 index 000000000..ff83c2161 --- /dev/null +++ b/definitions/io.luau @@ -0,0 +1,7 @@ +local io = {} + +function io.input(): string + error("unimplemented") +end + +return io diff --git a/examples/user_input_no_prompt.luau b/examples/user_input_no_prompt.luau new file mode 100644 index 000000000..daf54def3 --- /dev/null +++ b/examples/user_input_no_prompt.luau @@ -0,0 +1,10 @@ +-- User input can be provided in multiple ways: +-- 1. lute user_input.luau +-- 2. echo "Hello!" | lute user_input_no_prompt.luau +-- 3. lute user_input_no_prompt.luau < input_text.txt + +local io = require("@std/io") + +-- Get user input +local input = io.input() +print(input) diff --git a/examples/user_input_with_prompt.luau b/examples/user_input_with_prompt.luau new file mode 100644 index 000000000..4ed801af8 --- /dev/null +++ b/examples/user_input_with_prompt.luau @@ -0,0 +1,10 @@ +-- User input can be provided in multiple ways: +-- 1. lute user_input.luau +-- 2. echo "Hello!" | lute user_input_with_prompt.luau +-- 3. lute user_input_with_prompt.luau < input_text.txt + +local io = require("@std/io") + +-- Get user input with prompt +local name = io.input("Please enter your name: ") +print(name) diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 257098dd0..a5109698f 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -34,7 +34,7 @@ target_sources(Lute.CLI.lib PRIVATE target_compile_features(Lute.CLI.lib PUBLIC cxx_std_17) target_include_directories(Lute.CLI.lib PUBLIC include ${CLI_GENERATED_INCLUDE_DIR}) -target_link_libraries(Lute.CLI.lib PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Require Lute.Runtime Luau.CLI.lib) +target_link_libraries(Lute.CLI.lib PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Require Lute.Runtime Luau.CLI.lib) target_compile_options(Lute.CLI.lib PRIVATE ${LUTE_OPTIONS}) add_executable(Lute.CLI) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 31a0a003a..6da662276 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -14,6 +14,7 @@ #include "lute/compile.h" #include "lute/crypto.h" #include "lute/fs.h" +#include "lute/io.h" #include "lute/luau.h" #include "lute/net.h" #include "lute/options.h" @@ -74,6 +75,7 @@ static void luteopen_libs(lua_State* L) {"@lute/vm", luteopen_vm}, {"@lute/system", luteopen_system}, {"@lute/time", luteopen_time}, + {"@lute/io", luteopen_io}, }}; for (const auto& [name, func] : libs) diff --git a/lute/io/CMakeLists.txt b/lute/io/CMakeLists.txt new file mode 100644 index 000000000..6d3c9c9f3 --- /dev/null +++ b/lute/io/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(Lute.IO STATIC) + +target_sources(Lute.IO PRIVATE + include/lute/io.h + + src/io.cpp +) + +target_compile_features(Lute.IO PUBLIC cxx_std_17) +target_include_directories(Lute.IO PUBLIC "include" ${LIBUV_INCLUDE_DIR}) +target_link_libraries(Lute.IO PRIVATE Lute.Runtime Luau.VM uv_a) +target_compile_options(Lute.IO PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/io/include/lute/io.h b/lute/io/include/lute/io.h new file mode 100644 index 000000000..7626d9565 --- /dev/null +++ b/lute/io/include/lute/io.h @@ -0,0 +1,22 @@ +#pragma once + +#include "lua.h" +#include "lualib.h" + +// open the library as a standard global luau library +int luaopen_io(lua_State* L); +// open the library as a table on top of the stack +int luteopen_io(lua_State* L); + +namespace io +{ + +int read(lua_State* L); + +static const luaL_Reg lib[] = { + {"read", read}, + + {nullptr, nullptr} +}; + +} // namespace io diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp new file mode 100644 index 000000000..2c7611188 --- /dev/null +++ b/lute/io/src/io.cpp @@ -0,0 +1,134 @@ +#include "lute/io.h" +#include "Luau/Variant.h" +#include "lute/runtime.h" + +#include +#include + +#include "lua.h" +#include "lualib.h" + + +namespace io +{ + +struct IOHandle +{ + Luau::Variant streamVariant; + uv_loop_t* loop = nullptr; + ResumeToken resumeToken; + std::shared_ptr self; + std::vector buffer; + + void closeHandles() + { + auto closeCb = [](uv_handle_t* handle) + { + IOHandle* ioh = static_cast(handle->data); + ioh->self.reset(); + }; + + uv_stream_t *stream = getStream(); + uv_read_stop(stream); + uv_close((uv_handle_t*)stream, closeCb); + } + + uv_stream_t* getStream() { + if (auto* tty = streamVariant.get_if()) + return (uv_stream_t*)tty; + if (auto* pipe = streamVariant.get_if()) + return (uv_stream_t*)pipe; + return nullptr; + } +}; + +static void allocBuffer(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf) +{ + IOHandle* ioh = static_cast(handle->data); + ioh->buffer.resize(suggestedSize); + buf->base = ioh->buffer.data(); + buf->len = ioh->buffer.size(); +} + +// IOHandle is closed immediately after one read since we don't need a long running stream for this API. +static void onTtyRead(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) +{ + IOHandle* handle = static_cast(stream->data); + + if (nread > 0) + { + handle->resumeToken->complete( + [data = std::string(buf->base, nread)](lua_State* L) -> int + { + lua_pushlstring(L, data.c_str(), data.size()); + return 1; + } + ); + } + else if (nread < 0) + { + handle->resumeToken->fail(uv_strerror(nread)); + } + + handle->closeHandles(); +} + +int read(lua_State* L) +{ + auto handle = std::make_shared(); + handle->loop = uv_default_loop(); + handle->resumeToken = getResumeToken(L); + handle->self = handle; + + uv_handle_type ht = uv_guess_handle(fileno(stdin)); + if (ht == UV_TTY) + { + uv_tty_t& tty = handle->streamVariant.emplace(); + int status = uv_tty_init(handle->loop, &tty, fileno(stdin), 0); + if (status < 0) + luaL_error(L, "Failed to initialize TTY: %s", uv_strerror(status)); + } + else if (ht == UV_NAMED_PIPE || ht == UV_FILE) + { + uv_pipe_t& pipe = handle->streamVariant.emplace(); + int status = uv_pipe_init(handle->loop, static_cast(&pipe), 0); + if (status < 0) + luaL_error(L, "Failed to initialize pipe: %s", uv_strerror(status)); + uv_pipe_open(static_cast(&pipe), fileno(stdin)); + } + else + { + luaL_error(L, "Unsupported stdin type"); + } + + uv_stream_t *stream = handle->getStream(); + stream->data = handle.get(); + uv_read_start(stream, allocBuffer, onTtyRead); + return lua_yield(L, 0); +} + +} // namespace io + +int luaopen_io(lua_State* L) +{ + luaL_register(L, "io", io::lib); + return 1; +} + +int luteopen_io(lua_State* L) +{ + lua_createtable(L, 0, std::size(io::lib)); + + for (auto& [name, func] : io::lib) + { + if (!name || !func) + break; + + lua_pushcfunction(L, func, name); + lua_setfield(L, -2, name); + } + + lua_setreadonly(L, -1, 1); + + return 1; +} diff --git a/lute/std/libs/io.luau b/lute/std/libs/io.luau new file mode 100644 index 000000000..3cb0d63d2 --- /dev/null +++ b/lute/std/libs/io.luau @@ -0,0 +1,19 @@ +--!strict +-- @std/io +-- stdlib for `@lute/io` +-- Provides standard input, output, and other I/O utility functions + +local io = require("@lute/io") + +-- User input can be provided via command line stdin, piped input, or redirection from a file. See `examples/user_input.luau`. +function input(prompt: string?): string + if prompt then + -- We temporarily use `print` for prompt until we have proper stdout support, so there is a `\n` between prompt and input. + print(prompt) + end + return io.read() +end + +return table.freeze({ + input = input, +}) From ce1c5f021bf8318e0fd810ea9a43b9aa2d15c327 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:52:40 -0700 Subject: [PATCH 060/642] Update license year to include 2025 (#456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not visible in a monospaced font, but the mighty en dash strikes again. 😉 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index d3e2c46f5..d33f22ed8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Roblox Corporation +Copyright (c) 2024–2025 Roblox Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From b60c005dddf67948cd3874e792fa90236604c731 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:38:09 -0700 Subject: [PATCH 061/642] Call `uv_setup_args` at `lute` and `lute-tests` entry points (#455) --- lute/cli/CMakeLists.txt | 2 ++ lute/cli/include/lute/uvstate.h | 12 ++++++++++++ lute/cli/src/main.cpp | 2 ++ lute/cli/src/uvstate.cpp | 13 +++++++++++++ lute/runtime/src/runtime.cpp | 7 ------- tests/src/main.cpp | 19 ++++++++++++++++++- 6 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 lute/cli/include/lute/uvstate.h create mode 100644 lute/cli/src/uvstate.cpp diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index a5109698f..389e2733a 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -26,10 +26,12 @@ target_sources(Lute.CLI.lib PRIVATE include/lute/climain.h include/lute/compile.h include/lute/tc.h + include/lute/uvstate.h src/climain.cpp src/compile.cpp src/tc.cpp + src/uvstate.cpp ) target_compile_features(Lute.CLI.lib PUBLIC cxx_std_17) diff --git a/lute/cli/include/lute/uvstate.h b/lute/cli/include/lute/uvstate.h new file mode 100644 index 000000000..7922f1546 --- /dev/null +++ b/lute/cli/include/lute/uvstate.h @@ -0,0 +1,12 @@ +#pragma once + +struct UvGlobalState +{ + UvGlobalState(const UvGlobalState&) = delete; + UvGlobalState& operator=(const UvGlobalState&) = delete; + UvGlobalState(UvGlobalState&&) = delete; + UvGlobalState& operator=(UvGlobalState&&) = delete; + + UvGlobalState(int argc, char** argv); + ~UvGlobalState(); +}; diff --git a/lute/cli/src/main.cpp b/lute/cli/src/main.cpp index 6453368a3..9230310f4 100644 --- a/lute/cli/src/main.cpp +++ b/lute/cli/src/main.cpp @@ -1,6 +1,8 @@ #include "lute/climain.h" +#include "lute/uvstate.h" int main(int argc, char** argv) { + UvGlobalState uvState(argc, argv); return cliMain(argc, argv); } diff --git a/lute/cli/src/uvstate.cpp b/lute/cli/src/uvstate.cpp new file mode 100644 index 000000000..8a7b97962 --- /dev/null +++ b/lute/cli/src/uvstate.cpp @@ -0,0 +1,13 @@ +#include "lute/uvstate.h" + +#include "uv.h" + +UvGlobalState::UvGlobalState(int argc, char** argv) +{ + uv_setup_args(argc, argv); +} + +UvGlobalState::~UvGlobalState() +{ + uv_library_shutdown(); +} diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 02e5f77b9..761fbcabb 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -25,13 +25,6 @@ static void lua_close_checked(lua_State* L) static std::string getExecPath(const char* argv0) { - // FIXME: uv_exepath requires that uv_setup_args(argc, argv) was called. - // This function can allocate on some platforms. However, the state is - // persisted for the duration of the program and cleaning it up with - // uv_library_shutdown will only delete it once. This is a problem in unit - // tests that call cliMain multiple times. There, the function would leak - // memory. On the three main platforms supported by Lute however, uv doesn't - // require uv_setup_args to be called, so it's currently left out. char buf[LUTE_PATH_MAX]; size_t len = sizeof(buf); if (uv_exepath(buf, &len) == 0) diff --git a/tests/src/main.cpp b/tests/src/main.cpp index a3f832e49..9f894fae8 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -1,2 +1,19 @@ -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#define DOCTEST_CONFIG_IMPLEMENT #include "doctest.h" + +#include "lute/uvstate.h" + +int main(int argc, char** argv) +{ + UvGlobalState uvState(argc, argv); + + doctest::Context context; + context.applyCommandLine(argc, argv); + + int res = context.run(); + + if (context.shouldExit()) + return res; + + return res; +} From 53411325b493bf904344e7204b99ffb708c932b8 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 20 Oct 2025 17:43:28 -0700 Subject: [PATCH 062/642] fix: Fix typecheck errors in std library (#457) --- lute/std/libs/json.luau | 8 ++++---- lute/std/libs/syntax/printer.luau | 2 +- lute/std/libs/task.luau | 5 ++--- lute/std/libs/test.luau | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 89da1dabe..4c600f165 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -52,7 +52,7 @@ local function writeSpaces(state: SerializerState) state.cursor += 4 end else - buffer.writeu8(state.buf, state.cursor, string.byte(" ")) + buffer.writeu8(state.buf, state.cursor, (string.byte(" "))) state.cursor += 1 end @@ -66,7 +66,7 @@ local function writeString(state: SerializerState, str: string) state.cursor += #str end -local serializeAny +local serializeAny: (SerializerState, (Array | Object | boolean | number | string)?) -> () local ESCAPE_MAP = { [0x5C] = string.byte("\\"), -- 5C = '\' @@ -196,7 +196,7 @@ serializeAny = function(state: SerializerState, value: Value) elseif valueType == "string" then serializeString(state, value :: string) elseif valueType == "table" then - if #(value :: {}) == 0 and next(value :: {}) ~= nil then + if #(value :: {}) == 0 and next(value :: { [unknown]: unknown }) ~= nil then serializeTable(state, value :: Object) else serializeArray(state, value :: Array) @@ -329,7 +329,7 @@ local function deserializeString(state: DeserializerState): string return deserializerError(state, "Unterminated string") end -local deserialize +local deserialize: (DeserializerState) -> (Array | Object | boolean | number | string)? local function deserializeArray(state: DeserializerState): Array state.cursor += 1 diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 50706e4ee..401841db7 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -26,7 +26,7 @@ local function printToken(token: luau.Token): string return printTriviaList(token.leadingtrivia) .. token.text .. printTriviaList(token.trailingtrivia) end -local function printString(expr: luau.AstExprConstantString): string +local function printString(expr: luau.AstExprConstantString | luau.AstTypeSingletonString): string local result = printTriviaList(expr.leadingtrivia) if expr.quotestyle == "single" then diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index 1c22cbfe2..53a1d0f68 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -17,7 +17,7 @@ local function create(f, ...): task end) coroutine.resume(data.co, ...) - return data + return data :: task end local function await(t: task) @@ -38,9 +38,8 @@ end local function awaitall(...: task) local tasks = table.pack(...) - tasks.n = nil - for i, v in tasks do + for i, v in ipairs(tasks) do if not v.co then error(`awaitAll: argument {i} is not a task`) end diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index 0b6c184fe..00a7fe3e9 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -135,7 +135,7 @@ function test.run() local passed = 0 -- Error handler for beforeall hook - local function beforeallErrorHandler(suite: TestSuiteData) + local function beforeallErrorHandler(suite: TestSuite) return function(err) -- If beforeall fails, skip all tests in the suite total += #suite.cases From e1520fce27d7b15c7172e0b248cfe22b23f06923 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:08:08 -0700 Subject: [PATCH 063/642] Move `getExecPath` logic to Lute.Process and clean up all uses of `argv[0]` in Lute.CLI (#458) --- lute/cli/include/lute/compile.h | 4 +-- lute/cli/src/climain.cpp | 50 +++++++++++++-------------- lute/cli/src/compile.cpp | 31 +++++++++++++++-- lute/process/include/lute/process.h | 4 +++ lute/process/src/process.cpp | 52 +++++++++++++++++++++++++---- lute/runtime/include/lute/runtime.h | 4 +-- lute/runtime/src/runtime.cpp | 21 +----------- lute/vm/src/spawn.cpp | 2 +- 8 files changed, 108 insertions(+), 60 deletions(-) diff --git a/lute/cli/include/lute/compile.h b/lute/cli/include/lute/compile.h index 5a3e2ab2e..9bc173816 100644 --- a/lute/cli/include/lute/compile.h +++ b/lute/cli/include/lute/compile.h @@ -8,6 +8,6 @@ struct AppendedBytecodeResult std::string BytecodeData; }; -AppendedBytecodeResult checkForAppendedBytecode(const std::string& executablePath); +AppendedBytecodeResult checkForAppendedBytecode(); -int compileScript(const std::string& inputFilePath, const std::string& outputFilePath, const std::string& currentExecutablePath); +int compileScript(const std::string& inputFilePath, const std::string& outputFilePath); diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 6da662276..ae2b5bec1 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -178,7 +178,7 @@ static bool runFile(Runtime& runtime, const char* name, lua_State* GL, int progr return runBytecode(runtime, bytecode, chunkname, GL, program_argc, program_argv); } -static void displayHelp(const char* argv0) +static void displayHelp() { printf("Usage: lute [options] [arguments...]\n"); printf("\n"); @@ -214,7 +214,7 @@ static void displayVersion() printf("%s\n", LUTE_VERSION_FULL); } -static void displayRunHelp(const char* argv0) +static void displayRunHelp() { printf("Usage: lute run [args...]\n"); printf("\n"); @@ -222,7 +222,7 @@ static void displayRunHelp(const char* argv0) printf(" -h, --help Display this usage message.\n"); } -static void displayCheckHelp(const char* argv0) +static void displayCheckHelp() { printf("Usage: lute check [file2.luau...]\n"); printf("\n"); @@ -230,7 +230,7 @@ static void displayCheckHelp(const char* argv0) printf(" -h, --help Display this usage message.\n"); } -static void displayCompileHelp(const char* argv0) +static void displayCompileHelp() { printf("Usage: lute compile [output_executable]\n"); printf("\n"); @@ -296,7 +296,7 @@ static std::optional getValidPath(std::string filePath) return std::nullopt; } -int handleRunCommand(int argc, char** argv, char* argv0, int argOffset) +int handleRunCommand(int argc, char** argv, int argOffset) { std::string filePath; int program_argc = 0; @@ -308,13 +308,13 @@ int handleRunCommand(int argc, char** argv, char* argv0, int argOffset) if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - displayRunHelp(argv[0]); + displayRunHelp(); return 0; } else if (currentArg[0] == '-') { fprintf(stderr, "Error: Unrecognized option '%s' for 'run' command.\n\n", currentArg); - displayRunHelp(argv[0]); + displayRunHelp(); return 1; } else @@ -329,11 +329,11 @@ int handleRunCommand(int argc, char** argv, char* argv0, int argOffset) if (filePath.empty()) { fprintf(stderr, "Error: No file specified for 'run' command.\n\n"); - displayRunHelp(argv[0]); + displayRunHelp(); return 1; } - Runtime runtime(argv0); + Runtime runtime; lua_State* L = setupCliState(runtime); std::optional validPath = getValidPath(filePath); @@ -357,13 +357,13 @@ int handleCheckCommand(int argc, char** argv, int argOffset) if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - displayCheckHelp(argv[0]); + displayCheckHelp(); return 0; } else if (currentArg[0] == '-') { fprintf(stderr, "Error: Unrecognized option '%s' for 'check' command.\n\n", currentArg); - displayCheckHelp(argv[0]); + displayCheckHelp(); return 1; } else @@ -375,7 +375,7 @@ int handleCheckCommand(int argc, char** argv, int argOffset) if (files.empty()) { fprintf(stderr, "Error: No files specified for 'check' command.\n\n"); - displayCheckHelp(argv[0]); + displayCheckHelp(); return 1; } @@ -393,7 +393,7 @@ int handleCompileCommand(int argc, char** argv, int argOffset) if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - displayCompileHelp(argv[0]); + displayCompileHelp(); return 0; } else if (inputFilePath.empty()) @@ -407,7 +407,7 @@ int handleCompileCommand(int argc, char** argv, int argOffset) else { fprintf(stderr, "Error: Too many arguments for 'compile' command.\n\n"); - displayCompileHelp(argv[0]); + displayCompileHelp(); return 1; } } @@ -415,7 +415,7 @@ int handleCompileCommand(int argc, char** argv, int argOffset) if (inputFilePath.empty()) { fprintf(stderr, "Error: No input file specified for 'compile' command.\n\n"); - displayCompileHelp(argv[0]); + displayCompileHelp(); return 1; } @@ -445,12 +445,12 @@ int handleCompileCommand(int argc, char** argv, int argOffset) #endif } - return compileScript(inputFilePath, outputFilePath, argv[0]); + return compileScript(inputFilePath, outputFilePath); } -int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv, char* argv0) +int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv) { - Runtime runtime(argv0); + Runtime runtime; lua_State* L = setupCliState(runtime); std::string bytecode = Luau::compile(std::string(result.contents), copts()); @@ -461,10 +461,10 @@ int cliMain(int argc, char** argv) { Luau::assertHandler() = assertionHandler; - AppendedBytecodeResult embedded = checkForAppendedBytecode(argv[0]); + AppendedBytecodeResult embedded = checkForAppendedBytecode(); if (embedded.found) { - Runtime runtime(argv[0]); + Runtime runtime; lua_State* GL = setupCliState(runtime); bool success = runBytecode(runtime, embedded.BytecodeData, "=__EMBEDDED__", GL, argc, argv); @@ -479,7 +479,7 @@ int cliMain(int argc, char** argv) if (argc < 2) { // TODO: REPL? - displayHelp(argv[0]); + displayHelp(); return 0; } @@ -488,7 +488,7 @@ int cliMain(int argc, char** argv) if (strcmp(command, "run") == 0) { - return handleRunCommand(argc, argv, argv[0], argOffset); + return handleRunCommand(argc, argv, argOffset); } else if (strcmp(command, "check") == 0) { @@ -500,7 +500,7 @@ int cliMain(int argc, char** argv) } else if (strcmp(command, "-h") == 0 || strcmp(command, "--help") == 0) { - displayHelp(argv[0]); + displayHelp(); return 0; } else if (strcmp(command, "--version") == 0) @@ -510,12 +510,12 @@ int cliMain(int argc, char** argv) } else if (std::optional result = getCliCommand(command); result) { - return handleCliCommand(*result, argc - argOffset, &argv[argOffset], argv[0]); + return handleCliCommand(*result, argc - argOffset, &argv[argOffset]); } else { // Default to 'run' command argOffset = 1; - return handleRunCommand(argc, argv, argv[0], argOffset); + return handleRunCommand(argc, argv, argOffset); } } diff --git a/lute/cli/src/compile.cpp b/lute/cli/src/compile.cpp index a15bf433a..dbb0b0f8c 100644 --- a/lute/cli/src/compile.cpp +++ b/lute/cli/src/compile.cpp @@ -1,17 +1,32 @@ #include "lute/compile.h" #include "lute/options.h" +#include "lute/process.h" #include "uv.h" #include +#include const char MAGIC_FLAG[] = "LUTEBYTE"; const size_t MAGIC_FLAG_SIZE = sizeof(MAGIC_FLAG) - 1; const size_t BYTECODE_SIZE_FIELD_SIZE = sizeof(uint64_t); -AppendedBytecodeResult checkForAppendedBytecode(const std::string& executablePath) +AppendedBytecodeResult checkForAppendedBytecode() { AppendedBytecodeResult result; + + std::string executablePath; + { + std::string error; + std::optional execPathOpt = process::getExecPath(&error); + if (!execPathOpt) + { + fprintf(stderr, "Could not get path to executable: %s\n", error.c_str()); + return result; + } + executablePath = *execPathOpt; + } + std::ifstream exeFile(executablePath, std::ios::binary | std::ios::ate); if (!exeFile) { @@ -54,8 +69,20 @@ AppendedBytecodeResult checkForAppendedBytecode(const std::string& executablePat return result; } -int compileScript(const std::string& inputFilePath, const std::string& outputFilePath, const std::string& currentExecutablePath) +int compileScript(const std::string& inputFilePath, const std::string& outputFilePath) { + std::string currentExecutablePath; + { + std::string error; + std::optional execPathOpt = process::getExecPath(&error); + if (!execPathOpt) + { + fprintf(stderr, "Could not get path to current executable: %s\n", error.c_str()); + return 1; + } + currentExecutablePath = *execPathOpt; + } + std::optional source = readFile(inputFilePath); if (!source) { diff --git a/lute/process/include/lute/process.h b/lute/process/include/lute/process.h index be4c2268b..04de45120 100644 --- a/lute/process/include/lute/process.h +++ b/lute/process/include/lute/process.h @@ -3,6 +3,9 @@ #include "lua.h" #include "lualib.h" +#include +#include + // open the library as a standard global luau library int luaopen_process(lua_State* L); // open the library as a table on top of the stack @@ -18,6 +21,7 @@ int cwd(lua_State* L); int exitFunc(lua_State* L); +std::optional getExecPath(std::string* error); int execpath(lua_State* L); static const luaL_Reg lib[] = { diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index e24b849f8..5a20dca58 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -1,16 +1,27 @@ #include "lute/process.h" #include "lute/runtime.h" -#include -#include -#include -#include -#include -#include + #include "Luau/Common.h" #include "lua.h" #include "lualib.h" +#include + +#include +#include +#include +#include +#include +#include + +#include // IWYU pragma: keep +#ifdef PATH_MAX +#define LUTE_PATH_MAX PATH_MAX +#else +#define LUTE_PATH_MAX 8192 +#endif + namespace process { @@ -474,9 +485,36 @@ int cwd(lua_State* L) return 1; }; +std::optional getExecPath(std::string* error) +{ + // Executable path is not expected to change during process lifetime, so we + // can safely cache it after the first retrieval. + static std::optional cachedPath = std::nullopt; + if (cachedPath) + return *cachedPath; + + char buf[LUTE_PATH_MAX]; + size_t len = sizeof(buf); + + if (int status = uv_exepath(buf, &len); status < 0) + { + if (error) + *error = uv_strerror(status); + return std::nullopt; + } + + cachedPath = std::string(buf, len); + return *cachedPath; +} + int execpath(lua_State* L) { - lua_pushstring(L, getRuntime(L)->execPath.c_str()); + std::string error; + std::optional execPath = getExecPath(&error); + if (!execPath) + luaL_error(L, "Failed to get executable path: %s", error.c_str()); + + lua_pushlstring(L, execPath->c_str(), execPath->size()); return 1; } diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index 0426f24b1..a4d417173 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -40,7 +40,7 @@ using RuntimeStep = Luau::Variant; struct Runtime { - Runtime(const char* argv0 = nullptr); + Runtime(); ~Runtime(); bool runToCompletion(); @@ -81,8 +81,6 @@ struct Runtime std::vector runningThreads; - std::string execPath; - private: std::mutex continuationMutex; std::vector> continuations; diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 761fbcabb..737e44b4f 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -9,13 +9,6 @@ #include #include -#include // IWYU pragma: keep - -#ifdef PATH_MAX -#define LUTE_PATH_MAX PATH_MAX -#else -#define LUTE_PATH_MAX 8192 -#endif static void lua_close_checked(lua_State* L) { @@ -23,21 +16,9 @@ static void lua_close_checked(lua_State* L) lua_close(L); } -static std::string getExecPath(const char* argv0) -{ - char buf[LUTE_PATH_MAX]; - size_t len = sizeof(buf); - if (uv_exepath(buf, &len) == 0) - return {buf, len}; - if (argv0) - return argv0; - return {}; -} - -Runtime::Runtime(const char* argv0) +Runtime::Runtime() : globalState(nullptr, lua_close_checked) , dataCopy(nullptr, lua_close_checked) - , execPath(getExecPath(argv0)) { stop.store(false); activeTokens.store(0); diff --git a/lute/vm/src/spawn.cpp b/lute/vm/src/spawn.cpp index bcd8944b5..26bd7a369 100644 --- a/lute/vm/src/spawn.cpp +++ b/lute/vm/src/spawn.cpp @@ -200,7 +200,7 @@ int lua_spawn(lua_State* L) { const char* file = luaL_checkstring(L, 1); - auto child = std::make_shared(getRuntime(L)->execPath.c_str()); + auto child = std::make_shared(); setupState( *child, From 1f0aa30d8efc5c3938d9f067759203f846ace5d6 Mon Sep 17 00:00:00 2001 From: checkraisefold Date: Mon, 20 Oct 2025 18:36:03 -0700 Subject: [PATCH 064/642] Refactor `lute setup` to write to luaurc, add std type defs (#447) Refactors the `lute setup` command to: 1. Put the regular lute typedefs into a `lute` subdirectory, and add a `std` subdirectory with the standard library files inside 2. Accept a flag to create a new luaurc, or overwrite the aliases of one if it exists, to add the type definition folders. Unfortunate caveat is that the std lib json encoder is not deterministic, so each time it moves the json keys around - this is pretty annoying and creates a lot of diff noise. Not sure if there's much to be done about this Luthier: 1. Fix a backslash escaping bug for std lib and command C++ file gen that cropped up when I added this 2. Change definition generation to use above structure 3. Use projectRelative more 4. Change order of `generateFilesIfNeeded` calls to fix a bug - CLI commands rely on the generated type def output, so they should be generated after type defs Also fix the trailing comma in the project luaurc which is typically not valid json Closes #424 --------- Co-authored-by: ariel --- .luaurc | 2 +- lute/cli/commands/setup/init.luau | 58 +++++++++++++++++++++++++++---- lute/cli/src/climain.cpp | 1 + tools/luthier.luau | 50 ++++++++++++++++---------- 4 files changed, 85 insertions(+), 26 deletions(-) diff --git a/.luaurc b/.luaurc index df8328cad..b928cef88 100644 --- a/.luaurc +++ b/.luaurc @@ -3,6 +3,6 @@ "aliases": { "batteries": "./batteries", "std": "./lute/std/libs", - "lute": "./definitions", + "lute": "./definitions" } } diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 21f02dc4c..c92cbb3da 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -1,12 +1,31 @@ local definitions = require("@self/generated/definitions") local fs = require("@lute/fs") local process = require("@lute/process") +local json = require("@std/json") -local homedir = process.homedir() +local BASE_PATH = ".lute/typedefs/0.1.0" +local BASE_PATH_PRETTY = `~/{BASE_PATH}` +local TEMPLATE_RC_FILE = { + aliases = { + lute = `{BASE_PATH_PRETTY}/lute`, + std = `{BASE_PATH_PRETTY}/std`, + }, +} -function mkdirdashp(home: string, subpath: string) +local homeDir = process.homedir() +local cwd = process.cwd() +local args = { ... } + +function mkdirdashp(home: string, subpath: string, noIncludeLast: boolean?) local subdir = home - for _, part in string.split(subpath, "/") do + local parts = string.split(subpath, "/") + local numParts = #parts + + for idx, part in parts do + if noIncludeLast and (idx == numParts) then + break + end + subdir = subdir .. "/" .. part if not fs.exists(subdir) then fs.mkdir(subdir) @@ -15,11 +34,36 @@ function mkdirdashp(home: string, subpath: string) end -- TODO we're assuming the files are completely unmodified, but we really shouldn't do that. -print("Found existing /.lute/ directory! Overwriting type definitions.") -mkdirdashp(homedir, ".lute/typedefs/0.1.0") +print(`Writing definitions at {BASE_PATH_PRETTY}`) +mkdirdashp(homeDir, BASE_PATH) for key, value in definitions :: { [string]: string } do - fs.writestringtofile(homedir .. "/.lute/typedefs/0.1.0/" .. key, value) + local filePath = `{BASE_PATH}/{key}` + mkdirdashp(homeDir, filePath, true) + fs.writestringtofile(`{homeDir}/{filePath}`, value) +end + +print(`Successfully wrote type definition files to {BASE_PATH_PRETTY}`) + +if not table.find(args, "--with-luaurc") then + return +end + +local luauRcPath = `{cwd}/.luaurc` +local rcFile = nil +print(`Writing luaurc file at {luauRcPath}`) + +if fs.exists(luauRcPath) then + local contents = fs.readfiletostring(luauRcPath) + rcFile = json.deserialize(contents) :: any + + rcFile.aliases = rcFile.aliases or {} + for key, value in TEMPLATE_RC_FILE.aliases :: { [string]: string } do + rcFile.aliases[key] = value + end +else + rcFile = TEMPLATE_RC_FILE end +fs.writestringtofile(luauRcPath, json.serialize(rcFile, true)) -print("Successfully wrote type definition files to `~/.lute/typedefs/0.1.0/`") +print(`Successfully wrote luaurc file to {luauRcPath}`) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index ae2b5bec1..181de8a67 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -203,6 +203,7 @@ static void displayHelp() printf("Setup Options:\n"); printf(" lute setup"); printf(" Generates type definition files for the language server.\n"); + printf(" --with-luaurc Defines aliases to the type definition files in the working directory's luaurc file.\n"); printf("\n"); printf("General Options:\n"); printf(" -h, --help Display this usage message.\n"); diff --git a/tools/luthier.luau b/tools/luthier.luau index a59725c9b..5cd6f2a03 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -300,7 +300,9 @@ local function generateStdLibFilesIfNeeded() local lines = string.split(libSrc, "\n") local generatedLines = table.create(#lines) for _, line in lines do - table.insert(generatedLines, `"{string.gsub(line, '"', '\\"')}\\n"`) + local escapedLine = string.gsub(line, "\\", "\\\\") + escapedLine = string.gsub(escapedLine, '"', '\\"') + table.insert(generatedLines, `"{escapedLine}\\n"`) end fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) else @@ -337,15 +339,21 @@ end local function getTypeDefinitionsHash(): string local libsPath = projectRelative("definitions") + local stdPath = projectRelative("lute", "std", "libs") local toHash = "" - traverseFileTree(libsPath, function(path, relativePath) - if safeFsType(path) == "file" then - toHash ..= "./" .. relativePath - toHash ..= fs.readfiletostring(path) - end - end) + local function traverseDefs(path: string, namespace: string) + traverseFileTree(path, function(path, relativePath) + if safeFsType(path) == "file" then + toHash ..= `./{namespace}/{relativePath}` + toHash ..= fs.readfiletostring(path) + end + end) + end + + traverseDefs(libsPath, "lute") + traverseDefs(stdPath, "std") local digest = crypto.digest(crypto.hash.blake2b256, toHash) return hexify(digest) @@ -373,18 +381,22 @@ local function generateTypeDefinitionFiles() print("Type definitions out of date, creating new ones.") + local libsPath = projectRelative("definitions") + local stdPath = projectRelative("lute", "std", "libs") local dictionaryEntries: { [string]: string } = {} - for _, file in fs.listdir("definitions") do - if file.type ~= "file" then - continue - end - - local content = fs.readasync(`definitions/{file.name}`) - - dictionaryEntries[file.name] = content + local function traverseDefs(path: string, namespace: string) + traverseFileTree(path, function(path, relativePath) + if safeFsType(path) == "file" then + local content = fs.readasync(path) + dictionaryEntries[`{namespace}/{relativePath}`] = content + end + end) end + traverseDefs(libsPath, "lute") + traverseDefs(stdPath, "std") + local stringDictionary = "" for key, value in dictionaryEntries do @@ -396,7 +408,7 @@ local function generateTypeDefinitionFiles() fs.close(hash) fs.writestringtofile( - "lute/cli/commands/setup/generated/definitions.luau", + projectRelative("lute", "cli", "commands", "setup", "generated", "definitions.luau"), string.format(TYPEDEF_PATTERN, stringDictionary) ) end @@ -469,7 +481,9 @@ local function generateCliCommandsFilesIfNeeded() local lines = string.split(libSrc, "\n") local generatedLines = table.create(#lines) for _, line in lines do - table.insert(generatedLines, `"{string.gsub(line, '"', '\\"')}\\n"`) + local escapedLine = string.gsub(line, "\\", "\\\\") + escapedLine = string.gsub(escapedLine, '"', '\\"') + table.insert(generatedLines, `"{escapedLine}\\n"`) end fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) else @@ -506,8 +520,8 @@ end local function generateFilesIfNeeded() generateStdLibFilesIfNeeded() - generateCliCommandsFilesIfNeeded() generateTypeDefinitionFiles() + generateCliCommandsFilesIfNeeded() end local function getExePath(): string From bbae2db78c7f45effc4c59fe8cbd1b0d70cd0a89 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:48:21 -0700 Subject: [PATCH 065/642] fix: rename `io.read()` in definitions (#459) Forgot to rename the `.read()` call in `definitions/io.luau` when renaming the `input` to `read` in `io.cpp` in https://github.com/luau-lang/lute/pull/437 and was causing a type error --- definitions/io.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/definitions/io.luau b/definitions/io.luau index ff83c2161..32fd620dc 100644 --- a/definitions/io.luau +++ b/definitions/io.luau @@ -1,6 +1,6 @@ local io = {} -function io.input(): string +function io.read(): string error("unimplemented") end From 97f75988a3f81d251fc39717592113848d2ce2f6 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:00:04 -0700 Subject: [PATCH 066/642] docs: update docs site config to have latest definitions (#460) --- docs/.vitepress/config.mts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c00548994..99e18cec9 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -22,12 +22,15 @@ export default defineConfig({ { text: "Reference", items: [ + { text: 'crypto', link: '/reference/crypto' }, { text: 'fs', link: '/reference/fs' }, + { text: 'io', link: '/reference/io' }, { text: 'luau', link: '/reference/luau' }, { text: 'net', link: '/reference/net' }, { text: 'process', link: '/reference/process' }, { text: 'system', link: '/reference/system' }, { text: 'task', link: '/reference/task' }, + { text: 'time', link: '/reference/time' }, { text: 'vm', link: '/reference/vm' }, ] } From 2128b80707f55a6c892eb6fc2ab6b0970b299d94 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:31:18 -0700 Subject: [PATCH 067/642] Fix race condition in release workflow (#464) For the `0.1.0-nightly.20251018` release, there was a race condition between the "Create Draft Release" and "Publish Release" steps of the release workflow. We successfully created the draft release with all the built binaries: image However, when the release was being published, GitHub was still internally registering the new release tag. Rather than publishing the draft release, the publish step was unable to find the still-registering tag, so it instead published a brand-new release. This one wasn't set to a prerelease, and it had none of the binaries attached since the draft release's settings didn't carry over. image I've since deleted both of these since releases being immutable makes it difficult to fix this retroactively. To avoid this in the future, I've added a short `sleep` to our release workflow; even 1 second seems to have prevented this issue in all other nightly releases, but I've put 5 seconds to be safe. I've also replaced our final release step with a call to `gh release edit`, which will immediately fail if the tag cannot be found (couldn't find an analogue in `luau-lang/action-gh-release@v2`). --- .github/workflows/release.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c27c13582..4a6368f60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -163,8 +163,10 @@ jobs: prerelease: ${{ github.event.inputs.nightly || github.event_name == 'schedule' }} files: release/*.zip + - name: Wait for Tag Creation in GitHub + run: sleep 5 + - name: Publish Release - uses: luau-lang/action-gh-release@v2 - with: - tag_name: ${{ steps.tag_release.outputs.tag }} - draft: false + run: gh release edit "${{ steps.tag_release.outputs.tag }}" --draft=false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 68e696fadbb66a54930609df0c78dfe326380044 Mon Sep 17 00:00:00 2001 From: ariel Date: Wed, 22 Oct 2025 16:15:23 -0700 Subject: [PATCH 068/642] runtime: implement `system.tmpdir` to provide access to the temporary files directory. (#466) Varying OSes provide different preferred locations for temporary files, and libuv provides an API by which we can get at that. We should make that available so that people can build cross-platform code that interacts with these directories safely. --- definitions/system.luau | 4 ++++ lute/system/include/lute/system.h | 2 ++ lute/system/src/system.cpp | 24 ++++++++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/definitions/system.luau b/definitions/system.luau index b51285996..c7d14f1c9 100644 --- a/definitions/system.luau +++ b/definitions/system.luau @@ -21,6 +21,10 @@ function system.hostname(): string error("unimplemented") end +function system.tmpdir(): string + error("unimplemented") +end + function system.totalmemory(): number error("unimplemented") end diff --git a/lute/system/include/lute/system.h b/lute/system/include/lute/system.h index b856ca0b8..7310d1ebe 100644 --- a/lute/system/include/lute/system.h +++ b/lute/system/include/lute/system.h @@ -21,6 +21,7 @@ int lua_freememory(lua_State* L); int lua_totalmemory(lua_State* L); int lua_hostname(lua_State* L); int lua_uptime(lua_State* L); +int lua_tmpdir(lua_State* L); static const luaL_Reg lib[] = { {"cpus", lua_cpus}, @@ -29,6 +30,7 @@ static const luaL_Reg lib[] = { {"totalmemory", lua_totalmemory}, {"hostname", lua_hostname}, {"uptime", lua_uptime}, + {"tmpdir", lua_tmpdir}, {nullptr, nullptr} }; diff --git a/lute/system/src/system.cpp b/lute/system/src/system.cpp index 331a9db48..8a5027d90 100644 --- a/lute/system/src/system.cpp +++ b/lute/system/src/system.cpp @@ -123,6 +123,30 @@ int lua_uptime(lua_State* L) return 1; } + +int lua_tmpdir(lua_State* L) +{ + size_t size = 255; + std::string tmpdir; + tmpdir.reserve(size); + + int res = uv_os_tmpdir(tmpdir.data(), &size); + if (res == UV_ENOBUFS) + { + tmpdir.reserve(size); // libuv updates the size to what's required + res = uv_os_tmpdir(tmpdir.data(), &size); + } + + if (res != 0) + { + luaL_error(L, "libuv error: %s", uv_strerror(res)); + } + + lua_pushstring(L, tmpdir.c_str()); + + return 1; + +} } // namespace libsystem int luaopen_system(lua_State* L) From e17579f8007266edbc25fc19ad5f06c680afaeed Mon Sep 17 00:00:00 2001 From: ariel Date: Thu, 23 Oct 2025 10:30:59 -0700 Subject: [PATCH 069/642] Improve luau API usage by using `pushlstring` where size is known. (#467) I looked at all of our usage of `lua_pushstring` in Lute today, and replaced any instance with `lua_pushlstring` if it was a dynamic string value where we know the size already (e.g. because we determined the size using libuv APIs). --- lute/fs/src/fs.cpp | 2 +- lute/process/src/process.cpp | 11 ++++++----- lute/system/src/system.cpp | 4 ++-- tests/path.posix.test.luau | 4 ++-- tests/path.win32.test.luau | 18 +++--------------- 5 files changed, 14 insertions(+), 25 deletions(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 39e48d7e7..ebc7e4874 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -629,7 +629,7 @@ int fs_watch(lua_State* L) eventHandle->callbackReference->push(L); // filename - lua_pushstring(L, filename.c_str()); + lua_pushlstring(L, filename.c_str(), filename.size()); // events lua_createtable(L, 0, 2); diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 5a20dca58..ef27533a4 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -119,7 +119,7 @@ struct ProcessHandle if (!finalSignalStr.empty()) { - lua_pushstring(L, finalSignalStr.c_str()); + lua_pushlstring(L, finalSignalStr.c_str(), finalSignalStr.size()); } else { @@ -442,7 +442,7 @@ int homedir(lua_State* L) return 1; } - lua_pushstring(L, buffer.c_str()); + lua_pushlstring(L, buffer.c_str(), buffer.size()); return 1; } @@ -480,7 +480,7 @@ int cwd(lua_State* L) return 1; } - lua_pushstring(L, buffer.c_str()); + lua_pushlstring(L, buffer.c_str(), buffer.size()); return 1; }; @@ -601,8 +601,9 @@ static int envIterNext(lua_State* L) return 0; } - lua_pushstring(L, iter->items[iter->index].name); - lua_pushstring(L, iter->items[iter->index].value); + uv_env_item_t item = iter->items[iter->index]; + lua_pushstring(L, item.name); + lua_pushstring(L, item.value); iter->index++; return 2; } diff --git a/lute/system/src/system.cpp b/lute/system/src/system.cpp index 8a5027d90..247f33442 100644 --- a/lute/system/src/system.cpp +++ b/lute/system/src/system.cpp @@ -104,7 +104,7 @@ int lua_hostname(lua_State* L) luaL_error(L, "libuv error: %s", uv_strerror(res)); } - lua_pushstring(L, hostname.c_str()); + lua_pushlstring(L, hostname.c_str(), hostname.size()); return 1; } @@ -142,7 +142,7 @@ int lua_tmpdir(lua_State* L) luaL_error(L, "libuv error: %s", uv_strerror(res)); } - lua_pushstring(L, tmpdir.c_str()); + lua_pushlstring(L, tmpdir.c_str(), tmpdir.size()); return 1; diff --git a/tests/path.posix.test.luau b/tests/path.posix.test.luau index 3ba105120..c1d05adb4 100644 --- a/tests/path.posix.test.luau +++ b/tests/path.posix.test.luau @@ -479,7 +479,7 @@ test.suite("PathPosixResolveSuite", function(suite) -- This test assumes we're in some working directory local result = path.posix.resolve("documents/file.txt") -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / - assert.eq(result.absolute, system.os ~= "windows") + assert.eq(result.absolute, system.win32) -- The exact parts will depend on the current working directory -- But we can check that it ends with our relative path local endIndex = #result.parts @@ -509,7 +509,7 @@ test.suite("PathPosixResolveSuite", function(suite) -- Should return normalized current working directory local result = path.posix.resolve() -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / - assert.eq(result.absolute, system.os ~= "windows") + assert.eq(result.absolute, system.win32) -- Can't test exact parts since cwd varies end) diff --git a/tests/path.win32.test.luau b/tests/path.win32.test.luau index af4e02f21..33a9c77d8 100644 --- a/tests/path.win32.test.luau +++ b/tests/path.win32.test.luau @@ -602,11 +602,7 @@ test.suite("PathWin32ResolveSuite", function(suite) -- This test assumes we're in some working directory local result = path.win32.resolve("Documents\\file.txt") -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters - if system.os == "windows" then - assert.eq(result.kind, "absolute") - else - assert.eq(result.kind, "relative") - end + assert.eq(result.kind, if system.win32 then "absolute" else "relative") -- The exact parts will depend on the current working directory -- But we can check that it ends with our relative path -- In an ideal world, we'd mock `process.cwd` @@ -649,22 +645,14 @@ test.suite("PathWin32ResolveSuite", function(suite) -- Should return normalized current working directory local result = path.win32.resolve() -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters - if system.os == "windows" then - assert.eq(result.kind, "absolute") - else - assert.eq(result.kind, "relative") - end + assert.eq(result.kind, if system.win32 then "absolute" else "relative") -- Can't test exact parts since cwd varies end) suite:case("resolve_drive_relative_path", function(assert) local result = path.win32.resolve("C:Documents\\file.txt") -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters - if system.os == "windows" then - assert.eq(result.kind, "absolute") - else - assert.eq(result.kind, "relative") - end + assert.eq(result.kind, if system.win32 then "absolute" else "relative") assert.eq(result.driveLetter, "C") -- The exact parts depend on cwd, but should be absolute local endIndex = #result.parts From 6b32999de5fa3ae4a10f4078b9ff7dfae14bbd24 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 23 Oct 2025 11:25:55 -0700 Subject: [PATCH 070/642] Port AST serializer tests to new runner (#469) This needed to happen sooner or later, and I preferred sooner because the old tests were spitting out a bunch of output which makes it hard to debug CI --- tests/testAstSerializer.test.luau | 296 +++++++++++++++--------------- 1 file changed, 146 insertions(+), 150 deletions(-) diff --git a/tests/testAstSerializer.test.luau b/tests/testAstSerializer.test.luau index c411125f8..8542c931c 100644 --- a/tests/testAstSerializer.test.luau +++ b/tests/testAstSerializer.test.luau @@ -1,173 +1,169 @@ -local luau = require("@lute/luau") +local asserts = require("@std/assert") local fs = require("@lute/fs") +local luau = require("@lute/luau") +local path = require("@std/path") +local test = require("@std/test") local parser = require("@std/syntax/parser") local printer = require("@std/syntax/printer") local T = require("@lute/luau") local function assertEqualsLocation( + assert: asserts.Asserts, actual: T.Location, startLine: number, startColumn: number, endLine: number, endColumn: number ) - assert(actual.begin.line == startLine) - assert(actual.begin.column == startColumn) - assert(actual["end"].line == endLine) - assert(actual["end"].column == endColumn) -end - -local function test_tokenContainsLeadingSpaces() - local block = luau.parse(" local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localkeyword - assert(#token.leadingtrivia == 1) - assert(token.leadingtrivia[1].tag == "whitespace") - assert(token.leadingtrivia[1].text == " ") - assertEqualsLocation(token.leadingtrivia[1].location, 0, 0, 0, 2) -end - -local function test_tokenContainsLeadingNewline() - local block = luau.parse("\n" .. "local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localkeyword - assert(#token.leadingtrivia == 1) - assert(token.leadingtrivia[1].tag == "whitespace") - assert(token.leadingtrivia[1].text == "\n") - assertEqualsLocation(token.leadingtrivia[1].location, 0, 0, 1, 0) -end - -local function test_tokenContainsLeadingSingleLineComment() - local block = luau.parse("-- comment\n" .. "local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localkeyword - assert(#token.leadingtrivia == 2) - assert(token.leadingtrivia[1].tag == "comment") - assert(token.leadingtrivia[1].text == "-- comment") - assertEqualsLocation(token.leadingtrivia[1].location, 0, 0, 0, 10) - assert(token.leadingtrivia[2].tag == "whitespace") - assert(token.leadingtrivia[2].text == "\n") - assertEqualsLocation(token.leadingtrivia[2].location, 0, 10, 1, 0) + assert.eq(actual.begin.line, startLine) + assert.eq(actual.begin.column, startColumn) + assert.eq(actual["end"].line, endLine) + assert.eq(actual["end"].column, endColumn) end -local function test_tokenContainsLeadingBlockComment() - local block = luau.parse("--[[ comment ]] local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localkeyword - assert(#token.leadingtrivia == 2) - assert(token.leadingtrivia[1].tag == "blockcomment") - assert(token.leadingtrivia[1].text == "--[[ comment ]]") - assertEqualsLocation(token.leadingtrivia[1].location, 0, 0, 0, 15) - assert(token.leadingtrivia[2].tag == "whitespace") - assert(token.leadingtrivia[2].text == " ") - assertEqualsLocation(token.leadingtrivia[2].location, 0, 15, 0, 16) -end - -local function test_tokenizeWhitespace() - local block = luau.parse(" \n\t\t\n\n" .. "local x = 1").root - assert(#block.statements == 1) - - local l = block.statements[1] - assert(l.tag == "local") - - local token = l.localkeyword - assert(#token.leadingtrivia == 3) - assert(token.leadingtrivia[1].text == " \n") - assert(token.leadingtrivia[2].text == "\t\t\n") - assert(token.leadingtrivia[3].text == "\n") -end - -local function test_triviaSplitBetweenLeadingAndTrailing() - local block = luau.parse("local x = 'test' -- comment\n" .. "-- comment 2\nlocal y = 'value'").root - assert(#block.statements == 2) - - local firstStmt = block.statements[1] - assert(firstStmt.tag == "local") +test.suite("AstSerializer", function(suite) + suite:case("tokenContainsLeadingSpaces", function(assert) + local block = luau.parse(" local x = 1").root + assert.eq(#block.statements, 1) + + local l = block.statements[1] + assert.eq(l.tag, "local") + + local token = (l :: luau.AstStatLocal).localkeyword + assert.eq(#token.leadingtrivia, 1) + assert.eq(token.leadingtrivia[1].tag, "whitespace") + assert.eq(token.leadingtrivia[1].text, " ") + assertEqualsLocation(assert, token.leadingtrivia[1].location, 0, 0, 0, 2) + end) + + suite:case("tokenContainsLeadingNewline", function(assert) + local block = luau.parse("\n" .. "local x = 1").root + assert.eq(#block.statements, 1) + + local l = block.statements[1] + assert.eq(l.tag, "local") + + local token = (l :: luau.AstStatLocal).localkeyword + assert.eq(#token.leadingtrivia, 1) + assert.eq(token.leadingtrivia[1].tag, "whitespace") + assert.eq(token.leadingtrivia[1].text, "\n") + assertEqualsLocation(assert, token.leadingtrivia[1].location, 0, 0, 1, 0) + end) + + suite:case("tokenContainsLeadingSingleLineComment", function(assert) + local block = luau.parse("-- comment\n" .. "local x = 1").root + assert.eq(#block.statements, 1) + + local l = block.statements[1] + assert.eq(l.tag, "local") + + local token = (l :: luau.AstStatLocal).localkeyword + assert.eq(#token.leadingtrivia, 2) + assert.eq(token.leadingtrivia[1].tag, "comment") + assert.eq(token.leadingtrivia[1].text, "-- comment") + assertEqualsLocation(assert, token.leadingtrivia[1].location, 0, 0, 0, 10) + assert.eq(token.leadingtrivia[2].tag, "whitespace") + assert.eq(token.leadingtrivia[2].text, "\n") + assertEqualsLocation(assert, token.leadingtrivia[2].location, 0, 10, 1, 0) + end) + + suite:case("tokenContainsLeadingBlockComment", function(assert) + local block = luau.parse("--[[ comment ]] local x = 1").root + assert.eq(#block.statements, 1) + + local l = block.statements[1] + assert.eq(l.tag, "local") + + local token = (l :: luau.AstStatLocal).localkeyword + assert.eq(#token.leadingtrivia, 2) + assert.eq(token.leadingtrivia[1].tag, "blockcomment") + assert.eq(token.leadingtrivia[1].text, "--[[ comment ]]") + assertEqualsLocation(assert, token.leadingtrivia[1].location, 0, 0, 0, 15) + assert.eq(token.leadingtrivia[2].tag, "whitespace") + assert.eq(token.leadingtrivia[2].text, " ") + assertEqualsLocation(assert, token.leadingtrivia[2].location, 0, 15, 0, 16) + end) + + suite:case("tokenizeWhitespace", function(assert) + local block = luau.parse(" \n\t\t\n\n" .. "local x = 1").root + assert.eq(#block.statements, 1) + + local l = block.statements[1] + assert.eq(l.tag, "local") + + local token = (l :: luau.AstStatLocal).localkeyword + assert.eq(#token.leadingtrivia, 3) + assert.eq(token.leadingtrivia[1].text, " \n") + assert.eq(token.leadingtrivia[2].text, "\t\t\n") + assert.eq(token.leadingtrivia[3].text, "\n") + end) + + suite:case("triviaSplitBetweenLeadingAndTrailing", function(assert) + local block = luau.parse("local x = 'test' -- comment\n" .. "-- comment 2\nlocal y = 'value'").root + assert.eq(#block.statements, 2) + + local firstStmt = block.statements[1] + assert.eq(firstStmt.tag, "local") + + local trailingToken: luau.AstExpr = (firstStmt :: luau.AstStatLocal).values[1].node + assert.eq(trailingToken.tag, "string") + trailingToken = trailingToken :: luau.AstExprConstantString + assert.eq(#trailingToken.trailingtrivia, 3) + assert.eq(trailingToken.trailingtrivia[1].text, " ") + assert.eq(trailingToken.trailingtrivia[2].text, "-- comment") + assert.eq(trailingToken.trailingtrivia[3].text, "\n") + + local secondStmt = block.statements[2] + assert.eq(secondStmt.tag, "local") + + local leadingToken = (secondStmt :: luau.AstStatLocal).localkeyword + assert.eq(#leadingToken.leadingtrivia, 2) + assert.eq(leadingToken.leadingtrivia[1].text, "-- comment 2") + assert.eq(leadingToken.leadingtrivia[2].text, "\n") + end) + + suite:case("roundtrippableAst", function(assert) + local function visitDirectory(directory: string) + for _, entry in fs.listdir(directory) do + local p = path.join(directory, entry.name) + local source = fs.readfiletostring(path.format(p)) + local result = printer.printfile(parser.parsefile(source)) + + assert.eq(source, result) + end + end - local trailingToken = firstStmt.values[1].node - assert(trailingToken.tag == "string") - assert(#trailingToken.trailingtrivia == 3) - assert(trailingToken.trailingtrivia[1].text == " ") - assert(trailingToken.trailingtrivia[2].text == "-- comment") - assert(trailingToken.trailingtrivia[3].text == "\n") + visitDirectory("examples") + visitDirectory("tests/astSerializerTests") + end) - local secondStmt = block.statements[2] - assert(secondStmt.tag == "local") + suite:case("lineOffsetsField", function(assert) + local singleLineCode = "local x = 1" + local result = luau.parse(singleLineCode) - local leadingToken = secondStmt.localkeyword - assert(#leadingToken.leadingtrivia == 2) - assert(leadingToken.leadingtrivia[1].text == "-- comment 2") - assert(leadingToken.leadingtrivia[2].text == "\n") -end + assert.eq(type(result.lineoffsets), "table") + assert.eq(#result.lineoffsets, 1) + assert.eq(result.lineoffsets[1], 0) -local function test_roundtrippableAst() - local function visitDirectory(directory: string) - for _, entry in fs.listdir(directory) do - local path = `{directory}/{entry.name}` - print(path) - local source = fs.readfiletostring(path) - local result = printer.printfile(parser.parsefile(source)) + local multiLineCode = "local x = 1\nlocal y = 2\nlocal z = 3" + local multiResult = luau.parse(multiLineCode) - print(result) - assert(source == result) - end - end + assert.eq(type(multiResult.lineoffsets), "table") + assert.eq(#multiResult.lineoffsets, 3) + assert.eq(multiResult.lineoffsets[1], 0) + assert.eq(multiResult.lineoffsets[2], 12) + assert.eq(multiResult.lineoffsets[3], 24) - visitDirectory("examples") - visitDirectory("tests/astSerializerTests") -end + local emptyLineCode = "local x = 1\n\nlocal y = 2" + local emptyResult = luau.parse(emptyLineCode) -local function test_lineOffsetsField() - local singleLineCode = "local x = 1" - local result = luau.parse(singleLineCode) - - assert(result.lineoffsets) - assert(type(result.lineoffsets) == "table", "lineoffsets should be a table") - assert(#result.lineoffsets == 1, "Should have 1 line offset for single line code") -- only line 0 - assert(result.lineoffsets[1] == 0, "First line should start at offset 0") - - local multiLineCode = "local x = 1\nlocal y = 2\nlocal z = 3" - local multiResult = luau.parse(multiLineCode) - - assert(multiResult.lineoffsets) - assert(type(multiResult.lineoffsets) == "table", "lineoffsets should be a table for multiline") - assert(#multiResult.lineoffsets == 3, "Should have 3 line offsets for 3-line code") -- lines 0, 1, 2 - assert(multiResult.lineoffsets[1] == 0, "First line should start at offset 0") - assert(multiResult.lineoffsets[2] == 12, "Second line should start after 'local x = 1\\n'") - assert(multiResult.lineoffsets[3] == 24, "Third line should start after 'local x = 1\\nlocal y = 2\\n'") - - local emptyLineCode = "local x = 1\n\nlocal y = 2" - local emptyResult = luau.parse(emptyLineCode) - - assert(emptyResult.lineoffsets) - assert(#emptyResult.lineoffsets == 3, "Should have 3 line offsets including empty line") - assert(emptyResult.lineoffsets[1] == 0, "First line should start at offset 0") - assert(emptyResult.lineoffsets[2] == 12, "Second line (empty) should start after 'local x = 1\\n'") - assert(emptyResult.lineoffsets[3] == 13, "Third line should start after empty line") -end + assert.eq(type(emptyResult.lineoffsets), "table") + assert.eq(#emptyResult.lineoffsets, 3) + assert.eq(emptyResult.lineoffsets[1], 0) + assert.eq(emptyResult.lineoffsets[2], 12) + assert.eq(emptyResult.lineoffsets[3], 13) + end) +end) -test_tokenContainsLeadingSpaces() -test_tokenContainsLeadingNewline() -test_tokenContainsLeadingSingleLineComment() -test_tokenContainsLeadingBlockComment() -test_tokenizeWhitespace() -test_triviaSplitBetweenLeadingAndTrailing() -test_roundtrippableAst() -test_lineOffsetsField() +test.run() From d9b47a84f5fc99396a62e56e373597dc04729634 Mon Sep 17 00:00:00 2001 From: ariel Date: Thu, 23 Oct 2025 11:56:56 -0700 Subject: [PATCH 071/642] Change base URL in VitePress config (#470) We changed the domain to not be under a folder, but be under a subdomain (`lute.luau.org`, instead of `luau-lang.github.io/lute/`), so we need to update the vitepress default. --- docs/.vitepress/config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 99e18cec9..cff200169 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -4,7 +4,7 @@ import { defineConfig } from 'vitepress' export default defineConfig({ title: "Lute", description: "Luau for General-Purpose Programming", - base: "/lute/", + base: "/", themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ From 2457656c6d8f918bff59240483bcd27bac325aba Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 23 Oct 2025 12:24:43 -0700 Subject: [PATCH 072/642] feature: Create a Virtual File System implementation that can restore code from a bytecode bundle. (#450) This PR implements a 'bundle' virtual file system, for navigating requires and loading bytecode from an executable compiled with `lute compile`. --- lute/require/CMakeLists.txt | 2 + lute/require/include/lute/bundlevfs.h | 31 ++++++ lute/require/include/lute/require.h | 2 + lute/require/include/lute/requirevfs.h | 6 ++ lute/require/src/bundlevfs.cpp | 129 +++++++++++++++++++++++++ lute/require/src/require.cpp | 7 +- lute/require/src/requirevfs.cpp | 54 ++++++++++- 7 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 lute/require/include/lute/bundlevfs.h create mode 100644 lute/require/src/bundlevfs.cpp diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index 0059b866c..f4ad8dc01 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(Lute.Require) target_sources(Lute.Require PRIVATE + include/lute/bundlevfs.h include/lute/clivfs.h include/lute/filevfs.h include/lute/modulepath.h @@ -9,6 +10,7 @@ target_sources(Lute.Require PRIVATE include/lute/requirevfs.h include/lute/stdlibvfs.h + src/bundlevfs.cpp src/clivfs.cpp src/filevfs.cpp src/modulepath.cpp diff --git a/lute/require/include/lute/bundlevfs.h b/lute/require/include/lute/bundlevfs.h new file mode 100644 index 000000000..0b2ee0bca --- /dev/null +++ b/lute/require/include/lute/bundlevfs.h @@ -0,0 +1,31 @@ +#pragma once + +#include "lute/modulepath.h" + +#include "Luau/DenseHash.h" + +#include +#include +#include + +class BundleVfs +{ +public: + BundleVfs(Luau::DenseHashMap bundleMap); + + NavigationStatus resetToPath(const std::string& path); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& name); + + bool isModulePresent() const; + std::string getIdentifier() const; + std::optional getContents(const std::string& path) const; + + bool isConfigPresent() const; + std::optional getConfig() const; + +private: + const Luau::DenseHashMap filePathToBytecode; + std::optional modulePath; +}; diff --git a/lute/require/include/lute/require.h b/lute/require/include/lute/require.h index 00facfc6d..4be08c9b0 100644 --- a/lute/require/include/lute/require.h +++ b/lute/require/include/lute/require.h @@ -1,5 +1,6 @@ #pragma once +#include "lute/bundlevfs.h" #include "lute/clivfs.h" #include "lute/requirevfs.h" @@ -13,6 +14,7 @@ struct RequireCtx { RequireCtx(); RequireCtx(CliVfs cliVfs); + RequireCtx(BundleVfs bundleVfs); RequireVfs vfs; }; diff --git a/lute/require/include/lute/requirevfs.h b/lute/require/include/lute/requirevfs.h index 8d143fe66..9029b7355 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -1,5 +1,6 @@ #pragma once +#include "lute/bundlevfs.h" #include "lute/clivfs.h" #include "lute/filevfs.h" #include "lute/modulepath.h" @@ -15,6 +16,7 @@ class RequireVfs public: RequireVfs() = default; RequireVfs(CliVfs cliVfs); + RequireVfs(BundleVfs bundleVfs); bool isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const; @@ -34,12 +36,15 @@ class RequireVfs bool isConfigPresent(lua_State* L) const; std::string getConfig(lua_State* L) const; + bool isPrecompiled() const { return vfsType == VFSType::Bundle; }; + private: enum class VFSType { Disk, Std, Cli, + Bundle, Lute, }; @@ -48,6 +53,7 @@ class RequireVfs FileVfs fileVfs; StdLibVfs stdLibVfs; std::optional cliVfs = std::nullopt; + std::optional bundleVfs = std::nullopt; std::string lutePath; bool atFakeRoot = false; diff --git a/lute/require/src/bundlevfs.cpp b/lute/require/src/bundlevfs.cpp new file mode 100644 index 000000000..a377503cd --- /dev/null +++ b/lute/require/src/bundlevfs.cpp @@ -0,0 +1,129 @@ +#include "lute/bundlevfs.h" + +#include "Luau/Common.h" +#include "Luau/DenseHash.h" + +#include +#include + +constexpr std::string_view kBundlePrefix = "@bundle/"; + +BundleVfs::BundleVfs(Luau::DenseHashMap bundleMap) + : filePathToBytecode(std::move(bundleMap)) +{ +} + +static bool isBundleModule(const Luau::DenseHashMap& bundleMap, const std::string& path) +{ + // Strip @bundle/ prefix if present + std::string lookupPath = path; + if (path.rfind(kBundlePrefix, 0) == 0) + lookupPath = path.substr(kBundlePrefix.size()); + + // Check direct file match + if (bundleMap.find(lookupPath) != nullptr) + return true; + + return false; +} + +static bool isBundleDirectory(const Luau::DenseHashMap& bundleMap, const std::string& path) +{ + // Strip @bundle/ prefix if present + std::string lookupPath = path; + if (path.rfind(kBundlePrefix, 0) == 0) + lookupPath = path.substr(kBundlePrefix.size()); + + // A directory exists if any file in the bundle starts with this path followed by a slash + std::string prefix = lookupPath + "/"; + for (const auto& [filePath, _] : bundleMap) + { + if (filePath.rfind(prefix, 0) == 0) + return true; + } + + return false; +} + +NavigationStatus BundleVfs::resetToPath(const std::string& path) +{ + // Handle "@bundle" root + if (path == "@bundle") + { + modulePath = ModulePath::create( + "@bundle", + "", + [this](const std::string& p) { return isBundleModule(filePathToBytecode, p); }, + [this](const std::string& p) { return isBundleDirectory(filePathToBytecode, p); } + ); + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; + } + + // Handle "@bundle/path/to/file" + if (path.rfind(kBundlePrefix, 0) != 0) + return NavigationStatus::NotFound; + + // Strip "@bundle/" to get the actual path + std::string filePath = path.substr(kBundlePrefix.size()); + + modulePath = ModulePath::create( + "@bundle", + filePath, + [this](const std::string& p) { return isBundleModule(filePathToBytecode, p); }, + [this](const std::string& p) { return isBundleDirectory(filePathToBytecode, p); } + ); + + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; +} + +NavigationStatus BundleVfs::toParent() +{ + LUAU_ASSERT(modulePath); + return modulePath->toParent(); +} + +NavigationStatus BundleVfs::toChild(const std::string& name) +{ + LUAU_ASSERT(modulePath); + return modulePath->toChild(name); +} + +bool BundleVfs::isModulePresent() const +{ + return isBundleModule(filePathToBytecode, getIdentifier()); +} + +std::string BundleVfs::getIdentifier() const +{ + LUAU_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUAU_ASSERT(result.status == NavigationStatus::Success); + return result.realPath; +} + +std::optional BundleVfs::getContents(const std::string& path) const +{ + // Strip @bundle/ prefix if present + std::string lookupPath = path; + if (path.rfind(kBundlePrefix, 0) == 0) + lookupPath = path.substr(kBundlePrefix.size()); + + // Try direct lookup + const std::string* value = filePathToBytecode.find(lookupPath); + if (value != nullptr) + return *value; + + return std::nullopt; +} + +bool BundleVfs::isConfigPresent() const +{ + // Currently, we do not support .luaurc files in bundles. + return false; +} + +std::optional BundleVfs::getConfig() const +{ + // Currently, we do not support .luaurc files in bundles. + return std::nullopt; +} diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index 6c9938673..e50f79591 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -125,7 +125,7 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname luaL_error(L, "could not read file '%s'", loadname); // now we can compile & run module on the new thread - std::string bytecode = Luau::compile(*contents, copts()); + std::string bytecode = reqCtx->vfs.isPrecompiled() ? *contents : Luau::compile(*contents, copts()); bool errored = true; if (luau_load(ML, chunkname, bytecode.data(), bytecode.size(), 0) == 0) { @@ -199,3 +199,8 @@ RequireCtx::RequireCtx(CliVfs cliVfs) : vfs(cliVfs) { } + +RequireCtx::RequireCtx(BundleVfs bundleVfs) + : vfs(bundleVfs) +{ +} diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 921bc1692..90f4e1a06 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -1,5 +1,6 @@ #include "lute/requirevfs.h" +#include "lute/bundlevfs.h" #include "lute/modulepath.h" #include "lute/stdlibvfs.h" @@ -12,13 +13,19 @@ RequireVfs::RequireVfs(CliVfs cliVfs) { } +RequireVfs::RequireVfs(BundleVfs bundleVfs) + : bundleVfs(std::move(bundleVfs)) +{ +} + bool RequireVfs::isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const { bool isStdin = (requirerChunkname == "=stdin"); bool isFile = (!requirerChunkname.empty() && requirerChunkname[0] == '@'); bool isStdLibFile = (requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@std/"); bool isCliFile = (requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@cli/"); - return isStdin || isFile || isStdLibFile || (isCliFile && cliVfs); + bool isBundleFile = (requirerChunkname.size() >= 9 && requirerChunkname.substr(0, 9) == "@@bundle/"); + return isStdin || isFile || isStdLibFile || (isCliFile && cliVfs) || (isBundleFile && bundleVfs); } NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkname) @@ -38,6 +45,13 @@ NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkn return cliVfs->resetToPath(std::string(requirerChunkname.substr(1))); } + if ((requirerChunkname.size() >= 9 && requirerChunkname.substr(0, 9) == "@@bundle/")) + { + vfsType = VFSType::Bundle; + LUAU_ASSERT(bundleVfs); + return bundleVfs->resetToPath(std::string(requirerChunkname.substr(1))); + } + vfsType = VFSType::Disk; if (requirerChunkname == "=stdin") return fileVfs.resetToStdIn(); @@ -76,6 +90,10 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) LUAU_ASSERT(cliVfs); status = cliVfs->resetToPath(std::string(path)); break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + status = bundleVfs->resetToPath(std::string(path)); + break; case VFSType::Lute: break; } @@ -98,6 +116,10 @@ NavigationStatus RequireVfs::toParent(lua_State* L) LUAU_ASSERT(cliVfs); status = cliVfs->toParent(); break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + status = bundleVfs->toParent(); + break; case VFSType::Lute: luaL_error(L, "cannot get the parent of @lute"); break; @@ -128,6 +150,9 @@ NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) case VFSType::Cli: LUAU_ASSERT(cliVfs); return cliVfs->toChild(std::string(name)); + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + return bundleVfs->toChild(std::string(name)); case VFSType::Lute: luaL_error(L, "'%s' is not a lute library", std::string(name).c_str()); break; @@ -147,6 +172,9 @@ bool RequireVfs::isModulePresent(lua_State* L) const case VFSType::Cli: LUAU_ASSERT(cliVfs); return cliVfs->isModulePresent(); + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + return bundleVfs->isModulePresent(); case VFSType::Lute: luaL_error(L, "@lute is not requirable"); break; @@ -171,6 +199,10 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c LUAU_ASSERT(cliVfs); contents = cliVfs->getContents(loadname); break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + contents = bundleVfs->getContents(loadname); + break; case VFSType::Lute: break; } @@ -193,6 +225,10 @@ std::string RequireVfs::getChunkname(lua_State* L) const LUAU_ASSERT(cliVfs); chunkname = "@" + cliVfs->getIdentifier(); break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + chunkname = "@" + bundleVfs->getIdentifier(); + break; case VFSType::Lute: break; } @@ -214,6 +250,10 @@ std::string RequireVfs::getLoadname(lua_State* L) const LUAU_ASSERT(cliVfs); loadname = cliVfs->getIdentifier(); break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + loadname = bundleVfs->getIdentifier(); + break; case VFSType::Lute: break; } @@ -235,6 +275,10 @@ std::string RequireVfs::getCacheKey(lua_State* L) const LUAU_ASSERT(cliVfs); cacheKey = cliVfs->getIdentifier(); break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + cacheKey = bundleVfs->getIdentifier(); + break; case VFSType::Lute: break; } @@ -259,6 +303,10 @@ bool RequireVfs::isConfigPresent(lua_State* L) const LUAU_ASSERT(cliVfs); isPresent = cliVfs->isConfigPresent(); break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + isPresent = bundleVfs->isConfigPresent(); + break; case VFSType::Lute: break; } @@ -291,6 +339,10 @@ std::string RequireVfs::getConfig(lua_State* L) const LUAU_ASSERT(cliVfs); configContents = cliVfs->getConfig(); break; + case VFSType::Bundle: + LUAU_ASSERT(bundleVfs); + configContents = bundleVfs->getConfig(); + break; case VFSType::Lute: break; } From abe1a07b5ba74eda3ca3ce7c4177d98fac7b5177 Mon Sep 17 00:00:00 2001 From: ariel Date: Thu, 23 Oct 2025 13:05:34 -0700 Subject: [PATCH 073/642] std/json: distinguish more explicitly between array and object (#468) We got some useful feedback from @vocksel about not being able to get a clean refinement between array and object with the existing types setup, and the correct solution to this problem here is for us to leverage another newproxy as a key to distinguish between objects and arrays. We've provided some additional functions to the interface of `json` both to construct `object`s and to test against values being `object`s or `array`s. There's also some clean up in here for json as a library generally, including correcting its casing now that it's a standard library and including `object` and `array` as part of the exported types as well. --- lute/std/libs/json.luau | 72 +++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 4c600f165..414d64ddd 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -1,12 +1,14 @@ local json = { --- Not actually a nil value, a newproxy stand-in for a null value since Luau has no actual representation of `null` - NULL = newproxy() :: nil, + null = newproxy() :: nil, } -type JSONPrimitive = nil | number | string | boolean -type Object = { [string]: Value } -type Array = { Value } -export type Value = JSONPrimitive | Array | Object +export type object = { [string]: value } +export type array = { [number]: value } +export type value = nil | number | string | boolean | array | object + +-- a unique key to identify an object vs a table +local object_key = newproxy() -- serialization @@ -66,7 +68,7 @@ local function writeString(state: SerializerState, str: string) state.cursor += #str end -local serializeAny: (SerializerState, (Array | Object | boolean | number | string)?) -> () +local serializeAny: (SerializerState, (array | object | boolean | number | string)?) -> () local ESCAPE_MAP = { [0x5C] = string.byte("\\"), -- 5C = '\' @@ -107,7 +109,7 @@ local function serializeString(state: SerializerState, str: string) writeByte(state, string.byte('"')) end -local function serializeArray(state: SerializerState, array: Array) +local function serializeArray(state: SerializerState, array: array) state.depth += 1 writeByte(state, string.byte("[")) @@ -142,7 +144,7 @@ local function serializeArray(state: SerializerState, array: Array) writeByte(state, string.byte("]")) end -local function serializeTable(state: SerializerState, object: Object) +local function serializeTable(state: SerializerState, object: object) writeByte(state, string.byte("{")) if state.prettyPrint then @@ -153,6 +155,10 @@ local function serializeTable(state: SerializerState, object: Object) local first = true for key, value in object do + if key == object_key then + continue + end + if not first then writeByte(state, string.byte(",")) writeByte(state, string.byte(" ")) @@ -184,10 +190,10 @@ local function serializeTable(state: SerializerState, object: Object) writeByte(state, string.byte("}")) end -serializeAny = function(state: SerializerState, value: Value) +serializeAny = function(state: SerializerState, value: value) local valueType = type(value) - if value == json.NULL then + if value == json.null then writeString(state, "null") elseif valueType == "boolean" then writeString(state, if value then "true" else "false") @@ -197,9 +203,9 @@ serializeAny = function(state: SerializerState, value: Value) serializeString(state, value :: string) elseif valueType == "table" then if #(value :: {}) == 0 and next(value :: { [unknown]: unknown }) ~= nil then - serializeTable(state, value :: Object) + serializeTable(state, value :: object) else - serializeArray(state, value :: Array) + serializeArray(state, value :: array) end else error("Unknown value", 2) @@ -329,12 +335,12 @@ local function deserializeString(state: DeserializerState): string return deserializerError(state, "Unterminated string") end -local deserialize: (DeserializerState) -> (Array | Object | boolean | number | string)? +local deserialize: (DeserializerState) -> (array | object | boolean | number | string)? -local function deserializeArray(state: DeserializerState): Array +local function deserializeArray(state: DeserializerState): array state.cursor += 1 - local current: Array = {} + local current: array = {} local expectingValue = false while state.cursor < #state.src do @@ -369,7 +375,7 @@ local function deserializeArray(state: DeserializerState): Array return current end -local function deserializeObject(state: DeserializerState): Object +local function deserializeObject(state: DeserializerState): object state.cursor += 1 local current = {} @@ -429,14 +435,14 @@ local function deserializeObject(state: DeserializerState): Object return current end -deserialize = function(state: DeserializerState): Value +deserialize = function(state: DeserializerState): value skipWhitespace(state) local fourChars = string.sub(state.src, state.cursor, state.cursor + 3) if fourChars == "null" then state.cursor += 4 - return json.NULL + return json.null elseif fourChars == "true" then state.cursor += 4 return true @@ -459,7 +465,7 @@ end -- user-facing -json.serialize = function(value: Value, prettyPrint: boolean?) +function json.serialize(value: value, prettyPrint: boolean?) local state: SerializerState = { buf = buffer.create(1024), cursor = 0, @@ -472,7 +478,7 @@ json.serialize = function(value: Value, prettyPrint: boolean?) return buffer.readstring(state.buf, 0, state.cursor) end -json.deserialize = function(src: string) +function json.deserialize(src: string) local state = { src = src, cursor = 0, @@ -481,4 +487,30 @@ json.deserialize = function(src: string) return deserialize(state) end +function json.object(props: { [string]: value }): object + local res: object = { [object_key] = true } + + for key, value in props do + res[key] = value + end + + return res +end + +function json.asobject(value: value): object? + if typeof(value) == "table" and value[object_key] then + return value :: object + end + + return nil +end + +function json.asarray(value: value): array? + if typeof(value) == "table" and not value[object_key] then + return value :: array + end + + return nil +end + return table.freeze(json) From 6ce00f1439f4da8717cf9282641b79cbe20887da Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:36:54 -0700 Subject: [PATCH 074/642] stdlib: add `std/fs` and `std/process` to handle file paths as luau `path` and `pathlike` types instead of `string` (#462) ### Problem Some Lute `fs` and `process` functions have been processing file paths as `string` types while other places in Lute have been using `path` or `pathlike` for file path handling/processing. We want to add support for `path` and `pathlike` file paths to `fs` and `process` functions. ### Solution We're introducing `fs` and `process` to the Lute standard library as `@std/fs` and `@std/process` that act as wrappers around the existing Lute `fs` and `process` files where we can pass in `pathlike` type paths in `@std/fs` and return `path` type paths in `@std/process` instead of needing to parse and format each call. There's also a refactor of the `path/` directory that separates out type definitions into a separate file outside of the `init`. This was partially due to a previous attempt of handling path types and running into cyclic dependencies due to the nature of the `process.luau` file, but seemed reasonable to keep around for potential future use. Added tests and found some minor bugs in `process.cpp` that is also fixed in this pr --- docs/scripts/reference.luau | 4 +- examples/async_read.luau | 2 +- examples/directories.luau | 2 +- examples/homedir_cwd.luau | 2 +- examples/linter.luau | 2 +- examples/process.luau | 2 +- examples/process_env.luau | 2 +- examples/writeFile.luau | 2 +- lute/cli/commands/setup/init.luau | 4 +- lute/process/src/process.cpp | 18 ++- lute/std/libs/fs.luau | 110 ++++++++++++++ lute/std/libs/path/init.luau | 5 +- .../libs/path/{posix.luau => posix/init.luau} | 9 +- lute/std/libs/path/posix/types.luau | 8 + lute/std/libs/path/types.luau | 7 + .../libs/path/{win32.luau => win32/init.luau} | 13 +- lute/std/libs/path/win32/types.luau | 11 ++ lute/std/libs/process.luau | 42 ++++++ lute/std/libs/test.luau | 2 +- tests/stdfs.test.luau | 138 ++++++++++++++++++ tests/stdprocess.test.luau | 51 +++++++ tests/testAstSerializer.test.luau | 2 +- 22 files changed, 404 insertions(+), 34 deletions(-) create mode 100644 lute/std/libs/fs.luau rename lute/std/libs/path/{posix.luau => posix/init.luau} (97%) create mode 100644 lute/std/libs/path/posix/types.luau create mode 100644 lute/std/libs/path/types.luau rename lute/std/libs/path/{win32.luau => win32/init.luau} (97%) create mode 100644 lute/std/libs/path/win32/types.luau create mode 100644 lute/std/libs/process.luau create mode 100644 tests/stdfs.test.luau create mode 100644 tests/stdprocess.test.luau diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 7ec155498..d162cdb93 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -1,5 +1,5 @@ -local fs = require("@lute/fs") -local process = require("@lute/process") +local fs = require("@std/fs") +local process = require("@std/process") local function projectRelative(...) local cwd = process.cwd() diff --git a/examples/async_read.luau b/examples/async_read.luau index ba0abc2df..3038459a9 100644 --- a/examples/async_read.luau +++ b/examples/async_read.luau @@ -1,4 +1,4 @@ -local fs = require("@lute/fs") +local fs = require("@std/fs") local task = require("@std/task") -- blocking diff --git a/examples/directories.luau b/examples/directories.luau index 7f968b948..34c73bb27 100644 --- a/examples/directories.luau +++ b/examples/directories.luau @@ -1,4 +1,4 @@ -local fs = require("@lute/fs") +local fs = require("@std/fs") for _, file in fs.listdir("./examples") do print(`Example {file.name} is a {file.type}`) diff --git a/examples/homedir_cwd.luau b/examples/homedir_cwd.luau index 7ed6a2669..1d757baf0 100644 --- a/examples/homedir_cwd.luau +++ b/examples/homedir_cwd.luau @@ -1,3 +1,3 @@ -local process = require("@lute/process") +local process = require("@std/process") print(process.cwd(), process.homedir()) diff --git a/examples/linter.luau b/examples/linter.luau index f3cc1dd92..bc0928021 100644 --- a/examples/linter.luau +++ b/examples/linter.luau @@ -1,4 +1,4 @@ -local fs = require("@lute/fs") +local fs = require("@std/fs") local luau = require("@lute/luau") local pp = require("@batteries/pp") diff --git a/examples/process.luau b/examples/process.luau index aa1bc46a7..b2825d4c4 100644 --- a/examples/process.luau +++ b/examples/process.luau @@ -1,4 +1,4 @@ -local process = require("@lute/process") +local process = require("@std/process") local task = require("@std/task") local result = process.run({ "echo", "Hello, lute!" }) diff --git a/examples/process_env.luau b/examples/process_env.luau index a1d8aa673..edb2c680d 100644 --- a/examples/process_env.luau +++ b/examples/process_env.luau @@ -1,4 +1,4 @@ -local process = require("@lute/process") +local process = require("@std/process") print(process.env.HOME) print(process.env.LUTE_HOME) diff --git a/examples/writeFile.luau b/examples/writeFile.luau index c02b49918..d01485781 100644 --- a/examples/writeFile.luau +++ b/examples/writeFile.luau @@ -1,4 +1,4 @@ -local fs = require("@lute/fs") +local fs = require("@std/fs") -- Open a file if it doesn't exist, truncate it, local file = fs.open("dest", "w+") diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index c92cbb3da..430e65671 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -1,6 +1,6 @@ local definitions = require("@self/generated/definitions") -local fs = require("@lute/fs") -local process = require("@lute/process") +local fs = require("@std/fs") +local process = require("@std/process") local json = require("@std/json") local BASE_PATH = ".lute/typedefs/0.1.0" diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index ef27533a4..a3bb43cea 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -293,16 +293,23 @@ int run(lua_State* L) const char* shellArg = "-c"; #endif - const char* shell = customShell.empty() ? nullptr : customShell.c_str(); - if (!shell) + std::string resolvedShell; + if (customShell.empty()) { char shellBuffer[1024]; size_t shellSize = sizeof(shellBuffer); int result = uv_os_getenv(shellVar, shellBuffer, &shellSize); - shell = result == 0 ? shellBuffer : shellFallback; + resolvedShell = result == 0 ? shellBuffer : shellFallback; + } + else + { + resolvedShell = customShell; } - args = {shell, shellArg, commandStr}; + args.clear(); + args.emplace_back(resolvedShell); + args.emplace_back(shellArg); + args.emplace_back(commandStr); } auto handle = std::make_shared(); @@ -332,6 +339,7 @@ int run(lua_State* L) if (err != 0) { luaL_error(L, "Failed to get current environment: %s", uv_strerror(err)); + uv_os_free_environ(currentEnvItems, currentEnvCount); return 0; } for (int i = 0; i < currentEnvCount; i++) @@ -341,6 +349,8 @@ int run(lua_State* L) env[currentEnvItems[i].name] = currentEnvItems[i].value; } } + uv_os_free_environ(currentEnvItems, currentEnvCount); + // Turn the new environment into a char** array envStrings.reserve(env.size()); envPtr.reserve(env.size() + 1); diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau new file mode 100644 index 000000000..1cc822d40 --- /dev/null +++ b/lute/std/libs/fs.luau @@ -0,0 +1,110 @@ +--!strict +-- @std/fs +-- stdlib for `@lute/fs` +-- Provides file system utility functions that handles file paths as path types instead of strings + +local fs = require("@lute/fs") +local pathlib = require("@std/path") + +export type handlemode = fs.HandleMode +export type filehandle = fs.FileHandle +export type filetype = fs.FileType +export type filemetadata = fs.FileMetadata +export type directoryentry = fs.DirectoryEntry +export type watchhandle = fs.WatchHandle +export type watchevent = fs.WatchEvent +export type pathlike = pathlib.pathlike + +local function open(path: pathlike, mode: handlemode?): filehandle + return fs.open(pathlib.format(path), mode) +end + +local function read(handle: filehandle): string + return fs.read(handle) +end + +local function write(handle: filehandle, contents: string): () + return fs.write(handle, contents) +end + +local function close(handle: filehandle): () + return fs.close(handle) +end + +local function remove(path: pathlike): () + return fs.remove(pathlib.format(path)) +end + +local function stat(path: pathlike): filemetadata + return fs.stat(pathlib.format(path)) +end + +local function type(path: pathlike): filetype + return fs.type(pathlib.format(path)) +end + +local function mkdir(path: pathlike): () + return fs.mkdir(pathlib.format(path)) +end + +local function link(src: pathlike, dest: pathlike): () + return fs.link(pathlib.format(src), pathlib.format(dest)) +end + +local function symlink(src: pathlike, dest: pathlike): () + -- make sure our path is absolute path for symlink and if your os is + return fs.symlink(pathlib.format(src), pathlib.format(dest)) +end + +local function watch(path: pathlike, callback: (filename: pathlike, event: watchevent) -> ()): watchhandle + return fs.watch(pathlib.format(path), callback) +end + +local function exists(path: pathlike): boolean + return fs.exists(pathlib.format(path)) +end + +local function copy(src: pathlike, dest: pathlike): () + return fs.copy(pathlib.format(src), pathlib.format(dest)) +end + +local function listdir(path: pathlike): { directoryentry } + return fs.listdir(pathlib.format(path)) +end + +local function rmdir(path: pathlike): () + return fs.rmdir(pathlib.format(path)) +end + +local function readfiletostring(filepath: pathlike): string + return fs.readfiletostring(pathlib.format(filepath)) +end + +local function writestringtofile(filepath: pathlike, contents: string): () + return fs.writestringtofile(pathlib.format(filepath), contents) +end + +local function readasync(filepath: pathlike): string + return fs.readasync(pathlib.format(filepath)) +end + +return table.freeze({ + open = open, + read = read, + write = write, + close = close, + remove = remove, + stat = stat, + type = type, + mkdir = mkdir, + link = link, + symlink = symlink, + watch = watch, + exists = exists, + copy = copy, + listdir = listdir, + rmdir = rmdir, + readfiletostring = readfiletostring, + writestringtofile = writestringtofile, + readasync = readasync, +}) diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau index df0fd6ca9..7082e339a 100644 --- a/lute/std/libs/path/init.luau +++ b/lute/std/libs/path/init.luau @@ -3,9 +3,10 @@ local system = require("@std/system") local posix = require("@self/posix") local win32 = require("@self/win32") -export type path = posix.path | win32.path +local pathtypes = require("@self/types") -export type pathlike = string | path +export type path = pathtypes.path +export type pathlike = pathtypes.pathlike local onWindows = system.win32 diff --git a/lute/std/libs/path/posix.luau b/lute/std/libs/path/posix/init.luau similarity index 97% rename from lute/std/libs/path/posix.luau rename to lute/std/libs/path/posix/init.luau index f3f04f4c1..3c14eeba0 100644 --- a/lute/std/libs/path/posix.luau +++ b/lute/std/libs/path/posix/init.luau @@ -1,11 +1,8 @@ local process = require("@lute/process") +local posixtypes = require("@self/types") -export type path = { - parts: { string }, - absolute: boolean, -} - -export type pathlike = string | path +export type path = posixtypes.path +export type pathlike = posixtypes.pathlike local posix = {} diff --git a/lute/std/libs/path/posix/types.luau b/lute/std/libs/path/posix/types.luau new file mode 100644 index 000000000..8f921ecd1 --- /dev/null +++ b/lute/std/libs/path/posix/types.luau @@ -0,0 +1,8 @@ +export type path = { + parts: { string }, + absolute: boolean, +} + +export type pathlike = string | path + +return {} diff --git a/lute/std/libs/path/types.luau b/lute/std/libs/path/types.luau new file mode 100644 index 000000000..08cf91461 --- /dev/null +++ b/lute/std/libs/path/types.luau @@ -0,0 +1,7 @@ +local posixtypes = require("@std/path/posix/types") +local win32types = require("@std/path/win32/types") + +export type path = posixtypes.path | win32types.path +export type pathlike = string | path + +return {} diff --git a/lute/std/libs/path/win32.luau b/lute/std/libs/path/win32/init.luau similarity index 97% rename from lute/std/libs/path/win32.luau rename to lute/std/libs/path/win32/init.luau index deda728ce..6bb8da26f 100644 --- a/lute/std/libs/path/win32.luau +++ b/lute/std/libs/path/win32/init.luau @@ -1,14 +1,9 @@ local process = require("@lute/process") +local win32types = require("@self/types") -export type pathkind = "unc" | "absolute" | "relative" - -export type path = { - parts: { string }, - kind: pathkind, - driveLetter: string?, -} - -export type pathlike = string | path +export type pathkind = win32types.pathkind +export type path = win32types.path +export type pathlike = win32types.pathlike local win32 = {} diff --git a/lute/std/libs/path/win32/types.luau b/lute/std/libs/path/win32/types.luau new file mode 100644 index 000000000..e8b5d504d --- /dev/null +++ b/lute/std/libs/path/win32/types.luau @@ -0,0 +1,11 @@ +export type pathkind = "unc" | "absolute" | "relative" + +export type path = { + parts: { string }, + kind: pathkind, + driveLetter: string?, +} + +export type pathlike = string | path + +return {} diff --git a/lute/std/libs/process.luau b/lute/std/libs/process.luau new file mode 100644 index 000000000..7188c9169 --- /dev/null +++ b/lute/std/libs/process.luau @@ -0,0 +1,42 @@ +--!strict +-- @std/process +-- stdlib for `@lute/process` +-- Provides process-related utility functions that handles file paths as path types instead of strings + +local process = require("@lute/process") +local pathlib = require("@std/path") + +export type stdiokind = process.StdioKind +export type processrunoptions = process.ProcessRunOptions +export type processresult = process.ProcessResult +export type path = pathlib.path +export type pathlike = pathlib.pathlike + +local function homedir(): path + return pathlib.parse(process.homedir()) +end + +local function cwd(): path + return pathlib.parse(process.cwd()) +end + +local function run(args: string | { string }, options: processrunoptions?): processresult + return process.run(args, options) +end + +local function exit(exitcode: number): never + return process.exit(exitcode) +end + +local function execpath(): path + return pathlib.parse(process.execpath()) +end + +return table.freeze({ + env = process.env, + homedir = homedir, + cwd = cwd, + run = run, + exit = exit, + execpath = execpath, +}) diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index 00a7fe3e9..91d31e096 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -1,5 +1,5 @@ --!strict -local ps = require("@lute/process") +local ps = require("@std/process") local assert = require("@std/assert") -- Test Types type Test = (assert.Asserts) -> () diff --git a/tests/stdfs.test.luau b/tests/stdfs.test.luau new file mode 100644 index 000000000..3e56187b6 --- /dev/null +++ b/tests/stdfs.test.luau @@ -0,0 +1,138 @@ +local test = require("@std/test") +local path = require("@std/path") +local fs = require("@std/fs") +local tmpdir = "tmp" + +test.suite("FsSuite", function(suite) + suite:beforeall(function() + if not fs.exists(tmpdir) then + fs.mkdir(tmpdir) + end + end) + + suite:afterall(function() + fs.rmdir(tmpdir) + end) + + suite:case("open_read_write_close_and_stat", function(assert) + local file = path.join(tmpdir, "file.txt") + + local h = fs.open(file, "w+") + assert.eq(fs.exists(file), true) + fs.write(h, "abc") + fs.close(h) + + local m = fs.stat(file) + assert.eq(m.size, 3) + + local hr = fs.open(file, "r") + assert.eq(fs.read(hr), "abc") + fs.close(hr) + + fs.remove(file) + end) + + suite:case("writestring_and_readfile", function(assert) + local file = path.join(tmpdir, "hello.txt") + fs.writestringtofile(file, "hello lute") + + local contents = fs.readfiletostring(file) + assert.eq(contents, "hello lute") + + fs.remove(file) + end) + + suite:case("exists_and_type_file_and_dir", function(assert) + local file = path.join(tmpdir, "roblox.txt") + fs.writestringtofile(file, "roblox") + + assert.eq(fs.exists(file), true) + assert.eq(fs.type(file), "file") + assert.eq(fs.exists(tmpdir), true) + assert.eq(fs.type(tmpdir), "dir") + + fs.remove(file) + end) + + suite:case("mkdir_listdir_rmdir", function(assert) + local dir = path.join(tmpdir, "subdir") + if not fs.exists(dir) then + fs.mkdir(dir) + end + + assert.eq(fs.exists(dir), true) + assert.eq(fs.type(dir), "dir") + + local entries = fs.listdir(tmpdir) + local found = false + for _, e in entries do + if e.name == "subdir" then + found = true + end + end + assert.eq(found, true) + fs.rmdir(dir) + end) + + suite:case("readasync_and_writestring", function(assert) + local file = path.join(tmpdir, "async.txt") + fs.writestringtofile(file, "async content") + + local got = fs.readasync(file) + assert.eq(got, "async content") + + fs.remove(file) + end) + + suite:case("link_and_symlink_and_copy", function(assert) + local srcfile = "src.txt" + local src = path.join(tmpdir, srcfile) + fs.writestringtofile(src, "linkcontent") + + local dstlink = path.join(tmpdir, "dstlink") + fs.link(src, dstlink) + assert.eq(fs.readfiletostring(dstlink), "linkcontent") + fs.remove(dstlink) + + local dstcopy = path.join(tmpdir, "dstcopy.txt") + fs.copy(src, dstcopy) + assert.eq(fs.readfiletostring(dstcopy), "linkcontent") + fs.remove(dstcopy) + + local dstsymlink = path.join(tmpdir, "dstsymlink") + fs.symlink(srcfile, dstsymlink) + assert.eq(fs.readfiletostring(dstsymlink), "linkcontent") + fs.remove(dstsymlink) + + fs.remove(src) + end) + + suite:case("watch_callback_on_change", function(assert) + local watched = path.join(tmpdir, "watched.txt") + if fs.exists(watched) then + fs.remove(watched) + end + + local triggered = false + local handle = fs.watch(tmpdir, function(filename, event) + if tostring(filename):find("watched.txt") then + triggered = true + end + end) + + fs.writestringtofile(watched, "x") + + local start = os.time() + while not triggered and os.time() - start < 2 do + end + + assert.eq(type(triggered) == "boolean", true) + + handle:close() + if fs.exists(watched) then + fs.remove(watched) + end + end) +end) + +test.run() diff --git a/tests/stdprocess.test.luau b/tests/stdprocess.test.luau new file mode 100644 index 000000000..86574f2b9 --- /dev/null +++ b/tests/stdprocess.test.luau @@ -0,0 +1,51 @@ +local test = require("@std/test") +local process = require("@std/process") +local pathlib = require("@std/path") + +test.suite("ProcessSuite", function(suite) + suite:case("homedir_and_cwd_and_execpath", function(assert) + local hd = process.homedir() + assert.neq(pathlib.format(hd), "") + + local cwd = process.cwd() + assert.neq(pathlib.format(cwd), "") + + local ex = process.execpath() + assert.neq(pathlib.format(ex), "") + end) + + suite:case("run_echo_array", function(assert) + local r = process.run({ "echo", "hello-from-lute" }) + assert.eq(r.stdout, "hello-from-lute\n") + end) + + suite:case("run_shell_and_cwd", function(assert) + local r = process.run({ "pwd" }, { cwd = "/" }) + assert.tableeq(r, { + exitcode = 0, + stdout = "/\n", + stderr = "", + ok = true, + }) + + local r2 = process.run({ "echo hello!" }, { shell = true }) + assert.tableeq(r2, { + exitcode = 0, + stdout = "hello!\n", + stderr = "", + ok = true, + }) + end) + + suite:case("run_nonzero_exitcode", function(assert) + local r = process.run({ "sh", "-c", "exit 41" }) + assert.tableeq(r, { + exitcode = 41, + stdout = "", + stderr = "", + ok = false, + }) + end) +end) + +test.run() diff --git a/tests/testAstSerializer.test.luau b/tests/testAstSerializer.test.luau index 8542c931c..f4dbfb45b 100644 --- a/tests/testAstSerializer.test.luau +++ b/tests/testAstSerializer.test.luau @@ -1,5 +1,5 @@ local asserts = require("@std/assert") -local fs = require("@lute/fs") +local fs = require("@std/fs") local luau = require("@lute/luau") local path = require("@std/path") local test = require("@std/test") From 717977c3372790890ba1637ebbbd6614d68d6248 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:54:59 -0700 Subject: [PATCH 075/642] docs fix: use lute lib instead of std for `reference.luau` (#471) reverting the `fs` and `process` of `reference.luau` for docs gen to use `@lute` instead of `@std` because it uses `table.concat` on the `string` version of cwd fixes failing build for docs reverted both just for consistency in the requires for this file (that will hopefully be replaced with another version of doc gen soon) --- docs/scripts/reference.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index d162cdb93..7ec155498 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -1,5 +1,5 @@ -local fs = require("@std/fs") -local process = require("@std/process") +local fs = require("@lute/fs") +local process = require("@lute/process") local function projectRelative(...) local cwd = process.cwd() From 6cd4c996d3400471b9979f7421440eac603de321 Mon Sep 17 00:00:00 2001 From: ariel Date: Fri, 24 Oct 2025 11:41:17 -0700 Subject: [PATCH 076/642] infra: set LUTE_ROOT_DIR in CI to the checkout (#476) We suddenly started getting CI failures for `getSourceRoot`, but we can set the environment variable instead to make sure it works consistently. --- .github/actions/setup-and-build/action.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index def71560d..07ffa1ebf 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -25,6 +25,10 @@ outputs: runs: using: "composite" steps: + - name: Set checkout path as LUTE_ROOT_DIR + shell: bash + run: echo "LUTE_ROOT_DIR=$(pwd)" >> $GITHUB_ENV + - name: Install tools uses: Roblox/setup-foreman@v3 with: From 225d10a9ea1ac9394670fe25b867cd50c149231d Mon Sep 17 00:00:00 2001 From: nerix Date: Fri, 24 Oct 2025 20:52:53 +0200 Subject: [PATCH 077/642] fix: clang-cl build (#474) Fixes the build to work with clang-cl. - `clang-cl` errored out on the generated strings, because they had `\r` at the end (from the default Windows line endings[^1]). - `/Zc:externConstexpr` is unsupported/not needed on `clang-cl` [^1]: Might be worth to have something like a `splitlines` that works with both LF and CRLF. Co-authored-by: ariel --- CMakeLists.txt | 5 +++-- tools/luthier.luau | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b56cea21d..c3f4a2489 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,8 +110,9 @@ if(MSVC) list(APPEND LUTE_OPTIONS /D_CRT_SECURE_NO_WARNINGS) # We need to use the portable CRT functions. list(APPEND LUTE_OPTIONS "/we4018") # Signed/unsigned mismatch list(APPEND LUTE_OPTIONS "/we4388") # Also signed/unsigned mismatch - list(APPEND LUTE_OPTIONS "/Zc:externConstexpr") # MSVC does not comply with C++ standard by default - if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + list(APPEND LUTE_OPTIONS "/Zc:externConstexpr") # MSVC does not comply with C++ standard by default + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") list(APPEND LUTE_OPTIONS "/EHsc") # The CMake clang-cl target doesn't enable exceptions by default endif() # FIXME: list(APPEND LUTE_OPTIONS /WX) # Warnings are errors diff --git a/tools/luthier.luau b/tools/luthier.luau index 5cd6f2a03..81e780641 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -302,6 +302,7 @@ local function generateStdLibFilesIfNeeded() for _, line in lines do local escapedLine = string.gsub(line, "\\", "\\\\") escapedLine = string.gsub(escapedLine, '"', '\\"') + escapedLine = string.gsub(escapedLine, "\r", "") table.insert(generatedLines, `"{escapedLine}\\n"`) end fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) @@ -483,6 +484,7 @@ local function generateCliCommandsFilesIfNeeded() for _, line in lines do local escapedLine = string.gsub(line, "\\", "\\\\") escapedLine = string.gsub(escapedLine, '"', '\\"') + escapedLine = string.gsub(escapedLine, "\r", "") table.insert(generatedLines, `"{escapedLine}\\n"`) end fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) From b6c90ba6709314201ab3011dabf4c575885b7cf0 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 24 Oct 2025 11:54:45 -0700 Subject: [PATCH 078/642] feature: Implement a Static Require Tracer in order to bundle files correctly when bytecode compiling them. (#452) This PR implement a static require tracer for Luau as a prerequisite to enabling multi file bytecode compilation. This module statically resolves requires relative to a given entry module, effectively snapshotting the state of the filesystem for embedding in a lute executable. This module graph will be embedded in the lute executable when `lute compile` is invoked and will let the bundle virtual file system [PR](https://github.com/luau-lang/lute/pull/450) correctly navigate the directory structure to resolve `require` calls and match up those paths to the pre-compiled bytecode embedded in the bundle. --- lute/cli/CMakeLists.txt | 2 + lute/cli/include/lute/staticrequires.h | 35 ++++ lute/cli/src/staticrequires.cpp | 199 +++++++++++++++++++++++ tests/CMakeLists.txt | 3 +- tests/src/staticrequires.test.cpp | 128 +++++++++++++++ tests/src/staticrequires/circular_a.luau | 7 + tests/src/staticrequires/circular_b.luau | 7 + tests/src/staticrequires/lib/helper.luau | 10 ++ tests/src/staticrequires/main.luau | 9 + tests/src/staticrequires/shared.luau | 5 + tests/src/staticrequires/utils.luau | 7 + 11 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 lute/cli/include/lute/staticrequires.h create mode 100644 lute/cli/src/staticrequires.cpp create mode 100644 tests/src/staticrequires.test.cpp create mode 100644 tests/src/staticrequires/circular_a.luau create mode 100644 tests/src/staticrequires/circular_b.luau create mode 100644 tests/src/staticrequires/lib/helper.luau create mode 100644 tests/src/staticrequires/main.luau create mode 100644 tests/src/staticrequires/shared.luau create mode 100644 tests/src/staticrequires/utils.luau diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 389e2733a..b438b9a85 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -25,11 +25,13 @@ add_library(Lute.CLI.lib STATIC) target_sources(Lute.CLI.lib PRIVATE include/lute/climain.h include/lute/compile.h + include/lute/staticrequires.h include/lute/tc.h include/lute/uvstate.h src/climain.cpp src/compile.cpp + src/staticrequires.cpp src/tc.cpp src/uvstate.cpp ) diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h new file mode 100644 index 000000000..35f6dccf2 --- /dev/null +++ b/lute/cli/include/lute/staticrequires.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Luau/DenseHash.h" + +#include +#include +#include + +class StaticRequireTracer +{ +public: + StaticRequireTracer(); + + // Trace dependencies starting from an entry point file + // rootDirectory: Base directory for resolving all requires + // entryPoint: Path to entry point file (relative to rootDirectory) + // Returns list of all files in dependency order (entry point first) + std::vector trace(const std::string& rootDirectory, const std::string& entryPoint); + + // Get the require graph built during the last trace + // Maps each file to the list of files it requires + const Luau::DenseHashMap>& getRequireGraph() const { return requireGraph; } + +private: + Luau::DenseHashSet visited{""}; + std::vector discovered; + Luau::DenseHashMap> requireGraph{""}; + std::string rootDirectory; + + // Extract all require() paths from source code + std::vector extractRequires(const std::string& source); + + // Resolve a require path relative to the requiring file + std::optional resolveRequire(const std::string& requirer, const std::string& required); +}; diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp new file mode 100644 index 000000000..c3562e057 --- /dev/null +++ b/lute/cli/src/staticrequires.cpp @@ -0,0 +1,199 @@ +#include "lute/modulepath.h" +#include "lute/staticrequires.h" + +#include "Luau/Ast.h" +#include "Luau/Parser.h" +#include "Luau/FileUtils.h" +#include "Luau/VecDeque.h" + +#include + +StaticRequireTracer::StaticRequireTracer() = default; + +// AST visitor to extract require() calls +class RequireExtractor : public Luau::AstVisitor +{ +public: + std::vector requirePaths; + + bool visit(Luau::AstExprCall* call) override + { + if (auto global = call->func->as()) + { + if (global->name == "require" && call->args.size > 0) + { + if (auto str = call->args.data[0]->as()) + { + requirePaths.emplace_back(str->value.data, str->value.size); + } + } + } + return true; + } +}; + +std::vector StaticRequireTracer::trace(const std::string& rootDirectory, const std::string& entryPoint) +{ + visited.clear(); + discovered.clear(); + requireGraph.clear(); + this->rootDirectory = rootDirectory; + + Luau::VecDeque toProcess; + toProcess.push_back(entryPoint); + + while (!toProcess.empty()) + { + std::string filePath = toProcess.front(); + toProcess.pop_front(); + + // Get normalized path for visited tracking + std::string fullPath = joinPaths(rootDirectory, filePath); + std::string absPath = normalizePath(fullPath); + + // Skip if already visited (handles circular dependencies) + if (visited.contains(absPath)) + continue; + + visited.insert(absPath); + std::optional source = readFile(fullPath); + if (!source) + { + fprintf(stderr, "Warning: Could not read file '%s'\n", fullPath.c_str()); + continue; + } + + // Add to discovered list (use the relative path from rootDirectory) + discovered.push_back(filePath); + + std::vector requiresInFile = extractRequires(*source); + + std::vector resolvedDeps; + + for (const auto& req : requiresInFile) + { + std::optional resolvedPath = resolveRequire(filePath, req); + if (resolvedPath) + { + toProcess.push_back(*resolvedPath); + resolvedDeps.push_back(*resolvedPath); + } + else + { + fprintf(stderr, "Warning: Could not resolve require('%s') from '%s'\n", req.c_str(), filePath.c_str()); + } + } + + // Store the resolved dependencies in the graph + requireGraph[filePath] = std::move(resolvedDeps); + } + + return discovered; +} + +std::vector StaticRequireTracer::extractRequires(const std::string& source) +{ + Luau::Allocator allocator; + Luau::AstNameTable names(allocator); + + Luau::ParseOptions options; + Luau::ParseResult result = Luau::Parser::parse(source.c_str(), source.size(), names, allocator, options); + + if (result.errors.size() > 0) + { + // Even with parse errors, we can still try to extract requires + // Parse errors might be from type errors or other issues that don't prevent require extraction + // Should we do something here, or should it be best effort? + } + + RequireExtractor extractor; + result.root->visit(&extractor); + + return extractor.requirePaths; +} + +std::optional StaticRequireTracer::resolveRequire(const std::string& requirer, const std::string& required) +{ + // Get the directory containing the requiring file (relative to rootDirectory) + std::string requirerDir; + size_t lastSlash = requirer.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + requirerDir = requirer.substr(0, lastSlash); + } + else + { + requirerDir = ""; + } + + // Helper functions for ModulePath - paths are relative to rootDirectory + auto isFileFunc = [this](const std::string& path) -> bool { + std::string fullPath = joinPaths(rootDirectory, path); + return isFile(fullPath); + }; + + auto isDir = [this](const std::string& path) -> bool { + std::string fullPath = joinPaths(rootDirectory, path); + return isDirectory(fullPath); + }; + + // Create a ModulePath with root as rootDirectory, starting at requirer's directory + // This allows us to navigate up with .. beyond requirerDir, but not beyond rootDirectory + std::optional modulePath = ModulePath::create( + "", // Root is empty (relative path base) + requirerDir, // Start at the requirer's directory + isFileFunc, + isDir + ); + + if (!modulePath) + return std::nullopt; + + // Navigate according to the require path + // Split require path by '/' and navigate + std::string reqPath = required; + size_t pos = 0; + + while (pos < reqPath.size()) + { + size_t nextSlash = reqPath.find('/', pos); + std::string component; + + if (nextSlash == std::string::npos) + { + component = reqPath.substr(pos); + pos = reqPath.size(); + } + else + { + component = reqPath.substr(pos, nextSlash - pos); + pos = nextSlash + 1; + } + + if (component.empty() || component == ".") + continue; + + if (component == "..") + { + if (modulePath->toParent() != NavigationStatus::Success) + return std::nullopt; + } + else + { + if (modulePath->toChild(component) != NavigationStatus::Success) + return std::nullopt; + } + } + + // Get the resolved path (relative to rootDirectory) + ResolvedRealPath resolved = modulePath->getRealPath(); + if (resolved.status != NavigationStatus::Success) + return std::nullopt; + + // Strip leading slash if present (occurs when root is empty) + std::string result = resolved.realPath; + if (!result.empty() && result[0] == '/') + result = result.substr(1); + + return result; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 82a5500bd..0a77322d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,7 +11,8 @@ target_sources(Lute.Test PRIVATE src/modulepath.test.cpp src/require.test.cpp - src/stdsystem.test.cpp) + src/stdsystem.test.cpp + src/staticrequires.test.cpp) set_target_properties(Lute.Test PROPERTIES OUTPUT_NAME lute-tests) target_compile_features(Lute.Test PUBLIC cxx_std_17) diff --git a/tests/src/staticrequires.test.cpp b/tests/src/staticrequires.test.cpp new file mode 100644 index 000000000..16a907ab4 --- /dev/null +++ b/tests/src/staticrequires.test.cpp @@ -0,0 +1,128 @@ +#include "doctest.h" +#include "luteprojectroot.h" +#include "lute/staticrequires.h" + +#include "Luau/FileUtils.h" + +#include +#include +#include + +TEST_CASE("staticrequiretracer_simple_dependencies") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + StaticRequireTracer tracer; + std::vector deps = tracer.trace(testDir, "main.luau"); + + // Should find: main.luau, utils.luau, lib/helper.luau, shared.luau + REQUIRE(deps.size() == 4); + + // Entry point should be first + CHECK(deps[0] == "main.luau"); + + // Verify all expected files are present (paths are relative to testDir) + std::vector expectedFiles = { + "main.luau", + "utils.luau", + "lib/helper.luau", + "shared.luau" + }; + + for (const auto& expected : expectedFiles) + { + bool found = std::find(deps.begin(), deps.end(), expected) != deps.end(); + CHECK(found); + } +} + +TEST_CASE("staticrequiretracer_circular_dependencies") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + StaticRequireTracer tracer; + std::vector deps = tracer.trace(testDir, "circular_a.luau"); + + // Should find both circular_a and circular_b without infinite loop + REQUIRE(deps.size() == 2); + + // Entry point should be first + CHECK(deps[0] == "circular_a.luau"); + + // Both files should be in the list + bool hasA = std::find(deps.begin(), deps.end(), "circular_a.luau") != deps.end(); + bool hasB = std::find(deps.begin(), deps.end(), "circular_b.luau") != deps.end(); + + CHECK(hasA); + CHECK(hasB); +} + +TEST_CASE("staticrequiretracer_no_dependencies") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + StaticRequireTracer tracer; + std::vector deps = tracer.trace(testDir, "utils.luau"); + + // utils.luau has no requires, should only return itself + REQUIRE(deps.size() == 1); + CHECK(deps[0] == "utils.luau"); +} + +TEST_CASE("staticrequiretracer_relative_paths") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + StaticRequireTracer tracer; + std::vector deps = tracer.trace(testDir, "lib/helper.luau"); + + // helper.luau requires ../shared, should resolve correctly + REQUIRE(deps.size() == 2); + + CHECK(deps[0] == "lib/helper.luau"); + + bool hasShared = std::find(deps.begin(), deps.end(), "shared.luau") != deps.end(); + CHECK(hasShared); +} + +TEST_CASE("staticrequiretracer_require_graph") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + StaticRequireTracer tracer; + std::vector deps = tracer.trace(testDir, "main.luau"); + + const auto& graph = tracer.getRequireGraph(); + + // main.luau should require utils.luau and lib/helper.luau + REQUIRE(graph.contains("main.luau")); + const auto* mainDeps = graph.find("main.luau"); + REQUIRE(mainDeps != nullptr); + REQUIRE(mainDeps->size() == 2); + CHECK(std::find(mainDeps->begin(), mainDeps->end(), "utils.luau") != mainDeps->end()); + CHECK(std::find(mainDeps->begin(), mainDeps->end(), "lib/helper.luau") != mainDeps->end()); + + // lib/helper.luau should require shared.luau + REQUIRE(graph.contains("lib/helper.luau")); + const auto* helperDeps = graph.find("lib/helper.luau"); + REQUIRE(helperDeps != nullptr); + REQUIRE(helperDeps->size() == 1); + CHECK((*helperDeps)[0] == "shared.luau"); + + // utils.luau should have no dependencies + REQUIRE(graph.contains("utils.luau")); + const auto* utilsDeps = graph.find("utils.luau"); + REQUIRE(utilsDeps != nullptr); + CHECK(utilsDeps->empty()); + + // shared.luau should have no dependencies + REQUIRE(graph.contains("shared.luau")); + const auto* sharedDeps = graph.find("shared.luau"); + REQUIRE(sharedDeps != nullptr); + CHECK(sharedDeps->empty()); +} diff --git a/tests/src/staticrequires/circular_a.luau b/tests/src/staticrequires/circular_a.luau new file mode 100644 index 000000000..99b431a0e --- /dev/null +++ b/tests/src/staticrequires/circular_a.luau @@ -0,0 +1,7 @@ +-- Module A that requires B (circular dependency) +local b = require("./circular_b") + +return { + name = "A", + b = b, +} diff --git a/tests/src/staticrequires/circular_b.luau b/tests/src/staticrequires/circular_b.luau new file mode 100644 index 000000000..eb342c930 --- /dev/null +++ b/tests/src/staticrequires/circular_b.luau @@ -0,0 +1,7 @@ +-- Module B that requires A (circular dependency) +local a = require("./circular_a") + +return { + name = "B", + a = a, +} diff --git a/tests/src/staticrequires/lib/helper.luau b/tests/src/staticrequires/lib/helper.luau new file mode 100644 index 000000000..33591e43c --- /dev/null +++ b/tests/src/staticrequires/lib/helper.luau @@ -0,0 +1,10 @@ +-- Helper module that also requires another module +local shared = require("../shared") + +print("Helper module") +return { + help = function() + return "helper" + end, + shared = shared, +} diff --git a/tests/src/staticrequires/main.luau b/tests/src/staticrequires/main.luau new file mode 100644 index 000000000..b17b4fe38 --- /dev/null +++ b/tests/src/staticrequires/main.luau @@ -0,0 +1,9 @@ +-- Entry point that requires other modules +local utils = require("./utils") +local lib = require("./lib/helper") + +print("Main module") +return { + utils = utils, + lib = lib, +} diff --git a/tests/src/staticrequires/shared.luau b/tests/src/staticrequires/shared.luau new file mode 100644 index 000000000..9ded675b6 --- /dev/null +++ b/tests/src/staticrequires/shared.luau @@ -0,0 +1,5 @@ +-- Shared module +print("Shared module") +return { + version = "1.0", +} diff --git a/tests/src/staticrequires/utils.luau b/tests/src/staticrequires/utils.luau new file mode 100644 index 000000000..b9e9b1666 --- /dev/null +++ b/tests/src/staticrequires/utils.luau @@ -0,0 +1,7 @@ +-- Utilities module +print("Utils module") +return { + add = function(a, b) + return a + b + end, +} From b11dfdd874d19dbe9e26a5ba00997f50c22f3a16 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 24 Oct 2025 11:57:50 -0700 Subject: [PATCH 079/642] stdlib: path join shouldn't mutate argument objects (#475) Separating this out from a different PR so the fix can get in sooner (resolves #463) Co-authored-by: ariel --- lute/std/libs/path/posix/init.luau | 12 ++++++++++-- lute/std/libs/path/win32/init.luau | 11 ++++++++++- tests/path.posix.test.luau | 4 ++++ tests/path.win32.test.luau | 5 +++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lute/std/libs/path/posix/init.luau b/lute/std/libs/path/posix/init.luau index 3c14eeba0..bb7f39f0a 100644 --- a/lute/std/libs/path/posix/init.luau +++ b/lute/std/libs/path/posix/init.luau @@ -102,7 +102,7 @@ local function joinHelper(path: path, addend: pathlike): () end function posix.join(...: pathlike): path - local parts = { ... } + local parts: { pathlike } = { ... } if #parts == 0 then return { parts = {}, @@ -110,7 +110,15 @@ function posix.join(...: pathlike): path } end - local path = posix.parse(parts[1]) + local path: path + if typeof(parts[1]) == "string" then + path = posix.parse(parts[1]) + else + path = { + parts = table.clone((parts[1] :: path).parts), + absolute = (parts[1] :: path).absolute, + } + end for i = 2, #parts do joinHelper(path, parts[i]) diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index 6bb8da26f..d42b26778 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -144,7 +144,16 @@ function win32.join(...: pathlike): path } end - local path = win32.parse(parts[1]) + local path: path + if typeof(parts[1]) == "string" then + path = win32.parse(parts[1]) + else + path = { + parts = table.clone((parts[1] :: path).parts), + kind = (parts[1] :: path).kind, + driveLetter = (parts[1] :: path).driveLetter, + } + end for i = 2, #parts do joinHelper(path, parts[i]) diff --git a/tests/path.posix.test.luau b/tests/path.posix.test.luau index c1d05adb4..edf073508 100644 --- a/tests/path.posix.test.luau +++ b/tests/path.posix.test.luau @@ -454,6 +454,10 @@ test.suite("PathPosixJoinSuite", function(suite) assert.eq(result.parts[2], "user") assert.eq(result.parts[3], "documents") assert.eq(result.parts[4], "file.txt") + assert.eq(#pathobj.parts, 2) -- original pathobj should be unchanged + assert.eq(pathobj.parts[1], "home") + assert.eq(pathobj.parts[2], "user") + assert.eq(pathobj.absolute, true) end) suite:case("join_error_on_absolute_addend", function(assert) diff --git a/tests/path.win32.test.luau b/tests/path.win32.test.luau index 33a9c77d8..9ed8b235f 100644 --- a/tests/path.win32.test.luau +++ b/tests/path.win32.test.luau @@ -553,6 +553,11 @@ test.suite("PathWin32JoinSuite", function(suite) assert.eq(result.parts[2], "username") assert.eq(result.parts[3], "Documents") assert.eq(result.parts[4], "file.txt") + assert.eq(#pathobj.parts, 2) -- original pathobj should be unchanged + assert.eq(pathobj.parts[1], "Users") + assert.eq(pathobj.parts[2], "username") + assert.eq(pathobj.kind, "absolute") + assert.eq(pathobj.driveLetter, "C") end) suite:case("join_error_on_absolute_addend", function(assert) From 727f04c73cbead14c26b52093c94cb7e6ab8fd5d Mon Sep 17 00:00:00 2001 From: ariel Date: Fri, 24 Oct 2025 12:06:10 -0700 Subject: [PATCH 080/642] infra: specify that tune files are TOML (#477) This will make those files not an unidentified language, which makes me personally slightly happier. --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..ff36c5bc1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.tune linguist-language=TOML From fbbebdca17d765a1b03e21daacbbc860bbb0912b Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 24 Oct 2025 12:34:25 -0700 Subject: [PATCH 081/642] stdlib: wrap tmpdir result in path (#473) Wrap `system.tmpdir` in path to make our stdlib interfaces more consistent --------- Co-authored-by: ariel --- lute/std/libs/path/init.luau | 4 ++-- .../libs/{system.luau => system/init.luau} | 19 +++++++++---------- lute/std/libs/system/platforminfo.luau | 10 ++++++++++ 3 files changed, 21 insertions(+), 12 deletions(-) rename lute/std/libs/{system.luau => system/init.luau} (63%) create mode 100644 lute/std/libs/system/platforminfo.luau diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau index 7082e339a..89fd3828b 100644 --- a/lute/std/libs/path/init.luau +++ b/lute/std/libs/path/init.luau @@ -1,4 +1,4 @@ -local system = require("@std/system") +local platforminfo = require("@std/system/platforminfo") local posix = require("@self/posix") local win32 = require("@self/win32") @@ -8,7 +8,7 @@ local pathtypes = require("@self/types") export type path = pathtypes.path export type pathlike = pathtypes.pathlike -local onWindows = system.win32 +local onWindows = platforminfo.win32 local function basename(path: pathlike): string? if onWindows then diff --git a/lute/std/libs/system.luau b/lute/std/libs/system/init.luau similarity index 63% rename from lute/std/libs/system.luau rename to lute/std/libs/system/init.luau index 21287647a..01f02b495 100644 --- a/lute/std/libs/system.luau +++ b/lute/std/libs/system/init.luau @@ -4,13 +4,12 @@ -- Provides re-exports of `@lute/system` and boolean properties for OS detection local system = require("@lute/system") +local path = require("@std/path") +local platforminfo = require("@self/platforminfo") -local osName = string.lower(system.os) - -local win32 = osName == "windows_nt" -local linux = osName == "linux" -local macos = osName == "darwin" -local unix = macos or linux or osName == "unix" or osName == "posix" +local function tmpdir(): path.path + return path.parse(system.tmpdir()) +end return table.freeze({ os = system.os, @@ -25,8 +24,8 @@ return table.freeze({ cpus = system.cpus, -- boolean properties - win32 = win32, - linux = linux, - macos = macos, - unix = unix, + win32 = platforminfo.win32, + linux = platforminfo.linux, + macos = platforminfo.macos, + unix = platforminfo.unix, }) diff --git a/lute/std/libs/system/platforminfo.luau b/lute/std/libs/system/platforminfo.luau new file mode 100644 index 000000000..cefe662a0 --- /dev/null +++ b/lute/std/libs/system/platforminfo.luau @@ -0,0 +1,10 @@ +local system = require("@lute/system") + +local osName = string.lower(system.os) + +local win32 = osName == "windows_nt" +local linux = osName == "linux" +local macos = osName == "darwin" +local unix = macos or linux or osName == "unix" or osName == "posix" + +return table.freeze({ win32 = win32, linux = linux, macos = macos, unix = unix }) From 232f989ed5ead97bccc43012097d370a0fabea16 Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Fri, 24 Oct 2025 15:14:54 -0700 Subject: [PATCH 082/642] Fix CI and pin foreman lute version (#480) --- .github/actions/setup-and-build/action.yml | 4 ---- foreman.toml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index 07ffa1ebf..def71560d 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -25,10 +25,6 @@ outputs: runs: using: "composite" steps: - - name: Set checkout path as LUTE_ROOT_DIR - shell: bash - run: echo "LUTE_ROOT_DIR=$(pwd)" >> $GITHUB_ENV - - name: Install tools uses: Roblox/setup-foreman@v3 with: diff --git a/foreman.toml b/foreman.toml index 77b192ea6..9a3c147bc 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "0.1.0-nightly.20251003" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251023" } From 177e6bc5aa28f1437cbe40c35d8209569748d9ab Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Fri, 24 Oct 2025 16:14:38 -0700 Subject: [PATCH 083/642] Create workflow to update Luau and Lute weekly (#481) --- .github/workflows/update-prs.yml | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 .github/workflows/update-prs.yml diff --git a/.github/workflows/update-prs.yml b/.github/workflows/update-prs.yml new file mode 100644 index 000000000..98a4fbb3b --- /dev/null +++ b/.github/workflows/update-prs.yml @@ -0,0 +1,132 @@ +name: Update Luau/Lute + +on: + schedule: + - cron: '0 0 * * 6' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-luau: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get latest Luau release + id: latest-release + run: | + # Fetch the latest release tag from luau-lang/luau + LATEST_TAG=$(curl -s https://api.github.com/repos/luau-lang/luau/releases/latest | jq -r '.tag_name') + echo "Latest Luau release: $LATEST_TAG" + echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT + + # Fetch the commit hash for this tag + COMMIT_HASH=$(curl -s https://api.github.com/repos/luau-lang/luau/git/ref/tags/$LATEST_TAG | jq -r '.object.sha') + echo "Commit hash: $COMMIT_HASH" + echo "commit=$COMMIT_HASH" >> $GITHUB_OUTPUT + + - name: Check current version + id: current-version + run: | + CURRENT_BRANCH=$(grep '^branch' extern/luau.tune | sed -E 's/^branch *= *"(.*)"/\1/') + echo "Current Luau version: $CURRENT_BRANCH" + echo "branch=$CURRENT_BRANCH" >> $GITHUB_OUTPUT + + - name: Update version files + if: steps.current-version.outputs.branch != steps.latest-release.outputs.tag + run: | + # Update the branch and revision in luau.tune if needed + if [ "${{ steps.current-version.outputs.branch }}" != "${{ steps.latest-release.outputs.tag }}" ]; then + sed -i 's/^branch = ".*"/branch = "${{ steps.latest-release.outputs.tag }}"/' extern/luau.tune + sed -i 's/^revision = ".*"/revision = "${{ steps.latest-release.outputs.commit }}"/' extern/luau.tune + + echo "Updated luau.tune:" + cat extern/luau.tune + fi + + - name: Create Pull Request + if: steps.current-version.outputs.branch != steps.latest-release.outputs.tag + uses: peter-evans/create-pull-request@v7 + with: + commit-message: "chore: update dependencies" + title: "Update Luau to ${{ steps.latest-release.outputs.tag }}" + body: | + ${{ format('**Luau**: Updated from `{0}` to `{1}`', steps.current-version.outputs.branch, steps.latest-release.outputs.tag) }} + + **Release Notes:** + ${{ format('https://github.com/luau-lang/luau/releases/tag/{0}', steps.latest-release.outputs.tag) }} + + --- + *This PR was automatically created by the [Update Luau workflow](https://github.com/${{ github.repository }}/actions/workflows/update-prs.yml)* + branch: update-luau + delete-branch: true + + - name: No update needed + if: steps.current-version.outputs.branch == steps.latest-release.outputs.tag + run: | + echo "Luau is already up to date: ${{ steps.current-version.outputs.branch }}" + + update-lute: + runs-on: ubuntu-latest + needs: update-luau + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get latest Lute release + id: latest-release + run: | + # Fetch the latest release tag from luau-lang/lute + LATEST_LUTE=$( + curl -s https://api.github.com/repos/luau-lang/lute/releases \ + | jq -r 'map(select(.draft | not))[0]?.tag_name' + ) + echo "Latest Lute release: $LATEST_LUTE" + echo "tag=$LATEST_LUTE" >> $GITHUB_OUTPUT + + - name: Check current version + id: current-version + run: | + CURRENT_LUTE=$(grep '^lute = ' rokit.toml | sed -E 's/^lute = ".*@(.*)"/\1/') + echo "Current Lute version: $CURRENT_LUTE" + echo "lute=$CURRENT_LUTE" >> $GITHUB_OUTPUT + + - name: Update version files + run: | + # Update lute version in rokit.toml and foreman.toml + if [ "${{ steps.current-version.outputs.lute }}" != "${{ steps.latest-release.outputs.tag }}" ]; then + sed -i 's/^lute = "luau-lang\/lute@.*"/lute = "luau-lang\/lute@${{ steps.latest-release.outputs.tag }}"/' rokit.toml + echo "Updated rokit.toml:" + cat rokit.toml + echo "" + sed -i 's/^lute = { github = "luau-lang\/lute", version = ".*" }/lute = { github = "luau-lang\/lute", version = "=${{ steps.latest-release.outputs.tag }}" }/' foreman.toml + echo "Updated foreman.toml:" + cat foreman.toml + fi + + - name: Create Pull Request + if: steps.current-version.outputs.lute != steps.latest-release.outputs.tag + uses: peter-evans/create-pull-request@v7 + with: + commit-message: "chore: update dependencies" + title: "Update Lute to ${{ steps.latest-release.outputs.tag }}" + body: | + ${{ format('**Lute**: Updated from `{0}` to `{1}`', steps.current-version.outputs.lute, steps.latest-release.outputs.tag)}} + + **Release Notes:** + ${{ format('https://github.com/luau-lang/lute/releases/tag/{0}', steps.latest-release.outputs.tag)}} + + --- + *This PR was automatically created by the [Update Luau workflow](https://github.com/${{ github.repository }}/actions/workflows/update-prs.yml)* + branch: update-lute + delete-branch: true + + - name: No update needed + if: steps.current-version.outputs.lute == steps.latest-release.outputs.tag + run: | + echo "Lute is already up to date: ${{ steps.current-version.outputs.lute }}" From f27ec6fdbe0c4d6bcd4cc1c712aa645032b9fc5d Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:57:45 -0700 Subject: [PATCH 084/642] Fix multiple runtime APIs that were incorrectly using `std::string::reserve` (#478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem Multiple runtime APIs incorrectly were using `std::string::reserve` where they should have been using `std::string::resize`. This led to UB that seemed to behave fine at runtime, but the introduction of #467 caused all of these APIs to break. ### Solution We introduce `uvutils::getStringFromUv` as a utility API to help reduce errors when interacting with libuv. Internally, this handles all the buffer logic and simply returns a value or an error to the caller. In fixing this, I also discovered two issues with our CI: 1. One of the checks in `path.posix.test.luau` is flipped but was mistakenly passing because of the previously broken `process.cwd()` API. 2. Our Luau tests don't even run on Windows! 😞 I've fixed (1) and have made a separate issue for (2): #483. --- lute/io/src/io.cpp | 2 +- lute/process/src/process.cpp | 55 +++++++---------------------- lute/runtime/CMakeLists.txt | 2 ++ lute/runtime/include/lute/uvutils.h | 24 +++++++++++++ lute/runtime/src/uvutils.cpp | 39 ++++++++++++++++++++ lute/system/src/system.cpp | 50 ++++++++------------------ tests/path.posix.test.luau | 4 +-- 7 files changed, 94 insertions(+), 82 deletions(-) create mode 100644 lute/runtime/include/lute/uvutils.h create mode 100644 lute/runtime/src/uvutils.cpp diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp index 2c7611188..b8ca486c4 100644 --- a/lute/io/src/io.cpp +++ b/lute/io/src/io.cpp @@ -2,7 +2,7 @@ #include "Luau/Variant.h" #include "lute/runtime.h" -#include +#include "uv.h" #include #include "lua.h" diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index a3bb43cea..4a4d49c22 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -1,12 +1,13 @@ #include "lute/process.h" #include "lute/runtime.h" +#include "lute/uvutils.h" #include "Luau/Common.h" #include "lua.h" #include "lualib.h" -#include +#include "uv.h" #include #include @@ -432,28 +433,12 @@ int run(lua_State* L) int homedir(lua_State* L) { - std::string buffer; - - size_t homedir_size = 255; - buffer.reserve(homedir_size); - - int status = uv_os_homedir(buffer.data(), &homedir_size); - if (status == UV_ENOBUFS) - { - // libuv gives us the new size if it's under sized - buffer.reserve(homedir_size); - - status = uv_os_homedir(buffer.data(), &homedir_size); - } - - if (status != 0) - { - luaL_error(L, "failed to get home directory"); - return 1; - } - - lua_pushlstring(L, buffer.c_str(), buffer.size()); + auto result = uvutils::getStringFromUv(uv_os_homedir); + if (uvutils::UvError* error = result.get_if()) + luaL_error(L, "failed to get home directory: %s", error->toString().c_str()); + std::string* homeDir = result.get_if(); + lua_pushlstring(L, homeDir->c_str(), homeDir->size()); return 1; } @@ -470,28 +455,12 @@ int exitFunc(lua_State* L) int cwd(lua_State* L) { - std::string buffer; - - size_t cwd_size = 255; - buffer.reserve(cwd_size); - - int status = uv_cwd(buffer.data(), &cwd_size); - if (status == UV_ENOBUFS) - { - // libuv gives us the new size if it's under sized - buffer.reserve(cwd_size); - - status = uv_cwd(buffer.data(), &cwd_size); - } - - if (status != 0) - { - luaL_error(L, "failed to get current working directory"); - return 1; - } - - lua_pushlstring(L, buffer.c_str(), buffer.size()); + auto result = uvutils::getStringFromUv(uv_cwd); + if (uvutils::UvError* error = result.get_if()) + luaL_error(L, "failed to get current working directory: %s", error->toString().c_str()); + std::string* cwd = result.get_if(); + lua_pushlstring(L, cwd->c_str(), cwd->size()); return 1; }; diff --git a/lute/runtime/CMakeLists.txt b/lute/runtime/CMakeLists.txt index 0b4cfb7f7..839d88377 100644 --- a/lute/runtime/CMakeLists.txt +++ b/lute/runtime/CMakeLists.txt @@ -4,9 +4,11 @@ target_sources(Lute.Runtime PRIVATE include/lute/ref.h include/lute/runtime.h include/lute/userdatas.h + include/lute/uvutils.h src/ref.cpp src/runtime.cpp + src/uvutils.cpp ) target_compile_features(Lute.Runtime PUBLIC cxx_std_17) diff --git a/lute/runtime/include/lute/uvutils.h b/lute/runtime/include/lute/uvutils.h new file mode 100644 index 000000000..32f81455f --- /dev/null +++ b/lute/runtime/include/lute/uvutils.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Luau/Variant.h" + +#include +#include + +namespace uvutils +{ + +struct UvError +{ + UvError(int code); + std::string toString() const; + + int code; +}; + +typedef int (*BufferWriter)(char* buffer, size_t* size); + +// Abstracts away buffer management when getting strings from libuv functions. +Luau::Variant getStringFromUv(BufferWriter bufferWriter, size_t initialBufferSize = 256); + +} // namespace uvutils diff --git a/lute/runtime/src/uvutils.cpp b/lute/runtime/src/uvutils.cpp new file mode 100644 index 000000000..eebd00b7c --- /dev/null +++ b/lute/runtime/src/uvutils.cpp @@ -0,0 +1,39 @@ +#include "lute/uvutils.h" + +#include "uv.h" + +namespace uvutils +{ + +UvError::UvError(int code) + : code(code) +{ +} + +std::string UvError::toString() const +{ + return uv_strerror(code); +} + +Luau::Variant getStringFromUv(BufferWriter bufferWriter, size_t initialBufferSize) +{ + std::string buffer; + size_t size = initialBufferSize; + buffer.resize(size); + + int writeStatus = bufferWriter(buffer.data(), &size); + if (writeStatus == UV_ENOBUFS) + { + // `size` now contains the required size + buffer.resize(size); + writeStatus = bufferWriter(buffer.data(), &size); + } + + if (writeStatus < 0) + return UvError{writeStatus}; + + buffer.resize(size); + return buffer; +} + +} // namespace uvutils diff --git a/lute/system/src/system.cpp b/lute/system/src/system.cpp index 247f33442..399ddb468 100644 --- a/lute/system/src/system.cpp +++ b/lute/system/src/system.cpp @@ -1,13 +1,16 @@ #include "lute/system.h" +#include "lute/uvutils.h" + #include "lua.h" #include "lualib.h" + #include "uv.h" + #include #include #include #include #include -#include namespace libsystem { @@ -88,24 +91,12 @@ int lua_totalmemory(lua_State* L) int lua_hostname(lua_State* L) { - size_t sz = 255; - std::string hostname; - hostname.reserve(sz); - - int res = uv_os_gethostname(hostname.data(), &sz); - if (res == UV_ENOBUFS) - { - hostname.reserve(sz); // libuv updates the size to what's required - res = uv_os_gethostname(hostname.data(), &sz); - } - - if (res != 0) - { - luaL_error(L, "libuv error: %s", uv_strerror(res)); - } - - lua_pushlstring(L, hostname.c_str(), hostname.size()); + auto result = uvutils::getStringFromUv(uv_os_gethostname); + if (uvutils::UvError* error = result.get_if()) + luaL_error(L, "failed to get hostname: %s", error->toString().c_str()); + std::string* hostname = result.get_if(); + lua_pushlstring(L, hostname->c_str(), hostname->size()); return 1; } @@ -126,26 +117,13 @@ int lua_uptime(lua_State* L) int lua_tmpdir(lua_State* L) { - size_t size = 255; - std::string tmpdir; - tmpdir.reserve(size); - - int res = uv_os_tmpdir(tmpdir.data(), &size); - if (res == UV_ENOBUFS) - { - tmpdir.reserve(size); // libuv updates the size to what's required - res = uv_os_tmpdir(tmpdir.data(), &size); - } - - if (res != 0) - { - luaL_error(L, "libuv error: %s", uv_strerror(res)); - } - - lua_pushlstring(L, tmpdir.c_str(), tmpdir.size()); + auto result = uvutils::getStringFromUv(uv_os_tmpdir); + if (uvutils::UvError* error = result.get_if()) + luaL_error(L, "failed to get temporary directory: %s", error->toString().c_str()); + std::string* tmpDir = result.get_if(); + lua_pushlstring(L, tmpDir->c_str(), tmpDir->size()); return 1; - } } // namespace libsystem diff --git a/tests/path.posix.test.luau b/tests/path.posix.test.luau index edf073508..269b721d7 100644 --- a/tests/path.posix.test.luau +++ b/tests/path.posix.test.luau @@ -483,7 +483,7 @@ test.suite("PathPosixResolveSuite", function(suite) -- This test assumes we're in some working directory local result = path.posix.resolve("documents/file.txt") -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / - assert.eq(result.absolute, system.win32) + assert.neq(result.absolute, system.win32) -- The exact parts will depend on the current working directory -- But we can check that it ends with our relative path local endIndex = #result.parts @@ -513,7 +513,7 @@ test.suite("PathPosixResolveSuite", function(suite) -- Should return normalized current working directory local result = path.posix.resolve() -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / - assert.eq(result.absolute, system.win32) + assert.neq(result.absolute, system.win32) -- Can't test exact parts since cwd varies end) From 153d5f23220e4a707df787b2896093a8197c031f Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 27 Oct 2025 16:17:16 -0700 Subject: [PATCH 085/642] stdlib: fix memory leak in luau.compile (#485) Fixed a memory leak which was causing GC for my other changes to fail. Specifically, there was a `std::string` which wasn't being freed because it was being moved into a userdata object, so I attached a destructor to it. I also got rid of the tag which was being applied to the userdata object being constructed because it wasn't being checked, and we were already applying a type-specific metatable to it. Thanks @~vrn-sn for helping me come up the fix! Also added a missing export for `system.tmpdir`. --------- Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- lute/luau/src/luau.cpp | 10 +++++- lute/runtime/include/lute/userdatas.h | 7 ++-- lute/std/libs/system/init.luau | 1 + tests/compile.test.luau | 52 +++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 tests/compile.test.luau diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index b0735ce32..18410577d 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -17,6 +17,7 @@ #include #include #include +#include const char* COMPILE_RESULT_TYPE = "CompileResult"; @@ -2644,7 +2645,14 @@ int compile_luau(lua_State* L) std::string bytecode = Luau::compile(std::string(source, source_size), opts); - std::string* userdata = static_cast(lua_newuserdatatagged(L, sizeof(std::string), kCompilerResultTag)); + std::string* userdata = static_cast(lua_newuserdatadtor( + L, + sizeof(std::string), + [](void* ptr) + { + static_cast(ptr)->std::string::~string(); + } + )); new (userdata) std::string(std::move(bytecode)); diff --git a/lute/runtime/include/lute/userdatas.h b/lute/runtime/include/lute/userdatas.h index a951dbb36..2986772b5 100644 --- a/lute/runtime/include/lute/userdatas.h +++ b/lute/runtime/include/lute/userdatas.h @@ -1,7 +1,6 @@ #pragma once // all tags count down from 128 -constexpr int kDurationTag = 127; -constexpr int kInstantTag = 126; -constexpr int kCompilerResultTag = 125; -constexpr int kWatchHandleTag = 124; \ No newline at end of file +constexpr int kDurationTag = 127; +constexpr int kInstantTag = 126; +constexpr int kWatchHandleTag = 125; \ No newline at end of file diff --git a/lute/std/libs/system/init.luau b/lute/std/libs/system/init.luau index 01f02b495..a815a74e0 100644 --- a/lute/std/libs/system/init.luau +++ b/lute/std/libs/system/init.luau @@ -14,6 +14,7 @@ end return table.freeze({ os = system.os, arch = system.arch, + tmpdir = tmpdir, -- function re-exports threadcount = system.threadcount, diff --git a/tests/compile.test.luau b/tests/compile.test.luau new file mode 100644 index 000000000..93b928b66 --- /dev/null +++ b/tests/compile.test.luau @@ -0,0 +1,52 @@ +local fs = require("@std/fs") +local process = require("@std/process") +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") + +local COMPILER_CONTENTS = [[ +local luau = require("@lute/luau") +local fs = require("@std/fs") + +local args = { ... } +assert(#args == 2, "Expected one argument: path to Luau script to compile") + +local handle = fs.open(args[2], "r") +local contents = fs.read(handle) +fs.close(handle) +luau.compile(contents) +print(`SUCCESS`) +]] + +local COMPILEE_CONTENTS = [[return "Hello world"]] + +local lutePath = process.execpath() +local tmpDir = system.tmpdir() + +test.suite("Lute CLI", function(suite) + suite:case("help1", function(check) + -- Setup + -- Create files + local compilerPath = path.join(tmpDir, "compiler.luau") + local compilerFile = fs.open(compilerPath, "w+") + fs.write(compilerFile, COMPILER_CONTENTS) + fs.close(compilerFile) + + local compileePath = path.join(tmpDir, "compilee.luau") + local compileeFile = fs.open(compileePath, "w+") + fs.write(compileeFile, COMPILEE_CONTENTS) + fs.close(compileeFile) + + local result = process.run({ path.format(lutePath), path.format(compilerPath), path.format(compileePath) }) + -- Compilation should work with no memory leaks + check.eq(result.stderr, "") + check.eq(result.stdout, "SUCCESS\n") + check.eq(result.exitcode, 0) + + -- -- Cleanup + fs.remove(compilerPath) + fs.remove(compileePath) + end) +end) + +test.run() From e66c4c58b5d6ed4bd2a891f5f55f221dfa25aaca Mon Sep 17 00:00:00 2001 From: ariel Date: Tue, 28 Oct 2025 00:38:02 -0700 Subject: [PATCH 086/642] Change create-pull-request action to use luau-lang version (#486) @Nicell wrote this, but it can't work without a luau-lang-localized version because of our CI policies. --- .github/workflows/update-prs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-prs.yml b/.github/workflows/update-prs.yml index 98a4fbb3b..af32f437b 100644 --- a/.github/workflows/update-prs.yml +++ b/.github/workflows/update-prs.yml @@ -51,7 +51,7 @@ jobs: - name: Create Pull Request if: steps.current-version.outputs.branch != steps.latest-release.outputs.tag - uses: peter-evans/create-pull-request@v7 + uses: luau-lang/create-pull-request@v7 with: commit-message: "chore: update dependencies" title: "Update Luau to ${{ steps.latest-release.outputs.tag }}" @@ -111,7 +111,7 @@ jobs: - name: Create Pull Request if: steps.current-version.outputs.lute != steps.latest-release.outputs.tag - uses: peter-evans/create-pull-request@v7 + uses: luau-lang/create-pull-request@v7 with: commit-message: "chore: update dependencies" title: "Update Lute to ${{ steps.latest-release.outputs.tag }}" From e83a026243cff440154e31a222b33b2098e492ea Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 28 Oct 2025 10:15:23 -0700 Subject: [PATCH 087/642] Port codemod into lute (#454) This PR copies the [codemod implementation](https://github.com/luau-lang/codemod) mostly as-is into lute, invokable via `lute transform`. The main tweak I made was to load migrations by the path passed via the CLI, rather than using `require`, so that migration paths don't have to be passed relative to the codemod source code. --------- Co-authored-by: Sora Kanosue Co-authored-by: Sora Kanosue --- examples/transformee.luau | 3 + examples/transformer.luau | 88 ++++++ lute/cli/commands/transform/init.luau | 164 ++++++++++++ .../cli/commands/transform/lib/arguments.luau | 69 +++++ lute/cli/commands/transform/lib/files.luau | 68 +++++ lute/cli/commands/transform/lib/ignore.luau | 250 ++++++++++++++++++ lute/cli/commands/transform/lib/types.luau | 33 +++ lute/std/libs/string.luau | 59 +++++ lute/std/libs/syntax/query.luau | 27 ++ tests/transform.test.luau | 68 +++++ 10 files changed, 829 insertions(+) create mode 100644 examples/transformee.luau create mode 100644 examples/transformer.luau create mode 100644 lute/cli/commands/transform/init.luau create mode 100644 lute/cli/commands/transform/lib/arguments.luau create mode 100644 lute/cli/commands/transform/lib/files.luau create mode 100644 lute/cli/commands/transform/lib/ignore.luau create mode 100644 lute/cli/commands/transform/lib/types.luau create mode 100644 lute/std/libs/string.luau create mode 100644 lute/std/libs/syntax/query.luau create mode 100644 tests/transform.test.luau diff --git a/examples/transformee.luau b/examples/transformee.luau new file mode 100644 index 000000000..e99c87e57 --- /dev/null +++ b/examples/transformee.luau @@ -0,0 +1,3 @@ +local x = math.sqrt(-1) +-- x ~= x should become math.isnan(x) +local b = x ~= x diff --git a/examples/transformer.luau b/examples/transformer.luau new file mode 100644 index 000000000..3f33906e6 --- /dev/null +++ b/examples/transformer.luau @@ -0,0 +1,88 @@ +local parser = require("@std/syntax/parser") +local printer = require("@std/syntax/printer") +local visitor = require("@std/syntax/visitor") +local luau = require("@lute/luau") + +function transformation(ctx) + local parsedResult = parser.parsefile(ctx.source) + + local v = visitor.createVisitor() + + v.visitBinary = function(node: luau.AstExprBinary) + if node.operator.text ~= "~=" then + return true + end + + if node.lhsoperand.tag ~= "local" or node.rhsoperand.tag ~= "local" then + return true + end + + if node.lhsoperand.token.text ~= node.rhsoperand.token.text then + return true + end + + local operand: luau.AstExprLocal = node.lhsoperand + operand.token.leadingtrivia = {} + operand.token.trailingtrivia = {} + local openingPosition: luau.Position = operand.token.position; + + -- transform node into an AstExprCall + (node :: any).operator = nil + (node :: any).lhsoperand = nil + (node :: any).rhsoperand = nil + + ((node :: any) :: luau.AstExprCall).tag = "call" + + local func: luau.AstExpr = { + tag = "global", + name = { + leadingtrivia = {}, + position = openingPosition, + text = "math.isnan", + trailingtrivia = {}, + }, + } + ((node :: any) :: luau.AstExprCall).func = func + + local openparens: luau.Token<"("> = { + leadingtrivia = {}, + position = { line = openingPosition.line, column = openingPosition.column + #"math.isnan" }, + text = "(", + trailingtrivia = {}, + } + ((node :: any) :: luau.AstExprCall).openparens = openparens + + local arguments: luau.Punctuated = { { node = operand } } + ((node :: any) :: luau.AstExprCall).arguments = arguments + + local closeparens: luau.Token<")"> = { + leadingtrivia = {}, + position = { + line = openingPosition.line, + column = openingPosition.column + #"math.isnan(" + #operand.token.text, + }, + text = ")", + trailingtrivia = {}, + } + ((node :: any) :: luau.AstExprCall).closeparens = closeparens; + + ((node :: any) :: luau.AstExprCall).self = false + + local argLocation: luau.Location = { + begin = { line = openingPosition.line, column = openingPosition.column + #"math.isnan(" }, + ["end"] = { + line = openingPosition.line, + column = openingPosition.column + #"math.isnan(" + #operand.token.text, + }, + } + ((node :: any) :: luau.AstExprCall).argLocation = argLocation + + return false + end + + visitor.visitBlock(parsedResult.root, v) + + return printer.printfile(parsedResult) +end + +return transformation diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau new file mode 100644 index 000000000..85c75fb3d --- /dev/null +++ b/lute/cli/commands/transform/init.luau @@ -0,0 +1,164 @@ +local fs = require("@lute/fs") +local luau = require("@lute/luau") +local pathLib = require("@std/path") + +local arguments = require("@self/lib/arguments") +local files = require("@self/lib/files") +local types = require("@self/lib/types") + +local function exhaustiveMatch(value: never): never + error(`Unknown value in exhaustive match: {value}`) +end + +local function loadFromPath(path: string): ...any + local migrationHandle = fs.open(path, "r") + assert(migrationHandle ~= nil, `Failed to open migration file at '{path}'`) + + local migrationBytecode = luau.compile(fs.read(migrationHandle)) + + fs.close(migrationHandle) + + return luau.load(migrationBytecode, path)() +end + +local function loadMigration(path: string): types.Migration + local success, loaded = pcall(loadFromPath, path) + assert(success, `{path} failed to require: {loaded}`) + assert(loaded, `{path} is missing a return`) + + if typeof(loaded) == "function" then + local migration = {} + migration.transform = loaded + return migration :: types.Migration -- FIXME: Luau + elseif typeof(loaded) == "table" then + -- FIXME(Luau): https://github.com/luau-lang/luau/issues/1803 for the type assertions + assert( + typeof((loaded :: any).transform) == "function", + `{path} must contain a 'transform' property of type function` + ) + if (loaded :: any).options then + assert(typeof(loaded.options) == "table", `Expected return of {path}.options to be a table`) + for _, option in loaded.options do + assert(typeof(option.name) == "string", `Expected option.name to be a string`) + assert( + option.kind == "string" or option.kind == "boolean", + `Expected option.kind to be either 'string' or 'boolean', got {option.kind}` + ) + end + end + if (loaded :: any).initialize then + assert(typeof(loaded.initialize) == "function", `Expected {path}.initialize to be a function`) + end + return loaded :: types.Migration + else + error(`Expected '{path}' to return a function or table, got {typeof(loaded)}`) + end +end + +local function processMigrationOptions(migration: types.Migration, options: { [string]: string }): { [string]: any } + if migration.options == nil then + return table.freeze({}) :: any -- FIXME(Luau): https://github.com/luau-lang/luau/issues/1802 + end + + local processedOptions: { [string]: string | boolean } = {} + + for _, option in migration.options do + local value = options[option.name] + if value then + if option.kind == "boolean" then + local lowered = value:lower() + assert( + lowered == "true" or lowered == "false", + `Option '--{option.name}' expects a boolean value 'true' or 'false'` + ) + processedOptions[option.name] = lowered == "true" + elseif option.kind == "string" then + processedOptions[option.name] = value + else + exhaustiveMatch(option.kind) + end + else + assert(option.default ~= nil, `Missing required option '--{option.name}'`) + processedOptions[option.name] = option.default + end + end + + return table.freeze(processedOptions) +end + +local function applyMigration( + migration: types.Migration, + paths: { pathLib.path }, + options: { [string]: string | boolean }, + dryRun: boolean +) + local deletedPaths = {} + + if migration.initialize then + print("Initializing migration") + local ctx = { + options = options, + } + migration.initialize(ctx) + end + + for _, pathObj in paths do + local pathStr = pathLib.format(pathObj) + + print(`Applying to {pathStr}`) + + local source = fs.readfiletostring(pathStr) + + local ctx = { + path = pathStr, + source = source, + options = options, + } + + -- TODO: should we wrap in pcall? For now we don't do this to preserve stack trace + local result = migration.transform(ctx) + + assert(typeof(result) == "string", `Transformed expected to return string, but got {typeof(result)}`) + + if result == types.DELETION_MARKER then + print(`Marking {pathStr} for deletion`) + table.insert(deletedPaths, pathStr) + elseif result ~= source and not dryRun then + fs.writestringtofile(pathStr, result) + end + end + + if not dryRun then + for _, pathStr in deletedPaths do + print(`Deleting {pathStr}`) + fs.remove(pathStr) + end + end +end + +local function main(...: string) + local args = arguments.parse({ ... }) + + if args.dryRun then + print("Executing in dry run mode") + end + + print(`Loading migration '{args.migrationPath}'`) + local migration = loadMigration(args.migrationPath) + + print("Finding source files") -- todo: might type error + local files = files.getSourceFiles(args.filePaths) + if #files == 0 then + error("error: no source files provided") + end + + print("Processing migration options") + local migrationOptions = processMigrationOptions(migration, args.migrationOptions) + + print("Applying migration") + applyMigration(migration, files, migrationOptions, args.dryRun) + + print(`Processed {#files} files!`) +end + +main(...) diff --git a/lute/cli/commands/transform/lib/arguments.luau b/lute/cli/commands/transform/lib/arguments.luau new file mode 100644 index 000000000..c2be2cd3c --- /dev/null +++ b/lute/cli/commands/transform/lib/arguments.luau @@ -0,0 +1,69 @@ +local string = require("@std/string") + +type Config = { + --- Prevent making changes to files + dryRun: boolean, + --- Path to the migration module to execute + migrationPath: string, + --- Configuration flags to pass to the migration + migrationOptions: { [string]: string }, + --- List of file paths to process + --- Note: no directory traversal or filtering has been applied on this list + filePaths: { string }, +} + +local function parse(arguments: { string }): Config + -- TODO: replace with proper CLI system + -- For now, first argument is the transformer script, whilst the rest are either flags or files to apply it on + + if #arguments < 2 then + error("Usage: lute transform [options...] ") + end + + local config = { + dryRun = false, + migrationPath = nil :: string?, + migrationOptions = {}, + filePaths = {}, + } + + local i = 1 + while i <= #arguments do + local argument = arguments[i] + + if string.startswith(argument, "--") then + local name = string.removeprefix(argument, "--") + + if config.migrationPath == nil then + -- Options before the codemod file are parsed as options for the tool itself + if name == "dry-run" then + config.dryRun = true + else + error(`Unknown flag '--{name}'`) + end + else + i += 1 + assert(i <= #arguments, `Missing value for '{name}'`) + local value = arguments[i] + + config.migrationOptions[name] = value + end + else + if config.migrationPath == nil then + config.migrationPath = argument + else + table.insert(config.filePaths, argument) + end + end + + i += 1 + end + + assert(config.migrationPath, "ASSERTION FAILED: codemodPath ~= nil") + + return config +end + +return { + parse = parse, +} diff --git a/lute/cli/commands/transform/lib/files.luau b/lute/cli/commands/transform/lib/files.luau new file mode 100644 index 000000000..65d53df1d --- /dev/null +++ b/lute/cli/commands/transform/lib/files.luau @@ -0,0 +1,68 @@ +local fs = require("@lute/fs") +local process = require("@lute/process") + +local ignore = require("./ignore") +local path = require("@std/path") +local string = require("@std/string") + +local function extend(tbl: { T }, other: { T }) + for _, entry in other do + table.insert(tbl, entry) + end +end + +local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: ignore.IgnoreResolver): { path.path } + local results = {} + + for _, entry in fs.listdir(path.format(directory)) do + local fullPath = path.join(directory, entry.name) + + if entry.type == "dir" then + if ignoreResolver:isIgnoredDirectory(fullPath) then + print("Skipping", fullPath) + continue + end + + extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver)) + else + -- TODO: allow customisation of the filter when traversing a directory. e.g., globs? file endings? ignore paths? + if string.endswith(entry.name, ".luau") or string.endswith(entry.name, ".lua") then + if ignoreResolver:isIgnoredFile(fullPath) then + print("Skipping", fullPath) + continue + end + table.insert(results, fullPath) + end + end + end + + return results +end + +local function getSourceFiles(paths: { string }): { path.path } + local files = {} + + local ignoreResolver = ignore.new() + + for _, filepath in paths do + local pathObj = path.parse(filepath) + + if not path.isabsolute(pathObj) then + pathObj = path.join(process.cwd(), pathObj) + end + + filepath = path.format(pathObj) + print(filepath) + if fs.type(filepath) == "dir" then + extend(files, traverseDirectoryRecursive(pathObj, ignoreResolver)) + else + table.insert(files, pathObj) + end + end + + return files +end + +return { + getSourceFiles = getSourceFiles, +} diff --git a/lute/cli/commands/transform/lib/ignore.luau b/lute/cli/commands/transform/lib/ignore.luau new file mode 100644 index 000000000..b81f99d10 --- /dev/null +++ b/lute/cli/commands/transform/lib/ignore.luau @@ -0,0 +1,250 @@ +local fs = require("@lute/fs") + +local path = require("@std/path") +local string = require("@std/string") + +--- Performs ignore resolution on a set of +local IgnoreResolver = {} +IgnoreResolver.__index = IgnoreResolver + +type Glob = { pattern: string, onlyMatchDirectories: boolean } + +type GitignoreData = { + location: string, + ignores: { Glob }, + whitelists: { Glob }, + next: GitignoreData?, +} + +type IgnoreResolverData = { + _ignoreCache: { [string]: GitignoreData }, +} + +export type IgnoreResolver = setmetatable + +function IgnoreResolver.new(): IgnoreResolver + local self = {} + + self._ignoreCache = {} + + return setmetatable(self, IgnoreResolver) +end + +--- Converts a glob pattern into a Luau string pattern for efficient matching +--- The output pattern should be matched against a filepath that is relative to the .gitignore location +local function parseGlob(glob: string): Glob + local onlyMatchDirectories = false + + if glob:sub(-1) == "/" then + onlyMatchDirectories = true + glob = glob:sub(1, -2) + end + + local lua_parts = {} + local matchAnywhere = false + local idx = 1 + local len = #glob + + -- If there is a separator at the beginning or middle (or both) of the pattern, then the pattern is relative to + -- the directory level of the particular .gitignore file itself. + -- i.e., anchor the pattern to the start + if glob:find("/") then + table.insert(lua_parts, "^") + if glob:sub(1, 1) == "." then + idx = 2 + end + else + -- If no leading slash, it can match anywhere, including after a directory. + -- This means it can start at the beginning of the string, or after a slash. + -- We can't match for two distinct patterns bc Lua lacks an OR pattern operator + -- Instead add unique marker we check for in matchesGlob, indicating we match + -- against the two distinct patterns: anchored at beginning, or after a slash + table.insert(lua_parts, "MATCH_ANYWHERE:") + matchAnywhere = true + end + + while idx <= len do + local char = glob:sub(idx, idx) + if char == "*" then + if glob:sub(idx, idx + 1) == "**" then + if idx == 1 and glob:sub(idx + 2, idx + 2) == "/" then + -- Leading '**/' at start of the file. If we've already determined matchAnywhere, + -- we don't need to add an extra character match + if matchAnywhere then + idx = idx + 2 + continue + end + end + if glob:sub(idx + 2, idx + 2) == "/" then + -- ** between slashes (e.g., a/**/b) + -- Zero or more directory segments - match everything, and don't include the following `/` slash + table.insert(lua_parts, ".*") + idx = idx + 3 + else + -- Trailing ** (e.g., foo/**) or just `**` + table.insert(lua_parts, ".*") -- Matches anything, including slashes + idx = idx + 2 + end + else + -- Single asterisk `*` (matches anything except /) + table.insert(lua_parts, "[^/]*") + idx = idx + 1 + end + elseif char == "?" then + -- Match any single character, except for / + table.insert(lua_parts, "[^/]") + idx = idx + 1 + elseif char == "-" then + table.insert(lua_parts, "%-") + idx = idx + 1 + else + table.insert(lua_parts, char) + idx = idx + 1 + end + end + + -- Add the ending anchor if not already an implicit `.*` from `**` + if glob:sub(-2) ~= "**" then + table.insert(lua_parts, "$") + end + + return { pattern = table.concat(lua_parts), onlyMatchDirectories = onlyMatchDirectories } +end + +local function parseGitignoreContents(location: string, contents: string): GitignoreData + local lines = contents:split("\n") + + local ignoreData: GitignoreData = { + location = location, + ignores = {}, + whitelists = {}, + } + + for _, line in lines do + line = string.trim(line) + + if line == "" or string.startswith(line, "#") then + continue + end + + if string.startswith(line, "!") then + local globPattern = string.removeprefix(line, "!") + local glob = parseGlob(globPattern) + table.insert(ignoreData.whitelists, glob) + else + local glob = parseGlob(line) + table.insert(ignoreData.ignores, glob) + end + end + + return ignoreData +end + +local function parseGitignore(filepath: path.path): GitignoreData + local contents = fs.readfiletostring(path.format(filepath)) + return parseGitignoreContents(assert(path.dirname(filepath)), contents) +end + +local function matchesGlob(glob: Glob, filepath: string, isDirectory: boolean): boolean + if glob.onlyMatchDirectories and not isDirectory then + return false + end + -- If the pattern starts with our matchAnywhere marker, we need to test + -- both: at start of path OR after any directory separator + if glob.pattern:match("^MATCH_ANYWHERE:") then + local actualPattern = glob.pattern:gsub("^MATCH_ANYWHERE:(.*)$", "%1") + + local matchAtStart = filepath:match("^" .. actualPattern) ~= nil + local matchAfterDir = filepath:match("/" .. actualPattern) ~= nil + + return matchAtStart or matchAfterDir + else + return filepath:match(glob.pattern) ~= nil + end +end + +local function isIgnored(ignoreData: GitignoreData, filepath: string, isDirectory: boolean) + local matched, ignored = false, false + + -- TODO: compute relative path correctly + local relativePath = string.removeprefix(filepath, ignoreData.location) + + for _, ignore in ignoreData.ignores do + if matchesGlob(ignore, relativePath, isDirectory) then + matched = true + ignored = true + break + end + end + + if ignored then + for _, whitelist in ignoreData.whitelists do + if matchesGlob(whitelist, filepath, isDirectory) then + matched = true + ignored = false + break + end + end + end + + if not matched and ignoreData.next then + return isIgnored(ignoreData.next, filepath, isDirectory) + end + + return ignored +end + +--- Checks whether the given file matches an ignore +function IgnoreResolver.isIgnoredFile(self: IgnoreResolver, filepath: path.path) + local directory = path.dirname(filepath) + assert(directory ~= nil, "filepath has no directory!") + + local ignoreData = self:_readIgnoreRecursive(directory) + return isIgnored(ignoreData, path.format(filepath), false) +end + +function IgnoreResolver.isIgnoredDirectory(self: IgnoreResolver, directorypath: path.path) + -- Hardcode: skip the .git directory + if path.basename(directorypath) == ".git" then + return true + end + + local parentDirectory = path.dirname(directorypath) + if parentDirectory then + local ignoreData = self:_readIgnoreRecursive(parentDirectory) + return isIgnored(ignoreData, path.format(directorypath), true) + else + return false + end +end + +function IgnoreResolver._readIgnoreRecursive(self: IgnoreResolver, directory: string) + if self._ignoreCache[directory] ~= nil then + return self._ignoreCache[directory] + end + + local parentPath = path.dirname(directory) + local baseIgnores = if parentPath then self:_readIgnoreRecursive(parentPath) else nil + local gitignorePath = path.join(directory, ".gitignore") + + local exists, fileType = pcall(fs.type, gitignorePath) + + if exists and fileType == "file" then + local myIgnoreData = parseGitignore(gitignorePath) + + if #myIgnoreData.ignores > 0 or #myIgnoreData.whitelists > 0 then + myIgnoreData.next = baseIgnores + baseIgnores = myIgnoreData + end + end + + if not baseIgnores then + baseIgnores = { location = "", ignores = {}, whitelists = {}, next = nil } :: GitignoreData + end + assert(baseIgnores, "Luau") + + self._ignoreCache[directory] = baseIgnores + return baseIgnores +end + +return IgnoreResolver diff --git a/lute/cli/commands/transform/lib/types.luau b/lute/cli/commands/transform/lib/types.luau new file mode 100644 index 000000000..b04bc9fc6 --- /dev/null +++ b/lute/cli/commands/transform/lib/types.luau @@ -0,0 +1,33 @@ +export type Context = { + path: string, + source: string, + options: Options, +} + +export type Transformer = (Context) -> string + +export type ConfigOption = + { + kind: "string", + name: string, + default: string?, + } + | { + kind: "boolean", + name: string, + default: boolean?, + } + +export type InitializationContext = { + options: Options, +} + +export type Migration = { + options: { ConfigOption }?, + initialize: ((InitializationContext) -> ())?, + transform: Transformer, +} + +return { + DELETION_MARKER = "$DELETION_MARKER", +} diff --git a/lute/std/libs/string.luau b/lute/std/libs/string.luau new file mode 100644 index 000000000..7f875355d --- /dev/null +++ b/lute/std/libs/string.luau @@ -0,0 +1,59 @@ +local strings = {} + +function strings.startswith(str: string, prefix: string): boolean + if #prefix > #str then + return false + end + + return str:sub(1, #prefix) == prefix +end + +function strings.removeprefix(str: string, prefix: string): string + if not strings.startswith(str, prefix) then + return str + end + + return str:sub(#prefix + 1) +end + +function strings.endswith(str: string, suffix: string): boolean + return str:sub(-#suffix) == suffix +end + +function strings.removesuffix(str: string, suffix: string): string + if not strings.endswith(str, suffix) then + return str + end + + return str:sub(0, -#suffix - 1) +end + +function strings.trim(str: string): string + -- Pattern is guaranteed to return a result + return str:match("^%s*(.-)%s*$") :: string +end + +return table.freeze({ + startswith = strings.startswith, + removeprefix = strings.removeprefix, + endswith = strings.endswith, + removesuffix = strings.removesuffix, + trim = strings.trim, + + -- re-exports of the table library + byte = string.byte, + char = string.char, + find = string.find, + format = string.format, + gmatch = string.gmatch, + gsub = string.gsub, + len = string.len, + lower = string.lower, + match = string.match, + pack = string.pack, + packsize = string.packsize, + rep = string.rep, + reverse = string.reverse, + split = string.split, + sub = string.sub, +}) diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau new file mode 100644 index 000000000..0dab2f423 --- /dev/null +++ b/lute/std/libs/syntax/query.luau @@ -0,0 +1,27 @@ +local luau = require("@lute/luau") + +local visitor = require("./visitor") + +--- Selects a list of expressions from the provided block that match the provided predicate. +--- Useful for defining declarative-based programs, where the matched nodes are mutated for the migration +local function selectif(block: luau.AstStatBlock, predicate: (luau.AstExpr) -> T?): { T } + local nodes = {} + local finder = visitor.createVisitor() + + finder.visitExpression = function(node) + local result = predicate(node) + if result then + table.insert(nodes, result) + end + + return true + end + + visitor.visitBlock(block, finder) + + return nodes +end + +return { + selectif = selectif, +} diff --git a/tests/transform.test.luau b/tests/transform.test.luau new file mode 100644 index 000000000..8d9b68968 --- /dev/null +++ b/tests/transform.test.luau @@ -0,0 +1,68 @@ +local fs = require("@lute/fs") +local path = require("@std/path") +local process = require("@lute/process") +local test = require("@std/test") + +local TRANSFORMEE_CONTENT = [[ +local x = math.sqrt(-1) +local b = x ~= x +]] + +local lutePath = process.execpath() + +test.suite("lute transform", function(suite) + local transformTestDir = path.format(path.join("build", "transform_tests")) + + suite:beforeall(function() + -- Make a temp folder in the build directory + if not fs.exists("build") then + fs.mkdir("build") + end + + if not fs.exists(transformTestDir) then + fs.mkdir(transformTestDir) + end + end) + + suite:case("transform visitor style", function(assert) + -- Setup + -- Copy examples/transformer.luau to build/transform_tests/transformer.luau + local transformerExample = path.format(path.join("examples", "transformer.luau")) + local transformerDest = path.format(path.join(transformTestDir, "transformer.luau")) + fs.copy(transformerExample, transformerDest) + + -- Create a transformee file + local transformeePath = path.format(path.join(transformTestDir, "transformee.luau")) + local transformeeHandle = fs.open(transformeePath, "w+") + fs.write(transformeeHandle, TRANSFORMEE_CONTENT) + fs.close(transformeeHandle) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "transform", transformerDest, transformeePath }, { + cwd = process.cwd(), + }) + + -- Check + assert.eq(result.exitcode, 0) + + local transformeeHandle = fs.open(transformeePath, "r") + local transformeeContent = fs.read(transformeeHandle) + assert.eq(transformeeContent:find("x ~= x"), nil) + assert.neq(transformeeContent:find("math.isnan(x)", 1, true), nil) + fs.close(transformeeHandle) + + -- Teardown + fs.remove(transformerDest) + fs.remove(transformeePath) + end) + + suite:afterall(function() + for _, file in fs.listdir(transformTestDir) do + fs.remove(path.format(path.join(transformTestDir, file.name))) + end + fs.rmdir(transformTestDir) + end) +end) + +test.run() From 0ccff127b6107e823e3b777859376f34de62df87 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 28 Oct 2025 10:35:35 -0700 Subject: [PATCH 088/642] stdlib: loadbypath (#461) Implementation of `loadbypath` in user land using `luau.load`. Addresses #321 . --------- Co-authored-by: Sora Kanosue Co-authored-by: Sora Kanosue --- definitions/luau.luau | 2 +- examples/compile.luau | 2 +- examples/linter.luau | 2 +- examples/parsing.luau | 2 +- lute/luau/src/luau.cpp | 11 +- lute/std/libs/luau.luau | 183 +++++++++++++++++++++++++++++ lute/std/libs/path/posix/init.luau | 9 +- lute/std/libs/path/win32/init.luau | 9 +- lute/std/libs/syntax/parser.luau | 2 +- lute/std/libs/syntax/printer.luau | 2 +- lute/std/libs/syntax/visitor.luau | 2 +- tests/loadbypath.test.luau | 68 +++++++++++ tests/testAstSerializer.test.luau | 4 +- 13 files changed, 271 insertions(+), 27 deletions(-) create mode 100644 lute/std/libs/luau.luau create mode 100644 tests/loadbypath.test.luau diff --git a/definitions/luau.luau b/definitions/luau.luau index efee44c1f..a017f8d2d 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -576,7 +576,7 @@ function luau.compile(source: string): Bytecode error("not implemented") end -function luau.load(bytecode: Bytecode, chunkname: string?, env: { [any]: any }?): (...any) -> ...any +function luau.load(bytecode: Bytecode, chunkname: string, env: { [any]: any }?): (...any) -> ...any error("not implemented") end diff --git a/examples/compile.luau b/examples/compile.luau index 51872e794..2f7757459 100644 --- a/examples/compile.luau +++ b/examples/compile.luau @@ -1,4 +1,4 @@ -local luau = require("@lute/luau") +local luau = require("@std/luau") local bytecode_container = luau.compile('return "Hello, world!"') diff --git a/examples/linter.luau b/examples/linter.luau index bc0928021..ea44b0fae 100644 --- a/examples/linter.luau +++ b/examples/linter.luau @@ -1,5 +1,5 @@ local fs = require("@std/fs") -local luau = require("@lute/luau") +local luau = require("@std/luau") local pp = require("@batteries/pp") diff --git a/examples/parsing.luau b/examples/parsing.luau index 55c37f6a9..374ce6608 100644 --- a/examples/parsing.luau +++ b/examples/parsing.luau @@ -1,4 +1,4 @@ -local luau = require("@lute/luau") +local luau = require("@std/luau") local pretty = require("@batteries/pp") diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 18410577d..a6312717b 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -2664,13 +2664,12 @@ int compile_luau(lua_State* L) int load_luau(lua_State* L) { - const std::string* bytecode_string = static_cast(luaL_checkudata(L, 1, COMPILE_RESULT_TYPE)); - const char* path = luaL_optlstring(L, 2, "=luau.load", nullptr); - std::string chunk_name = path; - if (chunk_name != "=luau.load") - chunk_name.insert(0, "@"); + const std::string* bytecodeString = static_cast(luaL_checkudata(L, 1, COMPILE_RESULT_TYPE)); + const char* chunkname = luaL_checkstring(L, 2); + int envIndex = lua_isnoneornil(L, 3) ? 0 : 3; - luau_load(L, chunk_name.c_str(), bytecode_string->c_str(), bytecode_string->length(), lua_gettop(L) > 2 ? 3 : 0); + if (luau_load(L, chunkname, bytecodeString->c_str(), bytecodeString->length(), envIndex) != 0) + lua_error(L); return 1; } diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau new file mode 100644 index 000000000..fe9fbcb50 --- /dev/null +++ b/lute/std/libs/luau.luau @@ -0,0 +1,183 @@ +local luteLuau = require("@lute/luau") + +local fs = require("@lute/fs") +local path = require("@std/path") + +local luau = {} + +-- Export all types from luteLuau +export type Position = luteLuau.Position +export type Location = luteLuau.Location + +export type Whitespace = luteLuau.Whitespace +export type SingleLineComment = luteLuau.SingleLineComment +export type MultiLineComment = luteLuau.MultiLineComment + +export type Trivia = luteLuau.Trivia + +export type Token = luteLuau.Token + +export type Eof = luteLuau.Eof + +export type Pair = luteLuau.Pair +export type Punctuated = luteLuau.Punctuated + +export type AstLocal = luteLuau.AstLocal + +export type AstExprGroup = luteLuau.AstExprGroup + +export type AstExprConstantNil = luteLuau.AstExprConstantNil + +export type AstExprConstantBool = luteLuau.AstExprConstantBool + +export type AstExprConstantNumber = luteLuau.AstExprConstantNumber + +export type AstExprConstantString = luteLuau.AstExprConstantString + +export type AstExprLocal = luteLuau.AstExprLocal + +export type AstExprGlobal = luteLuau.AstExprGlobal + +export type AstExprVarargs = luteLuau.AstExprVarargs + +export type AstExprCall = luteLuau.AstExprCall + +export type AstExprIndexName = luteLuau.AstExprIndexName + +export type AstExprIndexExpr = luteLuau.AstExprIndexExpr + +export type AstFunctionBody = luteLuau.AstFunctionBody + +export type AstExprAnonymousFunction = luteLuau.AstExprAnonymousFunction + +export type AstExprTableItem = luteLuau.AstExprTableItem + +export type AstExprTable = luteLuau.AstExprTable + +export type AstExprUnary = luteLuau.AstExprUnary + +export type AstExprBinary = luteLuau.AstExprBinary + +export type AstExprInterpString = luteLuau.AstExprInterpString + +export type AstExprTypeAssertion = luteLuau.AstExprTypeAssertion + +export type AstExprIfElseIfs = luteLuau.AstExprIfElseIfs + +export type AstExprIfElse = luteLuau.AstExprIfElse + +export type AstExpr = luteLuau.AstExpr + +export type AstStatBlock = luteLuau.AstStatBlock + +export type AstStatElseIf = luteLuau.AstStatElseIf + +export type AstStatIf = luteLuau.AstStatIf + +export type AstStatWhile = luteLuau.AstStatWhile + +export type AstStatRepeat = luteLuau.AstStatRepeat + +export type AstStatBreak = luteLuau.AstStatBreak + +export type AstStatContinue = luteLuau.AstStatContinue + +export type AstStatReturn = luteLuau.AstStatReturn + +export type AstStatExpr = luteLuau.AstStatExpr + +export type AstStatLocal = luteLuau.AstStatLocal + +export type AstStatFor = luteLuau.AstStatFor + +export type AstStatForIn = luteLuau.AstStatForIn + +export type AstStatAssign = luteLuau.AstStatAssign + +export type AstStatCompoundAssign = luteLuau.AstStatCompoundAssign + +export type AstAttribute = luteLuau.AstAttribute + +export type AstStatFunction = luteLuau.AstStatFunction + +export type AstStatLocalFunction = luteLuau.AstStatLocalFunction + +export type AstStatTypeAlias = luteLuau.AstStatTypeAlias + +export type AstStatTypeFunction = luteLuau.AstStatTypeFunction + +export type AstStat = luteLuau.AstStat + +export type AstGenericType = luteLuau.AstGenericType + +export type AstGenericTypePack = luteLuau.AstGenericTypePack + +export type AstTypeReference = luteLuau.AstTypeReference + +export type AstTypeSingletonBool = luteLuau.AstTypeSingletonBool + +export type AstTypeSingletonString = luteLuau.AstTypeSingletonString + +export type AstTypeTypeof = luteLuau.AstTypeTypeof + +export type AstTypeGroup = luteLuau.AstTypeGroup + +export type AstTypeOptional = luteLuau.AstTypeOptional + +export type AstTypeUnion = luteLuau.AstTypeUnion + +export type AstTypeIntersection = luteLuau.AstTypeIntersection + +export type AstTypeArray = luteLuau.AstTypeArray + +export type AstTypeTableItem = luteLuau.AstTypeTableItem + +export type AstTypeTable = luteLuau.AstTypeTable + +export type AstTypeFunctionParameter = luteLuau.AstTypeFunctionParameter + +export type AstTypeFunction = luteLuau.AstTypeFunction + +export type AstType = luteLuau.AstType + +export type AstTypePackExplicit = luteLuau.AstTypePackExplicit + +export type AstTypePackGeneric = luteLuau.AstTypePackGeneric + +export type AstTypePackVariadic = luteLuau.AstTypePackVariadic + +export type AstTypePack = luteLuau.AstTypePack + +export type ParseResult = luteLuau.ParseResult + +export type Bytecode = luteLuau.Bytecode + +function luau.parse(source: string): ParseResult + return luteLuau.parse(source) +end + +function luau.parseexpr(source: string): AstExpr + return luteLuau.parseexpr(source) +end + +function luau.compile(source: string): Bytecode + return luteLuau.compile(source) +end + +function luau.load(bytecode: Bytecode, chunkname: string?, env: { [any]: any }?): (...any) -> ...any + return luteLuau.load(bytecode, if chunkname ~= nil then `@{chunkname}` else "=luau.load", env) +end + +function luau.loadbypath(requirePath: path.pathlike): any + local requirePathStr = if typeof(requirePath) == "string" then requirePath else path.format(requirePath) + + local migrationHandle = fs.open(requirePathStr, "r") + + local migrationBytecode = luau.compile(fs.read(migrationHandle)) + + fs.close(migrationHandle) + + return luau.load(migrationBytecode, requirePathStr)() +end + +return luau diff --git a/lute/std/libs/path/posix/init.luau b/lute/std/libs/path/posix/init.luau index bb7f39f0a..8a2b14183 100644 --- a/lute/std/libs/path/posix/init.luau +++ b/lute/std/libs/path/posix/init.luau @@ -110,15 +110,12 @@ function posix.join(...: pathlike): path } end - local path: path - if typeof(parts[1]) == "string" then - path = posix.parse(parts[1]) - else - path = { + local path: path = if typeof(parts[1]) == "string" + then posix.parse(parts[1]) + else { parts = table.clone((parts[1] :: path).parts), absolute = (parts[1] :: path).absolute, } - end for i = 2, #parts do joinHelper(path, parts[i]) diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index d42b26778..e2ccadb8c 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -144,16 +144,13 @@ function win32.join(...: pathlike): path } end - local path: path - if typeof(parts[1]) == "string" then - path = win32.parse(parts[1]) - else - path = { + local path: path = if typeof(parts[1]) == "string" + then win32.parse(parts[1]) + else { parts = table.clone((parts[1] :: path).parts), kind = (parts[1] :: path).kind, driveLetter = (parts[1] :: path).driveLetter, } - end for i = 2, #parts do joinHelper(path, parts[i]) diff --git a/lute/std/libs/syntax/parser.luau b/lute/std/libs/syntax/parser.luau index 5c76d2620..3ff7035d4 100644 --- a/lute/std/libs/syntax/parser.luau +++ b/lute/std/libs/syntax/parser.luau @@ -1,6 +1,6 @@ --!strict -local luau = require("@lute/luau") +local luau = require("@std/luau") --- Parses Luau source code into an AstStatBlock local function parse(source: string): luau.AstStatBlock diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 401841db7..31903ac2e 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -1,5 +1,5 @@ --!strict -local luau = require("@lute/luau") +local luau = require("@std/luau") local visitor = require("./visitor") local function exhaustiveMatch(value: never): never diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 513b2c53a..53fcf75c8 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -1,6 +1,6 @@ --!strict -local luau = require("@lute/luau") +local luau = require("@std/luau") export type Visitor = { visitBlock: (luau.AstStatBlock) -> boolean, diff --git a/tests/loadbypath.test.luau b/tests/loadbypath.test.luau new file mode 100644 index 000000000..3754faa46 --- /dev/null +++ b/tests/loadbypath.test.luau @@ -0,0 +1,68 @@ +local fs = require("@lute/fs") +local process = require("@lute/process") +local path = require("@std/path") +local test = require("@std/test") + +local REQUIRER_CONTENTS = [[ +local args = { ... } +assert(#args == 2, "Expected one argument: path to Luau script to require") + +local luau = require("@std/luau") +local result = luau.loadbypath(args[2]) +print(result) +]] + +local REQUIREE_CONTENTS = [[return "Success"]] + +local lutePath = process.execpath() + +local tmpDirStr = ".tmp" +local tmpDirExists = fs.exists(tmpDirStr) +local testDir = path.join(tmpDirStr, "lute_require_test") +local testDirStr = path.format(testDir) + +test.suite("Lute CLI", function(suite) + suite:beforeall(function() + if not tmpDirExists then + fs.mkdir(tmpDirStr) + end + + if not fs.exists(testDirStr) then + fs.mkdir(testDirStr) + end + end) + + suite:case("help1", function(check) + -- Setup + -- Create files + local requirerPath = path.format(path.join(testDir, "requirer.luau")) + local requirerFile = fs.open(requirerPath, "w+") + fs.write(requirerFile, REQUIRER_CONTENTS) + fs.close(requirerFile) + + local requireePath = path.format(path.join(testDir, "requiree.luau")) + local requireeFile = fs.open(requireePath, "w+") + fs.write(requireeFile, REQUIREE_CONTENTS) + fs.close(requireeFile) + + local result = process.run({ lutePath, requirerPath, requireePath }) + check.eq(result.exitcode, 0) + check.eq(result.stdout, "Success\n") + + -- Cleanup + fs.remove(requirerPath) + fs.remove(requireePath) + end) + + suite:afterall(function() + if fs.exists(testDirStr) then + fs.rmdir(testDirStr) + end + + if not tmpDirExists then + fs.rmdir(tmpDirStr) + end + end) +end) + +test.run() diff --git a/tests/testAstSerializer.test.luau b/tests/testAstSerializer.test.luau index f4dbfb45b..92e2fccde 100644 --- a/tests/testAstSerializer.test.luau +++ b/tests/testAstSerializer.test.luau @@ -1,12 +1,12 @@ local asserts = require("@std/assert") local fs = require("@std/fs") -local luau = require("@lute/luau") +local luau = require("@std/luau") local path = require("@std/path") local test = require("@std/test") local parser = require("@std/syntax/parser") local printer = require("@std/syntax/printer") -local T = require("@lute/luau") +local T = require("@std/luau") local function assertEqualsLocation( assert: asserts.Asserts, From 49707fefcca3678af765a2c171c7f1df5584acec Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 28 Oct 2025 11:58:29 -0700 Subject: [PATCH 089/642] Lute transform can use stdlib loadbypath (#488) Fixes failing lute transform unit test --- lute/cli/commands/transform/init.luau | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index 85c75fb3d..f8b617551 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -1,28 +1,13 @@ local fs = require("@lute/fs") -local luau = require("@lute/luau") +local luau = require("@std/luau") local pathLib = require("@std/path") local arguments = require("@self/lib/arguments") local files = require("@self/lib/files") local types = require("@self/lib/types") -local function exhaustiveMatch(value: never): never - error(`Unknown value in exhaustive match: {value}`) -end - -local function loadFromPath(path: string): ...any - local migrationHandle = fs.open(path, "r") - assert(migrationHandle ~= nil, `Failed to open migration file at '{path}'`) - - local migrationBytecode = luau.compile(fs.read(migrationHandle)) - - fs.close(migrationHandle) - - return luau.load(migrationBytecode, path)() -end - local function loadMigration(path: string): types.Migration - local success, loaded = pcall(loadFromPath, path) + local success, loaded = pcall(luau.loadbypath, path) assert(success, `{path} failed to require: {loaded}`) assert(loaded, `{path} is missing a return`) From ae9b5d87329fb166773848626f5ba0d26f7a5eb1 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:38:20 -0700 Subject: [PATCH 090/642] Use `std::destroy_at` instead of manually invoking destructors (#489) C++17 introduces `std::destroy_at` under the `` header, which prevents confusion about which destructor to use when paired with placement new. For many cases, choosing the correct destructor is trivial, but for type names that are inside a namespace or aliased, it can be somewhat confusing which destructor to call. Using `std::destroy_at` automatically compiles the call to the correct destructor based on the pointer type. --- lute/cli/src/climain.cpp | 3 ++- lute/fs/src/fs.cpp | 4 +--- lute/luau/src/luau.cpp | 4 +++- lute/process/src/process.cpp | 4 +--- lute/vm/src/spawn.cpp | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 181de8a67..ffc0b25db 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -35,6 +35,7 @@ #endif #include +#include #include #include @@ -45,7 +46,7 @@ void* createCliRequireContext(lua_State* L) sizeof(RequireCtx), [](void* ptr) { - static_cast(ptr)->~RequireCtx(); + std::destroy_at(static_cast(ptr)); } ); diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index ebc7e4874..a716edb46 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -1050,9 +1050,7 @@ static void initalizeFS(lua_State* L) kWatchHandleTag, [](lua_State* L, void* ud) { - auto* handle = static_cast(ud); - - handle->~WatchHandle(); + std::destroy_at(static_cast(ud)); } ); diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index a6312717b..715977a99 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -14,9 +14,11 @@ #include "lua.h" #include "lualib.h" + #include #include #include +#include #include const char* COMPILE_RESULT_TYPE = "CompileResult"; @@ -2650,7 +2652,7 @@ int compile_luau(lua_State* L) sizeof(std::string), [](void* ptr) { - static_cast(ptr)->std::string::~string(); + std::destroy_at(static_cast(ptr)); } )); diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 4a4d49c22..a263775d4 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -41,8 +41,6 @@ struct ProcessHandle std::shared_ptr self; std::atomic pendingCloses{0}; - ~ProcessHandle() {} - void closeHandles() { auto closeCb = [](uv_handle_t* handle) @@ -604,7 +602,7 @@ static int envIter(lua_State* L) sizeof(EnvIter), [](void* ptr) { - static_cast(ptr)->~EnvIter(); + std::destroy_at(static_cast(ptr)); } ); diff --git a/lute/vm/src/spawn.cpp b/lute/vm/src/spawn.cpp index 26bd7a369..407c1e801 100644 --- a/lute/vm/src/spawn.cpp +++ b/lute/vm/src/spawn.cpp @@ -178,7 +178,7 @@ static void* createChildVmRequireContext(lua_State* L) sizeof(RequireCtx), [](void* ptr) { - static_cast(ptr)->~RequireCtx(); + std::destroy_at(static_cast(ptr)); } ); @@ -255,7 +255,7 @@ int lua_spawn(lua_State* L) ); // Remove the Ref we have in current VM, now it will not cause the actual lua_unref - target->~TargetFunction(); + std::destroy_at(target); } ); From 4a781d882246aae0ac94871e8b07da09b3a9a020 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:57:37 -0700 Subject: [PATCH 091/642] Use visitor pattern for exhaustivity in IO stream handling (#487) There are many other places where this pattern would be safer than `get_if`, but this is the only one where the overload pattern isn't needed (since all variants are handled identically). I'll need to think on how we want to introduce the overload pattern (either somewhere central in Lute or in the Luau repo itself). --- lute/io/src/io.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp index b8ca486c4..462bf5cbe 100644 --- a/lute/io/src/io.cpp +++ b/lute/io/src/io.cpp @@ -34,11 +34,13 @@ struct IOHandle } uv_stream_t* getStream() { - if (auto* tty = streamVariant.get_if()) - return (uv_stream_t*)tty; - if (auto* pipe = streamVariant.get_if()) - return (uv_stream_t*)pipe; - return nullptr; + return Luau::visit( + [](auto& stream) -> uv_stream_t* + { + return (uv_stream_t*)&stream; + }, + streamVariant + ); } }; From 311cd7e116984b08b5056a6c9ccfc9bbab6cfa15 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 28 Oct 2025 14:23:56 -0700 Subject: [PATCH 092/642] feature: Introduce a multifile payload format for embedding into the lute binary (#484) This PR is an incremental step towards supporting multi-file bytecode compilation. This PR defines a `LuteExePayload`, for encoding and decoding the multi file bytecode bundles that will be appended to the `lute` binary when shipping executables. Once this PR is in, we can start writing the glue code that will let us produce a lute executable with multiple .luau files embedded. --- lute/cli/CMakeLists.txt | 4 +- lute/cli/include/lute/compile.h | 56 ++++++ lute/cli/src/compile.cpp | 305 ++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 4 +- tests/src/compile.test.cpp | 273 ++++++++++++++++++++++++++++ 5 files changed, 639 insertions(+), 3 deletions(-) create mode 100644 tests/src/compile.test.cpp diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index b438b9a85..cda9acb32 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -37,8 +37,8 @@ target_sources(Lute.CLI.lib PRIVATE ) target_compile_features(Lute.CLI.lib PUBLIC cxx_std_17) -target_include_directories(Lute.CLI.lib PUBLIC include ${CLI_GENERATED_INCLUDE_DIR}) -target_link_libraries(Lute.CLI.lib PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Require Lute.Runtime Luau.CLI.lib) +target_include_directories(Lute.CLI.lib PUBLIC include ${CLI_GENERATED_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR}) +target_link_libraries(Lute.CLI.lib PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Require Lute.Runtime Luau.CLI.lib zlibstatic) target_compile_options(Lute.CLI.lib PRIVATE ${LUTE_OPTIONS}) add_executable(Lute.CLI) diff --git a/lute/cli/include/lute/compile.h b/lute/cli/include/lute/compile.h index 9bc173816..0054ee905 100644 --- a/lute/cli/include/lute/compile.h +++ b/lute/cli/include/lute/compile.h @@ -1,7 +1,10 @@ #pragma once +#include "Luau/DenseHash.h" #include "Luau/FileUtils.h" +#include + struct AppendedBytecodeResult { bool found = false; @@ -11,3 +14,56 @@ struct AppendedBytecodeResult AppendedBytecodeResult checkForAppendedBytecode(); int compileScript(const std::string& inputFilePath, const std::string& outputFilePath); + +struct LuteDecodeResult; + +struct LuteEncodeResult +{ + std::string payload; + size_t bytesWritten = 0; + size_t compressedPayloadSizeBytes = 0; + size_t uncompressedPayloadSizeBytes = 0; +}; + +/** + * Represents a bundle of compiled Luau files ready for injection. + * + * Uncompressed bundle format (before compression): + * For each file: + * [path_length: uint32_t] + * [path_string: char[path_length]] + * [bytecode_size: uint64_t] + * [bytecode_data: byte[bytecode_size]] + * + * Injectable payload format (after compression, what goes into executable): + * [compressed_bundle_data: byte[compressed_size]] + * [compressed_size: uint64_t] + * [uncompressed_size: uint64_t] + * [num_files: uint32_t] + * [entry_point_path_string: char[entry_point_path_length]] + * [entry_point_path_length: uint32_t] + + */ +struct LuteExePayload +{ + LuteExePayload() = default; + void add(const std::string& luauFilePath); + + std::optional encode(); + static std::optional decode(const std::string_view binary); + + std::string entryPointPath; + Luau::DenseHashMap filePathToBytecode{""}; // path -> bytecode + +private: + bool parseFromDecompressedBundle(std::string_view decompressedBundle); + std::vector filePaths; +}; + +struct LuteDecodeResult +{ + LuteExePayload payload; + size_t bytesRead = 0; + size_t compressedPayloadSizeBytes = 0; + size_t uncompressedPayloadSizeBytes = 0; +}; diff --git a/lute/cli/src/compile.cpp b/lute/cli/src/compile.cpp index dbb0b0f8c..265106675 100644 --- a/lute/cli/src/compile.cpp +++ b/lute/cli/src/compile.cpp @@ -2,10 +2,18 @@ #include "lute/options.h" #include "lute/process.h" + #include "uv.h" +#include "zlib.h" +#include #include #include +#include + +#ifndef _WIN32 +#include +#endif const char MAGIC_FLAG[] = "LUTEBYTE"; const size_t MAGIC_FLAG_SIZE = sizeof(MAGIC_FLAG) - 1; @@ -146,3 +154,300 @@ int compileScript(const std::string& inputFilePath, const std::string& outputFil return 0; } + +void LuteExePayload::add(const std::string& luauFilePath) +{ + // First file added becomes the entry point + if (filePaths.empty()) + entryPointPath = luauFilePath; + + filePaths.push_back(luauFilePath); +} + +std::optional LuteExePayload::encode() +{ + // Encoding an empty payload is an error + if (filePaths.empty()) + { + fprintf(stderr, "Encode failed: No files added to payload\n"); + return std::nullopt; + } + + LuteEncodeResult result; + // Step 1: Build uncompressed bytecode bundle + // Format: For each file, append [path_len][path][bytecode_size][bytecode] + std::string uncompressedBundle; + for (const auto& filePath : filePaths) + { + // Read source file from disk + std::optional source = readFile(filePath); + if (!source) + { + fprintf(stderr, "Encode failed: Could not read file '%s'\n", filePath.c_str()); + return std::nullopt; + } + + // Compile Luau source to bytecode + std::string bytecode = Luau::compile(*source, copts()); + if (bytecode.empty()) + { + fprintf(stderr, "Encode failed: Could not compile file '%s' to bytecode\n", filePath.c_str()); + return std::nullopt; + } + + filePathToBytecode[filePath] = bytecode; + + // Append path_length field (uint32_t, 4 bytes) + uint32_t pathLength = static_cast(filePath.size()); + uncompressedBundle.append(reinterpret_cast(&pathLength), sizeof(uint32_t)); + + // Append path_string field (variable length) + uncompressedBundle.append(filePath); + + // Append bytecode_size field (uint64_t, 8 bytes) + uint64_t bytecodeSize = bytecode.size(); + uncompressedBundle.append(reinterpret_cast(&bytecodeSize), sizeof(uint64_t)); + + // Append bytecode_data field (variable length) + uncompressedBundle.append(bytecode); + } + + // Step 2: Compress the bundled bytecode using zlib + uLong uncompressedSize = uncompressedBundle.size(); + + // Calculate maximum possible compressed size + uLong compressedSize = compressBound(uncompressedSize); + std::vector compressedData(compressedSize); + + // Compress with maximum compression level + int compressResult = compress2( + compressedData.data(), // destination buffer + &compressedSize, // in/out: buffer size / actual compressed size + reinterpret_cast(uncompressedBundle.data()), // source data + uncompressedSize, // source size + Z_BEST_COMPRESSION // compression level (9) + ); + + if (compressResult != Z_OK) + { + fprintf(stderr, "Encode failed: Compression error (zlib error %d)\n", compressResult); + return std::nullopt; + } + result.payload.clear(); + size_t totalBytes = compressedSize // Size of compressed data + + sizeof(uint64_t) // Length of compressed data (uint64_t) + + sizeof(uint64_t) // Lengths of uncompressed data (uint64_t) + + sizeof(uint32_t) // Number of modules(files) in the bundle (uint32_t) + + entryPointPath.length() // Module entry point path length + + sizeof(uint32_t) // Length of entry point field + + MAGIC_FLAG_SIZE; // LUTEBYTE - the magic flag that tells us to decode the bundled modules + result.payload.reserve(totalBytes); + // Step 3: Append the metadata needed + // Append compressed_data field (variable length, compressedSize bytes) + result.payload.append(reinterpret_cast(compressedData.data()), compressedSize); + + // Append compressed_size field (uint64_t, 8 bytes) + uint64_t compressedSizeField = compressedSize; + result.payload.append(reinterpret_cast(&compressedSizeField), sizeof(uint64_t)); + + // Append uncompressed_size field (uint64_t, 8 bytes) + uint64_t uncompressedSizeField = uncompressedSize; + result.payload.append(reinterpret_cast(&uncompressedSizeField), sizeof(uint64_t)); + + // Append num_files field (uint32_t, 4 bytes) + uint32_t numFiles = static_cast(filePaths.size()); + result.payload.append(reinterpret_cast(&numFiles), sizeof(uint32_t)); + + // Append entry_point_path_string field (variable length) + result.payload.append(entryPointPath); + + // Append entry_point_path_length field (uint32_t, 4 bytes) + uint32_t entryPointPathLength = static_cast(entryPointPath.size()); + result.payload.append(reinterpret_cast(&entryPointPathLength), sizeof(uint32_t)); + + // Step 4: Append the LUTE BYTE + result.payload.append(MAGIC_FLAG, MAGIC_FLAG_SIZE); + + result.bytesWritten = result.payload.size(); + result.compressedPayloadSizeBytes = compressedSize; + result.uncompressedPayloadSizeBytes = uncompressedSize; + + return result; +} + +std::optional LuteExePayload::decode(const std::string_view binary) +{ + LuteDecodeResult result; + result.payload.filePathToBytecode.clear(); + + // Check minimum size for magic flag + if (binary.size() < MAGIC_FLAG_SIZE + sizeof(uint32_t)) + { + fprintf(stderr, "Decode failed: Binary too small (%zu bytes) to contain valid payload\n", binary.size()); + return std::nullopt; + } + + // Check for LUTEBYTE magic flag at the end + size_t magicOffset = binary.size() - MAGIC_FLAG_SIZE; + if (memcmp(binary.data() + magicOffset, MAGIC_FLAG, MAGIC_FLAG_SIZE) != 0) + { + fprintf(stderr, "Decode failed: LUTEBYTE magic flag not found\n"); + return std::nullopt; + } + + // Helper to read fixed-size values backwards + auto readValue = [&binary](size_t& pos, const char* fieldName, auto& value) -> bool + { + // We need this because the auto& parameter acts like a generic and we would like to strip out the & here + using T = std::decay_t; + if (pos < sizeof(T)) + { + fprintf(stderr, "Decode failed: Incomplete %s field\n", fieldName); + return false; + } + pos -= sizeof(T); + memcpy(&value, binary.data() + pos, sizeof(T)); + return true; + }; + + // Helper to read variable-length bytes backwards + auto readBytes = [&binary](size_t& pos, size_t length, const char* fieldName) -> std::optional { + if (pos < length) + { + fprintf(stderr, "Decode failed: Incomplete %s field\n", fieldName); + return std::nullopt; + } + pos -= length; + return std::string(binary.data() + pos, length); + }; + + // Read metadata from LUTEBYTE back to entry_point_path_length: [entry_point_path_length][entry_point_path][num_files][uncompressed_size][compressed_size][compressed_data]...[LUTEBYTE] + size_t pos = binary.size() - MAGIC_FLAG_SIZE; + + // Read entry_point_path_length + uint32_t entryPointPathLength; + if (!readValue(pos, "entry_point_path_length", entryPointPathLength)) + return std::nullopt; + + // Read entry_point_path + auto entryPointPath = readBytes(pos, entryPointPathLength, "entry_point_path"); + if (!entryPointPath) + return std::nullopt; + result.payload.entryPointPath = *entryPointPath; + + // Read num_files + uint32_t numFiles; + if (!readValue(pos, "num_files", numFiles)) + return std::nullopt; + + // Read uncompressed_size + uint64_t uncompressedSize; + if (!readValue(pos, "uncompressed_size", uncompressedSize)) + return std::nullopt; + + // Read compressed_size + uint64_t compressedSize; + if (!readValue(pos, "compressed_size", compressedSize)) + return std::nullopt; + + // Read compressed data + if (pos < compressedSize) + { + fprintf(stderr, "Decode failed: Incomplete compressed data (expected %llu bytes)\n", static_cast(compressedSize)); + return std::nullopt; + } + pos -= compressedSize; + + // Decompress the bundle + std::vector uncompressedData(uncompressedSize); + uLongf actualUncompressedSize = uncompressedSize; + int zlibResult = uncompress( + uncompressedData.data(), + &actualUncompressedSize, + reinterpret_cast(binary.data() + pos), + compressedSize + ); + + if (zlibResult != Z_OK) + { + fprintf(stderr, "Decode failed: Decompression error (zlib error %d)\n", zlibResult); + return std::nullopt; + } + + // Parse the decompressed bundle + std::string_view decompressedBundle(reinterpret_cast(uncompressedData.data()), actualUncompressedSize); + if (!result.payload.parseFromDecompressedBundle(decompressedBundle)) + { + fprintf(stderr, "Decode failed: Failed to parse decompressed bundle\n"); + return std::nullopt; + } + + // Validate that the number of parsed files matches the metadata + if (result.payload.filePathToBytecode.size() != numFiles) + { + fprintf(stderr, "Decode failed: Expected %u files but parsed %zu\n", numFiles, result.payload.filePathToBytecode.size()); + return std::nullopt; + } + + // Populate result metrics + result.bytesRead = binary.size(); + result.compressedPayloadSizeBytes = compressedSize; + result.uncompressedPayloadSizeBytes = actualUncompressedSize; + return result; +} + +bool LuteExePayload::parseFromDecompressedBundle(std::string_view decompressedBundle) +{ + size_t offset = 0; + filePathToBytecode.clear(); + + while (offset < decompressedBundle.size()) + { + // Read path length + if (offset + sizeof(uint32_t) > decompressedBundle.size()) + { + fprintf(stderr, "Invalid bundle: incomplete path length field\n"); + return false; + } + + uint32_t pathLength; + memcpy(&pathLength, decompressedBundle.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + + // Read path string + if (offset + pathLength > decompressedBundle.size()) + { + fprintf(stderr, "Invalid bundle: incomplete path string\n"); + return false; + } + + std::string filePath(decompressedBundle.data() + offset, pathLength); + offset += pathLength; + + // Read bytecode size + if (offset + sizeof(uint64_t) > decompressedBundle.size()) + { + fprintf(stderr, "Invalid bundle: incomplete bytecode size field\n"); + return false; + } + + uint64_t bytecodeSize; + memcpy(&bytecodeSize, decompressedBundle.data() + offset, sizeof(uint64_t)); + offset += sizeof(uint64_t); + + // Read bytecode + if (offset + bytecodeSize > decompressedBundle.size()) + { + fprintf(stderr, "Invalid bundle: incomplete bytecode data\n"); + return false; + } + + std::string bytecode(decompressedBundle.data() + offset, bytecodeSize); + offset += bytecodeSize; + + // Store in map + filePathToBytecode[filePath] = bytecode; + } + + return true; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0a77322d8..4952d0ac2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,7 +12,9 @@ target_sources(Lute.Test PRIVATE src/modulepath.test.cpp src/require.test.cpp src/stdsystem.test.cpp - src/staticrequires.test.cpp) + src/staticrequires.test.cpp + src/stdsystem.test.cpp + src/compile.test.cpp) set_target_properties(Lute.Test PROPERTIES OUTPUT_NAME lute-tests) target_compile_features(Lute.Test PUBLIC cxx_std_17) diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp new file mode 100644 index 000000000..3feb1dcb3 --- /dev/null +++ b/tests/src/compile.test.cpp @@ -0,0 +1,273 @@ +#include "Luau/FileUtils.h" +#include "doctest.h" +#include "lute/compile.h" +#include "luteprojectroot.h" + +#include +#include +#include +#include + +TEST_CASE("lutepayload_single_file_roundtrip") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create payload and add file + LuteExePayload originalPayload; + originalPayload.add(testFilePath); + + // Encode + auto encodeResult = originalPayload.encode(); + REQUIRE(encodeResult.has_value()); + REQUIRE(encodeResult->bytesWritten > 0); + REQUIRE(encodeResult->compressedPayloadSizeBytes > 0); + REQUIRE(encodeResult->uncompressedPayloadSizeBytes > 0); + REQUIRE(!encodeResult->payload.empty()); + + // Verify compression is working (compressed should be smaller than uncompressed) + CHECK(encodeResult->compressedPayloadSizeBytes <= encodeResult->uncompressedPayloadSizeBytes); + + // Decode + auto decodeResult = LuteExePayload::decode(encodeResult->payload); + REQUIRE(decodeResult.has_value()); + + // Verify metrics match + CHECK(decodeResult->bytesRead == encodeResult->bytesWritten); + CHECK(decodeResult->compressedPayloadSizeBytes == encodeResult->compressedPayloadSizeBytes); + CHECK(decodeResult->uncompressedPayloadSizeBytes == encodeResult->uncompressedPayloadSizeBytes); + + // Verify entry point + CHECK(decodeResult->payload.entryPointPath == testFilePath); + CHECK(decodeResult->payload.entryPointPath == originalPayload.entryPointPath); + + // Verify bytecode map has one entry + REQUIRE(decodeResult->payload.filePathToBytecode.size() == 1); + + // Verify bytecode matches + auto it = decodeResult->payload.filePathToBytecode.find(testFilePath); + REQUIRE(it != nullptr); + auto originalIt = originalPayload.filePathToBytecode.find(testFilePath); + REQUIRE(originalIt != nullptr); + CHECK(*it == *originalIt); +} + +TEST_CASE("lutepayload_multiple_files_roundtrip") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + std::vector testFiles = { + joinPaths(testDir, "main.luau"), + joinPaths(testDir, "utils.luau"), + joinPaths(testDir, "lib/helper.luau"), + joinPaths(testDir, "shared.luau") + }; + + // Create payload with multiple files + LuteExePayload originalPayload; + for (const auto& file : testFiles) + { + originalPayload.add(file); + } + + // First file should be the entry point + CHECK(originalPayload.entryPointPath == testFiles[0]); + + auto encodeResult = originalPayload.encode(); + REQUIRE(encodeResult.has_value()); + REQUIRE(!encodeResult->payload.empty()); + + auto decodeResult = LuteExePayload::decode(encodeResult->payload); + REQUIRE(decodeResult.has_value()); + + // Verify entry point + CHECK(decodeResult->payload.entryPointPath == testFiles[0]); + + // Verify all files are present + REQUIRE(decodeResult->payload.filePathToBytecode.size() == testFiles.size()); + + for (const auto& file : testFiles) + { + auto decodedIt = decodeResult->payload.filePathToBytecode.find(file); + REQUIRE(decodedIt != nullptr); + + auto originalIt = originalPayload.filePathToBytecode.find(file); + REQUIRE(originalIt != nullptr); + + // Verify bytecode matches + CHECK(*decodedIt == *originalIt); + } +} + +TEST_CASE("lutepayload_invalid_magic_flag") +{ + // Create a payload with invalid magic flag + std::string invalidPayload = "INVALID_"; + invalidPayload.append(100, 'X'); // Add some data + + auto decodeResult = LuteExePayload::decode(invalidPayload); + CHECK(!decodeResult.has_value()); +} + +TEST_CASE("lutepayload_too_small_payload") +{ + // Payload smaller than minimum size + std::string tinyPayload = "TINY"; + + auto decodeResult = LuteExePayload::decode(tinyPayload); + CHECK(!decodeResult.has_value()); +} + +TEST_CASE("lutepayload_corrupted_metadata") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create valid payload + LuteExePayload originalPayload; + originalPayload.add(testFilePath); + auto encodeResult = originalPayload.encode(); + REQUIRE(encodeResult.has_value()); + + // Corrupt the payload by modifying bytes near the end (metadata area) + std::string corruptedPayload = encodeResult->payload; + size_t corruptPos = corruptedPayload.size() - 20; + if (corruptPos < corruptedPayload.size()) + { + corruptedPayload[corruptPos] = ~corruptedPayload[corruptPos]; + corruptedPayload[corruptPos + 1] = ~corruptedPayload[corruptPos + 1]; + } + + // Attempt to decode corrupted payload + auto decodeResult = LuteExePayload::decode(corruptedPayload); + // Should either fail or produce different results + if (decodeResult.has_value()) + { + // If it doesn't fail outright, the data should be corrupted + CHECK(decodeResult->payload.entryPointPath != originalPayload.entryPointPath); + } +} + +TEST_CASE("lutepayload_entry_point_is_first_added") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + std::string firstFile = joinPaths(testDir, "main.luau"); + std::string secondFile = joinPaths(testDir, "utils.luau"); + + LuteExePayload payload; + payload.add(firstFile); + payload.add(secondFile); + + // Entry point should be the first file added + CHECK(payload.entryPointPath == firstFile); +} + +TEST_CASE("lutepayload_nonexistent_file") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string nonExistentFile = joinPaths(luteProjectRoot, "tests/src/this_file_does_not_exist.luau"); + + LuteExePayload payload; + payload.add(nonExistentFile); + + // Encoding should fail because file doesn't exist + auto encodeResult = payload.encode(); + CHECK(!encodeResult.has_value()); +} + +TEST_CASE("lutepayload_empty_payload") +{ + // Create payload without adding any files + LuteExePayload emptyPayload; + CHECK(emptyPayload.entryPointPath.empty()); + + // Encoding an empty payload should fail + auto encodeResult = emptyPayload.encode(); + CHECK(!encodeResult.has_value()); +} + +TEST_CASE("lutepayload_compression_effectiveness") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + LuteExePayload payload; + payload.add(testFilePath); + + auto encodeResult = payload.encode(); + REQUIRE(encodeResult.has_value()); + + // Verify that compression is actually reducing size + // compressed should be smaller than uncompressed (barring any weird edge cases) + CHECK(encodeResult->compressedPayloadSizeBytes <= encodeResult->uncompressedPayloadSizeBytes); + + // Verify the total payload includes overhead for metadata + // Total = compressed data + metadata (sizes, counts, paths, magic flag) + CHECK(encodeResult->bytesWritten >= encodeResult->compressedPayloadSizeBytes); +} + +TEST_CASE("lutepayload_bytecode_integrity") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create two separate payloads with the same file + LuteExePayload payload1; + payload1.add(testFilePath); + auto encode1 = payload1.encode(); + REQUIRE(encode1.has_value()); + + LuteExePayload payload2; + payload2.add(testFilePath); + auto encode2 = payload2.encode(); + REQUIRE(encode2.has_value()); + + // The bytecode for the same file should be identical + auto it1 = payload1.filePathToBytecode.find(testFilePath); + auto it2 = payload2.filePathToBytecode.find(testFilePath); + REQUIRE(it1 != nullptr); + REQUIRE(it2 != nullptr); + CHECK(*it1 == *it2); + + // The encoded payloads should be identical (deterministic encoding) + CHECK(encode1->payload == encode2->payload); +} + +TEST_CASE("lutepayload_validates_numfiles_metadata") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create and encode a valid payload + LuteExePayload payload; + payload.add(testFilePath); + auto encodeResult = payload.encode(); + REQUIRE(encodeResult.has_value()); + + // Corrupt the numFiles field (located before entry point path) + // Format: [...][compressed_size][uncompressed_size][num_files][entry_point_path][entry_point_path_length][LUTEBYTE] + std::string corruptedPayload = encodeResult->payload; + + // Find the numFiles field: 8 bytes (magic) + 4 bytes (entry path len) + entry path len + 4 bytes (num_files) + size_t magicSize = 8; // "LUTEBYTE" + + // Read entry point path length from the correct position + size_t entryPathLenPos = corruptedPayload.size() - magicSize - sizeof(uint32_t); + uint32_t entryPathLen; + memcpy(&entryPathLen, corruptedPayload.data() + entryPathLenPos, sizeof(uint32_t)); + + // numFiles is right before the entry point path + size_t numFilesPos = corruptedPayload.size() - magicSize - sizeof(uint32_t) - entryPathLen - sizeof(uint32_t); + + // Change numFiles from 1 to 99 + uint32_t fakeNumFiles = 99; + memcpy(corruptedPayload.data() + numFilesPos, &fakeNumFiles, sizeof(uint32_t)); + + // Decoding should fail due to numFiles mismatch + auto decodeResult = LuteExePayload::decode(corruptedPayload); + CHECK(!decodeResult.has_value()); +} From a22651ec749deee7aff92f2c5059964333d68c4e Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:11:17 -0700 Subject: [PATCH 093/642] refactor: add std tests folder and rename astSerializer to parser (#492) --- tests/{testAstSerializer.test.luau => parser.test.luau} | 4 ++-- .../{astSerializerTests => parserExamples}/assignment-1.luau | 0 .../{astSerializerTests => parserExamples}/attributes-1.luau | 0 .../break-continue-1.luau | 0 .../compound-assignment-1.luau | 0 .../compound-types-1.luau | 0 .../compound-types-2.luau | 0 .../compound-types-3.luau | 0 .../function-declaration-1.luau | 0 .../function-declaration-2.luau | 0 .../function-declaration-3.luau | 0 .../function-declaration-4.luau | 0 .../function-type-1.luau | 0 .../function-type-2.luau | 0 .../function-type-3.luau | 0 .../generic-for-loop-1.luau | 0 .../if-expression-1.luau | 0 .../if-expression-2.luau | 0 .../if-statement-1.luau | 0 .../interpolated-string-1.luau | 0 .../interpolated-string-2.luau | 0 .../local-assignment-1.luau | 0 .../local-function-declaration-1.luau | 0 .../numeric-for-loop-1.luau | 0 .../repeat-until-1.luau | 0 tests/{astSerializerTests => parserExamples}/table-1.luau | 0 tests/{astSerializerTests => parserExamples}/table-2.luau | 0 .../{astSerializerTests => parserExamples}/type-alias-1.luau | 0 .../{astSerializerTests => parserExamples}/type-alias-2.luau | 0 .../type-assertion-1.luau | 0 .../type-function-1.luau | 0 .../type-singletons-1.luau | 0 .../{astSerializerTests => parserExamples}/type-tables-1.luau | 0 .../{astSerializerTests => parserExamples}/type-tables-2.luau | 0 tests/{astSerializerTests => parserExamples}/while-1.luau | 0 tests/{stdfs.test.luau => std/fs.test.luau} | 0 tests/{stdprocess.test.luau => std/process.test.luau} | 0 37 files changed, 2 insertions(+), 2 deletions(-) rename tests/{testAstSerializer.test.luau => parser.test.luau} (98%) rename tests/{astSerializerTests => parserExamples}/assignment-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/attributes-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/break-continue-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/compound-assignment-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/compound-types-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/compound-types-2.luau (100%) rename tests/{astSerializerTests => parserExamples}/compound-types-3.luau (100%) rename tests/{astSerializerTests => parserExamples}/function-declaration-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/function-declaration-2.luau (100%) rename tests/{astSerializerTests => parserExamples}/function-declaration-3.luau (100%) rename tests/{astSerializerTests => parserExamples}/function-declaration-4.luau (100%) rename tests/{astSerializerTests => parserExamples}/function-type-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/function-type-2.luau (100%) rename tests/{astSerializerTests => parserExamples}/function-type-3.luau (100%) rename tests/{astSerializerTests => parserExamples}/generic-for-loop-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/if-expression-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/if-expression-2.luau (100%) rename tests/{astSerializerTests => parserExamples}/if-statement-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/interpolated-string-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/interpolated-string-2.luau (100%) rename tests/{astSerializerTests => parserExamples}/local-assignment-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/local-function-declaration-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/numeric-for-loop-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/repeat-until-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/table-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/table-2.luau (100%) rename tests/{astSerializerTests => parserExamples}/type-alias-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/type-alias-2.luau (100%) rename tests/{astSerializerTests => parserExamples}/type-assertion-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/type-function-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/type-singletons-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/type-tables-1.luau (100%) rename tests/{astSerializerTests => parserExamples}/type-tables-2.luau (100%) rename tests/{astSerializerTests => parserExamples}/while-1.luau (100%) rename tests/{stdfs.test.luau => std/fs.test.luau} (100%) rename tests/{stdprocess.test.luau => std/process.test.luau} (100%) diff --git a/tests/testAstSerializer.test.luau b/tests/parser.test.luau similarity index 98% rename from tests/testAstSerializer.test.luau rename to tests/parser.test.luau index 92e2fccde..bc4d2b666 100644 --- a/tests/testAstSerializer.test.luau +++ b/tests/parser.test.luau @@ -22,7 +22,7 @@ local function assertEqualsLocation( assert.eq(actual["end"].column, endColumn) end -test.suite("AstSerializer", function(suite) +test.suite("Parser", function(suite) suite:case("tokenContainsLeadingSpaces", function(assert) local block = luau.parse(" local x = 1").root assert.eq(#block.statements, 1) @@ -135,7 +135,7 @@ test.suite("AstSerializer", function(suite) end visitDirectory("examples") - visitDirectory("tests/astSerializerTests") + visitDirectory("tests/parserExamples") end) suite:case("lineOffsetsField", function(assert) diff --git a/tests/astSerializerTests/assignment-1.luau b/tests/parserExamples/assignment-1.luau similarity index 100% rename from tests/astSerializerTests/assignment-1.luau rename to tests/parserExamples/assignment-1.luau diff --git a/tests/astSerializerTests/attributes-1.luau b/tests/parserExamples/attributes-1.luau similarity index 100% rename from tests/astSerializerTests/attributes-1.luau rename to tests/parserExamples/attributes-1.luau diff --git a/tests/astSerializerTests/break-continue-1.luau b/tests/parserExamples/break-continue-1.luau similarity index 100% rename from tests/astSerializerTests/break-continue-1.luau rename to tests/parserExamples/break-continue-1.luau diff --git a/tests/astSerializerTests/compound-assignment-1.luau b/tests/parserExamples/compound-assignment-1.luau similarity index 100% rename from tests/astSerializerTests/compound-assignment-1.luau rename to tests/parserExamples/compound-assignment-1.luau diff --git a/tests/astSerializerTests/compound-types-1.luau b/tests/parserExamples/compound-types-1.luau similarity index 100% rename from tests/astSerializerTests/compound-types-1.luau rename to tests/parserExamples/compound-types-1.luau diff --git a/tests/astSerializerTests/compound-types-2.luau b/tests/parserExamples/compound-types-2.luau similarity index 100% rename from tests/astSerializerTests/compound-types-2.luau rename to tests/parserExamples/compound-types-2.luau diff --git a/tests/astSerializerTests/compound-types-3.luau b/tests/parserExamples/compound-types-3.luau similarity index 100% rename from tests/astSerializerTests/compound-types-3.luau rename to tests/parserExamples/compound-types-3.luau diff --git a/tests/astSerializerTests/function-declaration-1.luau b/tests/parserExamples/function-declaration-1.luau similarity index 100% rename from tests/astSerializerTests/function-declaration-1.luau rename to tests/parserExamples/function-declaration-1.luau diff --git a/tests/astSerializerTests/function-declaration-2.luau b/tests/parserExamples/function-declaration-2.luau similarity index 100% rename from tests/astSerializerTests/function-declaration-2.luau rename to tests/parserExamples/function-declaration-2.luau diff --git a/tests/astSerializerTests/function-declaration-3.luau b/tests/parserExamples/function-declaration-3.luau similarity index 100% rename from tests/astSerializerTests/function-declaration-3.luau rename to tests/parserExamples/function-declaration-3.luau diff --git a/tests/astSerializerTests/function-declaration-4.luau b/tests/parserExamples/function-declaration-4.luau similarity index 100% rename from tests/astSerializerTests/function-declaration-4.luau rename to tests/parserExamples/function-declaration-4.luau diff --git a/tests/astSerializerTests/function-type-1.luau b/tests/parserExamples/function-type-1.luau similarity index 100% rename from tests/astSerializerTests/function-type-1.luau rename to tests/parserExamples/function-type-1.luau diff --git a/tests/astSerializerTests/function-type-2.luau b/tests/parserExamples/function-type-2.luau similarity index 100% rename from tests/astSerializerTests/function-type-2.luau rename to tests/parserExamples/function-type-2.luau diff --git a/tests/astSerializerTests/function-type-3.luau b/tests/parserExamples/function-type-3.luau similarity index 100% rename from tests/astSerializerTests/function-type-3.luau rename to tests/parserExamples/function-type-3.luau diff --git a/tests/astSerializerTests/generic-for-loop-1.luau b/tests/parserExamples/generic-for-loop-1.luau similarity index 100% rename from tests/astSerializerTests/generic-for-loop-1.luau rename to tests/parserExamples/generic-for-loop-1.luau diff --git a/tests/astSerializerTests/if-expression-1.luau b/tests/parserExamples/if-expression-1.luau similarity index 100% rename from tests/astSerializerTests/if-expression-1.luau rename to tests/parserExamples/if-expression-1.luau diff --git a/tests/astSerializerTests/if-expression-2.luau b/tests/parserExamples/if-expression-2.luau similarity index 100% rename from tests/astSerializerTests/if-expression-2.luau rename to tests/parserExamples/if-expression-2.luau diff --git a/tests/astSerializerTests/if-statement-1.luau b/tests/parserExamples/if-statement-1.luau similarity index 100% rename from tests/astSerializerTests/if-statement-1.luau rename to tests/parserExamples/if-statement-1.luau diff --git a/tests/astSerializerTests/interpolated-string-1.luau b/tests/parserExamples/interpolated-string-1.luau similarity index 100% rename from tests/astSerializerTests/interpolated-string-1.luau rename to tests/parserExamples/interpolated-string-1.luau diff --git a/tests/astSerializerTests/interpolated-string-2.luau b/tests/parserExamples/interpolated-string-2.luau similarity index 100% rename from tests/astSerializerTests/interpolated-string-2.luau rename to tests/parserExamples/interpolated-string-2.luau diff --git a/tests/astSerializerTests/local-assignment-1.luau b/tests/parserExamples/local-assignment-1.luau similarity index 100% rename from tests/astSerializerTests/local-assignment-1.luau rename to tests/parserExamples/local-assignment-1.luau diff --git a/tests/astSerializerTests/local-function-declaration-1.luau b/tests/parserExamples/local-function-declaration-1.luau similarity index 100% rename from tests/astSerializerTests/local-function-declaration-1.luau rename to tests/parserExamples/local-function-declaration-1.luau diff --git a/tests/astSerializerTests/numeric-for-loop-1.luau b/tests/parserExamples/numeric-for-loop-1.luau similarity index 100% rename from tests/astSerializerTests/numeric-for-loop-1.luau rename to tests/parserExamples/numeric-for-loop-1.luau diff --git a/tests/astSerializerTests/repeat-until-1.luau b/tests/parserExamples/repeat-until-1.luau similarity index 100% rename from tests/astSerializerTests/repeat-until-1.luau rename to tests/parserExamples/repeat-until-1.luau diff --git a/tests/astSerializerTests/table-1.luau b/tests/parserExamples/table-1.luau similarity index 100% rename from tests/astSerializerTests/table-1.luau rename to tests/parserExamples/table-1.luau diff --git a/tests/astSerializerTests/table-2.luau b/tests/parserExamples/table-2.luau similarity index 100% rename from tests/astSerializerTests/table-2.luau rename to tests/parserExamples/table-2.luau diff --git a/tests/astSerializerTests/type-alias-1.luau b/tests/parserExamples/type-alias-1.luau similarity index 100% rename from tests/astSerializerTests/type-alias-1.luau rename to tests/parserExamples/type-alias-1.luau diff --git a/tests/astSerializerTests/type-alias-2.luau b/tests/parserExamples/type-alias-2.luau similarity index 100% rename from tests/astSerializerTests/type-alias-2.luau rename to tests/parserExamples/type-alias-2.luau diff --git a/tests/astSerializerTests/type-assertion-1.luau b/tests/parserExamples/type-assertion-1.luau similarity index 100% rename from tests/astSerializerTests/type-assertion-1.luau rename to tests/parserExamples/type-assertion-1.luau diff --git a/tests/astSerializerTests/type-function-1.luau b/tests/parserExamples/type-function-1.luau similarity index 100% rename from tests/astSerializerTests/type-function-1.luau rename to tests/parserExamples/type-function-1.luau diff --git a/tests/astSerializerTests/type-singletons-1.luau b/tests/parserExamples/type-singletons-1.luau similarity index 100% rename from tests/astSerializerTests/type-singletons-1.luau rename to tests/parserExamples/type-singletons-1.luau diff --git a/tests/astSerializerTests/type-tables-1.luau b/tests/parserExamples/type-tables-1.luau similarity index 100% rename from tests/astSerializerTests/type-tables-1.luau rename to tests/parserExamples/type-tables-1.luau diff --git a/tests/astSerializerTests/type-tables-2.luau b/tests/parserExamples/type-tables-2.luau similarity index 100% rename from tests/astSerializerTests/type-tables-2.luau rename to tests/parserExamples/type-tables-2.luau diff --git a/tests/astSerializerTests/while-1.luau b/tests/parserExamples/while-1.luau similarity index 100% rename from tests/astSerializerTests/while-1.luau rename to tests/parserExamples/while-1.luau diff --git a/tests/stdfs.test.luau b/tests/std/fs.test.luau similarity index 100% rename from tests/stdfs.test.luau rename to tests/std/fs.test.luau diff --git a/tests/stdprocess.test.luau b/tests/std/process.test.luau similarity index 100% rename from tests/stdprocess.test.luau rename to tests/std/process.test.luau From f708ab37c33fd89455d4cf823ab21c05ac293884 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 29 Oct 2025 14:40:37 -0700 Subject: [PATCH 094/642] lute transform: add exhaustive match back (#494) `exhaustiveMatch` got accidentally deleted in a previous PR. Also took the chance to update the transform test since we have `tempDir` now. The migration processing code is already written in a way that `exhaustiveMatch` is never actually reachable right now, but it's a good check to have in case we expand the migration API in the future. --- lute/cli/commands/transform/init.luau | 4 ++++ tests/transform.test.luau | 32 +++++---------------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index f8b617551..e6fb05de6 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -6,6 +6,10 @@ local arguments = require("@self/lib/arguments") local files = require("@self/lib/files") local types = require("@self/lib/types") +local function exhaustiveMatch(value: never): never + error(`Unknown value in exhaustive match: {value}`) +end + local function loadMigration(path: string): types.Migration local success, loaded = pcall(luau.loadbypath, path) assert(success, `{path} failed to require: {loaded}`) diff --git a/tests/transform.test.luau b/tests/transform.test.luau index 8d9b68968..aadb60c3e 100644 --- a/tests/transform.test.luau +++ b/tests/transform.test.luau @@ -1,6 +1,7 @@ -local fs = require("@lute/fs") +local fs = require("@std/fs") local path = require("@std/path") local process = require("@lute/process") +local system = require("@std/system") local test = require("@std/test") local TRANSFORMEE_CONTENT = [[ @@ -9,39 +10,25 @@ local b = x ~= x ]] local lutePath = process.execpath() +local tmpDir = system.tmpdir() test.suite("lute transform", function(suite) - local transformTestDir = path.format(path.join("build", "transform_tests")) - - suite:beforeall(function() - -- Make a temp folder in the build directory - if not fs.exists("build") then - fs.mkdir("build") - end - - if not fs.exists(transformTestDir) then - fs.mkdir(transformTestDir) - end - end) - suite:case("transform visitor style", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau local transformerExample = path.format(path.join("examples", "transformer.luau")) - local transformerDest = path.format(path.join(transformTestDir, "transformer.luau")) + local transformerDest = path.format(path.join(tmpDir, "transformer.luau")) fs.copy(transformerExample, transformerDest) -- Create a transformee file - local transformeePath = path.format(path.join(transformTestDir, "transformee.luau")) + local transformeePath = path.format(path.join(tmpDir, "transformee.luau")) local transformeeHandle = fs.open(transformeePath, "w+") fs.write(transformeeHandle, TRANSFORMEE_CONTENT) fs.close(transformeeHandle) -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "transform", transformerDest, transformeePath }, { - cwd = process.cwd(), - }) + local result = process.run({ lutePath, "transform", transformerDest, transformeePath }) -- Check assert.eq(result.exitcode, 0) @@ -56,13 +43,6 @@ test.suite("lute transform", function(suite) fs.remove(transformerDest) fs.remove(transformeePath) end) - - suite:afterall(function() - for _, file in fs.listdir(transformTestDir) do - fs.remove(path.format(path.join(transformTestDir, file.name))) - end - fs.rmdir(transformTestDir) - end) end) test.run() From 061c1a82ab8a3564d29c92d9db6b4e92639a91e3 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 29 Oct 2025 16:14:10 -0700 Subject: [PATCH 095/642] restructure std tests (#495) I moved some tests around to match the source folder structure so it's more obvious where tests should go when they're added in the future. Open to any feedback! --- tests/{ => cli}/compile.test.luau | 4 ++-- tests/{ => cli}/loadbypath.test.luau | 4 ++-- tests/{ => cli}/transform.test.luau | 0 tests/{ => std/path}/path.posix.test.luau | 0 tests/{ => std/path}/path.win32.test.luau | 0 tests/{ => std/syntax}/parser.test.luau | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename tests/{ => cli}/compile.test.luau (93%) rename tests/{ => cli}/loadbypath.test.luau (94%) rename tests/{ => cli}/transform.test.luau (100%) rename tests/{ => std/path}/path.posix.test.luau (100%) rename tests/{ => std/path}/path.win32.test.luau (100%) rename tests/{ => std/syntax}/parser.test.luau (100%) diff --git a/tests/compile.test.luau b/tests/cli/compile.test.luau similarity index 93% rename from tests/compile.test.luau rename to tests/cli/compile.test.luau index 93b928b66..89f4ffd90 100644 --- a/tests/compile.test.luau +++ b/tests/cli/compile.test.luau @@ -23,8 +23,8 @@ local COMPILEE_CONTENTS = [[return "Hello world"]] local lutePath = process.execpath() local tmpDir = system.tmpdir() -test.suite("Lute CLI", function(suite) - suite:case("help1", function(check) +test.suite("Lute CLI Compile", function(suite) + suite:case("Run compile", function(check) -- Setup -- Create files local compilerPath = path.join(tmpDir, "compiler.luau") diff --git a/tests/loadbypath.test.luau b/tests/cli/loadbypath.test.luau similarity index 94% rename from tests/loadbypath.test.luau rename to tests/cli/loadbypath.test.luau index 3754faa46..ed61f4e4d 100644 --- a/tests/loadbypath.test.luau +++ b/tests/cli/loadbypath.test.luau @@ -21,7 +21,7 @@ local tmpDirExists = fs.exists(tmpDirStr) local testDir = path.join(tmpDirStr, "lute_require_test") local testDirStr = path.format(testDir) -test.suite("Lute CLI", function(suite) +test.suite("Lute CLI Run", function(suite) suite:beforeall(function() if not tmpDirExists then fs.mkdir(tmpDirStr) @@ -32,7 +32,7 @@ test.suite("Lute CLI", function(suite) end end) - suite:case("help1", function(check) + suite:case("loadbypath", function(check) -- Setup -- Create files local requirerPath = path.format(path.join(testDir, "requirer.luau")) diff --git a/tests/transform.test.luau b/tests/cli/transform.test.luau similarity index 100% rename from tests/transform.test.luau rename to tests/cli/transform.test.luau diff --git a/tests/path.posix.test.luau b/tests/std/path/path.posix.test.luau similarity index 100% rename from tests/path.posix.test.luau rename to tests/std/path/path.posix.test.luau diff --git a/tests/path.win32.test.luau b/tests/std/path/path.win32.test.luau similarity index 100% rename from tests/path.win32.test.luau rename to tests/std/path/path.win32.test.luau diff --git a/tests/parser.test.luau b/tests/std/syntax/parser.test.luau similarity index 100% rename from tests/parser.test.luau rename to tests/std/syntax/parser.test.luau From 70e4df9251648fb92cfa2998a602fcda1f8ac630 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:07:11 -0700 Subject: [PATCH 096/642] refactor: stdlib/path/init.luau file to follow style of `definitions/` (#497) --- lute/std/libs/path/init.luau | 37 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau index 89fd3828b..a4e1704dd 100644 --- a/lute/std/libs/path/init.luau +++ b/lute/std/libs/path/init.luau @@ -5,12 +5,14 @@ local win32 = require("@self/win32") local pathtypes = require("@self/types") +local pathlib = {} + export type path = pathtypes.path export type pathlike = pathtypes.pathlike local onWindows = platforminfo.win32 -local function basename(path: pathlike): string? +function pathlib.basename(path: pathlike): string? if onWindows then return win32.basename(path :: win32.pathlike) else @@ -18,7 +20,7 @@ local function basename(path: pathlike): string? end end -local function dirname(path: pathlike): string +function pathlib.dirname(path: pathlike): string if onWindows then return win32.dirname(path :: win32.pathlike) else @@ -26,7 +28,7 @@ local function dirname(path: pathlike): string end end -local function extname(path: pathlike): string +function pathlib.extname(path: pathlike): string if onWindows then return win32.extname(path :: win32.pathlike) else @@ -34,7 +36,7 @@ local function extname(path: pathlike): string end end -local function format(path: pathlike): string +function pathlib.format(path: pathlike): string if onWindows then return win32.format(path :: win32.pathlike) else @@ -42,7 +44,7 @@ local function format(path: pathlike): string end end -local function isabsolute(path: pathlike): boolean +function pathlib.isabsolute(path: pathlike): boolean if onWindows then return win32.isabsolute(path :: win32.pathlike) else @@ -50,7 +52,7 @@ local function isabsolute(path: pathlike): boolean end end -local function join(...: pathlike): path +function pathlib.join(...: pathlike): path if onWindows then return win32.join(...) else @@ -58,7 +60,7 @@ local function join(...: pathlike): path end end -local function normalize(path: pathlike): path +function pathlib.normalize(path: pathlike): path if onWindows then return win32.normalize(path :: win32.pathlike) else @@ -66,7 +68,7 @@ local function normalize(path: pathlike): path end end -local function parse(path: pathlike): path +function pathlib.parse(path: pathlike): path if onWindows then return win32.parse(path :: win32.pathlike) else @@ -74,7 +76,7 @@ local function parse(path: pathlike): path end end -function resolve(...: pathlike): path +function pathlib.resolve(...: pathlike): path if onWindows then return win32.resolve(...) else @@ -82,16 +84,7 @@ function resolve(...: pathlike): path end end -return table.freeze({ - win32 = win32, - posix = posix, - basename = basename, - dirname = dirname, - extname = extname, - format = format, - isabsolute = isabsolute, - join = join, - normalize = normalize, - parse = parse, - resolve = resolve, -}) +pathlib.win32 = win32 +pathlib.posix = posix + +return table.freeze(pathlib) From f42c262d876a5edaae3b2aa7c6c2af4190d6cffd Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 30 Oct 2025 09:44:43 -0700 Subject: [PATCH 097/642] refactor: rename exprifelse field names (#493) These field names are [misleading](https://home.sandiego.edu/~baber/logic/conditionals), so rename them to match what we have in our C++ Ast types. Also, stop exporting AstExprIfElseIfs, because it's more like a utility type rather than an actual AST node. --- definitions/luau.luau | 12 ++++++------ lute/luau/src/luau.cpp | 14 +++++++------- lute/std/libs/syntax/visitor.luau | 14 +++++++------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index a017f8d2d..96d175f21 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -190,7 +190,7 @@ export type AstExprIfElseIfs = { elseifkeyword: Token<"elseif">, condition: AstExpr, thenkeyword: Token<"then">, - consequent: AstExpr, + thenexpr: AstExpr, } export type AstExprIfElse = { @@ -198,10 +198,10 @@ export type AstExprIfElse = { ifkeyword: Token<"if">, condition: AstExpr, thenkeyword: Token<"then">, - consequent: AstExpr, + thenexpr: AstExpr, elseifs: { AstExprIfElseIfs }, elsekeyword: Token<"else">, - antecedent: AstExpr, + elseexpr: AstExpr, } export type AstExpr = @@ -233,7 +233,7 @@ export type AstStatElseIf = { elseifkeyword: Token<"elseif">, condition: AstExpr, thenkeyword: Token<"then">, - consequent: AstStatBlock, + thenblock: AstStatBlock, } export type AstStatIf = { @@ -241,10 +241,10 @@ export type AstStatIf = { ifkeyword: Token<"if">, condition: AstExpr, thenkeyword: Token<"then">, - consequent: AstStatBlock, + thenblock: AstStatBlock, elseifs: { AstStatElseIf }, elsekeyword: Token<"else">?, -- TODO: this could be elseif! - antecedent: AstStatBlock?, + elseblock: AstStatBlock?, endkeyword: Token<"end">, } diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 715977a99..f92502234 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1045,7 +1045,7 @@ struct AstSerialize : public Luau::AstVisitor } else lua_pushnil(L); - lua_setfield(L, -2, "consequent"); + lua_setfield(L, -2, "thenexpr"); lua_createtable(L, 0, preambleSize + 4); int i = 0; @@ -1070,7 +1070,7 @@ struct AstSerialize : public Luau::AstVisitor } else lua_pushnil(L); - lua_setfield(L, -2, "consequent"); + lua_setfield(L, -2, "thenexpr"); lua_rawseti(L, -2, i + 1); i++; @@ -1085,7 +1085,7 @@ struct AstSerialize : public Luau::AstVisitor } else lua_pushnil(L); - lua_setfield(L, -2, "antecedent"); + lua_setfield(L, -2, "elseexpr"); } void serialize(Luau::AstExprInterpString* node) @@ -1166,7 +1166,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "thenkeyword"); node->thenbody->visit(this); - lua_setfield(L, -2, "consequent"); + lua_setfield(L, -2, "thenblock"); lua_createtable(L, 0, 0); int i = 0; @@ -1185,7 +1185,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "thenkeyword"); elseif->thenbody->visit(this); - lua_setfield(L, -2, "consequent"); + lua_setfield(L, -2, "thenblock"); lua_rawseti(L, -2, i + 1); node = elseif; @@ -1200,7 +1200,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "elsekeyword"); node->elsebody->visit(this); - lua_setfield(L, -2, "antecedent"); + lua_setfield(L, -2, "elseblock"); serializeToken(node->elsebody->location.end, "end"); lua_setfield(L, -2, "endkeyword"); @@ -1211,7 +1211,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "elsekeyword"); lua_pushnil(L); - lua_setfield(L, -2, "antecedent"); + lua_setfield(L, -2, "elseblock"); serializeToken(node->thenbody->location.end, "end"); lua_setfield(L, -2, "endkeyword"); diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 53fcf75c8..87b270eb1 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -168,18 +168,18 @@ local function visitIf(node: luau.AstStatIf, visitor: Visitor) visitToken(node.ifkeyword, visitor) visitExpression(node.condition, visitor) visitToken(node.thenkeyword, visitor) - visitBlock(node.consequent, visitor) + visitBlock(node.thenblock, visitor) for _, elseifNode in node.elseifs do visitToken(elseifNode.elseifkeyword, visitor) visitExpression(elseifNode.condition, visitor) visitToken(elseifNode.thenkeyword, visitor) - visitBlock(elseifNode.consequent, visitor) + visitBlock(elseifNode.thenblock, visitor) end if node.elsekeyword then visitToken(node.elsekeyword, visitor) end - if node.antecedent then - visitBlock(node.antecedent, visitor) + if node.elseblock then + visitBlock(node.elseblock, visitor) end visitToken(node.endkeyword, visitor) end @@ -554,15 +554,15 @@ local function visitIfExpression(node: luau.AstExprIfElse, visitor: Visitor) visitToken(node.ifkeyword, visitor) visitExpression(node.condition, visitor) visitToken(node.thenkeyword, visitor) - visitExpression(node.consequent, visitor) + visitExpression(node.thenexpr, visitor) for _, elseifs in node.elseifs do visitToken(elseifs.elseifkeyword, visitor) visitExpression(elseifs.condition, visitor) visitToken(elseifs.thenkeyword, visitor) - visitExpression(elseifs.consequent, visitor) + visitExpression(elseifs.thenexpr, visitor) end visitToken(node.elsekeyword, visitor) - visitExpression(node.antecedent, visitor) + visitExpression(node.elseexpr, visitor) end end From 760ea45da2cf54f71543622f4c7e336e60d40487 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 30 Oct 2025 10:37:43 -0700 Subject: [PATCH 098/642] Feature: lute test subcommand (#498) This PR introduces a skeleton for the `lute test` subcommand. In future PR's I'll build out the functionality described in the help message, e.g. `--list, --filter, --reporter = ...` --------- Co-authored-by: Sora Kanosue --- lute/cli/commands/test/init.luau | 44 ++++++++++++++++++++++++++++++++ tests/cli/test.test.luau | 26 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 lute/cli/commands/test/init.luau create mode 100644 tests/cli/test.test.luau diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau new file mode 100644 index 000000000..f611245f3 --- /dev/null +++ b/lute/cli/commands/test/init.luau @@ -0,0 +1,44 @@ +local function printHelp() + print([[ +Usage: lute test [OPTIONS] [PATHS...] + +Run tests discovered in .test.luau and .spec.luau files. + +OPTIONS: + -h, --help Show this help message + --list List all discovered test cases without running them + --filter=PATTERN Run only tests matching PATTERN + Examples: + --filter=SuiteName (run all tests in suite) + --filter=SuiteName.testname (run specific test) + --filter=testname (run anonymous test) + --reporter=REPORTER Choose test reporter (default: rich) + Options: + rich - Color output with visual diffs + simple - Plain text output + - Custom reporter from file path + +PATHS: + Directories or files to search for tests (default: ./tests) + +EXAMPLES: + lute test Run all tests in ./tests + lute test --list List all test cases + lute test --filter=MyTestSuite Run all tests in MyTestSuite + lute test --reporter=simple src/ Run tests with simple reporter +]]) +end + +local function main(...: string) + local args = { ... } + + -- Check for help flag + for _, arg in args do + if arg == "-h" or arg == "--help" then + printHelp() + return + end + end +end + +main(...) diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau new file mode 100644 index 000000000..9c2b891d8 --- /dev/null +++ b/tests/cli/test.test.luau @@ -0,0 +1,26 @@ +local path = require("@std/path") +local process = require("@std/process") +local test = require("@std/test") + +local lutePath = path.format(process.execpath()) + +test.suite("lute test", function(suite) + suite:case("lute test help message", function(assert) + -- Run lute test with -h flag + local result = process.run({ lutePath, "test", "-h" }) + + -- Check that it exits successfully and prints help + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Usage:", 1, true), nil) + end) + + suite:case("lute test runs successfully", function(assert) + -- Run lute test without arguments + local result = process.run({ lutePath, "test" }) + + -- Check that it succeeds + assert.eq(result.exitcode, 0) + end) +end) + +test.run() From 0d96184411a732918807b7ab402c0c658942be3c Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 30 Oct 2025 11:54:43 -0700 Subject: [PATCH 099/642] stdlib: add table.extend to the lute standard library (#501) This adds a method to the standard library that takes an arraylike table and extends it by another arraylike table. --- lute/cli/commands/transform/lib/files.luau | 11 +++-------- lute/std/libs/table.luau | 7 +++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lute/cli/commands/transform/lib/files.luau b/lute/cli/commands/transform/lib/files.luau index 65d53df1d..e910a0385 100644 --- a/lute/cli/commands/transform/lib/files.luau +++ b/lute/cli/commands/transform/lib/files.luau @@ -4,12 +4,7 @@ local process = require("@lute/process") local ignore = require("./ignore") local path = require("@std/path") local string = require("@std/string") - -local function extend(tbl: { T }, other: { T }) - for _, entry in other do - table.insert(tbl, entry) - end -end +local tbl = require("@std/table") local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: ignore.IgnoreResolver): { path.path } local results = {} @@ -23,7 +18,7 @@ local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: continue end - extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver)) + tbl.extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver)) else -- TODO: allow customisation of the filter when traversing a directory. e.g., globs? file endings? ignore paths? if string.endswith(entry.name, ".luau") or string.endswith(entry.name, ".lua") then @@ -54,7 +49,7 @@ local function getSourceFiles(paths: { string }): { path.path } filepath = path.format(pathObj) print(filepath) if fs.type(filepath) == "dir" then - extend(files, traverseDirectoryRecursive(pathObj, ignoreResolver)) + tbl.extend(files, traverseDirectoryRecursive(pathObj, ignoreResolver)) else table.insert(files, pathObj) end diff --git a/lute/std/libs/table.luau b/lute/std/libs/table.luau index 5627f43c7..5f467a481 100644 --- a/lute/std/libs/table.luau +++ b/lute/std/libs/table.luau @@ -33,11 +33,18 @@ local function fold(table: { [K]: V }, f: (A, V) -> A, initial: A): A return acc end +local function extend(tbl: array, other: array) + for _, entry in other do + table.insert(tbl, entry) + end +end + return table.freeze({ -- std extension for table map = map, filter = filter, fold = fold, + extend = extend, -- re-exports of the table library clear = table.clear, From b8e3352b3a26b7bc3c00b24c4d1f4b9bb3b9032e Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 30 Oct 2025 12:12:48 -0700 Subject: [PATCH 100/642] Feature: Fetch all .spec.luau files / .test.luau files (#499) This PR updates the `lute test` subcommand to find all .spec.luau / .test.luau files in a given directory, defaulting to ./test --- lute/cli/commands/test/finder.luau | 47 +++++++++++++++++++++++ lute/cli/commands/test/init.luau | 8 ++++ tests/cli/discovery/example.spec.luau | 9 +++++ tests/{ => cli/discovery}/smoke.test.luau | 0 tests/cli/test.test.luau | 9 +++++ 5 files changed, 73 insertions(+) create mode 100644 lute/cli/commands/test/finder.luau create mode 100644 tests/cli/discovery/example.spec.luau rename tests/{ => cli/discovery}/smoke.test.luau (100%) diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau new file mode 100644 index 000000000..74ea75328 --- /dev/null +++ b/lute/cli/commands/test/finder.luau @@ -0,0 +1,47 @@ +local ps = require("@std/process") +local fs = require("@std/fs") +local path = require("@std/path") +local string = require("@std/string") +local table = require("@std/table") + +local function istestfile(filename: string): boolean + return string.endswith(filename, ".test.luau") or string.endswith(filename, ".spec.luau") +end + +local function findtestfilesrec(directory: path.path): { path.path } + local results = {} + + for _, entry in fs.listdir(directory) do + local fullPath = path.join(directory, entry.name) + + if entry.type == "dir" then + table.extend(results, findtestfilesrec(fullPath)) + elseif istestfile(entry.name) then + table.insert(results, fullPath) + end + end + + return results +end + +local function findtestfiles(paths: { string }): { path.path } + local files = {} + local cwd = ps.cwd() + for _, filepath in paths do + local pathObj = path.parse(filepath) + + if not path.isabsolute(pathObj) then + pathObj = path.join(cwd, pathObj) + end + + if fs.type(pathObj) == "dir" then + table.extend(files, findtestfilesrec(pathObj)) + elseif istestfile(filepath) then + table.insert(files, pathObj) + end + end + + return files +end + +return table.freeze({ findtestfiles = findtestfiles }) diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index f611245f3..c67b150fd 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -1,3 +1,5 @@ +local finder = require("@self/finder") + local function printHelp() print([[ Usage: lute test [OPTIONS] [PATHS...] @@ -39,6 +41,12 @@ local function main(...: string) return end end + + local searchpath = if #args > 0 then args else { "./tests" } + + local files = finder.findtestfiles(searchpath) + + print(`Found {#files} test files`) end main(...) diff --git a/tests/cli/discovery/example.spec.luau b/tests/cli/discovery/example.spec.luau new file mode 100644 index 000000000..de73312ba --- /dev/null +++ b/tests/cli/discovery/example.spec.luau @@ -0,0 +1,9 @@ +local test = require("@std/test") + +test.suite("ExampleSuite", function(suite) + suite:case("should pass", function(assert) + assert.eq(2 + 2, 4) + end) +end) + +test.run() diff --git a/tests/smoke.test.luau b/tests/cli/discovery/smoke.test.luau similarity index 100% rename from tests/smoke.test.luau rename to tests/cli/discovery/smoke.test.luau diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 9c2b891d8..df6777b68 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -21,6 +21,15 @@ test.suite("lute test", function(suite) -- Check that it succeeds assert.eq(result.exitcode, 0) end) + + suite:case("lute test discovers test files in custom directory", function(assert) + -- Run lute test with custom directory path + local result = process.run({ lutePath, "test", "tests/cli/discovery" }) + + -- Check that it discovers test files (.test.luau and .spec.luau) + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Found 2 test files", 1, true), nil) + end) end) test.run() From 0bc7f7c017daded884b9881ad41c1d19cfb8d0b1 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:19:21 -0700 Subject: [PATCH 101/642] Run clang-format on all .cpp/h files in `lute` directory (#502) Ran these commands: - `git clang-format $(git log --max-parents=0 --format=%H) -- lute/**/*.cpp` - `git clang-format $(git log --max-parents=0 --format=%H) -- lute/**/*.h` --- lute/cli/include/lute/staticrequires.h | 5 +- lute/cli/src/compile.cpp | 46 +++--- lute/cli/src/staticrequires.cpp | 10 +- lute/crypto/include/lute/crypto.h | 5 +- lute/crypto/src/crypto.cpp | 202 ++++++++++++------------- lute/io/src/io.cpp | 7 +- lute/net/src/net.cpp | 14 +- lute/require/include/lute/requirevfs.h | 5 +- lute/require/src/bundlevfs.cpp | 20 ++- 9 files changed, 168 insertions(+), 146 deletions(-) diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h index 35f6dccf2..f43066909 100644 --- a/lute/cli/include/lute/staticrequires.h +++ b/lute/cli/include/lute/staticrequires.h @@ -19,7 +19,10 @@ class StaticRequireTracer // Get the require graph built during the last trace // Maps each file to the list of files it requires - const Luau::DenseHashMap>& getRequireGraph() const { return requireGraph; } + const Luau::DenseHashMap>& getRequireGraph() const + { + return requireGraph; + } private: Luau::DenseHashSet visited{""}; diff --git a/lute/cli/src/compile.cpp b/lute/cli/src/compile.cpp index 265106675..5c9f0395f 100644 --- a/lute/cli/src/compile.cpp +++ b/lute/cli/src/compile.cpp @@ -62,7 +62,8 @@ AppendedBytecodeResult checkForAppendedBytecode() exeFile.seekg(fileSize - static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE)); exeFile.read(reinterpret_cast(&BytecodeSize), BYTECODE_SIZE_FIELD_SIZE); - if (fileSize < static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE + BytecodeSize)) { + if (fileSize < static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE + BytecodeSize)) + { fprintf(stderr, "Warning: Found magic flag but file size inconsistent.\n"); exeFile.close(); return result; @@ -116,14 +117,15 @@ int compileScript(const std::string& inputFilePath, const std::string& outputFil std::vector exeBuffer(exeSize); if (!exeFile.read(exeBuffer.data(), exeSize)) { - fprintf(stderr, "Error reading current executable %s\n", currentExecutablePath.c_str()); - exeFile.close(); - return 1; + fprintf(stderr, "Error reading current executable %s\n", currentExecutablePath.c_str()); + exeFile.close(); + return 1; } exeFile.close(); std::ofstream outFile(outputFilePath, std::ios::binary | std::ios::trunc); - if (!outFile) { + if (!outFile) + { fprintf(stderr, "Error creating output file %s\n", outputFilePath.c_str()); return 1; } @@ -139,10 +141,10 @@ int compileScript(const std::string& inputFilePath, const std::string& outputFil if (!outFile.good()) { - fprintf(stderr, "Error writing to output file %s\n", outputFilePath.c_str()); - outFile.close(); - remove(outputFilePath.c_str()); - return 1; + fprintf(stderr, "Error writing to output file %s\n", outputFilePath.c_str()); + outFile.close(); + remove(outputFilePath.c_str()); + return 1; } outFile.close(); @@ -221,11 +223,11 @@ std::optional LuteExePayload::encode() // Compress with maximum compression level int compressResult = compress2( - compressedData.data(), // destination buffer - &compressedSize, // in/out: buffer size / actual compressed size - reinterpret_cast(uncompressedBundle.data()), // source data - uncompressedSize, // source size - Z_BEST_COMPRESSION // compression level (9) + compressedData.data(), // destination buffer + &compressedSize, // in/out: buffer size / actual compressed size + reinterpret_cast(uncompressedBundle.data()), // source data + uncompressedSize, // source size + Z_BEST_COMPRESSION // compression level (9) ); if (compressResult != Z_OK) @@ -271,7 +273,7 @@ std::optional LuteExePayload::encode() result.bytesWritten = result.payload.size(); result.compressedPayloadSizeBytes = compressedSize; result.uncompressedPayloadSizeBytes = uncompressedSize; - + return result; } @@ -311,7 +313,8 @@ std::optional LuteExePayload::decode(const std::string_view bi }; // Helper to read variable-length bytes backwards - auto readBytes = [&binary](size_t& pos, size_t length, const char* fieldName) -> std::optional { + auto readBytes = [&binary](size_t& pos, size_t length, const char* fieldName) -> std::optional + { if (pos < length) { fprintf(stderr, "Decode failed: Incomplete %s field\n", fieldName); @@ -321,7 +324,8 @@ std::optional LuteExePayload::decode(const std::string_view bi return std::string(binary.data() + pos, length); }; - // Read metadata from LUTEBYTE back to entry_point_path_length: [entry_point_path_length][entry_point_path][num_files][uncompressed_size][compressed_size][compressed_data]...[LUTEBYTE] + // Read metadata from LUTEBYTE back to entry_point_path_length: + // [entry_point_path_length][entry_point_path][num_files][uncompressed_size][compressed_size][compressed_data]...[LUTEBYTE] size_t pos = binary.size() - MAGIC_FLAG_SIZE; // Read entry_point_path_length @@ -361,12 +365,8 @@ std::optional LuteExePayload::decode(const std::string_view bi // Decompress the bundle std::vector uncompressedData(uncompressedSize); uLongf actualUncompressedSize = uncompressedSize; - int zlibResult = uncompress( - uncompressedData.data(), - &actualUncompressedSize, - reinterpret_cast(binary.data() + pos), - compressedSize - ); + int zlibResult = + uncompress(uncompressedData.data(), &actualUncompressedSize, reinterpret_cast(binary.data() + pos), compressedSize); if (zlibResult != Z_OK) { diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index c3562e057..a9cfecbdd 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -127,12 +127,14 @@ std::optional StaticRequireTracer::resolveRequire(const std::string } // Helper functions for ModulePath - paths are relative to rootDirectory - auto isFileFunc = [this](const std::string& path) -> bool { + auto isFileFunc = [this](const std::string& path) -> bool + { std::string fullPath = joinPaths(rootDirectory, path); return isFile(fullPath); }; - auto isDir = [this](const std::string& path) -> bool { + auto isDir = [this](const std::string& path) -> bool + { std::string fullPath = joinPaths(rootDirectory, path); return isDirectory(fullPath); }; @@ -140,8 +142,8 @@ std::optional StaticRequireTracer::resolveRequire(const std::string // Create a ModulePath with root as rootDirectory, starting at requirer's directory // This allows us to navigate up with .. beyond requirerDir, but not beyond rootDirectory std::optional modulePath = ModulePath::create( - "", // Root is empty (relative path base) - requirerDir, // Start at the requirer's directory + "", // Root is empty (relative path base) + requirerDir, // Start at the requirer's directory isFileFunc, isDir ); diff --git a/lute/crypto/include/lute/crypto.h b/lute/crypto/include/lute/crypto.h index 5c4e64b5e..e8c3711f7 100644 --- a/lute/crypto/include/lute/crypto.h +++ b/lute/crypto/include/lute/crypto.h @@ -25,10 +25,7 @@ int lua_pwhash(lua_State* L); static const char kVerifyPasswordHashName[] = "verify"; int lua_pwhash_verify(lua_State* L); -static const luaL_Reg lib[] = { - {kDigestName, lua_digest}, - {nullptr, nullptr} -}; +static const luaL_Reg lib[] = {{kDigestName, lua_digest}, {nullptr, nullptr}}; static const std::string properties[] = { kHashProperty, diff --git a/lute/crypto/src/crypto.cpp b/lute/crypto/src/crypto.cpp index fc474e7c0..b92f27b1b 100644 --- a/lute/crypto/src/crypto.cpp +++ b/lute/crypto/src/crypto.cpp @@ -11,141 +11,141 @@ namespace crypto { - struct HashFunction - { - std::string name; - const env_md_st* md; - }; - - static const int kHashFunctionTag = 81; +struct HashFunction +{ + std::string name; + const env_md_st* md; +}; - static const HashFunction hashFunctions[] = { - {"md5", EVP_md5()}, - {"sha1", EVP_sha1()}, - {"sha256", EVP_sha256()}, - {"sha512", EVP_sha512()}, - {"blake2b256", EVP_blake2b256()}, - }; +static const int kHashFunctionTag = 81; - int makeHashFunctionMap(lua_State* L) - { - lua_createtable(L, 0, std::size(hashFunctions)); +static const HashFunction hashFunctions[] = { + {"md5", EVP_md5()}, + {"sha1", EVP_sha1()}, + {"sha256", EVP_sha256()}, + {"sha512", EVP_sha512()}, + {"blake2b256", EVP_blake2b256()}, +}; - for (auto& [name, md] : hashFunctions) - { - lua_pushlightuserdatatagged(L, (void*) md, kHashFunctionTag); - lua_setfield(L, -2, name.c_str()); - } +int makeHashFunctionMap(lua_State* L) +{ + lua_createtable(L, 0, std::size(hashFunctions)); - return 1; + for (auto& [name, md] : hashFunctions) + { + lua_pushlightuserdatatagged(L, (void*)md, kHashFunctionTag); + lua_setfield(L, -2, name.c_str()); } - const env_md_st* getHashFunction(lua_State* L, int idx) - { - if (auto typ = static_cast(lua_tolightuserdatatagged(L, idx, kHashFunctionTag))) - return typ; + return 1; +} - luaL_typeerrorL(L, idx, "hash function"); - } +const env_md_st* getHashFunction(lua_State* L, int idx) +{ + if (auto typ = static_cast(lua_tolightuserdatatagged(L, idx, kHashFunctionTag))) + return typ; - struct BinaryData - { - const void* data; - size_t length; - }; + luaL_typeerrorL(L, idx, "hash function"); +} - BinaryData extractData(lua_State* L, int idx) - { - if (!lua_isstring(L, idx) && !lua_isbuffer(L, idx)) - luaL_typeerrorL(L, idx, "string or buffer"); +struct BinaryData +{ + const void* data; + size_t length; +}; - if (lua_isstring(L, idx)) - { - size_t length = 0; - const char* data = lua_tolstring(L, idx, &length); +BinaryData extractData(lua_State* L, int idx) +{ + if (!lua_isstring(L, idx) && !lua_isbuffer(L, idx)) + luaL_typeerrorL(L, idx, "string or buffer"); - return BinaryData{data, length}; - } + if (lua_isstring(L, idx)) + { + size_t length = 0; + const char* data = lua_tolstring(L, idx, &length); + return BinaryData{data, length}; + } - if (lua_isbuffer(L, idx)) - { - size_t length = 0; - void* data = lua_tobuffer(L, idx, &length); - return BinaryData{data, length}; - } + if (lua_isbuffer(L, idx)) + { + size_t length = 0; + void* data = lua_tobuffer(L, idx, &length); - luaL_error(L, "failed to extract binary data from stack: %d", idx); + return BinaryData{data, length}; } - int lua_digest(lua_State* L) - { - int argumentCount = lua_gettop(L); - if (argumentCount != 2) - luaL_error(L, "%s: expected 2 arguments, but got %d", kDigestName, argumentCount); + luaL_error(L, "failed to extract binary data from stack: %d", idx); +} - const env_md_st* hashFunction = getHashFunction(L, 1); - BinaryData message = extractData(L, 2); +int lua_digest(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 2) + luaL_error(L, "%s: expected 2 arguments, but got %d", kDigestName, argumentCount); - void* buffer = lua_newbuffer(L, EVP_MD_size(hashFunction)); - if (EVP_Digest(message.data, message.length, (uint8_t*) buffer, nullptr, hashFunction, nullptr) == 0) - luaL_error(L, "%s: failed to compute hash", kDigestName); + const env_md_st* hashFunction = getHashFunction(L, 1); + BinaryData message = extractData(L, 2); - return 1; - } + void* buffer = lua_newbuffer(L, EVP_MD_size(hashFunction)); + if (EVP_Digest(message.data, message.length, (uint8_t*)buffer, nullptr, hashFunction, nullptr) == 0) + luaL_error(L, "%s: failed to compute hash", kDigestName); - // hash(password: string): buffer - int lua_pwhash(lua_State* L) - { - int argumentCount = lua_gettop(L); - if (argumentCount != 1) - luaL_error(L, "%s: expected 1 arguments, but got %d", kPasswordHashName, argumentCount); + return 1; +} - size_t length = 0; - const char* password = luaL_checklstring(L, 1, &length); +// hash(password: string): buffer +int lua_pwhash(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 1) + luaL_error(L, "%s: expected 1 arguments, but got %d", kPasswordHashName, argumentCount); - void* buffer = lua_newbuffer(L, crypto_pwhash_STRBYTES); - if (crypto_pwhash_str((char*) buffer, password, length, - crypto_pwhash_OPSLIMIT_SENSITIVE, crypto_pwhash_MEMLIMIT_SENSITIVE)) { - luaL_error(L, "%s: hit memory limit for password hashing", kPasswordHashName); - } + size_t length = 0; + const char* password = luaL_checklstring(L, 1, &length); - return 1; + void* buffer = lua_newbuffer(L, crypto_pwhash_STRBYTES); + if (crypto_pwhash_str((char*)buffer, password, length, crypto_pwhash_OPSLIMIT_SENSITIVE, crypto_pwhash_MEMLIMIT_SENSITIVE)) + { + luaL_error(L, "%s: hit memory limit for password hashing", kPasswordHashName); } - // verify(hash: buffer, password: string) - int lua_pwhash_verify(lua_State* L) - { - int argumentCount = lua_gettop(L); - if (argumentCount != 2) - luaL_error(L, "%s: expected 2 arguments, but got %d", kPasswordHashName, argumentCount); + return 1; +} - size_t hashLength = crypto_pwhash_STRBYTES; - void* hashedPassword = luaL_checkbuffer(L, 1, &hashLength); +// verify(hash: buffer, password: string) +int lua_pwhash_verify(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 2) + luaL_error(L, "%s: expected 2 arguments, but got %d", kPasswordHashName, argumentCount); - size_t length = 0; - const char* password = luaL_checklstring(L, 2, &length); + size_t hashLength = crypto_pwhash_STRBYTES; + void* hashedPassword = luaL_checkbuffer(L, 1, &hashLength); - lua_pushboolean(L, crypto_pwhash_str_verify((const char*) hashedPassword, password, length) == 0); + size_t length = 0; + const char* password = luaL_checklstring(L, 2, &length); - return 1; - } + lua_pushboolean(L, crypto_pwhash_str_verify((const char*)hashedPassword, password, length) == 0); - int makePasswordHashLibrary(lua_State* L) - { - lua_createtable(L, 0, 2); + return 1; +} - // hash - lua_pushcfunction(L, lua_pwhash, kPasswordHashName); - lua_setfield(L, -2, kPasswordHashName); +int makePasswordHashLibrary(lua_State* L) +{ + lua_createtable(L, 0, 2); - // verify - lua_pushcfunction(L, lua_pwhash_verify, kVerifyPasswordHashName); - lua_setfield(L, -2, kVerifyPasswordHashName); + // hash + lua_pushcfunction(L, lua_pwhash, kPasswordHashName); + lua_setfield(L, -2, kPasswordHashName); - return 1; - } + // verify + lua_pushcfunction(L, lua_pwhash_verify, kVerifyPasswordHashName); + lua_setfield(L, -2, kVerifyPasswordHashName); + + return 1; +} } // namespace crypto diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp index 462bf5cbe..0562f782c 100644 --- a/lute/io/src/io.cpp +++ b/lute/io/src/io.cpp @@ -28,12 +28,13 @@ struct IOHandle ioh->self.reset(); }; - uv_stream_t *stream = getStream(); + uv_stream_t* stream = getStream(); uv_read_stop(stream); uv_close((uv_handle_t*)stream, closeCb); } - uv_stream_t* getStream() { + uv_stream_t* getStream() + { return Luau::visit( [](auto& stream) -> uv_stream_t* { @@ -103,7 +104,7 @@ int read(lua_State* L) luaL_error(L, "Unsupported stdin type"); } - uv_stream_t *stream = handle->getStream(); + uv_stream_t* stream = handle->getStream(); stream->data = handle.get(); uv_read_start(stream, allocBuffer, onTtyRead); return lua_yield(L, 0); diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index e666e1a68..561308396 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -22,13 +22,17 @@ namespace net { static const std::string kEmptyHeaderKey = ""; -struct CurlResponse { +struct CurlResponse +{ std::string error; std::vector body; Luau::DenseHashMap headers; long status = 0; - CurlResponse() : headers(kEmptyHeaderKey) {} + CurlResponse() + : headers(kEmptyHeaderKey) + { + } }; static size_t writeFunction(void* contents, size_t size, size_t nmemb, void* context) @@ -74,8 +78,8 @@ static CurlResponse requestData( { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - } - + } + if (!headers.empty()) { for (const auto& header_pair : headers) @@ -178,7 +182,7 @@ int request(lua_State* L) token->fail("network request failed: " + resp.error); return; } - + token->complete( [resp = std::move(resp)](lua_State* L) { diff --git a/lute/require/include/lute/requirevfs.h b/lute/require/include/lute/requirevfs.h index 9029b7355..a225526b3 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -36,7 +36,10 @@ class RequireVfs bool isConfigPresent(lua_State* L) const; std::string getConfig(lua_State* L) const; - bool isPrecompiled() const { return vfsType == VFSType::Bundle; }; + bool isPrecompiled() const + { + return vfsType == VFSType::Bundle; + }; private: enum class VFSType diff --git a/lute/require/src/bundlevfs.cpp b/lute/require/src/bundlevfs.cpp index a377503cd..51c4387bc 100644 --- a/lute/require/src/bundlevfs.cpp +++ b/lute/require/src/bundlevfs.cpp @@ -53,8 +53,14 @@ NavigationStatus BundleVfs::resetToPath(const std::string& path) modulePath = ModulePath::create( "@bundle", "", - [this](const std::string& p) { return isBundleModule(filePathToBytecode, p); }, - [this](const std::string& p) { return isBundleDirectory(filePathToBytecode, p); } + [this](const std::string& p) + { + return isBundleModule(filePathToBytecode, p); + }, + [this](const std::string& p) + { + return isBundleDirectory(filePathToBytecode, p); + } ); return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; } @@ -69,8 +75,14 @@ NavigationStatus BundleVfs::resetToPath(const std::string& path) modulePath = ModulePath::create( "@bundle", filePath, - [this](const std::string& p) { return isBundleModule(filePathToBytecode, p); }, - [this](const std::string& p) { return isBundleDirectory(filePathToBytecode, p); } + [this](const std::string& p) + { + return isBundleModule(filePathToBytecode, p); + }, + [this](const std::string& p) + { + return isBundleDirectory(filePathToBytecode, p); + } ); return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; From 1e67500bd9d74b0ba04dc2bac8af1372fbd52840 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 30 Oct 2025 14:25:24 -0700 Subject: [PATCH 102/642] stdlib: expose test environment from test library (#503) This exposes a method in std/test to grab the registered test cases out of the internal environment. --------- Co-authored-by: ariel --- lute/std/libs/test.luau | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index 91d31e096..a91c90615 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -127,6 +127,11 @@ function test.suite(name: string, registerFn: (TestSuite) -> ()) table.insert(env.suites, suite) end +-- get all registered tests without running them (internal) +function test._registered() + return table.freeze({ anonymous = env.anonymous, suites = env.suites }) +end + -- run all the tests function test.run() local failures: { FailedTest } = {} From 83e18074884c21ed257974f0a925e2c1ac3011f4 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:35:24 -0700 Subject: [PATCH 103/642] refactor: update test files to use `system.tmpdir` (#504) should be the last files that aren't using `system.tmpdir()` yet --- tests/cli/loadbypath.test.luau | 12 ++---------- tests/std/fs.test.luau | 18 +++++------------- tests/std/process.test.luau | 4 ++-- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/tests/cli/loadbypath.test.luau b/tests/cli/loadbypath.test.luau index ed61f4e4d..0c3c91369 100644 --- a/tests/cli/loadbypath.test.luau +++ b/tests/cli/loadbypath.test.luau @@ -1,6 +1,7 @@ local fs = require("@lute/fs") local process = require("@lute/process") local path = require("@std/path") +local system = require("@std/system") local test = require("@std/test") local REQUIRER_CONTENTS = [[ @@ -16,17 +17,12 @@ local REQUIREE_CONTENTS = [[return "Success"]] local lutePath = process.execpath() -local tmpDirStr = ".tmp" -local tmpDirExists = fs.exists(tmpDirStr) +local tmpDirStr = system.tmpdir() local testDir = path.join(tmpDirStr, "lute_require_test") local testDirStr = path.format(testDir) test.suite("Lute CLI Run", function(suite) suite:beforeall(function() - if not tmpDirExists then - fs.mkdir(tmpDirStr) - end - if not fs.exists(testDirStr) then fs.mkdir(testDirStr) end @@ -58,10 +54,6 @@ test.suite("Lute CLI Run", function(suite) if fs.exists(testDirStr) then fs.rmdir(testDirStr) end - - if not tmpDirExists then - fs.rmdir(tmpDirStr) - end end) end) diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 3e56187b6..d277133cb 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -1,19 +1,11 @@ -local test = require("@std/test") -local path = require("@std/path") local fs = require("@std/fs") -local tmpdir = "tmp" - -test.suite("FsSuite", function(suite) - suite:beforeall(function() - if not fs.exists(tmpdir) then - fs.mkdir(tmpdir) - end - end) +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") - suite:afterall(function() - fs.rmdir(tmpdir) - end) +local tmpdir = system.tmpdir() +test.suite("FsSuite", function(suite) suite:case("open_read_write_close_and_stat", function(assert) local file = path.join(tmpdir, "file.txt") diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 86574f2b9..4ff9d2a79 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -1,6 +1,6 @@ -local test = require("@std/test") -local process = require("@std/process") local pathlib = require("@std/path") +local process = require("@std/process") +local test = require("@std/test") test.suite("ProcessSuite", function(suite) suite:case("homedir_and_cwd_and_execpath", function(assert) From 7a6d5f7a261624c79164179f5dc7921d72428bd8 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:39:46 -0700 Subject: [PATCH 104/642] stdlib: add `__tostring` to paths (#500) ### Problem Paths didn't have a default way of parsing between a `path` or `pathlike` and a string other than using `.format` ### Solution We add `__tostring` to paths so users can now just `print(path)` or `tostring(path)` to get the string version of a path part of https://github.com/luau-lang/lute/pull/491! --- lute/std/libs/path/posix/init.luau | 25 +++++++++++++++---------- lute/std/libs/path/posix/types.luau | 4 ++++ lute/std/libs/path/win32/init.luau | 25 +++++++++++++++---------- lute/std/libs/path/win32/types.luau | 4 ++++ tests/std/path/path.posix.test.luau | 16 ++++++++++++++++ tests/std/path/path.win32.test.luau | 16 ++++++++++++++++ 6 files changed, 70 insertions(+), 20 deletions(-) diff --git a/lute/std/libs/path/posix/init.luau b/lute/std/libs/path/posix/init.luau index 8a2b14183..4f939d4ec 100644 --- a/lute/std/libs/path/posix/init.luau +++ b/lute/std/libs/path/posix/init.luau @@ -1,10 +1,11 @@ local process = require("@lute/process") local posixtypes = require("@self/types") -export type path = posixtypes.path +export type path = setmetatable export type pathlike = posixtypes.pathlike local posix = {} +local path_mt = {} function posix.basename(path: pathlike): string? if typeof(path) == "string" then @@ -104,18 +105,18 @@ end function posix.join(...: pathlike): path local parts: { pathlike } = { ... } if #parts == 0 then - return { + return setmetatable({ parts = {}, absolute = false, - } + }, path_mt) end local path: path = if typeof(parts[1]) == "string" then posix.parse(parts[1]) - else { + else setmetatable({ parts = table.clone((parts[1] :: path).parts), absolute = (parts[1] :: path).absolute, - } + }, path_mt) for i = 2, #parts do joinHelper(path, parts[i]) @@ -151,15 +152,15 @@ function posix.normalize(path: pathlike): path end end - return { + return setmetatable({ parts = newParts, absolute = path.absolute, - } + }, path_mt) end function posix.parse(path: pathlike): path if typeof(path) == "table" then - return path + return setmetatable(path, path_mt) end if typeof(path) ~= "string" then error("Expected string or path") @@ -188,10 +189,10 @@ function posix.parse(path: pathlike): path end end - return { + return setmetatable({ parts = parts, absolute = isAbs, - } + }, path_mt) end function posix.resolve(...: pathlike): path @@ -226,4 +227,8 @@ function posix.resolve(...: pathlike): path return posix.normalize(path) end +function path_mt:__tostring(): string + return posix.format(self :: path) +end + return table.freeze(posix) diff --git a/lute/std/libs/path/posix/types.luau b/lute/std/libs/path/posix/types.luau index 8f921ecd1..25228496d 100644 --- a/lute/std/libs/path/posix/types.luau +++ b/lute/std/libs/path/posix/types.luau @@ -1,3 +1,7 @@ +export type pathinterface = { + __tostring: () -> string, +} + export type path = { parts: { string }, absolute: boolean, diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index e2ccadb8c..ec436605c 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -2,10 +2,11 @@ local process = require("@lute/process") local win32types = require("@self/types") export type pathkind = win32types.pathkind -export type path = win32types.path +export type path = setmetatable export type pathlike = win32types.pathlike local win32 = {} +local path_mt = {} function win32.basename(path: pathlike): string? if typeof(path) == "string" then @@ -137,20 +138,20 @@ end function win32.join(...: pathlike): path local parts = { ... } if #parts == 0 then - return { + return setmetatable({ parts = {}, kind = "relative", driveLetter = nil, - } + }, path_mt) end local path: path = if typeof(parts[1]) == "string" then win32.parse(parts[1]) - else { + else setmetatable({ parts = table.clone((parts[1] :: path).parts), kind = (parts[1] :: path).kind, driveLetter = (parts[1] :: path).driveLetter, - } + }, path_mt) for i = 2, #parts do joinHelper(path, parts[i]) @@ -186,16 +187,16 @@ function win32.normalize(path: pathlike): path end end - return { + return setmetatable({ parts = newParts, kind = path.kind, driveLetter = path.driveLetter, - } + }, path_mt) end function win32.parse(path: pathlike): path if typeof(path) == "table" then - return path + return setmetatable(path, path_mt) end if typeof(path) ~= "string" then error("Expected string or path") @@ -241,11 +242,11 @@ function win32.parse(path: pathlike): path end end - return { + return setmetatable({ parts = parts, kind = kind, driveLetter = driveLetter, - } + }, path_mt) end function win32.resolve(...: pathlike): path @@ -280,4 +281,8 @@ function win32.resolve(...: pathlike): path return win32.normalize(path) end +function path_mt:__tostring(): string + return win32.format(self :: path) +end + return table.freeze(win32) diff --git a/lute/std/libs/path/win32/types.luau b/lute/std/libs/path/win32/types.luau index e8b5d504d..470b3aa72 100644 --- a/lute/std/libs/path/win32/types.luau +++ b/lute/std/libs/path/win32/types.luau @@ -1,5 +1,9 @@ export type pathkind = "unc" | "absolute" | "relative" +export type pathinterface = { + __tostring: () -> string, +} + export type path = { parts: { string }, kind: pathkind, diff --git a/tests/std/path/path.posix.test.luau b/tests/std/path/path.posix.test.luau index 269b721d7..c04db23bd 100644 --- a/tests/std/path/path.posix.test.luau +++ b/tests/std/path/path.posix.test.luau @@ -535,4 +535,20 @@ test.suite("PathPosixResolveSuite", function(suite) end) end) +test.suite("PathPosixToStringSuite", function(suite) + suite:case("toString_posix_absolute_path", function(assert) + local filePath = "/home/user/documents/file.txt" + local pathobj: posix.path = posix.parse(filePath) + local result = tostring(pathobj) + assert.eq(result, filePath) + end) + + suite:case("toString_posix_relative_path", function(assert) + local filePath = "documents/file.txt" + local pathobj: posix.path = posix.parse(filePath) + local result = tostring(pathobj) + assert.eq(result, filePath) + end) +end) + test.run() diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index 9ed8b235f..ee1e9d547 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -685,4 +685,20 @@ test.suite("PathWin32ResolveSuite", function(suite) end) end) +test.suite("PathWin32ToStringSuite", function(suite) + suite:case("toString_windows_absolute_path", function(assert) + local filePath = "C:\\Users\\username\\Documents\\file.txt" + local pathObj: win32.path = win32.parse(filePath) + local result = tostring(pathObj) + assert.eq(result, filePath) + end) + + suite:case("toString_windows_relative_path", function(assert) + local filePath = "C:\\Users\\username\\Documents\\file.txt" + local pathObj: win32.path = path.win32.parse(filePath) + local result = tostring(pathObj) + assert.eq(result, filePath) + end) +end) + test.run() From 293f25dd8bf58cf0588286cde26829af9616a5cf Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:29:28 -0700 Subject: [PATCH 105/642] @std/fs: replace mkdir with createdirectory to handle pathlike objects and create intermediate directories (#491) ### Problem Resolves https://github.com/luau-lang/lute/issues/413 We're currently using `fs.mkdir` for all use cases without the option of making intermediate parent folders, and the `mkdirdashp` function in lute setup commands can be moved to the `std/fs` library for broader use cases. ### Solution We replace `fs.mkdir` in the standard library with `fs.createdirectory` (a more user friendly name!) that takes in a `path: pathlike` argument as well as an `options` type to specify if the user wants to make intermediate parent folders with `makeparents: boolean` For example: `fs.createdirectory(tmpdir, { makeparents = true })` (see more in `examples/create_directory.luau`) We also update the lute setup commands file to use `fs.createdirectory()` and add/update relevant tests --- examples/create_directory.luau | 22 ++++++++++++++++++++++ lute/cli/commands/setup/init.luau | 30 +++++++----------------------- lute/std/libs/fs.luau | 30 +++++++++++++++++++++++++----- tests/std/fs.test.luau | 29 ++++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 29 deletions(-) create mode 100644 examples/create_directory.luau diff --git a/examples/create_directory.luau b/examples/create_directory.luau new file mode 100644 index 000000000..31357a5c2 --- /dev/null +++ b/examples/create_directory.luau @@ -0,0 +1,22 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") + +local tmpdir = system.tmpdir() +local directory = path.join(tmpdir, "example_dir") + +fs.createdirectory(directory, { makeparents = true }) + +if fs.exists(directory) and fs.type(directory) == "dir" then + print("Directory successfully created") +else + print("Failed to create directory") +end + +fs.rmdir(directory) + +if not fs.exists(directory) then + print("Directory successfully removed") +else + print("Failed to remove directory") +end diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 430e65671..55ace32c7 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -1,9 +1,10 @@ local definitions = require("@self/generated/definitions") local fs = require("@std/fs") +local path = require("@std/path") local process = require("@std/process") local json = require("@std/json") -local BASE_PATH = ".lute/typedefs/0.1.0" +local BASE_PATH = path.parse(".lute/typedefs/0.1.0") local BASE_PATH_PRETTY = `~/{BASE_PATH}` local TEMPLATE_RC_FILE = { aliases = { @@ -16,31 +17,14 @@ local homeDir = process.homedir() local cwd = process.cwd() local args = { ... } -function mkdirdashp(home: string, subpath: string, noIncludeLast: boolean?) - local subdir = home - local parts = string.split(subpath, "/") - local numParts = #parts - - for idx, part in parts do - if noIncludeLast and (idx == numParts) then - break - end - - subdir = subdir .. "/" .. part - if not fs.exists(subdir) then - fs.mkdir(subdir) - end - end -end - -- TODO we're assuming the files are completely unmodified, but we really shouldn't do that. print(`Writing definitions at {BASE_PATH_PRETTY}`) -mkdirdashp(homeDir, BASE_PATH) +fs.createdirectory(path.join(homeDir, BASE_PATH), { makeparents = true }) for key, value in definitions :: { [string]: string } do - local filePath = `{BASE_PATH}/{key}` - mkdirdashp(homeDir, filePath, true) - fs.writestringtofile(`{homeDir}/{filePath}`, value) + local filePath = path.parse(`{BASE_PATH}/{key}`) + fs.createdirectory(path.dirname(filePath), { makeparents = true }) + fs.writestringtofile(filePath, value) end print(`Successfully wrote type definition files to {BASE_PATH_PRETTY}`) @@ -49,7 +33,7 @@ if not table.find(args, "--with-luaurc") then return end -local luauRcPath = `{cwd}/.luaurc` +local luauRcPath = path.parse(`{cwd}/.luaurc`) local rcFile = nil print(`Writing luaurc file at {luauRcPath}`) diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 1cc822d40..0eb4ec078 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -15,6 +15,10 @@ export type watchhandle = fs.WatchHandle export type watchevent = fs.WatchEvent export type pathlike = pathlib.pathlike +export type createdirectoryoptions = { + makeparents: boolean?, +} + local function open(path: pathlike, mode: handlemode?): filehandle return fs.open(pathlib.format(path), mode) end @@ -43,10 +47,6 @@ local function type(path: pathlike): filetype return fs.type(pathlib.format(path)) end -local function mkdir(path: pathlike): () - return fs.mkdir(pathlib.format(path)) -end - local function link(src: pathlike, dest: pathlike): () return fs.link(pathlib.format(src), pathlib.format(dest)) end @@ -88,6 +88,26 @@ local function readasync(filepath: pathlike): string return fs.readasync(pathlib.format(filepath)) end +local function createdirectory(path: pathlike, options: createdirectoryoptions?): () + if options and options.makeparents then + local parts: { string } = pathlib.parse(path).parts + + local subdir: pathlike = "." + if pathlib.isabsolute(path) then + subdir = pathlib.format({ absolute = true, parts = {} }) + end + + for _, part in parts do + subdir = pathlib.join(subdir, part) + if not exists(subdir) then + fs.mkdir(pathlib.format(subdir)) + end + end + else + fs.mkdir(pathlib.format(path)) + end +end + return table.freeze({ open = open, read = read, @@ -96,7 +116,6 @@ return table.freeze({ remove = remove, stat = stat, type = type, - mkdir = mkdir, link = link, symlink = symlink, watch = watch, @@ -107,4 +126,5 @@ return table.freeze({ readfiletostring = readfiletostring, writestringtofile = writestringtofile, readasync = readasync, + createdirectory = createdirectory, }) diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index d277133cb..060e7f0d6 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -49,7 +49,7 @@ test.suite("FsSuite", function(suite) suite:case("mkdir_listdir_rmdir", function(assert) local dir = path.join(tmpdir, "subdir") if not fs.exists(dir) then - fs.mkdir(dir) + fs.createdirectory(dir, { makeparents = true }) end assert.eq(fs.exists(dir), true) @@ -125,6 +125,33 @@ test.suite("FsSuite", function(suite) fs.remove(watched) end end) + + suite:case("createdirectory_makeparents_true", function(assert) + local nestedDir = path.join(tmpdir, "nested", "directory") + fs.createdirectory(nestedDir, { makeparents = true }) + assert.eq(fs.exists(nestedDir), true) + + fs.rmdir(nestedDir) + fs.rmdir(path.join(tmpdir, "nested")) + end) + + suite:case("createdirectory_makeparents_false", function(assert) + local nestedDir = path.join(tmpdir, "this", "should", "not", "work") + local success, err = pcall(function() + fs.createdirectory(nestedDir, { makeparents = false }) + end) + assert.neq(success, true) + assert.neq(err, nil) + end) + + suite:case("createdirectory_no_options", function(assert) + local nestedDir = path.join(tmpdir, "this", "should", "not", "work") + local success, err = pcall(function() + fs.createdirectory(nestedDir) + end) + assert.neq(success, true) + assert.neq(err, nil) + end) end) test.run() From ba9102319f6ae01dcb990abaa339dbb602de5acc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:32:43 -0700 Subject: [PATCH 106/642] Update Luau to 0.698 (#507) **Luau**: Updated from `0.690` to `0.698` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.698 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: vrn-sn <61795485+vrn-sn@users.noreply.github.com> --- extern/luau.tune | 4 +-- lute/cli/CMakeLists.txt | 4 ++- lute/cli/include/lute/luauflags.h | 3 ++ lute/cli/src/climain.cpp | 3 ++ lute/cli/src/luauflags.cpp | 25 +++++++++++++ lute/cli/src/tc.cpp | 2 +- lute/luau/src/luau.cpp | 13 +------ lute/require/include/lute/bundlevfs.h | 2 +- lute/require/include/lute/clivfs.h | 2 +- lute/require/include/lute/filevfs.h | 2 +- lute/require/include/lute/modulepath.h | 8 +++++ lute/require/include/lute/requirevfs.h | 2 +- lute/require/include/lute/stdlibvfs.h | 2 +- lute/require/src/bundlevfs.cpp | 4 +-- lute/require/src/clivfs.cpp | 4 +-- lute/require/src/filevfs.cpp | 4 +-- lute/require/src/require.cpp | 49 ++++++++++++++++++++------ lute/require/src/requirevfs.cpp | 16 ++++----- lute/require/src/stdlibvfs.cpp | 4 +-- tests/std/syntax/parser.test.luau | 24 +++++++++++++ 20 files changed, 129 insertions(+), 48 deletions(-) create mode 100644 lute/cli/include/lute/luauflags.h create mode 100644 lute/cli/src/luauflags.cpp diff --git a/extern/luau.tune b/extern/luau.tune index 669ad5ee3..e3d1371e0 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.690" -revision = "fb63edcceadafc7144d3de7a6ccb0fee83978cf0" +branch = "0.698" +revision = "ff6d381e57bcd1799d850d7fabe543c0f0980a5d" diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index cda9acb32..708991b03 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -25,12 +25,14 @@ add_library(Lute.CLI.lib STATIC) target_sources(Lute.CLI.lib PRIVATE include/lute/climain.h include/lute/compile.h + include/lute/luauflags.h include/lute/staticrequires.h include/lute/tc.h include/lute/uvstate.h src/climain.cpp src/compile.cpp + src/luauflags.cpp src/staticrequires.cpp src/tc.cpp src/uvstate.cpp @@ -38,7 +40,7 @@ target_sources(Lute.CLI.lib PRIVATE target_compile_features(Lute.CLI.lib PUBLIC cxx_std_17) target_include_directories(Lute.CLI.lib PUBLIC include ${CLI_GENERATED_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR}) -target_link_libraries(Lute.CLI.lib PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Require Lute.Runtime Luau.CLI.lib zlibstatic) +target_link_libraries(Lute.CLI.lib PRIVATE Luau.Common Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Require Lute.Runtime Luau.CLI.lib zlibstatic) target_compile_options(Lute.CLI.lib PRIVATE ${LUTE_OPTIONS}) add_executable(Lute.CLI) diff --git a/lute/cli/include/lute/luauflags.h b/lute/cli/include/lute/luauflags.h new file mode 100644 index 000000000..00482a16c --- /dev/null +++ b/lute/cli/include/lute/luauflags.h @@ -0,0 +1,3 @@ +#pragma once + +void setLuauFlags(); diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index ffc0b25db..eb7cec10f 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -16,6 +16,7 @@ #include "lute/fs.h" #include "lute/io.h" #include "lute/luau.h" +#include "lute/luauflags.h" #include "lute/net.h" #include "lute/options.h" #include "lute/process.h" @@ -28,6 +29,7 @@ #include "lute/time.h" #include "lute/version.h" #include "lute/vm.h" + #include "uv.h" #ifdef _WIN32 @@ -462,6 +464,7 @@ int handleCliCommand(CliCommandResult result, int program_argc, char** program_a int cliMain(int argc, char** argv) { Luau::assertHandler() = assertionHandler; + setLuauFlags(); AppendedBytecodeResult embedded = checkForAppendedBytecode(); if (embedded.found) diff --git a/lute/cli/src/luauflags.cpp b/lute/cli/src/luauflags.cpp new file mode 100644 index 000000000..b461cf844 --- /dev/null +++ b/lute/cli/src/luauflags.cpp @@ -0,0 +1,25 @@ +#include "lute/luauflags.h" + +#include "Luau/Common.h" + +#include +#include + +static void setLuauFlag(std::string_view name, bool state) +{ + for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) + { + if (name == flag->name) + { + flag->value = state; + return; + } + } + + throw std::runtime_error("Unrecognized Luau flag"); +} + +void setLuauFlags() +{ + setLuauFlag("LuauAutocompleteAttributes", true); +} diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index 5d6c8b214..ad9d3f3dd 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -2,7 +2,7 @@ #include "Luau/BuiltinDefinitions.h" #include "Luau/Error.h" -#include "Luau/Transpiler.h" +#include "Luau/PrettyPrinter.h" #include "Luau/TypeAttach.h" static const std::string kLuteDefinitions = R"LUTE_TYPES( diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index f92502234..0bfffc894 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -634,18 +634,7 @@ struct AstSerialize : public Luau::AstVisitor } void serializeAttribute(Luau::AstAttr* node) { - switch (node->type) - { - case Luau::AstAttr::Checked: - serializeToken(node->location.begin, "@checked"); - break; - case Luau::AstAttr::Native: - serializeToken(node->location.begin, "@native"); - break; - case Luau::AstAttr::Deprecated: - serializeToken(node->location.begin, "@deprecated"); - break; - } + serializeToken(node->location.begin, ("@" + std::string(node->name.value)).c_str()); serializeNodePreamble(node, "attribute"); } diff --git a/lute/require/include/lute/bundlevfs.h b/lute/require/include/lute/bundlevfs.h index 0b2ee0bca..b638ca94c 100644 --- a/lute/require/include/lute/bundlevfs.h +++ b/lute/require/include/lute/bundlevfs.h @@ -22,7 +22,7 @@ class BundleVfs std::string getIdentifier() const; std::optional getContents(const std::string& path) const; - bool isConfigPresent() const; + ConfigStatus getConfigStatus() const; std::optional getConfig() const; private: diff --git a/lute/require/include/lute/clivfs.h b/lute/require/include/lute/clivfs.h index 4541a5680..c1c33b20b 100644 --- a/lute/require/include/lute/clivfs.h +++ b/lute/require/include/lute/clivfs.h @@ -16,7 +16,7 @@ class CliVfs std::string getIdentifier() const; std::optional getContents(const std::string& path) const; - bool isConfigPresent() const; + ConfigStatus getConfigStatus() const; std::optional getConfig() const; private: diff --git a/lute/require/include/lute/filevfs.h b/lute/require/include/lute/filevfs.h index 687a46bbe..ce601a53d 100644 --- a/lute/require/include/lute/filevfs.h +++ b/lute/require/include/lute/filevfs.h @@ -18,7 +18,7 @@ class FileVfs std::string getAbsoluteFilePath() const; std::optional getContents(const std::string& path) const; - bool isConfigPresent() const; + ConfigStatus getConfigStatus() const; std::optional getConfig() const; private: diff --git a/lute/require/include/lute/modulepath.h b/lute/require/include/lute/modulepath.h index 232ca8329..c8cd8f54d 100644 --- a/lute/require/include/lute/modulepath.h +++ b/lute/require/include/lute/modulepath.h @@ -11,6 +11,14 @@ enum class NavigationStatus NotFound }; +enum class ConfigStatus +{ + Absent, + Ambiguous, + PresentJson, + PresentLuau +}; + struct ResolvedRealPath { enum class PathType diff --git a/lute/require/include/lute/requirevfs.h b/lute/require/include/lute/requirevfs.h index a225526b3..1540835a0 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -33,7 +33,7 @@ class RequireVfs std::string getLoadname(lua_State* L) const; std::string getCacheKey(lua_State* L) const; - bool isConfigPresent(lua_State* L) const; + ConfigStatus getConfigStatus(lua_State* L) const; std::string getConfig(lua_State* L) const; bool isPrecompiled() const diff --git a/lute/require/include/lute/stdlibvfs.h b/lute/require/include/lute/stdlibvfs.h index fee171ee8..faa7023fe 100644 --- a/lute/require/include/lute/stdlibvfs.h +++ b/lute/require/include/lute/stdlibvfs.h @@ -16,7 +16,7 @@ class StdLibVfs std::string getIdentifier() const; std::optional getContents(const std::string& path) const; - bool isConfigPresent() const; + ConfigStatus getConfigStatus() const; std::optional getConfig() const; private: diff --git a/lute/require/src/bundlevfs.cpp b/lute/require/src/bundlevfs.cpp index 51c4387bc..c52f79419 100644 --- a/lute/require/src/bundlevfs.cpp +++ b/lute/require/src/bundlevfs.cpp @@ -128,10 +128,10 @@ std::optional BundleVfs::getContents(const std::string& path) const return std::nullopt; } -bool BundleVfs::isConfigPresent() const +ConfigStatus BundleVfs::getConfigStatus() const { // Currently, we do not support .luaurc files in bundles. - return false; + return ConfigStatus::Absent; } std::optional BundleVfs::getConfig() const diff --git a/lute/require/src/clivfs.cpp b/lute/require/src/clivfs.cpp index f232d081d..314b8e838 100644 --- a/lute/require/src/clivfs.cpp +++ b/lute/require/src/clivfs.cpp @@ -74,10 +74,10 @@ std::optional CliVfs::getContents(const std::string& path) const return readCliModule(path); } -bool CliVfs::isConfigPresent() const +ConfigStatus CliVfs::getConfigStatus() const { // Currently, we do not support .luaurc files in CLI commands. - return false; + return ConfigStatus::Absent; } std::optional CliVfs::getConfig() const diff --git a/lute/require/src/filevfs.cpp b/lute/require/src/filevfs.cpp index b9ae194f2..f8dbc9fc7 100644 --- a/lute/require/src/filevfs.cpp +++ b/lute/require/src/filevfs.cpp @@ -88,10 +88,10 @@ std::optional FileVfs::getContents(const std::string& path) const return readFile(path); } -bool FileVfs::isConfigPresent() const +ConfigStatus FileVfs::getConfigStatus() const { LUAU_ASSERT(modulePath); - return isFile(modulePath->getPotentialLuaurcPath()); + return isFile(modulePath->getPotentialLuaurcPath()) ? ConfigStatus::PresentJson : ConfigStatus::Absent; } std::optional FileVfs::getConfig() const diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index e50f79591..b541aa648 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -33,13 +33,41 @@ static luarequire_WriteResult write(std::optional contents, char* b static luarequire_NavigateResult convert(NavigationStatus status) { - if (status == NavigationStatus::Success) - return NAVIGATE_SUCCESS; - - if (status == NavigationStatus::Ambiguous) - return NAVIGATE_AMBIGUOUS; - - return NAVIGATE_NOT_FOUND; + luarequire_NavigateResult navigateResult = NAVIGATE_NOT_FOUND; + switch (status) + { + case NavigationStatus::Success: + navigateResult = NAVIGATE_SUCCESS; + break; + case NavigationStatus::Ambiguous: + navigateResult = NAVIGATE_AMBIGUOUS; + break; + case NavigationStatus::NotFound: + navigateResult = NAVIGATE_NOT_FOUND; + break; + }; + return navigateResult; +} + +static luarequire_ConfigStatus convert(ConfigStatus status) +{ + luarequire_ConfigStatus configStatus = CONFIG_AMBIGUOUS; + switch (status) + { + case ConfigStatus::Absent: + configStatus = CONFIG_ABSENT; + break; + case ConfigStatus::Ambiguous: + configStatus = CONFIG_AMBIGUOUS; + break; + case ConfigStatus::PresentJson: + configStatus = CONFIG_PRESENT_JSON; + break; + case ConfigStatus::PresentLuau: + configStatus = CONFIG_PRESENT_LUAU; + break; + }; + return configStatus; } static bool is_require_allowed(lua_State* L, void* ctx, const char* requirer_chunkname) @@ -96,10 +124,10 @@ static luarequire_WriteResult get_cache_key(lua_State* L, void* ctx, char* buffe return write(reqCtx->vfs.getCacheKey(L), buffer, buffer_size, size_out); } -static bool is_config_present(lua_State* L, void* ctx) +static luarequire_ConfigStatus get_config_status(lua_State* L, void* ctx) { RequireCtx* reqCtx = static_cast(ctx); - return reqCtx->vfs.isConfigPresent(L); + return convert(reqCtx->vfs.getConfigStatus(L)); } static luarequire_WriteResult get_config(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) @@ -181,12 +209,11 @@ void requireConfigInit(luarequire_Configuration* config) config->to_parent = to_parent; config->to_child = to_child; config->is_module_present = is_module_present; - config->is_config_present = is_config_present; + config->get_config_status = get_config_status; config->get_chunkname = get_chunkname; config->get_loadname = get_loadname; config->get_cache_key = get_cache_key; config->get_config = get_config; - config->get_alias = nullptr; // We use get_config instead of get_alias. config->load = load; } diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 90f4e1a06..2678b6d84 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -285,32 +285,32 @@ std::string RequireVfs::getCacheKey(lua_State* L) const return cacheKey; } -bool RequireVfs::isConfigPresent(lua_State* L) const +ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const { if (atFakeRoot) - return true; + return ConfigStatus::PresentJson; - bool isPresent = false; + ConfigStatus status = ConfigStatus::Absent; switch (vfsType) { case VFSType::Disk: - isPresent = fileVfs.isConfigPresent(); + status = fileVfs.getConfigStatus(); break; case VFSType::Std: - isPresent = stdLibVfs.isConfigPresent(); + status = stdLibVfs.getConfigStatus(); break; case VFSType::Cli: LUAU_ASSERT(cliVfs); - isPresent = cliVfs->isConfigPresent(); + status = cliVfs->getConfigStatus(); break; case VFSType::Bundle: LUAU_ASSERT(bundleVfs); - isPresent = bundleVfs->isConfigPresent(); + status = bundleVfs->getConfigStatus(); break; case VFSType::Lute: break; } - return isPresent; + return status; } std::string RequireVfs::getConfig(lua_State* L) const diff --git a/lute/require/src/stdlibvfs.cpp b/lute/require/src/stdlibvfs.cpp index 6b0e0041d..ecc2aa4dc 100644 --- a/lute/require/src/stdlibvfs.cpp +++ b/lute/require/src/stdlibvfs.cpp @@ -77,10 +77,10 @@ std::optional StdLibVfs::getContents(const std::string& path) const return readStdLibModule(path); } -bool StdLibVfs::isConfigPresent() const +ConfigStatus StdLibVfs::getConfigStatus() const { // Currently, we do not support .luaurc files in the standard library. - return false; + return ConfigStatus::Absent; } std::optional StdLibVfs::getConfig() const diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index bc4d2b666..9bdd8ac91 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -164,6 +164,30 @@ test.suite("Parser", function(suite) assert.eq(emptyResult.lineoffsets[2], 12) assert.eq(emptyResult.lineoffsets[3], 13) end) + + suite:case("canParseAttributes", function(assert) + local code = [[ + @checked + local function foo() + end + ]] + local subcases = { + { attr = "@checked", code = code }, + { attr = "@native", code = code:gsub("checked", "native") }, + { attr = "@deprecated", code = code:gsub("checked", "deprecated") }, + } + + for _, subcase in ipairs(subcases) do + local block = luau.parse(subcase.code).root + assert.eq(#block.statements, 1) + + local l = block.statements[1] + assert.eq(l.tag, "localfunction") + + local lf = l :: luau.AstStatLocalFunction + assert.eq(lf.attributes[1].text, subcase.attr) + end + end) end) test.run() From c8632045b3d732b31bc549bcab989ce42e84c435 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:40:59 +0000 Subject: [PATCH 107/642] Update Lute to 0.1.0-nightly.20251031 (#508) **Lute**: Updated from `0.1.0-nightly.20251003` to `0.1.0-nightly.20251031` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20251031 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: vrn-sn <61795485+vrn-sn@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 9a3c147bc..7649c0e76 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251023" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251031" } diff --git a/rokit.toml b/rokit.toml index cd6590579..296323c95 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20251003" +lute = "luau-lang/lute@0.1.0-nightly.20251031" From e11f8b0c0d933e7bbd0463c9213a364fb966f920 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:38:18 -0700 Subject: [PATCH 108/642] Support `.config.luau` files in Lute (#512) --- lute/require/include/lute/modulepath.h | 2 +- lute/require/src/filevfs.cpp | 26 ++++++++- lute/require/src/modulepath.cpp | 11 ++-- tests/src/require.test.cpp | 54 ++++++++++++++++--- tests/src/require/config_tests/README.md | 1 + .../config_ambiguity/.config.luau | 7 +++ .../config_tests/config_ambiguity/.luaurc | 5 ++ .../config_ambiguity}/dependency.luau | 0 .../config_ambiguity/requirer.luau} | 0 .../config_cannot_be_required/.config.luau | 1 + .../config_cannot_be_required/requirer.luau | 1 + .../config_luau_timeout/.config.luau | 2 + .../config_luau_timeout/requirer.luau} | 0 .../{ => config_tests}/with_config/.luaurc | 0 .../with_config/src/.luaurc | 0 .../with_config/src/alias_requirer.luau | 1 + .../with_config/src/dependency.luau | 1 + .../src/directory_alias_requirer.luau | 0 .../with_config/src/other_dependency.luau | 0 .../src/parent_alias_requirer.luau | 0 .../with_config/src/parent_ambiguity/.luaurc | 0 .../src/parent_ambiguity/folder.luau | 0 .../src/parent_ambiguity/folder/requirer.luau | 0 .../with_config/src/parent_ambiguity/foo.luau | 0 .../subdirectory/subdirectory_dependency.luau | 0 .../with_config/src/submodule/.luaurc | 0 .../with_config/src/submodule/init.luau | 1 + .../with_config_luau/.config.luau | 8 +++ .../with_config_luau/src/.config.luau | 8 +++ .../with_config_luau/src/alias_requirer.luau | 1 + .../with_config_luau/src/dependency.luau | 1 + .../src/directory_alias_requirer.luau | 1 + .../src/other_dependency.luau | 1 + .../src/parent_alias_requirer.luau | 1 + .../src/parent_ambiguity/.config.luau | 7 +++ .../src/parent_ambiguity/folder.luau | 0 .../src/parent_ambiguity/folder/requirer.luau | 1 + .../src/parent_ambiguity/foo.luau | 1 + .../subdirectory/subdirectory_dependency.luau | 1 + .../src/submodule/.config.luau | 7 +++ .../with_config_luau/src/submodule/init.luau | 1 + 41 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 tests/src/require/config_tests/README.md create mode 100644 tests/src/require/config_tests/config_ambiguity/.config.luau create mode 100644 tests/src/require/config_tests/config_ambiguity/.luaurc rename tests/src/require/{with_config/src => config_tests/config_ambiguity}/dependency.luau (100%) rename tests/src/require/{with_config/src/alias_requirer.luau => config_tests/config_ambiguity/requirer.luau} (100%) create mode 100644 tests/src/require/config_tests/config_cannot_be_required/.config.luau create mode 100644 tests/src/require/config_tests/config_cannot_be_required/requirer.luau create mode 100644 tests/src/require/config_tests/config_luau_timeout/.config.luau rename tests/src/require/{with_config/src/submodule/init.luau => config_tests/config_luau_timeout/requirer.luau} (100%) rename tests/src/require/{ => config_tests}/with_config/.luaurc (100%) rename tests/src/require/{ => config_tests}/with_config/src/.luaurc (100%) create mode 100644 tests/src/require/config_tests/with_config/src/alias_requirer.luau create mode 100644 tests/src/require/config_tests/with_config/src/dependency.luau rename tests/src/require/{ => config_tests}/with_config/src/directory_alias_requirer.luau (100%) rename tests/src/require/{ => config_tests}/with_config/src/other_dependency.luau (100%) rename tests/src/require/{ => config_tests}/with_config/src/parent_alias_requirer.luau (100%) rename tests/src/require/{ => config_tests}/with_config/src/parent_ambiguity/.luaurc (100%) rename tests/src/require/{ => config_tests}/with_config/src/parent_ambiguity/folder.luau (100%) rename tests/src/require/{ => config_tests}/with_config/src/parent_ambiguity/folder/requirer.luau (100%) rename tests/src/require/{ => config_tests}/with_config/src/parent_ambiguity/foo.luau (100%) rename tests/src/require/{ => config_tests}/with_config/src/subdirectory/subdirectory_dependency.luau (100%) rename tests/src/require/{ => config_tests}/with_config/src/submodule/.luaurc (100%) create mode 100644 tests/src/require/config_tests/with_config/src/submodule/init.luau create mode 100644 tests/src/require/config_tests/with_config_luau/.config.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/.config.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/alias_requirer.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/dependency.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/directory_alias_requirer.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/other_dependency.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/parent_alias_requirer.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/.config.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/foo.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/submodule/.config.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/submodule/init.luau diff --git a/lute/require/include/lute/modulepath.h b/lute/require/include/lute/modulepath.h index c8cd8f54d..f671010d4 100644 --- a/lute/require/include/lute/modulepath.h +++ b/lute/require/include/lute/modulepath.h @@ -51,7 +51,7 @@ class ModulePath ); ResolvedRealPath getRealPath() const; - std::string getPotentialLuaurcPath() const; + std::string getPotentialConfigPath(const std::string& name) const; NavigationStatus toParent(); NavigationStatus toChild(const std::string& name); diff --git a/lute/require/src/filevfs.cpp b/lute/require/src/filevfs.cpp index f8dbc9fc7..72fc81bb6 100644 --- a/lute/require/src/filevfs.cpp +++ b/lute/require/src/filevfs.cpp @@ -3,7 +3,9 @@ #include "lute/modulepath.h" #include "Luau/Common.h" +#include "Luau/Config.h" #include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" #include #include @@ -91,11 +93,31 @@ std::optional FileVfs::getContents(const std::string& path) const ConfigStatus FileVfs::getConfigStatus() const { LUAU_ASSERT(modulePath); - return isFile(modulePath->getPotentialLuaurcPath()) ? ConfigStatus::PresentJson : ConfigStatus::Absent; + + bool luaurcExists = isFile(modulePath->getPotentialConfigPath(Luau::kConfigName)); + bool luauConfigExists = isFile(modulePath->getPotentialConfigPath(Luau::kLuauConfigName)); + + if (luaurcExists && luauConfigExists) + return ConfigStatus::Ambiguous; + else if (luauConfigExists) + return ConfigStatus::PresentLuau; + else if (luaurcExists) + return ConfigStatus::PresentJson; + + return ConfigStatus::Absent; } std::optional FileVfs::getConfig() const { LUAU_ASSERT(modulePath); - return readFile(modulePath->getPotentialLuaurcPath()); + + ConfigStatus status = getConfigStatus(); + LUAU_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); + + if (status == ConfigStatus::PresentJson) + return readFile(modulePath->getPotentialConfigPath(Luau::kConfigName)); + else if (status == ConfigStatus::PresentLuau) + return readFile(modulePath->getPotentialConfigPath(Luau::kLuauConfigName)); + + LUAU_UNREACHABLE(); } diff --git a/lute/require/src/modulepath.cpp b/lute/require/src/modulepath.cpp index 0e7e94dc7..15cc31a58 100644 --- a/lute/require/src/modulepath.cpp +++ b/lute/require/src/modulepath.cpp @@ -147,7 +147,7 @@ ResolvedRealPath ModulePath::getRealPath() const return {NavigationStatus::Success, partialRealPath + suffix, relativePathWithSuffix, *resolvedType}; } -std::string ModulePath::getPotentialLuaurcPath() const +std::string ModulePath::getPotentialConfigPath(const std::string& name) const { ResolvedRealPath result = getRealPath(); @@ -161,7 +161,7 @@ std::string ModulePath::getPotentialLuaurcPath() const if (hasSuffix(directory, suffix)) { directory.remove_suffix(suffix.size()); - return std::string(directory) + "/.luaurc"; + return std::string(directory) + "/" + name; } } for (std::string_view suffix : kSuffixes) @@ -169,11 +169,11 @@ std::string ModulePath::getPotentialLuaurcPath() const if (hasSuffix(directory, suffix)) { directory.remove_suffix(suffix.size()); - return std::string(directory) + "/.luaurc"; + return std::string(directory) + "/" + name; } } - return std::string(directory) + "/.luaurc"; + return std::string(directory) + "/" + name; } NavigationStatus ModulePath::toParent() @@ -196,6 +196,9 @@ NavigationStatus ModulePath::toParent() NavigationStatus ModulePath::toChild(const std::string& name) { + if (name == ".config") + return NavigationStatus::NotFound; + if (modulePath.empty()) modulePath = name; else diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index 410a25494..ec4ff99d5 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -16,7 +16,7 @@ TEST_CASE_FIXTURE(CliRuntimeFixture, "require_exists") TEST_CASE("require_modules") { - auto doPassingSubcase = [](std::vector& argv, std::string requirePath, std::vector expectedResults) + auto doPassingSubcase = [](std::vector argv, std::string requirePath, std::vector expectedResults) { std::string pass = "pass"; argv.push_back(pass.data()); @@ -28,7 +28,7 @@ TEST_CASE("require_modules") CHECK_EQ(cliMain(argv.size(), argv.data()), 0); }; - auto doFailingSubcase = [](std::vector& argv, std::string requirePath, std::vector expectedResults) + auto doFailingSubcase = [](std::vector argv, std::string requirePath, std::vector expectedResults) { std::string fail = "fail"; argv.push_back(fail.data()); @@ -102,22 +102,51 @@ TEST_CASE("require_modules") SUBCASE("with_module_alias") { - doPassingSubcase(argv, {"./with_config/src/alias_requirer"}, {"result from dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config/src/alias_requirer"}, {"result from dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config_luau/src/alias_requirer"}, {"result from dependency"}); } SUBCASE("with_directory_alias") { - doPassingSubcase(argv, {"./with_config/src/directory_alias_requirer"}, {"result from subdirectory_dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config/src/directory_alias_requirer"}, {"result from subdirectory_dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config_luau/src/directory_alias_requirer"}, {"result from subdirectory_dependency"}); } SUBCASE("with_parent_configuration_alias") { - doPassingSubcase(argv, {"./with_config/src/parent_alias_requirer"}, {"result from other_dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config/src/parent_alias_requirer"}, {"result from other_dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config_luau/src/parent_alias_requirer"}, {"result from other_dependency"}); } SUBCASE("init_does_not_read_sibling_luaurc") { - doPassingSubcase(argv, {"./with_config/src/submodule"}, {"result from dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config/src/submodule"}, {"result from dependency"}); + doPassingSubcase(argv, {"./config_tests/with_config_luau/src/submodule"}, {"result from dependency"}); + } + + SUBCASE("config_ambiguity") + { + doFailingSubcase( + argv, + {"./config_tests/config_ambiguity/requirer"}, + {R"(error requiring module "@dep": could not resolve alias "dep" (ambiguous configuration file))"} + ); + } + + SUBCASE("config_cannot_be_required") + { + doFailingSubcase( + argv, + {"./config_tests/config_cannot_be_required/requirer"}, + {R"(error requiring module "./.config": could not resolve child component ".config")"} + ); + } + + SUBCASE("config_luau_timeout") + { + doFailingSubcase( + argv, {"./config_tests/config_luau_timeout/requirer"}, {R"(error requiring module "@dep": configuration execution timed out)"} + ); } SUBCASE("lute_modules") @@ -139,9 +168,20 @@ TEST_CASE("require_with_parent_ambiguity") // this test's entry point. Instead, we manually start the entry point here. char executablePlaceholder[] = "lute"; + + // .luaurc + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string requirer = joinPaths(luteProjectRoot, "tests/src/require/config_tests/with_config/src/parent_ambiguity/folder/requirer.luau"); + std::vector argv = {executablePlaceholder, requirer.data()}; + CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + } + + // .config.luau for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) { - std::string requirer = joinPaths(luteProjectRoot, "tests/src/require/with_config/src/parent_ambiguity/folder/requirer.luau"); + std::string requirer = + joinPaths(luteProjectRoot, "tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau"); std::vector argv = {executablePlaceholder, requirer.data()}; CHECK_EQ(cliMain(argv.size(), argv.data()), 0); } diff --git a/tests/src/require/config_tests/README.md b/tests/src/require/config_tests/README.md new file mode 100644 index 000000000..026899696 --- /dev/null +++ b/tests/src/require/config_tests/README.md @@ -0,0 +1 @@ +The `with_config` and `with_config_luau` directories should be identical, apart from the configuration file type they use. diff --git a/tests/src/require/config_tests/config_ambiguity/.config.luau b/tests/src/require/config_tests/config_ambiguity/.config.luau new file mode 100644 index 000000000..27af9236f --- /dev/null +++ b/tests/src/require/config_tests/config_ambiguity/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + dep = "./dependency" + } + } +} diff --git a/tests/src/require/config_tests/config_ambiguity/.luaurc b/tests/src/require/config_tests/config_ambiguity/.luaurc new file mode 100644 index 000000000..90fe77c24 --- /dev/null +++ b/tests/src/require/config_tests/config_ambiguity/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "dep": "./dependency" + } +} diff --git a/tests/src/require/with_config/src/dependency.luau b/tests/src/require/config_tests/config_ambiguity/dependency.luau similarity index 100% rename from tests/src/require/with_config/src/dependency.luau rename to tests/src/require/config_tests/config_ambiguity/dependency.luau diff --git a/tests/src/require/with_config/src/alias_requirer.luau b/tests/src/require/config_tests/config_ambiguity/requirer.luau similarity index 100% rename from tests/src/require/with_config/src/alias_requirer.luau rename to tests/src/require/config_tests/config_ambiguity/requirer.luau diff --git a/tests/src/require/config_tests/config_cannot_be_required/.config.luau b/tests/src/require/config_tests/config_cannot_be_required/.config.luau new file mode 100644 index 000000000..a3cb35058 --- /dev/null +++ b/tests/src/require/config_tests/config_cannot_be_required/.config.luau @@ -0,0 +1 @@ +return {"result from .config.luau"} diff --git a/tests/src/require/config_tests/config_cannot_be_required/requirer.luau b/tests/src/require/config_tests/config_cannot_be_required/requirer.luau new file mode 100644 index 000000000..38c00431a --- /dev/null +++ b/tests/src/require/config_tests/config_cannot_be_required/requirer.luau @@ -0,0 +1 @@ +return require("./.config") diff --git a/tests/src/require/config_tests/config_luau_timeout/.config.luau b/tests/src/require/config_tests/config_luau_timeout/.config.luau new file mode 100644 index 000000000..9c128d7e1 --- /dev/null +++ b/tests/src/require/config_tests/config_luau_timeout/.config.luau @@ -0,0 +1,2 @@ +-- Infinite loop +while true do end diff --git a/tests/src/require/with_config/src/submodule/init.luau b/tests/src/require/config_tests/config_luau_timeout/requirer.luau similarity index 100% rename from tests/src/require/with_config/src/submodule/init.luau rename to tests/src/require/config_tests/config_luau_timeout/requirer.luau diff --git a/tests/src/require/with_config/.luaurc b/tests/src/require/config_tests/with_config/.luaurc similarity index 100% rename from tests/src/require/with_config/.luaurc rename to tests/src/require/config_tests/with_config/.luaurc diff --git a/tests/src/require/with_config/src/.luaurc b/tests/src/require/config_tests/with_config/src/.luaurc similarity index 100% rename from tests/src/require/with_config/src/.luaurc rename to tests/src/require/config_tests/with_config/src/.luaurc diff --git a/tests/src/require/config_tests/with_config/src/alias_requirer.luau b/tests/src/require/config_tests/with_config/src/alias_requirer.luau new file mode 100644 index 000000000..4375a7835 --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/alias_requirer.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/src/require/config_tests/with_config/src/dependency.luau b/tests/src/require/config_tests/with_config/src/dependency.luau new file mode 100644 index 000000000..b1d7f9c81 --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/dependency.luau @@ -0,0 +1 @@ +return { "result from dependency" } diff --git a/tests/src/require/with_config/src/directory_alias_requirer.luau b/tests/src/require/config_tests/with_config/src/directory_alias_requirer.luau similarity index 100% rename from tests/src/require/with_config/src/directory_alias_requirer.luau rename to tests/src/require/config_tests/with_config/src/directory_alias_requirer.luau diff --git a/tests/src/require/with_config/src/other_dependency.luau b/tests/src/require/config_tests/with_config/src/other_dependency.luau similarity index 100% rename from tests/src/require/with_config/src/other_dependency.luau rename to tests/src/require/config_tests/with_config/src/other_dependency.luau diff --git a/tests/src/require/with_config/src/parent_alias_requirer.luau b/tests/src/require/config_tests/with_config/src/parent_alias_requirer.luau similarity index 100% rename from tests/src/require/with_config/src/parent_alias_requirer.luau rename to tests/src/require/config_tests/with_config/src/parent_alias_requirer.luau diff --git a/tests/src/require/with_config/src/parent_ambiguity/.luaurc b/tests/src/require/config_tests/with_config/src/parent_ambiguity/.luaurc similarity index 100% rename from tests/src/require/with_config/src/parent_ambiguity/.luaurc rename to tests/src/require/config_tests/with_config/src/parent_ambiguity/.luaurc diff --git a/tests/src/require/with_config/src/parent_ambiguity/folder.luau b/tests/src/require/config_tests/with_config/src/parent_ambiguity/folder.luau similarity index 100% rename from tests/src/require/with_config/src/parent_ambiguity/folder.luau rename to tests/src/require/config_tests/with_config/src/parent_ambiguity/folder.luau diff --git a/tests/src/require/with_config/src/parent_ambiguity/folder/requirer.luau b/tests/src/require/config_tests/with_config/src/parent_ambiguity/folder/requirer.luau similarity index 100% rename from tests/src/require/with_config/src/parent_ambiguity/folder/requirer.luau rename to tests/src/require/config_tests/with_config/src/parent_ambiguity/folder/requirer.luau diff --git a/tests/src/require/with_config/src/parent_ambiguity/foo.luau b/tests/src/require/config_tests/with_config/src/parent_ambiguity/foo.luau similarity index 100% rename from tests/src/require/with_config/src/parent_ambiguity/foo.luau rename to tests/src/require/config_tests/with_config/src/parent_ambiguity/foo.luau diff --git a/tests/src/require/with_config/src/subdirectory/subdirectory_dependency.luau b/tests/src/require/config_tests/with_config/src/subdirectory/subdirectory_dependency.luau similarity index 100% rename from tests/src/require/with_config/src/subdirectory/subdirectory_dependency.luau rename to tests/src/require/config_tests/with_config/src/subdirectory/subdirectory_dependency.luau diff --git a/tests/src/require/with_config/src/submodule/.luaurc b/tests/src/require/config_tests/with_config/src/submodule/.luaurc similarity index 100% rename from tests/src/require/with_config/src/submodule/.luaurc rename to tests/src/require/config_tests/with_config/src/submodule/.luaurc diff --git a/tests/src/require/config_tests/with_config/src/submodule/init.luau b/tests/src/require/config_tests/with_config/src/submodule/init.luau new file mode 100644 index 000000000..4375a7835 --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/submodule/init.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/src/require/config_tests/with_config_luau/.config.luau b/tests/src/require/config_tests/with_config_luau/.config.luau new file mode 100644 index 000000000..b64979de2 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/.config.luau @@ -0,0 +1,8 @@ +return { + luau = { + aliases = { + dep = "./this_should_be_overwritten_by_child_luaurc", + otherdep = "./src/other_dependency" + } + } +} diff --git a/tests/src/require/config_tests/with_config_luau/src/.config.luau b/tests/src/require/config_tests/with_config_luau/src/.config.luau new file mode 100644 index 000000000..63d3bbc8e --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/.config.luau @@ -0,0 +1,8 @@ +return { + luau = { + aliases = { + dep = "./dependency", + subdir = "./subdirectory" + } + } +} diff --git a/tests/src/require/config_tests/with_config_luau/src/alias_requirer.luau b/tests/src/require/config_tests/with_config_luau/src/alias_requirer.luau new file mode 100644 index 000000000..4375a7835 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/alias_requirer.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/src/require/config_tests/with_config_luau/src/dependency.luau b/tests/src/require/config_tests/with_config_luau/src/dependency.luau new file mode 100644 index 000000000..b1d7f9c81 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/dependency.luau @@ -0,0 +1 @@ +return { "result from dependency" } diff --git a/tests/src/require/config_tests/with_config_luau/src/directory_alias_requirer.luau b/tests/src/require/config_tests/with_config_luau/src/directory_alias_requirer.luau new file mode 100644 index 000000000..43507a5e6 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/directory_alias_requirer.luau @@ -0,0 +1 @@ +return (require("@subdir/subdirectory_dependency")) diff --git a/tests/src/require/config_tests/with_config_luau/src/other_dependency.luau b/tests/src/require/config_tests/with_config_luau/src/other_dependency.luau new file mode 100644 index 000000000..abf2670a4 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/other_dependency.luau @@ -0,0 +1 @@ +return { "result from other_dependency" } diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_alias_requirer.luau b/tests/src/require/config_tests/with_config_luau/src/parent_alias_requirer.luau new file mode 100644 index 000000000..a8e8de094 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/parent_alias_requirer.luau @@ -0,0 +1 @@ +return require("@otherdep") diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/.config.luau b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/.config.luau new file mode 100644 index 000000000..bc1ec7f0e --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + foo = "./foo", + } + } +} diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder.luau b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder.luau new file mode 100644 index 000000000..e69de29bb diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau new file mode 100644 index 000000000..2f613057d --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau @@ -0,0 +1 @@ +return require("@foo") diff --git a/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/foo.luau b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/foo.luau new file mode 100644 index 000000000..aa00aca1e --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/foo.luau @@ -0,0 +1 @@ +return { "result from foo" } diff --git a/tests/src/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau b/tests/src/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau new file mode 100644 index 000000000..f555900e8 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau @@ -0,0 +1 @@ +return { "result from subdirectory_dependency" } diff --git a/tests/src/require/config_tests/with_config_luau/src/submodule/.config.luau b/tests/src/require/config_tests/with_config_luau/src/submodule/.config.luau new file mode 100644 index 000000000..eb309fd1f --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/submodule/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + dep = "./this_should_not_be_read_by_init_luau", + } + } +} diff --git a/tests/src/require/config_tests/with_config_luau/src/submodule/init.luau b/tests/src/require/config_tests/with_config_luau/src/submodule/init.luau new file mode 100644 index 000000000..4375a7835 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/submodule/init.luau @@ -0,0 +1 @@ +return require("@dep") From ad7a86127ffd6baae54ef53c890d864afba7a5ec Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 3 Nov 2025 10:48:45 -0800 Subject: [PATCH 109/642] bugfix: UAF in fs callbacks (#514) There is a UAF bug in the `defaultCallback` where we access the result of a uv_fs_t handle after delete'ing it. --- lute/fs/src/fs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index a716edb46..6f822d0f7 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -457,7 +457,7 @@ static void defaultCallback(uv_fs_t* req) if (err) { - token->fail(uv_strerror(req->result)); + token->fail(uv_strerror(err)); return; } From f80716f92f1f1736f2d6f4f35f7a8dd654e50cee Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:38:53 -0800 Subject: [PATCH 110/642] Add `luau.resolverequire` API (#496) The new `luau.resolverequire` API allows us to resolve a given require path (`path`) relative to a given file (`fromchunkname`), returning the absolute file path of the target module. --- definitions/luau.luau | 4 + lute/luau/CMakeLists.txt | 4 +- lute/luau/include/lute/luau.h | 3 + lute/luau/include/lute/resolverequire.h | 9 ++ lute/luau/src/resolverequire.cpp | 182 ++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 lute/luau/include/lute/resolverequire.h create mode 100644 lute/luau/src/resolverequire.cpp diff --git a/definitions/luau.luau b/definitions/luau.luau index 96d175f21..7068291d6 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -580,4 +580,8 @@ function luau.load(bytecode: Bytecode, chunkname: string, env: { [any]: any }?): error("not implemented") end +function luau.resolverequire(path: string, fromchunkname: string): string + error("not implemented") +end + return luau diff --git a/lute/luau/CMakeLists.txt b/lute/luau/CMakeLists.txt index edbac0404..b4c03ccfa 100644 --- a/lute/luau/CMakeLists.txt +++ b/lute/luau/CMakeLists.txt @@ -2,11 +2,13 @@ add_library(Lute.Luau STATIC) target_sources(Lute.Luau PRIVATE include/lute/luau.h + include/lute/resolverequire.h src/luau.cpp + src/resolverequire.cpp ) target_compile_features(Lute.Luau PUBLIC cxx_std_17) target_include_directories(Lute.Luau PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Luau PRIVATE Lute.Runtime Luau.VM uv_a Luau.Analysis Luau.Ast Luau.Compiler) +target_link_libraries(Lute.Luau PRIVATE Lute.Require Lute.Runtime uv_a Luau.Analysis Luau.Ast Luau.Compiler Luau.Require Luau.VM) target_compile_options(Lute.Luau PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/luau/include/lute/luau.h b/lute/luau/include/lute/luau.h index fe124c788..527980983 100644 --- a/lute/luau/include/lute/luau.h +++ b/lute/luau/include/lute/luau.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/resolverequire.h" + #include "lua.h" #include "lualib.h" @@ -24,6 +26,7 @@ static const luaL_Reg lib[] = { {"parseexpr", luau_parseexpr}, {"compile", compile_luau}, {"load", load_luau}, + {"resolverequire", resolverequire_luau}, {nullptr, nullptr}, }; diff --git a/lute/luau/include/lute/resolverequire.h b/lute/luau/include/lute/resolverequire.h new file mode 100644 index 000000000..62134700e --- /dev/null +++ b/lute/luau/include/lute/resolverequire.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +struct lua_State; + +std::optional resolveRequire(std::string requirePath, std::string requirerChunkname, std::string* error); +int resolverequire_luau(lua_State* L); diff --git a/lute/luau/src/resolverequire.cpp b/lute/luau/src/resolverequire.cpp new file mode 100644 index 000000000..16fbd3b15 --- /dev/null +++ b/lute/luau/src/resolverequire.cpp @@ -0,0 +1,182 @@ +#include "lute/resolverequire.h" + +#include "Luau/Common.h" +#include "Luau/RequireNavigator.h" + +#include "lute/filevfs.h" +#include "lute/modulepath.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include + +// FileVfsContext +class FileVfsContext : public Luau::Require::NavigationContext +{ +public: + FileVfsContext(std::string requirerChunkname); + + std::string getRequirerIdentifier() const override; + + NavigateResult reset(const std::string& identifier) override; + NavigateResult jumpToAlias(const std::string& path) override; + + NavigateResult toParent() override; + NavigateResult toChild(const std::string& component) override; + + ConfigStatus getConfigStatus() const override; + + ConfigBehavior getConfigBehavior() const override; + std::optional getAlias(const std::string& alias) const override; + std::optional getConfig() const override; + + FileVfs vfs; + std::string requirerChunkname; +}; + +using NC = Luau::Require::NavigationContext; + +static NC::NavigateResult convert(NavigationStatus status) +{ + NC::NavigateResult result = NC::NavigateResult::NotFound; + switch (status) + { + case NavigationStatus::Success: + result = NC::NavigateResult::Success; + break; + case NavigationStatus::Ambiguous: + result = NC::NavigateResult::Ambiguous; + break; + case NavigationStatus::NotFound: + result = NC::NavigateResult::NotFound; + break; + } + return result; +} + +static NC::ConfigStatus convert(ConfigStatus status) +{ + NC::ConfigStatus result = NC::ConfigStatus::Ambiguous; + switch (status) + { + case ConfigStatus::Absent: + result = NC::ConfigStatus::Absent; + break; + case ConfigStatus::Ambiguous: + result = NC::ConfigStatus::Ambiguous; + break; + case ConfigStatus::PresentJson: + result = NC::ConfigStatus::PresentJson; + break; + case ConfigStatus::PresentLuau: + result = NC::ConfigStatus::PresentLuau; + break; + } + return result; +} + +FileVfsContext::FileVfsContext(std::string requirerChunkname) + : requirerChunkname(std::move(requirerChunkname)) +{ +} + +std::string FileVfsContext::getRequirerIdentifier() const +{ + return requirerChunkname; +} + +NC::NavigateResult FileVfsContext::reset(const std::string& identifier) +{ + return convert(vfs.resetToPath(identifier)); +} + +NC::NavigateResult FileVfsContext::jumpToAlias(const std::string& path) +{ + return convert(vfs.resetToPath(path)); +} + +NC::NavigateResult FileVfsContext::toParent() +{ + return convert(vfs.toParent()); +} + +NC::NavigateResult FileVfsContext::toChild(const std::string& component) +{ + return convert(vfs.toChild(component)); +} + +NC::ConfigStatus FileVfsContext::getConfigStatus() const +{ + return convert(vfs.getConfigStatus()); +} + +NC::ConfigBehavior FileVfsContext::getConfigBehavior() const +{ + return NC::ConfigBehavior::GetConfig; +} + +std::optional FileVfsContext::getAlias(const std::string& alias) const +{ + return std::nullopt; +} + +std::optional FileVfsContext::getConfig() const +{ + return vfs.getConfig(); +} + +// ErrorCapturer +class ErrorCapturer : public Luau::Require::ErrorHandler +{ +public: + void reportError(std::string message) override; + std::optional error = std::nullopt; +}; + +void ErrorCapturer::reportError(std::string message) +{ + error = message; +} + +// Public API +std::optional resolveRequire(std::string requirePath, std::string requirerChunkname, std::string* error) +{ + if (requirerChunkname.empty() || requirerChunkname[0] != '@') + { + if (error) + *error = "requirer chunkname must start with '@'"; + return std::nullopt; + } + + FileVfsContext context{requirerChunkname.substr(1)}; + ErrorCapturer errorCapturer{}; + + Luau::Require::Navigator navigator{context, errorCapturer}; + Luau::Require::Navigator::Status status = navigator.navigate(requirePath); + + if (status == Luau::Require::Navigator::Status::ErrorReported) + { + if (error && errorCapturer.error) + *error = *errorCapturer.error; + return std::nullopt; + } + + std::string absolutePath = context.vfs.getAbsoluteFilePath(); + return absolutePath; +} + +int resolverequire_luau(lua_State* L) +{ + std::string requirePath = luaL_checkstring(L, 1); + std::string requirerChunkname = luaL_checkstring(L, 2); + + std::string error; + std::optional absolutePath = resolveRequire(requirePath, requirerChunkname, &error); + if (!absolutePath) + luaL_error(L, "%s", error.c_str()); + + lua_pushlstring(L, absolutePath->c_str(), absolutePath->size()); + return 1; +} From f5be7a668d0e058114680e753af107d1bf9558c0 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 3 Nov 2025 14:00:56 -0800 Subject: [PATCH 111/642] refactor: Add some utilities for reading and writing Lute Executables in a more systematic fashion (#490) This PR adds a `LuteExecutable` wrapper, that handles extracting the `LuteExePayload` out of a binary and also creating a valid `lute` executable with the payload injected in correctly. This will set us up to be able to do cross compilation a bit easier, by separating the `payload` from the actual act of manipulating an executable file. --- lute/cli/include/lute/compile.h | 24 +++++ lute/cli/src/compile.cpp | 96 +++++++++++++++++ tests/src/compile.test.cpp | 185 ++++++++++++++++++++++++++++++++ 3 files changed, 305 insertions(+) diff --git a/lute/cli/include/lute/compile.h b/lute/cli/include/lute/compile.h index 0054ee905..4cdaef178 100644 --- a/lute/cli/include/lute/compile.h +++ b/lute/cli/include/lute/compile.h @@ -67,3 +67,27 @@ struct LuteDecodeResult size_t compressedPayloadSizeBytes = 0; size_t uncompressedPayloadSizeBytes = 0; }; + +/** + * Manages creating and reading Lute executables with embedded bytecode bundles. + * + * Binary format (from end of file, reading backward): + * [lute runtime executable's bytes] + * [compressed bundle data] + * [compressed_size: uint64_t] + * [uncompressed_size: uint64_t] + * [num_files: uint32_t] + * [entry_point_path_string: char[entry_point_path_length]] + * [entry_point_path_length: uint32_t] + * [MAGIC_FLAG: "LUTEBYTE" (8 bytes)] + */ +struct LuteExecutable +{ + LuteExecutable(const std::string& luteRuntimePath); + + bool create(const std::string& outputPath, LuteExePayload& payload); + std::optional extract(); + + std::string executablePath; +}; + diff --git a/lute/cli/src/compile.cpp b/lute/cli/src/compile.cpp index 5c9f0395f..71f41acaf 100644 --- a/lute/cli/src/compile.cpp +++ b/lute/cli/src/compile.cpp @@ -451,3 +451,99 @@ bool LuteExePayload::parseFromDecompressedBundle(std::string_view decompressedBu return true; } + +LuteExecutable::LuteExecutable(const std::string& luteRuntimePath) + : executablePath(luteRuntimePath) +{ +} + +bool LuteExecutable::create(const std::string& outputPath, LuteExePayload& payload) +{ + // Read the current executable (lute runtime) + std::ifstream sourceExe(executablePath, std::ios::binary | std::ios::ate); + if (!sourceExe) + { + fprintf(stderr, "Error: Failed to read executable '%s'\n", executablePath.c_str()); + return false; + } + + std::streampos exeSize = sourceExe.tellg(); + if (exeSize < 0) + return false; + + sourceExe.seekg(0, std::ios::beg); + std::vector exeData(exeSize); + if (!sourceExe.read(exeData.data(), exeSize)) + return false; + + // Encode the payload + std::optional encodeResult = payload.encode(); + + if (!encodeResult) + { + fprintf(stderr, "Error: Failed to encode payload\n"); + return false; + } + + // Write output file: executable + payload + std::ofstream outputFile(outputPath, std::ios::binary); + if (!outputFile) + { + fprintf(stderr, "Error: Failed to create output file '%s'\n", outputPath.c_str()); + return false; + } + + // Write original executable + outputFile.write(exeData.data(), exeData.size()); + + // Write encoded payload + outputFile.write(encodeResult->payload.data(), encodeResult->payload.size()); + + // Set executable permissions (using libuv's permission constants) +#ifndef _WIN32 + chmod(outputPath.c_str(), S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); +#endif + + return true; +} + +std::optional LuteExecutable::extract() +{ + if (isDirectory(executablePath)) + return std::nullopt; + + std::ifstream exeFile(executablePath, std::ios::binary | std::ios::ate); + + if (!exeFile) + return std::nullopt; + + // Get file size + std::streampos fileSize = exeFile.tellg(); + if (fileSize < 0) + return std::nullopt; + + // Early check: validate LUTEBYTE magic flag at end before reading entire file + if (fileSize < static_cast(MAGIC_FLAG_SIZE)) + return std::nullopt; + + exeFile.seekg(-static_cast(MAGIC_FLAG_SIZE), std::ios::end); + char magicBuffer[MAGIC_FLAG_SIZE]; + if (!exeFile.read(magicBuffer, MAGIC_FLAG_SIZE)) + return std::nullopt; + + if (memcmp(magicBuffer, MAGIC_FLAG, MAGIC_FLAG_SIZE) != 0) + return std::nullopt; + + // Magic flag found, now read entire file + exeFile.seekg(0, std::ios::beg); + std::vector fileData(fileSize); + if (!exeFile.read(fileData.data(), fileSize)) + return std::nullopt; + + // Decode the payload + std::optional decodedPayload = LuteExePayload::decode(std::string_view(fileData.data(), fileData.size())); + if (!decodedPayload) + return std::nullopt; + + return decodedPayload->payload; +} diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp index 3feb1dcb3..7eb466895 100644 --- a/tests/src/compile.test.cpp +++ b/tests/src/compile.test.cpp @@ -271,3 +271,188 @@ TEST_CASE("lutepayload_validates_numfiles_metadata") auto decodeResult = LuteExePayload::decode(corruptedPayload); CHECK(!decodeResult.has_value()); } + +TEST_CASE("luteexecutable_single_file_roundtrip") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create a temporary dummy executable as the base runtime + std::string dummyExePath = joinPaths(luteProjectRoot, "tests/temp_dummy_exe"); + { + std::ofstream dummyExe(dummyExePath, std::ios::binary); + REQUIRE(dummyExe.is_open()); + // Write some dummy data to simulate an executable + std::string dummyContent = "FAKE_EXECUTABLE_HEADER_DATA"; + dummyExe.write(dummyContent.data(), dummyContent.size()); + dummyExe.close(); + } + + // Create payload with a test file + LuteExePayload originalPayload; + originalPayload.add(testFilePath); + + // Create LuteExecutable and write it out + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe"); + LuteExecutable executable(dummyExePath); + + bool createSuccess = executable.create(outputExePath, originalPayload); + REQUIRE(createSuccess); + + // Verify output file was created + std::ifstream checkFile(outputExePath, std::ios::binary); + REQUIRE(checkFile.is_open()); + checkFile.close(); + + // Extract the payload from the created executable + LuteExecutable readExecutable(outputExePath); + auto extractedPayload = readExecutable.extract(); + REQUIRE(extractedPayload.has_value()); + + // Verify entry point matches + CHECK(extractedPayload->entryPointPath == originalPayload.entryPointPath); + + // Verify the file count matches + REQUIRE(extractedPayload->filePathToBytecode.size() == 1); + + // Verify bytecode matches + auto originalBytecodeIt = originalPayload.filePathToBytecode.find(testFilePath); + auto extractedBytecodeIt = extractedPayload->filePathToBytecode.find(testFilePath); + REQUIRE(originalBytecodeIt != nullptr); + REQUIRE(extractedBytecodeIt != nullptr); + CHECK(*originalBytecodeIt == *extractedBytecodeIt); + + // Clean up temporary files + std::remove(dummyExePath.c_str()); + std::remove(outputExePath.c_str()); +} + +TEST_CASE("luteexecutable_multiple_files_roundtrip") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + + std::vector testFiles = { + joinPaths(testDir, "main.luau"), + joinPaths(testDir, "utils.luau"), + joinPaths(testDir, "lib/helper.luau"), + joinPaths(testDir, "shared.luau") + }; + + // Create a temporary dummy executable + std::string dummyExePath = joinPaths(luteProjectRoot, "tests/temp_dummy_exe_multi"); + { + std::ofstream dummyExe(dummyExePath, std::ios::binary); + REQUIRE(dummyExe.is_open()); + std::string dummyContent = "FAKE_EXECUTABLE_WITH_LONGER_HEADER_TO_TEST_MULTI_FILE"; + dummyExe.write(dummyContent.data(), dummyContent.size()); + dummyExe.close(); + } + + // Create payload with multiple files + LuteExePayload originalPayload; + for (const auto& file : testFiles) + { + originalPayload.add(file); + } + + // First file should be the entry point + CHECK(originalPayload.entryPointPath == testFiles[0]); + + // Create the executable + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe_multi"); + LuteExecutable executable(dummyExePath); + + bool createSuccess = executable.create(outputExePath, originalPayload); + REQUIRE(createSuccess); + + // Extract the payload + LuteExecutable readExecutable(outputExePath); + auto extractedPayload = readExecutable.extract(); + REQUIRE(extractedPayload.has_value()); + + // Verify entry point + CHECK(extractedPayload->entryPointPath == testFiles[0]); + + // Verify all files are present + REQUIRE(extractedPayload->filePathToBytecode.size() == testFiles.size()); + + // Verify each file's bytecode matches + for (const auto& file : testFiles) + { + auto extractedIt = extractedPayload->filePathToBytecode.find(file); + REQUIRE(extractedIt != nullptr); + + auto originalIt = originalPayload.filePathToBytecode.find(file); + REQUIRE(originalIt != nullptr); + + CHECK(*extractedIt == *originalIt); + } + + // Clean up + std::remove(dummyExePath.c_str()); + std::remove(outputExePath.c_str()); +} + +TEST_CASE("luteexecutable_extract_from_plain_executable") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + + // Create a plain executable without any embedded payload + std::string plainExePath = joinPaths(luteProjectRoot, "tests/temp_plain_exe"); + { + std::ofstream plainExe(plainExePath, std::ios::binary); + REQUIRE(plainExe.is_open()); + std::string content = "PLAIN_EXECUTABLE_NO_PAYLOAD"; + plainExe.write(content.data(), content.size()); + plainExe.close(); + } + + // Attempt to extract - should return nullopt since there's no payload + LuteExecutable executable(plainExePath); + auto extractedPayload = executable.extract(); + CHECK(!extractedPayload.has_value()); + + // Clean up + std::remove(plainExePath.c_str()); +} + +TEST_CASE("luteexecutable_extract_preserves_original_executable") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create dummy executable with specific content + std::string dummyExePath = joinPaths(luteProjectRoot, "tests/temp_dummy_exe_preserve"); + std::string originalExeContent = "ORIGINAL_EXE_CONTENT_12345"; + { + std::ofstream dummyExe(dummyExePath, std::ios::binary); + REQUIRE(dummyExe.is_open()); + dummyExe.write(originalExeContent.data(), originalExeContent.size()); + dummyExe.close(); + } + + // Create payload and executable + LuteExePayload payload; + payload.add(testFilePath); + + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe_preserve"); + LuteExecutable executable(dummyExePath); + bool createSuccess = executable.create(outputExePath, payload); + REQUIRE(createSuccess); + + // Read the output file and verify the original executable content is at the beginning + std::ifstream outputFile(outputExePath, std::ios::binary); + REQUIRE(outputFile.is_open()); + + std::vector buffer(originalExeContent.size()); + outputFile.read(buffer.data(), buffer.size()); + outputFile.close(); + + std::string extractedPrefix(buffer.begin(), buffer.end()); + CHECK(extractedPrefix == originalExeContent); + + // Clean up + std::remove(dummyExePath.c_str()); + std::remove(outputExePath.c_str()); +} From 5cedca01d93a1e8806f97bcfb7acee62a65430fc Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 3 Nov 2025 14:17:21 -0800 Subject: [PATCH 112/642] stdlib: add kinds to ast types (#510) This PR adds kinds to AST types, which lets us do nice things with refinements like: ```luau if node.kind == "expr" then if node.tag == "conditional" then node. -- we get autocomplete for things like 'condition' here end end ``` This also required updating the serialization implementation of the parser. --- definitions/luau.luau | 130 ++-- lute/luau/src/luau.cpp | 135 +++-- lute/std/libs/luau.luau | 18 +- tests/std/syntax/parser.test.luau | 975 +++++++++++++++++++++++++++++- 4 files changed, 1135 insertions(+), 123 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 7068291d6..5e7d8aa16 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -36,6 +36,7 @@ export type Token = { position: Position, text: Kind, trailingtrivia: { Trivia }, + istoken: true, } export type Eof = Token<""> & { tag: "eof" } @@ -44,6 +45,7 @@ export type Pair = { node: T, separator: Token? } export type Punctuated = { Pair } export type AstLocal = { + kind: "local", name: Token, colon: Token<":">?, annotation: AstType?, @@ -134,24 +136,28 @@ export type AstExprAnonymousFunction = { body: AstFunctionBody, } -export type AstExprTableItem = - | { kind: "list", value: AstExpr, separator: Token<"," | ";">? } - | { - kind: "record", - key: Token, - equals: Token<"=">, - value: AstExpr, - separator: Token<"," | ";">?, - } - | { - kind: "general", - indexeropen: Token<"[">, - key: AstExpr, - indexerclose: Token<"]">, - equals: Token<"=">, - value: AstExpr, - separator: Token<"," | ";">?, - } +-- helper types for items contained in an AstExprTable, not actually AstExprs themselves +export type AstExprTableItemList = { kind: "list", value: AstExpr, separator: Token<"," | ";">? } + +export type AstExprTableItemRecord = { + kind: "record", + key: Token, + equals: Token<"=">, + value: AstExpr, + separator: Token<"," | ";">?, +} + +export type AstExprTableItemGeneral = { + kind: "general", + indexeropen: Token<"[">, + key: AstExpr, + indexerclose: Token<"]">, + equals: Token<"=">, + value: AstExpr, + separator: Token<"," | ";">?, +} + +export type AstExprTableItem = AstExprTableItemList | AstExprTableItemRecord | AstExprTableItemGeneral export type AstExprTable = { tag: "table", @@ -186,7 +192,8 @@ export type AstExprTypeAssertion = { annotation: AstType, } -export type AstExprIfElseIfs = { +-- helper type for elseif clauses of an if-else expression, not actually an AstExpr itself +export type AstElseIfExpr = { elseifkeyword: Token<"elseif">, condition: AstExpr, thenkeyword: Token<"then">, @@ -199,12 +206,12 @@ export type AstExprIfElse = { condition: AstExpr, thenkeyword: Token<"then">, thenexpr: AstExpr, - elseifs: { AstExprIfElseIfs }, + elseifs: { AstElseIfExpr }, elsekeyword: Token<"else">, elseexpr: AstExpr, } -export type AstExpr = +export type AstExpr = { kind: "expr" } & ( | AstExprGroup | AstExprConstantNil | AstExprConstantBool @@ -223,13 +230,14 @@ export type AstExpr = | AstExprInterpString | AstExprTypeAssertion | AstExprIfElse +) export type AstStatBlock = { tag: "block", statements: { AstStat }, } -export type AstStatElseIf = { +export type AstElseIfStat = { elseifkeyword: Token<"elseif">, condition: AstExpr, thenkeyword: Token<"then">, @@ -242,7 +250,7 @@ export type AstStatIf = { condition: AstExpr, thenkeyword: Token<"then">, thenblock: AstStatBlock, - elseifs: { AstStatElseIf }, + elseifs: { AstElseIfStat }, elsekeyword: Token<"else">?, -- TODO: this could be elseif! elseblock: AstStatBlock?, endkeyword: Token<"end">, @@ -328,7 +336,7 @@ export type AstStatCompoundAssign = { value: AstExpr, } -export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { tag: "attribute" } +export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { kind: "attribute" } export type AstStatFunction = { tag: "function", @@ -369,7 +377,7 @@ export type AstStatTypeFunction = { body: AstFunctionBody, } -export type AstStat = +export type AstStat = { kind: "stat" } & ( | AstStatBlock | AstStatIf | AstStatWhile @@ -387,6 +395,7 @@ export type AstStat = | AstStatLocalFunction | AstStatTypeAlias | AstStatTypeFunction +) export type AstGenericType = { tag: "generic", @@ -435,7 +444,7 @@ export type AstTypeGroup = { tag: "group", openparens: Token<"(">, type: AstType, - closeparens: Token<">">, + closeparens: Token<")">, } export type AstTypeOptional = Token<"?"> & { tag: "optional" } @@ -461,36 +470,38 @@ export type AstTypeArray = { closebrace: Token<"}">, } -export type AstTypeTableItem = - { - kind: "indexer", - access: Token<"read" | "write">?, - indexeropen: Token<"[">, - key: AstType, - indexerclose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, - } - | { - kind: "stringproperty", - access: Token<"read" | "write">?, - indexeropen: Token<"[">, - key: AstTypeSingletonString, - indexerclose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, - } - | { - kind: "property", - access: Token<"read" | "write">?, - key: AstTypeSingletonString, - indexerclose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, - } +export type AstTypeTableItemIndexer = { + kind: "indexer", + access: Token<"read" | "write">?, + indexeropen: Token<"[">, + key: AstType, + indexerclose: Token<"]">, + colon: Token<":">, + value: AstType, + separator: Token<"," | ";">?, +} + +export type AstTypeTableItemStringProperty = { + kind: "stringproperty", + access: Token<"read" | "write">?, + indexeropen: Token<"[">, + key: AstTypeSingletonString, + indexerclose: Token<"]">, + colon: Token<":">, + value: AstType, + separator: Token<"," | ";">?, +} + +export type AstTypeTableItemProperty = { + kind: "property", + access: Token<"read" | "write">?, + key: Token, + colon: Token<":">, + value: AstType, + separator: Token<"," | ";">?, +} + +export type AstTypeTableItem = AstTypeTableItemIndexer | AstTypeTableItemStringProperty | AstTypeTableItemProperty export type AstTypeTable = { tag: "table", @@ -519,7 +530,7 @@ export type AstTypeFunction = { returntypes: AstTypePack, } -export type AstType = +export type AstType = { kind: "type" } & ( | AstTypeReference | AstTypeSingletonBool | AstTypeSingletonString @@ -531,6 +542,7 @@ export type AstType = | AstTypeArray | AstTypeTable | AstTypeFunction +) export type AstTypePackExplicit = { tag: "explicit", @@ -553,7 +565,9 @@ export type AstTypePackVariadic = { type: AstType, } -export type AstTypePack = AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic +export type AstTypePack = { kind: "typepack" } & (AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic) + +export type AstNode = { location: Location } & (AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute) export type ParseResult = { root: AstStatBlock, diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 0bfffc894..036941416 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -319,7 +319,12 @@ struct AstSerialize : public Luau::AstVisitor if (lua_isnil(L, -1)) { lua_pop(L, 1); - lua_createtable(L, 0, 4); + lua_createtable(L, 0, 6); + + lua_pushlstring(L, "local", 5); + lua_setfield(L, -2, "kind"); + + withLocation(local->location); // set up reference for this local into the local table lua_pushlightuserdata(L, local); @@ -435,14 +440,17 @@ struct AstSerialize : public Luau::AstVisitor } // preambleSize should encode the size of the fields we're setting up for _all_ nodes. - static const size_t preambleSize = 2; - void serializeNodePreamble(Luau::AstNode* node, const char* tag) + static const size_t preambleSize = 3; + void serializeNodePreamble(Luau::AstNode* node, const char* tag, const char* kind) { lua_rawcheckstack(L, 2); lua_pushstring(L, tag); lua_setfield(L, -2, "tag"); + lua_pushstring(L, kind); + lua_setfield(L, -2, "kind"); + withLocation(node->location); } @@ -483,7 +491,7 @@ struct AstSerialize : public Luau::AstVisitor void serializeToken(Luau::Position position, const char* text, int nrec = 0) { lua_rawcheckstack(L, 3); - lua_createtable(L, 0, nrec + 4); + lua_createtable(L, 0, nrec + 5); const auto trivia = extractTrivia(position); if (lastTokenRef != LUA_NOREF) @@ -518,6 +526,9 @@ struct AstSerialize : public Luau::AstVisitor lua_createtable(L, 0, 0); lua_setfield(L, -2, "trailingtrivia"); + lua_pushboolean(L, 1); + lua_setfield(L, -2, "istoken"); + lastTokenRef = lua_ref(L, -1); } @@ -635,7 +646,12 @@ struct AstSerialize : public Luau::AstVisitor void serializeAttribute(Luau::AstAttr* node) { serializeToken(node->location.begin, ("@" + std::string(node->name.value)).c_str()); - serializeNodePreamble(node, "attribute"); + lua_rawcheckstack(L, 2); + + lua_pushstring(L, "attribute"); + lua_setfield(L, -2, "kind"); + + withLocation(node->location); } void serializeEof(Luau::Position eofPosition) @@ -649,9 +665,9 @@ struct AstSerialize : public Luau::AstVisitor void serialize(Luau::AstExprGroup* node) { lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 3); + lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "group"); + serializeNodePreamble(node, "group", "expr"); serializeToken(node->location.begin, "("); lua_setfield(L, -2, "openparens"); @@ -666,13 +682,13 @@ struct AstSerialize : public Luau::AstVisitor void serialize(Luau::AstExprConstantNil* node) { serializeToken(node->location.begin, "nil", preambleSize); - serializeNodePreamble(node, "nil"); + serializeNodePreamble(node, "nil", "expr"); } void serialize(Luau::AstExprConstantBool* node) { serializeToken(node->location.begin, node->value ? "true" : "false", preambleSize + 1); - serializeNodePreamble(node, "boolean"); + serializeNodePreamble(node, "boolean", "expr"); lua_pushboolean(L, node->value); lua_setfield(L, -2, "value"); @@ -683,7 +699,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); serializeToken(node->location.begin, cstNode->value.data, preambleSize + 1); - serializeNodePreamble(node, "number"); + serializeNodePreamble(node, "number", "expr"); lua_pushnumber(L, node->value); lua_setfield(L, -2, "value"); @@ -693,7 +709,7 @@ struct AstSerialize : public Luau::AstVisitor { const auto cstNode = lookupCstNode(node); serializeToken(node->location.begin, cstNode->sourceString.data, preambleSize + 2); - serializeNodePreamble(node, "string"); + serializeNodePreamble(node, "string", "expr"); switch (cstNode->quoteStyle) { @@ -726,7 +742,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "local"); + serializeNodePreamble(node, "local", "expr"); serializeToken(node->location.begin, node->local->name.value); lua_setfield(L, -2, "token"), @@ -743,7 +759,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 1); - serializeNodePreamble(node, "global"); + serializeNodePreamble(node, "global", "expr"); serializeToken(node->location.begin, node->name.value); lua_setfield(L, -2, "name"); @@ -752,7 +768,7 @@ struct AstSerialize : public Luau::AstVisitor void serialize(Luau::AstExprVarargs* node) { serializeToken(node->location.begin, "...", preambleSize); - serializeNodePreamble(node, "vararg"); + serializeNodePreamble(node, "vararg", "expr"); } void serialize(Luau::AstExprCall* node) @@ -762,7 +778,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 6); - serializeNodePreamble(node, "call"); + serializeNodePreamble(node, "call", "expr"); node->func->visit(this); lua_setfield(L, -2, "func"); @@ -794,7 +810,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "indexname"); + serializeNodePreamble(node, "indexname", "expr"); node->expr->visit(this); lua_setfield(L, -2, "expression"); @@ -815,7 +831,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "index"); + serializeNodePreamble(node, "index", "expr"); node->expr->visit(this); lua_setfield(L, -2, "expression"); @@ -921,7 +937,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 3); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "function"); + serializeNodePreamble(node, "function", "expr"); serializeAttributes(node->attributes); lua_setfield(L, -2, "attributes"); @@ -942,7 +958,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 3); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "table"); + serializeNodePreamble(node, "table", "expr"); serializeToken(node->location.begin, "{"); lua_setfield(L, -2, "openbrace"); @@ -964,7 +980,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "unary"); + serializeNodePreamble(node, "unary", "expr"); const auto cstNode = lookupCstNode(node); serializeToken(cstNode->opPosition, toString(node->op).data()); @@ -979,7 +995,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "binary"); + serializeNodePreamble(node, "binary", "expr"); node->left->visit(this); lua_setfield(L, -2, "lhsoperand"); @@ -997,7 +1013,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "cast"); + serializeNodePreamble(node, "cast", "expr"); node->expr->visit(this); lua_setfield(L, -2, "operand"); @@ -1017,7 +1033,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 7); - serializeNodePreamble(node, "conditional"); + serializeNodePreamble(node, "conditional", "expr"); serializeToken(node->location.begin, "if"); lua_setfield(L, -2, "ifkeyword"); @@ -1084,7 +1100,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 3); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "interpolatedstring"); + serializeNodePreamble(node, "interpolatedstring", "expr"); lua_createtable(L, node->strings.size, 0); lua_createtable(L, node->expressions.size, 0); @@ -1119,7 +1135,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "error"); + serializeNodePreamble(node, "error", "expr"); serializeExprs(node->expressions); lua_setfield(L, -2, "expressions"); @@ -1132,7 +1148,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 1); - serializeNodePreamble(node, "block"); + serializeNodePreamble(node, "block", "stat"); serializeStats(node->body); lua_setfield(L, -2, "statements"); @@ -1143,7 +1159,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 8); - serializeNodePreamble(node, "conditional"); + serializeNodePreamble(node, "conditional", "stat"); serializeToken(node->location.begin, "if"); lua_setfield(L, -2, "ifkeyword"); @@ -1212,7 +1228,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 5); - serializeNodePreamble(node, "while"); + serializeNodePreamble(node, "while", "stat"); serializeToken(node->location.begin, "while"); lua_setfield(L, -2, "whilekeyword"); @@ -1238,7 +1254,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "repeat"); + serializeNodePreamble(node, "repeat", "stat"); serializeToken(node->location.begin, "repeat"); lua_setfield(L, -2, "repeatKeyword"); @@ -1258,14 +1274,14 @@ struct AstSerialize : public Luau::AstVisitor { lua_rawcheckstack(L, 2); serializeToken(node->location.begin, "break", preambleSize); - serializeNodePreamble(node, "break"); + serializeNodePreamble(node, "break", "stat"); } void serializeStat(Luau::AstStatContinue* node) { lua_rawcheckstack(L, 2); serializeToken(node->location.begin, "continue", preambleSize); - serializeNodePreamble(node, "continue"); + serializeNodePreamble(node, "continue", "stat"); } void serializeStat(Luau::AstStatReturn* node) @@ -1273,7 +1289,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "return"); + serializeNodePreamble(node, "return", "stat"); serializeToken(node->location.begin, "return"); lua_setfield(L, -2, "returnkeyword"); @@ -1288,7 +1304,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 1); - serializeNodePreamble(node, "expression"); + serializeNodePreamble(node, "expression", "stat"); node->expr->visit(this); lua_setfield(L, -2, "expression"); @@ -1299,7 +1315,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "local"); + serializeNodePreamble(node, "local", "stat"); serializeToken(node->location.begin, "local"); lua_setfield(L, -2, "localkeyword"); @@ -1325,7 +1341,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); - serializeNodePreamble(node, "for"); + serializeNodePreamble(node, "for", "stat"); serializeToken(node->location.begin, "for"); lua_setfield(L, -2, "forkeyword"); @@ -1377,7 +1393,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); - serializeNodePreamble(node, "forin"); + serializeNodePreamble(node, "forin", "stat"); serializeToken(node->location.begin, "for"); lua_setfield(L, -2, "forkeyword"); @@ -1414,7 +1430,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); - serializeNodePreamble(node, "assign"); + serializeNodePreamble(node, "assign", "stat"); serializePunctuated(node->vars, cstNode->varsCommaPositions, ","); lua_setfield(L, -2, "variables"); @@ -1431,7 +1447,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "compoundassign"); + serializeNodePreamble(node, "compoundassign", "stat"); node->var->visit(this); lua_setfield(L, -2, "variable"); @@ -1449,7 +1465,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "function"); + serializeNodePreamble(node, "function", "stat"); const auto cstNode = lookupCstNode(node); @@ -1471,7 +1487,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 5); - serializeNodePreamble(node, "localfunction"); + serializeNodePreamble(node, "localfunction", "stat"); serializeAttributes(node->func->attributes); lua_setfield(L, -2, "attributes"); @@ -1503,7 +1519,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 9); - serializeNodePreamble(node, "typealias"); + serializeNodePreamble(node, "typealias", "stat"); const auto cstNode = lookupCstNode(node); @@ -1549,7 +1565,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); - serializeNodePreamble(node, "typefunction"); + serializeNodePreamble(node, "typefunction", "stat"); if (node->exported) serializeToken(node->location.begin, "export"); @@ -1590,7 +1606,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "error"); + serializeNodePreamble(node, "error", "stat"); serializeExprs(node->expressions); lua_setfield(L, -2, "expressions"); @@ -1606,7 +1622,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 6); - serializeNodePreamble(node, "reference"); + serializeNodePreamble(node, "reference", "type"); const auto cstNode = node->prefix || node->hasParameterList ? lookupCstNode(node).get() : nullptr; @@ -1648,7 +1664,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "array"); + serializeNodePreamble(node, "array", "type"); serializeToken(node->location.begin, "{"); lua_setfield(L, -2, "openbrace"); @@ -1674,7 +1690,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "table"); + serializeNodePreamble(node, "table", "type"); serializeToken(node->location.begin, "{"); lua_setfield(L, -2, "openbrace"); @@ -1756,6 +1772,9 @@ struct AstSerialize : public Luau::AstVisitor auto initialPosition = item.stringPosition; serializeToken(item.stringPosition, item.stringInfo->sourceString.data); + lua_pushstring(L, "string"); + lua_setfield(L, -2, "tag"); + switch (item.stringInfo->quoteStyle) { case Luau::CstExprConstantString::QuotedSingle: @@ -1816,7 +1835,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 10); - serializeNodePreamble(node, "function"); + serializeNodePreamble(node, "function", "type"); const auto cstNode = lookupCstNode(node); @@ -1896,7 +1915,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "typeof"); + serializeNodePreamble(node, "typeof", "type"); serializeToken(node->location.begin, "typeof"); lua_setfield(L, -2, "typeof"); @@ -1919,7 +1938,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "union"); + serializeNodePreamble(node, "union", "type"); if (cstNode->leadingPosition) serializeToken(*cstNode->leadingPosition, "|"); @@ -1970,7 +1989,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "intersection"); + serializeNodePreamble(node, "intersection", "type"); if (cstNode->leadingPosition) serializeToken(*cstNode->leadingPosition, "&"); @@ -1985,7 +2004,7 @@ struct AstSerialize : public Luau::AstVisitor void serializeType(Luau::AstTypeSingletonBool* node) { serializeToken(node->location.begin, node->value ? "true" : "false", preambleSize + 1); - serializeNodePreamble(node, "boolean"); + serializeNodePreamble(node, "boolean", "type"); lua_pushboolean(L, node->value); lua_setfield(L, -2, "value"); @@ -1995,7 +2014,7 @@ struct AstSerialize : public Luau::AstVisitor { const auto cstNode = lookupCstNode(node); serializeToken(node->location.begin, cstNode->sourceString.data, preambleSize + 1); - serializeNodePreamble(node, "string"); + serializeNodePreamble(node, "string", "type"); switch (cstNode->quoteStyle) { @@ -2021,7 +2040,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "group"); + serializeNodePreamble(node, "group", "type"); serializeToken(node->location.begin, "("); lua_setfield(L, -2, "openparens"); @@ -2038,7 +2057,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "generic"); + serializeNodePreamble(node, "generic", "type"); const auto cstNode = lookupCstNode(node); @@ -2063,7 +2082,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "generic"); + serializeNodePreamble(node, "generic", "typepack"); const auto cstNode = lookupCstNode(node); @@ -2096,7 +2115,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 4); - serializeNodePreamble(node, "explicit"); + serializeNodePreamble(node, "explicit", "typepack"); const auto cstNode = lookupCstNode(node); @@ -2127,7 +2146,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "generic"); + serializeNodePreamble(node, "generic", "typepack"); serializeToken(node->location.begin, node->genericName.value); lua_setfield(L, -2, "name"); @@ -2142,7 +2161,7 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); lua_createtable(L, 0, preambleSize + 2); - serializeNodePreamble(node, "variadic"); + serializeNodePreamble(node, "variadic", "typepack"); if (!forVarArg) serializeToken(node->location.begin, "..."); diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index fe9fbcb50..2d895a5ba 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -50,6 +50,12 @@ export type AstFunctionBody = luteLuau.AstFunctionBody export type AstExprAnonymousFunction = luteLuau.AstExprAnonymousFunction +export type AstExprTableItemList = luteLuau.AstExprTableItemList + +export type AstExprTableItemRecord = luteLuau.AstExprTableItemRecord + +export type AstExprTableItemGeneral = luteLuau.AstExprTableItemGeneral + export type AstExprTableItem = luteLuau.AstExprTableItem export type AstExprTable = luteLuau.AstExprTable @@ -62,7 +68,7 @@ export type AstExprInterpString = luteLuau.AstExprInterpString export type AstExprTypeAssertion = luteLuau.AstExprTypeAssertion -export type AstExprIfElseIfs = luteLuau.AstExprIfElseIfs +export type AstElseIfExpr = luteLuau.AstElseIfExpr export type AstExprIfElse = luteLuau.AstExprIfElse @@ -70,7 +76,7 @@ export type AstExpr = luteLuau.AstExpr export type AstStatBlock = luteLuau.AstStatBlock -export type AstStatElseIf = luteLuau.AstStatElseIf +export type AstElseIfStat = luteLuau.AstElseIfStat export type AstStatIf = luteLuau.AstStatIf @@ -130,6 +136,12 @@ export type AstTypeIntersection = luteLuau.AstTypeIntersection export type AstTypeArray = luteLuau.AstTypeArray +export type AstTypeTableItemIndexer = luteLuau.AstTypeTableItemIndexer + +export type AstTypeTableItemStringProperty = luteLuau.AstTypeTableItemStringProperty + +export type AstTypeTableItemProperty = luteLuau.AstTypeTableItemProperty + export type AstTypeTableItem = luteLuau.AstTypeTableItem export type AstTypeTable = luteLuau.AstTypeTable @@ -148,6 +160,8 @@ export type AstTypePackVariadic = luteLuau.AstTypePackVariadic export type AstTypePack = luteLuau.AstTypePack +export type AstNode = luteLuau.AstNode + export type ParseResult = luteLuau.ParseResult export type Bytecode = luteLuau.Bytecode diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 9bdd8ac91..39285de7d 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -108,11 +108,11 @@ test.suite("Parser", function(suite) local trailingToken: luau.AstExpr = (firstStmt :: luau.AstStatLocal).values[1].node assert.eq(trailingToken.tag, "string") - trailingToken = trailingToken :: luau.AstExprConstantString - assert.eq(#trailingToken.trailingtrivia, 3) - assert.eq(trailingToken.trailingtrivia[1].text, " ") - assert.eq(trailingToken.trailingtrivia[2].text, "-- comment") - assert.eq(trailingToken.trailingtrivia[3].text, "\n") + local expr = trailingToken :: luau.AstExprConstantString + assert.eq(#expr.trailingtrivia, 3) + assert.eq(expr.trailingtrivia[1].text, " ") + assert.eq(expr.trailingtrivia[2].text, "-- comment") + assert.eq(expr.trailingtrivia[3].text, "\n") local secondStmt = block.statements[2] assert.eq(secondStmt.tag, "local") @@ -190,4 +190,969 @@ test.suite("Parser", function(suite) end) end) +test.suite("parseExpr", function(suite) + suite:case("parseExprGroup", function(assert) + local groupExpr = parser.parseexpr("(x)") + assert.eq(groupExpr.tag, "group") + assert.eq(groupExpr.kind, "expr") + local expr = groupExpr :: luau.AstExprGroup + assert.eq(expr.openparens.text, "(") + assert.eq(expr.closeparens.text, ")") + assert.eq(expr.expression.tag, "global") + end) + + suite:case("parseExprNil", function(assert) + local expr = parser.parseexpr("nil") + assert.eq(expr.tag, "nil") + assert.eq(expr.kind, "expr") + assert.eq((expr :: luau.AstExprConstantNil).text, "nil") + end) + + suite:case("parseExprBoolean", function(assert) + local trueExpr = parser.parseexpr("true") + assert.eq(trueExpr.tag, "boolean") + assert.eq(trueExpr.kind, "expr") + assert.eq((trueExpr :: luau.AstExprConstantBool).text, "true") + assert.eq((trueExpr :: luau.AstExprConstantBool).value, true) + + local falseExpr = parser.parseexpr("false") + assert.eq(falseExpr.tag, "boolean") + assert.eq(falseExpr.kind, "expr") + assert.eq((falseExpr :: luau.AstExprConstantBool).text, "false") + assert.eq((falseExpr :: luau.AstExprConstantBool).value, false) + end) + + suite:case("parseExprNumber", function(assert) + local numberExpr = parser.parseexpr("42") + assert.eq(numberExpr.tag, "number") + assert.eq(numberExpr.kind, "expr") + assert.eq((numberExpr :: luau.AstExprConstantNumber).text, "42") + assert.eq((numberExpr :: luau.AstExprConstantNumber).value, 42) + + local floatExpr = parser.parseexpr("3.14") + assert.eq(floatExpr.tag, "number") + assert.eq(floatExpr.kind, "expr") + assert.eq((floatExpr :: luau.AstExprConstantNumber).text, "3.14") + assert.eq((floatExpr :: luau.AstExprConstantNumber).value, 3.14) + end) + + suite:case("parseExprString", function(assert) + local singleQuoteExpr = parser.parseexpr("'hello'") + assert.eq(singleQuoteExpr.tag, "string") + assert.eq(singleQuoteExpr.kind, "expr") + assert.eq((singleQuoteExpr :: luau.AstExprConstantString).text, "hello") + assert.eq((singleQuoteExpr :: luau.AstExprConstantString).quotestyle, "single") + + local doubleQuoteExpr = parser.parseexpr('"world"') + assert.eq(doubleQuoteExpr.tag, "string") + assert.eq(doubleQuoteExpr.kind, "expr") + assert.eq((doubleQuoteExpr :: luau.AstExprConstantString).text, "world") + assert.eq((doubleQuoteExpr :: luau.AstExprConstantString).quotestyle, "double") + + local blockQuoteExpr = parser.parseexpr("[[world]]") + assert.eq(blockQuoteExpr.tag, "string") + assert.eq(blockQuoteExpr.kind, "expr") + assert.eq((blockQuoteExpr :: luau.AstExprConstantString).text, "world") + assert.eq((blockQuoteExpr :: luau.AstExprConstantString).quotestyle, "block") + + local interpExpr = parser.parseexpr("`foobar`") + assert.eq(interpExpr.tag, "string") + assert.eq(interpExpr.kind, "expr") + assert.eq((interpExpr :: luau.AstExprConstantString).text, "foobar") + assert.eq((interpExpr :: luau.AstExprConstantString).quotestyle, "interp") + end) + + suite:case("parseExprLocal", function(assert) + local localExpr = parser.parseexpr("x") + assert.eq(localExpr.tag, "global") + assert.eq(localExpr.kind, "expr") + assert.eq((localExpr :: luau.AstExprGlobal).name.text, "x") + end) + + suite:case("parseExprGlobal", function(assert) + local globalExpr = parser.parseexpr("_G") + assert.eq(globalExpr.tag, "global") + assert.eq(globalExpr.kind, "expr") + assert.eq((globalExpr :: luau.AstExprGlobal).name.text, "_G") + end) + + suite:case("parseExprVarargs", function(assert) + local varargsExpr = parser.parseexpr("...") + assert.eq(varargsExpr.tag, "vararg") + assert.eq(varargsExpr.kind, "expr") + assert.eq((varargsExpr :: luau.AstExprVarargs).text, "...") + end) + + suite:case("parseExprCall", function(assert) + local callExpr = parser.parseexpr("foo()") + assert.eq(callExpr.tag, "call") + assert.eq(callExpr.kind, "expr") + assert.eq((callExpr :: luau.AstExprCall).func.tag, "global") + assert.eq(((callExpr :: luau.AstExprCall).func :: luau.AstExprGlobal).name.text, "foo") + assert.eq((callExpr :: luau.AstExprCall).self, false) + assert.eq(#(callExpr :: luau.AstExprCall).arguments, 0) + + local callWithArgsExpr = parser.parseexpr("bar(1, 2)") + assert.eq(callWithArgsExpr.tag, "call") + assert.eq(callWithArgsExpr.kind, "expr") + assert.eq(#(callWithArgsExpr :: luau.AstExprCall).arguments, 2) + assert.eq((callWithArgsExpr :: luau.AstExprCall).arguments[1].node.tag, "number") + assert.eq((callWithArgsExpr :: luau.AstExprCall).arguments[2].node.tag, "number") + end) + + suite:case("parseExprIndexName", function(assert) + local indexExpr = parser.parseexpr("obj.field") + assert.eq(indexExpr.tag, "indexname") + assert.eq(indexExpr.kind, "expr") + assert.eq((indexExpr :: luau.AstExprIndexName).expression.tag, "global") + assert.eq((indexExpr :: luau.AstExprIndexName).accessor.text, ".") + assert.eq((indexExpr :: luau.AstExprIndexName).index.text, "field") + end) + + suite:case("parseExprIndexExpr", function(assert) + local indexExpr = parser.parseexpr("arr[1]") + assert.eq(indexExpr.tag, "index") + assert.eq(indexExpr.kind, "expr") + assert.eq((indexExpr :: luau.AstExprIndexExpr).expression.tag, "global") + assert.eq((indexExpr :: luau.AstExprIndexExpr).index.tag, "number") + assert.eq((indexExpr :: luau.AstExprIndexExpr).openbrackets.text, "[") + assert.eq((indexExpr :: luau.AstExprIndexExpr).closebrackets.text, "]") + end) + + suite:case("parseExprAnonymousFunction", function(assert) + local anonFuncExpr = parser.parseexpr("function(a, b) return a + b end") + assert.eq(anonFuncExpr.tag, "function") + assert.eq(anonFuncExpr.kind, "expr") + + local funcExpr = anonFuncExpr :: luau.AstExprAnonymousFunction + assert.eq(#funcExpr.attributes, 0) + assert.eq(funcExpr.functionkeyword.text, "function") + + local funcBody = funcExpr.body + assert.eq(funcBody.opengenerics, nil) + assert.eq(funcBody.generics, nil) + assert.eq(funcBody.genericpacks, nil) + assert.eq(funcBody.closegenerics, nil) + assert.eq(funcBody.self, nil) + assert.eq(funcBody.openparens.text, "(") + assert.eq(#funcBody.parameters, 2) + assert.eq(funcBody.parameters[1].node.name.text, "a") + assert.eq(funcBody.parameters[2].node.name.text, "b") + assert.eq(funcBody.vararg, nil) + assert.eq(funcBody.varargcolon, nil) + assert.eq(funcBody.varargannotation, nil) + assert.eq(funcBody.closeparens.text, ")") + assert.eq(funcBody.returnspecifier, nil) + assert.eq(funcBody.returnannotation, nil) + assert.eq(funcBody.endkeyword.text, "end") + + anonFuncExpr = parser.parseexpr("function(a: A, ...: B...): A return a end") + assert.eq(anonFuncExpr.tag, "function") + assert.eq(anonFuncExpr.kind, "expr") + + funcExpr = anonFuncExpr :: luau.AstExprAnonymousFunction + assert.eq(#funcExpr.attributes, 0) + assert.eq(funcExpr.functionkeyword.text, "function") + + funcBody = funcExpr.body + assert.neq(funcBody.opengenerics, nil) + assert.neq(funcBody.generics, nil) + + assert.eq(#funcBody.generics :: luau.Punctuated, 1) + local gen = (funcBody.generics :: luau.Punctuated)[1].node + assert.eq(gen.name.text, "A") + assert.eq(gen.equals, nil) + assert.eq(gen.default, nil) + + assert.neq(funcBody.genericpacks, nil) + assert.eq(#funcBody.genericpacks :: luau.Punctuated, 1) + local genPack = (funcBody.genericpacks :: luau.Punctuated)[1].node + assert.eq(genPack.name.text, "B") + assert.eq(genPack.ellipsis.text, "...") + assert.eq(genPack.equals, nil) + assert.eq(genPack.default, nil) + + assert.neq(funcBody.closegenerics, nil) + assert.eq((funcBody.closegenerics :: luau.Token<">">).text, ">") + + assert.eq(funcBody.openparens.text, "(") + assert.eq(#funcBody.parameters, 1) + + local param = funcBody.parameters[1].node + assert.eq(param.name.text, "a") + assert.neq(param.colon, nil) + assert.eq((param.colon :: luau.Token<":">).text, ":") + assert.neq(param.annotation, nil) + assert.eq((param.annotation :: luau.AstType).tag, "reference") + + assert.neq(funcBody.vararg, nil) + assert.eq((funcBody.vararg :: luau.Token<"...">).text, "...") + assert.neq(funcBody.varargcolon, nil) + assert.eq((funcBody.varargcolon :: luau.Token<":">).text, ":") + assert.neq(funcBody.varargannotation, nil) + assert.eq((funcBody.varargannotation :: luau.AstTypePack).tag, "generic") + + assert.eq(funcBody.closeparens.text, ")") + assert.neq(funcBody.returnspecifier, nil) + assert.eq((funcBody.returnspecifier :: luau.Token<":">).text, ":") + assert.neq(funcBody.returnannotation, nil) + assert.eq((funcBody.returnannotation :: luau.AstTypePack).tag, "explicit") + end) + + suite:case("parseExprTable", function(assert) + local emptyTableExpr = parser.parseexpr("{}") + assert.eq(emptyTableExpr.tag, "table") + assert.eq(emptyTableExpr.kind, "expr") + local tableExpr = emptyTableExpr :: luau.AstExprTable + assert.eq(tableExpr.openbrace.text, "{") + assert.eq(tableExpr.closebrace.text, "}") + assert.eq(#tableExpr.entries, 0) + + local mixedTable = parser.parseexpr("{1, foo = bar; [2] = baz}") + assert.eq(mixedTable.tag, "table") + assert.eq(mixedTable.kind, "expr") + local mixedTableExpr = mixedTable :: luau.AstExprTable + assert.eq(#mixedTableExpr.entries, 3) + + local entry1 = mixedTableExpr.entries[1] + assert.eq(entry1.kind, "list") + assert.eq(entry1.value.tag, "number") + assert.neq(entry1.separator, nil) + assert.eq((entry1.separator :: luau.Token<"," | ";">).text, ",") + + local entry2: any = mixedTableExpr.entries[2] + assert.eq(entry2.kind, "record") + assert.eq(entry2.key.text, "foo") + assert.eq(entry2.equals.text, "=") + assert.eq(entry2.value.tag, "global") + assert.neq(entry2.separator, nil) + assert.eq((entry2.separator :: luau.Token<"," | ";">).text, ";") + + local entry3: any = mixedTableExpr.entries[3] + assert.eq(entry3.kind, "general") + assert.eq(entry3.indexeropen.text, "[") + assert.eq(entry3.key.tag, "number") + assert.eq(entry3.indexerclose.text, "]") + assert.eq(entry3.equals.text, "=") + assert.eq(entry3.value.tag, "global") + assert.eq(entry3.separator, nil) + end) + + suite:case("parseExprUnary", function(assert) + local notExpr = parser.parseexpr("not x") + assert.eq(notExpr.tag, "unary") + assert.eq(notExpr.kind, "expr") + local expr = notExpr :: luau.AstExprUnary + assert.eq(expr.operator.text, "not") + assert.eq(expr.operand.tag, "global") + + local negExpr = parser.parseexpr("-42") + assert.eq(negExpr.tag, "unary") + assert.eq(negExpr.kind, "expr") + expr = negExpr :: luau.AstExprUnary + assert.eq(expr.operator.text, "-") + assert.eq(expr.operand.tag, "number") + + local lenExpr = parser.parseexpr("#str") + assert.eq(lenExpr.tag, "unary") + assert.eq(lenExpr.kind, "expr") + expr = lenExpr :: luau.AstExprUnary + assert.eq(expr.operator.text, "#") + assert.eq(expr.operand.tag, "global") + end) + + suite:case("parseExprBinary", function(assert) + local addExpr = parser.parseexpr("1 + 2") + assert.eq(addExpr.tag, "binary") + assert.eq(addExpr.kind, "expr") + local expr = addExpr :: luau.AstExprBinary + assert.eq(expr.lhsoperand.tag, "number") + assert.eq(expr.operator.text, "+") + assert.eq(expr.rhsoperand.tag, "number") + end) + + suite:case("parseExprInterpString", function(assert) + local interpExpr = parser.parseexpr("`Hello, {name}! Today is {dayOfWeek}.`") + assert.eq(interpExpr.tag, "interpolatedstring") + assert.eq(interpExpr.kind, "expr") + local expr = interpExpr :: luau.AstExprInterpString + assert.eq(#expr.strings, 3) + + assert.eq(expr.strings[1].text, "Hello, ") + assert.eq(expr.strings[2].text, "! Today is ") + assert.eq(expr.strings[3].text, ".") + + assert.eq(#expr.expressions, 2) + assert.eq(expr.expressions[1].tag, "global") + assert.eq(expr.expressions[2].tag, "global") + end) + + suite:case("parseExprTypeAssertion", function(assert) + local castExpr = parser.parseexpr("x :: string") + assert.eq(castExpr.tag, "cast") + assert.eq(castExpr.kind, "expr") + local expr = castExpr :: luau.AstExprTypeAssertion + assert.eq(expr.operand.tag, "global") + assert.eq(expr.operator.text, "::") + assert.eq(expr.annotation.tag, "reference") + end) +end) + +local function checkIsBlock(node: any, assert: asserts.Asserts) + assert.eq(node.kind, "stat") + assert.eq(node.tag, "block") +end + +test.suite("parse", function(suite) + suite:case("parseEmptyProgram", function(assert) + local block = parser.parse("") + checkIsBlock(block, assert) + assert.eq(#block.statements, 0) + end) + + suite:case("parseIfStatement", function(assert) + local block = + parser.parse("if x > 0 then print('positive') elseif x < 0 then print('negative') else print('zero') end") + assert.eq(#block.statements, 1) + + local ifStat = block.statements[1] :: luau.AstStatIf + assert.eq(ifStat.tag, "conditional") + assert.eq(ifStat.ifkeyword.text, "if") + assert.eq(ifStat.condition.tag, "binary") + assert.eq(ifStat.thenkeyword.text, "then") + checkIsBlock(ifStat.thenblock, assert) + + local thenBlock = ifStat.thenblock + assert.eq(#thenBlock.statements, 1) + + assert.eq(#ifStat.elseifs, 1) + local elseIf = ifStat.elseifs[1] + assert.eq(elseIf.elseifkeyword.text, "elseif") + assert.eq(elseIf.condition.tag, "binary") + assert.eq(elseIf.thenkeyword.text, "then") + checkIsBlock(elseIf.thenblock, assert) + + assert.neq(ifStat.elsekeyword, nil) + assert.eq((ifStat.elsekeyword :: luau.Token<"else">).text, "else") + assert.neq(ifStat.elseblock, nil) + checkIsBlock(ifStat.elseblock, assert) + assert.eq(ifStat.endkeyword.text, "end") + + block = parser.parse("if condition then doSomething() end") + assert.eq(#block.statements, 1) + local simpleIfStat = block.statements[1] :: luau.AstStatIf + assert.eq(simpleIfStat.tag, "conditional") + assert.eq(simpleIfStat.ifkeyword.text, "if") + assert.eq(simpleIfStat.condition.tag, "global") + assert.eq(simpleIfStat.thenkeyword.text, "then") + checkIsBlock(simpleIfStat.thenblock, assert) + assert.eq(#simpleIfStat.thenblock.statements, 1) + assert.eq(#simpleIfStat.elseifs, 0) + assert.eq(simpleIfStat.elsekeyword, nil) + assert.eq(simpleIfStat.elseblock, nil) + assert.eq(simpleIfStat.endkeyword.text, "end") + end) + + suite:case("parseStatWhile", function(assert) + local block = parser.parse("while i <= 10 do print(i); i = i + 1 end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local whileStat = block.statements[1] :: luau.AstStatWhile + assert.eq(whileStat.tag, "while") + assert.eq(whileStat.whilekeyword.text, "while") + assert.eq(whileStat.condition.tag, "binary") + assert.eq(whileStat.dokeyword.text, "do") + checkIsBlock(whileStat.body, assert) + assert.eq(#whileStat.body.statements, 2) + assert.eq(whileStat.endkeyword.text, "end") + + block = parser.parse("while true do break end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + whileStat = block.statements[1] :: luau.AstStatWhile + assert.eq(whileStat.tag, "while") + assert.eq(whileStat.whilekeyword.text, "while") + assert.eq(whileStat.condition.tag, "boolean") + assert.eq(whileStat.dokeyword.text, "do") + checkIsBlock(whileStat.body, assert) + assert.eq(#whileStat.body.statements, 1) + + local breakStat = whileStat.body.statements[1] :: luau.AstStat + assert.eq(breakStat.kind, "stat") + breakStat = breakStat :: luau.AstStatBreak + assert.eq(breakStat.tag, "break") + assert.eq(breakStat.text, "break") + + assert.eq(whileStat.endkeyword.text, "end") + + block = parser.parse("while true do continue end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + whileStat = block.statements[1] :: luau.AstStatWhile + assert.eq(whileStat.tag, "while") + assert.eq(whileStat.whilekeyword.text, "while") + assert.eq(whileStat.condition.tag, "boolean") + assert.eq(whileStat.dokeyword.text, "do") + checkIsBlock(whileStat.body, assert) + assert.eq(#whileStat.body.statements, 1) + + local continueStat = whileStat.body.statements[1] :: luau.AstStat + assert.eq(continueStat.kind, "stat") + continueStat = continueStat :: luau.AstStatContinue + assert.eq(continueStat.tag, "continue") + assert.eq(continueStat.text, "continue") + + assert.eq(whileStat.endkeyword.text, "end") + end) + + suite:case("parseStatRepeat", function(assert) + local block = parser.parse("repeat i = i + 1; print(i) until i > 5") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local repeatStat = block.statements[1] :: luau.AstStatRepeat + assert.eq(repeatStat.tag, "repeat") + assert.eq(repeatStat.repeatKeyword.text, "repeat") + checkIsBlock(repeatStat.body, assert) + assert.eq(#repeatStat.body.statements, 2) + assert.eq(repeatStat.untilKeyword.text, "until") + assert.eq(repeatStat.condition.tag, "binary") + end) + + suite:case("parseStatLocal", function(assert) + local block = parser.parse("local b, c = 1, 2") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + assert.eq(block.statements[1].kind, "stat") + + local l = block.statements[1] :: luau.AstStatLocal + assert.eq(l.tag, "local") + assert.eq(l.localkeyword.text, "local") + assert.eq(#l.variables, 2) + assert.eq(l.variables[1].node.name.text, "b") + assert.neq(l.variables[1].separator, nil) + assert.eq((l.variables[1].separator :: luau.Token<",">).text, ",") + assert.eq(l.variables[2].node.name.text, "c") + assert.eq(l.variables[2].separator, nil) + assert.neq(l.equals, nil) + assert.eq((l.equals :: luau.Token<"=">).text, "=") + assert.eq(#l.values, 2) + assert.eq(l.values[1].node.tag, "number") + assert.neq(l.values[1].separator, nil) + assert.eq((l.values[1].separator :: luau.Token<",">).text, ",") + assert.eq(l.values[2].node.tag, "number") + assert.eq(l.values[2].separator, nil) + + block = parser.parse("local x") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + assert.eq(block.statements[1].kind, "stat") + + l = block.statements[1] :: luau.AstStatLocal + assert.eq(l.tag, "local") + assert.eq(l.localkeyword.text, "local") + assert.eq(#l.variables, 1) + assert.eq(l.variables[1].node.name.text, "x") + assert.eq(l.variables[1].separator, nil) + assert.eq(l.equals, nil) + assert.eq(#l.values, 0) + end) + + suite:case("parseStatFor", function(assert) + local block = parser.parse("for i = 1, 10, 2 do print(i) end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local forStat = block.statements[1] :: luau.AstStatFor + assert.eq(forStat.tag, "for") + assert.eq(forStat.forkeyword.text, "for") + assert.eq(forStat.variable.name.text, "i") + assert.eq(forStat.equals.text, "=") + assert.eq(forStat.from.tag, "number") + assert.eq(forStat.tocomma.text, ",") + assert.eq(forStat.to.tag, "number") + assert.neq(forStat.stepcomma, nil) + assert.eq((forStat.stepcomma :: luau.Token<",">).text, ",") + assert.neq(forStat.step, nil) + assert.eq(((forStat.step :: any) :: luau.AstExpr).tag, "number") + assert.eq(forStat.dokeyword.text, "do") + checkIsBlock(forStat.body, assert) + assert.eq(#forStat.body.statements, 1) + assert.eq(forStat.endkeyword.text, "end") + + block = parser.parse("for j = 0, 5 do print(j) end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + forStat = block.statements[1] :: luau.AstStatFor + assert.eq(forStat.tag, "for") + assert.eq(forStat.forkeyword.text, "for") + assert.eq(forStat.variable.name.text, "j") + assert.eq(forStat.equals.text, "=") + assert.eq(forStat.from.tag, "number") + assert.eq(forStat.tocomma.text, ",") + assert.eq(forStat.to.tag, "number") + assert.eq(forStat.stepcomma, nil) + assert.eq(forStat.step, nil) + assert.eq(forStat.dokeyword.text, "do") + checkIsBlock(forStat.body, assert) + assert.eq(#forStat.body.statements, 1) + assert.eq(forStat.endkeyword.text, "end") + end) + + suite:case("parseForInLoop", function(assert) + local block = parser.parse("for key, value in arr do print(key, value) end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local forInStat = block.statements[1] :: luau.AstStatForIn + assert.eq(forInStat.tag, "forin") + assert.eq(forInStat.forkeyword.text, "for") + assert.eq(#forInStat.variables, 2) + assert.eq(forInStat.variables[1].node.name.text, "key") + assert.eq(forInStat.variables[2].node.name.text, "value") + assert.eq(forInStat.inkeyword.text, "in") + assert.eq(#forInStat.values, 1) + assert.eq(forInStat.values[1].node.tag, "global") + assert.eq(forInStat.dokeyword.text, "do") + checkIsBlock(forInStat.body, assert) + assert.eq(#forInStat.body.statements, 1) + assert.eq(forInStat.endkeyword.text, "end") + end) + + suite:case("parseStatAssign", function(assert) + local block = parser.parse("x = 1") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local assign = block.statements[1] :: luau.AstStatAssign + assert.eq(assign.tag, "assign") + assert.eq(#assign.variables, 1) + assert.eq(assign.variables[1].node.tag, "global") + assert.eq(assign.equals.text, "=") + assert.eq(#assign.values, 1) + assert.eq(assign.values[1].node.tag, "number") + end) + + suite:case("parseStatCompoundAssign", function(assert) + local block = parser.parse("x += 5") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local compound = block.statements[1] :: luau.AstStatCompoundAssign + assert.eq(compound.tag, "compoundassign") + assert.eq(compound.variable.tag, "global") + assert.eq(compound.operand.text, "+=") + assert.eq(compound.value.tag, "number") + end) + + suite:case("parseStatFunction", function(assert) + local block = parser.parse("@checked @native @deprecated function add(a, b) return a + b end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local funcStat = block.statements[1] :: luau.AstStatFunction + assert.eq(funcStat.tag, "function") + assert.eq(#funcStat.attributes, 3) + assert.eq((funcStat.attributes[1] :: luau.AstAttribute).text, "@checked") + assert.eq((funcStat.attributes[2] :: luau.AstAttribute).text, "@native") + assert.eq((funcStat.attributes[3] :: luau.AstAttribute).text, "@deprecated") + assert.eq(funcStat.functionkeyword.text, "function") + assert.eq(funcStat.name.tag, "global") + assert.eq((funcStat.name :: luau.AstExprGlobal).name.text, "add") + -- body parsing tested in parseExprAnonymousFunction + end) + + suite:case("parseStatLocalFunction", function(assert) + local block = parser.parse("local function multiply(x, y) return x * y end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local localFuncStat = block.statements[1] :: luau.AstStatLocalFunction + assert.eq(localFuncStat.tag, "localfunction") + assert.eq(#localFuncStat.attributes, 0) + assert.eq(localFuncStat.localkeyword.text, "local") + assert.eq(localFuncStat.functionkeyword.text, "function") + assert.eq(localFuncStat.name.name.text, "multiply") + end) + + suite:case("parseStatTypeAlias", function(assert) + local block = parser.parse("type Vector3 = {x: number, y: number, z: number}") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.tag, "typealias") + assert.eq(typeAlias.export, nil) + assert.eq(typeAlias.typeToken.text, "type") + assert.eq(typeAlias.name.text, "Vector3") + assert.eq(typeAlias.opengenerics, nil) + assert.eq(typeAlias.generics, nil) + assert.eq(typeAlias.closegenerics, nil) + assert.eq(typeAlias.equals.text, "=") + assert.eq(typeAlias.type.kind, "type") + assert.eq(typeAlias.type.tag, "table") + + block = parser.parse("export type Point = {x: T, y: T, f: (U...) -> ()}") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.neq(typeAlias.export, nil) + assert.eq((typeAlias.export :: luau.Token<"export">).text, "export") + assert.neq(typeAlias.opengenerics, nil) + assert.eq((typeAlias.opengenerics :: luau.Token<"<">).text, "<") + assert.neq(typeAlias.generics, nil) + assert.eq(#typeAlias.generics :: luau.Punctuated, 1) + assert.neq(typeAlias.genericpacks, nil) + assert.eq(#typeAlias.genericpacks :: luau.Punctuated, 1) + assert.neq(typeAlias.closegenerics, nil) + assert.eq((typeAlias.closegenerics :: luau.Token<">">).text, ">") + end) + + suite:case("parseStatTypeFunction", function(assert) + local block = parser.parse("type function foo() return types.number end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + local typeFunc = block.statements[1] :: luau.AstStatTypeFunction + assert.eq(typeFunc.tag, "typefunction") + assert.eq(typeFunc.export, nil) + assert.eq(typeFunc.type.text, "type") + assert.eq(typeFunc.functionkeyword.text, "function") + assert.eq(typeFunc.name.text, "foo") + + block = parser.parse("export type function foo() return types.number end") + assert.eq(block.tag, "block") + assert.eq(#block.statements, 1) + + typeFunc = block.statements[1] :: luau.AstStatTypeFunction + assert.neq(typeFunc.export, nil) + assert.eq((typeFunc.export :: luau.Token<"export">).text, "export") + end) +end) + +test.suite("parse types", function(suite) + -- AstGenericType and AstGenericTypePack are tested in parseExprAnonymousFunction + suite:case("testTypeReference", function(assert) + local block = parser.parse("type MyString = string") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "reference") + + local typeRef = typeAlias.type :: luau.AstTypeReference + assert.eq(typeRef.prefix, nil) + assert.eq(typeRef.prefixpoint, nil) + assert.eq(typeRef.name.text, "string") + assert.eq(typeRef.openparameters, nil) + assert.eq(typeRef.parameters, nil) + assert.eq(typeRef.closeparameters, nil) + + block = parser.parse("type MyTable = MyModule.Table") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "reference") + + typeRef = typeAlias.type :: luau.AstTypeReference + assert.neq(typeRef.prefix, nil) + assert.eq((typeRef.prefix :: luau.Token).text, "MyModule") + assert.neq(typeRef.prefixpoint, nil) + assert.eq((typeRef.prefixpoint :: luau.Token<".">).text, ".") + assert.eq(typeRef.name.text, "Table") + assert.neq(typeRef.openparameters, nil) + assert.eq((typeRef.openparameters :: luau.Token<"<">).text, "<") + assert.neq(typeRef.parameters, nil) + assert.eq(#typeRef.parameters :: luau.Punctuated, 2) + assert.eq((typeRef.parameters :: luau.Punctuated)[1].node.tag, "reference") + assert.eq((typeRef.parameters :: luau.Punctuated)[2].node.tag, "reference") + assert.neq(typeRef.closeparameters, nil) + assert.eq((typeRef.closeparameters :: luau.Token<">">).text, ">") + end) + + suite:case("testTypeSingletonBoolean", function(assert) + local block = parser.parse("type TrueType = true") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "boolean") + + local singletonBool = typeAlias.type :: luau.AstTypeSingletonBool + assert.eq(singletonBool.text, "true") + assert.eq(singletonBool.value, true) + + block = parser.parse("type FalseType = false") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "boolean") + + singletonBool = typeAlias.type :: luau.AstTypeSingletonBool + assert.eq(singletonBool.text, "false") + assert.eq(singletonBool.value, false) + end) + + suite:case("testTypeSingletonString", function(assert) + local block = parser.parse("type HelloType = 'hello'") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "string") + + local singletonStr = typeAlias.type :: luau.AstTypeSingletonString + assert.eq(singletonStr.text, "hello") + assert.eq(singletonStr.quotestyle, "single") + + block = parser.parse('type WorldType = "world"') + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "string") + + singletonStr = typeAlias.type :: luau.AstTypeSingletonString + assert.eq(singletonStr.text, "world") + assert.eq(singletonStr.quotestyle, "double") + end) + + suite:case("testTypeTypeof", function(assert) + local block = parser.parse("type T = typeof(someVariable)") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "typeof") + + local typeofType = typeAlias.type :: luau.AstTypeTypeof + assert.eq(typeofType.typeof.text, "typeof") + assert.eq(typeofType.openparens.text, "(") + assert.eq(typeofType.expression.tag, "global") + assert.eq(typeofType.closeparens.text, ")") + end) + + suite:case("testTypeGroup", function(assert) + local block = parser.parse("type T = (string)") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "group") + + local groupType = typeAlias.type :: luau.AstTypeGroup + assert.eq(groupType.openparens.text, "(") + assert.eq(groupType.type.tag, "reference") + assert.eq(groupType.closeparens.text, ")") + end) + + suite:case("testTypeUnion", function(assert) + local block = parser.parse("type T = string | number | boolean") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "union") + + local unionType = typeAlias.type :: luau.AstTypeUnion + assert.eq(unionType.leading, nil) + assert.eq(#unionType.types, 3) + assert.eq(unionType.types[1].node.tag, "reference") + assert.eq(unionType.types[2].node.tag, "reference") + assert.eq(unionType.types[3].node.tag, "reference") + + block = parser.parse("type T = | number?") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "union") + + unionType = typeAlias.type :: luau.AstTypeUnion + assert.neq(unionType.leading, nil) + assert.eq((unionType.leading :: luau.Token<"|">).text, "|") + assert.eq(#unionType.types, 2) + assert.eq(unionType.types[1].node.tag, "reference") + assert.eq(unionType.types[2].node.tag, "optional") + assert.eq((unionType.types[2].node :: luau.AstTypeOptional).text, "?") + end) + + suite:case("testTypeIntersection", function(assert) + local block = parser.parse("type T = A & B & C") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "intersection") + + local intersectionType = typeAlias.type :: luau.AstTypeIntersection + assert.eq(intersectionType.leading, nil) + assert.eq(#intersectionType.types, 3) + assert.eq(intersectionType.types[1].node.tag, "reference") + assert.eq(intersectionType.types[2].node.tag, "reference") + assert.eq(intersectionType.types[3].node.tag, "reference") + + block = parser.parse("type T = & B") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "intersection") + + intersectionType = typeAlias.type :: luau.AstTypeIntersection + assert.neq(intersectionType.leading, nil) + assert.eq((intersectionType.leading :: luau.Token<"&">).text, "&") + assert.eq(#intersectionType.types, 1) + assert.eq(intersectionType.types[1].node.tag, "reference") + end) + + suite:case("testTypeArray", function(assert) + local block = parser.parse("type T = { number }") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "array") + + local arrayType = typeAlias.type :: luau.AstTypeArray + assert.eq(arrayType.openbrace.text, "{") + assert.eq(arrayType.access, nil) + assert.eq(arrayType.type.tag, "reference") + assert.eq(arrayType.closebrace.text, "}") + + block = parser.parse("type T = { read string }") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "array") + + arrayType = typeAlias.type :: luau.AstTypeArray + assert.neq(arrayType.access, nil) + assert.eq((arrayType.access :: luau.Token<"read" | "write">).text, "read") + + block = parser.parse("type T = { write number }") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "array") + + arrayType = typeAlias.type :: luau.AstTypeArray + assert.neq(arrayType.access, nil) + assert.eq((arrayType.access :: luau.Token<"read" | "write">).text, "write") + end) + + suite:case("parseTypeTable", function(assert) + local block = parser.parse("type T = {}") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "table") + + local tableType = typeAlias.type :: luau.AstTypeTable + assert.eq(tableType.openbrace.text, "{") + assert.eq(#tableType.entries, 0) + assert.eq(tableType.closebrace.text, "}") + + block = parser.parse("type T = {[number] : string}") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "table") + + tableType = typeAlias.type :: luau.AstTypeTable + assert.eq(#tableType.entries, 1) + local tableTypeItem: any = tableType.entries[1] + assert.eq(tableTypeItem.kind, "indexer") + assert.eq(tableTypeItem.access, nil) + assert.eq(tableTypeItem.indexeropen.text, "[") + assert.eq(tableTypeItem.key.tag, "reference") + assert.eq(tableTypeItem.indexerclose.text, "]") + assert.eq(tableTypeItem.colon.text, ":") + assert.eq(tableTypeItem.value.tag, "reference") + assert.eq(tableTypeItem.separator, nil) + + block = parser.parse("type T = {['hello'] : 'world', read ['foo'] : number; write ['bar'] : boolean}") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "table") + tableType = typeAlias.type :: luau.AstTypeTable + assert.eq(#tableType.entries, 3) + tableTypeItem = tableType.entries[1] + assert.eq(tableTypeItem.kind, "stringproperty") + assert.eq(tableTypeItem.access, nil) + assert.eq(tableTypeItem.indexeropen.text, "[") + assert.eq((tableTypeItem.key :: luau.AstTypeSingletonString).tag, "string") + assert.eq((tableTypeItem.key :: luau.AstTypeSingletonString).text, "hello") + assert.eq(tableTypeItem.indexerclose.text, "]") + assert.eq(tableTypeItem.colon.text, ":") + assert.eq(tableTypeItem.value.tag, "string") + assert.neq(tableTypeItem.separator, nil) + assert.eq((tableTypeItem.separator :: luau.Token<"," | ";">).text, ",") + + tableTypeItem = tableType.entries[2] + assert.eq(tableTypeItem.kind, "stringproperty") + assert.neq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.access :: luau.Token<"read" | "write">).text, "read") + assert.neq(tableTypeItem.separator, nil) + assert.eq((tableTypeItem.separator :: luau.Token<"," | ";">).text, ";") + + tableTypeItem = tableType.entries[3] + assert.eq(tableTypeItem.kind, "stringproperty") + assert.neq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.access :: luau.Token<"read" | "write">).text, "write") + assert.eq(tableTypeItem.separator, nil) + + block = parser.parse("type T = { hello: 'world'; read foo: number, write bar: boolean }") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "table") + tableType = typeAlias.type :: luau.AstTypeTable + assert.eq(#tableType.entries, 3) + tableTypeItem = tableType.entries[1] + assert.eq(tableTypeItem.kind, "property") + assert.eq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.key :: luau.Token).text, "hello") + assert.eq(tableTypeItem.colon.text, ":") + assert.eq(tableTypeItem.value.tag, "string") + assert.neq(tableTypeItem.separator, nil) + assert.eq((tableTypeItem.separator :: luau.Token<"," | ";">).text, ";") + + tableTypeItem = tableType.entries[2] + assert.eq(tableTypeItem.kind, "property") + assert.neq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.access :: luau.Token<"read" | "write">).text, "read") + assert.neq(tableTypeItem.separator, nil) + assert.eq((tableTypeItem.separator :: luau.Token<"," | ";">).text, ",") + + tableTypeItem = tableType.entries[3] + assert.eq(tableTypeItem.kind, "property") + assert.neq(tableTypeItem.access, nil) + assert.eq((tableTypeItem.access :: luau.Token<"read" | "write">).text, "write") + assert.eq(tableTypeItem.separator, nil) + end) + + suite:case("parseTypeFunction", function(assert) + local block = parser.parse("type T = (n : number, string) -> boolean") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "function") + + local funcType = typeAlias.type :: luau.AstTypeFunction + assert.eq(funcType.opengenerics, nil) + assert.eq(funcType.generics, nil) + assert.eq(funcType.genericpacks, nil) + assert.eq(funcType.closegenerics, nil) + assert.eq(funcType.openparens.text, "(") + assert.eq(#funcType.parameters, 2) + + local param = funcType.parameters[1].node + assert.neq(param.name, nil) + assert.eq((param.name :: luau.Token).text, "n") + assert.neq(param.colon, nil) + assert.eq((param.colon :: luau.Token<":">).text, ":") + assert.eq(param.type.tag, "reference") + + param = funcType.parameters[2].node + assert.eq(param.name, nil) + assert.eq(param.colon, nil) + assert.eq(param.type.tag, "reference") + + assert.eq(funcType.vararg, nil) + assert.eq(funcType.closeparens.text, ")") + assert.eq(funcType.returnarrow.text, "->") + assert.eq(funcType.returntypes.kind, "typepack") + + block = parser.parse("type T = (item: T, ...U) -> ()") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "function") + + funcType = typeAlias.type :: luau.AstTypeFunction + assert.neq(funcType.opengenerics, nil) + assert.eq((funcType.opengenerics :: luau.Token<"<">).text, "<") + assert.neq(funcType.generics, nil) + assert.eq(#funcType.generics :: luau.Punctuated, 1) + assert.neq(funcType.genericpacks, nil) + assert.eq(#funcType.genericpacks :: luau.Punctuated, 1) + assert.eq((funcType.genericpacks :: luau.Punctuated)[1].node.tag, "generic") + + local genericTypePack = (funcType.genericpacks :: luau.Punctuated)[1].node + assert.eq(genericTypePack.name.text, "U") + assert.eq(genericTypePack.ellipsis.text, "...") + + assert.neq(funcType.closegenerics, nil) + assert.eq((funcType.closegenerics :: luau.Token<">">).text, ">") + assert.neq(funcType.vararg, nil) + assert.eq((funcType.vararg :: luau.AstTypePack).kind, "typepack") + + block = parser.parse("type T = () -> ...number") + typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "function") + + funcType = typeAlias.type :: luau.AstTypeFunction + assert.eq(funcType.returntypes.kind, "typepack") + assert.eq(funcType.returntypes.tag, "variadic") + local variadicType: luau.AstTypePackVariadic = funcType.returntypes :: any + assert.neq(variadicType.ellipsis, nil) + assert.eq((variadicType.ellipsis :: luau.Token<"...">).text, "...") + assert.eq(variadicType.type.tag, "reference") + end) +end) + test.run() From d258dc94db9e5d856a8478a2e0575acab0524614 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 3 Nov 2025 16:46:20 -0800 Subject: [PATCH 113/642] stdlib: lute test shouldn't point to xpcall for error msg (#518) While working on tests for something else, I noticed that in cases where a test case fails for reasons other than an assertion, the testing harness reports the source of the error as an `xpcall` call in the lute's test implementation. After discussing with @~Vighnesh-V, I've implemented a small workaround where we look lower down in the stack for the source of the error, so that we can actually point to the test case causing the failure in failure reporting. --- lute/std/libs/test.luau | 24 +++++++++----- tests/cli/test.test.luau | 67 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index a91c90615..e5a28966c 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -57,8 +57,8 @@ end -- Test Reporting type FailedTest = { test: string, -- name of the test case that failed - assertion: string, -- name of the assertion that failed (e.g., "assert.eq") - error: string, -- the error message from the assertion + assertion: string?, -- name of the assertion that failed (e.g., "assert.eq") + error: string?, -- the error message from the assertion filename: string, -- source file where the failure occurred linenumber: number, -- line number of the failure } @@ -73,7 +73,9 @@ type TestRunResult = { local function printFailedTest(failed: FailedTest) print(`❌ {failed.test}`) print(` {failed.filename}:{failed.linenumber}`) - print(` {failed.assertion}: {failed.error}`) + if failed.assertion ~= nil and failed.error ~= nil then + print(` {failed.assertion}: {failed.error}`) + end print() end @@ -213,10 +215,14 @@ function test.run() local fileName, lineNumber = debug.info(4, "sl") local assertName = debug.info(3, "n") + if assertName == "xpcall" then + fileName, lineNumber = debug.info(2, "sl") + end + local failedtest: FailedTest = { test = tc.name, - assertion = assertName, - error = err.msg, + assertion = if assertName == "xpcall" then nil else assertName, + error = if assertName == "xpcall" then nil else err.msg, filename = fileName, linenumber = lineNumber, } @@ -248,10 +254,14 @@ function test.run() local fileName, lineNumber = debug.info(4, "sl") local assertName = debug.info(3, "n") + if assertName == "xpcall" then + fileName, lineNumber = debug.info(2, "sl") + end + local failedtest: FailedTest = { test = testFullName, - assertion = assertName, - error = err.msg, + assertion = if assertName == "xpcall" then nil else assertName, + error = if assertName == "xpcall" then nil else err.msg, filename = fileName, linenumber = lineNumber, } diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index df6777b68..7c94eb183 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -1,8 +1,11 @@ +local fs = require("@std/fs") local path = require("@std/path") local process = require("@std/process") +local system = require("@std/system") local test = require("@std/test") local lutePath = path.format(process.execpath()) +local tmpDir = system.tmpdir() test.suite("lute test", function(suite) suite:case("lute test help message", function(assert) @@ -30,6 +33,70 @@ test.suite("lute test", function(suite) assert.eq(result.exitcode, 0) assert.neq(result.stdout:find("Found 2 test files", 1, true), nil) end) + + suite:case("lute doesn't report xpcall as error when accessing field of nil in suite", function(assert) + -- Setup + local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") + local handle = fs.open(testFilePath, "w+") + fs.write( + handle, + [[ +local test = require("@std/test") + +test.suite("nil_field_suite", function(suite) + suite:case("access_field_of_nil", function(assert) + local t = nil + local value = t.field -- This will cause an error + end) +end) + +test.run() + ]] + ) + fs.close(handle) + + -- Run lute on the created test file + local result = process.run({ lutePath, tostring(testFilePath) }) + + -- Check that the output doesn't include xpcall + assert.eq(result.exitcode, 1) + assert.eq(result.stdout:find("xpcall", 1, true), nil) + -- Check that the error points to filepath + correct line number + assert.neq(result.stdout:find(`{tostring(testFilePath)}:6`, 1, true), nil) + + fs.remove(testFilePath) + end) + + suite:case("lute doesn't report xpcall as error when accessing field of nil in case", function(assert) + -- Setup + local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") + local handle = fs.open(testFilePath, "w+") + fs.write( + handle, + [[ +local test = require("@std/test") + +test.case("access_field_of_nil", function(assert) + local t = nil + local value = t.field -- This will cause an error +end) + +test.run() + ]] + ) + fs.close(handle) + + -- Run lute on the created test file + local result = process.run({ lutePath, tostring(testFilePath) }) + + -- Check that the output doesn't include xpcall + assert.eq(result.exitcode, 1) + assert.eq(result.stdout:find("xpcall", 1, true), nil) + -- Check that the error points to filepath + correct line number + assert.neq(result.stdout:find(`{tostring(testFilePath)}:5`, 1, true), nil) + + fs.remove(testFilePath) + end) end) test.run() From 8d504a07fe7bf287d7d680046ea46fd4a61c26a9 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:36:27 -0800 Subject: [PATCH 114/642] refactor: stdlib/syntax file formats (#520) Splitting up https://github.com/luau-lang/lute/pull/516 to make it easier to review This one is refactoring modules in `std/libs/syntax` and its use cases --- examples/transformer.luau | 4 ++-- lute/std/libs/syntax/parser.luau | 16 +++++++-------- lute/std/libs/syntax/printer.luau | 33 +++++++++++++++---------------- lute/std/libs/syntax/query.luau | 17 +++++++++------- lute/std/libs/syntax/visitor.luau | 24 ++++++++++++---------- 5 files changed, 50 insertions(+), 44 deletions(-) diff --git a/examples/transformer.luau b/examples/transformer.luau index 3f33906e6..f491df0b0 100644 --- a/examples/transformer.luau +++ b/examples/transformer.luau @@ -6,7 +6,7 @@ local luau = require("@lute/luau") function transformation(ctx) local parsedResult = parser.parsefile(ctx.source) - local v = visitor.createVisitor() + local v = visitor.create() v.visitBinary = function(node: luau.AstExprBinary) if node.operator.text ~= "~=" then @@ -80,7 +80,7 @@ function transformation(ctx) return false end - visitor.visitBlock(parsedResult.root, v) + visitor.visitblock(parsedResult.root, v) return printer.printfile(parsedResult) end diff --git a/lute/std/libs/syntax/parser.luau b/lute/std/libs/syntax/parser.luau index 3ff7035d4..bd33268ff 100644 --- a/lute/std/libs/syntax/parser.luau +++ b/lute/std/libs/syntax/parser.luau @@ -1,13 +1,17 @@ --!strict +-- @std/syntax/parser +-- Parser for Luau source code into Luau AST nodes local luau = require("@std/luau") +local parser = {} + --- Parses Luau source code into an AstStatBlock -local function parse(source: string): luau.AstStatBlock +function parser.parse(source: string): luau.AstStatBlock return luau.parse(source).root end -local function parseexpr(source: string): luau.AstExpr +function parser.parseexpr(source: string): luau.AstExpr return luau.parseexpr(source) end @@ -16,13 +20,9 @@ export type ParseResult = { eof: luau.Eof, } -local function parsefile(source: string): ParseResult +function parser.parsefile(source: string): ParseResult local result = luau.parse(source) return { root = result.root, eof = result.eof } end -return { - parse = parse, - parseexpr = parseexpr, - parsefile = parsefile, -} +return table.freeze(parser) diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 31903ac2e..56a00eb69 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -1,7 +1,11 @@ --!strict +-- @std/syntax/printer + local luau = require("@std/luau") local visitor = require("./visitor") +local printer = {} + local function exhaustiveMatch(value: never): never error(`Unknown value in exhaustive match: {value}`) end @@ -64,7 +68,7 @@ local function printInterpolatedString(expr: luau.AstExprInterpString): string else result ..= "{" result ..= printTriviaList(expr.strings[i].trailingtrivia) - result ..= printExpr(expr.expressions[i]) + result ..= printer.printexpr(expr.expressions[i]) end end @@ -77,7 +81,7 @@ type PrintVisitor = visitor.Visitor & { } local function printVisitor() - local printer = visitor.createVisitor() :: PrintVisitor + local printer = visitor.create() :: PrintVisitor printer.result = buffer.create(1024) printer.cursor = 0 @@ -130,30 +134,25 @@ function printNode(node: T, visit: (node: T, visitor: visitor.Visitor) -> ()) end --- Returns a string representation of an AstExpr -function printExpr(expr: luau.AstExpr): string - return printNode(expr, visitor.visitExpression) +function printer.printexpr(expr: luau.AstExpr): string + return printNode(expr, visitor.visitexpression) end --- Returns a string representation of an AstStat -local function printStatement(statement: luau.AstStat): string - return printNode(statement, visitor.visitStatement) +function printer.printstatement(statement: luau.AstStat): string + return printNode(statement, visitor.visitstatement) end -- Returns a string representation of an AstType -function printType(type: luau.AstType): string - return printNode(type, visitor.visitType) +function printer.printtype(type: luau.AstType): string + return printNode(type, visitor.visittype) end -function printFile(result: { root: luau.AstStatBlock, eof: luau.Eof }): string +function printer.printfile(result: { root: luau.AstStatBlock, eof: luau.Eof }): string local printer = printVisitor() - visitor.visitBlock(result.root, printer) - visitor.visitToken(result.eof, printer) + visitor.visitblock(result.root, printer) + visitor.visittoken(result.eof, printer) return buffer.readstring(printer.result, 0, printer.cursor) end -return { - printexpr = printExpr, - printstatement = printStatement, - printtype = printType, - printfile = printFile, -} +return table.freeze(printer) diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 0dab2f423..f0f3bd29f 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -1,12 +1,17 @@ -local luau = require("@lute/luau") +--!strict +-- @std/syntax/query +-- Provides utility functions for querying Luau AST nodes +local luau = require("@std/luau") local visitor = require("./visitor") +local query = {} + --- Selects a list of expressions from the provided block that match the provided predicate. --- Useful for defining declarative-based programs, where the matched nodes are mutated for the migration -local function selectif(block: luau.AstStatBlock, predicate: (luau.AstExpr) -> T?): { T } +function query.selectif(block: luau.AstStatBlock, predicate: (luau.AstExpr) -> T?): { T } local nodes = {} - local finder = visitor.createVisitor() + local finder = visitor.create() finder.visitExpression = function(node) local result = predicate(node) @@ -17,11 +22,9 @@ local function selectif(block: luau.AstStatBlock, predicate: (luau.AstExpr) - return true end - visitor.visitBlock(block, finder) + visitor.visitblock(block, finder) return nodes end -return { - selectif = selectif, -} +return table.freeze(query) diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 87b270eb1..df279ccfc 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -1,7 +1,11 @@ --!strict +-- @std/syntax/visitor +-- Visitor pattern implementation for traversing Luau AST nodes local luau = require("@std/luau") +local visitorlib = {} + export type Visitor = { visitBlock: (luau.AstStatBlock) -> boolean, visitBlockEnd: (luau.AstStatBlock) -> (), @@ -873,16 +877,16 @@ function visitTypePack(type: luau.AstTypePack, visitor: Visitor) end end -local function createVisitor() +local function create() return table.clone(defaultVisitor) end -return { - createVisitor = createVisitor, - visitBlock = visitBlock, - visitStatement = visitStatement, - visitExpression = visitExpression, - visitType = visitType, - visitTypePack = visitTypePack, - visitToken = visitToken, -} +visitorlib.create = create +visitorlib.visitblock = visitBlock +visitorlib.visitstatement = visitStatement +visitorlib.visitexpression = visitExpression +visitorlib.visittype = visitType +visitorlib.visittypepack = visitTypePack +visitorlib.visittoken = visitToken + +return table.freeze(visitorlib) From b736b0541d6cea15275dc25b68dd4170345bf670 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:36:38 -0800 Subject: [PATCH 115/642] refactor: stdlib fs, io, process, and system file formats (#521) Splitting up https://github.com/luau-lang/lute/pull/516 to make it easier to review this one is for `fs`, `io`, `process`, and `system` in `std/libs` so the docs generator can interpret the format to produce documentation --- lute/std/libs/fs.luau | 62 +++++++------------ lute/std/libs/io.luau | 8 +-- lute/std/libs/path/init.luau | 4 +- lute/std/libs/process.luau | 24 ++++--- lute/std/libs/system/init.luau | 39 ++++++------ .../{platforminfo.luau => platform.luau} | 0 6 files changed, 59 insertions(+), 78 deletions(-) rename lute/std/libs/system/{platforminfo.luau => platform.luau} (100%) diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 0eb4ec078..65df572a2 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -6,6 +6,8 @@ local fs = require("@lute/fs") local pathlib = require("@std/path") +local fslib = {} + export type handlemode = fs.HandleMode export type filehandle = fs.FileHandle export type filetype = fs.FileType @@ -19,76 +21,75 @@ export type createdirectoryoptions = { makeparents: boolean?, } -local function open(path: pathlike, mode: handlemode?): filehandle +function fslib.open(path: pathlike, mode: handlemode?): filehandle return fs.open(pathlib.format(path), mode) end -local function read(handle: filehandle): string +function fslib.read(handle: filehandle): string return fs.read(handle) end -local function write(handle: filehandle, contents: string): () +function fslib.write(handle: filehandle, contents: string): () return fs.write(handle, contents) end -local function close(handle: filehandle): () +function fslib.close(handle: filehandle): () return fs.close(handle) end -local function remove(path: pathlike): () +function fslib.remove(path: pathlike): () return fs.remove(pathlib.format(path)) end -local function stat(path: pathlike): filemetadata +function fslib.stat(path: pathlike): filemetadata return fs.stat(pathlib.format(path)) end -local function type(path: pathlike): filetype +function fslib.type(path: pathlike): filetype return fs.type(pathlib.format(path)) end -local function link(src: pathlike, dest: pathlike): () +function fslib.link(src: pathlike, dest: pathlike): () return fs.link(pathlib.format(src), pathlib.format(dest)) end -local function symlink(src: pathlike, dest: pathlike): () - -- make sure our path is absolute path for symlink and if your os is +function fslib.symlink(src: pathlike, dest: pathlike): () return fs.symlink(pathlib.format(src), pathlib.format(dest)) end -local function watch(path: pathlike, callback: (filename: pathlike, event: watchevent) -> ()): watchhandle +function fslib.watch(path: pathlike, callback: (filename: pathlike, event: watchevent) -> ()): watchhandle return fs.watch(pathlib.format(path), callback) end -local function exists(path: pathlike): boolean +function fslib.exists(path: pathlike): boolean return fs.exists(pathlib.format(path)) end -local function copy(src: pathlike, dest: pathlike): () +function fslib.copy(src: pathlike, dest: pathlike): () return fs.copy(pathlib.format(src), pathlib.format(dest)) end -local function listdir(path: pathlike): { directoryentry } +function fslib.listdir(path: pathlike): { directoryentry } return fs.listdir(pathlib.format(path)) end -local function rmdir(path: pathlike): () +function fslib.rmdir(path: pathlike): () return fs.rmdir(pathlib.format(path)) end -local function readfiletostring(filepath: pathlike): string +function fslib.readfiletostring(filepath: pathlike): string return fs.readfiletostring(pathlib.format(filepath)) end -local function writestringtofile(filepath: pathlike, contents: string): () +function fslib.writestringtofile(filepath: pathlike, contents: string): () return fs.writestringtofile(pathlib.format(filepath), contents) end -local function readasync(filepath: pathlike): string +function fslib.readasync(filepath: pathlike): string return fs.readasync(pathlib.format(filepath)) end -local function createdirectory(path: pathlike, options: createdirectoryoptions?): () +function fslib.createdirectory(path: pathlike, options: createdirectoryoptions?): () if options and options.makeparents then local parts: { string } = pathlib.parse(path).parts @@ -99,7 +100,7 @@ local function createdirectory(path: pathlike, options: createdirectoryoptions?) for _, part in parts do subdir = pathlib.join(subdir, part) - if not exists(subdir) then + if not fslib.exists(subdir) then fs.mkdir(pathlib.format(subdir)) end end @@ -108,23 +109,4 @@ local function createdirectory(path: pathlike, options: createdirectoryoptions?) end end -return table.freeze({ - open = open, - read = read, - write = write, - close = close, - remove = remove, - stat = stat, - type = type, - link = link, - symlink = symlink, - watch = watch, - exists = exists, - copy = copy, - listdir = listdir, - rmdir = rmdir, - readfiletostring = readfiletostring, - writestringtofile = writestringtofile, - readasync = readasync, - createdirectory = createdirectory, -}) +return table.freeze(fslib) diff --git a/lute/std/libs/io.luau b/lute/std/libs/io.luau index 3cb0d63d2..fa61cfeeb 100644 --- a/lute/std/libs/io.luau +++ b/lute/std/libs/io.luau @@ -5,8 +5,10 @@ local io = require("@lute/io") +local iolib = {} + -- User input can be provided via command line stdin, piped input, or redirection from a file. See `examples/user_input.luau`. -function input(prompt: string?): string +function iolib.input(prompt: string?): string if prompt then -- We temporarily use `print` for prompt until we have proper stdout support, so there is a `\n` between prompt and input. print(prompt) @@ -14,6 +16,4 @@ function input(prompt: string?): string return io.read() end -return table.freeze({ - input = input, -}) +return table.freeze(iolib) diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau index a4e1704dd..7e53b3462 100644 --- a/lute/std/libs/path/init.luau +++ b/lute/std/libs/path/init.luau @@ -1,4 +1,4 @@ -local platforminfo = require("@std/system/platforminfo") +local platform = require("@std/system/platform") local posix = require("@self/posix") local win32 = require("@self/win32") @@ -10,7 +10,7 @@ local pathlib = {} export type path = pathtypes.path export type pathlike = pathtypes.pathlike -local onWindows = platforminfo.win32 +local onWindows = platform.win32 function pathlib.basename(path: pathlike): string? if onWindows then diff --git a/lute/std/libs/process.luau b/lute/std/libs/process.luau index 7188c9169..671063a10 100644 --- a/lute/std/libs/process.luau +++ b/lute/std/libs/process.luau @@ -6,37 +6,35 @@ local process = require("@lute/process") local pathlib = require("@std/path") +local processlib = {} + export type stdiokind = process.StdioKind export type processrunoptions = process.ProcessRunOptions export type processresult = process.ProcessResult export type path = pathlib.path export type pathlike = pathlib.pathlike -local function homedir(): path +function processlib.homedir(): path return pathlib.parse(process.homedir()) end -local function cwd(): path +function processlib.cwd(): path return pathlib.parse(process.cwd()) end -local function run(args: string | { string }, options: processrunoptions?): processresult +function processlib.run(args: string | { string }, options: processrunoptions?): processresult return process.run(args, options) end -local function exit(exitcode: number): never +function processlib.exit(exitcode: number): never return process.exit(exitcode) end -local function execpath(): path +function processlib.execpath(): path return pathlib.parse(process.execpath()) end -return table.freeze({ - env = process.env, - homedir = homedir, - cwd = cwd, - run = run, - exit = exit, - execpath = execpath, -}) +-- re-exports +processlib.env = process.env + +return table.freeze(processlib) diff --git a/lute/std/libs/system/init.luau b/lute/std/libs/system/init.luau index a815a74e0..ae9e210c0 100644 --- a/lute/std/libs/system/init.luau +++ b/lute/std/libs/system/init.luau @@ -5,28 +5,29 @@ local system = require("@lute/system") local path = require("@std/path") -local platforminfo = require("@self/platforminfo") +local platform = require("@self/platform") -local function tmpdir(): path.path +local systemlib = {} + +function systemlib.tmpdir(): path.path return path.parse(system.tmpdir()) end -return table.freeze({ - os = system.os, - arch = system.arch, - tmpdir = tmpdir, +-- re-exports +systemlib.os = system.os +systemlib.arch = system.arch + +systemlib.threadcount = system.threadcount +systemlib.hostname = system.hostname +systemlib.totalmemory = system.totalmemory +systemlib.freememory = system.freememory +systemlib.uptime = system.uptime +systemlib.cpus = system.cpus - -- function re-exports - threadcount = system.threadcount, - hostname = system.hostname, - totalmemory = system.totalmemory, - freememory = system.freememory, - uptime = system.uptime, - cpus = system.cpus, +-- boolean properties +systemlib.win32 = platform.win32 +systemlib.linux = platform.linux +systemlib.macos = platform.macos +systemlib.unix = platform.unix - -- boolean properties - win32 = platforminfo.win32, - linux = platforminfo.linux, - macos = platforminfo.macos, - unix = platforminfo.unix, -}) +return table.freeze(systemlib) diff --git a/lute/std/libs/system/platforminfo.luau b/lute/std/libs/system/platform.luau similarity index 100% rename from lute/std/libs/system/platforminfo.luau rename to lute/std/libs/system/platform.luau From 5dd84e413c9642b9f3f5b9adb9ca96020ab9b638 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:38:50 -0800 Subject: [PATCH 116/642] refactor: misc. stdlib restyling for time, header comments for assert, json, luau, test, and examples (#524) Splitting up https://github.com/luau-lang/lute/pull/516 to make it easier to review this one has styling changes in std/time so the docs generator can interpret the format to produce documentation, header comments for assert, json, luau, and test, and updating example/usage files --- examples/system_lib.luau | 4 ++-- lute/std/libs/assert.luau | 4 ++++ lute/std/libs/json.luau | 4 ++++ lute/std/libs/luau.luau | 5 +++++ lute/std/libs/test.luau | 3 +++ lute/std/libs/time.luau | 12 +++++++++--- tests/cli/loadbypath.test.luau | 8 ++++---- 7 files changed, 31 insertions(+), 9 deletions(-) diff --git a/examples/system_lib.luau b/examples/system_lib.luau index 83296ee79..96957006b 100644 --- a/examples/system_lib.luau +++ b/examples/system_lib.luau @@ -1,4 +1,4 @@ -local system = require("@lute/system") +local system = require("@std/system") print( string.format( @@ -7,7 +7,7 @@ print( system.os, system.arch, system.threadcount(), - system.uptime(), + tostring(system.uptime()), system.freememory(), system.totalmemory() ) diff --git a/lute/std/libs/assert.luau b/lute/std/libs/assert.luau index 684df4807..a0052afb1 100644 --- a/lute/std/libs/assert.luau +++ b/lute/std/libs/assert.luau @@ -1,3 +1,7 @@ +--!strict +-- @std/assert +-- Standard assertion library for Luau + local assertions = {} function assertions.eq(lhs: T, rhs: T) diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 414d64ddd..7396a1762 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -1,3 +1,7 @@ +--!strict +-- @std/json +-- JSON serialization and deserialization library + local json = { --- Not actually a nil value, a newproxy stand-in for a null value since Luau has no actual representation of `null` null = newproxy() :: nil, diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 2d895a5ba..42b513b43 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -1,3 +1,8 @@ +--!strict +-- @std/luau +-- stdlib for `@lute/luau` +-- Provides utilities for parsing and compiling Luau source code + local luteLuau = require("@lute/luau") local fs = require("@lute/fs") diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index e5a28966c..28fc61a90 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -1,4 +1,7 @@ --!strict +-- @std/test +-- A testing framework for Luau + local ps = require("@std/process") local assert = require("@std/assert") -- Test Types diff --git a/lute/std/libs/time.luau b/lute/std/libs/time.luau index ee2e63b99..17fd0acc8 100644 --- a/lute/std/libs/time.luau +++ b/lute/std/libs/time.luau @@ -1,3 +1,7 @@ +--!strict +-- @std/time +-- Provides utility functions for time durations + local NANOS_PER_SEC = 1_000_000_000 local NANOS_PER_MILLI = 1_000_000 local NANOS_PER_MICRO = 1_000 @@ -13,6 +17,8 @@ type durationdata = { _nanoseconds: number, } +local time = {} + local duration = {} duration.__index = duration @@ -164,6 +170,6 @@ function duration.__tostring(self: duration): string return string.format("%d.%09d", self._seconds, self._nanoseconds) end -return table.freeze({ - duration = duration, -}) +time.duration = duration + +return table.freeze(time) diff --git a/tests/cli/loadbypath.test.luau b/tests/cli/loadbypath.test.luau index 0c3c91369..b5c9601f8 100644 --- a/tests/cli/loadbypath.test.luau +++ b/tests/cli/loadbypath.test.luau @@ -1,5 +1,5 @@ -local fs = require("@lute/fs") -local process = require("@lute/process") +local fs = require("@std/fs") +local process = require("@std/process") local path = require("@std/path") local system = require("@std/system") local test = require("@std/test") @@ -24,7 +24,7 @@ local testDirStr = path.format(testDir) test.suite("Lute CLI Run", function(suite) suite:beforeall(function() if not fs.exists(testDirStr) then - fs.mkdir(testDirStr) + fs.createdirectory(testDirStr) end end) @@ -41,7 +41,7 @@ test.suite("Lute CLI Run", function(suite) fs.write(requireeFile, REQUIREE_CONTENTS) fs.close(requireeFile) - local result = process.run({ lutePath, requirerPath, requireePath }) + local result = process.run({ tostring(lutePath), requirerPath, requireePath }) check.eq(result.exitcode, 0) check.eq(result.stdout, "Success\n") From 9e301f6def807bd41b95776acc14308b582b4083 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 5 Nov 2025 10:34:39 -0800 Subject: [PATCH 117/642] stdlib: tweak error msg in tests with runtime errors (#525) Follow on work to #518 to tweak the error message for tests that fail with runtime errors --- lute/std/libs/test.luau | 25 ++++++++++++++----------- tests/cli/test.test.luau | 5 +++++ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau index 28fc61a90..43d6256ca 100644 --- a/lute/std/libs/test.luau +++ b/lute/std/libs/test.luau @@ -60,8 +60,8 @@ end -- Test Reporting type FailedTest = { test: string, -- name of the test case that failed - assertion: string?, -- name of the assertion that failed (e.g., "assert.eq") - error: string?, -- the error message from the assertion + assertion: string?, -- if an assertion failed, name of the assertion that failed (e.g., "assert.eq") + message: string?, -- the error message to report with a failed assertion filename: string, -- source file where the failure occurred linenumber: number, -- line number of the failure } @@ -75,9 +75,12 @@ type TestRunResult = { local function printFailedTest(failed: FailedTest) print(`❌ {failed.test}`) - print(` {failed.filename}:{failed.linenumber}`) - if failed.assertion ~= nil and failed.error ~= nil then - print(` {failed.assertion}: {failed.error}`) + if failed.assertion ~= nil then + print(` {failed.filename}:{failed.linenumber}`) + print(` {failed.assertion}: {failed.message}`) + else + print(` Failed with: Runtime error in {failed.test} in:`) + print(` {failed.filename}:{failed.linenumber}`) end print() end @@ -154,7 +157,7 @@ function test.run() local failedtest: FailedTest = { test = `{suite.name}.{tc.name}`, assertion = "beforeall", - error = `beforeall hook failed: {err}`, + message = `beforeall hook failed: {err}`, filename = "", linenumber = 0, } @@ -171,7 +174,7 @@ function test.run() local failedtest: FailedTest = { test = testFullName, assertion = "beforeeach", - error = `beforeeach hook failed: {err}`, + message = `beforeeach hook failed: {err}`, filename = "", linenumber = 0, } @@ -186,7 +189,7 @@ function test.run() local failedtest: FailedTest = { test = testFullName, assertion = "aftereach", - error = `aftereach hook failed: {err}`, + message = `aftereach hook failed: {err}`, filename = "", linenumber = 0, } @@ -202,7 +205,7 @@ function test.run() local failedtest: FailedTest = { test = suiteName, assertion = "afterall", - error = `afterall hook failed: {err}`, + message = `afterall hook failed: {err}`, filename = "", linenumber = 0, } @@ -225,7 +228,7 @@ function test.run() local failedtest: FailedTest = { test = tc.name, assertion = if assertName == "xpcall" then nil else assertName, - error = if assertName == "xpcall" then nil else err.msg, + message = if assertName == "xpcall" then nil else err.msg, filename = fileName, linenumber = lineNumber, } @@ -264,7 +267,7 @@ function test.run() local failedtest: FailedTest = { test = testFullName, assertion = if assertName == "xpcall" then nil else assertName, - error = if assertName == "xpcall" then nil else err.msg, + message = if assertName == "xpcall" then nil else err.msg, filename = fileName, linenumber = lineNumber, } diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 7c94eb183..5bbcde543 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -62,6 +62,10 @@ test.run() assert.eq(result.exitcode, 1) assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number + assert.neq( + result.stdout:find(`Failed with: Runtime error in nil_field_suite.access_field_of_nil in:`, 1, true), + nil + ) assert.neq(result.stdout:find(`{tostring(testFilePath)}:6`, 1, true), nil) fs.remove(testFilePath) @@ -93,6 +97,7 @@ test.run() assert.eq(result.exitcode, 1) assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number + assert.neq(result.stdout:find(`Failed with: Runtime error in access_field_of_nil in:`, 1, true), nil) assert.neq(result.stdout:find(`{tostring(testFilePath)}:5`, 1, true), nil) fs.remove(testFilePath) From f4a1ad2ba77bb80a2258216dcbcb5c1770ebc864 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 5 Nov 2025 11:51:16 -0800 Subject: [PATCH 118/642] feature: lute compile supports multiple files and static require tracing. (#515) This PR extends the `lute compile` command, which takes a file and: - Figures out all the transitive requires for that file - Creates a bundle of precompiled bytecode - Encodes the bundle into a payload - Produces a runnable executable with the `lute` runtime embedded, along with the bundle appended to the end of the binary This PR explicitly doesn't handle making this work with `.luaurc` or `config.luau` files - this will come in a subsequent PR. --- lute/cli/include/lute/compile.h | 11 -- lute/cli/include/lute/staticrequires.h | 3 +- lute/cli/src/climain.cpp | 248 ++++++++++++++++++++----- lute/cli/src/compile.cpp | 139 -------------- lute/cli/src/staticrequires.cpp | 28 ++- tests/src/compile.test.cpp | 49 ++++- tests/src/staticrequires/main.luau | 8 +- 7 files changed, 281 insertions(+), 205 deletions(-) diff --git a/lute/cli/include/lute/compile.h b/lute/cli/include/lute/compile.h index 4cdaef178..c257d999c 100644 --- a/lute/cli/include/lute/compile.h +++ b/lute/cli/include/lute/compile.h @@ -5,16 +5,6 @@ #include -struct AppendedBytecodeResult -{ - bool found = false; - std::string BytecodeData; -}; - -AppendedBytecodeResult checkForAppendedBytecode(); - -int compileScript(const std::string& inputFilePath, const std::string& outputFilePath); - struct LuteDecodeResult; struct LuteEncodeResult @@ -90,4 +80,3 @@ struct LuteExecutable std::string executablePath; }; - diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h index f43066909..2b93093a0 100644 --- a/lute/cli/include/lute/staticrequires.h +++ b/lute/cli/include/lute/staticrequires.h @@ -9,7 +9,7 @@ class StaticRequireTracer { public: - StaticRequireTracer(); + StaticRequireTracer() = default; // Trace dependencies starting from an entry point file // rootDirectory: Base directory for resolving all requires @@ -24,6 +24,7 @@ class StaticRequireTracer return requireGraph; } + void printRequireGraph() const; private: Luau::DenseHashSet visited{""}; std::vector discovered; diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index eb7cec10f..eae730a74 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -3,12 +3,14 @@ #include "Luau/Common.h" #include "Luau/CodeGen.h" #include "Luau/Compiler.h" +#include "Luau/DenseHash.h" #include "Luau/FileUtils.h" #include "Luau/Require.h" #include "lua.h" #include "lualib.h" +#include "lute/bundlevfs.h" #include "lute/clicommands.h" #include "lute/clivfs.h" #include "lute/compile.h" @@ -23,6 +25,7 @@ #include "lute/ref.h" #include "lute/require.h" #include "lute/runtime.h" +#include "lute/staticrequires.h" #include "lute/system.h" #include "lute/task.h" #include "lute/tc.h" @@ -90,6 +93,31 @@ static void luteopen_libs(lua_State* L) } } +void* createBundleRequireContext(lua_State* L, Luau::DenseHashMap bundleMap) +{ + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + std::destroy_at(static_cast(ptr)); + } + ); + + if (!ctx) + luaL_error(L, "unable to allocate RequireCtx"); + + ctx = new (ctx) RequireCtx{BundleVfs{std::move(bundleMap)}}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + return ctx; +} + lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit) { return setupState( @@ -108,6 +136,21 @@ lua_State* setupCliState(Runtime& runtime, std::function preSa ); } +lua_State* setupBundleState(Runtime& runtime, Luau::DenseHashMap bundleMap) +{ + return setupState( + runtime, + [bundleMap = std::move(bundleMap)](lua_State* L) + { + luteopen_libs(L); + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createBundleRequireContext(L, std::move(bundleMap))); + } + ); +} + static bool setupArguments(lua_State* L, int argc, char** argv) { if (!lua_checkstack(L, argc)) @@ -188,7 +231,7 @@ static void displayHelp() printf("Commands:\n"); printf(" run (default) Run a Luau script.\n"); printf(" check Type check Luau files.\n"); - printf(" compile Compile a Luau script into the executable.\n"); + printf(" compile Compile a Luau script into a standalone executable.\n"); printf(" setup Generate type definition files for the language server."); printf("\n"); printf("Run Options (when using 'run' or no command):\n"); @@ -200,8 +243,8 @@ static void displayHelp() printf(" Performs a type check on the specified files.\n"); printf("\n"); printf("Compile Options:\n"); - printf(" lute compile [output_executable]\n"); - printf(" Compiles the script, embedding it into a new executable.\n"); + printf(" lute compile [--output ]\n"); + printf(" Compiles entry point and auto-discovered dependencies into a standalone executable.\n"); printf("\n"); printf("Setup Options:\n"); printf(" lute setup"); @@ -236,12 +279,15 @@ static void displayCheckHelp() static void displayCompileHelp() { - printf("Usage: lute compile [output_executable]\n"); + printf("Usage: lute compile [options]\n"); printf("\n"); printf("Compile Options:\n"); - printf(" output_executable Optional name for the compiled executable.\n"); - printf(" Defaults to '_compiled'.\n"); - printf(" -h, --help Display this usage message.\n"); + printf("\t--output Name for the compiled executable.\n"); + printf("\t\tDefaults to entry file's base name (with .exe on Windows).\n"); + printf("\t--bundle-stats Display bundle size and compression statistics.\n"); + printf("\t--show-require-graph Print the require dependency graph.\n"); + printf("\t-h, --help Display this usage message.\n"); + printf("\n"); } static int assertionHandler(const char* expr, const char* file, int line, const char* function) @@ -388,9 +434,12 @@ int handleCheckCommand(int argc, char** argv, int argOffset) int handleCompileCommand(int argc, char** argv, int argOffset) { - std::string inputFilePath; - std::string outputFilePath; + std::string filePath; + std::string outputPath; + bool bundleStats = false; + bool showRequireGraph = false; + // Parse arguments for (int i = argOffset; i < argc; ++i) { const char* currentArg = argv[i]; @@ -400,56 +449,162 @@ int handleCompileCommand(int argc, char** argv, int argOffset) displayCompileHelp(); return 0; } - else if (inputFilePath.empty()) + else if (strcmp(currentArg, "--output") == 0) + { + if (i + 1 >= argc) + { + fprintf(stderr, "Error: --output requires a path argument.\n\n"); + displayCompileHelp(); + return 1; + } + outputPath = argv[++i]; + } + else if (strcmp(currentArg, "--bundle-stats") == 0) + bundleStats = true; + else if (strcmp(currentArg, "--show-require-graph") == 0) + showRequireGraph = true; + else if (currentArg[0] == '-') { - inputFilePath = currentArg; + fprintf(stderr, "Error: Unrecognized option '%s' for 'compile' command.\n\n", currentArg); + displayCompileHelp(); + return 1; } - else if (outputFilePath.empty()) + else if (filePath.empty()) { - outputFilePath = currentArg; + filePath = currentArg; } else { - fprintf(stderr, "Error: Too many arguments for 'compile' command.\n\n"); + fprintf(stderr, "Error: Unexpected argument '%s'.\n\n", currentArg); displayCompileHelp(); return 1; } } - if (inputFilePath.empty()) + if (filePath.empty()) { - fprintf(stderr, "Error: No input file specified for 'compile' command.\n\n"); + fprintf(stderr, "Error: No file specified for 'compile' command.\n\n"); displayCompileHelp(); return 1; } - if (outputFilePath.empty()) + // Validate file exists + std::optional validPath = getValidPath(filePath); + if (!validPath) { - std::string inputBase = inputFilePath; - size_t lastSlash = inputBase.find_last_of("/"); + fprintf(stderr, "Error: File '%s' does not exist.\n", filePath.c_str()); + return 1; + } + + // Set default output path if not specified + if (outputPath.empty()) + { + // Extract base name from input file (remove directory and extension) + std::string baseName = *validPath; + + // Remove directory path + size_t lastSlash = baseName.find_last_of("/\\"); if (lastSlash != std::string::npos) - { - inputBase = inputBase.substr(lastSlash + 1); - } -#ifdef _WIN32 - size_t lastBackslash = inputBase.find_last_of("\\"); - if (lastBackslash != std::string::npos) - { - inputBase = inputBase.substr(lastBackslash + 1); - } -#endif - size_t lastDot = inputBase.find_last_of('.'); + baseName = baseName.substr(lastSlash + 1); + + // Remove extension + size_t lastDot = baseName.find_last_of('.'); if (lastDot != std::string::npos) - { - inputBase = inputBase.substr(0, lastDot); - } - outputFilePath = inputBase; + baseName = baseName.substr(0, lastDot); + + outputPath = baseName; #ifdef _WIN32 - outputFilePath += ".exe"; + outputPath += ".exe"; #endif } - return compileScript(inputFilePath, outputFilePath); + // Normalize paths to be relative to working directory + std::string normalizedEntry = normalizePath(*validPath); + + // Split into directory and filename for cleaner trace output + std::string rootDirectory; + std::string entryFilename; + + size_t lastSlash = normalizedEntry.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + rootDirectory = normalizedEntry.substr(0, lastSlash); + entryFilename = normalizedEntry.substr(lastSlash + 1); + } + else + { + rootDirectory = "."; + entryFilename = normalizedEntry; + } + + // Perform static require trace + StaticRequireTracer tracer; + std::vector discoveredFiles = tracer.trace(rootDirectory, entryFilename); + + if (discoveredFiles.empty()) + { + fprintf(stderr, "Error: No files discovered during require trace.\n"); + return 1; + } + + if (showRequireGraph) + tracer.printRequireGraph(); + + // Create payload and add all discovered files + LuteExePayload payload; + for (const auto& file : discoveredFiles) + { + // Construct full path from root directory and relative file path + std::string fullPath = joinPaths(rootDirectory, file); + payload.add(fullPath); + } + + // Encode the payload + printf("Compiling and bundling bytecode...\n"); + std::optional encodeResult = payload.encode(); + if (!encodeResult) + { + fprintf(stderr, "Error: Failed to encode bundle\n"); + return 1; + } + + // Show bundle stats if requested + if (bundleStats) + { + printf("\nBundle Statistics:\n"); + printf(" Files bundled: %zu\n", discoveredFiles.size()); + printf(" Uncompressed size: %zu bytes\n", encodeResult->uncompressedPayloadSizeBytes); + printf(" Compressed size: %zu bytes\n", encodeResult->compressedPayloadSizeBytes); + printf( + " Space saved: %.2f%%\n", + 100.0 * (1.0 - (double)encodeResult->compressedPayloadSizeBytes / (double)encodeResult->uncompressedPayloadSizeBytes) + ); + printf( + " Compression ratio: %.2f:1\n", (double)encodeResult->uncompressedPayloadSizeBytes / (double)encodeResult->compressedPayloadSizeBytes + ); + printf(" Total payload size: %zu bytes\n", encodeResult->bytesWritten); + printf("\n"); + } + + // Get current executable path + std::string errorMsg; + std::optional exePath = process::getExecPath(&errorMsg); + if (!exePath) + { + fprintf(stderr, "Error: Failed to get executable path: %s\n", errorMsg.c_str()); + return 1; + } + + // Create the executable with embedded payload + LuteExecutable executable(*exePath); + if (!executable.create(outputPath, payload)) + { + fprintf(stderr, "Error: Failed to create executable.\n"); + return 1; + } + + printf("Created executable '%s'\n", outputPath.c_str()); + return 0; } int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv) @@ -466,15 +621,24 @@ int cliMain(int argc, char** argv) Luau::assertHandler() = assertionHandler; setLuauFlags(); - AppendedBytecodeResult embedded = checkForAppendedBytecode(); - if (embedded.found) + std::string err = ""; + if (auto exePath = process::getExecPath(&err)) { - Runtime runtime; - lua_State* GL = setupCliState(runtime); + LuteExecutable exe{*exePath}; + if (auto payload = exe.extract()) + { + Runtime runtime; - bool success = runBytecode(runtime, embedded.BytecodeData, "=__EMBEDDED__", GL, argc, argv); + lua_State* GL = setupBundleState(runtime, payload->filePathToBytecode); - return success ? 0 : 1; + std::string entryPoint = payload->entryPointPath; + auto entryModule = payload->filePathToBytecode.find(entryPoint); + if (entryModule != nullptr) + { + bool success = runBytecode(runtime, *entryModule, "@@bundle/" + entryPoint, GL, argc, argv); + return success ? 0 : 1; + } + } } #ifdef _WIN32 diff --git a/lute/cli/src/compile.cpp b/lute/cli/src/compile.cpp index 71f41acaf..173a8a892 100644 --- a/lute/cli/src/compile.cpp +++ b/lute/cli/src/compile.cpp @@ -17,145 +17,6 @@ const char MAGIC_FLAG[] = "LUTEBYTE"; const size_t MAGIC_FLAG_SIZE = sizeof(MAGIC_FLAG) - 1; -const size_t BYTECODE_SIZE_FIELD_SIZE = sizeof(uint64_t); - -AppendedBytecodeResult checkForAppendedBytecode() -{ - AppendedBytecodeResult result; - - std::string executablePath; - { - std::string error; - std::optional execPathOpt = process::getExecPath(&error); - if (!execPathOpt) - { - fprintf(stderr, "Could not get path to executable: %s\n", error.c_str()); - return result; - } - executablePath = *execPathOpt; - } - - std::ifstream exeFile(executablePath, std::ios::binary | std::ios::ate); - if (!exeFile) - { - return result; - } - - std::streampos fileSize = exeFile.tellg(); - if (fileSize < static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE)) - { - exeFile.close(); - return result; - } - - std::vector flagBuffer(MAGIC_FLAG_SIZE); - exeFile.seekg(fileSize - static_cast(MAGIC_FLAG_SIZE)); - exeFile.read(flagBuffer.data(), MAGIC_FLAG_SIZE); - - if (memcmp(flagBuffer.data(), MAGIC_FLAG, MAGIC_FLAG_SIZE) != 0) - { - exeFile.close(); - return result; - } - - uint64_t BytecodeSize; - exeFile.seekg(fileSize - static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE)); - exeFile.read(reinterpret_cast(&BytecodeSize), BYTECODE_SIZE_FIELD_SIZE); - - if (fileSize < static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE + BytecodeSize)) - { - fprintf(stderr, "Warning: Found magic flag but file size inconsistent.\n"); - exeFile.close(); - return result; - } - - result.BytecodeData.resize(BytecodeSize); - exeFile.seekg(fileSize - static_cast(MAGIC_FLAG_SIZE + BYTECODE_SIZE_FIELD_SIZE + BytecodeSize)); - exeFile.read(&result.BytecodeData[0], BytecodeSize); - - exeFile.close(); - result.found = true; - return result; -} - -int compileScript(const std::string& inputFilePath, const std::string& outputFilePath) -{ - std::string currentExecutablePath; - { - std::string error; - std::optional execPathOpt = process::getExecPath(&error); - if (!execPathOpt) - { - fprintf(stderr, "Could not get path to current executable: %s\n", error.c_str()); - return 1; - } - currentExecutablePath = *execPathOpt; - } - - std::optional source = readFile(inputFilePath); - if (!source) - { - fprintf(stderr, "Error opening input file %s\n", inputFilePath.c_str()); - return 1; - } - - std::string bytecode = Luau::compile(*source, copts()); - if (bytecode.empty()) - { - fprintf(stderr, "Error compiling %s to bytecode.\n", inputFilePath.c_str()); - return 1; - } - - std::ifstream exeFile(currentExecutablePath, std::ios::binary | std::ios::ate); - if (!exeFile) - { - fprintf(stderr, "Error opening current executable %s\n", currentExecutablePath.c_str()); - return 1; - } - std::streamsize exeSize = exeFile.tellg(); - exeFile.seekg(0, std::ios::beg); - std::vector exeBuffer(exeSize); - if (!exeFile.read(exeBuffer.data(), exeSize)) - { - fprintf(stderr, "Error reading current executable %s\n", currentExecutablePath.c_str()); - exeFile.close(); - return 1; - } - exeFile.close(); - - std::ofstream outFile(outputFilePath, std::ios::binary | std::ios::trunc); - if (!outFile) - { - fprintf(stderr, "Error creating output file %s\n", outputFilePath.c_str()); - return 1; - } - - outFile.write(exeBuffer.data(), exeSize); - - uint64_t bytecodeSize = bytecode.size(); - outFile.write(bytecode.data(), bytecodeSize); - - outFile.write(reinterpret_cast(&bytecodeSize), BYTECODE_SIZE_FIELD_SIZE); - - outFile.write(MAGIC_FLAG, MAGIC_FLAG_SIZE); - - if (!outFile.good()) - { - fprintf(stderr, "Error writing to output file %s\n", outputFilePath.c_str()); - outFile.close(); - remove(outputFilePath.c_str()); - return 1; - } - - outFile.close(); - - printf("Successfully compiled %s to %s\n", inputFilePath.c_str(), outputFilePath.c_str()); -#ifndef _WIN32 - chmod(outputFilePath.c_str(), S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); -#endif - - return 0; -} void LuteExePayload::add(const std::string& luauFilePath) { diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index a9cfecbdd..e90e086c0 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -8,8 +8,6 @@ #include -StaticRequireTracer::StaticRequireTracer() = default; - // AST visitor to extract require() calls class RequireExtractor : public Luau::AstVisitor { @@ -80,7 +78,12 @@ std::vector StaticRequireTracer::trace(const std::string& rootDirec } else { - fprintf(stderr, "Warning: Could not resolve require('%s') from '%s'\n", req.c_str(), filePath.c_str()); + // Skip warning for built-in libraries (@std and @lute) + bool isBuiltinLibrary = req.rfind("@std/", 0) == 0 || req.rfind("@lute/", 0) == 0; + if (!isBuiltinLibrary) + { + fprintf(stderr, "Warning: Could not resolve require('%s') from '%s'\n", req.c_str(), filePath.c_str()); + } } } @@ -199,3 +202,22 @@ std::optional StaticRequireTracer::resolveRequire(const std::string return result; } + +void StaticRequireTracer::printRequireGraph() const +{ + printf("\nRequire dependency graph:\n"); + for (const auto& [file, deps] : requireGraph) + { + printf("\t%s\n", file.c_str()); + for (const auto& dep : deps) + { + printf("\t\t -> %s\n", dep.c_str()); + } + + if (deps.empty()) + { + printf("\t\t(no dependencies)\n"); + } + } + printf("\n"); +} diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp index 7eb466895..2e24995dd 100644 --- a/tests/src/compile.test.cpp +++ b/tests/src/compile.test.cpp @@ -1,5 +1,6 @@ #include "Luau/FileUtils.h" #include "doctest.h" +#include "lute/climain.h" #include "lute/compile.h" #include "luteprojectroot.h" @@ -59,10 +60,7 @@ TEST_CASE("lutepayload_multiple_files_roundtrip") std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); std::vector testFiles = { - joinPaths(testDir, "main.luau"), - joinPaths(testDir, "utils.luau"), - joinPaths(testDir, "lib/helper.luau"), - joinPaths(testDir, "shared.luau") + joinPaths(testDir, "main.luau"), joinPaths(testDir, "utils.luau"), joinPaths(testDir, "lib/helper.luau"), joinPaths(testDir, "shared.luau") }; // Create payload with multiple files @@ -333,10 +331,7 @@ TEST_CASE("luteexecutable_multiple_files_roundtrip") std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); std::vector testFiles = { - joinPaths(testDir, "main.luau"), - joinPaths(testDir, "utils.luau"), - joinPaths(testDir, "lib/helper.luau"), - joinPaths(testDir, "shared.luau") + joinPaths(testDir, "main.luau"), joinPaths(testDir, "utils.luau"), joinPaths(testDir, "lib/helper.luau"), joinPaths(testDir, "shared.luau") }; // Create a temporary dummy executable @@ -456,3 +451,41 @@ TEST_CASE("luteexecutable_extract_preserves_original_executable") std::remove(dummyExePath.c_str()); std::remove(outputExePath.c_str()); } + +TEST_CASE("compile_command_e2e") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + + // Use a simple test file that prints something we can verify + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); + + // Create a temporary output path for the compiled executable + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_compiled_e2e"); +#ifdef _WIN32 + outputExePath += ".exe"; +#endif + + // Build argv for cliMain: ["lute", "compile", , "--output", ] + char executablePlaceholder[] = "lute"; + char compileCommand[] = "compile"; + char outputFlag[] = "--output"; + + std::vector argv = {executablePlaceholder, compileCommand, testFilePath.data(), outputFlag, outputExePath.data()}; + + // Run the compile command + int compileResult = cliMain(argv.size(), argv.data()); + REQUIRE(compileResult == 0); + + // Verify the output file was created + std::ifstream checkFile(outputExePath, std::ios::binary); + REQUIRE(checkFile.is_open()); + checkFile.close(); + + // Now run the compiled executable to verify it works + std::vector runArgv = {outputExePath.data()}; + int runResult = cliMain(runArgv.size(), runArgv.data()); + CHECK(runResult == 0); + + // Clean up + std::remove(outputExePath.c_str()); +} diff --git a/tests/src/staticrequires/main.luau b/tests/src/staticrequires/main.luau index b17b4fe38..6e9405843 100644 --- a/tests/src/staticrequires/main.luau +++ b/tests/src/staticrequires/main.luau @@ -1,9 +1,15 @@ -- Entry point that requires other modules local utils = require("./utils") local lib = require("./lib/helper") +local process = require("@std/process") +local path = require("@std/path") + +-- Use standard library to verify it works in compiled bundles +local cwd = path.format(process.cwd()) +print("Main module running from: " .. cwd) -print("Main module") return { utils = utils, lib = lib, + cwd = cwd, } From 25c10223915d8c9b6c847477a6297fa9c0d9d451 Mon Sep 17 00:00:00 2001 From: ariel Date: Wed, 5 Nov 2025 12:24:11 -0800 Subject: [PATCH 119/642] cli: fix the paths used in lute setup to correctly install definitions in the correct location (#530) When we refactored to using the path library in more places, we broke `lute setup` and made it dump everything in the local directory instead of in the correct home directory location. We can fix it fairly easily by adjusting the paths that get used, but I also just did some better hygiene on using the path library in setup then. Fixes #517. --- lute/cli/commands/setup/init.luau | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 55ace32c7..072e0689e 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -4,8 +4,9 @@ local path = require("@std/path") local process = require("@std/process") local json = require("@std/json") -local BASE_PATH = path.parse(".lute/typedefs/0.1.0") -local BASE_PATH_PRETTY = `~/{BASE_PATH}` +local TYPEDEFS_PATH = path.parse(".lute/typedefs/0.1.0") +local BASE_PATH = path.join(process.homedir(), TYPEDEFS_PATH) +local BASE_PATH_PRETTY = `~/{TYPEDEFS_PATH}` local TEMPLATE_RC_FILE = { aliases = { lute = `{BASE_PATH_PRETTY}/lute`, @@ -13,16 +14,14 @@ local TEMPLATE_RC_FILE = { }, } -local homeDir = process.homedir() -local cwd = process.cwd() local args = { ... } -- TODO we're assuming the files are completely unmodified, but we really shouldn't do that. print(`Writing definitions at {BASE_PATH_PRETTY}`) -fs.createdirectory(path.join(homeDir, BASE_PATH), { makeparents = true }) +fs.createdirectory(BASE_PATH, { makeparents = true }) for key, value in definitions :: { [string]: string } do - local filePath = path.parse(`{BASE_PATH}/{key}`) + local filePath = path.join(BASE_PATH, key) fs.createdirectory(path.dirname(filePath), { makeparents = true }) fs.writestringtofile(filePath, value) end @@ -33,7 +32,7 @@ if not table.find(args, "--with-luaurc") then return end -local luauRcPath = path.parse(`{cwd}/.luaurc`) +local luauRcPath = path.join(process.cwd(), ".luaurc") local rcFile = nil print(`Writing luaurc file at {luauRcPath}`) From 5b5d0b69a0e58a623d8e36f85a424f285bc50908 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:07:37 -0800 Subject: [PATCH 120/642] refactor: stdlib string, table, task, vector file formats (#523) Splitting up https://github.com/luau-lang/lute/pull/516 to make it easier to review this one is for string, table, task, vector in std/libs so the docs generator can interpret the format to produce documentation --------- Co-authored-by: ariel --- lute/cli/commands/test/finder.luau | 10 +-- .../cli/commands/transform/lib/arguments.luau | 6 +- lute/cli/commands/transform/lib/files.luau | 10 +-- lute/cli/commands/transform/lib/ignore.luau | 12 ++-- lute/std/libs/string.luau | 59 ----------------- lute/std/libs/stringext.luau | 41 ++++++++++++ lute/std/libs/table.luau | 64 ------------------- lute/std/libs/tableext.luau | 49 ++++++++++++++ lute/std/libs/task.luau | 19 +++--- lute/std/libs/vector.luau | 36 ----------- lute/std/libs/vectorext.luau | 20 ++++++ tests/src/require/lute/std.luau | 8 +-- 12 files changed, 144 insertions(+), 190 deletions(-) delete mode 100644 lute/std/libs/string.luau create mode 100644 lute/std/libs/stringext.luau delete mode 100644 lute/std/libs/table.luau create mode 100644 lute/std/libs/tableext.luau delete mode 100644 lute/std/libs/vector.luau create mode 100644 lute/std/libs/vectorext.luau diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index 74ea75328..413427a89 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -1,11 +1,11 @@ local ps = require("@std/process") local fs = require("@std/fs") local path = require("@std/path") -local string = require("@std/string") -local table = require("@std/table") +local stringext = require("@std/stringext") +local tableext = require("@std/tableext") local function istestfile(filename: string): boolean - return string.endswith(filename, ".test.luau") or string.endswith(filename, ".spec.luau") + return stringext.endswith(filename, ".test.luau") or stringext.endswith(filename, ".spec.luau") end local function findtestfilesrec(directory: path.path): { path.path } @@ -15,7 +15,7 @@ local function findtestfilesrec(directory: path.path): { path.path } local fullPath = path.join(directory, entry.name) if entry.type == "dir" then - table.extend(results, findtestfilesrec(fullPath)) + tableext.extend(results, findtestfilesrec(fullPath)) elseif istestfile(entry.name) then table.insert(results, fullPath) end @@ -35,7 +35,7 @@ local function findtestfiles(paths: { string }): { path.path } end if fs.type(pathObj) == "dir" then - table.extend(files, findtestfilesrec(pathObj)) + tableext.extend(files, findtestfilesrec(pathObj)) elseif istestfile(filepath) then table.insert(files, pathObj) end diff --git a/lute/cli/commands/transform/lib/arguments.luau b/lute/cli/commands/transform/lib/arguments.luau index c2be2cd3c..896cbd468 100644 --- a/lute/cli/commands/transform/lib/arguments.luau +++ b/lute/cli/commands/transform/lib/arguments.luau @@ -1,4 +1,4 @@ -local string = require("@std/string") +local stringext = require("@std/stringext") type Config = { --- Prevent making changes to files @@ -31,8 +31,8 @@ local function parse(arguments: { string }): Config while i <= #arguments do local argument = arguments[i] - if string.startswith(argument, "--") then - local name = string.removeprefix(argument, "--") + if stringext.startswith(argument, "--") then + local name = stringext.removeprefix(argument, "--") if config.migrationPath == nil then -- Options before the codemod file are parsed as options for the tool itself diff --git a/lute/cli/commands/transform/lib/files.luau b/lute/cli/commands/transform/lib/files.luau index e910a0385..09580b8f4 100644 --- a/lute/cli/commands/transform/lib/files.luau +++ b/lute/cli/commands/transform/lib/files.luau @@ -3,8 +3,8 @@ local process = require("@lute/process") local ignore = require("./ignore") local path = require("@std/path") -local string = require("@std/string") -local tbl = require("@std/table") +local stringext = require("@std/stringext") +local tableext = require("@std/tableext") local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: ignore.IgnoreResolver): { path.path } local results = {} @@ -18,10 +18,10 @@ local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: continue end - tbl.extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver)) + tableext.extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver)) else -- TODO: allow customisation of the filter when traversing a directory. e.g., globs? file endings? ignore paths? - if string.endswith(entry.name, ".luau") or string.endswith(entry.name, ".lua") then + if stringext.endswith(entry.name, ".luau") or stringext.endswith(entry.name, ".lua") then if ignoreResolver:isIgnoredFile(fullPath) then print("Skipping", fullPath) continue @@ -49,7 +49,7 @@ local function getSourceFiles(paths: { string }): { path.path } filepath = path.format(pathObj) print(filepath) if fs.type(filepath) == "dir" then - tbl.extend(files, traverseDirectoryRecursive(pathObj, ignoreResolver)) + tableext.extend(files, traverseDirectoryRecursive(pathObj, ignoreResolver)) else table.insert(files, pathObj) end diff --git a/lute/cli/commands/transform/lib/ignore.luau b/lute/cli/commands/transform/lib/ignore.luau index b81f99d10..77dbc3fb0 100644 --- a/lute/cli/commands/transform/lib/ignore.luau +++ b/lute/cli/commands/transform/lib/ignore.luau @@ -1,7 +1,7 @@ local fs = require("@lute/fs") local path = require("@std/path") -local string = require("@std/string") +local stringext = require("@std/stringext") --- Performs ignore resolution on a set of local IgnoreResolver = {} @@ -121,14 +121,14 @@ local function parseGitignoreContents(location: string, contents: string): Gitig } for _, line in lines do - line = string.trim(line) + line = stringext.trim(line) - if line == "" or string.startswith(line, "#") then + if line == "" or stringext.startswith(line, "#") then continue end - if string.startswith(line, "!") then - local globPattern = string.removeprefix(line, "!") + if stringext.startswith(line, "!") then + local globPattern = stringext.removeprefix(line, "!") local glob = parseGlob(globPattern) table.insert(ignoreData.whitelists, glob) else @@ -167,7 +167,7 @@ local function isIgnored(ignoreData: GitignoreData, filepath: string, isDirector local matched, ignored = false, false -- TODO: compute relative path correctly - local relativePath = string.removeprefix(filepath, ignoreData.location) + local relativePath = stringext.removeprefix(filepath, ignoreData.location) for _, ignore in ignoreData.ignores do if matchesGlob(ignore, relativePath, isDirectory) then diff --git a/lute/std/libs/string.luau b/lute/std/libs/string.luau deleted file mode 100644 index 7f875355d..000000000 --- a/lute/std/libs/string.luau +++ /dev/null @@ -1,59 +0,0 @@ -local strings = {} - -function strings.startswith(str: string, prefix: string): boolean - if #prefix > #str then - return false - end - - return str:sub(1, #prefix) == prefix -end - -function strings.removeprefix(str: string, prefix: string): string - if not strings.startswith(str, prefix) then - return str - end - - return str:sub(#prefix + 1) -end - -function strings.endswith(str: string, suffix: string): boolean - return str:sub(-#suffix) == suffix -end - -function strings.removesuffix(str: string, suffix: string): string - if not strings.endswith(str, suffix) then - return str - end - - return str:sub(0, -#suffix - 1) -end - -function strings.trim(str: string): string - -- Pattern is guaranteed to return a result - return str:match("^%s*(.-)%s*$") :: string -end - -return table.freeze({ - startswith = strings.startswith, - removeprefix = strings.removeprefix, - endswith = strings.endswith, - removesuffix = strings.removesuffix, - trim = strings.trim, - - -- re-exports of the table library - byte = string.byte, - char = string.char, - find = string.find, - format = string.format, - gmatch = string.gmatch, - gsub = string.gsub, - len = string.len, - lower = string.lower, - match = string.match, - pack = string.pack, - packsize = string.packsize, - rep = string.rep, - reverse = string.reverse, - split = string.split, - sub = string.sub, -}) diff --git a/lute/std/libs/stringext.luau b/lute/std/libs/stringext.luau new file mode 100644 index 000000000..65d1633d7 --- /dev/null +++ b/lute/std/libs/stringext.luau @@ -0,0 +1,41 @@ +--!strict +-- @std/stringext +-- stdlib for an extension of the built-in string library in Luau +-- Provides additional utility functions for string operations + +local stringext = {} + +function stringext.startswith(str: string, prefix: string): boolean + if #prefix > #str then + return false + end + + return str:sub(1, #prefix) == prefix +end + +function stringext.removeprefix(str: string, prefix: string): string + if not stringext.startswith(str, prefix) then + return str + end + + return str:sub(#prefix + 1) +end + +function stringext.endswith(str: string, suffix: string): boolean + return str:sub(-#suffix) == suffix +end + +function stringext.removesuffix(str: string, suffix: string): string + if not stringext.endswith(str, suffix) then + return str + end + + return str:sub(0, -#suffix - 1) +end + +function stringext.trim(str: string): string + -- Pattern is guaranteed to return a result + return str:match("^%s*(.-)%s*$") :: string +end + +return table.freeze(stringext) diff --git a/lute/std/libs/table.luau b/lute/std/libs/table.luau deleted file mode 100644 index 5f467a481..000000000 --- a/lute/std/libs/table.luau +++ /dev/null @@ -1,64 +0,0 @@ -export type array = { T } -export type dictionary = { [string]: T } - -local function map(table: { [K]: A }, f: (A) -> B): { [K]: B } - local new = {} - - for k, v in table do - new[k] = f(v) - end - - return new -end - -local function filter(table: { [K]: V }, predicate: (V) -> boolean): { [K]: V } - local new = {} - - for k, v in table do - if predicate(v) then - new[k] = v - end - end - - return new -end - -local function fold(table: { [K]: V }, f: (A, V) -> A, initial: A): A - local acc = initial - - for _, v in table do - acc = f(acc, v) - end - - return acc -end - -local function extend(tbl: array, other: array) - for _, entry in other do - table.insert(tbl, entry) - end -end - -return table.freeze({ - -- std extension for table - map = map, - filter = filter, - fold = fold, - extend = extend, - - -- re-exports of the table library - clear = table.clear, - clone = table.clone, - concat = table.concat, - create = table.create, - find = table.find, - freeze = table.freeze, - insert = table.insert, - isfrozen = table.isfrozen, - maxn = table.maxn, - move = table.move, - pack = table.pack, - remove = table.remove, - sort = table.sort, - unpack = table.unpack, -}) diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau new file mode 100644 index 000000000..e48fa268a --- /dev/null +++ b/lute/std/libs/tableext.luau @@ -0,0 +1,49 @@ +--!strict +-- @std/tableext +-- stdlib for an extension of the built-in table library in Luau +-- Provides additional utility functions for table operations + +local tableext = {} + +export type array = { T } +export type dictionary = { [string]: T } + +function tableext.map(table: { [K]: A }, f: (A) -> B): { [K]: B } + local new = {} + + for k, v in table do + new[k] = f(v) + end + + return new +end + +function tableext.filter(table: { [K]: V }, predicate: (V) -> boolean): { [K]: V } + local new = {} + + for k, v in table do + if predicate(v) then + new[k] = v + end + end + + return new +end + +function tableext.fold(table: { [K]: V }, f: (A, V) -> A, initial: A): A + local acc = initial + + for _, v in table do + acc = f(acc, v) + end + + return acc +end + +function tableext.extend(tbl: array, other: array) + for _, entry in other do + table.insert(tbl, entry) + end +end + +return table.freeze(tableext) diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index 53a1d0f68..aede1082e 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -1,12 +1,19 @@ +--!strict +-- @std/task +-- stdlib for `@lute/task` +-- Provides utilities for creating and managing asynchronous tasks + local task = require("@lute/task") +local tasklib = {} + export type task = { success: boolean?, result: any, co: thread, } -local function create(f, ...): task +function tasklib.create(f, ...): task local data = {} data.co = coroutine.create(function(...) @@ -20,7 +27,7 @@ local function create(f, ...): task return data :: task end -local function await(t: task) +function tasklib.await(t: task) if not t.co then error(`await: argument 1 is not a task`) end @@ -36,7 +43,7 @@ local function await(t: task) end end -local function awaitall(...: task) +function tasklib.awaitall(...: task) local tasks = table.pack(...) for i, v in ipairs(tasks) do @@ -70,8 +77,4 @@ local function awaitall(...: task) return table.unpack(results) end -return table.freeze({ - create = create, - await = await, - awaitall = awaitall, -}) +return table.freeze(tasklib) diff --git a/lute/std/libs/vector.luau b/lute/std/libs/vector.luau deleted file mode 100644 index fb2b590d5..000000000 --- a/lute/std/libs/vector.luau +++ /dev/null @@ -1,36 +0,0 @@ -local function withx(vec: vector, x: number): vector - return vector.create(x, vec.y, vec.z) -end - -local function withy(vec: vector, y: number): vector - return vector.create(vec.x, y, vec.z) -end - -local function withz(vec: vector, z: number): vector - return vector.create(vec.x, vec.y, z) -end - -return table.freeze({ - -- std extensions for vector - withx = withx, - withy = withy, - withz = withz, - - -- re-exports of the vector library - create = vector.create, - magnitude = vector.magnitude, - normalize = vector.normalize, - cross = vector.cross, - vector = vector.dot, - angle = vector.angle, - floor = vector.floor, - ceil = vector.ceil, - abs = vector.abs, - sign = vector.sign, - clamp = vector.clamp, - max = vector.max, - min = vector.min, - - zero = vector.zero, - one = vector.one, -}) diff --git a/lute/std/libs/vectorext.luau b/lute/std/libs/vectorext.luau new file mode 100644 index 000000000..6560845dc --- /dev/null +++ b/lute/std/libs/vectorext.luau @@ -0,0 +1,20 @@ +--!strict +-- @std/vectorext +-- stdlib for an extension of the built-in vector library in Luau +-- Provides additional utility functions for vector operations + +local vectorext = {} + +function vectorext.withx(vec: vector, x: number): vector + return vector.create(x, vec.y, vec.z) +end + +function vectorext.withy(vec: vector, y: number): vector + return vector.create(vec.x, y, vec.z) +end + +function vectorext.withz(vec: vector, z: number): vector + return vector.create(vec.x, vec.y, z) +end + +return table.freeze(vectorext) diff --git a/tests/src/require/lute/std.luau b/tests/src/require/lute/std.luau index c3624f856..2a5cefffc 100644 --- a/tests/src/require/lute/std.luau +++ b/tests/src/require/lute/std.luau @@ -1,13 +1,13 @@ -local table = require("@std/table") +local tableext = require("@std/tableext") local task = require("@std/task") local time = require("@std/time") -local vector = require("@std/vector") +local vectorext = require("@std/vectorext") local system = require("@std/system") -assert(type(table) == "table") +assert(type(tableext) == "table") assert(type(task) == "table") assert(type(time) == "table") -assert(type(vector) == "table") +assert(type(vectorext) == "table") assert(type(system) == "table") return { "successfully required @std modules" } From f710123205794e83ebe904f7a2d44a0e4d5d3794 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:07:57 -0800 Subject: [PATCH 121/642] Refactor: move std/time.duration into proper submodule (#532) Resolves https://github.com/luau-lang/lute/issues/526 and should be the last refactor before I can get vitepress doc generation working for stdlib! Also added tests for `@std/time` --- examples/task-wait.luau | 2 +- .../libs/{time.luau => time/duration.luau} | 9 +- lute/std/libs/time/init.luau | 12 ++ tests/std/time.test.luau | 159 ++++++++++++++++++ 4 files changed, 174 insertions(+), 8 deletions(-) rename lute/std/libs/{time.luau => time/duration.luau} (97%) create mode 100644 lute/std/libs/time/init.luau create mode 100644 tests/std/time.test.luau diff --git a/examples/task-wait.luau b/examples/task-wait.luau index 7ee372303..4a91afeda 100644 --- a/examples/task-wait.luau +++ b/examples/task-wait.luau @@ -1,5 +1,5 @@ local task = require("@lute/task") -local time = require("@lute/time") +local time = require("@std/time") print(task.wait(1)) print(task.wait(time.duration.seconds(1))) diff --git a/lute/std/libs/time.luau b/lute/std/libs/time/duration.luau similarity index 97% rename from lute/std/libs/time.luau rename to lute/std/libs/time/duration.luau index 17fd0acc8..5740b9c93 100644 --- a/lute/std/libs/time.luau +++ b/lute/std/libs/time/duration.luau @@ -1,6 +1,5 @@ --!strict --- @std/time --- Provides utility functions for time durations +-- Submodule of @std/time that provides utility functions for time durations local NANOS_PER_SEC = 1_000_000_000 local NANOS_PER_MILLI = 1_000_000 @@ -17,8 +16,6 @@ type durationdata = { _nanoseconds: number, } -local time = {} - local duration = {} duration.__index = duration @@ -170,6 +167,4 @@ function duration.__tostring(self: duration): string return string.format("%d.%09d", self._seconds, self._nanoseconds) end -time.duration = duration - -return table.freeze(time) +return table.freeze(duration) diff --git a/lute/std/libs/time/init.luau b/lute/std/libs/time/init.luau new file mode 100644 index 000000000..817123243 --- /dev/null +++ b/lute/std/libs/time/init.luau @@ -0,0 +1,12 @@ +--!strict +-- @std/time +-- Provides utility functions for time + +local duration = require("@self/duration") +local time = {} + +export type duration = duration.duration + +time.duration = duration + +return table.freeze(time) diff --git a/tests/std/time.test.luau b/tests/std/time.test.luau new file mode 100644 index 000000000..c671527d5 --- /dev/null +++ b/tests/std/time.test.luau @@ -0,0 +1,159 @@ +local test = require("@std/test") +local time = require("@std/time") + +local duration = time.duration + +test.suite("TimeSuite", function(suite) + suite:case("constructors_seconds_millis_micros_nanos", function(assert) + -- seconds + local d1 = duration.seconds(5) + assert.eq(d1:tomilliseconds(), 5000) + assert.eq(d1:tomicroseconds(), 5_000_000) + assert.eq(d1:tonanoseconds(), 5_000_000_000) + + -- milliseconds + local d2 = duration.milliseconds(1500) + assert.eq(d2:tomilliseconds(), 1500) + assert.eq(d2:subsecmillis(), 500) + -- 1.5 is exactly representable; check seconds too + assert.eq(d2:toseconds(), 1.5) + + -- microseconds + local d3 = duration.microseconds(1_234_567) + assert.eq(d3:tomicroseconds(), 1_234_567) + assert.eq(d3:subsecmicros(), 234_567) + assert.eq(d3:tomilliseconds(), 1234) -- floor down + + -- nanoseconds (with normalization) + local d4 = duration.nanoseconds(1_234_567_890) + assert.eq(d4:tonanoseconds(), 1_234_567_890) + assert.eq(d4:subsecnanos(), 234_567_890) + assert.eq(d4:tomilliseconds(), 1234) + + -- boundary normalization: exactly 1s in nanoseconds + local d5 = duration.nanoseconds(1_000_000_000) + assert.eq(d5:toseconds(), 1) + assert.eq(d5:subsecnanos(), 0) + end) + + suite:case("constructors_minutes_hours_days_weeks", function(assert) + local mins = duration.minutes(3) + assert.eq(mins:toseconds(), 180) + + local hours = duration.hours(2) + assert.eq(hours:toseconds(), 2 * 60 * 60) + + local days = duration.days(1) + assert.eq(days:toseconds(), 24 * 60 * 60) + + local weeks = duration.weeks(2) + assert.eq(weeks:toseconds(), 2 * 7 * 24 * 60 * 60) + end) + + suite:case("create_raw_values_and_normalization_via_ops", function(assert) + -- create keeps raw values without normalization + local d = duration.create(1, 500_000_000) + assert.eq(d:toseconds(), 1.5) + assert.eq(d:subsecnanos(), 500_000_000) + assert.eq(d:tomilliseconds(), 1500) + + -- if nanoseconds exceed a second, create still allows it + local d2 = duration.create(1, 1_500_000_000) + assert.eq(d2:toseconds(), 2.5) + assert.eq(d2:subsecnanos(), 1_500_000_000) + + -- operations like addition normalize nanoseconds + local normalized = d2 + duration.seconds(0) + assert.eq(normalized:toseconds(), 2.5) + assert.eq(normalized:subsecnanos(), 500_000_000) + end) + + suite:case("subsecond_accessors", function(assert) + local d = duration.milliseconds(987) + assert.eq(d:subsecmillis(), 987) + + local d2 = duration.microseconds(654_321) + assert.eq(d2:subsecmicros(), 654_321 % 1_000_000) + + local d3 = duration.nanoseconds(123_456_789) + assert.eq(d3:subsecnanos(), 123_456_789 % 1_000_000_000) + end) + + suite:case("conversions_to_ms_us_ns", function(assert) + local d = duration.seconds(2) + assert.eq(d:tomilliseconds(), 2000) + assert.eq(d:tomicroseconds(), 2_000_000) + assert.eq(d:tonanoseconds(), 2_000_000_000) + + local mix = duration.milliseconds(1234) + duration.microseconds(567_000) + duration.nanoseconds(890) + -- 1234ms + 567ms + 0.000890ms = 1801.00089ms + assert.eq(mix:tomilliseconds(), 1801) + assert.eq(mix:tomicroseconds(), 1_801_000) + assert.eq(mix:tonanoseconds(), 1_801_000_890) + end) + + suite:case("addition_normalizes_nanoseconds", function(assert) + local a = duration.milliseconds(800) + local b = duration.milliseconds(300) + local c = a + b + assert.eq(c:tomilliseconds(), 1100) + + local a2 = duration.nanoseconds(800_000_000) + local b2 = duration.nanoseconds(300_000_000) + local c2 = a2 + b2 -- should carry +1s and leave 100_000_000ns + assert.eq(c2:toseconds(), 1.1) + assert.eq(c2:subsecnanos(), 100_000_000) + end) + + suite:case("subtraction_borrows_nanoseconds", function(assert) + local a = duration.milliseconds(1100) -- 1s 100ms + local b = duration.milliseconds(200) + local c = a - b -- 900ms after borrow + assert.eq(c:tomilliseconds(), 900) + + local d = duration.seconds(5) - duration.seconds(5) + assert.eq(d:tomilliseconds(), 0) + end) + + suite:case("multiplication_and_division", function(assert) + local a = duration.milliseconds(250) + local twice = a * 2 + assert.eq(twice:tomilliseconds(), 500) + + local b = duration.milliseconds(600) * 3 + assert.eq(b:tomilliseconds(), 1800) + + local c = duration.seconds(2) / 2 + assert.eq(c:tomilliseconds(), 1000) + end) + + suite:case("comparisons_and_equality", function(assert) + local a = duration.seconds(1) + local b = duration.milliseconds(1000) + local c = duration.milliseconds(999) + + assert.eq(a == b, true) + assert.eq(a == c, false) + assert.eq(c < a, true) + assert.eq(c <= a, true) + assert.eq(a <= b, true) + assert.eq(a < b, false) + end) + + suite:case("tostring_format", function(assert) + local a = duration.seconds(1) + assert.eq(tostring(a), "1.000000000") + + local b = duration.milliseconds(1) + assert.eq(tostring(b), "0.001000000") + + local c = duration.nanoseconds(5) + assert.eq(tostring(c), "0.000000005") + + local d = duration.milliseconds(123) + duration.nanoseconds(456) + -- 0s 123_000_456ns + assert.eq(tostring(d), "0.123000456") + end) +end) + +test.run() From 10feb1b599a788b27acd8ffbe9ac4b70a1db668d Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:20:45 -0800 Subject: [PATCH 122/642] docs: Add basic standard library docs to lute.luau.org (#534) Docs docs docs Extending existing regex parsing for `definitions/` to our standard library modules too! Helps users have better visibility into using `@std/` This is a super rough/basic extension and it's not great (and the vitepress site pages were manually added to the config) but I think it's a good temporary step for improving our docs, especially as we're reaching Release A soon. These are some of the generated pages: image image image --- docs/.vitepress/config.mts | 92 +++++++++++++++++--- docs/index.md | 2 +- docs/scripts/reference.luau | 169 ++++++++++++++++++++++++++++++------ 3 files changed, 223 insertions(+), 40 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index cff200169..1278d214a 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -9,7 +9,7 @@ export default defineConfig({ // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Guide', link: '/guide/installation' }, - { text: 'Reference', link: '/reference/fs' } + { text: 'Reference', link: '/reference/definitions/crypto' } ], sidebar: [ @@ -22,16 +22,86 @@ export default defineConfig({ { text: "Reference", items: [ - { text: 'crypto', link: '/reference/crypto' }, - { text: 'fs', link: '/reference/fs' }, - { text: 'io', link: '/reference/io' }, - { text: 'luau', link: '/reference/luau' }, - { text: 'net', link: '/reference/net' }, - { text: 'process', link: '/reference/process' }, - { text: 'system', link: '/reference/system' }, - { text: 'task', link: '/reference/task' }, - { text: 'time', link: '/reference/time' }, - { text: 'vm', link: '/reference/vm' }, + // Definitions + { text: 'crypto', link: '/reference/definitions/crypto' }, + { text: 'fs', link: '/reference/definitions/fs' }, + { text: 'io', link: '/reference/definitions/io' }, + { text: 'luau', link: '/reference/definitions/luau' }, + { text: 'net', link: '/reference/definitions/net' }, + { text: 'process', link: '/reference/definitions/process' }, + { text: 'system', link: '/reference/definitions/system' }, + { text: 'task', link: '/reference/definitions/task' }, + { text: 'time', link: '/reference/definitions/time' }, + { text: 'vm', link: '/reference/definitions/vm' }, + + // Standard Library subfolder + { + text: "Standard Library", + items: [ + { text: 'assert', link: '/reference/std/assert' }, + { text: 'fs', link: '/reference/std/fs' }, + { text: 'io', link: '/reference/std/io' }, + { text: 'json', link: '/reference/std/json' }, + { text: 'luau', link: '/reference/std/luau' }, + { text: 'process', link: '/reference/std/process' }, + { text: 'stringext', link: '/reference/std/stringext' }, + { text: 'tableext', link: '/reference/std/tableext' }, + { text: 'task', link: '/reference/std/task' }, + { text: 'test', link: '/reference/std/test' }, + { text: 'vectorext', link: '/reference/std/vectorext' }, + + // Path lib subfolder + { + text: "Path", + items: [ + { text: 'path', link: '/reference/std/path/path' }, + { text: 'types', link: '/reference/std/path/types' }, + // Posix lib subfolder + { + text: "Posix", + items: [ + { text: 'posix', link: '/reference/std/path/posix/posix' }, + { text: 'types', link: '/reference/std/path/posix/types' }, + ] + }, + // Win32 lib subfolder + { + text: "Win32", + items: [ + { text: 'win32', link: '/reference/std/path/win32/win32' }, + { text: 'types', link: '/reference/std/path/win32/types' }, + ] + }, + ] + }, + // Syntax lib subfolder + { + text: "Syntax", + items: [ + { text: 'parser', link: '/reference/std/syntax/parser' }, + { text: 'printer', link: '/reference/std/syntax/printer' }, + { text: 'query', link: '/reference/std/syntax/query' }, + { text: 'visitor', link: '/reference/std/syntax/visitor' }, + ] + }, + // System lib subfolder + { + text: "System", + items: [ + { text: 'system', link: '/reference/std/system/system' }, + { text: 'platform', link: '/reference/std/system/platform' }, + ] + }, + // Time lib subfolder + { + text: "Time", + items: [ + { text: 'time', link: '/reference/std/time/time' }, + { text: 'duration', link: '/reference/std/time/duration' }, + ] + } + ] + } ] } ], diff --git a/docs/index.md b/docs/index.md index 07de07168..f88023c4c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ hero: link: /guide/installation - theme: alt text: Reference - link: /reference/fs + link: /reference/definitions/crypto # TODO: add buzz words # features: diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 7ec155498..8d7bb504b 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -1,11 +1,6 @@ -local fs = require("@lute/fs") -local process = require("@lute/process") - -local function projectRelative(...) - local cwd = process.cwd() - local pathSep = if string.find(string.lower(require("@lute/system").os), "windows") then "\\" else "/" - return table.concat({ cwd, "..", ... }, pathSep) -end +local fs = require("@std/fs") +local process = require("@std/process") +local path = require("@std/path") local function extractPropertySignature(module: string, line: string): (string?, string?) -- Match property declarations like: process.env = {} :: { [string]: string } @@ -26,13 +21,18 @@ local function extractPropertySignature(module: string, line: string): (string?, return nil, nil end -local function parseDefinitionFile(module: string, filePath: string): { { name: string, signature: string } } +-- docs for definitions/*.luau -- + +local function parseDefinitionFile( + module: path.pathlike, + filePath: path.pathlike +): { { name: string, signature: string } } local content = fs.readfiletostring(filePath) local lines = string.split(content, "\n") local definitions = {} for _, line in lines do - local name, signature = extractPropertySignature(module, line) + local name, signature = extractPropertySignature(tostring(module), line) if name and signature then table.insert(definitions, { @@ -45,12 +45,109 @@ local function parseDefinitionFile(module: string, filePath: string): { { name: return definitions end -local function generateMarkdown(moduleName: string, definitions: { { name: string, signature: string } }): string +local function generateDefinitionsMarkdown( + moduleName: string, + definitions: { { name: string, signature: string } } +): string + local lines = { + `# {moduleName}`, + "", + "```luau", + `local {moduleName} = require("@lute/{moduleName}")`, + "```", + "", + } + + table.sort(definitions, function(a, b) + return a.name < b.name + end) + + for _, def in definitions do + table.insert(lines, `## {moduleName}.{def.name}`) + table.insert(lines, "```luau") + table.insert(lines, def.signature) + table.insert(lines, "```") + table.insert(lines, "") + end + + return table.concat(lines, "\n") +end + +local function processDefinitionsDir(definitionsPath: path.pathlike, referencePath: path.pathlike) + pcall(fs.createdirectory, referencePath) + + local definitionFiles = fs.listdir(definitionsPath) + + for _, file in definitionFiles do + if file.type == "file" and string.find(file.name, "%.luau$") then + local moduleName = string.gsub(file.name, "%.luau$", "") + local definitionFilePath = path.join(definitionsPath, file.name) + local referenceFilePath = path.join(referencePath, moduleName .. ".md") + + print(`Processing {moduleName}...`) + + local definitions = parseDefinitionFile(moduleName, definitionFilePath) + local markdown = generateDefinitionsMarkdown(moduleName, definitions) + + fs.writestringtofile(referenceFilePath, markdown) + + print(`Generated {tostring(referenceFilePath)}`) + end + end +end + +-- docs for lute/std/libs/*.luau -- + +local STDLIB_MODULE_ALIASES = { + assert = "assertions", + fs = "fslib", + io = "iolib", + process = "processlib", + task = "tasklib", + path = "pathlib", + system = "systemlib", +} + +local function parseStdlibFile(module: path.pathlike, filePath: path.pathlike): { { name: string, signature: string } } + local content = fs.readfiletostring(filePath) + local lines = string.split(content, "\n") + local stdlibDefinitions = {} + + for _, line in lines do + local mod = STDLIB_MODULE_ALIASES[tostring(module)] or tostring(module) + + local name, signature = extractPropertySignature(mod, line) + + if name and signature then + table.insert(stdlibDefinitions, { + name = name, + signature = signature, + }) + end + end + + return stdlibDefinitions +end + +local function generateStdLibMarkdown( + moduleName: string, + definitions: { { name: string, signature: string } }, + stdlibFilePath: path.pathlike +): string + local referencePath = path.join(process.cwd(), "lute", "std", "libs") + + local refPathStr = tostring(referencePath) + local stdlibFilePathStr = tostring(stdlibFilePath) + + local relativePath = string.sub(stdlibFilePathStr, #refPathStr + 2) -- gets the path relative to std/libs for the require path + relativePath = string.gsub(relativePath, "%.luau$", "") + local requirePath = string.gsub(relativePath, "/init$", "") + local lines = { - "# " .. moduleName, + `# {moduleName}`, "", "```luau", - "local " .. moduleName .. ' = require("@lute/' .. moduleName .. '")', + `local {moduleName} = require("@lute/{requirePath}")`, "```", "", } @@ -60,7 +157,7 @@ local function generateMarkdown(moduleName: string, definitions: { { name: strin end) for _, def in definitions do - table.insert(lines, "## " .. def.name) + table.insert(lines, `## {moduleName}.{def.name}`) table.insert(lines, "```luau") table.insert(lines, def.signature) table.insert(lines, "```") @@ -70,28 +167,44 @@ local function generateMarkdown(moduleName: string, definitions: { { name: strin return table.concat(lines, "\n") end -local definitionsPath = projectRelative("definitions") -local referencePath = projectRelative("docs", "reference") +local function processStdlibDir(sourceDir: path.pathlike, referenceDir: path.pathlike) + pcall(fs.createdirectory, referenceDir) + + local entries = fs.listdir(sourceDir) -pcall(fs.mkdir, referencePath) + for _, entry in entries do + local sourcePath = path.join(sourceDir, entry.name) + local referencePath = path.join(referenceDir, entry.name) -local definitionFiles = fs.listdir(definitionsPath) + if entry.type == "dir" then + pcall(fs.createdirectory, referencePath) + processStdlibDir(sourcePath, referencePath) + elseif entry.type == "file" and string.find(entry.name, "%.luau$") then + local moduleName = string.gsub(entry.name, "%.luau$", "") + if moduleName == "init" then + moduleName = string.match(tostring(sourceDir), "([^/\\]+)$") or moduleName + end -for _, file in definitionFiles do - if file.type == "file" and string.find(file.name, "%.luau$") then - local moduleName = string.gsub(file.name, "%.luau$", "") - local definitionFilePath = definitionsPath .. "/" .. file.name - local referenceFilePath = referencePath .. "/" .. moduleName .. ".md" + local referenceFilePath = path.join(referenceDir, moduleName .. ".md") - print("Processing " .. moduleName .. "...") + print(`Processing {moduleName}...`) - local definitions = parseDefinitionFile(moduleName, definitionFilePath) - local markdown = generateMarkdown(moduleName, definitions) + local stdlibDefinitions = parseStdlibFile(moduleName, sourcePath) + local markdown = generateStdLibMarkdown(moduleName, stdlibDefinitions, sourcePath) - fs.writestringtofile(referenceFilePath, markdown) + fs.writestringtofile(referenceFilePath, markdown) - print("Generated " .. referenceFilePath) + print(`Generated {tostring(referenceFilePath)}`) + end end end +local definitionsPath = path.join(process.cwd(), "definitions") +local referencePath = path.join(process.cwd(), "docs", "reference", "definitions") +processDefinitionsDir(definitionsPath, referencePath) + +local stdlibPath = path.join(process.cwd(), "lute", "std", "libs") +local stdlibReferencePath = path.join(process.cwd(), "docs", "reference", "std") +processStdlibDir(stdlibPath, stdlibReferencePath) + print("Reference documentation generation complete!") From 3b2f2528c1d2975946fd48b7c1bfe689eda36226 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 6 Nov 2025 13:40:57 -0800 Subject: [PATCH 123/642] feature: list all discovered tests (#505) This PR adds support for discovering and eventually running test cases. Using the loadbypath api, we can load all the discovered test files and eventually run them (future PR). This PR just implements a simple list all test cases command, much like `doctest`. In order to populate the test environment, I've overridden `require` with one that intercepts calls to "@std/test" and returns the version of std/test require'd by the `test` subcommand itself. This ensures that loading modules registers tests in a way that makes them visible by the subcommand. --- lute/cli/commands/test/init.luau | 71 +++++++++++++++++++++++++++++--- lute/std/libs/luau.luau | 4 +- tests/cli/test.test.luau | 15 +++++-- 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index c67b150fd..49fddc0e6 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -1,4 +1,7 @@ local finder = require("@self/finder") +local luau = require("@std/luau") +local path = require("@std/path") +local test = require("@std/test") local function printHelp() print([[ @@ -31,22 +34,78 @@ EXAMPLES: ]]) end +--[[ + Any of the tests we load will use the instance of std/test initialized in this module (cli/commands/test/init.luau) + Tests that run via lute test don't need to explicitly call test.run, so we'll pass an empty run function so that we don't + execute any tests, while giving us time to remove unecessary run() calls (doing this because this doesn't support running tests just yet) + The reason we have to inject a custom require here is because each `loadbypath` on a test file will return the "@std/test" instance provided by + the EmbeddedStdVFS. However, running `lute test` from the root of the lute repo will use a distinct @std/test instance, which is provided by the .luaurc when running + this file, which means the test environment won't get populated. The consequence of this is that `lute test` will work perfectly everywhere except inside of + the lute repo, which sucks. Intercepting @std/test requires works correctly and ensures that the correct @std/test instance gets populated +]] +-- +local function lutetestrequire(req: string) + if req == "@std/test" then + return table.freeze({ case = test.case, suite = test.suite, run = function() end }) + end + return require(req) :: any +end + +local function loadtests(testfiles: { path.path }) + local fenv: { [unknown]: unknown } = setmetatable({ ["require"] = lutetestrequire }, { __index = _G }) + -- Load all test files first + for _, p in testfiles do + local success, err = pcall(luau.loadbypath, p, fenv) + + if not success then + print(`Error loading {path.format(p)}: {err}`) + end + end +end + +local function listtests() + -- Gets all registered tests using the shared test instance + local registered = test._registered() + local totalTests = #registered.anonymous + + for _, suite in registered.suites do + totalTests += #suite.cases + end + + print("Anonymous:") + for _, tc in registered.anonymous do + print(`\t{tc.name}`) + end + + for _, suite in registered.suites do + print(`{suite.name}:`) + for _, tc in suite.cases do + print(`\t{tc.name}`) + end + end +end + local function main(...: string) local args = { ... } + local testPaths = {} + local isListMode = false - -- Check for help flag for _, arg in args do if arg == "-h" or arg == "--help" then printHelp() return + elseif arg == "--list" then + isListMode = true + else + table.insert(testPaths, arg) end end + local searchpath = if #testPaths > 0 then testPaths else { "./tests" } + loadtests(finder.findtestfiles(searchpath)) - local searchpath = if #args > 0 then args else { "./tests" } - - local files = finder.findtestfiles(searchpath) - - print(`Found {#files} test files`) + if isListMode then + listtests() + end end main(...) diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 42b513b43..070284fc1 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -187,7 +187,7 @@ function luau.load(bytecode: Bytecode, chunkname: string?, env: { [any]: any }?) return luteLuau.load(bytecode, if chunkname ~= nil then `@{chunkname}` else "=luau.load", env) end -function luau.loadbypath(requirePath: path.pathlike): any +function luau.loadbypath(requirePath: path.pathlike, env: { [any]: any }?): any local requirePathStr = if typeof(requirePath) == "string" then requirePath else path.format(requirePath) local migrationHandle = fs.open(requirePathStr, "r") @@ -196,7 +196,7 @@ function luau.loadbypath(requirePath: path.pathlike): any fs.close(migrationHandle) - return luau.load(migrationBytecode, requirePathStr)() + return luau.load(migrationBytecode, requirePathStr, env)() end return luau diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 5bbcde543..97706bf01 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -3,10 +3,19 @@ local path = require("@std/path") local process = require("@std/process") local system = require("@std/system") local test = require("@std/test") - +local fs = require("@std/fs") local lutePath = path.format(process.execpath()) local tmpDir = system.tmpdir() +local expected = [[Anonymous: + Passing +ExampleSuite: + should pass +SmokeSuite: + make_fail + make_pass +]] + test.suite("lute test", function(suite) suite:case("lute test help message", function(assert) -- Run lute test with -h flag @@ -27,11 +36,11 @@ test.suite("lute test", function(suite) suite:case("lute test discovers test files in custom directory", function(assert) -- Run lute test with custom directory path - local result = process.run({ lutePath, "test", "tests/cli/discovery" }) + local result = process.run({ lutePath, "test", "--list", "tests/cli/discovery" }) -- Check that it discovers test files (.test.luau and .spec.luau) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Found 2 test files", 1, true), nil) + assert.eq(expected, result.stdout) end) suite:case("lute doesn't report xpcall as error when accessing field of nil in suite", function(assert) From 6bb7ea3ec414cd15c3654b6c8b7e21dbd78053fe Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:46:22 -0800 Subject: [PATCH 124/642] fix: update paths in lute site doc gen (#537) Didn't realize vitepress runs in `docs/` subfolder so adding a `..` (and a typo fix) to fix the github deployment for docs site fixes [build docs site failure](https://github.com/luau-lang/lute/actions/runs/19150222075/job/54738321290) --- docs/scripts/reference.luau | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 8d7bb504b..89e943cc5 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -134,7 +134,7 @@ local function generateStdLibMarkdown( definitions: { { name: string, signature: string } }, stdlibFilePath: path.pathlike ): string - local referencePath = path.join(process.cwd(), "lute", "std", "libs") + local referencePath = path.join(process.cwd(), "..", "lute", "std", "libs") local refPathStr = tostring(referencePath) local stdlibFilePathStr = tostring(stdlibFilePath) @@ -147,7 +147,7 @@ local function generateStdLibMarkdown( `# {moduleName}`, "", "```luau", - `local {moduleName} = require("@lute/{requirePath}")`, + `local {moduleName} = require("@std/{requirePath}")`, "```", "", } @@ -199,12 +199,12 @@ local function processStdlibDir(sourceDir: path.pathlike, referenceDir: path.pat end end -local definitionsPath = path.join(process.cwd(), "definitions") -local referencePath = path.join(process.cwd(), "docs", "reference", "definitions") +local definitionsPath = path.join(process.cwd(), "..", "definitions") +local referencePath = path.join(process.cwd(), "reference", "definitions") processDefinitionsDir(definitionsPath, referencePath) -local stdlibPath = path.join(process.cwd(), "lute", "std", "libs") -local stdlibReferencePath = path.join(process.cwd(), "docs", "reference", "std") +local stdlibPath = path.join(process.cwd(), "..", "lute", "std", "libs") +local stdlibReferencePath = path.join(process.cwd(), "reference", "std") processStdlibDir(stdlibPath, stdlibReferencePath) print("Reference documentation generation complete!") From 576d564ef48d7a6d26957a944d8ea7204e1632fe Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:19:12 -0800 Subject: [PATCH 125/642] docs: add build-docs-site to CI and fix (#538) --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ docs/scripts/reference.luau | 6 +++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0b1c8a19..3f73598c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,36 @@ jobs: - name: Run StyLua run: stylua --check . + build-docs-site: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + + - name: Install tools + uses: Roblox/setup-foreman@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + allow-external-github-orgs: true + + - name: Install dependencies + run: npm install + working-directory: docs + + - name: Build docs site + run: npm run build + working-directory: docs + + - name: Upload docs artifact + id: deployment + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist/ + aggregator: name: Gated Commits runs-on: ubuntu-latest diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 89e943cc5..83ef654b0 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -74,7 +74,7 @@ local function generateDefinitionsMarkdown( end local function processDefinitionsDir(definitionsPath: path.pathlike, referencePath: path.pathlike) - pcall(fs.createdirectory, referencePath) + fs.createdirectory(referencePath, { makeparents = true }) local definitionFiles = fs.listdir(definitionsPath) @@ -168,7 +168,7 @@ local function generateStdLibMarkdown( end local function processStdlibDir(sourceDir: path.pathlike, referenceDir: path.pathlike) - pcall(fs.createdirectory, referenceDir) + fs.createdirectory(referenceDir, { makeparents = true }) local entries = fs.listdir(sourceDir) @@ -177,7 +177,7 @@ local function processStdlibDir(sourceDir: path.pathlike, referenceDir: path.pat local referencePath = path.join(referenceDir, entry.name) if entry.type == "dir" then - pcall(fs.createdirectory, referencePath) + fs.createdirectory(referencePath, { makeparents = true }) processStdlibDir(sourcePath, referencePath) elseif entry.type == "file" and string.find(entry.name, "%.luau$") then local moduleName = string.gsub(entry.name, "%.luau$", "") From fa61b06e2925acb830787611ceea084491150967 Mon Sep 17 00:00:00 2001 From: Gargafield <70876593+Gargafield@users.noreply.github.com> Date: Fri, 7 Nov 2025 01:27:40 +0100 Subject: [PATCH 126/642] cmake: Fix error when depending on lute due to module path misconfiguration (#539) Be nice to dependers :) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c3f4a2489..1dd383a07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # set the module path -list(PREPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeBuild") +list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeBuild") include(get_version) From 41da777179156ab090e45d92b7c091afd5b17eb3 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 7 Nov 2025 09:35:35 -0800 Subject: [PATCH 127/642] refactor: Split up std test (#528) In order to make the components of std/test more independently requirable, I've split it up. I'm going to make the luacasing pass in a follow up PR [here](https://github.com/luau-lang/lute/pull/529) --- lute/std/libs/test.luau | 321 --------------------------- lute/std/libs/{ => test}/assert.luau | 2 +- lute/std/libs/test/init.luau | 89 ++++++++ lute/std/libs/test/reporter.luau | 45 ++++ lute/std/libs/test/runner.luau | 190 ++++++++++++++++ lute/std/libs/test/types.luau | 52 +++++ tests/std/syntax/parser.test.luau | 3 +- 7 files changed, 378 insertions(+), 324 deletions(-) delete mode 100644 lute/std/libs/test.luau rename lute/std/libs/{ => test}/assert.luau (99%) create mode 100644 lute/std/libs/test/init.luau create mode 100644 lute/std/libs/test/reporter.luau create mode 100644 lute/std/libs/test/runner.luau create mode 100644 lute/std/libs/test/types.luau diff --git a/lute/std/libs/test.luau b/lute/std/libs/test.luau deleted file mode 100644 index 43d6256ca..000000000 --- a/lute/std/libs/test.luau +++ /dev/null @@ -1,321 +0,0 @@ ---!strict --- @std/test --- A testing framework for Luau - -local ps = require("@std/process") -local assert = require("@std/assert") --- Test Types -type Test = (assert.Asserts) -> () -type TestCase = { name: string, case: Test } - -type TestSuite = { - name: string, - cases: { TestCase }, - _beforeeach: (() -> ())?, - _beforeall: (() -> ())?, - _aftereach: (() -> ())?, - _afterall: (() -> ())?, - case: (self: TestSuite, string, Test) -> (), - beforeeach: (self: TestSuite, () -> ()) -> (), - beforeall: (self: TestSuite, () -> ()) -> (), - aftereach: (self: TestSuite, () -> ()) -> (), - afterall: (self: TestSuite, () -> ()) -> (), -} - -local TestSuite = {} -TestSuite.__index = TestSuite - -function TestSuite.new(name: string): TestSuite - local self = setmetatable({ - name = name, - cases = {}, - _beforeeach = nil, - _beforeall = nil, - _aftereach = nil, - _afterall = nil, - }, TestSuite) - return self :: TestSuite -end - -function TestSuite:case(name: string, testCase: Test) - table.insert(self.cases, { name = name, case = testCase }) -end - -function TestSuite:beforeeach(hook: () -> ()) - self._beforeeach = hook -end - -function TestSuite:beforeall(hook: () -> ()) - self._beforeall = hook -end - -function TestSuite:aftereach(hook: () -> ()) - self._aftereach = hook -end - -function TestSuite:afterall(hook: () -> ()) - self._afterall = hook -end - --- Test Reporting -type FailedTest = { - test: string, -- name of the test case that failed - assertion: string?, -- if an assertion failed, name of the assertion that failed (e.g., "assert.eq") - message: string?, -- the error message to report with a failed assertion - filename: string, -- source file where the failure occurred - linenumber: number, -- line number of the failure -} - -type TestRunResult = { - failures: { FailedTest }, - total: number, - failed: number, - passed: number, -} - -local function printFailedTest(failed: FailedTest) - print(`❌ {failed.test}`) - if failed.assertion ~= nil then - print(` {failed.filename}:{failed.linenumber}`) - print(` {failed.assertion}: {failed.message}`) - else - print(` Failed with: Runtime error in {failed.test} in:`) - print(` {failed.filename}:{failed.linenumber}`) - end - print() -end - -local function simpleReporter(result: TestRunResult) - print("\n" .. string.rep("=", 50)) - print("TEST RESULTS") - print(string.rep("=", 50)) - - if #result.failures > 0 then - print(`\nFailed Tests ({#result.failures}):\n`) - for _, failed in result.failures do - printFailedTest(failed) - end - end - - print(string.rep("-", 50)) - print(`Total: {result.total}`) - print(`Passed: {result.passed} ✓`) - print(`Failed: {result.failed} ✗`) - print(string.rep("=", 50) .. "\n") -end - -export type TestReporter = (TestRunResult) -> () - --- Testing Environment -type TestEnvironment = { - anonymous: { TestCase }, - suites: { TestSuite }, - reporter: TestReporter, -} - -local env: TestEnvironment = { anonymous = {}, suites = {}, reporter = simpleReporter } - --- Test library export surface -local test = {} - -function test.setreporter(reporter: TestReporter) - test.reporter = reporter -end - --- register an anonymous test case -function test.case(name: string, case: Test) - local testcase = { name = name, case = case } - table.insert(env.anonymous, testcase) -end - --- register a test suite -function test.suite(name: string, registerFn: (TestSuite) -> ()) - local suite = TestSuite.new(name) - registerFn(suite) - table.insert(env.suites, suite) -end - --- get all registered tests without running them (internal) -function test._registered() - return table.freeze({ anonymous = env.anonymous, suites = env.suites }) -end - --- run all the tests -function test.run() - local failures: { FailedTest } = {} - local total = 0 - local failed = 0 - local passed = 0 - - -- Error handler for beforeall hook - local function beforeallErrorHandler(suite: TestSuite) - return function(err) - -- If beforeall fails, skip all tests in the suite - total += #suite.cases - failed += #suite.cases - for _, tc in suite.cases do - local failedtest: FailedTest = { - test = `{suite.name}.{tc.name}`, - assertion = "beforeall", - message = `beforeall hook failed: {err}`, - filename = "", - linenumber = 0, - } - table.insert(failures, failedtest) - end - return tostring(err) - end - end - - -- Error handler for beforeeach hook - local function beforeeachErrorHandler(testFullName: string) - return function(err) - failed += 1 - local failedtest: FailedTest = { - test = testFullName, - assertion = "beforeeach", - message = `beforeeach hook failed: {err}`, - filename = "", - linenumber = 0, - } - table.insert(failures, failedtest) - return tostring(err) - end - end - - -- Error handler for aftereach hook - local function afterachErrorHandler(testFullName: string) - return function(err) - local failedtest: FailedTest = { - test = testFullName, - assertion = "aftereach", - message = `aftereach hook failed: {err}`, - filename = "", - linenumber = 0, - } - table.insert(failures, failedtest) - return tostring(err) - end - end - - -- Error handler for afterall hook - local function afterallErrorHandler(suiteName: string) - return function(err) - failed += 1 - local failedtest: FailedTest = { - test = suiteName, - assertion = "afterall", - message = `afterall hook failed: {err}`, - filename = "", - linenumber = 0, - } - table.insert(failures, failedtest) - return tostring(err) - end - end - - -- Run anonymous tests - for _, tc in env.anonymous do - total += 1 - local function handlefailure(err) - local fileName, lineNumber = debug.info(4, "sl") - local assertName = debug.info(3, "n") - - if assertName == "xpcall" then - fileName, lineNumber = debug.info(2, "sl") - end - - local failedtest: FailedTest = { - test = tc.name, - assertion = if assertName == "xpcall" then nil else assertName, - message = if assertName == "xpcall" then nil else err.msg, - filename = fileName, - linenumber = lineNumber, - } - table.insert(failures, failedtest) - failed += 1 - end - - local success = xpcall(tc.case, handlefailure, assert) - if success then - passed += 1 - end - end - - -- Run tests from suites - for _, suite in env.suites do - -- Run beforeall hook once per suite - if suite._beforeall then - local beforeallSuccess = xpcall(suite._beforeall, beforeallErrorHandler(suite)) - if not beforeallSuccess then - -- If beforeall fails, skip all tests in the suite - continue - end - end - - for _, tc in suite.cases do - total += 1 - local testFullName = `{suite.name}.{tc.name}` - local function handlefailure(err) - local fileName, lineNumber = debug.info(4, "sl") - local assertName = debug.info(3, "n") - - if assertName == "xpcall" then - fileName, lineNumber = debug.info(2, "sl") - end - - local failedtest: FailedTest = { - test = testFullName, - assertion = if assertName == "xpcall" then nil else assertName, - message = if assertName == "xpcall" then nil else err.msg, - filename = fileName, - linenumber = lineNumber, - } - table.insert(failures, failedtest) - end - - -- Run beforeeach hook if it exists - if suite._beforeeach then - local beforeSuccess = xpcall(suite._beforeeach, beforeeachErrorHandler(testFullName)) - if not beforeSuccess then - continue - end - end - - local success = xpcall(tc.case, handlefailure, assert) - -- Run aftereach hook if it exists (runs even if test failed) - if suite._aftereach then - local afterSuccess = xpcall(suite._aftereach, afterachErrorHandler(testFullName)) - if not afterSuccess then - -- If test passed but aftereach failed, mark as failed - success = false - end - end - -- if the test and the teardown method passed, the test passes - if success then - passed += 1 - else - failed += 1 - end - end - - -- Run afterall hook once per suite (runs even if tests failed) - if suite._afterall then - xpcall(suite._afterall, afterallErrorHandler(suite.name)) - end - end - - local result: TestRunResult = { - failures = failures, - total = total, - failed = failed, - passed = passed, - } - env.reporter(result) - if failed ~= 0 then - ps.exit(1) - end - ps.exit(0) -end - -return table.freeze(test) diff --git a/lute/std/libs/assert.luau b/lute/std/libs/test/assert.luau similarity index 99% rename from lute/std/libs/assert.luau rename to lute/std/libs/test/assert.luau index a0052afb1..77b33b352 100644 --- a/lute/std/libs/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -1,5 +1,5 @@ --!strict --- @std/assert +-- @std/test/assert -- Standard assertion library for Luau local assertions = {} diff --git a/lute/std/libs/test/init.luau b/lute/std/libs/test/init.luau new file mode 100644 index 000000000..930b32d2a --- /dev/null +++ b/lute/std/libs/test/init.luau @@ -0,0 +1,89 @@ +--!strict +-- @std/test +-- Standard test library for Luau + +local types = require("@self/types") + +local reporter = require("@self/reporter") +local runner = require("@self/runner") +local ps = require("@std/process") + +type Test = types.Test +type TestCase = types.TestCase +type TestSuite = types.TestSuite +type TestEnvironment = types.TestEnvironment +type TestReporter = types.TestReporter + +local TestSuite = {} +TestSuite.__index = TestSuite + +function TestSuite.new(name: string): TestSuite + local self = setmetatable({ + name = name, + cases = {}, + _beforeeach = nil, + _beforeall = nil, + _aftereach = nil, + _afterall = nil, + }, TestSuite) + return self :: TestSuite +end + +function TestSuite:case(name: string, testCase: Test) + table.insert(self.cases, { name = name, case = testCase }) +end + +function TestSuite:beforeeach(hook: () -> ()) + self._beforeeach = hook +end + +function TestSuite:beforeall(hook: () -> ()) + self._beforeall = hook +end + +function TestSuite:aftereach(hook: () -> ()) + self._aftereach = hook +end + +function TestSuite:afterall(hook: () -> ()) + self._afterall = hook +end + +local env: TestEnvironment = { anonymous = {}, suites = {}, reporter = reporter.simple } + +-- Test library export surface +local test = {} + +function test.setreporter(reporter: TestReporter) + test.reporter = reporter +end + +-- register an anonymous test case +function test.case(name: string, case: Test) + local testcase = { name = name, case = case } + table.insert(env.anonymous, testcase) +end + +-- register a test suite +function test.suite(name: string, registerFn: (TestSuite) -> ()) + local suite = TestSuite.new(name) + registerFn(suite) + table.insert(env.suites, suite) +end + +-- get all registered tests without running them (internal) +function test._registered() + return table.freeze({ anonymous = env.anonymous, suites = env.suites }) +end + +-- run all the tests +function test.run() + local failed = runner.run(env) + reporter.simple(failed) + if failed.failed ~= 0 then + ps.exit(1) + end + ps.exit(0) +end + +return table.freeze(test) diff --git a/lute/std/libs/test/reporter.luau b/lute/std/libs/test/reporter.luau new file mode 100644 index 000000000..78fcf7cd6 --- /dev/null +++ b/lute/std/libs/test/reporter.luau @@ -0,0 +1,45 @@ +--!strict +-- @std/test/reporter +-- Standard test reporter library for Luau + +local types = require("./types") +local assert = require("./assert") + +type FailedTest = types.FailedTest +type TestRunResult = types.TestRunResult +type Assertions = assert.Asserts + +local reporter = {} + +local function printFailedTest(failed: FailedTest) + print(`❌ {failed.test}`) + if failed.assertion ~= nil then + print(` {failed.filename}:{failed.linenumber}`) + print(` {failed.assertion}: {failed.message}`) + else + print(` Failed with: Runtime error in {failed.test} in:`) + print(` {failed.filename}:{failed.linenumber}`) + end + print() +end + +function reporter.simple(result: TestRunResult) + print("\n" .. string.rep("=", 50)) + print("TEST RESULTS") + print(string.rep("=", 50)) + + if #result.failures > 0 then + print(`\nFailed Tests ({#result.failures}):\n`) + for _, failed in result.failures do + printFailedTest(failed) + end + end + + print(string.rep("-", 50)) + print(`Total: {result.total}`) + print(`Passed: {result.passed} ✓`) + print(`Failed: {result.failed} ✗`) + print(string.rep("=", 50) .. "\n") +end + +return table.freeze(reporter) diff --git a/lute/std/libs/test/runner.luau b/lute/std/libs/test/runner.luau new file mode 100644 index 000000000..64291f706 --- /dev/null +++ b/lute/std/libs/test/runner.luau @@ -0,0 +1,190 @@ +--!strict +-- @std/test/runner +-- Standard test runner library for Luau + +local types = require("./types") +local assert = require("./assert") + +type Test = types.Test +type TestCase = types.TestCase +type TestSuite = types.TestSuite +type TestEnvironment = types.TestEnvironment +type TestReporter = types.TestReporter +type FailedTest = types.FailedTest +type TestRunResult = types.TestRunResult + +local runner = {} + +function runner.run(env: TestEnvironment): TestRunResult + local failures: { FailedTest } = {} + local total = 0 + local failed = 0 + local passed = 0 + + -- Error handler for beforeall hook + local function beforeallErrorHandler(suite: TestSuite) + return function(err) + -- If beforeall fails, skip all tests in the suite + total += #suite.cases + failed += #suite.cases + for _, tc in suite.cases do + local failedtest: FailedTest = { + test = `{suite.name}.{tc.name}`, + assertion = "beforeall", + error = `beforeall hook failed: {err}`, + filename = "", + linenumber = 0, + } + table.insert(failures, failedtest) + end + return tostring(err) + end + end + + -- Error handler for beforeeach hook + local function beforeeachErrorHandler(testFullName: string) + return function(err) + failed += 1 + local failedtest: FailedTest = { + test = testFullName, + assertion = "beforeeach", + error = `beforeeach hook failed: {err}`, + filename = "", + linenumber = 0, + } + table.insert(failures, failedtest) + return tostring(err) + end + end + + -- Error handler for aftereach hook + local function afterachErrorHandler(testFullName: string) + return function(err) + local failedtest: FailedTest = { + test = testFullName, + assertion = "aftereach", + error = `aftereach hook failed: {err}`, + filename = "", + linenumber = 0, + } + table.insert(failures, failedtest) + return tostring(err) + end + end + + -- Error handler for afterall hook + local function afterallErrorHandler(suiteName: string) + return function(err) + failed += 1 + local failedtest: FailedTest = { + test = suiteName, + assertion = "afterall", + error = `afterall hook failed: {err}`, + filename = "", + linenumber = 0, + } + table.insert(failures, failedtest) + return tostring(err) + end + end + + -- Run anonymous tests + for _, tc in env.anonymous do + total += 1 + local function handlefailure(err) + local fileName, lineNumber = debug.info(4, "sl") + local assertName: string = debug.info(3, "n") + + if assertName == "xpcall" then + fileName, lineNumber = debug.info(2, "sl") + end + + local failedtest: FailedTest = { + test = tc.name, + assertion = if assertName == "xpcall" then nil else assertName, + error = if assertName == "xpcall" then nil else err.msg, + filename = fileName, + linenumber = lineNumber, + } + table.insert(failures, failedtest) + failed += 1 + end + + local success = xpcall(tc.case, handlefailure, assert) + if success then + passed += 1 + end + end + + -- Run tests from suites + for _, suite in env.suites do + -- Run beforeall hook once per suite + if suite._beforeall then + local beforeallSuccess = xpcall(suite._beforeall, beforeallErrorHandler(suite)) + if not beforeallSuccess then + -- If beforeall fails, skip all tests in the suite + continue + end + end + + for _, tc in suite.cases do + total += 1 + local testFullName = `{suite.name}.{tc.name}` + local function handlefailure(err) + local fileName, lineNumber = debug.info(4, "sl") + local assertName = debug.info(3, "n") + + if assertName == "xpcall" then + fileName, lineNumber = debug.info(2, "sl") + end + + local failedtest: FailedTest = { + test = testFullName, + assertion = if assertName == "xpcall" then nil else assertName, + error = if assertName == "xpcall" then nil else err.msg, + filename = fileName, + linenumber = lineNumber, + } + table.insert(failures, failedtest) + end + + -- Run beforeeach hook if it exists + if suite._beforeeach then + local beforeSuccess = xpcall(suite._beforeeach, beforeeachErrorHandler(testFullName)) + if not beforeSuccess then + continue + end + end + + local success = xpcall(tc.case, handlefailure, assert) + -- Run aftereach hook if it exists (runs even if test failed) + if suite._aftereach then + local afterSuccess = xpcall(suite._aftereach, afterachErrorHandler(testFullName)) + if not afterSuccess then + -- If test passed but aftereach failed, mark as failed + success = false + end + end + -- if the test and the teardown method passed, the test passes + if success then + passed += 1 + else + failed += 1 + end + end + + -- Run afterall hook once per suite (runs even if tests failed) + if suite._afterall then + xpcall(suite._afterall, afterallErrorHandler(suite.name)) + end + end + + return { + failures = failures, + total = total, + failed = failed, + passed = passed, + } +end + +return table.freeze(runner) diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau new file mode 100644 index 000000000..7f58deb34 --- /dev/null +++ b/lute/std/libs/test/types.luau @@ -0,0 +1,52 @@ +--!strict +-- @std/test/types +-- Standard test types library for Luau + +-- Test Reporting +local assert = require("./assert") + +export type Test = (assert.Asserts) -> () + +export type TestCase = { name: string, case: Test } + +export type TestSuite = { + name: string, + cases: { TestCase }, + _beforeeach: (() -> ())?, + _beforeall: (() -> ())?, + _aftereach: (() -> ())?, + _afterall: (() -> ())?, + case: (self: TestSuite, string, Test) -> (), + beforeeach: (self: TestSuite, () -> ()) -> (), + beforeall: (self: TestSuite, () -> ()) -> (), + aftereach: (self: TestSuite, () -> ()) -> (), + afterall: (self: TestSuite, () -> ()) -> (), +} + +export type FailedTest = { + test: string, -- name of the test case that failed + assertion: string?, -- if an assertion failed, name of the assertion that failed (e.g., "assert.eq") + message: string?, -- the error message to report with a failed assertion + filename: string, -- source file where the failure occurred + linenumber: number, -- line number of the failure +} + +export type TestRunResult = { + failures: { FailedTest }, + total: number, + failed: number, + passed: number, +} + +export type TestReporter = (TestRunResult) -> () + +-- Testing Environment +export type TestEnvironment = { + anonymous: { TestCase }, + suites: { TestSuite }, + reporter: TestReporter, +} + +export type TestRunner = (TestEnvironment) -> TestRunResult + +return {} diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 39285de7d..2d2a87052 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -1,9 +1,8 @@ -local asserts = require("@std/assert") local fs = require("@std/fs") local luau = require("@std/luau") local path = require("@std/path") local test = require("@std/test") - +local asserts = require("@std/test/assert") local parser = require("@std/syntax/parser") local printer = require("@std/syntax/printer") local T = require("@std/luau") From bc466b049c82d63f3637ebde81c1ebdb48246730 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 7 Nov 2025 10:10:25 -0800 Subject: [PATCH 128/642] refactor: luacase std test (#529) Ensure that std/test complies with lute style guidelines --- lute/std/libs/test/assert.luau | 2 +- lute/std/libs/test/init.luau | 10 ++++----- lute/std/libs/test/reporter.luau | 6 +++--- lute/std/libs/test/types.luau | 36 ++++++++++++++++---------------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index 77b33b352..fe130e876 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -81,6 +81,6 @@ function assertions.tableeq(lhs: { [unknown]: unknown }, rhs: { [unknown]: unkno end end -export type Asserts = typeof(assertions) +export type asserts = typeof(assertions) return table.freeze(assertions) diff --git a/lute/std/libs/test/init.luau b/lute/std/libs/test/init.luau index 930b32d2a..058843372 100644 --- a/lute/std/libs/test/init.luau +++ b/lute/std/libs/test/init.luau @@ -8,11 +8,11 @@ local reporter = require("@self/reporter") local runner = require("@self/runner") local ps = require("@std/process") -type Test = types.Test -type TestCase = types.TestCase -type TestSuite = types.TestSuite -type TestEnvironment = types.TestEnvironment -type TestReporter = types.TestReporter +type Test = types.test +type TestCase = types.testcase +type TestSuite = types.testsuite +type TestEnvironment = types.testenvironment +type TestReporter = types.testreporter local TestSuite = {} TestSuite.__index = TestSuite diff --git a/lute/std/libs/test/reporter.luau b/lute/std/libs/test/reporter.luau index 78fcf7cd6..11d952ef5 100644 --- a/lute/std/libs/test/reporter.luau +++ b/lute/std/libs/test/reporter.luau @@ -5,9 +5,9 @@ local types = require("./types") local assert = require("./assert") -type FailedTest = types.FailedTest -type TestRunResult = types.TestRunResult -type Assertions = assert.Asserts +type FailedTest = types.failedtest +type TestRunResult = types.testrunresult +type Assertions = assert.asserts local reporter = {} diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index 7f58deb34..817d5d55b 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -5,25 +5,25 @@ -- Test Reporting local assert = require("./assert") -export type Test = (assert.Asserts) -> () +export type test = (assert.asserts) -> () -export type TestCase = { name: string, case: Test } +export type testcase = { name: string, case: test } -export type TestSuite = { +export type testsuite = { name: string, - cases: { TestCase }, + cases: { testcase }, _beforeeach: (() -> ())?, _beforeall: (() -> ())?, _aftereach: (() -> ())?, _afterall: (() -> ())?, - case: (self: TestSuite, string, Test) -> (), - beforeeach: (self: TestSuite, () -> ()) -> (), - beforeall: (self: TestSuite, () -> ()) -> (), - aftereach: (self: TestSuite, () -> ()) -> (), - afterall: (self: TestSuite, () -> ()) -> (), + case: (self: testsuite, string, test) -> (), + beforeeach: (self: testsuite, () -> ()) -> (), + beforeall: (self: testsuite, () -> ()) -> (), + aftereach: (self: testsuite, () -> ()) -> (), + afterall: (self: testsuite, () -> ()) -> (), } -export type FailedTest = { +export type failedtest = { test: string, -- name of the test case that failed assertion: string?, -- if an assertion failed, name of the assertion that failed (e.g., "assert.eq") message: string?, -- the error message to report with a failed assertion @@ -31,22 +31,22 @@ export type FailedTest = { linenumber: number, -- line number of the failure } -export type TestRunResult = { - failures: { FailedTest }, +export type testrunresult = { + failures: { failedtest }, total: number, failed: number, passed: number, } -export type TestReporter = (TestRunResult) -> () +export type testreporter = (testrunresult) -> () -- Testing Environment -export type TestEnvironment = { - anonymous: { TestCase }, - suites: { TestSuite }, - reporter: TestReporter, +export type testenvironment = { + anonymous: { testcase }, + suites: { testsuite }, + reporter: testreporter, } -export type TestRunner = (TestEnvironment) -> TestRunResult +export type testrunner = (testenvironment) -> testrunresult return {} From 4f98891e9ca38dca6fff45073ba12b5e5e612af9 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:31:35 -0500 Subject: [PATCH 129/642] Fix infinite recursion in ignore (#540) --- lute/cli/commands/transform/lib/ignore.luau | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lute/cli/commands/transform/lib/ignore.luau b/lute/cli/commands/transform/lib/ignore.luau index 77dbc3fb0..b7c3652d5 100644 --- a/lute/cli/commands/transform/lib/ignore.luau +++ b/lute/cli/commands/transform/lib/ignore.luau @@ -142,7 +142,9 @@ end local function parseGitignore(filepath: path.path): GitignoreData local contents = fs.readfiletostring(path.format(filepath)) - return parseGitignoreContents(assert(path.dirname(filepath)), contents) + local parentDirectory = if #filepath.parts > 0 then path.dirname(filepath) else nil + assert(parentDirectory) + return parseGitignoreContents(parentDirectory, contents) end local function matchesGlob(glob: Glob, filepath: string, isDirectory: boolean): boolean @@ -196,7 +198,7 @@ end --- Checks whether the given file matches an ignore function IgnoreResolver.isIgnoredFile(self: IgnoreResolver, filepath: path.path) - local directory = path.dirname(filepath) + local directory = if #filepath.parts > 0 then path.dirname(filepath) else nil assert(directory ~= nil, "filepath has no directory!") local ignoreData = self:_readIgnoreRecursive(directory) @@ -205,11 +207,12 @@ end function IgnoreResolver.isIgnoredDirectory(self: IgnoreResolver, directorypath: path.path) -- Hardcode: skip the .git directory - if path.basename(directorypath) == ".git" then + local basename = path.basename(directorypath) + if basename == ".git" then return true end - local parentDirectory = path.dirname(directorypath) + local parentDirectory = if basename then path.dirname(directorypath) else nil if parentDirectory then local ignoreData = self:_readIgnoreRecursive(parentDirectory) return isIgnored(ignoreData, path.format(directorypath), true) @@ -223,7 +226,7 @@ function IgnoreResolver._readIgnoreRecursive(self: IgnoreResolver, directory: st return self._ignoreCache[directory] end - local parentPath = path.dirname(directory) + local parentPath = if path.basename(directory) then path.dirname(directory) else nil local baseIgnores = if parentPath then self:_readIgnoreRecursive(parentPath) else nil local gitignorePath = path.join(directory, ".gitignore") From 8f67b048331e6df28a5105f87c719e0b12f85b72 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:06:40 -0800 Subject: [PATCH 130/642] Feature: Lute Config and Module Resolvers (#535) Split from https://github.com/luau-lang/lute/pull/531 and exposes a Config and Module resolvers for Lute in separate files for ease of use and readability! Have tests for everything except `resolveModule` --- lute/luau/CMakeLists.txt | 6 +- lute/luau/include/lute/configresolver.h | 22 +++++++ lute/luau/include/lute/moduleresolver.h | 19 ++++++ lute/luau/src/configresolver.cpp | 79 +++++++++++++++++++++++++ lute/luau/src/moduleresolver.cpp | 40 +++++++++++++ tests/CMakeLists.txt | 9 +-- tests/src/configresolver.test.cpp | 31 ++++++++++ tests/src/moduleresolver.test.cpp | 24 ++++++++ tests/src/resolver/.luaurc | 6 ++ tests/src/resolver/mainmodule.luau | 27 +++++++++ tests/src/resolver/types.luau | 7 +++ 11 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 lute/luau/include/lute/configresolver.h create mode 100644 lute/luau/include/lute/moduleresolver.h create mode 100644 lute/luau/src/configresolver.cpp create mode 100644 lute/luau/src/moduleresolver.cpp create mode 100644 tests/src/configresolver.test.cpp create mode 100644 tests/src/moduleresolver.test.cpp create mode 100644 tests/src/resolver/.luaurc create mode 100644 tests/src/resolver/mainmodule.luau create mode 100644 tests/src/resolver/types.luau diff --git a/lute/luau/CMakeLists.txt b/lute/luau/CMakeLists.txt index b4c03ccfa..efe530aa9 100644 --- a/lute/luau/CMakeLists.txt +++ b/lute/luau/CMakeLists.txt @@ -1,14 +1,18 @@ add_library(Lute.Luau STATIC) target_sources(Lute.Luau PRIVATE + include/lute/configresolver.h include/lute/luau.h + include/lute/moduleresolver.h include/lute/resolverequire.h + src/configresolver.cpp src/luau.cpp + src/moduleresolver.cpp src/resolverequire.cpp ) target_compile_features(Lute.Luau PUBLIC cxx_std_17) target_include_directories(Lute.Luau PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Luau PRIVATE Lute.Require Lute.Runtime uv_a Luau.Analysis Luau.Ast Luau.Compiler Luau.Require Luau.VM) +target_link_libraries(Lute.Luau PRIVATE Lute.Require Lute.Runtime uv_a Luau.Analysis Luau.Ast Luau.Compiler Luau.CLI.lib Luau.Require Luau.VM) target_compile_options(Lute.Luau PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/luau/include/lute/configresolver.h b/lute/luau/include/lute/configresolver.h new file mode 100644 index 000000000..bb9dbe84b --- /dev/null +++ b/lute/luau/include/lute/configresolver.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Luau/Config.h" + +namespace Luau +{ + +// Based on LuteConfigResolver in tc.cpp. +struct LuteConfigResolver : Luau::ConfigResolver +{ + Luau::Config defaultConfig; + mutable std::unordered_map configCache; + mutable std::vector> configErrors; + + LuteConfigResolver(Luau::Mode mode); + + const Luau::Config& getConfig(const Luau::ModuleName& name) const override; + + const Luau::Config& readConfigRec(const std::string& path) const; +}; + +} // namespace Luau diff --git a/lute/luau/include/lute/moduleresolver.h b/lute/luau/include/lute/moduleresolver.h new file mode 100644 index 000000000..64a8051d8 --- /dev/null +++ b/lute/luau/include/lute/moduleresolver.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Luau/FileResolver.h" + +namespace Luau +{ + +// Based on CliFileResolver in Analyze.cpp. +struct LuteModuleResolver : Luau::FileResolver +{ + LuteModuleResolver() = default; + + std::optional readSource(const Luau::ModuleName& name) override; + + // We are currently resolving modules and requires only, and will add support for Roblox globals / types in a subsequent PR. + std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node) override; +}; + +} // namespace Luau diff --git a/lute/luau/src/configresolver.cpp b/lute/luau/src/configresolver.cpp new file mode 100644 index 000000000..3ea0f6a80 --- /dev/null +++ b/lute/luau/src/configresolver.cpp @@ -0,0 +1,79 @@ +#include "lute/configresolver.h" + +#include "Luau/LuauConfig.h" +#include "Luau/FileUtils.h" +#include "Luau/StringUtils.h" + +namespace Luau +{ + +LuteConfigResolver::LuteConfigResolver(Luau::Mode mode) +{ + defaultConfig.mode = mode; +} + +const Luau::Config& LuteConfigResolver::getConfig(const Luau::ModuleName& name) const +{ + std::optional path = getParentPath(name); + if (!path) + return defaultConfig; + + return readConfigRec(*path); +} + +const Luau::Config& LuteConfigResolver::readConfigRec(const std::string& path) const +{ + auto it = configCache.find(path); + if (it != configCache.end()) + return it->second; + + std::optional parent = getParentPath(path); + Luau::Config result = parent ? readConfigRec(*parent) : defaultConfig; + + std::optional configPath = joinPaths(path, Luau::kConfigName); + if (!isFile(*configPath)) + configPath = std::nullopt; + + std::optional luauConfigPath = joinPaths(path, Luau::kLuauConfigName); + if (!isFile(*luauConfigPath)) + luauConfigPath = std::nullopt; + + if (configPath && luauConfigPath) + { + std::string ambiguousError = format("Both %s and %s files exist", Luau::kConfigName, Luau::kLuauConfigName); + configErrors.emplace_back(*configPath, std::move(ambiguousError)); + } + else if (configPath) + { + if (std::optional contents = readFile(*configPath)) + { + Luau::ConfigOptions::AliasOptions aliasOpts; + aliasOpts.configLocation = *configPath; + aliasOpts.overwriteAliases = true; + + Luau::ConfigOptions opts; + opts.aliasOptions = std::move(aliasOpts); + + std::optional error = Luau::parseConfig(*contents, result, opts); + if (error) + configErrors.emplace_back(*configPath, *error); + } + } + else if (luauConfigPath) + { + if (std::optional contents = readFile(*luauConfigPath)) + { + Luau::ConfigOptions::AliasOptions aliasOpts; + aliasOpts.configLocation = *luauConfigPath; + aliasOpts.overwriteAliases = true; + + std::optional error = Luau::extractLuauConfig(*contents, result, aliasOpts, Luau::InterruptCallbacks{}); + if (error) + configErrors.emplace_back(*luauConfigPath, *error); + } + } + + return configCache[path] = result; +} + +} // namespace Luau diff --git a/lute/luau/src/moduleresolver.cpp b/lute/luau/src/moduleresolver.cpp new file mode 100644 index 000000000..52adab973 --- /dev/null +++ b/lute/luau/src/moduleresolver.cpp @@ -0,0 +1,40 @@ +#include "lute/moduleresolver.h" + +#include "Luau/Ast.h" +#include "Luau/FileUtils.h" + +#include "lute/resolverequire.h" + +namespace Luau +{ + +std::optional LuteModuleResolver::readSource(const Luau::ModuleName& name) +{ + if (std::optional source = readFile(name)) + return Luau::SourceCode{*source, Luau::SourceCode::Module}; + return std::nullopt; +} + +// We are currently resolving modules and requires only, and will add support for Roblox globals / types in a subsequent PR. +std::optional LuteModuleResolver::resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node) +{ + if (auto expr = node->as()) + { + std::string requirePath(expr->value.data, expr->value.size); + + std::string error; + std::string requirerChunkname = "@" + context->name; + std::optional absolutePath = resolveRequire(requirePath, std::move(requirerChunkname), &error); + if (!absolutePath) + { + printf("Failed to resolve require: %s\n", error.c_str()); + return std::nullopt; + } + + return Luau::ModuleInfo{*absolutePath}; + } + + return std::nullopt; +} + +} // namespace Luau diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4952d0ac2..6eb4d119a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,14 +9,15 @@ target_sources(Lute.Test PRIVATE src/luteprojectroot.h src/luteprojectroot.cpp + src/compile.test.cpp + src/configresolver.test.cpp src/modulepath.test.cpp + src/moduleresolver.test.cpp src/require.test.cpp - src/stdsystem.test.cpp src/staticrequires.test.cpp - src/stdsystem.test.cpp - src/compile.test.cpp) + src/stdsystem.test.cpp) set_target_properties(Lute.Test PROPERTIES OUTPUT_NAME lute-tests) target_compile_features(Lute.Test PUBLIC cxx_std_17) -target_link_libraries(Lute.Test PRIVATE Lute.CLI.lib Lute.Require Lute.Runtime Luau.CLI.lib Luau.Compiler Luau.VM) +target_link_libraries(Lute.Test PRIVATE Lute.CLI.lib Lute.Luau Lute.Require Lute.Runtime Luau.CLI.lib Luau.Analysis Luau.Compiler Luau.VM) target_compile_options(Lute.Test PRIVATE ${LUTE_OPTIONS}) diff --git a/tests/src/configresolver.test.cpp b/tests/src/configresolver.test.cpp new file mode 100644 index 000000000..89dd25314 --- /dev/null +++ b/tests/src/configresolver.test.cpp @@ -0,0 +1,31 @@ +// Tests for configuration resolver inheritance and ambiguity. +#include "doctest.h" +#include "luteprojectroot.h" + +#include "lute/configresolver.h" + +#include "Luau/FileUtils.h" + +TEST_CASE("configresolver") +{ + std::string root = getLuteProjectRootAbsolute(); + std::string baseDir = joinPaths(root, "tests/src/resolver"); + std::string file = joinPaths(baseDir, "mainmodule.luau"); + + // There is a .luaurc in tests/src/resolver; verify resolver reads it without crashing + Luau::LuteConfigResolver configResolver(Luau::Mode::Nonstrict); + const Luau::Config& cfg = configResolver.getConfig(file); + + // check that mode was set to strict per .luaurc + CHECK(cfg.mode == Luau::Mode::Strict); + + // check the alias root was set + const Luau::Config::AliasInfo* aliasInfo = cfg.aliases.find("root"); + CHECK(aliasInfo != nullptr); + CHECK(aliasInfo->value == "./"); + + // run readConfigRec on parent directory to verify inheritance + const Luau::Config& parentCfg = configResolver.readConfigRec(baseDir); + // mode should be the same as child since no override + CHECK(parentCfg.mode == Luau::Mode::Strict); +} diff --git a/tests/src/moduleresolver.test.cpp b/tests/src/moduleresolver.test.cpp new file mode 100644 index 000000000..ec121a9e8 --- /dev/null +++ b/tests/src/moduleresolver.test.cpp @@ -0,0 +1,24 @@ +// Simplified tests for module resolver functionality using public APIs. +#include "doctest.h" +#include "luteprojectroot.h" + +#include "Luau/Ast.h" +#include "Luau/FileUtils.h" + +#include "lute/moduleresolver.h" +#include "lute/resolverequire.h" + +#include + +TEST_CASE("moduleresolver_read_source") +{ + std::string root = getLuteProjectRootAbsolute(); + std::string file = joinPaths(root, "tests/src/resolver/mainmodule.luau"); + Luau::LuteModuleResolver resolver; + auto source = resolver.readSource(file); + REQUIRE(source); + CHECK(source->type == Luau::SourceCode::Module); + CHECK(source->source.find("require") != std::string::npos); +} + +// TODO: add tests for resolveModule diff --git a/tests/src/resolver/.luaurc b/tests/src/resolver/.luaurc new file mode 100644 index 000000000..b7a58bb73 --- /dev/null +++ b/tests/src/resolver/.luaurc @@ -0,0 +1,6 @@ +{ + "languageMode": "strict", + "aliases": { + "root": "./" + } +} diff --git a/tests/src/resolver/mainmodule.luau b/tests/src/resolver/mainmodule.luau new file mode 100644 index 000000000..6633c4fc9 --- /dev/null +++ b/tests/src/resolver/mainmodule.luau @@ -0,0 +1,27 @@ +local types = require("@root/types") +local tableext = require("@std/tableext") + +local testFilter: { [string]: number } = { + ["a"] = 1, + ["b"] = 2, +} + +local a = {} +type tb = tableext.dictionary + +function a._a(a: number) + return function(s: string): types.test + return types(s, a) + end +end + +a._b = tableext.map(testFilter, function(value) + return tostring(value) +end) + +a._metadata = { + key = -1, + id = "nice", +} + +return a diff --git a/tests/src/resolver/types.luau b/tests/src/resolver/types.luau new file mode 100644 index 000000000..68ecda81f --- /dev/null +++ b/tests/src/resolver/types.luau @@ -0,0 +1,7 @@ +export type test = { [string]: number } + +return function(s: string, n: number): test + return { + [s] = n, + } +end From 60db8692d19464fb9af149fa2f9e52c2e6fb896d Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:43:41 -0800 Subject: [PATCH 131/642] Add clang-format rules to enforce include order rules (#544) --- .clang-format | 16 +++++++++++++++- lute/cli/include/lute/staticrequires.h | 1 + lute/cli/include/lute/tc.h | 2 +- lute/cli/src/climain.cpp | 20 ++++++++++---------- lute/cli/src/staticrequires.cpp | 5 +++-- lute/crypto/src/crypto.cpp | 3 ++- lute/fs/src/fs.cpp | 14 ++++++++------ lute/io/src/io.cpp | 9 ++++++--- lute/luau/src/configresolver.cpp | 2 +- lute/luau/src/luau.cpp | 13 ++++++------- lute/luau/src/moduleresolver.cpp | 4 ++-- lute/luau/src/resolverequire.cpp | 6 +++--- lute/net/src/net.cpp | 8 +++++--- lute/process/src/process.cpp | 4 ++-- lute/require/src/require.cpp | 9 +++++---- lute/require/src/requirevfs.cpp | 4 ++-- lute/runtime/include/lute/runtime.h | 3 ++- lute/runtime/src/ref.cpp | 1 + lute/runtime/src/runtime.cpp | 2 +- lute/system/include/lute/system.h | 1 + lute/system/src/system.cpp | 1 + lute/task/src/task.cpp | 10 ++++++---- lute/time/include/lute/time.h | 2 ++ lute/time/src/time.cpp | 7 +++++-- lute/vm/include/lute/vm.h | 4 ++-- lute/vm/src/spawn.cpp | 4 ++-- tests/src/cliruntimefixture.cpp | 22 +++++++++++++--------- tests/src/cliruntimefixture.h | 4 ++-- tests/src/compile.test.cpp | 11 +++++++---- tests/src/configresolver.test.cpp | 8 ++++---- tests/src/main.cpp | 4 ++-- tests/src/modulepath.test.cpp | 6 +++--- tests/src/moduleresolver.test.cpp | 11 ++++++----- tests/src/require.test.cpp | 6 +++--- tests/src/staticrequires.test.cpp | 12 ++++-------- tests/src/stdsystem.test.cpp | 15 +++++++++------ 36 files changed, 148 insertions(+), 106 deletions(-) diff --git a/.clang-format b/.clang-format index b715e3c98..743a823c0 100644 --- a/.clang-format +++ b/.clang-format @@ -12,7 +12,21 @@ BreakConstructorInitializers: BeforeComma BreakInheritanceList: BeforeComma ColumnLimit: 150 IndentCaseLabels: false -SortIncludes: false +SortIncludes: CaseInsensitive +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^["](commands|generated/).*' + Priority: 1 + - Regex: '^["](lute)/.*' + Priority: 2 + - Regex: '^["](Luau)/.*' + Priority: 3 + - Regex: '^["](lua).*' + Priority: 4 + - Regex: '^["](openssl/|sodium/|uv|curl/|zlib).*' + Priority: 5 + - Regex: '^<[^/]*>' + Priority: 6 IndentWidth: 4 TabWidth: 4 ObjCBlockIndentWidth: 4 diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h index 2b93093a0..6073a87ab 100644 --- a/lute/cli/include/lute/staticrequires.h +++ b/lute/cli/include/lute/staticrequires.h @@ -25,6 +25,7 @@ class StaticRequireTracer } void printRequireGraph() const; + private: Luau::DenseHashSet visited{""}; std::vector discovered; diff --git a/lute/cli/include/lute/tc.h b/lute/cli/include/lute/tc.h index f88ce0231..55535d5d0 100644 --- a/lute/cli/include/lute/tc.h +++ b/lute/cli/include/lute/tc.h @@ -1,7 +1,7 @@ #pragma once #include "Luau/FileResolver.h" -#include "Luau/Frontend.h" #include "Luau/FileUtils.h" +#include "Luau/Frontend.h" int typecheck(const std::vector& sourceFiles); diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index eae730a74..4488f30af 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -1,15 +1,5 @@ #include "lute/climain.h" -#include "Luau/Common.h" -#include "Luau/CodeGen.h" -#include "Luau/Compiler.h" -#include "Luau/DenseHash.h" -#include "Luau/FileUtils.h" -#include "Luau/Require.h" - -#include "lua.h" -#include "lualib.h" - #include "lute/bundlevfs.h" #include "lute/clicommands.h" #include "lute/clivfs.h" @@ -33,6 +23,16 @@ #include "lute/version.h" #include "lute/vm.h" +#include "Luau/CodeGen.h" +#include "Luau/Common.h" +#include "Luau/Compiler.h" +#include "Luau/DenseHash.h" +#include "Luau/FileUtils.h" +#include "Luau/Require.h" + +#include "lua.h" +#include "lualib.h" + #include "uv.h" #ifdef _WIN32 diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index e90e086c0..164d49201 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -1,9 +1,10 @@ -#include "lute/modulepath.h" #include "lute/staticrequires.h" +#include "lute/modulepath.h" + #include "Luau/Ast.h" -#include "Luau/Parser.h" #include "Luau/FileUtils.h" +#include "Luau/Parser.h" #include "Luau/VecDeque.h" #include diff --git a/lute/crypto/src/crypto.cpp b/lute/crypto/src/crypto.cpp index b92f27b1b..c5bb54c0e 100644 --- a/lute/crypto/src/crypto.cpp +++ b/lute/crypto/src/crypto.cpp @@ -1,7 +1,8 @@ #include "lute/crypto.h" -#include "lua.h" +#include "lua.h" #include "lualib.h" + #include "openssl/digest.h" #include "sodium/crypto_pwhash.h" diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 6f822d0f7..728e7737c 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -1,13 +1,14 @@ #include "lute/fs.h" -#include "lua.h" -#include "lualib.h" -#include "uv.h" - #include "lute/runtime.h" #include "lute/time.h" #include "lute/userdatas.h" +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + #include #include #ifdef _WIN32 @@ -20,9 +21,10 @@ #include #include #include -#include -#include #include +#include + +#include #if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG) diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp index 0562f782c..926302da1 100644 --- a/lute/io/src/io.cpp +++ b/lute/io/src/io.cpp @@ -1,13 +1,16 @@ #include "lute/io.h" -#include "Luau/Variant.h" + #include "lute/runtime.h" -#include "uv.h" -#include +#include "Luau/Variant.h" #include "lua.h" #include "lualib.h" +#include "uv.h" + +#include + namespace io { diff --git a/lute/luau/src/configresolver.cpp b/lute/luau/src/configresolver.cpp index 3ea0f6a80..8e381990d 100644 --- a/lute/luau/src/configresolver.cpp +++ b/lute/luau/src/configresolver.cpp @@ -1,7 +1,7 @@ #include "lute/configresolver.h" -#include "Luau/LuauConfig.h" #include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" #include "Luau/StringUtils.h" namespace Luau diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 036941416..fb5d2b3c3 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1,16 +1,15 @@ #include "lute/luau.h" +#include "lute/userdatas.h" + #include "Luau/Ast.h" +#include "Luau/Compiler.h" #include "Luau/Location.h" -#include "Luau/ParseResult.h" -#include "Luau/Parser.h" +#include "Luau/NotNull.h" #include "Luau/ParseOptions.h" +#include "Luau/Parser.h" +#include "Luau/ParseResult.h" #include "Luau/ToString.h" -#include "Luau/Compiler.h" -#include "Luau/NotNull.h" - -#include "lute/userdatas.h" - #include "lua.h" #include "lualib.h" diff --git a/lute/luau/src/moduleresolver.cpp b/lute/luau/src/moduleresolver.cpp index 52adab973..b8c5ab9b6 100644 --- a/lute/luau/src/moduleresolver.cpp +++ b/lute/luau/src/moduleresolver.cpp @@ -1,10 +1,10 @@ #include "lute/moduleresolver.h" +#include "lute/resolverequire.h" + #include "Luau/Ast.h" #include "Luau/FileUtils.h" -#include "lute/resolverequire.h" - namespace Luau { diff --git a/lute/luau/src/resolverequire.cpp b/lute/luau/src/resolverequire.cpp index 16fbd3b15..0c885d0c4 100644 --- a/lute/luau/src/resolverequire.cpp +++ b/lute/luau/src/resolverequire.cpp @@ -1,11 +1,11 @@ #include "lute/resolverequire.h" -#include "Luau/Common.h" -#include "Luau/RequireNavigator.h" - #include "lute/filevfs.h" #include "lute/modulepath.h" +#include "Luau/Common.h" +#include "Luau/RequireNavigator.h" + #include "lua.h" #include "lualib.h" diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 561308396..fba5a1aa0 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -2,14 +2,13 @@ #include "lute/runtime.h" -#include "curl/curl.h" -#include "App.h" -#include "Loop.h" #include "Luau/DenseHash.h" #include "Luau/Variant.h" #include "lua.h" #include "lualib.h" + +#include "curl/curl.h" #include "uv.h" #include @@ -18,6 +17,9 @@ #include #include +#include "App.h" +#include "Loop.h" + namespace net { diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index a263775d4..a539a1263 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -1,4 +1,5 @@ #include "lute/process.h" + #include "lute/runtime.h" #include "lute/uvutils.h" @@ -9,14 +10,13 @@ #include "uv.h" +#include // IWYU pragma: keep #include #include #include #include #include #include - -#include // IWYU pragma: keep #ifdef PATH_MAX #define LUTE_PATH_MAX PATH_MAX #else diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index b541aa648..887ef9ee2 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -4,13 +4,14 @@ #include "lute/modulepath.h" #include "lute/options.h" -#include "lua.h" -#include "lualib.h" - -#include "Luau/Compiler.h" #include "Luau/CodeGen.h" +#include "Luau/Compiler.h" #include "Luau/Require.h" #include "Luau/StringUtils.h" + +#include "lua.h" +#include "lualib.h" + #include static luarequire_WriteResult write(std::optional contents, char* buffer, size_t bufferSize, size_t* sizeOut) diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 2678b6d84..daa295aae 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -4,10 +4,10 @@ #include "lute/modulepath.h" #include "lute/stdlibvfs.h" -#include "lualib.h" - #include "Luau/Common.h" +#include "lualib.h" + RequireVfs::RequireVfs(CliVfs cliVfs) : cliVfs(std::move(cliVfs)) { diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index a4d417173..1defc5ec8 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -1,8 +1,9 @@ #pragma once -#include "Luau/Variant.h" #include "lute/ref.h" +#include "Luau/Variant.h" + #include #include #include diff --git a/lute/runtime/src/ref.cpp b/lute/runtime/src/ref.cpp index 60e13baba..b8378fc4b 100644 --- a/lute/runtime/src/ref.cpp +++ b/lute/runtime/src/ref.cpp @@ -1,4 +1,5 @@ #include "lute/ref.h" + #include "lute/runtime.h" #include "lua.h" diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 737e44b4f..d36a7606c 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -7,8 +7,8 @@ #include "uv.h" -#include #include +#include static void lua_close_checked(lua_State* L) { diff --git a/lute/system/include/lute/system.h b/lute/system/include/lute/system.h index 7310d1ebe..0bfcde888 100644 --- a/lute/system/include/lute/system.h +++ b/lute/system/include/lute/system.h @@ -2,6 +2,7 @@ #include "lua.h" #include "lualib.h" + #include // open the library as a standard global luau library diff --git a/lute/system/src/system.cpp b/lute/system/src/system.cpp index 399ddb468..5d6277ae5 100644 --- a/lute/system/src/system.cpp +++ b/lute/system/src/system.cpp @@ -1,4 +1,5 @@ #include "lute/system.h" + #include "lute/uvutils.h" #include "lua.h" diff --git a/lute/task/src/task.cpp b/lute/task/src/task.cpp index e235992b2..59c9bcab7 100644 --- a/lute/task/src/task.cpp +++ b/lute/task/src/task.cpp @@ -1,12 +1,14 @@ #include "lute/task.h" -#include "lua.h" -#include "lualib.h" #include "lute/ref.h" #include "lute/runtime.h" -#include "uv.h" -#include "lute/runtime.h" #include "lute/time.h" + +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + #include #include #include diff --git a/lute/time/include/lute/time.h b/lute/time/include/lute/time.h index c7b2620bd..92c4d2301 100644 --- a/lute/time/include/lute/time.h +++ b/lute/time/include/lute/time.h @@ -2,7 +2,9 @@ #include "lua.h" #include "lualib.h" + #include "uv.h" + #include // open the library as a standard global luau library diff --git a/lute/time/src/time.cpp b/lute/time/src/time.cpp index 138f5d7e2..c73c3219d 100644 --- a/lute/time/src/time.cpp +++ b/lute/time/src/time.cpp @@ -1,13 +1,16 @@ #include "lute/time.h" + +#include "lute/userdatas.h" + #include "lua.h" #include "lualib.h" -#include "lute/userdatas.h" + #include "uv.h" +#include #include #include #include -#include const int64_t NANOSECONDS_PER_SECOND = 1000000000; const int64_t MICROSECONDS_PER_SECOND = 1000000; diff --git a/lute/vm/include/lute/vm.h b/lute/vm/include/lute/vm.h index ccac21805..7d83bbc72 100644 --- a/lute/vm/include/lute/vm.h +++ b/lute/vm/include/lute/vm.h @@ -1,10 +1,10 @@ #pragma once +#include "lute/spawn.h" + #include "lua.h" #include "lualib.h" -#include "lute/spawn.h" - // open the library as a standard global luau library int luaopen_vm(lua_State* L); // open the library as a table on top of the stack diff --git a/lute/vm/src/spawn.cpp b/lute/vm/src/spawn.cpp index 407c1e801..50eec4366 100644 --- a/lute/vm/src/spawn.cpp +++ b/lute/vm/src/spawn.cpp @@ -6,11 +6,11 @@ #include "Luau/Require.h" -#include - #include "lua.h" #include "lualib.h" +#include + struct TargetFunction { std::shared_ptr runtime; diff --git a/tests/src/cliruntimefixture.cpp b/tests/src/cliruntimefixture.cpp index 9a2baf32f..6f9907fa1 100644 --- a/tests/src/cliruntimefixture.cpp +++ b/tests/src/cliruntimefixture.cpp @@ -1,10 +1,10 @@ #include "cliruntimefixture.h" +#include "Luau/Compiler.h" + #include "lua.h" #include "lualib.h" -#include "Luau/Compiler.h" - static int capture(lua_State* L) { const char* str = luaL_tolstring(L, 1, nullptr); @@ -17,13 +17,17 @@ static int capture(lua_State* L) CliRuntimeFixture::CliRuntimeFixture() : runtime(std::make_unique()) { - L = setupCliState(*runtime, [](lua_State* L) { - lua_pushstring(L, "capturedoutput"); - lua_pushstring(L, ""); - lua_settable(L, LUA_REGISTRYINDEX); - lua_pushcfunction(L, capture, "capture"); - lua_setglobal(L, "capture"); - }); + L = setupCliState( + *runtime, + [](lua_State* L) + { + lua_pushstring(L, "capturedoutput"); + lua_pushstring(L, ""); + lua_settable(L, LUA_REGISTRYINDEX); + lua_pushcfunction(L, capture, "capture"); + lua_setglobal(L, "capture"); + } + ); } std::string CliRuntimeFixture::getCapturedOutput() diff --git a/tests/src/cliruntimefixture.h b/tests/src/cliruntimefixture.h index 0801a6d1e..e604a0666 100644 --- a/tests/src/cliruntimefixture.h +++ b/tests/src/cliruntimefixture.h @@ -1,6 +1,4 @@ #pragma once -#include "doctest.h" - #include "lute/climain.h" #include "lute/runtime.h" @@ -9,6 +7,8 @@ #include #include +#include "doctest.h" + class CliRuntimeFixture { public: diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp index 2e24995dd..02a77973b 100644 --- a/tests/src/compile.test.cpp +++ b/tests/src/compile.test.cpp @@ -1,14 +1,17 @@ -#include "Luau/FileUtils.h" -#include "doctest.h" -#include "lute/climain.h" #include "lute/compile.h" -#include "luteprojectroot.h" + +#include "lute/climain.h" + +#include "Luau/FileUtils.h" #include #include #include #include +#include "doctest.h" +#include "luteprojectroot.h" + TEST_CASE("lutepayload_single_file_roundtrip") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); diff --git a/tests/src/configresolver.test.cpp b/tests/src/configresolver.test.cpp index 89dd25314..0d7d912b3 100644 --- a/tests/src/configresolver.test.cpp +++ b/tests/src/configresolver.test.cpp @@ -1,11 +1,11 @@ // Tests for configuration resolver inheritance and ambiguity. -#include "doctest.h" -#include "luteprojectroot.h" - #include "lute/configresolver.h" #include "Luau/FileUtils.h" +#include "doctest.h" +#include "luteprojectroot.h" + TEST_CASE("configresolver") { std::string root = getLuteProjectRootAbsolute(); @@ -18,7 +18,7 @@ TEST_CASE("configresolver") // check that mode was set to strict per .luaurc CHECK(cfg.mode == Luau::Mode::Strict); - + // check the alias root was set const Luau::Config::AliasInfo* aliasInfo = cfg.aliases.find("root"); CHECK(aliasInfo != nullptr); diff --git a/tests/src/main.cpp b/tests/src/main.cpp index 9f894fae8..b30655e5d 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -1,8 +1,8 @@ #define DOCTEST_CONFIG_IMPLEMENT -#include "doctest.h" - #include "lute/uvstate.h" +#include "doctest.h" + int main(int argc, char** argv) { UvGlobalState uvState(argc, argv); diff --git a/tests/src/modulepath.test.cpp b/tests/src/modulepath.test.cpp index 84f63f373..df61f9e61 100644 --- a/tests/src/modulepath.test.cpp +++ b/tests/src/modulepath.test.cpp @@ -1,10 +1,10 @@ -#include "doctest.h" -#include "luteprojectroot.h" - #include "lute/modulepath.h" #include "Luau/FileUtils.h" +#include "doctest.h" +#include "luteprojectroot.h" + TEST_CASE("module_path") { for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) diff --git a/tests/src/moduleresolver.test.cpp b/tests/src/moduleresolver.test.cpp index ec121a9e8..ea3f991be 100644 --- a/tests/src/moduleresolver.test.cpp +++ b/tests/src/moduleresolver.test.cpp @@ -1,15 +1,16 @@ // Simplified tests for module resolver functionality using public APIs. -#include "doctest.h" -#include "luteprojectroot.h" +#include "lute/moduleresolver.h" + +#include "lute/resolverequire.h" #include "Luau/Ast.h" #include "Luau/FileUtils.h" -#include "lute/moduleresolver.h" -#include "lute/resolverequire.h" - #include +#include "doctest.h" +#include "luteprojectroot.h" + TEST_CASE("moduleresolver_read_source") { std::string root = getLuteProjectRootAbsolute(); diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index ec4ff99d5..0c857ee5b 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -1,11 +1,11 @@ -#include "Luau/FileUtils.h" -#include "doctest.h" - #include "lute/climain.h" +#include "Luau/FileUtils.h" + #include "lua.h" #include "cliruntimefixture.h" +#include "doctest.h" #include "luteprojectroot.h" TEST_CASE_FIXTURE(CliRuntimeFixture, "require_exists") diff --git a/tests/src/staticrequires.test.cpp b/tests/src/staticrequires.test.cpp index 16a907ab4..da0609b8e 100644 --- a/tests/src/staticrequires.test.cpp +++ b/tests/src/staticrequires.test.cpp @@ -1,5 +1,3 @@ -#include "doctest.h" -#include "luteprojectroot.h" #include "lute/staticrequires.h" #include "Luau/FileUtils.h" @@ -8,6 +6,9 @@ #include #include +#include "doctest.h" +#include "luteprojectroot.h" + TEST_CASE("staticrequiretracer_simple_dependencies") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); @@ -23,12 +24,7 @@ TEST_CASE("staticrequiretracer_simple_dependencies") CHECK(deps[0] == "main.luau"); // Verify all expected files are present (paths are relative to testDir) - std::vector expectedFiles = { - "main.luau", - "utils.luau", - "lib/helper.luau", - "shared.luau" - }; + std::vector expectedFiles = {"main.luau", "utils.luau", "lib/helper.luau", "shared.luau"}; for (const auto& expected : expectedFiles) { diff --git a/tests/src/stdsystem.test.cpp b/tests/src/stdsystem.test.cpp index 55e4fe30e..13fb205c8 100644 --- a/tests/src/stdsystem.test.cpp +++ b/tests/src/stdsystem.test.cpp @@ -1,8 +1,7 @@ -#include "doctest.h" +#include #include "cliruntimefixture.h" - -#include +#include "doctest.h" std::string getHostOS() { @@ -30,9 +29,13 @@ TEST_CASE_FIXTURE(CliRuntimeFixture, "check_std_system_env_bools") { std::string os = getHostOS(); - auto checkBool = [&](const std::string& field, bool expected) { - runCode("local system = require(\"@std/system\")\n" - "capture(system." + field + ")\n"); + auto checkBool = [&](const std::string& field, bool expected) + { + runCode( + "local system = require(\"@std/system\")\n" + "capture(system." + + field + ")\n" + ); std::string output = getCapturedOutput(); CHECK(output == (expected ? "true" : "false")); }; From 0f7e8a488ca60aa1aa00d93c7f60d500363d0d54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Nov 2025 07:40:25 +0000 Subject: [PATCH 132/642] Update Lute to 0.1.0-nightly.20251107 (#543) **Lute**: Updated from `0.1.0-nightly.20251031` to `0.1.0-nightly.20251107` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20251107 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 7649c0e76..764af7d4f 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251031" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251107" } diff --git a/rokit.toml b/rokit.toml index 296323c95..9cb34eb15 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20251031" +lute = "luau-lang/lute@0.1.0-nightly.20251107" From e2992c198274dac2318d2b62f0a1f5eeb0dd4644 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Nov 2025 07:47:57 +0000 Subject: [PATCH 133/642] Update Luau to 0.699 (#542) **Luau**: Updated from `0.698` to `0.699` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.699 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- extern/luau.tune | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index e3d1371e0..f5e80e34f 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.698" -revision = "ff6d381e57bcd1799d850d7fabe543c0f0980a5d" +branch = "0.699" +revision = "994b6416f1a2d16ac06c52b4e574bad5d8749053" From f4cc6a21e8019bb24c77306bd41f7dda2722c225 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Sat, 8 Nov 2025 00:59:34 -0800 Subject: [PATCH 134/642] Fetch out-of-date dependencies and force configure in Luthier for `build` and `run` subcommands (#545) --- tools/luthier.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/luthier.luau b/tools/luthier.luau index 81e780641..43f4e57ca 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -757,7 +757,7 @@ elseif subcommand == "configure" or subcommand == "tune" then elseif subcommand == "build" or subcommand == "craft" then generateFilesIfNeeded() - if not projectPathExists() then + if not projectPathExists() or not areTuneFilesUpToDate() then check(configure()) end @@ -765,7 +765,7 @@ elseif subcommand == "build" or subcommand == "craft" then elseif subcommand == "run" or subcommand == "play" then generateFilesIfNeeded() - if not projectPathExists() then + if not projectPathExists() or not areTuneFilesUpToDate() then check(configure()) end From 699b9094160e26a17eb5d354a8b3598465c496f3 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 10 Nov 2025 08:41:32 -0800 Subject: [PATCH 135/642] refactor: Suppress printing to stdout in test cases by logging to a reporter (#533) This PR adds a reporter to the Lute.CLI.Lib library. Right now, our tests just log directly to stderr / stdout and we don't have any control over this or ability to intercept and test these calls. I've introduced `LuteReporter` which has `TestReporter` and `CliReporter` as subclasses, which store reported strings / logs to stderr/stdout respectively. This lets us access the errors logged out in tests as well as suppresses random logging in our tests. Fixes: https://github.com/luau-lang/lute/issues/511 --- lute/cli/CMakeLists.txt | 4 + lute/cli/include/lute/climain.h | 13 +- lute/cli/include/lute/clireporter.h | 11 ++ lute/cli/include/lute/compile.h | 10 +- lute/cli/include/lute/reporter.h | 53 +++++ lute/cli/include/lute/staticrequires.h | 6 +- lute/cli/include/lute/tc.h | 4 +- lute/cli/src/climain.cpp | 262 ++++++++++++------------- lute/cli/src/clireporter.cpp | 13 ++ lute/cli/src/compile.cpp | 61 +++--- lute/cli/src/main.cpp | 4 +- lute/cli/src/staticrequires.cpp | 9 +- lute/cli/src/tc.cpp | 38 ++-- tests/CMakeLists.txt | 6 + tests/src/cliruntimefixture.cpp | 33 ++-- tests/src/cliruntimefixture.h | 10 +- tests/src/compile.test.cpp | 91 ++++----- tests/src/lutefixture.cpp | 11 ++ tests/src/lutefixture.h | 17 ++ tests/src/require.test.cpp | 28 +-- tests/src/staticrequires.test.cpp | 21 +- tests/src/stdsystem.test.cpp | 21 +- tests/src/testreporter.cpp | 27 +++ tests/src/testreporter.h | 22 +++ 24 files changed, 488 insertions(+), 287 deletions(-) create mode 100644 lute/cli/include/lute/clireporter.h create mode 100644 lute/cli/include/lute/reporter.h create mode 100644 lute/cli/src/clireporter.cpp create mode 100644 tests/src/lutefixture.cpp create mode 100644 tests/src/lutefixture.h create mode 100644 tests/src/testreporter.cpp create mode 100644 tests/src/testreporter.h diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 708991b03..2aaa97889 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -26,6 +26,7 @@ target_sources(Lute.CLI.lib PRIVATE include/lute/climain.h include/lute/compile.h include/lute/luauflags.h + include/lute/reporter.h include/lute/staticrequires.h include/lute/tc.h include/lute/uvstate.h @@ -46,6 +47,9 @@ target_compile_options(Lute.CLI.lib PRIVATE ${LUTE_OPTIONS}) add_executable(Lute.CLI) target_sources(Lute.CLI PRIVATE + include/lute/clireporter.h + + src/clireporter.cpp src/main.cpp ) diff --git a/lute/cli/include/lute/climain.h b/lute/cli/include/lute/climain.h index 4841d7ab3..3ebb588ec 100644 --- a/lute/cli/include/lute/climain.h +++ b/lute/cli/include/lute/climain.h @@ -4,7 +4,16 @@ struct lua_State; struct Runtime; +class LuteReporter; lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit = nullptr); -int cliMain(int argc, char** argv); -bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::string& chunkname, lua_State* GL, int program_argc, char** program_argv); \ No newline at end of file +int cliMain(int argc, char** argv, LuteReporter& reporter); +bool runBytecode( + Runtime& runtime, + const std::string& bytecode, + const std::string& chunkname, + lua_State* GL, + int program_argc, + char** program_argv, + LuteReporter& reporter +); diff --git a/lute/cli/include/lute/clireporter.h b/lute/cli/include/lute/clireporter.h new file mode 100644 index 000000000..fae6df254 --- /dev/null +++ b/lute/cli/include/lute/clireporter.h @@ -0,0 +1,11 @@ +#pragma once + +#include "lute/reporter.h" + +// Standard CLI reporter that writes to stderr and stdout +class CLIReporter : public LuteReporter +{ +public: + void reportError(const std::string& message) override; + void reportOutput(const std::string& message) override; +}; diff --git a/lute/cli/include/lute/compile.h b/lute/cli/include/lute/compile.h index c257d999c..7dc26f6e1 100644 --- a/lute/cli/include/lute/compile.h +++ b/lute/cli/include/lute/compile.h @@ -2,6 +2,7 @@ #include "Luau/DenseHash.h" #include "Luau/FileUtils.h" +#include "lute/reporter.h" #include @@ -36,22 +37,24 @@ struct LuteEncodeResult */ struct LuteExePayload { - LuteExePayload() = default; + LuteExePayload(LuteReporter& reporter); void add(const std::string& luauFilePath); std::optional encode(); - static std::optional decode(const std::string_view binary); + static std::optional decode(const std::string_view binary, LuteReporter& reporter); std::string entryPointPath; Luau::DenseHashMap filePathToBytecode{""}; // path -> bytecode private: + LuteReporter& reporter; bool parseFromDecompressedBundle(std::string_view decompressedBundle); std::vector filePaths; }; struct LuteDecodeResult { + LuteDecodeResult(LuteReporter& reporter); LuteExePayload payload; size_t bytesRead = 0; size_t compressedPayloadSizeBytes = 0; @@ -73,10 +76,11 @@ struct LuteDecodeResult */ struct LuteExecutable { - LuteExecutable(const std::string& luteRuntimePath); + LuteExecutable(const std::string& luteRuntimePath, LuteReporter& reporter); bool create(const std::string& outputPath, LuteExePayload& payload); std::optional extract(); std::string executablePath; + LuteReporter& reporter; }; diff --git a/lute/cli/include/lute/reporter.h b/lute/cli/include/lute/reporter.h new file mode 100644 index 000000000..a6ba8ce78 --- /dev/null +++ b/lute/cli/include/lute/reporter.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include + +// Interface for reporting output and errors from CLI operations +class LuteReporter +{ +public: + virtual ~LuteReporter() = default; + + // Report an error message (typically to stderr) + virtual void reportError(const std::string& message) = 0; + + // Report normal output (typically to stdout) + virtual void reportOutput(const std::string& message) = 0; + + // Variadic template version for formatted error messages using printf-style format specifiers + template + void formatError(const char* format, Args&&... args) + { + reportError(formatMessage(format, std::forward(args)...)); + } + + // Variadic template version for formatted output messages using printf-style format specifiers + template + void formatOutput(const char* format, Args&&... args) + { + reportOutput(formatMessage(format, std::forward(args)...)); + } + +private: + // Format implementation using snprintf + template + static std::string formatMessage(const char* format, Args&&... args) + { + // Calculate required buffer size + int size = std::snprintf(nullptr, 0, format, args...); + + if (size > 0) + { + // Allocate buffer and format the string + std::vector buffer(size + 1); + std::snprintf(buffer.data(), buffer.size(), format, args...); + + return std::string(buffer.data(), size); + } + + // Fallback if formatting fails + return std::string(format); + } +}; diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h index 6073a87ab..2f2347e68 100644 --- a/lute/cli/include/lute/staticrequires.h +++ b/lute/cli/include/lute/staticrequires.h @@ -2,6 +2,8 @@ #include "Luau/DenseHash.h" +#include "lute/reporter.h" + #include #include #include @@ -9,7 +11,7 @@ class StaticRequireTracer { public: - StaticRequireTracer() = default; + StaticRequireTracer(LuteReporter& reporter); // Trace dependencies starting from an entry point file // rootDirectory: Base directory for resolving all requires @@ -37,4 +39,6 @@ class StaticRequireTracer // Resolve a require path relative to the requiring file std::optional resolveRequire(const std::string& requirer, const std::string& required); + + LuteReporter& reporter; }; diff --git a/lute/cli/include/lute/tc.h b/lute/cli/include/lute/tc.h index 55535d5d0..fc3f98272 100644 --- a/lute/cli/include/lute/tc.h +++ b/lute/cli/include/lute/tc.h @@ -4,4 +4,6 @@ #include "Luau/FileUtils.h" #include "Luau/Frontend.h" -int typecheck(const std::vector& sourceFiles); +#include "lute/reporter.h" + +int typecheck(const std::vector& sourceFiles, LuteReporter& reporter); diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 4488f30af..962c64b43 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -13,6 +13,7 @@ #include "lute/options.h" #include "lute/process.h" #include "lute/ref.h" +#include "lute/reporter.h" #include "lute/require.h" #include "lute/runtime.h" #include "lute/staticrequires.h" @@ -44,6 +45,60 @@ #include #include +static const char* HELP_STRING = R"(Usage: lute [options] [arguments...] + +Commands: + run (default) Run a Luau script. + check Type check Luau files. + compile Compile a Luau script into a standalone executable. + setup Generate type definition files for the language server. + +Run Options (when using 'run' or no command): + lute [run] [args...] + Executes the script, passing [args...] to it. + +Check Options: + lute check [file2.luau...] + Performs a type check on the specified files. + +Compile Options: + lute compile [--output ] + Compiles entry point and auto-discovered dependencies into a standalone executable. + +Setup Options: + lute setup + Generates type definition files for the language server. + --with-luaurc Defines aliases to the type definition files in the working directory's luaurc file. + +General Options: + -h, --help Display this usage message. + --version Show the lute version. +)"; + +static const char* VERSION_STRING = LUTE_VERSION_FULL; + +static const char* RUN_HELP_STRING = R"(Usage: lute run [args...] + +Run Options: + -h, --help Display this usage message. +)"; + +static const char* CHECK_HELP_STRING = R"(Usage: lute check [file2.luau...] + +Check Options: + -h, --help Display this usage message. +)"; + +static const char* COMPILE_HELP_STRING = R"(Usage: lute compile [options] + +Compile Options: + --output Name for the compiled executable. + Defaults to entry file's base name (with .exe on Windows). + --bundle-stats Display bundle size and compression statistics. + --show-require-graph Print the require dependency graph. + -h, --help Display this usage message. +)"; + void* createCliRequireContext(lua_State* L) { void* ctx = lua_newuserdatadtor( @@ -162,7 +217,15 @@ static bool setupArguments(lua_State* L, int argc, char** argv) return true; } -bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::string& chunkname, lua_State* GL, int program_argc, char** program_argv) +bool runBytecode( + Runtime& runtime, + const std::string& bytecode, + const std::string& chunkname, + lua_State* GL, + int program_argc, + char** program_argv, + LuteReporter& reporter +) { // module needs to run in a new thread, isolated from the rest lua_State* L = lua_newthread(GL); @@ -173,9 +236,9 @@ bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::strin if (luau_load(L, chunkname.c_str(), bytecode.data(), bytecode.size(), 0) != 0) { if (const char* str = lua_tostring(L, -1)) - fprintf(stderr, "%s", str); + reporter.reportError(str); else - fprintf(stderr, "Failed to load bytecode"); + reporter.reportError("Failed to load bytecode"); lua_pop(GL, 1); return false; @@ -189,7 +252,7 @@ bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::strin if (!setupArguments(L, program_argc, program_argv)) { - fprintf(stderr, "Failed to pass arguments to Luau"); + reporter.reportError("Failed to pass arguments to Luau"); lua_pop(GL, 1); return false; } @@ -202,18 +265,18 @@ bool runBytecode(Runtime& runtime, const std::string& bytecode, const std::strin return runtime.runToCompletion(); } -static bool runFile(Runtime& runtime, const char* name, lua_State* GL, int program_argc, char** program_argv) +static bool runFile(Runtime& runtime, const char* name, lua_State* GL, int program_argc, char** program_argv, LuteReporter& reporter) { if (isDirectory(name)) { - fprintf(stderr, "Error: %s is a directory\n", name); + reporter.formatError("Error: %s is a directory", name); return false; } std::optional source = readFile(name); if (!source) { - fprintf(stderr, "Error opening %s\n", name); + reporter.formatError("Error opening %s", name); return false; } @@ -221,73 +284,7 @@ static bool runFile(Runtime& runtime, const char* name, lua_State* GL, int progr std::string bytecode = Luau::compile(*source, copts()); - return runBytecode(runtime, bytecode, chunkname, GL, program_argc, program_argv); -} - -static void displayHelp() -{ - printf("Usage: lute [options] [arguments...]\n"); - printf("\n"); - printf("Commands:\n"); - printf(" run (default) Run a Luau script.\n"); - printf(" check Type check Luau files.\n"); - printf(" compile Compile a Luau script into a standalone executable.\n"); - printf(" setup Generate type definition files for the language server."); - printf("\n"); - printf("Run Options (when using 'run' or no command):\n"); - printf(" lute [run] [args...]\n"); - printf(" Executes the script, passing [args...] to it.\n"); - printf("\n"); - printf("Check Options:\n"); - printf(" lute check [file2.luau...]\n"); - printf(" Performs a type check on the specified files.\n"); - printf("\n"); - printf("Compile Options:\n"); - printf(" lute compile [--output ]\n"); - printf(" Compiles entry point and auto-discovered dependencies into a standalone executable.\n"); - printf("\n"); - printf("Setup Options:\n"); - printf(" lute setup"); - printf(" Generates type definition files for the language server.\n"); - printf(" --with-luaurc Defines aliases to the type definition files in the working directory's luaurc file.\n"); - printf("\n"); - printf("General Options:\n"); - printf(" -h, --help Display this usage message.\n"); - printf(" --version Show the lute version.\n"); -} - -static void displayVersion() -{ - printf("%s\n", LUTE_VERSION_FULL); -} - -static void displayRunHelp() -{ - printf("Usage: lute run [args...]\n"); - printf("\n"); - printf("Run Options:\n"); - printf(" -h, --help Display this usage message.\n"); -} - -static void displayCheckHelp() -{ - printf("Usage: lute check [file2.luau...]\n"); - printf("\n"); - printf("Check Options:\n"); - printf(" -h, --help Display this usage message.\n"); -} - -static void displayCompileHelp() -{ - printf("Usage: lute compile [options]\n"); - printf("\n"); - printf("Compile Options:\n"); - printf("\t--output Name for the compiled executable.\n"); - printf("\t\tDefaults to entry file's base name (with .exe on Windows).\n"); - printf("\t--bundle-stats Display bundle size and compression statistics.\n"); - printf("\t--show-require-graph Print the require dependency graph.\n"); - printf("\t-h, --help Display this usage message.\n"); - printf("\n"); + return runBytecode(runtime, bytecode, chunkname, GL, program_argc, program_argv, reporter); } static int assertionHandler(const char* expr, const char* file, int line, const char* function) @@ -346,7 +343,7 @@ static std::optional getValidPath(std::string filePath) return std::nullopt; } -int handleRunCommand(int argc, char** argv, int argOffset) +int handleRunCommand(int argc, char** argv, int argOffset, LuteReporter& reporter) { std::string filePath; int program_argc = 0; @@ -358,13 +355,13 @@ int handleRunCommand(int argc, char** argv, int argOffset) if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - displayRunHelp(); + reporter.reportOutput(RUN_HELP_STRING); return 0; } else if (currentArg[0] == '-') { - fprintf(stderr, "Error: Unrecognized option '%s' for 'run' command.\n\n", currentArg); - displayRunHelp(); + reporter.formatError("Error: Unrecognized option '%s' for 'run' command.", currentArg); + reporter.reportOutput(RUN_HELP_STRING); return 1; } else @@ -378,8 +375,8 @@ int handleRunCommand(int argc, char** argv, int argOffset) if (filePath.empty()) { - fprintf(stderr, "Error: No file specified for 'run' command.\n\n"); - displayRunHelp(); + reporter.reportError("Error: No file specified for 'run' command."); + reporter.reportOutput(RUN_HELP_STRING); return 1; } @@ -389,15 +386,15 @@ int handleRunCommand(int argc, char** argv, int argOffset) std::optional validPath = getValidPath(filePath); if (!validPath) { - std::cerr << "Error: File '" << filePath << "' does not exist.\n"; + reporter.formatError("Error: File '%s' does not exist.", filePath.c_str()); return 1; } - bool success = runFile(runtime, validPath->c_str(), L, program_argc, program_argv); + bool success = runFile(runtime, validPath->c_str(), L, program_argc, program_argv, reporter); return success ? 0 : 1; } -int handleCheckCommand(int argc, char** argv, int argOffset) +int handleCheckCommand(int argc, char** argv, int argOffset, LuteReporter& reporter) { std::vector files; @@ -407,13 +404,13 @@ int handleCheckCommand(int argc, char** argv, int argOffset) if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - displayCheckHelp(); + reporter.reportOutput(CHECK_HELP_STRING); return 0; } else if (currentArg[0] == '-') { - fprintf(stderr, "Error: Unrecognized option '%s' for 'check' command.\n\n", currentArg); - displayCheckHelp(); + reporter.formatError("Error: Unrecognized option '%s' for 'check' command.", currentArg); + reporter.reportOutput(CHECK_HELP_STRING); return 1; } else @@ -424,15 +421,15 @@ int handleCheckCommand(int argc, char** argv, int argOffset) if (files.empty()) { - fprintf(stderr, "Error: No files specified for 'check' command.\n\n"); - displayCheckHelp(); + reporter.reportError("Error: No files specified for 'check' command."); + reporter.reportOutput(CHECK_HELP_STRING); return 1; } - return typecheck(files); + return typecheck(files, reporter); } -int handleCompileCommand(int argc, char** argv, int argOffset) +int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& reporter) { std::string filePath; std::string outputPath; @@ -446,15 +443,15 @@ int handleCompileCommand(int argc, char** argv, int argOffset) if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - displayCompileHelp(); + reporter.reportOutput(COMPILE_HELP_STRING); return 0; } else if (strcmp(currentArg, "--output") == 0) { if (i + 1 >= argc) { - fprintf(stderr, "Error: --output requires a path argument.\n\n"); - displayCompileHelp(); + reporter.reportError("Error: --output requires a path argument."); + reporter.reportOutput(COMPILE_HELP_STRING); return 1; } outputPath = argv[++i]; @@ -465,8 +462,8 @@ int handleCompileCommand(int argc, char** argv, int argOffset) showRequireGraph = true; else if (currentArg[0] == '-') { - fprintf(stderr, "Error: Unrecognized option '%s' for 'compile' command.\n\n", currentArg); - displayCompileHelp(); + reporter.formatError("Error: Unrecognized option '%s' for 'compile' command.", currentArg); + reporter.reportOutput(COMPILE_HELP_STRING); return 1; } else if (filePath.empty()) @@ -475,16 +472,16 @@ int handleCompileCommand(int argc, char** argv, int argOffset) } else { - fprintf(stderr, "Error: Unexpected argument '%s'.\n\n", currentArg); - displayCompileHelp(); + reporter.reportError("Error: Too many arguments for 'compile' command."); + reporter.reportOutput(COMPILE_HELP_STRING); return 1; } } if (filePath.empty()) { - fprintf(stderr, "Error: No file specified for 'compile' command.\n\n"); - displayCompileHelp(); + reporter.reportError("Error: No input file specified for 'compile' command."); + reporter.reportOutput(COMPILE_HELP_STRING); return 1; } @@ -492,7 +489,7 @@ int handleCompileCommand(int argc, char** argv, int argOffset) std::optional validPath = getValidPath(filePath); if (!validPath) { - fprintf(stderr, "Error: File '%s' does not exist.\n", filePath.c_str()); + reporter.formatError("Error: File '%s' does not exist.", filePath.c_str()); return 1; } @@ -538,12 +535,12 @@ int handleCompileCommand(int argc, char** argv, int argOffset) } // Perform static require trace - StaticRequireTracer tracer; + StaticRequireTracer tracer{reporter}; std::vector discoveredFiles = tracer.trace(rootDirectory, entryFilename); if (discoveredFiles.empty()) { - fprintf(stderr, "Error: No files discovered during require trace.\n"); + reporter.reportError("Error: No files discovered during require trace."); return 1; } @@ -551,7 +548,7 @@ int handleCompileCommand(int argc, char** argv, int argOffset) tracer.printRequireGraph(); // Create payload and add all discovered files - LuteExePayload payload; + LuteExePayload payload{reporter}; for (const auto& file : discoveredFiles) { // Construct full path from root directory and relative file path @@ -560,30 +557,30 @@ int handleCompileCommand(int argc, char** argv, int argOffset) } // Encode the payload - printf("Compiling and bundling bytecode...\n"); + reporter.reportOutput("Compiling and bundling bytecode..."); std::optional encodeResult = payload.encode(); if (!encodeResult) { - fprintf(stderr, "Error: Failed to encode bundle\n"); + reporter.reportError("Error: Failed to encode bundle"); return 1; } // Show bundle stats if requested if (bundleStats) { - printf("\nBundle Statistics:\n"); - printf(" Files bundled: %zu\n", discoveredFiles.size()); - printf(" Uncompressed size: %zu bytes\n", encodeResult->uncompressedPayloadSizeBytes); - printf(" Compressed size: %zu bytes\n", encodeResult->compressedPayloadSizeBytes); - printf( - " Space saved: %.2f%%\n", + reporter.reportOutput("\nBundle Statistics:"); + reporter.formatOutput("\tFiles bundled: %zu", discoveredFiles.size()); + reporter.formatOutput("\tUncompressed size: %zu bytes", encodeResult->uncompressedPayloadSizeBytes); + reporter.formatOutput("\tCompressed size: %zu bytes", encodeResult->compressedPayloadSizeBytes); + reporter.formatOutput( + "\tSpace saved: %.2f%%", 100.0 * (1.0 - (double)encodeResult->compressedPayloadSizeBytes / (double)encodeResult->uncompressedPayloadSizeBytes) ); - printf( - " Compression ratio: %.2f:1\n", (double)encodeResult->uncompressedPayloadSizeBytes / (double)encodeResult->compressedPayloadSizeBytes + reporter.formatOutput( + "\tCompression ratio: %.2f:1", (double)encodeResult->uncompressedPayloadSizeBytes / (double)encodeResult->compressedPayloadSizeBytes ); - printf(" Total payload size: %zu bytes\n", encodeResult->bytesWritten); - printf("\n"); + reporter.formatOutput("\tTotal payload size: %zu bytes", encodeResult->bytesWritten); + reporter.reportOutput(""); } // Get current executable path @@ -591,32 +588,32 @@ int handleCompileCommand(int argc, char** argv, int argOffset) std::optional exePath = process::getExecPath(&errorMsg); if (!exePath) { - fprintf(stderr, "Error: Failed to get executable path: %s\n", errorMsg.c_str()); + reporter.formatError("Error: Failed to get executable path: %s", errorMsg.c_str()); return 1; } // Create the executable with embedded payload - LuteExecutable executable(*exePath); + LuteExecutable executable{*exePath, reporter}; if (!executable.create(outputPath, payload)) { - fprintf(stderr, "Error: Failed to create executable.\n"); + reporter.reportError("Error: Failed to create executable."); return 1; } - printf("Created executable '%s'\n", outputPath.c_str()); + reporter.formatOutput("Created executable '%s'", outputPath.c_str()); return 0; } -int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv) +int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv, LuteReporter& reporter) { Runtime runtime; lua_State* L = setupCliState(runtime); std::string bytecode = Luau::compile(std::string(result.contents), copts()); - return runBytecode(runtime, bytecode, "@" + result.path, L, program_argc, program_argv) ? 0 : 1; + return runBytecode(runtime, bytecode, "@" + result.path, L, program_argc, program_argv, reporter) ? 0 : 1; } -int cliMain(int argc, char** argv) +int cliMain(int argc, char** argv, LuteReporter& reporter) { Luau::assertHandler() = assertionHandler; setLuauFlags(); @@ -624,18 +621,17 @@ int cliMain(int argc, char** argv) std::string err = ""; if (auto exePath = process::getExecPath(&err)) { - LuteExecutable exe{*exePath}; + LuteExecutable exe{*exePath, reporter}; if (auto payload = exe.extract()) { Runtime runtime; lua_State* GL = setupBundleState(runtime, payload->filePathToBytecode); - std::string entryPoint = payload->entryPointPath; auto entryModule = payload->filePathToBytecode.find(entryPoint); if (entryModule != nullptr) { - bool success = runBytecode(runtime, *entryModule, "@@bundle/" + entryPoint, GL, argc, argv); + bool success = runBytecode(runtime, *entryModule, "@@bundle/" + entryPoint, GL, argc, argv, reporter); return success ? 0 : 1; } } @@ -648,7 +644,7 @@ int cliMain(int argc, char** argv) if (argc < 2) { // TODO: REPL? - displayHelp(); + reporter.reportOutput(HELP_STRING); return 0; } @@ -657,34 +653,34 @@ int cliMain(int argc, char** argv) if (strcmp(command, "run") == 0) { - return handleRunCommand(argc, argv, argOffset); + return handleRunCommand(argc, argv, argOffset, reporter); } else if (strcmp(command, "check") == 0) { - return handleCheckCommand(argc, argv, argOffset); + return handleCheckCommand(argc, argv, argOffset, reporter); } else if (strcmp(command, "compile") == 0) { - return handleCompileCommand(argc, argv, argOffset); + return handleCompileCommand(argc, argv, argOffset, reporter); } else if (strcmp(command, "-h") == 0 || strcmp(command, "--help") == 0) { - displayHelp(); + reporter.reportOutput(HELP_STRING); return 0; } else if (strcmp(command, "--version") == 0) { - displayVersion(); + reporter.reportOutput(VERSION_STRING); return 0; } else if (std::optional result = getCliCommand(command); result) { - return handleCliCommand(*result, argc - argOffset, &argv[argOffset]); + return handleCliCommand(*result, argc - argOffset, &argv[argOffset], reporter); } else { // Default to 'run' command argOffset = 1; - return handleRunCommand(argc, argv, argOffset); + return handleRunCommand(argc, argv, argOffset, reporter); } } diff --git a/lute/cli/src/clireporter.cpp b/lute/cli/src/clireporter.cpp new file mode 100644 index 000000000..34d8d00d5 --- /dev/null +++ b/lute/cli/src/clireporter.cpp @@ -0,0 +1,13 @@ +#include "lute/clireporter.h" + +#include + +void CLIReporter::reportError(const std::string& message) +{ + fprintf(stderr, "%s\n", message.c_str()); +} + +void CLIReporter::reportOutput(const std::string& message) +{ + fprintf(stdout, "%s\n", message.c_str()); +} diff --git a/lute/cli/src/compile.cpp b/lute/cli/src/compile.cpp index 173a8a892..d45a4785c 100644 --- a/lute/cli/src/compile.cpp +++ b/lute/cli/src/compile.cpp @@ -18,6 +18,11 @@ const char MAGIC_FLAG[] = "LUTEBYTE"; const size_t MAGIC_FLAG_SIZE = sizeof(MAGIC_FLAG) - 1; +LuteExePayload::LuteExePayload(LuteReporter& reporter) + : reporter(reporter) +{ +} + void LuteExePayload::add(const std::string& luauFilePath) { // First file added becomes the entry point @@ -32,7 +37,7 @@ std::optional LuteExePayload::encode() // Encoding an empty payload is an error if (filePaths.empty()) { - fprintf(stderr, "Encode failed: No files added to payload\n"); + reporter.reportError("Encode failed: No files added to payload"); return std::nullopt; } @@ -46,7 +51,7 @@ std::optional LuteExePayload::encode() std::optional source = readFile(filePath); if (!source) { - fprintf(stderr, "Encode failed: Could not read file '%s'\n", filePath.c_str()); + reporter.formatError("Encode failed: Could not read file '%s'", filePath.c_str()); return std::nullopt; } @@ -54,7 +59,7 @@ std::optional LuteExePayload::encode() std::string bytecode = Luau::compile(*source, copts()); if (bytecode.empty()) { - fprintf(stderr, "Encode failed: Could not compile file '%s' to bytecode\n", filePath.c_str()); + reporter.formatError("Encode failed: Could not compile file '%s' to bytecode", filePath.c_str()); return std::nullopt; } @@ -93,7 +98,7 @@ std::optional LuteExePayload::encode() if (compressResult != Z_OK) { - fprintf(stderr, "Encode failed: Compression error (zlib error %d)\n", compressResult); + reporter.formatError("Encode failed: Compression error (zlib error %d)", compressResult); return std::nullopt; } result.payload.clear(); @@ -138,15 +143,15 @@ std::optional LuteExePayload::encode() return result; } -std::optional LuteExePayload::decode(const std::string_view binary) +std::optional LuteExePayload::decode(const std::string_view binary, LuteReporter& reporter) { - LuteDecodeResult result; + LuteDecodeResult result{reporter}; result.payload.filePathToBytecode.clear(); // Check minimum size for magic flag if (binary.size() < MAGIC_FLAG_SIZE + sizeof(uint32_t)) { - fprintf(stderr, "Decode failed: Binary too small (%zu bytes) to contain valid payload\n", binary.size()); + reporter.formatError("Decode failed: Binary too small (%zu bytes) to contain valid payload", binary.size()); return std::nullopt; } @@ -154,18 +159,18 @@ std::optional LuteExePayload::decode(const std::string_view bi size_t magicOffset = binary.size() - MAGIC_FLAG_SIZE; if (memcmp(binary.data() + magicOffset, MAGIC_FLAG, MAGIC_FLAG_SIZE) != 0) { - fprintf(stderr, "Decode failed: LUTEBYTE magic flag not found\n"); + reporter.reportError("Decode failed: LUTEBYTE magic flag not found"); return std::nullopt; } // Helper to read fixed-size values backwards - auto readValue = [&binary](size_t& pos, const char* fieldName, auto& value) -> bool + auto readValue = [&binary, &reporter](size_t& pos, const char* fieldName, auto& value) -> bool { // We need this because the auto& parameter acts like a generic and we would like to strip out the & here using T = std::decay_t; if (pos < sizeof(T)) { - fprintf(stderr, "Decode failed: Incomplete %s field\n", fieldName); + reporter.formatError("Decode failed: Incomplete %s field", fieldName); return false; } pos -= sizeof(T); @@ -174,11 +179,11 @@ std::optional LuteExePayload::decode(const std::string_view bi }; // Helper to read variable-length bytes backwards - auto readBytes = [&binary](size_t& pos, size_t length, const char* fieldName) -> std::optional + auto readBytes = [&binary, &reporter](size_t& pos, size_t length, const char* fieldName) -> std::optional { if (pos < length) { - fprintf(stderr, "Decode failed: Incomplete %s field\n", fieldName); + reporter.formatError("Decode failed: Incomplete %s field", fieldName); return std::nullopt; } pos -= length; @@ -218,7 +223,7 @@ std::optional LuteExePayload::decode(const std::string_view bi // Read compressed data if (pos < compressedSize) { - fprintf(stderr, "Decode failed: Incomplete compressed data (expected %llu bytes)\n", static_cast(compressedSize)); + reporter.formatError("Decode failed: Incomplete compressed data (expected %llu bytes)", static_cast(compressedSize)); return std::nullopt; } pos -= compressedSize; @@ -231,7 +236,7 @@ std::optional LuteExePayload::decode(const std::string_view bi if (zlibResult != Z_OK) { - fprintf(stderr, "Decode failed: Decompression error (zlib error %d)\n", zlibResult); + reporter.formatError("Decode failed: Decompression error (zlib error %d)", zlibResult); return std::nullopt; } @@ -239,14 +244,14 @@ std::optional LuteExePayload::decode(const std::string_view bi std::string_view decompressedBundle(reinterpret_cast(uncompressedData.data()), actualUncompressedSize); if (!result.payload.parseFromDecompressedBundle(decompressedBundle)) { - fprintf(stderr, "Decode failed: Failed to parse decompressed bundle\n"); + reporter.reportError("Decode failed: Failed to parse decompressed bundle"); return std::nullopt; } // Validate that the number of parsed files matches the metadata if (result.payload.filePathToBytecode.size() != numFiles) { - fprintf(stderr, "Decode failed: Expected %u files but parsed %zu\n", numFiles, result.payload.filePathToBytecode.size()); + reporter.formatError("Decode failed: Expected %u files but parsed %zu", numFiles, result.payload.filePathToBytecode.size()); return std::nullopt; } @@ -267,7 +272,7 @@ bool LuteExePayload::parseFromDecompressedBundle(std::string_view decompressedBu // Read path length if (offset + sizeof(uint32_t) > decompressedBundle.size()) { - fprintf(stderr, "Invalid bundle: incomplete path length field\n"); + reporter.reportError("Invalid bundle: incomplete path length field"); return false; } @@ -278,7 +283,7 @@ bool LuteExePayload::parseFromDecompressedBundle(std::string_view decompressedBu // Read path string if (offset + pathLength > decompressedBundle.size()) { - fprintf(stderr, "Invalid bundle: incomplete path string\n"); + reporter.reportError("Invalid bundle: incomplete path string"); return false; } @@ -288,7 +293,7 @@ bool LuteExePayload::parseFromDecompressedBundle(std::string_view decompressedBu // Read bytecode size if (offset + sizeof(uint64_t) > decompressedBundle.size()) { - fprintf(stderr, "Invalid bundle: incomplete bytecode size field\n"); + reporter.reportError("Invalid bundle: incomplete bytecode size field"); return false; } @@ -299,7 +304,7 @@ bool LuteExePayload::parseFromDecompressedBundle(std::string_view decompressedBu // Read bytecode if (offset + bytecodeSize > decompressedBundle.size()) { - fprintf(stderr, "Invalid bundle: incomplete bytecode data\n"); + reporter.reportError("Invalid bundle: incomplete bytecode data"); return false; } @@ -313,8 +318,14 @@ bool LuteExePayload::parseFromDecompressedBundle(std::string_view decompressedBu return true; } -LuteExecutable::LuteExecutable(const std::string& luteRuntimePath) +LuteDecodeResult::LuteDecodeResult(LuteReporter& reporter) + : payload(reporter) +{ +} + +LuteExecutable::LuteExecutable(const std::string& luteRuntimePath, LuteReporter& reporter) : executablePath(luteRuntimePath) + , reporter(reporter) { } @@ -324,7 +335,7 @@ bool LuteExecutable::create(const std::string& outputPath, LuteExePayload& paylo std::ifstream sourceExe(executablePath, std::ios::binary | std::ios::ate); if (!sourceExe) { - fprintf(stderr, "Error: Failed to read executable '%s'\n", executablePath.c_str()); + reporter.formatError("Error: Failed to read executable '%s'", executablePath.c_str()); return false; } @@ -342,7 +353,7 @@ bool LuteExecutable::create(const std::string& outputPath, LuteExePayload& paylo if (!encodeResult) { - fprintf(stderr, "Error: Failed to encode payload\n"); + reporter.reportError("Error: Failed to encode payload"); return false; } @@ -350,7 +361,7 @@ bool LuteExecutable::create(const std::string& outputPath, LuteExePayload& paylo std::ofstream outputFile(outputPath, std::ios::binary); if (!outputFile) { - fprintf(stderr, "Error: Failed to create output file '%s'\n", outputPath.c_str()); + reporter.formatError("Error: Failed to create output file '%s'", outputPath.c_str()); return false; } @@ -402,7 +413,7 @@ std::optional LuteExecutable::extract() return std::nullopt; // Decode the payload - std::optional decodedPayload = LuteExePayload::decode(std::string_view(fileData.data(), fileData.size())); + std::optional decodedPayload = LuteExePayload::decode(std::string_view(fileData.data(), fileData.size()), reporter); if (!decodedPayload) return std::nullopt; diff --git a/lute/cli/src/main.cpp b/lute/cli/src/main.cpp index 9230310f4..e120e6738 100644 --- a/lute/cli/src/main.cpp +++ b/lute/cli/src/main.cpp @@ -1,8 +1,10 @@ #include "lute/climain.h" #include "lute/uvstate.h" +#include "lute/clireporter.h" int main(int argc, char** argv) { UvGlobalState uvState(argc, argv); - return cliMain(argc, argv); + CLIReporter reporter; + return cliMain(argc, argv, reporter); } diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index 164d49201..9c2cf0545 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -31,6 +31,11 @@ class RequireExtractor : public Luau::AstVisitor } }; +StaticRequireTracer::StaticRequireTracer(LuteReporter& reporter) + : reporter(reporter) +{ +} + std::vector StaticRequireTracer::trace(const std::string& rootDirectory, const std::string& entryPoint) { visited.clear(); @@ -58,7 +63,7 @@ std::vector StaticRequireTracer::trace(const std::string& rootDirec std::optional source = readFile(fullPath); if (!source) { - fprintf(stderr, "Warning: Could not read file '%s'\n", fullPath.c_str()); + reporter.formatError("Warning: Could not read file '%s'\n", fullPath.c_str()); continue; } @@ -83,7 +88,7 @@ std::vector StaticRequireTracer::trace(const std::string& rootDirec bool isBuiltinLibrary = req.rfind("@std/", 0) == 0 || req.rfind("@lute/", 0) == 0; if (!isBuiltinLibrary) { - fprintf(stderr, "Warning: Could not resolve require('%s') from '%s'\n", req.c_str(), filePath.c_str()); + reporter.formatError("Warning: Could not resolve require('%s') from '%s'\n", req.c_str(), filePath.c_str()); } } } diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index ad9d3f3dd..7a210c7f5 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -122,59 +122,60 @@ struct LuteConfigResolver : Luau::ConfigResolver } }; -static void report(const char* name, const Luau::Location& loc, const char* type, const char* message) +static void report(const char* name, const Luau::Location& loc, const char* type, const char* message, LuteReporter& reporter) { // fprintf(stderr, "%s(%d,%d): %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, type, message); int columnEnd = (loc.begin.line == loc.end.line) ? loc.end.column : 100; // Use stdout to match luacheck behavior - fprintf(stdout, "%s:%d:%d-%d: (W0) %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, columnEnd, type, message); + reporter.formatOutput("%s:%d:%d-%d: (W0) %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, columnEnd, type, message); } -static void reportError(const Luau::Frontend& frontend, const Luau::TypeError& error) +static void reportError(const Luau::Frontend& frontend, const Luau::TypeError& error, LuteReporter& reporter) { std::string humanReadableName = frontend.fileResolver->getHumanReadableModuleName(error.moduleName); if (const Luau::SyntaxError* syntaxError = Luau::get_if(&error.data)) - report(humanReadableName.c_str(), error.location, "SyntaxError", syntaxError->message.c_str()); + report(humanReadableName.c_str(), error.location, "SyntaxError", syntaxError->message.c_str(), reporter); else report( humanReadableName.c_str(), error.location, "TypeError", - Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str() + Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str(), + reporter ); } -static void reportWarning(const char* name, const Luau::LintWarning& warning) +static void reportWarning(const char* name, const Luau::LintWarning& warning, LuteReporter& reporter) { - report(name, warning.location, Luau::LintWarning::getName(warning.code), warning.text.c_str()); + report(name, warning.location, Luau::LintWarning::getName(warning.code), warning.text.c_str(), reporter); } -static bool reportModuleResult(Luau::Frontend& frontend, const Luau::ModuleName& name, bool annotate) +static bool reportModuleResult(Luau::Frontend& frontend, const Luau::ModuleName& name, bool annotate, LuteReporter& reporter) { std::optional cr = frontend.getCheckResult(name, false); if (!cr) { - fprintf(stderr, "Failed to find result for %s\n", name.c_str()); + reporter.formatError("Failed to find result for %s\n", name.c_str()); return false; } if (!frontend.getSourceModule(name)) { - fprintf(stderr, "Error opening %s\n", name.c_str()); + reporter.formatError("Error opening %s\n", name.c_str()); return false; } for (auto& error : cr->errors) - reportError(frontend, error); + reportError(frontend, error, reporter); std::string humanReadableName = frontend.fileResolver->getHumanReadableModuleName(name); for (auto& error : cr->lintResult.errors) - reportWarning(humanReadableName.c_str(), error); + reportWarning(humanReadableName.c_str(), error, reporter); for (auto& warning : cr->lintResult.warnings) - reportWarning(humanReadableName.c_str(), warning); + reportWarning(humanReadableName.c_str(), warning, reporter); return cr->errors.empty() && cr->lintResult.errors.empty(); } @@ -220,13 +221,13 @@ std::vector processSourceFiles(const std::vector& sour return files; } -int typecheck(const std::vector& sourceFilesInput) +int typecheck(const std::vector& sourceFilesInput, LuteReporter& reporter) { std::vector sourceFiles = processSourceFiles(sourceFilesInput); if (sourceFiles.empty()) { - fprintf(stderr, "Error: lute check expects a file to type check.\n\n"); + reporter.reportError("Error: lute check expects a file to type check.\n\n"); return 1; } @@ -268,7 +269,8 @@ int typecheck(const std::vector& sourceFilesInput) humanReadableName.c_str(), location, "InternalCompilerError", - Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str() + Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str(), + reporter ); return 1; } @@ -276,14 +278,14 @@ int typecheck(const std::vector& sourceFilesInput) int failed = 0; for (const Luau::ModuleName& name : checkedModules) - failed += !reportModuleResult(frontend, name, annotate); + failed += !reportModuleResult(frontend, name, annotate, reporter); if (!configResolver.configErrors.empty()) { failed += int(configResolver.configErrors.size()); for (const auto& pair : configResolver.configErrors) - fprintf(stderr, "%s: %s\n", pair.first.c_str(), pair.second.c_str()); + reporter.formatError("%s: %s\n", pair.first.c_str(), pair.second.c_str()); } return failed ? 1 : 0; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6eb4d119a..48d98d001 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,11 +6,17 @@ target_sources(Lute.Test PRIVATE src/cliruntimefixture.h src/cliruntimefixture.cpp + src/lutefixture.h + src/lutefixture.cpp + src/luteprojectroot.h src/luteprojectroot.cpp src/compile.test.cpp src/configresolver.test.cpp + src/testreporter.h + src/testreporter.cpp + src/modulepath.test.cpp src/moduleresolver.test.cpp src/require.test.cpp diff --git a/tests/src/cliruntimefixture.cpp b/tests/src/cliruntimefixture.cpp index 6f9907fa1..033696078 100644 --- a/tests/src/cliruntimefixture.cpp +++ b/tests/src/cliruntimefixture.cpp @@ -5,12 +5,16 @@ #include "lua.h" #include "lualib.h" -static int capture(lua_State* L) +#include "Luau/Compiler.h" + +#include "Luau/Compiler.h" + +static int report(lua_State* L) { const char* str = luaL_tolstring(L, 1, nullptr); - lua_pushstring(L, "capturedoutput"); - lua_pushstring(L, str ? str : ""); - lua_settable(L, LUA_REGISTRYINDEX); + lua_getfield(L, LUA_REGISTRYINDEX, "reporter"); + TestReporter* reporter = static_cast(lua_touserdata(L, -1)); + reporter->reportOutput(str); return 0; } @@ -19,27 +23,18 @@ CliRuntimeFixture::CliRuntimeFixture() { L = setupCliState( *runtime, - [](lua_State* L) + [rep = reporter.get()](lua_State* L) { - lua_pushstring(L, "capturedoutput"); - lua_pushstring(L, ""); - lua_settable(L, LUA_REGISTRYINDEX); - lua_pushcfunction(L, capture, "capture"); - lua_setglobal(L, "capture"); + lua_pushlightuserdata(L, (void*)rep); + lua_setfield(L, LUA_REGISTRYINDEX, "reporter"); + lua_pushcfunction(L, report, ""); + lua_setglobal(L, "report"); } ); } -std::string CliRuntimeFixture::getCapturedOutput() -{ - lua_getfield(L, LUA_REGISTRYINDEX, "capturedoutput"); - const char* output = lua_tostring(L, -1); - lua_pop(L, 1); - return output ? output : ""; -} - bool CliRuntimeFixture::runCode(const std::string& source) { std::string bytecode = Luau::compile(source, Luau::CompileOptions()); - return runBytecode(*runtime, bytecode, "=stdin", L, 0, nullptr); + return runBytecode(*runtime, bytecode, "=stdin", L, 0, nullptr, getReporter()); } diff --git a/tests/src/cliruntimefixture.h b/tests/src/cliruntimefixture.h index e604a0666..e294d165c 100644 --- a/tests/src/cliruntimefixture.h +++ b/tests/src/cliruntimefixture.h @@ -2,24 +2,22 @@ #include "lute/climain.h" #include "lute/runtime.h" +#include "lutefixture.h" + #include "lua.h" #include #include -#include "doctest.h" - -class CliRuntimeFixture +class CliRuntimeFixture : public LuteFixture { public: CliRuntimeFixture(); - std::string getCapturedOutput(); - bool runCode(const std::string& source); lua_State* L; private: std::unique_ptr runtime; -}; \ No newline at end of file +}; diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp index 02a77973b..0bbfaacdd 100644 --- a/tests/src/compile.test.cpp +++ b/tests/src/compile.test.cpp @@ -1,24 +1,25 @@ -#include "lute/compile.h" - #include "lute/climain.h" +#include "lute/compile.h" #include "Luau/FileUtils.h" #include #include +#include #include #include #include "doctest.h" +#include "lutefixture.h" #include "luteprojectroot.h" -TEST_CASE("lutepayload_single_file_roundtrip") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_single_file_roundtrip") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); // Create payload and add file - LuteExePayload originalPayload; + LuteExePayload originalPayload{getReporter()}; originalPayload.add(testFilePath); // Encode @@ -33,7 +34,7 @@ TEST_CASE("lutepayload_single_file_roundtrip") CHECK(encodeResult->compressedPayloadSizeBytes <= encodeResult->uncompressedPayloadSizeBytes); // Decode - auto decodeResult = LuteExePayload::decode(encodeResult->payload); + auto decodeResult = LuteExePayload::decode(encodeResult->payload, getReporter()); REQUIRE(decodeResult.has_value()); // Verify metrics match @@ -56,7 +57,7 @@ TEST_CASE("lutepayload_single_file_roundtrip") CHECK(*it == *originalIt); } -TEST_CASE("lutepayload_multiple_files_roundtrip") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_multiple_files_roundtrip") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); @@ -67,7 +68,7 @@ TEST_CASE("lutepayload_multiple_files_roundtrip") }; // Create payload with multiple files - LuteExePayload originalPayload; + LuteExePayload originalPayload{getReporter()}; for (const auto& file : testFiles) { originalPayload.add(file); @@ -80,7 +81,7 @@ TEST_CASE("lutepayload_multiple_files_roundtrip") REQUIRE(encodeResult.has_value()); REQUIRE(!encodeResult->payload.empty()); - auto decodeResult = LuteExePayload::decode(encodeResult->payload); + auto decodeResult = LuteExePayload::decode(encodeResult->payload, getReporter()); REQUIRE(decodeResult.has_value()); // Verify entry point @@ -102,32 +103,32 @@ TEST_CASE("lutepayload_multiple_files_roundtrip") } } -TEST_CASE("lutepayload_invalid_magic_flag") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_invalid_magic_flag") { // Create a payload with invalid magic flag std::string invalidPayload = "INVALID_"; invalidPayload.append(100, 'X'); // Add some data - auto decodeResult = LuteExePayload::decode(invalidPayload); + auto decodeResult = LuteExePayload::decode(invalidPayload, getReporter()); CHECK(!decodeResult.has_value()); } -TEST_CASE("lutepayload_too_small_payload") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_too_small_payload") { // Payload smaller than minimum size std::string tinyPayload = "TINY"; - auto decodeResult = LuteExePayload::decode(tinyPayload); + auto decodeResult = LuteExePayload::decode(tinyPayload, getReporter()); CHECK(!decodeResult.has_value()); } -TEST_CASE("lutepayload_corrupted_metadata") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_corrupted_metadata") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); // Create valid payload - LuteExePayload originalPayload; + LuteExePayload originalPayload{getReporter()}; originalPayload.add(testFilePath); auto encodeResult = originalPayload.encode(); REQUIRE(encodeResult.has_value()); @@ -142,7 +143,7 @@ TEST_CASE("lutepayload_corrupted_metadata") } // Attempt to decode corrupted payload - auto decodeResult = LuteExePayload::decode(corruptedPayload); + auto decodeResult = LuteExePayload::decode(corruptedPayload, getReporter()); // Should either fail or produce different results if (decodeResult.has_value()) { @@ -151,7 +152,7 @@ TEST_CASE("lutepayload_corrupted_metadata") } } -TEST_CASE("lutepayload_entry_point_is_first_added") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_entry_point_is_first_added") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); @@ -159,7 +160,7 @@ TEST_CASE("lutepayload_entry_point_is_first_added") std::string firstFile = joinPaths(testDir, "main.luau"); std::string secondFile = joinPaths(testDir, "utils.luau"); - LuteExePayload payload; + LuteExePayload payload{getReporter()}; payload.add(firstFile); payload.add(secondFile); @@ -167,12 +168,12 @@ TEST_CASE("lutepayload_entry_point_is_first_added") CHECK(payload.entryPointPath == firstFile); } -TEST_CASE("lutepayload_nonexistent_file") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_nonexistent_file") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string nonExistentFile = joinPaths(luteProjectRoot, "tests/src/this_file_does_not_exist.luau"); - LuteExePayload payload; + LuteExePayload payload{getReporter()}; payload.add(nonExistentFile); // Encoding should fail because file doesn't exist @@ -180,10 +181,10 @@ TEST_CASE("lutepayload_nonexistent_file") CHECK(!encodeResult.has_value()); } -TEST_CASE("lutepayload_empty_payload") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_empty_payload") { // Create payload without adding any files - LuteExePayload emptyPayload; + LuteExePayload emptyPayload{getReporter()}; CHECK(emptyPayload.entryPointPath.empty()); // Encoding an empty payload should fail @@ -191,12 +192,12 @@ TEST_CASE("lutepayload_empty_payload") CHECK(!encodeResult.has_value()); } -TEST_CASE("lutepayload_compression_effectiveness") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_compression_effectiveness") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); - LuteExePayload payload; + LuteExePayload payload{getReporter()}; payload.add(testFilePath); auto encodeResult = payload.encode(); @@ -211,18 +212,18 @@ TEST_CASE("lutepayload_compression_effectiveness") CHECK(encodeResult->bytesWritten >= encodeResult->compressedPayloadSizeBytes); } -TEST_CASE("lutepayload_bytecode_integrity") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_bytecode_integrity") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); // Create two separate payloads with the same file - LuteExePayload payload1; + LuteExePayload payload1{getReporter()}; payload1.add(testFilePath); auto encode1 = payload1.encode(); REQUIRE(encode1.has_value()); - LuteExePayload payload2; + LuteExePayload payload2{getReporter()}; payload2.add(testFilePath); auto encode2 = payload2.encode(); REQUIRE(encode2.has_value()); @@ -238,13 +239,13 @@ TEST_CASE("lutepayload_bytecode_integrity") CHECK(encode1->payload == encode2->payload); } -TEST_CASE("lutepayload_validates_numfiles_metadata") +TEST_CASE_FIXTURE(LuteFixture, "lutepayload_validates_numfiles_metadata") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); // Create and encode a valid payload - LuteExePayload payload; + LuteExePayload payload{getReporter()}; payload.add(testFilePath); auto encodeResult = payload.encode(); REQUIRE(encodeResult.has_value()); @@ -269,11 +270,11 @@ TEST_CASE("lutepayload_validates_numfiles_metadata") memcpy(corruptedPayload.data() + numFilesPos, &fakeNumFiles, sizeof(uint32_t)); // Decoding should fail due to numFiles mismatch - auto decodeResult = LuteExePayload::decode(corruptedPayload); + auto decodeResult = LuteExePayload::decode(corruptedPayload, getReporter()); CHECK(!decodeResult.has_value()); } -TEST_CASE("luteexecutable_single_file_roundtrip") +TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_single_file_roundtrip") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); @@ -290,12 +291,12 @@ TEST_CASE("luteexecutable_single_file_roundtrip") } // Create payload with a test file - LuteExePayload originalPayload; + LuteExePayload originalPayload{getReporter()}; originalPayload.add(testFilePath); // Create LuteExecutable and write it out std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe"); - LuteExecutable executable(dummyExePath); + LuteExecutable executable{dummyExePath, getReporter()}; bool createSuccess = executable.create(outputExePath, originalPayload); REQUIRE(createSuccess); @@ -306,7 +307,7 @@ TEST_CASE("luteexecutable_single_file_roundtrip") checkFile.close(); // Extract the payload from the created executable - LuteExecutable readExecutable(outputExePath); + LuteExecutable readExecutable{outputExePath, getReporter()}; auto extractedPayload = readExecutable.extract(); REQUIRE(extractedPayload.has_value()); @@ -328,7 +329,7 @@ TEST_CASE("luteexecutable_single_file_roundtrip") std::remove(outputExePath.c_str()); } -TEST_CASE("luteexecutable_multiple_files_roundtrip") +TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_multiple_files_roundtrip") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); @@ -348,7 +349,7 @@ TEST_CASE("luteexecutable_multiple_files_roundtrip") } // Create payload with multiple files - LuteExePayload originalPayload; + LuteExePayload originalPayload{getReporter()}; for (const auto& file : testFiles) { originalPayload.add(file); @@ -359,13 +360,13 @@ TEST_CASE("luteexecutable_multiple_files_roundtrip") // Create the executable std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe_multi"); - LuteExecutable executable(dummyExePath); + LuteExecutable executable{dummyExePath, getReporter()}; bool createSuccess = executable.create(outputExePath, originalPayload); REQUIRE(createSuccess); // Extract the payload - LuteExecutable readExecutable(outputExePath); + LuteExecutable readExecutable{outputExePath, getReporter()}; auto extractedPayload = readExecutable.extract(); REQUIRE(extractedPayload.has_value()); @@ -392,7 +393,7 @@ TEST_CASE("luteexecutable_multiple_files_roundtrip") std::remove(outputExePath.c_str()); } -TEST_CASE("luteexecutable_extract_from_plain_executable") +TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_extract_from_plain_executable") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); @@ -407,7 +408,7 @@ TEST_CASE("luteexecutable_extract_from_plain_executable") } // Attempt to extract - should return nullopt since there's no payload - LuteExecutable executable(plainExePath); + LuteExecutable executable{plainExePath, getReporter()}; auto extractedPayload = executable.extract(); CHECK(!extractedPayload.has_value()); @@ -415,7 +416,7 @@ TEST_CASE("luteexecutable_extract_from_plain_executable") std::remove(plainExePath.c_str()); } -TEST_CASE("luteexecutable_extract_preserves_original_executable") +TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_extract_preserves_original_executable") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); @@ -431,11 +432,11 @@ TEST_CASE("luteexecutable_extract_preserves_original_executable") } // Create payload and executable - LuteExePayload payload; + LuteExePayload payload{getReporter()}; payload.add(testFilePath); std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe_preserve"); - LuteExecutable executable(dummyExePath); + LuteExecutable executable{dummyExePath, getReporter()}; bool createSuccess = executable.create(outputExePath, payload); REQUIRE(createSuccess); @@ -455,7 +456,7 @@ TEST_CASE("luteexecutable_extract_preserves_original_executable") std::remove(outputExePath.c_str()); } -TEST_CASE("compile_command_e2e") +TEST_CASE_FIXTURE(LuteFixture, "compile_command_e2e") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); @@ -476,7 +477,7 @@ TEST_CASE("compile_command_e2e") std::vector argv = {executablePlaceholder, compileCommand, testFilePath.data(), outputFlag, outputExePath.data()}; // Run the compile command - int compileResult = cliMain(argv.size(), argv.data()); + int compileResult = cliMain(argv.size(), argv.data(), getReporter()); REQUIRE(compileResult == 0); // Verify the output file was created @@ -486,7 +487,7 @@ TEST_CASE("compile_command_e2e") // Now run the compiled executable to verify it works std::vector runArgv = {outputExePath.data()}; - int runResult = cliMain(runArgv.size(), runArgv.data()); + int runResult = cliMain(runArgv.size(), runArgv.data(), getReporter()); CHECK(runResult == 0); // Clean up diff --git a/tests/src/lutefixture.cpp b/tests/src/lutefixture.cpp new file mode 100644 index 000000000..3bc95b9dc --- /dev/null +++ b/tests/src/lutefixture.cpp @@ -0,0 +1,11 @@ +#include "lutefixture.h" + +LuteFixture::LuteFixture() + : reporter(std::make_unique()) +{ +} + +TestReporter& LuteFixture::getReporter() +{ + return *reporter; +} diff --git a/tests/src/lutefixture.h b/tests/src/lutefixture.h new file mode 100644 index 000000000..ec7adedad --- /dev/null +++ b/tests/src/lutefixture.h @@ -0,0 +1,17 @@ +#pragma once + +#include "testreporter.h" + +#include + +// Base fixture class for all Lute tests that need a reporter +class LuteFixture +{ +public: + LuteFixture(); + virtual ~LuteFixture() = default; + + TestReporter& getReporter(); + + std::unique_ptr reporter; +}; diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index 0c857ee5b..8f54c7a96 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -6,17 +6,19 @@ #include "cliruntimefixture.h" #include "doctest.h" +#include "lutefixture.h" #include "luteprojectroot.h" + TEST_CASE_FIXTURE(CliRuntimeFixture, "require_exists") { lua_getglobal(L, "require"); CHECK(!lua_isnil(L, -1)); } -TEST_CASE("require_modules") +TEST_CASE_FIXTURE(LuteFixture, "require_modules") { - auto doPassingSubcase = [](std::vector argv, std::string requirePath, std::vector expectedResults) + auto doPassingSubcase = [&](std::vector argv, std::string requirePath, std::vector expectedResults) { std::string pass = "pass"; argv.push_back(pass.data()); @@ -25,10 +27,10 @@ TEST_CASE("require_modules") { argv.push_back(result.data()); } - CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); }; - auto doFailingSubcase = [](std::vector argv, std::string requirePath, std::vector expectedResults) + auto doFailingSubcase = [&](std::vector argv, std::string requirePath, std::vector expectedResults) { std::string fail = "fail"; argv.push_back(fail.data()); @@ -37,7 +39,7 @@ TEST_CASE("require_modules") { argv.push_back(result.data()); } - CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); }; char executablePlaceholder[] = "lute"; @@ -161,7 +163,7 @@ TEST_CASE("require_modules") } } -TEST_CASE("require_with_parent_ambiguity") +TEST_CASE_FIXTURE(LuteFixture, "require_with_parent_ambiguity") { // This test case cannot be included in the general "require_modules" test // because ambiguity prevents the test's requirer.luau from navigating to @@ -174,7 +176,7 @@ TEST_CASE("require_with_parent_ambiguity") { std::string requirer = joinPaths(luteProjectRoot, "tests/src/require/config_tests/with_config/src/parent_ambiguity/folder/requirer.luau"); std::vector argv = {executablePlaceholder, requirer.data()}; - CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); } // .config.luau @@ -183,11 +185,11 @@ TEST_CASE("require_with_parent_ambiguity") std::string requirer = joinPaths(luteProjectRoot, "tests/src/require/config_tests/with_config_luau/src/parent_ambiguity/folder/requirer.luau"); std::vector argv = {executablePlaceholder, requirer.data()}; - CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); } } -TEST_CASE("require_types") +TEST_CASE_FIXTURE(LuteFixture, "require_types") { char executablePlaceholder[] = "lute"; for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) @@ -195,11 +197,11 @@ TEST_CASE("require_types") std::string requirer = joinPaths(luteProjectRoot, "tests/src/require/without_config/types/tester.luau"); std::vector argv = {executablePlaceholder, requirer.data()}; - CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); } } -TEST_CASE("require_by_string_semantics_in_cli") +TEST_CASE_FIXTURE(LuteFixture, "require_by_string_semantics_in_cli") { char executablePlaceholder[] = "lute"; @@ -216,7 +218,7 @@ TEST_CASE("require_by_string_semantics_in_cli") for (std::string& inputPath : inputPaths) { std::vector argv = {executablePlaceholder, inputPath.data()}; - CHECK_EQ(cliMain(argv.size(), argv.data()), 0); + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); } } @@ -225,6 +227,6 @@ TEST_CASE("require_by_string_semantics_in_cli") { std::string inputPath = joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/init"); std::vector argv = {executablePlaceholder, inputPath.data()}; - CHECK_NE(cliMain(argv.size(), argv.data()), 0); + CHECK_NE(cliMain(argv.size(), argv.data(), getReporter()), 0); } } diff --git a/tests/src/staticrequires.test.cpp b/tests/src/staticrequires.test.cpp index da0609b8e..e55b1aaa4 100644 --- a/tests/src/staticrequires.test.cpp +++ b/tests/src/staticrequires.test.cpp @@ -7,14 +7,15 @@ #include #include "doctest.h" +#include "lutefixture.h" #include "luteprojectroot.h" -TEST_CASE("staticrequiretracer_simple_dependencies") +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_simple_dependencies") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); - StaticRequireTracer tracer; + StaticRequireTracer tracer{getReporter()}; std::vector deps = tracer.trace(testDir, "main.luau"); // Should find: main.luau, utils.luau, lib/helper.luau, shared.luau @@ -33,12 +34,12 @@ TEST_CASE("staticrequiretracer_simple_dependencies") } } -TEST_CASE("staticrequiretracer_circular_dependencies") +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_circular_dependencies") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); - StaticRequireTracer tracer; + StaticRequireTracer tracer{getReporter()}; std::vector deps = tracer.trace(testDir, "circular_a.luau"); // Should find both circular_a and circular_b without infinite loop @@ -55,12 +56,12 @@ TEST_CASE("staticrequiretracer_circular_dependencies") CHECK(hasB); } -TEST_CASE("staticrequiretracer_no_dependencies") +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_no_dependencies") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); - StaticRequireTracer tracer; + StaticRequireTracer tracer{getReporter()}; std::vector deps = tracer.trace(testDir, "utils.luau"); // utils.luau has no requires, should only return itself @@ -68,12 +69,12 @@ TEST_CASE("staticrequiretracer_no_dependencies") CHECK(deps[0] == "utils.luau"); } -TEST_CASE("staticrequiretracer_relative_paths") +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_relative_paths") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); - StaticRequireTracer tracer; + StaticRequireTracer tracer{getReporter()}; std::vector deps = tracer.trace(testDir, "lib/helper.luau"); // helper.luau requires ../shared, should resolve correctly @@ -85,12 +86,12 @@ TEST_CASE("staticrequiretracer_relative_paths") CHECK(hasShared); } -TEST_CASE("staticrequiretracer_require_graph") +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_require_graph") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); - StaticRequireTracer tracer; + StaticRequireTracer tracer{getReporter()}; std::vector deps = tracer.trace(testDir, "main.luau"); const auto& graph = tracer.getRequireGraph(); diff --git a/tests/src/stdsystem.test.cpp b/tests/src/stdsystem.test.cpp index 13fb205c8..2787720b3 100644 --- a/tests/src/stdsystem.test.cpp +++ b/tests/src/stdsystem.test.cpp @@ -1,3 +1,5 @@ +#include "Luau/StringUtils.h" + #include #include "cliruntimefixture.h" @@ -20,24 +22,27 @@ TEST_CASE_FIXTURE(CliRuntimeFixture, "std_system_os_matches_host_os") { runCode(R"( local system = require("@std/system") - capture(system.os) + report(system.os) )"); - CHECK(getCapturedOutput() == getHostOS()); + CHECK(getReporter().getOutputs()[0] == getHostOS()); } TEST_CASE_FIXTURE(CliRuntimeFixture, "check_std_system_env_bools") { std::string os = getHostOS(); - auto checkBool = [&](const std::string& field, bool expected) + auto checkBool = [this](const std::string& field, bool expected) { - runCode( - "local system = require(\"@std/system\")\n" - "capture(system." + - field + ")\n" + std::string code = Luau::format( + R"( + local system = require("@std/system") + report(system.%s))", + field.c_str() ); - std::string output = getCapturedOutput(); + runCode(code); + std::string output = this->getReporter().getOutputs()[0]; CHECK(output == (expected ? "true" : "false")); + this->getReporter().clear(); }; checkBool("win32", os == "Windows_NT"); diff --git a/tests/src/testreporter.cpp b/tests/src/testreporter.cpp new file mode 100644 index 000000000..b4d876011 --- /dev/null +++ b/tests/src/testreporter.cpp @@ -0,0 +1,27 @@ +#include "testreporter.h" + +void TestReporter::reportError(const std::string& message) +{ + errors.push_back(message); +} + +void TestReporter::reportOutput(const std::string& message) +{ + outputs.push_back(message); +} + +const std::vector& TestReporter::getErrors() const +{ + return errors; +} + +const std::vector& TestReporter::getOutputs() const +{ + return outputs; +} + +void TestReporter::clear() +{ + errors.clear(); + outputs.clear(); +} diff --git a/tests/src/testreporter.h b/tests/src/testreporter.h new file mode 100644 index 000000000..5e768e76c --- /dev/null +++ b/tests/src/testreporter.h @@ -0,0 +1,22 @@ +#pragma once + +#include "lute/reporter.h" +#include +#include + +// Test reporter that captures output in vectors for verification +class TestReporter : public LuteReporter +{ +public: + void reportError(const std::string& message) override; + void reportOutput(const std::string& message) override; + + const std::vector& getErrors() const; + const std::vector& getOutputs() const; + + void clear(); + +private: + std::vector errors; + std::vector outputs; +}; From 96461b78c7c27f3470db2e770c9d81f981db26bb Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 10 Nov 2025 08:50:06 -0800 Subject: [PATCH 136/642] Remove infinite loop test for .config.luau (#548) We already have a test for this in the Luau repository, and it's just causing issues for luau-lsp here. If there ever is a bug with this functionality, it needs to be fixed in the Luau repository anyway, so I think it's okay to remove from here. I was considering replacing it with a test that created the files in memory, but it seems like too much hassle for not much benefit. Resolves https://github.com/JohnnyMorganz/luau-lsp/issues/1246 on our end. --- tests/src/require.test.cpp | 7 ------- .../require/config_tests/config_luau_timeout/.config.luau | 2 -- .../require/config_tests/config_luau_timeout/requirer.luau | 1 - 3 files changed, 10 deletions(-) delete mode 100644 tests/src/require/config_tests/config_luau_timeout/.config.luau delete mode 100644 tests/src/require/config_tests/config_luau_timeout/requirer.luau diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index 8f54c7a96..49b544575 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -144,13 +144,6 @@ TEST_CASE_FIXTURE(LuteFixture, "require_modules") ); } - SUBCASE("config_luau_timeout") - { - doFailingSubcase( - argv, {"./config_tests/config_luau_timeout/requirer"}, {R"(error requiring module "@dep": configuration execution timed out)"} - ); - } - SUBCASE("lute_modules") { doPassingSubcase(argv, {"./lute/lute"}, {"successfully required @lute modules"}); diff --git a/tests/src/require/config_tests/config_luau_timeout/.config.luau b/tests/src/require/config_tests/config_luau_timeout/.config.luau deleted file mode 100644 index 9c128d7e1..000000000 --- a/tests/src/require/config_tests/config_luau_timeout/.config.luau +++ /dev/null @@ -1,2 +0,0 @@ --- Infinite loop -while true do end diff --git a/tests/src/require/config_tests/config_luau_timeout/requirer.luau b/tests/src/require/config_tests/config_luau_timeout/requirer.luau deleted file mode 100644 index 4375a7835..000000000 --- a/tests/src/require/config_tests/config_luau_timeout/requirer.luau +++ /dev/null @@ -1 +0,0 @@ -return require("@dep") From 8412e5c98e12a4833adb2e3414cfd4adfe2bcc54 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:03:06 -0800 Subject: [PATCH 137/642] site: use vitepress-sidebar to generate sidebar (#549) instead of needing to manually add and change the doc site paths when our files change, we use `vitepress-sidebar` to automatically generate the doc pages as well resolves https://github.com/luau-lang/lute/issues/547 --- docs/.vitepress/config.mts | 145 +--- docs/package-lock.json | 1323 +++++++++++++++++++++++++++--------- docs/package.json | 3 + 3 files changed, 1023 insertions(+), 448 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 1278d214a..8982a6b5f 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,117 +1,36 @@ import { defineConfig } from 'vitepress' +import { withSidebar } from 'vitepress-sidebar' -// https://vitepress.dev/reference/site-config -export default defineConfig({ - title: "Lute", - description: "Luau for General-Purpose Programming", - base: "/", - themeConfig: { - // https://vitepress.dev/reference/default-theme-config - nav: [ - { text: 'Guide', link: '/guide/installation' }, - { text: 'Reference', link: '/reference/definitions/crypto' } - ], - - sidebar: [ - { - text: "Getting Started", - items: [ - { text: 'Installation', link: '/guide/installation' }, - ] - }, - { - text: "Reference", - items: [ - // Definitions - { text: 'crypto', link: '/reference/definitions/crypto' }, - { text: 'fs', link: '/reference/definitions/fs' }, - { text: 'io', link: '/reference/definitions/io' }, - { text: 'luau', link: '/reference/definitions/luau' }, - { text: 'net', link: '/reference/definitions/net' }, - { text: 'process', link: '/reference/definitions/process' }, - { text: 'system', link: '/reference/definitions/system' }, - { text: 'task', link: '/reference/definitions/task' }, - { text: 'time', link: '/reference/definitions/time' }, - { text: 'vm', link: '/reference/definitions/vm' }, - - // Standard Library subfolder - { - text: "Standard Library", - items: [ - { text: 'assert', link: '/reference/std/assert' }, - { text: 'fs', link: '/reference/std/fs' }, - { text: 'io', link: '/reference/std/io' }, - { text: 'json', link: '/reference/std/json' }, - { text: 'luau', link: '/reference/std/luau' }, - { text: 'process', link: '/reference/std/process' }, - { text: 'stringext', link: '/reference/std/stringext' }, - { text: 'tableext', link: '/reference/std/tableext' }, - { text: 'task', link: '/reference/std/task' }, - { text: 'test', link: '/reference/std/test' }, - { text: 'vectorext', link: '/reference/std/vectorext' }, - - // Path lib subfolder - { - text: "Path", - items: [ - { text: 'path', link: '/reference/std/path/path' }, - { text: 'types', link: '/reference/std/path/types' }, - // Posix lib subfolder - { - text: "Posix", - items: [ - { text: 'posix', link: '/reference/std/path/posix/posix' }, - { text: 'types', link: '/reference/std/path/posix/types' }, - ] - }, - // Win32 lib subfolder - { - text: "Win32", - items: [ - { text: 'win32', link: '/reference/std/path/win32/win32' }, - { text: 'types', link: '/reference/std/path/win32/types' }, - ] - }, - ] - }, - // Syntax lib subfolder - { - text: "Syntax", - items: [ - { text: 'parser', link: '/reference/std/syntax/parser' }, - { text: 'printer', link: '/reference/std/syntax/printer' }, - { text: 'query', link: '/reference/std/syntax/query' }, - { text: 'visitor', link: '/reference/std/syntax/visitor' }, - ] - }, - // System lib subfolder - { - text: "System", - items: [ - { text: 'system', link: '/reference/std/system/system' }, - { text: 'platform', link: '/reference/std/system/platform' }, - ] - }, - // Time lib subfolder - { - text: "Time", - items: [ - { text: 'time', link: '/reference/std/time/time' }, - { text: 'duration', link: '/reference/std/time/duration' }, - ] - } - ] - } - ] - } - ], - - search: { - provider: 'local' +export default withSidebar( + defineConfig({ + title: 'Lute', + description: 'Luau for General-Purpose Programming', + base: "/", + themeConfig: { + nav: [ + { text: 'Guide', link: '/guide/installation' }, + { text: 'Reference', link: '/reference/' }, + ], + search: { provider: 'local' }, + socialLinks: [ + { icon: 'github', link: 'https://github.com/luau-lang/lute' }, + ], }, - - socialLinks: [ - { icon: 'github', link: 'https://github.com/luau-lang/lute' } - ] + }), + { + // ============ [ SIDEBAR OPTIONS ] ============ + // ============ [ RESOLVING PATHS ] ============ + documentRootPath: './', + scanStartPath: './reference/', + // ============ [ GROUPING ] ============ + // collapsed: true, // Collapse subgroups by default + // ============ [ GETTING MENU TITLE ] ============ + useTitleFromFileHeading: true, + useTitleFromFrontmatter: true, + // ============ [ STYLING MENU TITLE ] ============ + hyphenToSpace: true, + underscoreToSpace: true, + // ============ [ SORTING ] ============ + sortMenusByName: true, } -}) +) diff --git a/docs/package-lock.json b/docs/package-lock.json index d5b18208c..5555a0bc1 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -4,10 +4,29 @@ "requires": true, "packages": { "": { + "dependencies": { + "vitepress-sidebar": "^1.33.0" + }, "devDependencies": { "vitepress": "^1.6.3" } }, + "node_modules/@algolia/abtesting": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.9.0.tgz", + "integrity": "sha512-4q9QCxFPiDIx1n5w41A1JMkrXI8p0ugCQnCGFtCKZPmWtwgWCqwVRncIbp++81xSELFZVQUfiB7Kbsla1tIBSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@algolia/autocomplete-core": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", @@ -58,41 +77,41 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.23.4.tgz", - "integrity": "sha512-WIMT2Kxy+FFWXWQxIU8QgbTioL+SGE24zhpj0kipG4uQbzXwONaWt7ffaYLjfge3gcGSgJVv+1VlahVckafluQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.43.0.tgz", + "integrity": "sha512-YsKYkohIMxiYEAu8nppZi5EioYDUIo9Heoor8K8vMUnkUtGCOEU/Q4p5OWaYSSBx3evo09Ga9rG4jsKViIcDzQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.23.4.tgz", - "integrity": "sha512-4B9gChENsQA9kFmFlb+x3YhBz2Gx3vSsm81FHI1yJ3fn2zlxREHmfrjyqYoMunsU7BybT/o5Nb7ccCbm/vfseA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.43.0.tgz", + "integrity": "sha512-kDGJWt3nzf0nu5RPFXQhNGl6Q0cn35fazxVWXhd0Fw3Vo6gcVfrcezcBenHb66laxnVJ7uwr1uKhmsu3Wy25sQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.23.4.tgz", - "integrity": "sha512-bsj0lwU2ytiWLtl7sPunr+oLe+0YJql9FozJln5BnIiqfKOaseSDdV42060vUy+D4373f2XBI009K/rm2IXYMA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.43.0.tgz", + "integrity": "sha512-RAFipkAnI8xhL/Sgi/gpXgNWN5HDM6F7z4NNNOcI8ZMYysZEBsqVXojg/WdKEKkQCOHVTZ3mooIjc5BaQdyVtA==", "dev": true, "license": "MIT", "engines": { @@ -100,160 +119,160 @@ } }, "node_modules/@algolia/client-insights": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.23.4.tgz", - "integrity": "sha512-XSCtAYvJ/hnfDHfRVMbBH0dayR+2ofVZy3jf5qyifjguC6rwxDsSdQvXpT0QFVyG+h8UPGtDhMPoUIng4wIcZA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.43.0.tgz", + "integrity": "sha512-PmVs83THco8Qig3cAjU9a5eAGaSxsfgh7PdmWMQFE/MCmIcLPv0MVpgfcGGyPjZGYvPC4cg+3q7JJxcNSsEaTg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.23.4.tgz", - "integrity": "sha512-l/0QvqgRFFOf7BnKSJ3myd1WbDr86ftVaa3PQwlsNh7IpIHmvVcT83Bi5zlORozVGMwaKfyPZo6O48PZELsOeA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.43.0.tgz", + "integrity": "sha512-Bs4zMLXvkAr19FSOZWNizlNUpRFxZVxtvyEJ+q3n3+hPZUcKjo0LIh15qghhRcQPEihjBN6Gr/U+AqRfOCsvnA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.23.4.tgz", - "integrity": "sha512-TB0htrDgVacVGtPDyENoM6VIeYqR+pMsDovW94dfi2JoaRxfqu/tYmLpvgWcOknP6wLbr8bA+G7t/NiGksNAwQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.43.0.tgz", + "integrity": "sha512-pwHv+z8TZAKbwAWt9+v2gIqlqcCFiMdteTdgdPn2yOBRx4WUQdsIWAaG9GiV3by8jO51FuFQnTohhauuI63y3A==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.23.4.tgz", - "integrity": "sha512-uBGo6KwUP6z+u6HZWRui8UJClS7fgUIAiYd1prUqCbkzDiCngTOzxaJbEvrdkK0hGCQtnPDiuNhC5MhtVNN4Eg==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.43.0.tgz", + "integrity": "sha512-wKy6x6fKcnB1CsfeNNdGp4dzLzz04k8II3JLt6Sp81F8s57Ks3/K9qsysmL9SJa8P486s719bBttVLE8JJYurQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.23.4", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.23.4.tgz", - "integrity": "sha512-Si6rFuGnSeEUPU9QchYvbknvEIyCRK7nkeaPVQdZpABU7m4V/tsiWdHmjVodtx3h20VZivJdHeQO9XbHxBOcCw==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.43.0.tgz", + "integrity": "sha512-TA21h2KwqCUyPXhSAWF3R2UES/FAnzjaVPDI6cRPXeadX+pdrGN0GWat5gSUATJVcMHECn+lGvuMMRxO86o2Pg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.23.4", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.23.4.tgz", - "integrity": "sha512-EXGoVVTshraqPJgr5cMd1fq7Jm71Ew6MpGCEaxI5PErBpJAmKdtjRIzs6JOGKHRaWLi+jdbJPYc2y8RN4qcx5Q==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.43.0.tgz", + "integrity": "sha512-rvWVEiA1iLcFmHS3oIXGIBreHIxNZqEFDjiNyRtLEffgd62kul2DjXM7H5bOouDMTo1ywMWT9OeQnzrhlTGAwA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.23.4.tgz", - "integrity": "sha512-1t6glwKVCkjvBNlng2itTf8fwaLSqkL4JaMENgR3WTGR8mmW2akocUy/ZYSQcG4TcR7qu4zW2UMGAwLoWoflgQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.43.0.tgz", + "integrity": "sha512-scCijGd38npvH2uHbYhO4f1SR8It5R2FZqOjNcMfw/7Ph7Hxvl+cd7Mo6RzIxsNRcLW5RrwjtpTK3gpDe8r/WQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "@algolia/client-common": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.23.4.tgz", - "integrity": "sha512-UUuizcgc5+VSY8hqzDFVdJ3Wcto03lpbFRGPgW12pHTlUQHUTADtIpIhkLLOZRCjXmCVhtr97Z+eR6LcRYXa3Q==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.43.0.tgz", + "integrity": "sha512-jMkRLWJYr4Hcmpl89e4vIWs69Mkf8Uwx7MG5ZKk2UxW3G3TmouGjI0Ph5mVPmg3Jf1UG3AdmVDc4XupzycT1Jw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4" + "@algolia/client-common": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.23.4.tgz", - "integrity": "sha512-UhDg6elsek6NnV5z4VG1qMwR6vbp+rTMBEnl/v4hUyXQazU+CNdYkl++cpdmLwGI/7nXc28xtZiL90Es3I7viQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.43.0.tgz", + "integrity": "sha512-KyQiVz+HdYtissC0J9KIGhHhKytQyJX+82GVsbv5rSCXbETnAoojvUyCn+3KRtWUvMDYCsZ+Y7hM71STTUJUJg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4" + "@algolia/client-common": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.23.4.tgz", - "integrity": "sha512-jXGzGBRUS0oywQwnaCA6mMDJO7LoC3dYSLsyNfIqxDR4SNGLhtg3je0Y31lc24OA4nYyKAYgVLtjfrpcpsWShg==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.43.0.tgz", + "integrity": "sha512-UnUBNY0U+oT0bkYDsEqVsCkErC2w7idk4CRiLSzicqY8tGylD9oP0j13X/fse1CuiAFCCr3jfl+cBlN6dC0OFw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.23.4" + "@algolia/client-common": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -261,9 +280,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -271,13 +290,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -287,14 +306,14 @@ } }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -743,9 +762,9 @@ } }, "node_modules/@iconify-json/simple-icons": { - "version": "1.2.33", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.33.tgz", - "integrity": "sha512-nL5/UmI9x5PQ/AHv6bOaL2pH6twEdEz4pI89efB/K7HFn5etQnxMtGx9DFlOg/sRA2/yFpX8KXvc95CSDv5bJA==", + "version": "1.2.58", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.58.tgz", + "integrity": "sha512-XtXEoRALqztdNc9ujYBj2tTCPKdIPKJBdLNDebFF46VV1aOAwTbAYMgNsK5GMCpTJupLCmpBWDn+gX5SpECorQ==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -759,17 +778,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", - "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], @@ -781,9 +827,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", - "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], @@ -795,9 +841,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", - "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], @@ -809,9 +855,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", - "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], @@ -823,9 +869,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", - "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", "cpu": [ "arm64" ], @@ -837,9 +883,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", - "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", "cpu": [ "x64" ], @@ -851,9 +897,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", - "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], @@ -865,9 +911,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", - "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], @@ -879,9 +925,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", - "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], @@ -893,9 +939,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", - "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], @@ -906,10 +952,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", - "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", "cpu": [ "loong64" ], @@ -920,10 +966,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", - "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], @@ -935,9 +981,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", - "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", "cpu": [ "riscv64" ], @@ -949,9 +995,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", - "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], @@ -963,9 +1009,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", - "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], @@ -977,9 +1023,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", - "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], @@ -991,9 +1037,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", - "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], @@ -1004,10 +1050,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", - "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], @@ -1019,9 +1079,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", - "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], @@ -1032,10 +1092,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", - "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], @@ -1134,9 +1208,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -1207,9 +1281,9 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", - "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "dev": true, "license": "MIT", "engines": { @@ -1221,77 +1295,77 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz", + "integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.24", "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz", + "integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-core": "3.5.24", + "@vue/shared": "3.5.24" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz", + "integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.13", - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.24", + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24", "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.48", - "source-map-js": "^1.2.0" + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz", + "integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.24", + "@vue/shared": "3.5.24" } }, "node_modules/@vue/devtools-api": { - "version": "7.7.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.5.tgz", - "integrity": "sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==", + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz", + "integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.7.5" + "@vue/devtools-kit": "^7.7.7" } }, "node_modules/@vue/devtools-kit": { - "version": "7.7.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.5.tgz", - "integrity": "sha512-S9VAVJYVAe4RPx2JZb9ZTEi0lqTySz2CBeF0wHT5D3dkTLnT9yMMGegKNl4b2EIELwLSkcI9bl2qp0/jW+upqA==", + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz", + "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.7.5", + "@vue/devtools-shared": "^7.7.7", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -1301,9 +1375,9 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "7.7.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.5.tgz", - "integrity": "sha512-QBjG72RfpM0DKtpns2RZOxBltO226kOAls9e4Lri6YxS2gWTgL0H+wj1R2K76lxxIeOrqo4+2Ty6RQnzv+WSTQ==", + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz", + "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==", "dev": true, "license": "MIT", "dependencies": { @@ -1311,57 +1385,57 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz", + "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/shared": "3.5.13" + "@vue/shared": "3.5.24" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz", + "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/reactivity": "3.5.24", + "@vue/shared": "3.5.24" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz", + "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", + "@vue/reactivity": "3.5.24", + "@vue/runtime-core": "3.5.24", + "@vue/shared": "3.5.24", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz", + "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24" }, "peerDependencies": { - "vue": "3.5.13" + "vue": "3.5.24" } }, "node_modules/@vue/shared": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.24.tgz", + "integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==", "dev": true, "license": "MIT" }, @@ -1472,40 +1546,89 @@ } }, "node_modules/algoliasearch": { - "version": "5.23.4", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.23.4.tgz", - "integrity": "sha512-QzAKFHl3fm53s44VHrTdEo0TkpL3XVUYQpnZy1r6/EHvMAyIg+O4hwprzlsNmcCHTNyVcF2S13DAUn7XhkC6qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-abtesting": "5.23.4", - "@algolia/client-analytics": "5.23.4", - "@algolia/client-common": "5.23.4", - "@algolia/client-insights": "5.23.4", - "@algolia/client-personalization": "5.23.4", - "@algolia/client-query-suggestions": "5.23.4", - "@algolia/client-search": "5.23.4", - "@algolia/ingestion": "1.23.4", - "@algolia/monitoring": "1.23.4", - "@algolia/recommend": "5.23.4", - "@algolia/requester-browser-xhr": "5.23.4", - "@algolia/requester-fetch": "5.23.4", - "@algolia/requester-node-http": "5.23.4" + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.43.0.tgz", + "integrity": "sha512-hbkK41JsuGYhk+atBDxlcKxskjDCh3OOEDpdKZPtw+3zucBqhlojRG5e5KtCmByGyYvwZswVeaSWglgLn2fibg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.9.0", + "@algolia/client-abtesting": "5.43.0", + "@algolia/client-analytics": "5.43.0", + "@algolia/client-common": "5.43.0", + "@algolia/client-insights": "5.43.0", + "@algolia/client-personalization": "5.43.0", + "@algolia/client-query-suggestions": "5.43.0", + "@algolia/client-search": "5.43.0", + "@algolia/ingestion": "1.43.0", + "@algolia/monitoring": "1.43.0", + "@algolia/recommend": "5.43.0", + "@algolia/requester-browser-xhr": "5.43.0", + "@algolia/requester-fetch": "5.43.0", + "@algolia/requester-node-http": "5.43.0" }, "engines": { "node": ">= 14.0.0" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/birpc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz", - "integrity": "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz", + "integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -1539,6 +1662,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -1551,21 +1692,35 @@ } }, "node_modules/copy-anything": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", - "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", "dev": true, "license": "MIT", "dependencies": { - "is-what": "^4.1.8" + "is-what": "^5.2.0" }, "engines": { - "node": ">=12.13" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1597,6 +1752,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/emoji-regex-xs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", @@ -1656,6 +1823,19 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -1663,14 +1843,42 @@ "dev": true, "license": "MIT" }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/focus-trap": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", - "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", + "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "dev": true, "license": "MIT", "dependencies": { - "tabbable": "^6.2.0" + "tabbable": "^6.3.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/fsevents": { @@ -1688,6 +1896,41 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -1744,27 +1987,94 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-what": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", - "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.13" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/mark.js": { @@ -1890,10 +2200,34 @@ ], "license": "MIT" }, - "node_modules/minisearch": { + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz", - "integrity": "sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", "dev": true, "license": "MIT" }, @@ -1935,6 +2269,37 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -1950,9 +2315,9 @@ "license": "ISC" }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -1970,7 +2335,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1979,9 +2344,9 @@ } }, "node_modules/preact": { - "version": "10.26.5", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.5.tgz", - "integrity": "sha512-fmpDkgfGU6JYux9teDWLhj9mKN55tyepwYbxHgQuIxbWQzgFg5vk7Mrrtfx7xRxq798ynkY4DDDxZr235Kk+4w==", + "version": "10.27.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", + "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "dev": true, "license": "MIT", "funding": { @@ -1990,9 +2355,9 @@ } }, "node_modules/property-information": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", - "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "dev": true, "license": "MIT", "funding": { @@ -2000,6 +2365,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/qsu": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/qsu/-/qsu-1.10.3.tgz", + "integrity": "sha512-OoA9UClkVuWvfcOiQrXrqvcJU1x3v/Mx0A+zMTELw3zZ6TWDeGRVnObQuUusz5rksTYo3QcyXLi9zVQMLxg2cQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", @@ -2035,13 +2409,13 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", - "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2051,26 +2425,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.0", - "@rollup/rollup-android-arm64": "4.40.0", - "@rollup/rollup-darwin-arm64": "4.40.0", - "@rollup/rollup-darwin-x64": "4.40.0", - "@rollup/rollup-freebsd-arm64": "4.40.0", - "@rollup/rollup-freebsd-x64": "4.40.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", - "@rollup/rollup-linux-arm-musleabihf": "4.40.0", - "@rollup/rollup-linux-arm64-gnu": "4.40.0", - "@rollup/rollup-linux-arm64-musl": "4.40.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-musl": "4.40.0", - "@rollup/rollup-linux-s390x-gnu": "4.40.0", - "@rollup/rollup-linux-x64-gnu": "4.40.0", - "@rollup/rollup-linux-x64-musl": "4.40.0", - "@rollup/rollup-win32-arm64-msvc": "4.40.0", - "@rollup/rollup-win32-ia32-msvc": "4.40.0", - "@rollup/rollup-win32-x64-msvc": "4.40.0", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -2082,6 +2458,40 @@ "license": "MIT", "peer": true }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shiki": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", @@ -2099,6 +2509,18 @@ "@types/hast": "^3.0.4" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2130,6 +2552,71 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -2145,23 +2632,69 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/superjson": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", - "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz", + "integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==", "dev": true, "license": "MIT", "dependencies": { - "copy-anything": "^3.0.2" + "copy-anything": "^4" }, "engines": { "node": ">=16" } }, "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", "dev": true, "license": "MIT" }, @@ -2177,9 +2710,9 @@ } }, "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "dev": true, "license": "MIT", "dependencies": { @@ -2235,9 +2768,9 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2265,9 +2798,9 @@ } }, "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "dev": true, "license": "MIT", "dependencies": { @@ -2280,9 +2813,9 @@ } }, "node_modules/vite": { - "version": "5.4.18", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", - "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -2340,9 +2873,9 @@ } }, "node_modules/vitepress": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.3.tgz", - "integrity": "sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", "dev": true, "license": "MIT", "dependencies": { @@ -2381,18 +2914,32 @@ } } }, + "node_modules/vitepress-sidebar": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/vitepress-sidebar/-/vitepress-sidebar-1.33.0.tgz", + "integrity": "sha512-+z45vGG6OQRKNU7OMhmrp5y9dk9Y8loFZGE2LwzqloqA4p+ECP3/Ti6qrS8kB3N8Rol4v0Nf8HSnsgZEDU4HXQ==", + "license": "MIT", + "dependencies": { + "glob": "10.4.5", + "gray-matter": "4.0.3", + "qsu": "^1.10.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/vue": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz", + "integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-sfc": "3.5.13", - "@vue/runtime-dom": "3.5.13", - "@vue/server-renderer": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-sfc": "3.5.24", + "@vue/runtime-dom": "3.5.24", + "@vue/server-renderer": "3.5.24", + "@vue/shared": "3.5.24" }, "peerDependencies": { "typescript": "*" @@ -2403,6 +2950,112 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/docs/package.json b/docs/package.json index 03fe8a475..4764fb0e4 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,5 +7,8 @@ }, "devDependencies": { "vitepress": "^1.6.3" + }, + "dependencies": { + "vitepress-sidebar": "^1.33.0" } } From 3bb5806eec5a8b0644e419cf61518c59acae74a1 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:50:56 -0800 Subject: [PATCH 138/642] site: fix path in sidebar generation (#550) Sidebar generation is not resolving the paths correctly for the docs --- docs/.vitepress/config.mts | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 8982a6b5f..21aba7caa 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -19,9 +19,6 @@ export default withSidebar( }), { // ============ [ SIDEBAR OPTIONS ] ============ - // ============ [ RESOLVING PATHS ] ============ - documentRootPath: './', - scanStartPath: './reference/', // ============ [ GROUPING ] ============ // collapsed: true, // Collapse subgroups by default // ============ [ GETTING MENU TITLE ] ============ From c0c887474abf0b06ce4e0f3a39eb96796cd371a1 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 10 Nov 2025 13:08:05 -0800 Subject: [PATCH 139/642] refactor: StaticRequireTracer should be reworked to use the resolveRequire api (#527) This PR makes use of the resolverequire api exposed in https://github.com/luau-lang/lute/pull/496 to perform static require tracing. This will handle static resolution of aliases for free during compilation - a followup PR will actually handle loading bundled files when they are required via aliases. Because the `resolverequire` code operates in terms of absolute paths, all static require tracing is now done in terms of absolute paths as well. This has some knock on consequences for how creating Lute Payloads works, which requires me to refactor this in the same PR. Prior, the `LuteExePayload::add(file path)` method used to take a relative path to the source file being embedded, and all embedded files would be resolved relative to this path. This path was used as both a key in the bundle, as well as the filepath to the script. For example: ``` lute compile tests/staticrequires/src/main.luau ``` would embed: ``` tests/staticrequires/src/main.luau tests/staticrequires/src/utils.luau tests/staticrequires/src/deps/dep1.luau tests/staticrequires/src/deps/dep2.luau ``` However, static require tracing now strips off the longest common prefix of paths, in order to a) save some bytes and b) avoid exposing the directory structure any more than necessary. This means that the bundle will look like: ``` main.luau utils.luau deps/dep1.luau deps/dep2.luau ``` However, passing just these as keys is insufficient information for the `LuteExePayload` to be able to find and compile these scripts. That's why the `add` method now takes both a `bundleName` e.g. `main.luau` and a filepath, e.g. `/path/to/main.luau`. --- lute/cli/include/lute/compile.h | 6 +- lute/cli/include/lute/staticrequires.h | 34 +++-- lute/cli/src/climain.cpp | 60 +++----- lute/cli/src/compile.cpp | 35 +++-- lute/cli/src/staticrequires.cpp | 201 +++++++++++++------------ tests/src/compile.test.cpp | 29 ++-- tests/src/staticrequires.test.cpp | 198 +++++++++++++++++------- 7 files changed, 333 insertions(+), 230 deletions(-) diff --git a/lute/cli/include/lute/compile.h b/lute/cli/include/lute/compile.h index 7dc26f6e1..fb8ecefe6 100644 --- a/lute/cli/include/lute/compile.h +++ b/lute/cli/include/lute/compile.h @@ -1,8 +1,9 @@ #pragma once +#include "lute/reporter.h" + #include "Luau/DenseHash.h" #include "Luau/FileUtils.h" -#include "lute/reporter.h" #include @@ -38,7 +39,7 @@ struct LuteEncodeResult struct LuteExePayload { LuteExePayload(LuteReporter& reporter); - void add(const std::string& luauFilePath); + void add(const std::string& bundlePath, const std::string& sourcePath); std::optional encode(); static std::optional decode(const std::string_view binary, LuteReporter& reporter); @@ -50,6 +51,7 @@ struct LuteExePayload LuteReporter& reporter; bool parseFromDecompressedBundle(std::string_view decompressedBundle); std::vector filePaths; + Luau::DenseHashMap sourceToBundlePath{""}; }; struct LuteDecodeResult diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h index 2f2347e68..426db0f31 100644 --- a/lute/cli/include/lute/staticrequires.h +++ b/lute/cli/include/lute/staticrequires.h @@ -1,9 +1,9 @@ #pragma once -#include "Luau/DenseHash.h" - #include "lute/reporter.h" +#include "Luau/DenseHash.h" + #include #include #include @@ -14,31 +14,35 @@ class StaticRequireTracer StaticRequireTracer(LuteReporter& reporter); // Trace dependencies starting from an entry point file - // rootDirectory: Base directory for resolving all requires - // entryPoint: Path to entry point file (relative to rootDirectory) - // Returns list of all files in dependency order (entry point first) - std::vector trace(const std::string& rootDirectory, const std::string& entryPoint); - - // Get the require graph built during the last trace - // Maps each file to the list of files it requires - const Luau::DenseHashMap>& getRequireGraph() const + // entryPoint: absolute path to entry point file + void trace(const std::string& entryPoint); + + // Get discovered files as pairs of (bundlePath, absolutePath) + // bundlePath is the absolute path with the lowest common prefix stripped + std::vector> getStaticRequirePairs() const; + + // Check if an absolute path exists in the discovered files + bool containsAbsolute(const std::string& absolutePath) const; + + const std::string& getLowestCommonRoot() const { - return requireGraph; + return lowestCommonRoot; } void printRequireGraph() const; + // Find the lowest common root directory from a collection of absolute paths + static std::string findLowestCommonRoot(const std::vector& paths); private: Luau::DenseHashSet visited{""}; - std::vector discovered; - Luau::DenseHashMap> requireGraph{""}; - std::string rootDirectory; + std::vector discovered; // Absolute paths + Luau::DenseHashMap> requireGraph{""}; // Absolute paths + std::string lowestCommonRoot; // Extract all require() paths from source code std::vector extractRequires(const std::string& source); // Resolve a require path relative to the requiring file std::optional resolveRequire(const std::string& requirer, const std::string& required); - LuteReporter& reporter; }; diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 962c64b43..92025059c 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -485,19 +485,27 @@ int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& rep return 1; } - // Validate file exists - std::optional validPath = getValidPath(filePath); - if (!validPath) + std::string absoluteEntryPoint; + if (isAbsolutePath(filePath)) { - reporter.formatError("Error: File '%s' does not exist.", filePath.c_str()); - return 1; + absoluteEntryPoint = filePath; + } + else + { + std::optional cwd = getCurrentWorkingDirectory(); + if (!cwd) + { + reporter.reportError("Error: Failed to get current working directory.\n"); + return 1; + } + absoluteEntryPoint = normalizePath(joinPaths(*cwd, filePath)); } // Set default output path if not specified if (outputPath.empty()) { // Extract base name from input file (remove directory and extension) - std::string baseName = *validPath; + std::string baseName = filePath; // Remove directory path size_t lastSlash = baseName.find_last_of("/\\"); @@ -515,47 +523,25 @@ int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& rep #endif } - // Normalize paths to be relative to working directory - std::string normalizedEntry = normalizePath(*validPath); - - // Split into directory and filename for cleaner trace output - std::string rootDirectory; - std::string entryFilename; - - size_t lastSlash = normalizedEntry.find_last_of("/\\"); - if (lastSlash != std::string::npos) - { - rootDirectory = normalizedEntry.substr(0, lastSlash); - entryFilename = normalizedEntry.substr(lastSlash + 1); - } - else - { - rootDirectory = "."; - entryFilename = normalizedEntry; - } - // Perform static require trace StaticRequireTracer tracer{reporter}; - std::vector discoveredFiles = tracer.trace(rootDirectory, entryFilename); - - if (discoveredFiles.empty()) - { - reporter.reportError("Error: No files discovered during require trace."); - return 1; - } + tracer.trace(absoluteEntryPoint); if (showRequireGraph) tracer.printRequireGraph(); // Create payload and add all discovered files LuteExePayload payload{reporter}; - for (const auto& file : discoveredFiles) + auto staticRequirePairs = tracer.getStaticRequirePairs(); + // Add file with absolute path for reading and rooted path for bundle + // We don't want to leak your entire directory path in the bundle, so we + // try to pick the lowest common ancestor and keep that as the root. + for (const auto& [bundle, absolute] : staticRequirePairs) { - // Construct full path from root directory and relative file path - std::string fullPath = joinPaths(rootDirectory, file); - payload.add(fullPath); + payload.add(bundle, absolute); } + // Encode the payload reporter.reportOutput("Compiling and bundling bytecode..."); std::optional encodeResult = payload.encode(); @@ -569,7 +555,7 @@ int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& rep if (bundleStats) { reporter.reportOutput("\nBundle Statistics:"); - reporter.formatOutput("\tFiles bundled: %zu", discoveredFiles.size()); + reporter.formatOutput("\tFiles bundled: %zu", staticRequirePairs.size()); reporter.formatOutput("\tUncompressed size: %zu bytes", encodeResult->uncompressedPayloadSizeBytes); reporter.formatOutput("\tCompressed size: %zu bytes", encodeResult->compressedPayloadSizeBytes); reporter.formatOutput( diff --git a/lute/cli/src/compile.cpp b/lute/cli/src/compile.cpp index d45a4785c..7931d274d 100644 --- a/lute/cli/src/compile.cpp +++ b/lute/cli/src/compile.cpp @@ -23,13 +23,17 @@ LuteExePayload::LuteExePayload(LuteReporter& reporter) { } -void LuteExePayload::add(const std::string& luauFilePath) +void LuteExePayload::add(const std::string& bundlePath, const std::string& sourcePath) { // First file added becomes the entry point if (filePaths.empty()) - entryPointPath = luauFilePath; + entryPointPath = bundlePath; - filePaths.push_back(luauFilePath); + // Store the source path for reading files + filePaths.push_back(sourcePath); + + // Map source path to bundle path + sourceToBundlePath[sourcePath] = bundlePath; } std::optional LuteExePayload::encode() @@ -45,13 +49,17 @@ std::optional LuteExePayload::encode() // Step 1: Build uncompressed bytecode bundle // Format: For each file, append [path_len][path][bytecode_size][bytecode] std::string uncompressedBundle; - for (const auto& filePath : filePaths) + for (const auto& sourcePath : filePaths) { - // Read source file from disk - std::optional source = readFile(filePath); + // Get the bundle path (rooted path for the bundle) + const std::string* bundlePathPtr = sourceToBundlePath.find(sourcePath); + const std::string& bundlePath = bundlePathPtr ? *bundlePathPtr : sourcePath; + + // Read source file from disk using absolute source path + std::optional source = readFile(sourcePath); if (!source) { - reporter.formatError("Encode failed: Could not read file '%s'", filePath.c_str()); + reporter.formatError("Encode failed: Could not read file '%s'\n", sourcePath.c_str()); return std::nullopt; } @@ -59,18 +67,19 @@ std::optional LuteExePayload::encode() std::string bytecode = Luau::compile(*source, copts()); if (bytecode.empty()) { - reporter.formatError("Encode failed: Could not compile file '%s' to bytecode", filePath.c_str()); + reporter.formatError("Encode failed: Could not compile file '%s' to bytecode", sourcePath.c_str()); return std::nullopt; } - filePathToBytecode[filePath] = bytecode; + // Store bytecode with bundle path (rooted path) + filePathToBytecode[bundlePath] = bytecode; - // Append path_length field (uint32_t, 4 bytes) - uint32_t pathLength = static_cast(filePath.size()); + // Append path_length field (uint32_t, 4 bytes) - use bundle path + uint32_t pathLength = static_cast(bundlePath.size()); uncompressedBundle.append(reinterpret_cast(&pathLength), sizeof(uint32_t)); - // Append path_string field (variable length) - uncompressedBundle.append(filePath); + // Append path_string field (variable length) - use bundle path + uncompressedBundle.append(bundlePath); // Append bytecode_size field (uint64_t, 8 bytes) uint64_t bytecodeSize = bytecode.size(); diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index 9c2cf0545..a01101e56 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -1,12 +1,15 @@ #include "lute/staticrequires.h" #include "lute/modulepath.h" +#include "lute/resolverequire.h" +#include "lute/staticrequires.h" #include "Luau/Ast.h" #include "Luau/FileUtils.h" #include "Luau/Parser.h" #include "Luau/VecDeque.h" +#include #include // AST visitor to extract require() calls @@ -36,12 +39,17 @@ StaticRequireTracer::StaticRequireTracer(LuteReporter& reporter) { } -std::vector StaticRequireTracer::trace(const std::string& rootDirectory, const std::string& entryPoint) +void StaticRequireTracer::trace(const std::string& entryPoint) { visited.clear(); discovered.clear(); requireGraph.clear(); - this->rootDirectory = rootDirectory; + + if (!isAbsolutePath(entryPoint)) + { + fprintf(stderr, "Error: %s isn't an absolute path\n", entryPoint.c_str()); + return; + } Luau::VecDeque toProcess; toProcess.push_back(entryPoint); @@ -51,19 +59,15 @@ std::vector StaticRequireTracer::trace(const std::string& rootDirec std::string filePath = toProcess.front(); toProcess.pop_front(); - // Get normalized path for visited tracking - std::string fullPath = joinPaths(rootDirectory, filePath); - std::string absPath = normalizePath(fullPath); - // Skip if already visited (handles circular dependencies) - if (visited.contains(absPath)) + if (visited.contains(filePath)) continue; - visited.insert(absPath); - std::optional source = readFile(fullPath); + visited.insert(filePath); + std::optional source = readFile(filePath); if (!source) { - reporter.formatError("Warning: Could not read file '%s'\n", fullPath.c_str()); + reporter.formatError("Warning: Could not read file '%s'\n", filePath.c_str()); continue; } @@ -76,7 +80,14 @@ std::vector StaticRequireTracer::trace(const std::string& rootDirec for (const auto& req : requiresInFile) { - std::optional resolvedPath = resolveRequire(filePath, req); + // Skip warning for built-in libraries (@std and @lute) + // The new and improved requireResolver is really good - it'll even handle std/lute aliases that we've built in. + // For now, we can just explicitly skip these, since they are provided by the runtime + if (req.find("@std/") == 0 || req.find("@lute/") == 0) + continue; + std::string err = ""; + std::optional resolvedPath = ::resolveRequire(req, "@" + filePath, &err); + if (resolvedPath) { toProcess.push_back(*resolvedPath); @@ -87,9 +98,9 @@ std::vector StaticRequireTracer::trace(const std::string& rootDirec // Skip warning for built-in libraries (@std and @lute) bool isBuiltinLibrary = req.rfind("@std/", 0) == 0 || req.rfind("@lute/", 0) == 0; if (!isBuiltinLibrary) - { reporter.formatError("Warning: Could not resolve require('%s') from '%s'\n", req.c_str(), filePath.c_str()); - } + if (!err.empty()) + reporter.formatError("Warning: Could not resolve require('%s') from '%s':\n\t%s\n", req.c_str(), filePath.c_str(), err.c_str()); } } @@ -97,7 +108,7 @@ std::vector StaticRequireTracer::trace(const std::string& rootDirec requireGraph[filePath] = std::move(resolvedDeps); } - return discovered; + lowestCommonRoot = findLowestCommonRoot(discovered); } std::vector StaticRequireTracer::extractRequires(const std::string& source) @@ -121,109 +132,105 @@ std::vector StaticRequireTracer::extractRequires(const std::string& return extractor.requirePaths; } -std::optional StaticRequireTracer::resolveRequire(const std::string& requirer, const std::string& required) +std::vector> StaticRequireTracer::getStaticRequirePairs() const { - // Get the directory containing the requiring file (relative to rootDirectory) - std::string requirerDir; - size_t lastSlash = requirer.find_last_of("/\\"); - if (lastSlash != std::string::npos) - { - requirerDir = requirer.substr(0, lastSlash); - } - else - { - requirerDir = ""; - } + std::vector> pairs; + pairs.reserve(discovered.size()); - // Helper functions for ModulePath - paths are relative to rootDirectory - auto isFileFunc = [this](const std::string& path) -> bool - { - std::string fullPath = joinPaths(rootDirectory, path); - return isFile(fullPath); - }; + size_t commonRootLen = lowestCommonRoot.empty() ? 0 : lowestCommonRoot.length() + 1; // +1 for the trailing slash - auto isDir = [this](const std::string& path) -> bool + for (const auto& absolutePath : discovered) { - std::string fullPath = joinPaths(rootDirectory, path); - return isDirectory(fullPath); - }; - - // Create a ModulePath with root as rootDirectory, starting at requirer's directory - // This allows us to navigate up with .. beyond requirerDir, but not beyond rootDirectory - std::optional modulePath = ModulePath::create( - "", // Root is empty (relative path base) - requirerDir, // Start at the requirer's directory - isFileFunc, - isDir - ); - - if (!modulePath) - return std::nullopt; - - // Navigate according to the require path - // Split require path by '/' and navigate - std::string reqPath = required; - size_t pos = 0; - - while (pos < reqPath.size()) - { - size_t nextSlash = reqPath.find('/', pos); - std::string component; - - if (nextSlash == std::string::npos) - { - component = reqPath.substr(pos); - pos = reqPath.size(); - } + std::string bundlePath; + if (commonRootLen > 0 && absolutePath.length() > commonRootLen) + bundlePath = absolutePath.substr(commonRootLen); else - { - component = reqPath.substr(pos, nextSlash - pos); - pos = nextSlash + 1; - } + bundlePath = absolutePath; - if (component.empty() || component == ".") - continue; - - if (component == "..") - { - if (modulePath->toParent() != NavigationStatus::Success) - return std::nullopt; - } - else - { - if (modulePath->toChild(component) != NavigationStatus::Success) - return std::nullopt; - } + pairs.emplace_back(bundlePath, absolutePath); } - // Get the resolved path (relative to rootDirectory) - ResolvedRealPath resolved = modulePath->getRealPath(); - if (resolved.status != NavigationStatus::Success) - return std::nullopt; - - // Strip leading slash if present (occurs when root is empty) - std::string result = resolved.realPath; - if (!result.empty() && result[0] == '/') - result = result.substr(1); + return pairs; +} - return result; +bool StaticRequireTracer::containsAbsolute(const std::string& absolutePath) const +{ + return visited.contains(absolutePath); } void StaticRequireTracer::printRequireGraph() const { - printf("\nRequire dependency graph:\n"); + reporter.reportOutput("\nRequire dependency graph:"); + + size_t commonRootLen = lowestCommonRoot.empty() ? 0 : lowestCommonRoot.length() + 1; + for (const auto& [file, deps] : requireGraph) { - printf("\t%s\n", file.c_str()); + // Convert absolute path to bundle path for display + std::string displayFile; + if (commonRootLen > 0 && file.length() > commonRootLen) + displayFile = file.substr(commonRootLen); + else + displayFile = file; + + reporter.formatOutput("\t%s", displayFile.c_str()); for (const auto& dep : deps) { - printf("\t\t -> %s\n", dep.c_str()); - } + // Convert absolute path to bundle path for display + std::string displayDep; + if (commonRootLen > 0 && dep.length() > commonRootLen) + displayDep = dep.substr(commonRootLen); + else + displayDep = dep; + reporter.formatOutput("\t\t -> %s", displayDep.c_str()); + } if (deps.empty()) { - printf("\t\t(no dependencies)\n"); + reporter.reportOutput("\t\t(no dependencies)"); } } - printf("\n"); + reporter.reportOutput(""); +} + +std::string StaticRequireTracer::findLowestCommonRoot(const std::vector& paths) +{ + if (paths.empty()) + return ""; + + if (paths.size() == 1) + { + // For a single file, return its directory + size_t lastSlash = paths[0].find_last_of("/\\"); + if (lastSlash != std::string::npos) + return paths[0].substr(0, lastSlash); + return ""; + } + + // Sort the paths - the first and last will have the maximum difference + std::vector sortedPaths = paths; + std::sort(sortedPaths.begin(), sortedPaths.end()); + + const std::string& first = sortedPaths.front(); + const std::string& last = sortedPaths.back(); + + // Find common prefix between first and last + size_t i = 0; + while (i < first.length() && i < last.length() && first[i] == last[i]) + { + i++; + } + + // Back up to the last directory separator + std::string commonPrefix = first.substr(0, i); + size_t lastSlash = commonPrefix.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + // Special case: if the slash is at position 0, we're at the root directory + if (lastSlash == 0) + return "/"; + return commonPrefix.substr(0, lastSlash); + } + + return ""; } diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp index 0bbfaacdd..65fe79d23 100644 --- a/tests/src/compile.test.cpp +++ b/tests/src/compile.test.cpp @@ -1,6 +1,7 @@ -#include "lute/climain.h" #include "lute/compile.h" +#include "lute/climain.h" + #include "Luau/FileUtils.h" #include @@ -20,7 +21,7 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_single_file_roundtrip") // Create payload and add file LuteExePayload originalPayload{getReporter()}; - originalPayload.add(testFilePath); + originalPayload.add(testFilePath, testFilePath); // Encode auto encodeResult = originalPayload.encode(); @@ -71,7 +72,7 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_multiple_files_roundtrip") LuteExePayload originalPayload{getReporter()}; for (const auto& file : testFiles) { - originalPayload.add(file); + originalPayload.add(file, file); } // First file should be the entry point @@ -129,7 +130,7 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_corrupted_metadata") // Create valid payload LuteExePayload originalPayload{getReporter()}; - originalPayload.add(testFilePath); + originalPayload.add(testFilePath, testFilePath); auto encodeResult = originalPayload.encode(); REQUIRE(encodeResult.has_value()); @@ -161,8 +162,8 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_entry_point_is_first_added") std::string secondFile = joinPaths(testDir, "utils.luau"); LuteExePayload payload{getReporter()}; - payload.add(firstFile); - payload.add(secondFile); + payload.add(firstFile, firstFile); + payload.add(secondFile, secondFile); // Entry point should be the first file added CHECK(payload.entryPointPath == firstFile); @@ -174,7 +175,7 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_nonexistent_file") std::string nonExistentFile = joinPaths(luteProjectRoot, "tests/src/this_file_does_not_exist.luau"); LuteExePayload payload{getReporter()}; - payload.add(nonExistentFile); + payload.add(nonExistentFile, nonExistentFile); // Encoding should fail because file doesn't exist auto encodeResult = payload.encode(); @@ -198,7 +199,7 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_compression_effectiveness") std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/main.luau"); LuteExePayload payload{getReporter()}; - payload.add(testFilePath); + payload.add(testFilePath, testFilePath); auto encodeResult = payload.encode(); REQUIRE(encodeResult.has_value()); @@ -219,12 +220,12 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_bytecode_integrity") // Create two separate payloads with the same file LuteExePayload payload1{getReporter()}; - payload1.add(testFilePath); + payload1.add(testFilePath, testFilePath); auto encode1 = payload1.encode(); REQUIRE(encode1.has_value()); LuteExePayload payload2{getReporter()}; - payload2.add(testFilePath); + payload2.add(testFilePath, testFilePath); auto encode2 = payload2.encode(); REQUIRE(encode2.has_value()); @@ -246,7 +247,7 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_validates_numfiles_metadata") // Create and encode a valid payload LuteExePayload payload{getReporter()}; - payload.add(testFilePath); + payload.add(testFilePath, testFilePath); auto encodeResult = payload.encode(); REQUIRE(encodeResult.has_value()); @@ -292,7 +293,7 @@ TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_single_file_roundtrip") // Create payload with a test file LuteExePayload originalPayload{getReporter()}; - originalPayload.add(testFilePath); + originalPayload.add(testFilePath, testFilePath); // Create LuteExecutable and write it out std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe"); @@ -352,7 +353,7 @@ TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_multiple_files_roundtrip") LuteExePayload originalPayload{getReporter()}; for (const auto& file : testFiles) { - originalPayload.add(file); + originalPayload.add(file, file); } // First file should be the entry point @@ -433,7 +434,7 @@ TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_extract_preserves_original_execut // Create payload and executable LuteExePayload payload{getReporter()}; - payload.add(testFilePath); + payload.add(testFilePath, testFilePath); std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe_preserve"); LuteExecutable executable{dummyExePath, getReporter()}; diff --git a/tests/src/staticrequires.test.cpp b/tests/src/staticrequires.test.cpp index e55b1aaa4..2dcde67f5 100644 --- a/tests/src/staticrequires.test.cpp +++ b/tests/src/staticrequires.test.cpp @@ -14,23 +14,25 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_simple_dependencies") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "main.luau"); StaticRequireTracer tracer{getReporter()}; - std::vector deps = tracer.trace(testDir, "main.luau"); + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); // Should find: main.luau, utils.luau, lib/helper.luau, shared.luau - REQUIRE(deps.size() == 4); + REQUIRE(pairs.size() == 4); // Entry point should be first - CHECK(deps[0] == "main.luau"); + CHECK(pairs[0].first == "main.luau"); - // Verify all expected files are present (paths are relative to testDir) std::vector expectedFiles = {"main.luau", "utils.luau", "lib/helper.luau", "shared.luau"}; for (const auto& expected : expectedFiles) { - bool found = std::find(deps.begin(), deps.end(), expected) != deps.end(); - CHECK(found); + std::string absolutePath = joinPaths(tracer.getLowestCommonRoot(), expected); + CHECK(tracer.containsAbsolute(absolutePath)); } } @@ -38,88 +40,180 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_circular_dependencies") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "circular_a.luau"); StaticRequireTracer tracer{getReporter()}; - std::vector deps = tracer.trace(testDir, "circular_a.luau"); + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); // Should find both circular_a and circular_b without infinite loop - REQUIRE(deps.size() == 2); + REQUIRE(pairs.size() == 2); // Entry point should be first - CHECK(deps[0] == "circular_a.luau"); + CHECK(pairs[0].first == "circular_a.luau"); - // Both files should be in the list - bool hasA = std::find(deps.begin(), deps.end(), "circular_a.luau") != deps.end(); - bool hasB = std::find(deps.begin(), deps.end(), "circular_b.luau") != deps.end(); + // Both files should be in the list using absolute paths + std::string absoluteA = joinPaths(tracer.getLowestCommonRoot(), "circular_a.luau"); + std::string absoluteB = joinPaths(tracer.getLowestCommonRoot(), "circular_b.luau"); - CHECK(hasA); - CHECK(hasB); + CHECK(tracer.containsAbsolute(absoluteA)); + CHECK(tracer.containsAbsolute(absoluteB)); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_no_dependencies") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "utils.luau"); StaticRequireTracer tracer{getReporter()}; - std::vector deps = tracer.trace(testDir, "utils.luau"); + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); // utils.luau has no requires, should only return itself - REQUIRE(deps.size() == 1); - CHECK(deps[0] == "utils.luau"); + REQUIRE(pairs.size() == 1); + CHECK(pairs[0].first == "utils.luau"); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_relative_paths") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "lib/helper.luau"); StaticRequireTracer tracer{getReporter()}; - std::vector deps = tracer.trace(testDir, "lib/helper.luau"); + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); // helper.luau requires ../shared, should resolve correctly - REQUIRE(deps.size() == 2); + REQUIRE(pairs.size() == 2); - CHECK(deps[0] == "lib/helper.luau"); + CHECK(pairs[0].first == "lib/helper.luau"); - bool hasShared = std::find(deps.begin(), deps.end(), "shared.luau") != deps.end(); - CHECK(hasShared); + std::string absoluteShared = joinPaths(tracer.getLowestCommonRoot(), "shared.luau"); + CHECK(tracer.containsAbsolute(absoluteShared)); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_require_graph") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + StaticRequireTracer tracer{getReporter()}; + std::string entryPoint = joinPaths(testDir, "main.luau"); + + tracer.trace(entryPoint); + + // Build absolute paths + std::string mainPath = joinPaths(testDir, "main.luau"); + std::string utilsPath = joinPaths(testDir, "utils.luau"); + std::string helperPath = joinPaths(testDir, "lib/helper.luau"); + std::string sharedPath = joinPaths(testDir, "shared.luau"); + + // Verify all expected files were discovered + CHECK(tracer.containsAbsolute(mainPath)); + CHECK(tracer.containsAbsolute(utilsPath)); + CHECK(tracer.containsAbsolute(helperPath)); + CHECK(tracer.containsAbsolute(sharedPath)); + + // Verify the graph can be printed without errors (visual inspection of output) + tracer.printRequireGraph(); +} + +TEST_CASE("staticrequiretracer_find_lowest_common_root") +{ + SUBCASE("single_file") + { + std::vector paths = {"/home/user/project/src/main.luau"}; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root == "/home/user/project/src"); + } + + SUBCASE("same_directory") + { + std::vector paths = { + "/home/user/project/src/main.luau", "/home/user/project/src/utils.luau", "/home/user/project/src/helper.luau" + }; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root == "/home/user/project/src"); + } + + SUBCASE("nested_directories") + { + std::vector paths = { + "/home/user/project/src/main.luau", "/home/user/project/src/utils.luau", "/home/user/project/src/lib/helper.luau" + }; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root == "/home/user/project/src"); + } + + SUBCASE("different_subdirectories") + { + std::vector paths = {"/home/user/project/tests/src/main.luau", "/home/user/project/tests/lib/helper.luau"}; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root == "/home/user/project/tests"); + } + + SUBCASE("no_common_root") + { + std::vector paths = {"/home/user/project1/main.luau", "/var/lib/project2/utils.luau"}; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + // Should find at least the root directory "/" + CHECK(!root.empty()); + } + + SUBCASE("empty_vector") + { + std::vector paths = {}; + std::string root = StaticRequireTracer::findLowestCommonRoot(paths); + CHECK(root.empty()); + } +} + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_bundle_paths_and_contains") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "main.luau"); StaticRequireTracer tracer{getReporter()}; - std::vector deps = tracer.trace(testDir, "main.luau"); - - const auto& graph = tracer.getRequireGraph(); - - // main.luau should require utils.luau and lib/helper.luau - REQUIRE(graph.contains("main.luau")); - const auto* mainDeps = graph.find("main.luau"); - REQUIRE(mainDeps != nullptr); - REQUIRE(mainDeps->size() == 2); - CHECK(std::find(mainDeps->begin(), mainDeps->end(), "utils.luau") != mainDeps->end()); - CHECK(std::find(mainDeps->begin(), mainDeps->end(), "lib/helper.luau") != mainDeps->end()); - - // lib/helper.luau should require shared.luau - REQUIRE(graph.contains("lib/helper.luau")); - const auto* helperDeps = graph.find("lib/helper.luau"); - REQUIRE(helperDeps != nullptr); - REQUIRE(helperDeps->size() == 1); - CHECK((*helperDeps)[0] == "shared.luau"); - - // utils.luau should have no dependencies - REQUIRE(graph.contains("utils.luau")); - const auto* utilsDeps = graph.find("utils.luau"); - REQUIRE(utilsDeps != nullptr); - CHECK(utilsDeps->empty()); - - // shared.luau should have no dependencies - REQUIRE(graph.contains("shared.luau")); - const auto* sharedDeps = graph.find("shared.luau"); - REQUIRE(sharedDeps != nullptr); - CHECK(sharedDeps->empty()); + tracer.trace(entryPoint); + + // Get the lowest common root + std::string commonRoot = tracer.getLowestCommonRoot(); + CHECK(commonRoot == testDir); + + // Get discovered files as pairs + auto pairs = tracer.getStaticRequirePairs(); + REQUIRE(pairs.size() == 4); + + // Verify bundle paths (without common prefix) + std::vector expectedBundlePaths = {"main.luau", "utils.luau", "lib/helper.luau", "shared.luau"}; + + for (const auto& expected : expectedBundlePaths) + { + bool found = false; + for (const auto& [bundlePath, absolutePath] : pairs) + { + if (bundlePath == expected) + { + found = true; + break; + } + } + CHECK(found); + } + + // Test containsAbsolute() method with absolute paths + std::string mainAbsolute = joinPaths(commonRoot, "main.luau"); + CHECK(tracer.containsAbsolute(mainAbsolute)); + + std::string helperAbsolute = joinPaths(commonRoot, "lib/helper.luau"); + CHECK(tracer.containsAbsolute(helperAbsolute)); + + // Test non-existent module + std::string nonExistentAbsolute = joinPaths(commonRoot, "nonexistent.luau"); + CHECK(!tracer.containsAbsolute(nonExistentAbsolute)); } From 0ed842ecb2d4c5b858bc2e480ce76cf9fbbfec5e Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:54:08 -0800 Subject: [PATCH 140/642] cli: update tc.cpp to use Luau resolvers (#553) resolves https://github.com/luau-lang/lute/issues/541 I also updated the `LuteFileResolver` to extend `Luau::LuteModuleResolver` since that implementation is built on `Luau::FileResolver` and handles requires. There are already tests in `cli` that call some of these paths... but lmk if there are more we want to add for this PR --- lute/cli/src/tc.cpp | 66 ++++----------------------------------------- 1 file changed, 5 insertions(+), 61 deletions(-) diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index 7a210c7f5..025f7865d 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -5,6 +5,9 @@ #include "Luau/PrettyPrinter.h" #include "Luau/TypeAttach.h" +#include "lute/configresolver.h" +#include "lute/moduleresolver.h" + static const std::string kLuteDefinitions = R"LUTE_TYPES( -- Net api declare net: { @@ -30,7 +33,7 @@ declare function spawn(path: string): any )LUTE_TYPES"; -struct LuteFileResolver : Luau::FileResolver +struct LuteFileResolver : Luau::LuteModuleResolver { std::optional readSource(const Luau::ModuleName& name) override { @@ -55,71 +58,12 @@ struct LuteFileResolver : Luau::FileResolver return Luau::SourceCode{*source, sourceType}; } - std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node) override - { - // TODO: Need to handle requires - return std::nullopt; - } - std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override { if (name == "-") return "stdin"; return name; } - -private: - // TODO: add require resolver; -}; - -struct LuteConfigResolver : Luau::ConfigResolver -{ - Luau::Config defaultConfig; - - mutable std::unordered_map configCache; - mutable std::vector> configErrors; - - LuteConfigResolver(Luau::Mode mode) - { - defaultConfig.mode = mode; - } - - const Luau::Config& getConfig(const Luau::ModuleName& name) const override - { - std::optional path = getParentPath(name); - if (!path) - return defaultConfig; - - return readConfigRec(*path); - } - - const Luau::Config& readConfigRec(const std::string& path) const - { - auto it = configCache.find(path); - if (it != configCache.end()) - return it->second; - - std::optional parent = getParentPath(path); - Luau::Config result = parent ? readConfigRec(*parent) : defaultConfig; - - std::string configPath = joinPaths(path, Luau::kConfigName); - - if (std::optional contents = readFile(configPath)) - { - Luau::ConfigOptions::AliasOptions aliasOpts; - aliasOpts.configLocation = configPath; - aliasOpts.overwriteAliases = true; - - Luau::ConfigOptions opts; - opts.aliasOptions = std::move(aliasOpts); - - std::optional error = Luau::parseConfig(*contents, result, opts); - if (error) - configErrors.push_back({configPath, *error}); - } - - return configCache[path] = result; - } }; static void report(const char* name, const Luau::Location& loc, const char* type, const char* message, LuteReporter& reporter) @@ -240,7 +184,7 @@ int typecheck(const std::vector& sourceFilesInput, LuteReporter& re frontendOptions.runLintChecks = true; LuteFileResolver fileResolver; - LuteConfigResolver configResolver(mode); + Luau::LuteConfigResolver configResolver(mode); Luau::Frontend frontend(&fileResolver, &configResolver, frontendOptions); Luau::registerBuiltinGlobals(frontend, frontend.globals); From 00db049bde74c1360be50f7b4d542ea1c5b3e866 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 10 Nov 2025 16:01:05 -0800 Subject: [PATCH 141/642] bugfix: windows path api needs to be able to get the drive letter for a path (#554) There are a number of tests failing on windows runners because the github runners execute tests under the D:\ drive. However, many of our tests assume execution under the C drive. This PR adds a function called `getdrive(path)` to the win32 path api, as well as fixes some of those tests to be drive aware. --- lute/std/libs/path/win32/init.luau | 18 ++++++++++ tests/std/path/path.win32.test.luau | 52 +++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index ec436605c..3f7c1a5e5 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -59,6 +59,24 @@ function win32.extname(path: pathlike): string return filename:sub(dotIndex) end +function win32.drive(path: pathlike): path + if typeof(path) == "string" then + path = win32.parse(path) + end + + path = path :: path + + if path.driveLetter == nil then + error("Path does not have a drive letter: " .. win32.format(path)) + end + + return setmetatable({ + parts = {}, + kind = "absolute", + driveLetter = path.driveLetter, + }, path_mt) +end + function win32.format(path: pathlike): string if typeof(path) == "string" then return path diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index ee1e9d547..0a0678936 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -1,5 +1,6 @@ local system = require("@std/system") local test = require("@std/test") +local process = require("@std/process") local path = require("@std/path") local win32 = require("@std/path/win32") @@ -301,6 +302,47 @@ test.suite("PathWin32ExtnameSuite", function(suite) end) end) +test.suite("PathWin32GetDriveSuite", function(suite) + suite:case("getdrive_absolute_path_string", function(assert) + local result = path.win32.drive("C:\\Users\\username\\Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "C") + assert.eq(#result.parts, 0) + end) + + suite:case("getdrive_relative_path_with_drive", function(assert) + local result = path.win32.drive("D:Documents\\file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "D") + assert.eq(#result.parts, 0) + end) + + suite:case("getdrive_path_object", function(assert) + local pathobj: win32.path = { + parts = { "Users", "username", "Documents" }, + kind = "absolute", + driveLetter = "E", + } + local result = path.win32.drive(pathobj) + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "E") + assert.eq(#result.parts, 0) + end) + + suite:case("getdrive_root_path", function(assert) + local result = path.win32.drive("F:\\") + assert.eq(result.kind, "absolute") + assert.eq(result.driveLetter, "F") + assert.eq(#result.parts, 0) + end) + + suite:case("getdrive_error_on_empty_path", function(assert) + assert.errors(function() + return path.win32.drive("") + end) + end) +end) + test.suite("PathWin32IsAbsoluteSuite", function(suite) suite:case("isAbsolute_windows_absolute_path", function(assert) local result = path.win32.isabsolute("C:\\Users\\username\\Documents\\file.txt") @@ -655,10 +697,16 @@ test.suite("PathWin32ResolveSuite", function(suite) end) suite:case("resolve_drive_relative_path", function(assert) - local result = path.win32.resolve("C:Documents\\file.txt") + -- Get the drive letter from the current working directory + local cwd = process.cwd() + local cwdPath = path.win32.parse(cwd) + local driveLetter = if system.win32 then path.win32.drive(cwdPath).driveLetter else "C" + + local testPath = driveLetter .. ":Documents\\file.txt" + local result = path.win32.resolve(testPath) -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters assert.eq(result.kind, if system.win32 then "absolute" else "relative") - assert.eq(result.driveLetter, "C") + assert.eq(result.driveLetter, driveLetter) -- The exact parts depend on cwd, but should be absolute local endIndex = #result.parts assert.eq(result.parts[endIndex - 1], "Documents") From be99217bf58e5400b706abb0352073b6534dfe5f Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 10 Nov 2025 17:25:38 -0800 Subject: [PATCH 142/642] bugfix: createdirectory creates the wrong path on windows machines (#555) The `fs.createdirectory` api creates intermediate folders by incrementally creating each subportion of a path. When the passed in path is an absolute path, on posix systems we were creating a path that corresponds to `/`. However, on Windows systems, we were leaving off the drive letter, which means that these paths were treated as relative. E.g. instead of creating: `C:\\some\absolute\path`, we were creating `C:some\absolute\path`, relative to the `cwd`. This caused a number of file systems tests to fail when creating these directories, because they were being created in wrong place. --- lute/std/libs/fs.luau | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 65df572a2..25ca1f2c6 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -5,6 +5,7 @@ local fs = require("@lute/fs") local pathlib = require("@std/path") +local sys = require("@std/system") local fslib = {} @@ -91,11 +92,18 @@ end function fslib.createdirectory(path: pathlike, options: createdirectoryoptions?): () if options and options.makeparents then - local parts: { string } = pathlib.parse(path).parts + local parsed = pathlib.parse(path) + local parts: { string } = parsed.parts local subdir: pathlike = "." if pathlib.isabsolute(path) then - subdir = pathlib.format({ absolute = true, parts = {} }) + if sys.win32 then + -- Windows path - get the drive root like "C:\" + subdir = pathlib.win32.drive(parsed) + else + -- POSIX absolute path - start from root "/" + subdir = pathlib.format({ absolute = true, parts = {} }) + end end for _, part in parts do From 0e0ccbfd407eea974293af166d7ce3a82dc084c4 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 11 Nov 2025 09:50:41 -0800 Subject: [PATCH 143/642] lute transform: pass parsed ASTs to codemods (#556) The existing API of `lute transform` requires that codemod authors call the parser themselves on the source code. Almost all codemod writers are going to operate on the AST anyway, so parse it for them and pass it in the context passed to the transforms. I updated `examples/transformer.luau`, which is used in the existing test for `lute transform`. --- examples/transformer.luau | 9 +++------ lute/cli/commands/transform/init.luau | 3 +++ lute/cli/commands/transform/lib/types.luau | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/transformer.luau b/examples/transformer.luau index f491df0b0..5e49a00e8 100644 --- a/examples/transformer.luau +++ b/examples/transformer.luau @@ -1,11 +1,8 @@ -local parser = require("@std/syntax/parser") local printer = require("@std/syntax/printer") local visitor = require("@std/syntax/visitor") local luau = require("@lute/luau") -function transformation(ctx) - local parsedResult = parser.parsefile(ctx.source) - +local function transformation(ctx) local v = visitor.create() v.visitBinary = function(node: luau.AstExprBinary) @@ -80,9 +77,9 @@ function transformation(ctx) return false end - visitor.visitblock(parsedResult.root, v) + visitor.visitblock(ctx.parseresult.root, v) - return printer.printfile(parsedResult) + return printer.printfile(ctx.parseresult) end return transformation diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index e6fb05de6..aa0ffa209 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -98,9 +98,12 @@ local function applyMigration( local source = fs.readfiletostring(pathStr) + local parseresult = luau.parse(source) + local ctx = { path = pathStr, source = source, + parseresult = parseresult, options = options, } diff --git a/lute/cli/commands/transform/lib/types.luau b/lute/cli/commands/transform/lib/types.luau index b04bc9fc6..e1a80b2da 100644 --- a/lute/cli/commands/transform/lib/types.luau +++ b/lute/cli/commands/transform/lib/types.luau @@ -1,6 +1,9 @@ +local luau = require("@std/luau") + export type Context = { path: string, source: string, + parseresult: luau.ParseResult, options: Options, } From 7264d3599fcea45411bc56e34510c8f284608759 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:54:17 -0800 Subject: [PATCH 144/642] API: luau.typeofmodule v1 (#531) ### Problem As part of an effort to bring type-directed documentation generation to Luau, we don't have an automated way of getting the return type of a Luau module, and this is a broader missing feature as well in Luau for being able to analyze the exact type of a module. ### Solution We introduce an API in `@lute/luau` that allows users to get the return type of a module path by calling `luau.typeofmodule()` and passing in the module path as a string. We create a `Luau::Frontend` to analyze the and resolve the Module, including resolving requires to return the full return type of the specified module! For ease of reviewing, this API temporarily returns the `string` representation while we bikeshed a Luau data structure representation of the `Type` that's returned. As written in `examples/get_module_return_type.luau` an example usage would be retrieving the return type of `@std/task`, which when we run ```luau local moduleTypeString = luau.typeofmodule("./lute/std/libs/task.luau") ``` We currently return a string representation of the module type: ``` { await: (t: { co: thread, result: any, success: boolean? }) -> any, awaitall: (...{ co: thread, result: any, success: boolean? }) -> (...any), create: (f: (a...) -> (b...), ...any) -> { co: thread, result: any, success: boolean? } } ``` Which helps us understand that the `task` library has the following functions `await`, `awaitall`, and `create` with their respective full return types. This is all built on top of @wmccrthy's solid boiler plate of the API (https://github.com/luau-lang/lute/pull/375) and @vrn-sn 's help with the resolve requires!! ### Future Work - We definitely want to return a data structure representation of the Type instead of the string - We also want to represent types with aliases instead of returning the exact type (something like how our `definitions/` files look with the exported types and functions). - If the file we're getting the type of ever has errors, then the result will include the errors because our Frontend mode is set to `NoCheck`, so there will have to be some work done on resolving the errors in the module files I think - Alias resolving isn't fully working yet with `@std/` directly passed in to the `typemodule` call (see comments below), but will be addressed shortly --- definitions/luau.luau | 5 +++ .../get_module_return_type.luau | 18 ++++++++ examples/module_return_type/mainmodule.luau | 24 +++++++++++ lute/luau/include/lute/luau.h | 3 ++ lute/luau/src/luau.cpp | 43 ++++++++++++++++++- lute/std/libs/luau.luau | 6 +++ tests/std/luau.test.luau | 37 ++++++++++++++++ tests/std/syntax/parser.test.luau | 3 ++ 8 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 examples/module_return_type/get_module_return_type.luau create mode 100644 examples/module_return_type/mainmodule.luau create mode 100644 tests/std/luau.test.luau diff --git a/definitions/luau.luau b/definitions/luau.luau index 5e7d8aa16..a14ac3b5b 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -598,4 +598,9 @@ function luau.resolverequire(path: string, fromchunkname: string): string error("not implemented") end +-- For now, we return a string representation of the module's type, but we will expand it to some Luau data structure representation of Type (similar to the AST types) in a subsequent PR. +function luau.typeofmodule(modulepath: string): string + error("not implemented") +end + return luau diff --git a/examples/module_return_type/get_module_return_type.luau b/examples/module_return_type/get_module_return_type.luau new file mode 100644 index 000000000..3b30613ff --- /dev/null +++ b/examples/module_return_type/get_module_return_type.luau @@ -0,0 +1,18 @@ +local luau = require("@std/luau") + +local moduleTypeString = luau.typeofmodule("examples/module_return_type/mainmodule.luau") + +print("Module return type\n", moduleTypeString) + +-- Output: +-- Module return type +-- { +-- _metadata: { +-- id: string, +-- key: number +-- }, +-- func1: (a: number) -> (s: string) -> boolean, +-- strings: { +-- [string]: string +-- } +-- } diff --git a/examples/module_return_type/mainmodule.luau b/examples/module_return_type/mainmodule.luau new file mode 100644 index 000000000..01a7f51a5 --- /dev/null +++ b/examples/module_return_type/mainmodule.luau @@ -0,0 +1,24 @@ +local tableext = require("@std/tableext") + +local testFilter: { [string]: number } = { + ["a"] = 1, +} + +local module = {} + +function module.func1(a: number) + return function(s: string): boolean + return s == tostring(a) + end +end + +module.strings = tableext.map(testFilter, function(value) + return tostring(value) +end) + +module._metadata = { + key = -1, + id = "nice", +} + +return module diff --git a/lute/luau/include/lute/luau.h b/lute/luau/include/lute/luau.h index 527980983..97b0a7d8d 100644 --- a/lute/luau/include/lute/luau.h +++ b/lute/luau/include/lute/luau.h @@ -21,12 +21,15 @@ int compile_luau(lua_State* L); int load_luau(lua_State* L); +int typeofmodule_luau(lua_State* L); + static const luaL_Reg lib[] = { {"parse", luau_parse}, {"parseexpr", luau_parseexpr}, {"compile", compile_luau}, {"load", load_luau}, {"resolverequire", resolverequire_luau}, + {"typeofmodule", typeofmodule_luau}, {nullptr, nullptr}, }; diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index fb5d2b3c3..3b8c7722e 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1,9 +1,9 @@ #include "lute/luau.h" -#include "lute/userdatas.h" - #include "Luau/Ast.h" +#include "Luau/BuiltinDefinitions.h" #include "Luau/Compiler.h" +#include "Luau/Frontend.h" #include "Luau/Location.h" #include "Luau/NotNull.h" #include "Luau/ParseOptions.h" @@ -14,6 +14,9 @@ #include "lua.h" #include "lualib.h" +#include "lute/configresolver.h" +#include "lute/moduleresolver.h" + #include #include #include @@ -2683,6 +2686,42 @@ int load_luau(lua_State* L) return 1; } +int typeofmodule_luau(lua_State* L) +{ + std::string modulePath = luaL_checkstring(L, 1); + + Luau::LuteModuleResolver moduleResolver; + Luau::LuteConfigResolver configResolver(Luau::Mode::NoCheck); + Luau::FrontendOptions fopts; + fopts.retainFullTypeGraphs = true; + + Luau::Frontend frontend(&moduleResolver, &configResolver, fopts); + Luau::registerBuiltinGlobals(frontend, frontend.globals); + Luau::freeze(frontend.globals.globalTypes); + + frontend.check(modulePath); + + Luau::ModulePtr modulePtr = frontend.moduleResolver.getModule(modulePath); + if (!modulePtr) + { + lua_pushnil(L); + return 1; + } + + // For now, we return a string representation of the module's type, but we will expand it to some Luau data structure representation of Type + // (similar to the AST types) in a subsequent PR. + Luau::ToStringOptions opts; + opts.exhaustive = true; + opts.useLineBreaks = true; + opts.functionTypeArguments = true; + opts.scope = modulePtr->getModuleScope(); + + std::string moduleTypeStr = Luau::toString(modulePtr->returnType, opts); + lua_pushlstring(L, moduleTypeStr.c_str(), moduleTypeStr.length()); + return 1; +} + + } // namespace luau static int index_result(lua_State* L) diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 070284fc1..4e12cb61e 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -199,4 +199,10 @@ function luau.loadbypath(requirePath: path.pathlike, env: { [any]: any }?): any return luau.load(migrationBytecode, requirePathStr, env)() end +function luau.typeofmodule(modulepath: path.pathlike): string + local modulePathStr = if typeof(modulepath) == "string" then modulepath else path.format(modulepath) + + return luteLuau.typeofmodule(modulePathStr) +end + return luau diff --git a/tests/std/luau.test.luau b/tests/std/luau.test.luau new file mode 100644 index 000000000..37491cbaf --- /dev/null +++ b/tests/std/luau.test.luau @@ -0,0 +1,37 @@ +local fs = require("@std/fs") +local luau = require("@std/luau") +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") + +local tmpDir = system.tmpdir() + +test.suite("LuauTypeOfModuleSuite", function(suite) + suite:case("typeofmodule_returns_as_string_no_require", function(assert) + local testPath = path.join(tmpDir, "typeofmodule_test.luau") + local testFile = fs.open(testPath, "w+") + fs.write( + testFile, + [[ + local function example_string(): string + return "example" + end + + return { + example_string = example_string, + } + ]] + ) + fs.close(testFile) + + local moduleTypeString = luau.typeofmodule(testPath) + local expectedOutputString = [[{ + example_string: () -> string +}]] + + -- Basic check for now until the data structure is created + assert.eq(moduleTypeString, expectedOutputString) + end) +end) + +test.run() diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 2d2a87052..727ff483c 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -125,6 +125,9 @@ test.suite("Parser", function(suite) suite:case("roundtrippableAst", function(assert) local function visitDirectory(directory: string) for _, entry in fs.listdir(directory) do + if entry.type ~= "file" then + continue + end local p = path.join(directory, entry.name) local source = fs.readfiletostring(path.format(p)) local result = printer.printfile(parser.parsefile(source)) From a8485be8a9e14d59278d2cc0c26d4ccf63cf34fd Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:43:03 -0800 Subject: [PATCH 145/642] stdlib tests: add tests to stringext, tableext, vectorext (#562) we love tests & code coverage --- tests/std/stringext.test.luau | 29 +++++++++++++++++++++++++ tests/std/tableext.test.luau | 40 +++++++++++++++++++++++++++++++++++ tests/std/vectorext.test.luau | 25 ++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 tests/std/stringext.test.luau create mode 100644 tests/std/tableext.test.luau create mode 100644 tests/std/vectorext.test.luau diff --git a/tests/std/stringext.test.luau b/tests/std/stringext.test.luau new file mode 100644 index 000000000..877ebbba4 --- /dev/null +++ b/tests/std/stringext.test.luau @@ -0,0 +1,29 @@ +local stringext = require("@std/stringext") +local test = require("@std/test") + +test.suite("StringExt", function(suite) + suite:case("startswith_and_endswith", function(assert) + assert.eq(stringext.startswith("hello", "he"), true) + assert.eq(stringext.startswith("hi", "hello"), false) + + assert.eq(stringext.endswith("foobar", "bar"), true) + assert.eq(stringext.endswith("foo", "foobar"), false) + end) + + suite:case("removeprefix_and_removesuffix", function(assert) + assert.eq(stringext.removeprefix("hello", "he"), "llo") + assert.eq(stringext.removeprefix("hello", "nope"), "hello") + + assert.eq(stringext.removesuffix("foobar", "bar"), "foo") + assert.eq(stringext.removesuffix("foobar", "nope"), "foobar") + end) + + suite:case("trim", function(assert) + assert.eq(stringext.trim(" a b "), "a b") + assert.eq(stringext.trim("\n\thi\n"), "hi") + assert.eq(stringext.trim(""), "") + assert.eq(stringext.trim(" "), "") + end) +end) + +test.run() diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau new file mode 100644 index 000000000..ea615de10 --- /dev/null +++ b/tests/std/tableext.test.luau @@ -0,0 +1,40 @@ +local tableext = require("@std/tableext") +local test = require("@std/test") + +test.suite("TableExt", function(suite) + suite:case("map_and_filter", function(assert) + local t: { [string]: number } = { a = 1, b = 2, c = 3 } + local mapped = tableext.map(t, function(v: number) + return v * 2 + end) + assert.eq(mapped.a, 2) + assert.eq(mapped.b, 4) + assert.eq(mapped.c, 6) + + local filtered = tableext.filter(t, function(v: number) + return v % 2 == 1 + end) + assert.eq(filtered.a, 1) + assert.eq(filtered.b, nil) + assert.eq(filtered.c, 3) + end) + + suite:case("fold", function(assert) + local arr: { number } = { 1, 2, 3, 4 } + local sum = tableext.fold(arr, function(acc: number, v: number) + return acc + v + end, 0) + assert.eq(sum, 10) + end) + + suite:case("extend", function(assert) + local a: { number } = { 1, 2 } + local b: { number } = { 3, 4 } + tableext.extend(a, b) + assert.eq(#a, 4) + assert.eq(a[3], 3) + assert.eq(a[4], 4) + end) +end) + +test.run() diff --git a/tests/std/vectorext.test.luau b/tests/std/vectorext.test.luau new file mode 100644 index 000000000..1b567999f --- /dev/null +++ b/tests/std/vectorext.test.luau @@ -0,0 +1,25 @@ +local test = require("@std/test") +local vectorext = require("@std/vectorext") + +test.suite("VectorExt", function(suite) + suite:case("withx_withy_withz", function(assert) + local v = vector.create(1, 2, 3) + + local vx = vectorext.withx(v, 10) + assert.eq(vx.x, 10) + assert.eq(vx.y, 2) + assert.eq(vx.z, 3) + + local vy = vectorext.withy(v, 20) + assert.eq(vy.x, 1) + assert.eq(vy.y, 20) + assert.eq(vy.z, 3) + + local vz = vectorext.withz(v, 30) + assert.eq(vz.x, 1) + assert.eq(vz.y, 2) + assert.eq(vz.z, 30) + end) +end) + +test.run() From ba9338deee43c53a2d8bbd2a4cb6db075bd7a918 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 11 Nov 2025 15:21:40 -0800 Subject: [PATCH 146/642] lute parse: fix serialization of union type separators (#563) This PR fixes a bug where the `|` separator was not being correctly serialized when it followed an optional option of a union. For example, the `|` in the union type `number? | string` was not being correctly serialized, and was instead being consumed as the preceding trivia of the `string` option. We now _always_ try to consume the separator if serializing an optional option which isn't the last option of the union. Added a test checking this functionality. Addresses #551. --------- Co-authored-by: ariel --- lute/luau/src/luau.cpp | 20 +++++++++++++++----- tests/std/syntax/parser.test.luau | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 3b8c7722e..bd134bf92 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1,5 +1,8 @@ #include "lute/luau.h" +#include "lute/configresolver.h" +#include "lute/moduleresolver.h" + #include "Luau/Ast.h" #include "Luau/BuiltinDefinitions.h" #include "Luau/Compiler.h" @@ -14,9 +17,6 @@ #include "lua.h" #include "lualib.h" -#include "lute/configresolver.h" -#include "lute/moduleresolver.h" - #include #include #include @@ -1962,7 +1962,14 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "tag"); lua_setfield(L, -2, "node"); - lua_pushnil(L); + // Since this option is an optional type, the separator is always present unless it's the last type + if (i < node->types.size - 1 && separatorPositions < cstNode->separatorPositions.size) + { + serializeToken(cstNode->separatorPositions.data[separatorPositions], "|"); + separatorPositions++; + } + else + lua_pushnil(L); lua_setfield(L, -2, "separator"); } else @@ -1970,13 +1977,16 @@ struct AstSerialize : public Luau::AstVisitor node->types.data[i]->visit(this); lua_setfield(L, -2, "node"); + // If the next type is optional, we don't have a separator token if (i < node->types.size - 1 && !node->types.data[i + 1]->is() && separatorPositions < cstNode->separatorPositions.size) + { serializeToken(cstNode->separatorPositions.data[separatorPositions], "|"); + separatorPositions++; + } else lua_pushnil(L); lua_setfield(L, -2, "separator"); - separatorPositions++; } lua_rawseti(L, -2, i + 1); diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 727ff483c..00694126b 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -956,6 +956,29 @@ test.suite("parse types", function(suite) assert.eq((unionType.types[2].node :: luau.AstTypeOptional).text, "?") end) + suite:case("testTypeUnionWithOptionalPart", function(assert) + local block = parser.parse("type T = number? | string") + local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + assert.eq(typeAlias.type.tag, "union") + + local unionType = typeAlias.type :: luau.AstTypeUnion + assert.eq(unionType.leading, nil) + assert.eq(#unionType.types, 3) + + local pair1 = unionType.types[1] + assert.eq(pair1.node.tag, "reference") + assert.eq(pair1.separator, nil) + + local pair2 = unionType.types[2] + assert.eq(pair2.node.tag, "optional") + assert.neq(pair2.separator, nil) + assert.eq((pair2.separator :: luau.Token<"|">).text, "|") + + local pair3 = unionType.types[3] + assert.eq(pair3.node.tag, "reference") + assert.eq(pair3.separator, nil) + end) + suite:case("testTypeIntersection", function(assert) local block = parser.parse("type T = A & B & C") local typeAlias = block.statements[1] :: luau.AstStatTypeAlias From 7f5ccf789962e0bde43e79fce2359fb4103d7695 Mon Sep 17 00:00:00 2001 From: ariel Date: Tue, 11 Nov 2025 16:03:54 -0800 Subject: [PATCH 147/642] infra: install a batteries-only version of the .luaurc file for CI. (#564) Fixes #561 by installing a batteries-only version of the .luaurc file in CI specifically. This allows us to author tests for batteries, without having to risk the failure mode where we don't notice the embedding of the standard library broke. --- .github/workflows/ci.yml | 4 ++-- .luaurc.ci | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .luaurc.ci diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f73598c9..3777c1a46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,8 +39,8 @@ jobs: options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} - - name: Remove .luaurc file - run: rm .luaurc + - name: Use CI .luaurc file + run: rm .luaurc && mv .luaurc.ci .luaurc - name: Run Luau Tests run: find tests -name "*.test.luau" | xargs -I {} ${{ steps.build_lute.outputs.exe_path }} run {} diff --git a/.luaurc.ci b/.luaurc.ci new file mode 100644 index 000000000..7882318d8 --- /dev/null +++ b/.luaurc.ci @@ -0,0 +1,6 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "./batteries", + } +} From 67acd8c0e9b289dcde4fa27b409a93aa139fffbe Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:27:51 -0800 Subject: [PATCH 148/642] fix: fs.open mode should handle optional mode (#566) `fs.open(path)` was erroring with `invalid argument #2 to 'open' (string expected, got nil)` when mode is not specified. We fix `fs.cpp` by defaulting the mode to read only if it's not specified through `HandleMode` in either `@lute/fs` or `@std/fs`. --- lute/fs/src/fs.cpp | 14 +++++++++----- tests/std/fs.test.luau | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 728e7737c..7c3adde63 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -324,21 +324,25 @@ std::optional openHelper(lua_State* L, const char* path, const char* int open(lua_State* L) { int nArgs = lua_gettop(L); - const char* path = luaL_checkstring(L, 1); - int openFlags = 0x0000; - // When the number of arguments is less 2 if (nArgs < 1) { luaL_errorL(L, "Error: no file supplied\n"); return 0; } + const char* path = luaL_checkstring(L, 1); - if (nArgs < 2) + int openFlags = 0x0000; + const char* mode = "r"; + // Default to read mode if no mode is supplied (i.e., mode is nil in Luau) + if (nArgs < 2 || lua_isnil(L, 2)) { openFlags = O_RDONLY; } + else + { + mode = luaL_checkstring(L, 2); + } - const char* mode = luaL_checkstring(L, 2); if (std::optional result = openHelper(L, path, mode, &openFlags)) { createFileHandle(L, *result); diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 060e7f0d6..2a8934108 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -152,6 +152,21 @@ test.suite("FsSuite", function(suite) assert.neq(success, true) assert.neq(err, nil) end) + + suite:case("fs_open_optional_mode", function(assert) + local file = path.join(tmpdir, "optional_mode.txt") + + local h1 = fs.open(file, "w+") + fs.write(h1, "created file") + fs.close(h1) + + local h2 = fs.open(file) -- with no mode specified, should default to "r" + local contents = fs.read(h2) + assert.eq(contents, "created file") + fs.close(h2) + + fs.remove(file) + end) end) test.run() From 372b1daff00161ba7dd43500e44666c59c2c9db6 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 11 Nov 2025 16:37:32 -0800 Subject: [PATCH 149/642] stdlib: Reworking AST queries (#506) Initial reworking of AST queries to try to make authoring codemods a bit more ergonomic. APIs are still in flux, so open to suggestions! Follow up work still needed: - Preserving leading and trailing trivia when replacing a node - Supporting node removal - Add `select` method of query - Add a `selectToken` query library method --- definitions/luau.luau | 90 ++++++----- examples/query.luau | 25 +++ examples/query_transformer.luau | 20 +++ lute/std/libs/syntax/printer.luau | 241 +++++++++++++++++++---------- lute/std/libs/syntax/query.luau | 152 ++++++++++++++++-- lute/std/libs/syntax/types.luau | 8 + lute/std/libs/syntax/utils.luau | 233 ++++++++++++++++++++++++++++ lute/std/libs/syntax/visitor.luau | 19 +++ tests/cli/transform.test.luau | 40 ++++- tests/std/syntax/printer.test.luau | 112 ++++++++++++++ tests/std/syntax/query.test.luau | 115 ++++++++++++++ 11 files changed, 913 insertions(+), 142 deletions(-) create mode 100644 examples/query.luau create mode 100644 examples/query_transformer.luau create mode 100644 lute/std/libs/syntax/types.luau create mode 100644 lute/std/libs/syntax/utils.luau create mode 100644 tests/std/syntax/printer.test.luau create mode 100644 tests/std/syntax/query.test.luau diff --git a/definitions/luau.luau b/definitions/luau.luau index a14ac3b5b..1d3a5a9d8 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -59,33 +59,27 @@ export type AstExprGroup = { closeparens: Token<")">, } -export type AstExprConstantNil = Token<"nil"> & { tag: "nil" } +export type AstExprConstantNil = Token<"nil"> & { kind: "expr", tag: "nil" } -export type AstExprConstantBool = Token<"true" | "false"> & { tag: "boolean", value: boolean } +export type AstExprConstantBool = Token<"true" | "false"> & { kind: "expr", tag: "boolean", value: boolean } -export type AstExprConstantNumber = Token & { tag: "number", value: number } +export type AstExprConstantNumber = Token & { kind: "expr", tag: "number", value: number } export type AstExprConstantString = Token & { + kind: "expr", tag: "string", quotestyle: "single" | "double" | "block" | "interp", blockdepth: number, } -export type AstExprLocal = { - tag: "local", - token: Token, - ["local"]: AstLocal, - upvalue: boolean, -} +export type AstExprLocal = { kind: "expr", tag: "local", token: Token, ["local"]: AstLocal, upvalue: boolean } -export type AstExprGlobal = { - tag: "global", - name: Token, -} +export type AstExprGlobal = { kind: "expr", tag: "global", name: Token } -export type AstExprVarargs = Token<"..."> & { tag: "vararg" } +export type AstExprVarargs = Token<"..."> & { kind: "expr", tag: "vararg" } export type AstExprCall = { + kind: "expr", tag: "call", func: AstExpr, openparens: Token<"(">?, @@ -96,6 +90,7 @@ export type AstExprCall = { } export type AstExprIndexName = { + kind: "expr", tag: "indexname", expression: AstExpr, accessor: Token<"." | ":">, @@ -104,6 +99,7 @@ export type AstExprIndexName = { } export type AstExprIndexExpr = { + kind: "expr", tag: "index", expression: AstExpr, openbrackets: Token<"[">, @@ -111,6 +107,7 @@ export type AstExprIndexExpr = { closebrackets: Token<"]">, } +-- I don't like that we have this type; a lot of its data should live in the parents, which should then have `AstStatBlock` as its body export type AstFunctionBody = { opengenerics: Token<"<">?, generics: Punctuated?, @@ -130,6 +127,7 @@ export type AstFunctionBody = { } export type AstExprAnonymousFunction = { + kind: "expr", tag: "function", attributes: { AstAttribute }, functionkeyword: Token<"function">, @@ -160,19 +158,17 @@ export type AstExprTableItemGeneral = { export type AstExprTableItem = AstExprTableItemList | AstExprTableItemRecord | AstExprTableItemGeneral export type AstExprTable = { + kind: "expr", tag: "table", openbrace: Token<"{">, entries: { AstExprTableItem }, closebrace: Token<"}">, } -export type AstExprUnary = { - tag: "unary", - operator: Token<"not" | "-" | "#">, - operand: AstExpr, -} +export type AstExprUnary = { kind: "expr", tag: "unary", operator: Token<"not" | "-" | "#">, operand: AstExpr } export type AstExprBinary = { + kind: "expr", tag: "binary", lhsoperand: AstExpr, operator: Token, -- TODO: enforce token type @@ -180,12 +176,14 @@ export type AstExprBinary = { } export type AstExprInterpString = { + kind: "expr", tag: "interpolatedstring", strings: { Token }, expressions: { AstExpr }, } export type AstExprTypeAssertion = { + kind: "expr", tag: "cast", operand: AstExpr, operator: Token<"::">, @@ -201,6 +199,7 @@ export type AstElseIfExpr = { } export type AstExprIfElse = { + kind: "expr", tag: "conditional", ifkeyword: Token<"if">, condition: AstExpr, @@ -233,6 +232,7 @@ export type AstExpr = { kind: "expr" } & ( ) export type AstStatBlock = { + kind: "stat", tag: "block", statements: { AstStat }, } @@ -245,6 +245,7 @@ export type AstElseIfStat = { } export type AstStatIf = { + kind: "stat", tag: "conditional", ifkeyword: Token<"if">, condition: AstExpr, @@ -257,6 +258,7 @@ export type AstStatIf = { } export type AstStatWhile = { + kind: "stat", tag: "while", whilekeyword: Token<"while">, condition: AstExpr, @@ -266,6 +268,7 @@ export type AstStatWhile = { } export type AstStatRepeat = { + kind: "stat", tag: "repeat", repeatKeyword: Token<"repeat">, body: AstStatBlock, @@ -273,22 +276,21 @@ export type AstStatRepeat = { condition: AstExpr, } -export type AstStatBreak = Token<"break"> & { tag: "break" } +export type AstStatBreak = Token<"break"> & { kind: "stat", tag: "break" } -export type AstStatContinue = Token<"continue"> & { tag: "continue" } +export type AstStatContinue = Token<"continue"> & { kind: "stat", tag: "continue" } export type AstStatReturn = { + kind: "stat", tag: "return", returnkeyword: Token<"return">, expressions: Punctuated, } -export type AstStatExpr = { - tag: "expression", - expression: AstExpr, -} +export type AstStatExpr = { kind: "stat", tag: "expression", expression: AstExpr } export type AstStatLocal = { + kind: "stat", tag: "local", localkeyword: Token<"local">, variables: Punctuated, @@ -297,6 +299,7 @@ export type AstStatLocal = { } export type AstStatFor = { + kind: "stat", tag: "for", forkeyword: Token<"for">, variable: AstLocal, @@ -312,6 +315,7 @@ export type AstStatFor = { } export type AstStatForIn = { + kind: "stat", tag: "forin", forkeyword: Token<"for">, variables: Punctuated, @@ -323,6 +327,7 @@ export type AstStatForIn = { } export type AstStatAssign = { + kind: "stat", tag: "assign", variables: Punctuated, equals: Token<"=">, @@ -330,6 +335,7 @@ export type AstStatAssign = { } export type AstStatCompoundAssign = { + kind: "stat", tag: "compoundassign", variable: AstExpr, operand: Token, -- TODO: enforce token type, @@ -339,6 +345,7 @@ export type AstStatCompoundAssign = { export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { kind: "attribute" } export type AstStatFunction = { + kind: "stat", tag: "function", attributes: { AstAttribute }, functionkeyword: Token<"function">, @@ -347,6 +354,7 @@ export type AstStatFunction = { } export type AstStatLocalFunction = { + kind: "stat", tag: "localfunction", attributes: { AstAttribute }, localkeyword: Token<"local">, @@ -356,6 +364,7 @@ export type AstStatLocalFunction = { } export type AstStatTypeAlias = { + kind: "stat", tag: "typealias", export: Token<"export">?, typeToken: Token<"type">, @@ -369,6 +378,7 @@ export type AstStatTypeAlias = { } export type AstStatTypeFunction = { + kind: "stat", tag: "typefunction", export: Token<"export">?, type: Token<"type">, @@ -413,6 +423,7 @@ export type AstGenericTypePack = { } export type AstTypeReference = { + kind: "type", tag: "reference", prefix: Token?, prefixpoint: Token<".">?, @@ -422,17 +433,12 @@ export type AstTypeReference = { closeparameters: Token<">">?, } -export type AstTypeSingletonBool = Token<"true" | "false"> & { - tag: "boolean", - value: boolean, -} +export type AstTypeSingletonBool = Token<"true" | "false"> & { kind: "type", tag: "boolean", value: boolean } -export type AstTypeSingletonString = Token & { - tag: "string", - quotestyle: "single" | "double", -} +export type AstTypeSingletonString = Token & { kind: "type", tag: "string", quotestyle: "single" | "double" } export type AstTypeTypeof = { + kind: "type", tag: "typeof", typeof: Token<"typeof">, openparens: Token<"(">, @@ -441,15 +447,17 @@ export type AstTypeTypeof = { } export type AstTypeGroup = { + kind: "type", tag: "group", openparens: Token<"(">, type: AstType, closeparens: Token<")">, } -export type AstTypeOptional = Token<"?"> & { tag: "optional" } +export type AstTypeOptional = Token<"?"> & { kind: "type", tag: "optional" } export type AstTypeUnion = { + kind: "type", tag: "union", leading: Token<"|">?, -- Separator may be nil for AstTypeOptional @@ -457,12 +465,14 @@ export type AstTypeUnion = { } export type AstTypeIntersection = { + kind: "type", tag: "intersection", leading: Token<"&">?, types: Punctuated, } export type AstTypeArray = { + kind: "type", tag: "array", openbrace: Token<"{">, access: Token<"read" | "write">?, @@ -504,6 +514,7 @@ export type AstTypeTableItemProperty = { export type AstTypeTableItem = AstTypeTableItemIndexer | AstTypeTableItemStringProperty | AstTypeTableItemProperty export type AstTypeTable = { + kind: "type", tag: "table", openbrace: Token<"{">, entries: { AstTypeTableItem }, @@ -517,6 +528,7 @@ export type AstTypeFunctionParameter = { } export type AstTypeFunction = { + kind: "type", tag: "function", opengenerics: Token<"<">?, generics: Punctuated?, @@ -545,6 +557,7 @@ export type AstType = { kind: "type" } & ( ) export type AstTypePackExplicit = { + kind: "typepack", tag: "explicit", openparens: Token<"(">?, types: Punctuated, @@ -552,13 +565,10 @@ export type AstTypePackExplicit = { closeparens: Token<")">?, } -export type AstTypePackGeneric = { - tag: "generic", - name: Token, - ellipsis: Token<"...">, -} +export type AstTypePackGeneric = { kind: "typepack", tag: "generic", name: Token, ellipsis: Token<"..."> } export type AstTypePackVariadic = { + kind: "typepack", tag: "variadic", --- May be nil when present as the vararg annotation in a function body ellipsis: Token<"...">?, @@ -567,7 +577,7 @@ export type AstTypePackVariadic = { export type AstTypePack = { kind: "typepack" } & (AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic) -export type AstNode = { location: Location } & (AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute) +export type AstNode = { location: Location? } & (AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute) export type ParseResult = { root: AstStatBlock, diff --git a/examples/query.luau b/examples/query.luau new file mode 100644 index 000000000..99760385e --- /dev/null +++ b/examples/query.luau @@ -0,0 +1,25 @@ +local luau = require("@std/luau") +local query = require("@std/syntax/query") +local utils = require("@std/syntax/utils") + +local ast = luau.parseexpr("require(OldComponent)") + +local p = query + .select(ast, utils.isExprCall) + :filter(function(call) + local calledFunc = call.func + return calledFunc.tag == "global" and calledFunc.name.text == "require" + end) + :filter(function(call) + local firstArg = call.arguments[1].node + return firstArg.tag == "indexname" and firstArg.index.text == "OldComponent" + end) + +-- Can we improve the type solver here so we don't need to add this annotation? +local argIndexNames = query.map(p, function(call: luau.AstExprCall) + return call.arguments[1].node :: luau.AstExprIndexName +end) + +local replacements = argIndexNames:replace(function(node) + return "NewComponent" +end) diff --git a/examples/query_transformer.luau b/examples/query_transformer.luau new file mode 100644 index 000000000..39a01638b --- /dev/null +++ b/examples/query_transformer.luau @@ -0,0 +1,20 @@ +local luau = require("@std/luau") +local query = require("@std/syntax/query") +local syntax_utils = require("@std/syntax/utils") + +local function transform(ctx) + return query + .selectFromRoot(ctx.parseresult.root, syntax_utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "~=" + and bin.lhsoperand.tag == "local" + and bin.rhsoperand.tag == "local" + and bin.lhsoperand.token.text == bin.rhsoperand.token.text + end) + :replace(function(bin) + return `math.isnan({(bin.lhsoperand :: luau.AstExprLocal).token.text})` + end) + :print() +end + +return transform diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 56a00eb69..b77ff34cf 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -2,157 +2,232 @@ -- @std/syntax/printer local luau = require("@std/luau") +local types = require("./types") local visitor = require("./visitor") -local printer = {} +local printerLib = {} +type PrintVisitor = visitor.Visitor & { + result: buffer, + cursor: number, + replacements: types.replacements, + write: (self: PrintVisitor, str: string) -> (), + printTrivia: (self: PrintVisitor, trivia: luau.Trivia) -> (), + printTriviaList: (self: PrintVisitor, trivia: { luau.Trivia }) -> (), + printToken: (self: PrintVisitor, token: luau.Token) -> (), + printString: (self: PrintVisitor, expr: luau.AstExprConstantString | luau.AstTypeSingletonString) -> (), + printInterpolatedString: (self: PrintVisitor, expr: luau.AstExprInterpString) -> (), + printReplacement: (self: PrintVisitor, node: luau.AstNode, replacement: types.replacement) -> (), +} local function exhaustiveMatch(value: never): never error(`Unknown value in exhaustive match: {value}`) end -local function printTrivia(trivia: luau.Trivia): string +local function printTrivia(self: PrintVisitor, trivia: luau.Trivia) if trivia.tag == "whitespace" or trivia.tag == "comment" or trivia.tag == "blockcomment" then - return trivia.text + self:write(trivia.text) else - return exhaustiveMatch(trivia.tag) + exhaustiveMatch(trivia.tag) end end -local function printTriviaList(trivia: { luau.Trivia }) - local result = "" +local function printTriviaList(self: PrintVisitor, trivia: { luau.Trivia }) for _, trivia in trivia do - result ..= printTrivia(trivia) + self:printTrivia(trivia) end - return result end -local function printToken(token: luau.Token): string - return printTriviaList(token.leadingtrivia) .. token.text .. printTriviaList(token.trailingtrivia) +local function printToken(self: PrintVisitor, token: luau.Token) + self:printTriviaList(token.leadingtrivia) + self:write(token.text) + self:printTriviaList(token.trailingtrivia) end -local function printString(expr: luau.AstExprConstantString | luau.AstTypeSingletonString): string - local result = printTriviaList(expr.leadingtrivia) +local function printString(self: PrintVisitor, expr: luau.AstExprConstantString | luau.AstTypeSingletonString) + self:printTriviaList(expr.leadingtrivia) if expr.quotestyle == "single" then - result ..= `'{expr.text}'` + self:write(`'{expr.text}'`) elseif expr.quotestyle == "double" then - result ..= `"{expr.text}"` + self:write(`"{expr.text}"`) elseif expr.quotestyle == "block" then local equals = string.rep("=", expr.blockdepth) - result ..= `[{equals}[{expr.text}]{equals}]` + self:write(`[{equals}[{expr.text}]{equals}]`) elseif expr.quotestyle == "interp" then - result ..= "`" .. expr.text .. "`" + self:write("`" .. expr.text .. "`") else - return exhaustiveMatch(expr.quotestyle) + exhaustiveMatch(expr.quotestyle) end - result ..= printTriviaList(expr.trailingtrivia) - return result + self:printTriviaList(expr.trailingtrivia) end -local function printInterpolatedString(expr: luau.AstExprInterpString): string - local result = "" - +local function printInterpolatedString(self: PrintVisitor, expr: luau.AstExprInterpString) for i = 1, #expr.strings do - result ..= printTriviaList(expr.strings[i].leadingtrivia) + self:printTriviaList(expr.strings[i].leadingtrivia) if i == 1 then - result ..= "`" + self:write("`") else - result ..= "}" + self:write("}") end - result ..= expr.strings[i].text + self:write(expr.strings[i].text) if i == #expr.strings then - result ..= "`" - result ..= printTriviaList(expr.strings[i].trailingtrivia) + self:write("`") + self:printTriviaList(expr.strings[i].trailingtrivia) else - result ..= "{" - result ..= printTriviaList(expr.strings[i].trailingtrivia) - result ..= printer.printexpr(expr.expressions[i]) + self:write("{") + self:printTriviaList(expr.strings[i].trailingtrivia) + visitor.visitexpression(expr.expressions[i], self) end end - - return result end -type PrintVisitor = visitor.Visitor & { - result: buffer, - cursor: number, -} +local function printReplacement(self: PrintVisitor, node: luau.AstNode, replacement: types.replacement) + if typeof(replacement) == "string" then + self:write(replacement) + elseif replacement.kind ~= nil then -- LUAUFIX: type solver upset comparing string singleton union to nil + -- TODO: preserve trivia + visitor.visit(replacement, self) + else + error("Unsupported replacement type") + end +end -local function printVisitor() - local printer = visitor.create() :: PrintVisitor +local function write(self: PrintVisitor, str: string) + local totalSize = self.cursor + #str + local bufferSize = buffer.len(self.result) - printer.result = buffer.create(1024) - printer.cursor = 0 + if totalSize >= bufferSize then + repeat + bufferSize *= 2 + until bufferSize >= totalSize - local function write(str: string) - local totalSize = printer.cursor + #str - local bufferSize = buffer.len(printer.result) + local newBuffer = buffer.create(bufferSize) + buffer.copy(newBuffer, 0, self.result) + self.result = newBuffer + end - if totalSize >= bufferSize then - repeat - bufferSize *= 2 - until bufferSize >= totalSize + buffer.writestring(self.result, self.cursor, str) + self.cursor = totalSize +end - local newBuffer = buffer.create(bufferSize) - buffer.copy(newBuffer, 0, printer.result) - printer.result = newBuffer - end +local function printVisitor(replacements: types.replacements?) + local result = buffer.create(1024) + local cursor = 0 - buffer.writestring(printer.result, printer.cursor, str) - printer.cursor = totalSize - end + local printer: PrintVisitor = {} :: PrintVisitor - printer.visitToken = function(node: luau.Token) - write(printToken(node)) + -- The visitor library doesn't use methods, so we access replacements through a closure + local function maybeReplace(node: luau.AstNode): boolean + if replacements == nil then + return true + end + local replacement = replacements[node] + if replacement == nil then + return true + end + printer:printReplacement(node, replacement) return false end - printer.visitString = function(node: luau.AstExprConstantString) - write(printString(node)) + printer.result = result + printer.cursor = cursor + printer.replacements = if replacements ~= nil then replacements else {} + printer.write = write + printer.printTrivia = printTrivia + printer.printTriviaList = printTriviaList + printer.printToken = printToken + printer.printString = printString + printer.printInterpolatedString = printInterpolatedString + printer.printReplacement = printReplacement + + printer.visitBlock = maybeReplace + printer.visitBlockEnd = maybeReplace + printer.visitIf = maybeReplace + printer.visitWhile = maybeReplace + printer.visitRepeat = maybeReplace + printer.visitReturn = maybeReplace + printer.visitLocalDeclaration = maybeReplace + printer.visitLocalDeclarationEnd = maybeReplace + printer.visitFor = maybeReplace + printer.visitForIn = maybeReplace + printer.visitAssign = maybeReplace + printer.visitCompoundAssign = maybeReplace + printer.visitFunction = maybeReplace + printer.visitLocalFunction = maybeReplace + printer.visitTypeAlias = maybeReplace + printer.visitStatTypeFunction = maybeReplace + + printer.visitExpression = maybeReplace + printer.visitExpressionEnd = maybeReplace + printer.visitLocalReference = maybeReplace + printer.visitGlobal = maybeReplace + printer.visitCall = maybeReplace + printer.visitUnary = maybeReplace + printer.visitBinary = maybeReplace + printer.visitAnonymousFunction = maybeReplace + printer.visitTableItem = maybeReplace + printer.visitTable = maybeReplace + printer.visitIndexName = maybeReplace + printer.visitIndexExpr = maybeReplace + printer.visitGroup = maybeReplace + printer.visitInterpolatedString = function(node: luau.AstExprInterpString) + printer:printInterpolatedString(node) return false end + printer.visitTypeAssertion = maybeReplace + printer.visitIfExpression = maybeReplace + printer.visitTypeReference = maybeReplace + printer.visitTypeBoolean = maybeReplace printer.visitTypeString = function(node: luau.AstTypeSingletonString) - write(printString(node)) + printer:printString(node) return false end + printer.visitTypeTypeof = maybeReplace + printer.visitTypeGroup = maybeReplace + printer.visitTypeUnion = maybeReplace + printer.visitTypeIntersection = maybeReplace + printer.visitTypeArray = maybeReplace + printer.visitTypeTable = maybeReplace + printer.visitTypeFunction = maybeReplace + + printer.visitTypePackExplicit = maybeReplace + printer.visitTypePackGeneric = maybeReplace + printer.visitTypePackVariadic = maybeReplace - printer.visitInterpolatedString = function(node: luau.AstExprInterpString) - write(printInterpolatedString(node)) + printer.visitToken = function(node: luau.Token) + printer:printToken(node) + return false + end + printer.visitNil = maybeReplace + printer.visitString = function(node: luau.AstExprConstantString) + printer:printString(node) return false end + printer.visitBoolean = maybeReplace + printer.visitNumber = maybeReplace + printer.visitLocal = maybeReplace + printer.visitVarargs = maybeReplace return printer end -function printNode(node: T, visit: (node: T, visitor: visitor.Visitor) -> ()): string - local printer = printVisitor() - visit(node, printer) +function printerLib.printnode(node: luau.AstNode, replacements: types.replacements?): string + local printer = printVisitor(replacements) + visitor.visit(node, printer) return buffer.readstring(printer.result, 0, printer.cursor) end ---- Returns a string representation of an AstExpr -function printer.printexpr(expr: luau.AstExpr): string - return printNode(expr, visitor.visitexpression) -end - ---- Returns a string representation of an AstStat -function printer.printstatement(statement: luau.AstStat): string - return printNode(statement, visitor.visitstatement) -end - --- Returns a string representation of an AstType -function printer.printtype(type: luau.AstType): string - return printNode(type, visitor.visittype) -end - -function printer.printfile(result: { root: luau.AstStatBlock, eof: luau.Eof }): string - local printer = printVisitor() +function printerLib.printfile( + result: { root: luau.AstStatBlock, eof: luau.Eof }, + replacements: types.replacements? +): string + local printer = printVisitor(replacements) visitor.visitblock(result.root, printer) visitor.visittoken(result.eof, printer) return buffer.readstring(printer.result, 0, printer.cursor) end -return table.freeze(printer) +return table.freeze(printerLib) diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index f0f3bd29f..6ba6a79b1 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -3,28 +3,152 @@ -- Provides utility functions for querying Luau AST nodes local luau = require("@std/luau") -local visitor = require("./visitor") +local printer = require("@std/syntax/printer") +local types = require("@std/syntax/types") +local visitor = require("@std/syntax/visitor") -local query = {} +type node = luau.AstNode ---- Selects a list of expressions from the provided block that match the provided predicate. ---- Useful for defining declarative-based programs, where the matched nodes are mutated for the migration -function query.selectif(block: luau.AstStatBlock, predicate: (luau.AstExpr) -> T?): { T } - local nodes = {} - local finder = visitor.create() +export type query = { + nodes: { T }, + filter: (self: query, pred: (T) -> boolean) -> query, + replace: (self: query, repl: (T) -> types.replacement?) -> types.replacements, + map: (self: query, fn: (T) -> U?) -> query, -- recursion violation reported + forEach: (self: query, callback: (T) -> ()) -> (), + __root: node, +} -- TODO: Add a select: (self: query, fn: (node) -> U?) -> query method and a flatMap: (self: query, fn: (T) -> { U }) -> query method - finder.visitExpression = function(node) - local result = predicate(node) - if result then - table.insert(nodes, result) +local queryLib = {} + +function queryLib.filter(self: query, pred: (T) -> boolean): query + local newNodes = {} + for _, node in self.nodes do + if pred(node) then + table.insert(newNodes, node) + end + end + self.nodes = newNodes + return self +end + +local function print(self: types.replacements): string + return printer.printnode(self.__root, self) +end + +function queryLib.replace(self: query, repl: (T) -> types.replacement?): types.replacements + local replacements = { print = print, __root = self.__root } + for _, node in self.nodes do + local r = repl(node) + if r ~= nil then + replacements[node] = r end + end + return replacements +end +-- map will return a query where its nodes are all the non-nil values returned by fn when called on each node of the original query +function queryLib.map(self: query, fn: (T) -> U?): query + local newNodes = {} + for _, node in self.nodes do + local mapped = fn(node) + if mapped ~= nil then + table.insert(newNodes, mapped) + end + end + self.nodes = newNodes + return self +end + +function queryLib.forEach(self: query, callback: (T) -> ()): () + for _, node in self.nodes do + callback(node) + end +end + +function queryLib.selectFromRoot(ast: node, fn: (node) -> T?): query + local nodes: { T } = {} + + local function visit(n: node) -- LUAUFIX: type checker doesn't like assigning visit in the visitor fields with n: node + local selected = fn(n) + if selected ~= nil then + table.insert(nodes, selected) + end return true end - visitor.visitblock(block, finder) + local selectVisitor: visitor.Visitor = { + visitBlock = visit, + visitBlockEnd = function(n) end, + visitIf = visit, + visitWhile = visit, + visitRepeat = visit, + visitReturn = visit, + visitLocalDeclaration = visit, + visitLocalDeclarationEnd = function(n) end, + visitFor = visit, + visitForIn = visit, + visitAssign = visit, + visitCompoundAssign = visit, + visitFunction = visit, + visitLocalFunction = visit, + visitTypeAlias = visit, + visitStatTypeFunction = visit, + + visitExpression = function(n) + return true + end, + visitExpressionEnd = function(n) end, + visitLocalReference = visit, + visitGlobal = visit, + visitCall = visit, + visitUnary = visit, + visitBinary = visit, + visitAnonymousFunction = visit, + visitTableItem = visit, + visitTable = visit, + visitIndexName = visit, + visitIndexExpr = visit, + visitGroup = visit, + visitInterpolatedString = visit, + visitTypeAssertion = visit, + visitIfExpression = visit, + + visitTypeReference = visit, + visitTypeBoolean = visit, + visitTypeString = visit, + visitTypeTypeof = visit, + visitTypeGroup = visit, + visitTypeUnion = visit, + visitTypeIntersection = visit, + visitTypeArray = visit, + visitTypeTable = visit, + visitTypeFunction = visit, + + visitTypePackExplicit = visit, + visitTypePackGeneric = visit, + visitTypePackVariadic = visit, + + visitToken = function(n) + return true + end, + visitNil = visit, + visitString = visit, + visitBoolean = visit, + visitNumber = visit, + visitLocal = visit, + visitVarargs = visit, + } + + visitor.visit(ast, selectVisitor) - return nodes + return { + nodes = nodes, + filter = queryLib.filter, + replace = queryLib.replace, + map = queryLib.map, + forEach = queryLib.forEach, + __root = ast, + } -- LUAUFIX: queryLib.map has generics quantified at a different level than expected end -return table.freeze(query) +return table.freeze(queryLib) diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau new file mode 100644 index 000000000..784f8eb3f --- /dev/null +++ b/lute/std/libs/syntax/types.luau @@ -0,0 +1,8 @@ +local luau = require("@std/luau") + +type node = luau.AstNode + +export type replacement = string | node +export type replacements = { [node]: replacement, print: (self: replacements) -> string, __root: node } + +return {} diff --git a/lute/std/libs/syntax/utils.luau b/lute/std/libs/syntax/utils.luau new file mode 100644 index 000000000..4c570cb9e --- /dev/null +++ b/lute/std/libs/syntax/utils.luau @@ -0,0 +1,233 @@ +local luau = require("@lute/luau") + +local utils = {} + +function utils.isLocal(n: luau.AstNode): luau.AstLocal? + return if n.kind == "local" then n else nil +end + +function utils.isExprGroup(n: luau.AstNode): luau.AstExprGroup? + return if n.kind == "expr" and n.tag == "group" then n else nil +end + +function utils.isExprConstantNil(n: luau.AstNode): luau.AstExprConstantNil? + return if n.kind == "expr" and n.tag == "nil" then n else nil +end + +function utils.isExprConstantBool(n: luau.AstNode): luau.AstExprConstantBool? + return if n.kind == "expr" and n.tag == "bool" then n else nil +end + +function utils.isExprConstantNumber(n: luau.AstNode): luau.AstExprConstantNumber? + return if n.kind == "expr" and n.tag == "number" then n else nil +end + +function utils.isExprConstantString(n: luau.AstNode): luau.AstExprConstantString? + return if n.kind == "expr" and n.tag == "string" then n else nil +end + +function utils.isExprLocal(n: luau.AstNode): luau.AstExprLocal? + return if n.kind == "expr" and n.tag == "local" then n else nil +end + +function utils.isExprGlobal(n: luau.AstNode): luau.AstExprGlobal? + return if n.kind == "expr" and n.tag == "global" then n else nil +end + +function utils.isExprVarargs(n: luau.AstNode): luau.AstExprVarargs? + return if n.kind == "expr" and n.tag == "vararg" then n else nil +end + +function utils.isExprCall(n: luau.AstNode): luau.AstExprCall? + return if n.kind == "expr" and n.tag == "call" then n else nil +end + +function utils.isExprIndexName(n: luau.AstNode): luau.AstExprIndexName? + return if n.kind == "expr" and n.tag == "indexname" then n else nil +end + +function utils.isExprIndexExpr(n: luau.AstNode): luau.AstExprIndexExpr? + return if n.kind == "expr" and n.tag == "index" then n else nil +end + +function utils.isExprAnonymousFunction(n: luau.AstNode): luau.AstExprAnonymousFunction? + return if n.kind == "expr" and n.tag == "function" then n else nil +end + +function utils.isExprTable(n: luau.AstNode): luau.AstExprTable? + return if n.kind == "expr" and n.tag == "table" then n else nil +end + +function utils.isExprUnary(n: luau.AstNode): luau.AstExprUnary? + return if n.kind == "expr" and n.tag == "unary" then n else nil +end + +function utils.isExprBinary(n: luau.AstNode): luau.AstExprBinary? + return if n.kind == "expr" and n.tag == "binary" then n else nil +end + +function utils.isExprInterpString(n: luau.AstNode): luau.AstExprInterpString? + return if n.kind == "expr" and n.tag == "interpolatedstring" then n else nil +end + +function utils.isExprTypeAssertion(n: luau.AstNode): luau.AstExprTypeAssertion? + return if n.kind == "expr" and n.tag == "cast" then n else nil +end + +function utils.isExprIfElse(n: luau.AstNode): luau.AstExprIfElse? + return if n.kind == "expr" and n.tag == "conditional" then n else nil +end + +function utils.isExpr(n: luau.AstNode): luau.AstExpr? + return if n.kind == "expr" then n else nil +end + +function utils.isStatBlock(n: luau.AstNode): luau.AstStatBlock? + return if n.kind == "stat" and n.tag == "block" then n else nil +end + +function utils.isStatIf(n: luau.AstNode): luau.AstStatIf? + return if n.kind == "stat" and n.tag == "conditional" then n else nil +end + +function utils.isStatWhile(n: luau.AstNode): luau.AstStatWhile? + return if n.kind == "stat" and n.tag == "while" then n else nil +end + +function utils.isStatRepeat(n: luau.AstNode): luau.AstStatRepeat? + return if n.kind == "stat" and n.tag == "repeat" then n else nil +end + +function utils.isStatBreak(n: luau.AstNode): luau.AstStatBreak? + return if n.kind == "stat" and n.tag == "break" then n else nil +end + +function utils.isStatContinue(n: luau.AstNode): luau.AstStatContinue? + return if n.kind == "stat" and n.tag == "continue" then n else nil +end + +function utils.isStatReturn(n: luau.AstNode): luau.AstStatReturn? + return if n.kind == "stat" and n.tag == "return" then n else nil +end + +function utils.isStatExpr(n: luau.AstNode): luau.AstStatExpr? + return if n.kind == "stat" and n.tag == "expression" then n else nil +end + +function utils.isStatLocal(n: luau.AstNode): luau.AstStatLocal? + return if n.kind == "stat" and n.tag == "local" then n else nil +end + +function utils.isStatFor(n: luau.AstNode): luau.AstStatFor? + return if n.kind == "stat" and n.tag == "for" then n else nil +end + +function utils.isStatForIn(n: luau.AstNode): luau.AstStatForIn? + return if n.kind == "stat" and n.tag == "forin" then n else nil +end + +function utils.isStatAssign(n: luau.AstNode): luau.AstStatAssign? + return if n.kind == "stat" and n.tag == "assign" then n else nil +end + +function utils.isStatCompoundAssign(n: luau.AstNode): luau.AstStatCompoundAssign? + return if n.kind == "stat" and n.tag == "compoundassign" then n else nil +end + +function utils.isStatFunction(n: luau.AstNode): luau.AstStatFunction? + return if n.kind == "stat" and n.tag == "function" then n else nil +end + +function utils.isStatLocalFunction(n: luau.AstNode): luau.AstStatLocalFunction? + return if n.kind == "stat" and n.tag == "localfunction" then n else nil +end + +function utils.isStatTypeAlias(n: luau.AstNode): luau.AstStatTypeAlias? + return if n.kind == "stat" and n.tag == "typealias" then n else nil +end + +function utils.isStatTypeFunction(n: luau.AstNode): luau.AstStatTypeFunction? + return if n.kind == "stat" and n.tag == "typefunction" then n else nil +end + +function utils.isStat(n: luau.AstNode): luau.AstStat? + return if n.kind == "stat" then n else nil +end + +function utils.isGenericType(n: luau.AstNode): luau.AstGenericType? + return if n.kind == "type" and n.tag == "generic" then n else nil +end + +function utils.isGenericTypePack(n: luau.AstNode): luau.AstGenericTypePack? + return if n.kind == "typepack" and n.tag == "genericpack" then n else nil +end + +function utils.isTypeReference(n: luau.AstNode): luau.AstTypeReference? + return if n.kind == "type" and n.tag == "reference" then n else nil +end + +function utils.isTypeSingletonBool(n: luau.AstNode): luau.AstTypeSingletonBool? + return if n.kind == "type" and n.tag == "boolean" then n else nil +end + +function utils.isTypeSingletonString(n: luau.AstNode): luau.AstTypeSingletonString? + return if n.kind == "type" and n.tag == "string" then n else nil +end + +function utils.isTypeTypeof(n: luau.AstNode): luau.AstTypeTypeof? + return if n.kind == "type" and n.tag == "typeof" then n else nil +end + +function utils.isTypeGroup(n: luau.AstNode): luau.AstTypeGroup? + return if n.kind == "type" and n.tag == "group" then n else nil +end + +function utils.isTypeOptional(n: luau.AstNode): luau.AstTypeOptional? + return if n.kind == "type" and n.tag == "optional" then n else nil +end + +function utils.isTypeUnion(n: luau.AstNode): luau.AstTypeUnion? + return if n.kind == "type" and n.tag == "union" then n else nil +end + +function utils.isTypeIntersection(n: luau.AstNode): luau.AstTypeIntersection? + return if n.kind == "type" and n.tag == "intersection" then n else nil +end + +function utils.isTypeArray(n: luau.AstNode): luau.AstTypeArray? + return if n.kind == "type" and n.tag == "array" then n else nil +end + +function utils.isTypeTable(n: luau.AstNode): luau.AstTypeTable? + return if n.kind == "type" and n.tag == "table" then n else nil +end + +function utils.isTypeFunction(n: luau.AstNode): luau.AstTypeFunction? + return if n.kind == "type" and n.tag == "function" then n else nil +end + +function utils.isType(n: luau.AstNode): luau.AstType? + return if n.kind == "type" then n else nil +end + +function utils.isTypePackExplicit(n: luau.AstNode): luau.AstTypePackExplicit? + return if n.kind == "typepack" and n.tag == "explicit" then n else nil +end + +function utils.isTypePackVariadic(n: luau.AstNode): luau.AstTypePackVariadic? + return if n.kind == "typepack" and n.tag == "variadic" then n else nil +end + +function utils.isTypePackGeneric(n: luau.AstNode): luau.AstTypePackGeneric? + return if n.kind == "typepack" and n.tag == "generic" then n else nil +end + +function utils.isTypePack(n: luau.AstNode): luau.AstTypePack? + return if n.kind == "typepack" then n else nil +end + +function utils.isToken(n: luau.AstNode): luau.Token? + return if n.kind == "token" then n else nil +end + +return table.freeze(utils) diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index df279ccfc..c3174c675 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -881,6 +881,24 @@ local function create() return table.clone(defaultVisitor) end +local function visit(node: luau.AstNode, visitor: Visitor) + if node.kind == "stat" then + visitStatement(node, visitor) + elseif node.kind == "expr" then + visitExpression(node, visitor) + elseif node.kind == "type" then + visitType(node, visitor) + elseif node.kind == "typepack" then + visitTypePack(node, visitor) + elseif node.kind == "local" then + visitLocal(node, visitor) + elseif node.kind == "attribute" then + visitAttribute(node, visitor) + else + exhaustiveMatch(node.kind) + end +end + visitorlib.create = create visitorlib.visitblock = visitBlock visitorlib.visitstatement = visitStatement @@ -888,5 +906,6 @@ visitorlib.visitexpression = visitExpression visitorlib.visittype = visitType visitorlib.visittypepack = visitTypePack visitorlib.visittoken = visitToken +visitorlib.visit = visit return table.freeze(visitorlib) diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index aadb60c3e..5b0fe2c0d 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -11,8 +11,21 @@ local b = x ~= x local lutePath = process.execpath() local tmpDir = system.tmpdir() +local transformeePath = path.format(path.join(tmpDir, "transformee.luau")) test.suite("lute transform", function(suite) + suite:beforeeach(function() + -- Create a transformee file + local transformeeHandle = fs.open(transformeePath, "w+") + fs.write(transformeeHandle, TRANSFORMEE_CONTENT) + fs.close(transformeeHandle) + end) + + suite:aftereach(function() + -- Remove the transformee file + fs.remove(transformeePath) + end) + suite:case("transform visitor style", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau @@ -20,12 +33,30 @@ test.suite("lute transform", function(suite) local transformerDest = path.format(path.join(tmpDir, "transformer.luau")) fs.copy(transformerExample, transformerDest) - -- Create a transformee file - local transformeePath = path.format(path.join(tmpDir, "transformee.luau")) - local transformeeHandle = fs.open(transformeePath, "w+") - fs.write(transformeeHandle, TRANSFORMEE_CONTENT) + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "transform", transformerDest, transformeePath }) + + -- Check + assert.eq(result.exitcode, 0) + + local transformeeHandle = fs.open(transformeePath, "r") + local transformeeContent = fs.read(transformeeHandle) + assert.eq(transformeeContent:find("x ~= x"), nil) + assert.neq(transformeeContent:find("math.isnan(x)", 1, true), nil) fs.close(transformeeHandle) + -- Teardown + fs.remove(transformerDest) + end) + + suite:case("transform query style", function(assert) + -- Setup + -- Copy examples/transformer.luau to build/transform_tests/transformer.luau + local transformerExample = path.format(path.join("examples", "query_transformer.luau")) + local transformerDest = path.format(path.join(tmpDir, "query_transformer.luau")) + fs.copy(transformerExample, transformerDest) + -- Do -- Run the transformer on the transformee local result = process.run({ lutePath, "transform", transformerDest, transformeePath }) @@ -41,7 +72,6 @@ test.suite("lute transform", function(suite) -- Teardown fs.remove(transformerDest) - fs.remove(transformeePath) end) end) diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau new file mode 100644 index 000000000..50cc51962 --- /dev/null +++ b/tests/std/syntax/printer.test.luau @@ -0,0 +1,112 @@ +local luau = require("@std/luau") +local parser = require("@std/syntax/parser") +local printer = require("@std/syntax/printer") +local query = require("@std/syntax/query") +local syntaxUtils = require("@std/syntax/utils") +local test = require("@std/test") + +test.suite("syntax_printer", function(suite) + suite:case("string_replacement", function(assert) + local source = [[ + local name = "World" + ]] + + local ast = parser.parse(source) + local replacements = query.selectFromRoot(ast, syntaxUtils.isExprConstantString):replace(function(s) + return if s.text == "World" then "1 + 2" else nil + end) + + local result = printer.printnode(ast, replacements) + + local expected = " local name = 1 + 2" -- TODO: preserve trivia so trailing newline is preserved + + assert.eq(result, expected) + end) + + suite:case("expr_replacement", function(assert) + local source = [[ + local name = "World" + ]] + + local ast = parser.parse(source) + local replacements = query.selectFromRoot(ast, syntaxUtils.isExprConstantString):replace(function(s) + return if s.text == "World" then parser.parseexpr("1 + 2") else nil -- LUAUFIX: I think this is because AstExpr doesn't subtype against AstNode + end) + + local result = printer.printnode(ast, replacements) + + local expected = " local name = 1 + 2" -- TODO: preserve trivia so trailing newline is preserved + + assert.eq(result, expected) + end) + + suite:case("stat_replacement", function(assert) + local source = [[ + local name = "World" + ]] + + local ast = parser.parse(source) + local replacements = query.selectFromRoot(ast, syntaxUtils.isStatLocal):replace(function(s) + return parser.parse("local name = 1 + 2") + end) + + local result = printer.printnode(ast, replacements) + + local expected = "local name = 1 + 2" -- TODO: preserve trivia so indentation and trailing newline is preserved + + assert.eq(result, expected) + end) + + suite:case("type_replacement", function(assert) + local source = [[local name: number = "World"]] + + local stat = parser.parse("local x: string") + local ann = (stat.statements[1] :: luau.AstStatLocal).variables[1].node.annotation + assert.neq(ann, nil) + + local ast = parser.parse(source) + local replacements = query + .selectFromRoot(ast, syntaxUtils.isStatLocal) + :map(function(assign) + local ann = assign.variables[1].node.annotation + assert.neq(ann, nil) + return ann + end) + :replace(function(t) + return ann + end) + + local result = printer.printnode(ast, replacements) + + local expected = [[local name: string= "World"]] -- TODO: preserve trivia so space after type is preserved + + assert.eq(result, expected) + end) + + suite:case("type_pack_replacement", function(assert) + local source = [[type t = (...number) -> ()]] + + local stat = parser.parse("type t = () -> (string, ...string)") + local tp = ((stat.statements[1] :: luau.AstStatTypeAlias).type :: luau.AstTypeFunction).returntypes + + local ast = parser.parse(source) + local replacements = query + .selectFromRoot(ast, syntaxUtils.isStatTypeAlias) + :map(function(ta) + if ta.type.tag == "function" then + return ta.type.returntypes + end + end) + :replace(function(t) + return tp + end) + + local result = printer.printnode(ast, replacements) + + local expected = [[type t = (...number) -> string, ...string)]] -- TODO: bug in the parser? AstTypePackExplicit is missing opening + + assert.eq(result, expected) + end) +end) + +test.run() diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau new file mode 100644 index 000000000..62e248ef7 --- /dev/null +++ b/tests/std/syntax/query.test.luau @@ -0,0 +1,115 @@ +local luau = require("@std/luau") +local parser = require("@std/syntax/parser") +local query = require("@std/syntax/query") +local test = require("@std/test") +local utils = require("@std/syntax/utils") + +test.suite("AST Query", function(suite) + suite:case("filter", function(assert) + local testQuery = query.selectFromRoot(parser.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + + local filteredQuery = testQuery:filter(function(n) + return n.value > 2 + end) + + assert.eq(#filteredQuery.nodes, 2) + assert.eq(filteredQuery.nodes[1].value, 3) + assert.eq(filteredQuery.nodes[2].value, 4) + assert.eq(filteredQuery, testQuery) -- ensure the original query is mutated + end) + + suite:case("replace", function(assert) + local testQuery = query.selectFromRoot(parser.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + + local replacements = testQuery:replace(function(n): string? + if n.value > 0 then + return "positive_" .. tostring(n.value) + end + return nil -- return nil for non-positive numbers + end) + + -- Should only have replacements for positive numbers + assert.eq(replacements[testQuery.nodes[2]], "positive_1") -- LUAUFIX CLI-178250: shouldn't need to cast + assert.eq(replacements[testQuery.nodes[3]], "positive_2") -- LUAUFIX CLI-178250: shouldn't need to cast + assert.eq(replacements[testQuery.nodes[4]], "positive_3") -- LUAUFIX CLI-178250: shouldn't need to cast + assert.eq(replacements[testQuery.nodes[5]], "positive_4") -- LUAUFIX CLI-178250: shouldn't need to cast + + -- Non-positive numbers should not have replacements (nil values) + assert.eq(replacements[testQuery.nodes[1]], nil) -- LUAUFIX CLI-178250: shouldn't need to cast + + -- Verify the replacement table only contains the expected entries + local count = 0 + for _, _ in replacements do + count += 1 + end + assert.eq(count, 6) -- 4 non-nil replacements + print + __root + end) + + suite:case("map", function(assert) + local testQuery = query.selectFromRoot(parser.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + + local mappedQuery = query.map(testQuery, function(n: luau.AstExprConstantNumber): number? + if n.value % 2 == 0 then + return n.value * 10 + end + return nil -- return nil for odd numbers + end) + + assert.eq(#mappedQuery.nodes, 3) + assert.eq(mappedQuery.nodes[1], 0) + assert.eq(mappedQuery.nodes[2], 20) + assert.eq(mappedQuery.nodes[3], 40) + end) + + suite:case("forEach", function(assert) + local testQuery = query.selectFromRoot(parser.parse("local _ = {0, 1, 2}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + + local sum = 0 + testQuery:forEach(function(n: luau.AstExprConstantNumber) + sum = sum + n.value + end) + + assert.eq(sum, 3) -- 0 + 1 + 2 = 3 + end) + + suite:case("select_nums", function(assert) + local ast = luau.parse("print(1 + 2)") + local queryResult: query.query = query.selectFromRoot( + ast.root, -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + function(n: luau.AstNode): luau.AstExprConstantNumber? + if n.kind == "expr" and n.tag == "number" then + return n + end + return nil + end + ) + + assert.eq(#queryResult.nodes, 2) + local num: luau.AstExpr = queryResult.nodes[1] + assert.eq(num.kind, "expr") + assert.eq(num.tag, "number") + assert.eq((num :: luau.AstExprConstantNumber).value, 1) + num = queryResult.nodes[2] + assert.eq(num.kind, "expr") + assert.eq(num.tag, "number") + assert.eq((num :: luau.AstExprConstantNumber).value, 2) + end) + + suite:case("select_utils", function(assert) + local ast = luau.parse("print(1 + 2)") + local queryResult: query.query = + query.selectFromRoot(ast.root :: luau.AstNode, utils.isExprConstantNumber) + + assert.eq(#queryResult.nodes, 2) + local num: luau.AstExpr = queryResult.nodes[1] + assert.eq(num.kind, "expr") + assert.eq(num.tag, "number") + assert.eq((num :: luau.AstExprConstantNumber).value, 1) + num = queryResult.nodes[2] + assert.eq(num.kind, "expr") + assert.eq(num.tag, "number") + assert.eq((num :: luau.AstExprConstantNumber).value, 2) + end) +end) + +test.run() From 98807faf93fff70488cb17e7e4ef89008e6df015 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:13:52 -0800 Subject: [PATCH 150/642] site: link folder and page names together for api docs & fix nav (#567) image --- docs/.vitepress/config.mts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 21aba7caa..82142c258 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -9,7 +9,7 @@ export default withSidebar( themeConfig: { nav: [ { text: 'Guide', link: '/guide/installation' }, - { text: 'Reference', link: '/reference/' }, + { text: 'Reference', link: '/reference/definitions/crypto' }, ], search: { provider: 'local' }, socialLinks: [ @@ -19,15 +19,11 @@ export default withSidebar( }), { // ============ [ SIDEBAR OPTIONS ] ============ - // ============ [ GROUPING ] ============ - // collapsed: true, // Collapse subgroups by default - // ============ [ GETTING MENU TITLE ] ============ + useFolderLinkFromSameNameSubFile: true, useTitleFromFileHeading: true, useTitleFromFrontmatter: true, - // ============ [ STYLING MENU TITLE ] ============ hyphenToSpace: true, underscoreToSpace: true, - // ============ [ SORTING ] ============ sortMenusByName: true, } ) From ee1b79636036de4bbbd99b7745da6edb129c6ad4 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 11 Nov 2025 23:46:47 -0800 Subject: [PATCH 151/642] bugfix: Use after free in fs.cpp's watch method (#568) This use after free manifested intermittently when running lots and lots of tests at a time in this PR: https://github.com/luau-lang/lute/pull/557. The cause of this uaf happens because we do not close the `fs_event_t` handle. When the `WatchHandle` wrapper gets closed, it invoked `uv_event_stop` but not `uv_close`, so the memory associated with the handle gets freed, but `libuv` doesn't know that this handle is closed. This causes uv's internal data structures to get messed up and we may accidentally touch this freed memory causing the Use-After-Free. `WatchHandle::close` already invokes `uv_event_stop`, so we can remove this from the implementation of `closeHandle`. --- lute/fs/src/fs.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 7c3adde63..34cde2b34 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -561,6 +561,8 @@ struct WatchHandle luaL_errorL(L, "Error stopping fs event: %s", uv_strerror(err)); } + uv_close((uv_handle_t*) &handle, nullptr); + isClosed = true; getRuntime(L)->releasePendingToken(); @@ -586,12 +588,6 @@ static int closeWatchHandle(lua_State* L) return 0; } - int err = uv_fs_event_stop(&handle->handle); - if (err) - { - luaL_errorL(L, "Error stopping fs event: %s", uv_strerror(err)); - } - handle->close(); return 0; From 0eae84cc440a11f20fe0b9b5b3125a281e9ab342 Mon Sep 17 00:00:00 2001 From: Andrew Zhilin Date: Wed, 12 Nov 2025 14:59:20 -0300 Subject: [PATCH 152/642] perf: base64 decoder with buffer-based LUT and stricter validation (#559) ## Summary - Replace table-based decoding LUT with buffer for faster lookups - Switch from file-scope `--!native` to function-level `@native` attributes - Implement RFC 4648-compliant padding handling (strip instead of count) - Add input validation to reject invalid base64 characters - Validate non-zero padding bits per RFC 4648 section 3.5 - Optimize decode loop using u32 writes ## Test coverage - RFC 4648 test vectors (encode/decode) - Invalid character detection - Invalid padding/length cases ## Performance ```text > luau --codegen -O2 -- Output size 1 KB old base64.decode 10.61 us/iter -> new base64.decode 7.31 us/iter -- Output size 1024 KB old base64.decode 10.51 ms/iter -> new base64.decode 6.80 ms/iter > luau -O2 -- Output size 1 KB old base64.decode 45.48 us/iter -> new base64.decode 36.32 us/iter -- Output size 1024 KB old base64.decode 47.37 ms/iter -> new base64.decode 34.48 ms/iter ``` --- batteries/base64.luau | 86 ++++++++++++++++++-------------- tests/batteries/base64.test.luau | 76 ++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 38 deletions(-) create mode 100644 tests/batteries/base64.test.luau diff --git a/batteries/base64.luau b/batteries/base64.luau index 3dbca38d8..4978f155a 100644 --- a/batteries/base64.luau +++ b/batteries/base64.luau @@ -1,10 +1,9 @@ ---!native --!optimize 2 --!strict local BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -local BASE64_ENCODING_LUT = table.create(4096) -local BASE64_DECODING_LUT = table.create(255, 0) +local BASE64_ENCODING_LUT = table.create(4096) :: { number } +local BASE64_DECODING_LUT = buffer.create(256) do for i = 0, 4095 do @@ -14,11 +13,14 @@ do bit32.bor(string.byte(BASE64_ALPHABET, hi), bit32.lshift(string.byte(BASE64_ALPHABET, lo), 8)) end + -- prefill lookup table with invalid base64 value (0xFF) + buffer.fill(BASE64_DECODING_LUT, 0, 0xFF) for i = 1, #BASE64_ALPHABET do - BASE64_DECODING_LUT[string.byte(BASE64_ALPHABET, i)] = i - 1 + buffer.writeu8(BASE64_DECODING_LUT, string.byte(BASE64_ALPHABET, i), i - 1) end end +@native local function encode(input_buffer: buffer): buffer assert(typeof(input_buffer) == "buffer", "Expected input to be a buffer") @@ -71,6 +73,7 @@ local function encode(input_buffer: buffer): buffer return output end +@native local function decode(input_buffer: buffer): buffer assert(typeof(input_buffer) == "buffer", "Expected input to be a buffer") @@ -80,49 +83,56 @@ local function decode(input_buffer: buffer): buffer return buffer.create(0) end - local padding_size = 0 - if input_length >= 2 and buffer.readu16(input_buffer, input_length - 2) == 0x3D3D then - padding_size = 2 - elseif input_length >= 1 and buffer.readu8(input_buffer, input_length - 1) == 0x3D then - padding_size = 1 + -- strip padding (rfc-4648 section 3.3: the excess pad characters MAY also be ignored) + while input_length > 0 and buffer.readu8(input_buffer, input_length - 1) == 0x3D do + input_length -= 1 + end + + -- rfc-4648 section 3.3 forbids padding that isn't preceded by at least one Base64 digit + if input_length == 0 then + error("Invalid base64 input", 2) end -- get correct output size - local output_length = ((input_length / 4) * 3) - padding_size + local output_length = (3 * input_length) // 4 local output = buffer.create(output_length) - local chunks = input_length // 4 - - for chunk_idx = 1, chunks do - local index = (chunk_idx - 1) * 4 - local out_index = (chunk_idx - 1) * 3 - - local value1 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index)] - local value2 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index + 1)] - local value3 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index + 2)] - local value4 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index + 3)] - - local chunk = bit32.bor(bit32.lshift(value1, 18), bit32.lshift(value2, 12), bit32.lshift(value3, 6), value4) - - local character1 = bit32.rshift(chunk, 16) - local character2 = bit32.band(bit32.rshift(chunk, 8), 0b11111111) - local character3 = bit32.band(chunk, 0b11111111) - -- always write the first byte - if out_index < output_length then - buffer.writeu8(output, out_index, character1) - end - - -- write second byte if have space (+padding) - if out_index + 1 < output_length then - buffer.writeu8(output, out_index + 1, character2) + local read_offset = 0 + local write_offset = 0 + -- loop invariant: at least 4 bytes to write + while write_offset + 4 <= output_length do + local b4 = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset + 3)) + local b3 = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset + 2)) + local b2 = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset + 1)) + local b1 = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset)) + if bit32.bor(b1, b2, b3, b4) >= 64 then + error("Invalid base64 input", 2) end + read_offset += 4 + -- u32 BE: [B1, B2, B3, 0] = b1<<26|b2<<20|b3<<14|b4<<8, trailing 0 will be overwritten next iteration + buffer.writeu32(output, write_offset, bit32.byteswap(b1 * 0x4000000 + b2 * 0x100000 + b3 * 0x4000 + b4 * 0x100)) + write_offset += 3 + end - -- Write third byte if we have space (+padding) - if out_index + 2 < output_length then - buffer.writeu8(output, out_index + 2, character3) + local u24be, nbits = 0, 0 + while read_offset < input_length do + local b = buffer.readu8(BASE64_DECODING_LUT, buffer.readu8(input_buffer, read_offset)) + read_offset += 1 + if b >= 64 then + error("Invalid base64 input", 2) end + u24be = u24be * 0x40 + b + nbits += 6 + end + while nbits >= 8 do + buffer.writeu8(output, write_offset, bit32.rshift(u24be, nbits - 8)) + nbits -= 8 + write_offset += 1 + end + -- 2 or 4 leftover bits must be zero + if nbits == 6 or (nbits == 2 and bit32.btest(u24be, 0x03)) or (nbits == 4 and bit32.btest(u24be, 0x0F)) then + error("Invalid base64 input", 2) end - return output end diff --git a/tests/batteries/base64.test.luau b/tests/batteries/base64.test.luau new file mode 100644 index 000000000..6f616369b --- /dev/null +++ b/tests/batteries/base64.test.luau @@ -0,0 +1,76 @@ +local test = require("@std/test") +local base64 = require("@batteries/base64") + +local function enc(s: string): string + return buffer.tostring(base64.encode(buffer.fromstring(s))) +end +local function dec(s: string): string + return buffer.tostring(base64.decode(buffer.fromstring(s))) +end + +-- RFC 4648 test vectors +local testVectors = { + { "", "" }, + { "f", "Zg==" }, + { "fo", "Zm8=" }, + { "foo", "Zm9v" }, + { "foob", "Zm9vYg==" }, + { "fooba", "Zm9vYmE=" }, + { "foobar", "Zm9vYmFy" }, + { "foobarb", "Zm9vYmFyYg==" }, + { "foobarba", "Zm9vYmFyYmE=" }, + { "foobarbaz", "Zm9vYmFyYmF6" }, +} + +local invalidInputs = { + "SGVsbG8\0", + "SGVsbG8\255", + "Hell@oWorld!", + "Test1#23", + "DataMon$ey", + "SGVsbG8-V29ybGQ_", + "SGVs=bG8=", +} + +local invalidLastBytes = { + "====", -- all padding + "A==A", -- char after padding + "A", -- invalid length + "AB==", -- impossible sequence + "AAB=", -- impossible sequence +} + +test.suite("Base64", function(suite) + for _, vector in ipairs(testVectors) do + local input = vector[1] + local expected_encoded = vector[2] + + suite:case("Encode: '" .. input .. "'", function(assert) + local encoded = enc(input) + assert.eq(expected_encoded, encoded) + end) + + suite:case("Decode: '" .. expected_encoded .. "'", function(assert) + local decoded = dec(expected_encoded) + assert.eq(input, decoded) + end) + end + + for _, invalid in invalidInputs do + suite:case("Invalid Decode: '" .. invalid .. "'", function(assert) + assert.errors(function() + dec(invalid) + end) + end) + end + + for _, invalid in invalidLastBytes do + suite:case("Invalid Decode (last bytes): '" .. invalid .. "'", function(assert) + assert.errors(function() + dec(invalid) + end) + end) + end +end) + +test.run() From d2d3ede04e200b2cf9d9e0639660043e8efe7f2e Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 12 Nov 2025 10:16:38 -0800 Subject: [PATCH 153/642] lute std: Move AST-related functions and types from @std/luau to @std/syntax (#565) We previously had redundant parse functions across @std/syntax/parser and @std/luau. I talked this over with @~aatxe and we came to the conclusion that @std/syntax content should be AST-related, while @std/luau is reserved for runtime-related content. --- examples/linter.luau | 4 +- examples/parsing.luau | 4 +- examples/transformer.luau | 32 +- lute/cli/commands/transform/init.luau | 3 +- lute/cli/commands/transform/lib/types.luau | 4 +- lute/std/libs/luau.luau | 167 --------- lute/std/libs/syntax/init.luau | 162 +++++++++ lute/std/libs/syntax/parser.luau | 17 +- lute/std/libs/syntax/printer.luau | 21 +- lute/std/libs/syntax/query.luau | 8 +- lute/std/libs/syntax/types.luau | 164 ++++++++- lute/std/libs/syntax/visitor.luau | 232 ++++++------ tests/std/syntax/parser.test.luau | 398 ++++++++++----------- tests/std/syntax/query.test.luau | 40 +-- 14 files changed, 701 insertions(+), 555 deletions(-) create mode 100644 lute/std/libs/syntax/init.luau diff --git a/examples/linter.luau b/examples/linter.luau index ea44b0fae..ba91f249e 100644 --- a/examples/linter.luau +++ b/examples/linter.luau @@ -1,5 +1,5 @@ local fs = require("@std/fs") -local luau = require("@std/luau") +local syntax = require("@std/syntax") local pp = require("@batteries/pp") @@ -32,7 +32,7 @@ end local path = tostring(...) local source = fs.readfiletostring(path) -local prog = luau.parse(source) +local prog = syntax.parse(source) local function lintForDivZeroByZero(prog) local function unwrapParens(n) diff --git a/examples/parsing.luau b/examples/parsing.luau index 374ce6608..fa6fc6e3a 100644 --- a/examples/parsing.luau +++ b/examples/parsing.luau @@ -1,6 +1,6 @@ -local luau = require("@std/luau") +local syntax = require("@std/syntax") local pretty = require("@batteries/pp") -local foo = luau.parseexpr("5") +local foo = syntax.parseexpr("5") print(pretty(foo)) diff --git a/examples/transformer.luau b/examples/transformer.luau index 5e49a00e8..ba8126a6b 100644 --- a/examples/transformer.luau +++ b/examples/transformer.luau @@ -1,11 +1,11 @@ local printer = require("@std/syntax/printer") local visitor = require("@std/syntax/visitor") -local luau = require("@lute/luau") +local syntax = require("@std/syntax") local function transformation(ctx) local v = visitor.create() - v.visitBinary = function(node: luau.AstExprBinary) + v.visitBinary = function(node: syntax.AstExprBinary) if node.operator.text ~= "~=" then return true end @@ -18,19 +18,19 @@ local function transformation(ctx) return true end - local operand: luau.AstExprLocal = node.lhsoperand + local operand: syntax.AstExprLocal = node.lhsoperand operand.token.leadingtrivia = {} operand.token.trailingtrivia = {} - local openingPosition: luau.Position = operand.token.position; + local openingPosition: syntax.Position = operand.token.position; -- transform node into an AstExprCall (node :: any).operator = nil (node :: any).lhsoperand = nil (node :: any).rhsoperand = nil - ((node :: any) :: luau.AstExprCall).tag = "call" + ((node :: any) :: syntax.AstExprCall).tag = "call" - local func: luau.AstExpr = { + local func: syntax.AstExpr = { tag = "global", name = { leadingtrivia = {}, @@ -39,20 +39,20 @@ local function transformation(ctx) trailingtrivia = {}, }, } - ((node :: any) :: luau.AstExprCall).func = func + ((node :: any) :: syntax.AstExprCall).func = func - local openparens: luau.Token<"("> = { + local openparens: syntax.Token<"("> = { leadingtrivia = {}, position = { line = openingPosition.line, column = openingPosition.column + #"math.isnan" }, text = "(", trailingtrivia = {}, } - ((node :: any) :: luau.AstExprCall).openparens = openparens + ((node :: any) :: syntax.AstExprCall).openparens = openparens - local arguments: luau.Punctuated = { { node = operand } } - ((node :: any) :: luau.AstExprCall).arguments = arguments + local arguments: syntax.Punctuated = { { node = operand } } + ((node :: any) :: syntax.AstExprCall).arguments = arguments - local closeparens: luau.Token<")"> = { + local closeparens: syntax.Token<")"> = { leadingtrivia = {}, position = { line = openingPosition.line, @@ -61,18 +61,18 @@ local function transformation(ctx) text = ")", trailingtrivia = {}, } - ((node :: any) :: luau.AstExprCall).closeparens = closeparens; + ((node :: any) :: syntax.AstExprCall).closeparens = closeparens; - ((node :: any) :: luau.AstExprCall).self = false + ((node :: any) :: syntax.AstExprCall).self = false - local argLocation: luau.Location = { + local argLocation: syntax.Location = { begin = { line = openingPosition.line, column = openingPosition.column + #"math.isnan(" }, ["end"] = { line = openingPosition.line, column = openingPosition.column + #"math.isnan(" + #operand.token.text, }, } - ((node :: any) :: luau.AstExprCall).argLocation = argLocation + ((node :: any) :: syntax.AstExprCall).argLocation = argLocation return false end diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index aa0ffa209..7fa60abaa 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -1,6 +1,7 @@ local fs = require("@lute/fs") local luau = require("@std/luau") local pathLib = require("@std/path") +local syntax = require("@std/syntax") local arguments = require("@self/lib/arguments") local files = require("@self/lib/files") @@ -98,7 +99,7 @@ local function applyMigration( local source = fs.readfiletostring(pathStr) - local parseresult = luau.parse(source) + local parseresult = syntax.parsefile(source) local ctx = { path = pathStr, diff --git a/lute/cli/commands/transform/lib/types.luau b/lute/cli/commands/transform/lib/types.luau index e1a80b2da..45ad573c7 100644 --- a/lute/cli/commands/transform/lib/types.luau +++ b/lute/cli/commands/transform/lib/types.luau @@ -1,9 +1,9 @@ -local luau = require("@std/luau") +local syntax = require("@std/syntax") export type Context = { path: string, source: string, - parseresult: luau.ParseResult, + parseresult: syntax.ParseResult, options: Options, } diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 4e12cb61e..464146c75 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -10,175 +10,8 @@ local path = require("@std/path") local luau = {} --- Export all types from luteLuau -export type Position = luteLuau.Position -export type Location = luteLuau.Location - -export type Whitespace = luteLuau.Whitespace -export type SingleLineComment = luteLuau.SingleLineComment -export type MultiLineComment = luteLuau.MultiLineComment - -export type Trivia = luteLuau.Trivia - -export type Token = luteLuau.Token - -export type Eof = luteLuau.Eof - -export type Pair = luteLuau.Pair -export type Punctuated = luteLuau.Punctuated - -export type AstLocal = luteLuau.AstLocal - -export type AstExprGroup = luteLuau.AstExprGroup - -export type AstExprConstantNil = luteLuau.AstExprConstantNil - -export type AstExprConstantBool = luteLuau.AstExprConstantBool - -export type AstExprConstantNumber = luteLuau.AstExprConstantNumber - -export type AstExprConstantString = luteLuau.AstExprConstantString - -export type AstExprLocal = luteLuau.AstExprLocal - -export type AstExprGlobal = luteLuau.AstExprGlobal - -export type AstExprVarargs = luteLuau.AstExprVarargs - -export type AstExprCall = luteLuau.AstExprCall - -export type AstExprIndexName = luteLuau.AstExprIndexName - -export type AstExprIndexExpr = luteLuau.AstExprIndexExpr - -export type AstFunctionBody = luteLuau.AstFunctionBody - -export type AstExprAnonymousFunction = luteLuau.AstExprAnonymousFunction - -export type AstExprTableItemList = luteLuau.AstExprTableItemList - -export type AstExprTableItemRecord = luteLuau.AstExprTableItemRecord - -export type AstExprTableItemGeneral = luteLuau.AstExprTableItemGeneral - -export type AstExprTableItem = luteLuau.AstExprTableItem - -export type AstExprTable = luteLuau.AstExprTable - -export type AstExprUnary = luteLuau.AstExprUnary - -export type AstExprBinary = luteLuau.AstExprBinary - -export type AstExprInterpString = luteLuau.AstExprInterpString - -export type AstExprTypeAssertion = luteLuau.AstExprTypeAssertion - -export type AstElseIfExpr = luteLuau.AstElseIfExpr - -export type AstExprIfElse = luteLuau.AstExprIfElse - -export type AstExpr = luteLuau.AstExpr - -export type AstStatBlock = luteLuau.AstStatBlock - -export type AstElseIfStat = luteLuau.AstElseIfStat - -export type AstStatIf = luteLuau.AstStatIf - -export type AstStatWhile = luteLuau.AstStatWhile - -export type AstStatRepeat = luteLuau.AstStatRepeat - -export type AstStatBreak = luteLuau.AstStatBreak - -export type AstStatContinue = luteLuau.AstStatContinue - -export type AstStatReturn = luteLuau.AstStatReturn - -export type AstStatExpr = luteLuau.AstStatExpr - -export type AstStatLocal = luteLuau.AstStatLocal - -export type AstStatFor = luteLuau.AstStatFor - -export type AstStatForIn = luteLuau.AstStatForIn - -export type AstStatAssign = luteLuau.AstStatAssign - -export type AstStatCompoundAssign = luteLuau.AstStatCompoundAssign - -export type AstAttribute = luteLuau.AstAttribute - -export type AstStatFunction = luteLuau.AstStatFunction - -export type AstStatLocalFunction = luteLuau.AstStatLocalFunction - -export type AstStatTypeAlias = luteLuau.AstStatTypeAlias - -export type AstStatTypeFunction = luteLuau.AstStatTypeFunction - -export type AstStat = luteLuau.AstStat - -export type AstGenericType = luteLuau.AstGenericType - -export type AstGenericTypePack = luteLuau.AstGenericTypePack - -export type AstTypeReference = luteLuau.AstTypeReference - -export type AstTypeSingletonBool = luteLuau.AstTypeSingletonBool - -export type AstTypeSingletonString = luteLuau.AstTypeSingletonString - -export type AstTypeTypeof = luteLuau.AstTypeTypeof - -export type AstTypeGroup = luteLuau.AstTypeGroup - -export type AstTypeOptional = luteLuau.AstTypeOptional - -export type AstTypeUnion = luteLuau.AstTypeUnion - -export type AstTypeIntersection = luteLuau.AstTypeIntersection - -export type AstTypeArray = luteLuau.AstTypeArray - -export type AstTypeTableItemIndexer = luteLuau.AstTypeTableItemIndexer - -export type AstTypeTableItemStringProperty = luteLuau.AstTypeTableItemStringProperty - -export type AstTypeTableItemProperty = luteLuau.AstTypeTableItemProperty - -export type AstTypeTableItem = luteLuau.AstTypeTableItem - -export type AstTypeTable = luteLuau.AstTypeTable - -export type AstTypeFunctionParameter = luteLuau.AstTypeFunctionParameter - -export type AstTypeFunction = luteLuau.AstTypeFunction - -export type AstType = luteLuau.AstType - -export type AstTypePackExplicit = luteLuau.AstTypePackExplicit - -export type AstTypePackGeneric = luteLuau.AstTypePackGeneric - -export type AstTypePackVariadic = luteLuau.AstTypePackVariadic - -export type AstTypePack = luteLuau.AstTypePack - -export type AstNode = luteLuau.AstNode - -export type ParseResult = luteLuau.ParseResult - export type Bytecode = luteLuau.Bytecode -function luau.parse(source: string): ParseResult - return luteLuau.parse(source) -end - -function luau.parseexpr(source: string): AstExpr - return luteLuau.parseexpr(source) -end - function luau.compile(source: string): Bytecode return luteLuau.compile(source) end diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau new file mode 100644 index 000000000..9ba850e4c --- /dev/null +++ b/lute/std/libs/syntax/init.luau @@ -0,0 +1,162 @@ +local parser = require("@self/parser") +local types = require("@self/types") + +export type Position = types.Position +export type Location = types.Location + +export type Whitespace = types.Whitespace +export type SingleLineComment = types.SingleLineComment +export type MultiLineComment = types.MultiLineComment + +export type Trivia = types.Trivia + +export type Token = types.Token + +export type Eof = types.Eof + +export type Pair = types.Pair +export type Punctuated = types.Punctuated + +export type AstLocal = types.AstLocal + +export type AstExprGroup = types.AstExprGroup + +export type AstExprConstantNil = types.AstExprConstantNil + +export type AstExprConstantBool = types.AstExprConstantBool + +export type AstExprConstantNumber = types.AstExprConstantNumber + +export type AstExprConstantString = types.AstExprConstantString + +export type AstExprLocal = types.AstExprLocal + +export type AstExprGlobal = types.AstExprGlobal + +export type AstExprVarargs = types.AstExprVarargs + +export type AstExprCall = types.AstExprCall + +export type AstExprIndexName = types.AstExprIndexName + +export type AstExprIndexExpr = types.AstExprIndexExpr + +export type AstFunctionBody = types.AstFunctionBody + +export type AstExprAnonymousFunction = types.AstExprAnonymousFunction + +export type AstExprTableItemList = types.AstExprTableItemList + +export type AstExprTableItemRecord = types.AstExprTableItemRecord + +export type AstExprTableItemGeneral = types.AstExprTableItemGeneral + +export type AstExprTableItem = types.AstExprTableItem + +export type AstExprTable = types.AstExprTable + +export type AstExprUnary = types.AstExprUnary + +export type AstExprBinary = types.AstExprBinary + +export type AstExprInterpString = types.AstExprInterpString + +export type AstExprTypeAssertion = types.AstExprTypeAssertion + +export type AstElseIfExpr = types.AstElseIfExpr + +export type AstExprIfElse = types.AstExprIfElse + +export type AstExpr = types.AstExpr + +export type AstStatBlock = types.AstStatBlock + +export type AstElseIfStat = types.AstElseIfStat + +export type AstStatIf = types.AstStatIf + +export type AstStatWhile = types.AstStatWhile + +export type AstStatRepeat = types.AstStatRepeat + +export type AstStatBreak = types.AstStatBreak + +export type AstStatContinue = types.AstStatContinue + +export type AstStatReturn = types.AstStatReturn + +export type AstStatExpr = types.AstStatExpr + +export type AstStatLocal = types.AstStatLocal + +export type AstStatFor = types.AstStatFor + +export type AstStatForIn = types.AstStatForIn + +export type AstStatAssign = types.AstStatAssign + +export type AstStatCompoundAssign = types.AstStatCompoundAssign + +export type AstAttribute = types.AstAttribute + +export type AstStatFunction = types.AstStatFunction + +export type AstStatLocalFunction = types.AstStatLocalFunction + +export type AstStatTypeAlias = types.AstStatTypeAlias + +export type AstStatTypeFunction = types.AstStatTypeFunction + +export type AstStat = types.AstStat + +export type AstGenericType = types.AstGenericType + +export type AstGenericTypePack = types.AstGenericTypePack + +export type AstTypeReference = types.AstTypeReference + +export type AstTypeSingletonBool = types.AstTypeSingletonBool + +export type AstTypeSingletonString = types.AstTypeSingletonString + +export type AstTypeTypeof = types.AstTypeTypeof + +export type AstTypeGroup = types.AstTypeGroup + +export type AstTypeOptional = types.AstTypeOptional + +export type AstTypeUnion = types.AstTypeUnion + +export type AstTypeIntersection = types.AstTypeIntersection + +export type AstTypeArray = types.AstTypeArray + +export type AstTypeTableItemIndexer = types.AstTypeTableItemIndexer + +export type AstTypeTableItemStringProperty = types.AstTypeTableItemStringProperty + +export type AstTypeTableItemProperty = types.AstTypeTableItemProperty + +export type AstTypeTableItem = types.AstTypeTableItem + +export type AstTypeTable = types.AstTypeTable + +export type AstTypeFunctionParameter = types.AstTypeFunctionParameter + +export type AstTypeFunction = types.AstTypeFunction + +export type AstType = types.AstType + +export type AstTypePackExplicit = types.AstTypePackExplicit + +export type AstTypePackGeneric = types.AstTypePackGeneric + +export type AstTypePackVariadic = types.AstTypePackVariadic + +export type AstTypePack = types.AstTypePack + +export type AstNode = types.AstNode + +export type ParseResult = types.ParseResult + +return table.freeze({ parse = parser.parse, parseexpr = parser.parseexpr, parsefile = parser.parsefile }) diff --git a/lute/std/libs/syntax/parser.luau b/lute/std/libs/syntax/parser.luau index bd33268ff..2ad15a371 100644 --- a/lute/std/libs/syntax/parser.luau +++ b/lute/std/libs/syntax/parser.luau @@ -2,27 +2,22 @@ -- @std/syntax/parser -- Parser for Luau source code into Luau AST nodes -local luau = require("@std/luau") +local luau = require("@lute/luau") +local types = require("./types") local parser = {} --- Parses Luau source code into an AstStatBlock -function parser.parse(source: string): luau.AstStatBlock +function parser.parse(source: string): types.AstStatBlock return luau.parse(source).root end -function parser.parseexpr(source: string): luau.AstExpr +function parser.parseexpr(source: string): types.AstExpr return luau.parseexpr(source) end -export type ParseResult = { - root: luau.AstStatBlock, - eof: luau.Eof, -} - -function parser.parsefile(source: string): ParseResult - local result = luau.parse(source) - return { root = result.root, eof = result.eof } +function parser.parsefile(source: string): types.ParseResult + return luau.parse(source) end return table.freeze(parser) diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index b77ff34cf..52c1f7ea4 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -1,7 +1,6 @@ --!strict -- @std/syntax/printer -local luau = require("@std/luau") local types = require("./types") local visitor = require("./visitor") @@ -23,7 +22,7 @@ local function exhaustiveMatch(value: never): never error(`Unknown value in exhaustive match: {value}`) end -local function printTrivia(self: PrintVisitor, trivia: luau.Trivia) +local function printTrivia(self: PrintVisitor, trivia: types.Trivia) if trivia.tag == "whitespace" or trivia.tag == "comment" or trivia.tag == "blockcomment" then self:write(trivia.text) else @@ -31,19 +30,19 @@ local function printTrivia(self: PrintVisitor, trivia: luau.Trivia) end end -local function printTriviaList(self: PrintVisitor, trivia: { luau.Trivia }) +local function printTriviaList(self: PrintVisitor, trivia: { types.Trivia }) for _, trivia in trivia do self:printTrivia(trivia) end end -local function printToken(self: PrintVisitor, token: luau.Token) +local function printToken(self: PrintVisitor, token: types.Token) self:printTriviaList(token.leadingtrivia) self:write(token.text) self:printTriviaList(token.trailingtrivia) end -local function printString(self: PrintVisitor, expr: luau.AstExprConstantString | luau.AstTypeSingletonString) +local function printString(self: PrintVisitor, expr: types.AstExprConstantString | types.AstTypeSingletonString) self:printTriviaList(expr.leadingtrivia) if expr.quotestyle == "single" then @@ -62,7 +61,7 @@ local function printString(self: PrintVisitor, expr: luau.AstExprConstantString self:printTriviaList(expr.trailingtrivia) end -local function printInterpolatedString(self: PrintVisitor, expr: luau.AstExprInterpString) +local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprInterpString) for i = 1, #expr.strings do self:printTriviaList(expr.strings[i].leadingtrivia) if i == 1 then @@ -119,7 +118,7 @@ local function printVisitor(replacements: types.replacements?) local printer: PrintVisitor = {} :: PrintVisitor -- The visitor library doesn't use methods, so we access replacements through a closure - local function maybeReplace(node: luau.AstNode): boolean + local function maybeReplace(node: types.AstNode): boolean if replacements == nil then return true end @@ -181,7 +180,7 @@ local function printVisitor(replacements: types.replacements?) printer.visitTypeReference = maybeReplace printer.visitTypeBoolean = maybeReplace - printer.visitTypeString = function(node: luau.AstTypeSingletonString) + printer.visitTypeString = function(node: types.AstTypeSingletonString) printer:printString(node) return false end @@ -197,12 +196,12 @@ local function printVisitor(replacements: types.replacements?) printer.visitTypePackGeneric = maybeReplace printer.visitTypePackVariadic = maybeReplace - printer.visitToken = function(node: luau.Token) + printer.visitToken = function(node: types.Token) printer:printToken(node) return false end printer.visitNil = maybeReplace - printer.visitString = function(node: luau.AstExprConstantString) + printer.visitString = function(node: types.AstExprConstantString) printer:printString(node) return false end @@ -221,7 +220,7 @@ function printerLib.printnode(node: luau.AstNode, replacements: types.replacemen end function printerLib.printfile( - result: { root: luau.AstStatBlock, eof: luau.Eof }, + result: { root: types.AstStatBlock, eof: types.Eof }, replacements: types.replacements? ): string local printer = printVisitor(replacements) diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 6ba6a79b1..d56dd3d53 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -3,11 +3,11 @@ -- Provides utility functions for querying Luau AST nodes local luau = require("@std/luau") -local printer = require("@std/syntax/printer") -local types = require("@std/syntax/types") -local visitor = require("@std/syntax/visitor") +local printer = require("./printer") +local types = require("./types") +local visitor = require("./visitor") -type node = luau.AstNode +type node = types.AstNode export type query = { nodes: { T }, diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index 784f8eb3f..47ba6b61a 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -1,8 +1,164 @@ -local luau = require("@std/luau") +local luau = require("@lute/luau") -type node = luau.AstNode +export type Position = luau.Position +export type Location = luau.Location -export type replacement = string | node -export type replacements = { [node]: replacement, print: (self: replacements) -> string, __root: node } +export type Whitespace = luau.Whitespace +export type SingleLineComment = luau.SingleLineComment +export type MultiLineComment = luau.MultiLineComment + +export type Trivia = luau.Trivia + +export type Token = luau.Token + +export type Eof = luau.Eof + +export type Pair = luau.Pair +export type Punctuated = luau.Punctuated + +export type AstLocal = luau.AstLocal + +export type AstExprGroup = luau.AstExprGroup + +export type AstExprConstantNil = luau.AstExprConstantNil + +export type AstExprConstantBool = luau.AstExprConstantBool + +export type AstExprConstantNumber = luau.AstExprConstantNumber + +export type AstExprConstantString = luau.AstExprConstantString + +export type AstExprLocal = luau.AstExprLocal + +export type AstExprGlobal = luau.AstExprGlobal + +export type AstExprVarargs = luau.AstExprVarargs + +export type AstExprCall = luau.AstExprCall + +export type AstExprIndexName = luau.AstExprIndexName + +export type AstExprIndexExpr = luau.AstExprIndexExpr + +export type AstFunctionBody = luau.AstFunctionBody + +export type AstExprAnonymousFunction = luau.AstExprAnonymousFunction + +export type AstExprTableItemList = luau.AstExprTableItemList + +export type AstExprTableItemRecord = luau.AstExprTableItemRecord + +export type AstExprTableItemGeneral = luau.AstExprTableItemGeneral + +export type AstExprTableItem = luau.AstExprTableItem + +export type AstExprTable = luau.AstExprTable + +export type AstExprUnary = luau.AstExprUnary + +export type AstExprBinary = luau.AstExprBinary + +export type AstExprInterpString = luau.AstExprInterpString + +export type AstExprTypeAssertion = luau.AstExprTypeAssertion + +export type AstElseIfExpr = luau.AstElseIfExpr + +export type AstExprIfElse = luau.AstExprIfElse + +export type AstExpr = luau.AstExpr + +export type AstStatBlock = luau.AstStatBlock + +export type AstElseIfStat = luau.AstElseIfStat + +export type AstStatIf = luau.AstStatIf + +export type AstStatWhile = luau.AstStatWhile + +export type AstStatRepeat = luau.AstStatRepeat + +export type AstStatBreak = luau.AstStatBreak + +export type AstStatContinue = luau.AstStatContinue + +export type AstStatReturn = luau.AstStatReturn + +export type AstStatExpr = luau.AstStatExpr + +export type AstStatLocal = luau.AstStatLocal + +export type AstStatFor = luau.AstStatFor + +export type AstStatForIn = luau.AstStatForIn + +export type AstStatAssign = luau.AstStatAssign + +export type AstStatCompoundAssign = luau.AstStatCompoundAssign + +export type AstAttribute = luau.AstAttribute + +export type AstStatFunction = luau.AstStatFunction + +export type AstStatLocalFunction = luau.AstStatLocalFunction + +export type AstStatTypeAlias = luau.AstStatTypeAlias + +export type AstStatTypeFunction = luau.AstStatTypeFunction + +export type AstStat = luau.AstStat + +export type AstGenericType = luau.AstGenericType + +export type AstGenericTypePack = luau.AstGenericTypePack + +export type AstTypeReference = luau.AstTypeReference + +export type AstTypeSingletonBool = luau.AstTypeSingletonBool + +export type AstTypeSingletonString = luau.AstTypeSingletonString + +export type AstTypeTypeof = luau.AstTypeTypeof + +export type AstTypeGroup = luau.AstTypeGroup + +export type AstTypeOptional = luau.AstTypeOptional + +export type AstTypeUnion = luau.AstTypeUnion + +export type AstTypeIntersection = luau.AstTypeIntersection + +export type AstTypeArray = luau.AstTypeArray + +export type AstTypeTableItemIndexer = luau.AstTypeTableItemIndexer + +export type AstTypeTableItemStringProperty = luau.AstTypeTableItemStringProperty + +export type AstTypeTableItemProperty = luau.AstTypeTableItemProperty + +export type AstTypeTableItem = luau.AstTypeTableItem + +export type AstTypeTable = luau.AstTypeTable + +export type AstTypeFunctionParameter = luau.AstTypeFunctionParameter + +export type AstTypeFunction = luau.AstTypeFunction + +export type AstType = luau.AstType + +export type AstTypePackExplicit = luau.AstTypePackExplicit + +export type AstTypePackGeneric = luau.AstTypePackGeneric + +export type AstTypePackVariadic = luau.AstTypePackVariadic + +export type AstTypePack = luau.AstTypePack + +export type AstNode = luau.AstNode + +export type ParseResult = luau.ParseResult + +export type replacement = string | AstNode +export type replacements = { [AstNode]: replacement, print: (self: replacements) -> string, __root: AstNode } return {} diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index c3174c675..298a30c60 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -2,67 +2,67 @@ -- @std/syntax/visitor -- Visitor pattern implementation for traversing Luau AST nodes -local luau = require("@std/luau") +local types = require("./types") local visitorlib = {} export type Visitor = { - visitBlock: (luau.AstStatBlock) -> boolean, - visitBlockEnd: (luau.AstStatBlock) -> (), - visitIf: (luau.AstStatIf) -> boolean, - visitWhile: (luau.AstStatWhile) -> boolean, - visitRepeat: (luau.AstStatRepeat) -> boolean, - visitReturn: (luau.AstStatReturn) -> boolean, - visitLocalDeclaration: (luau.AstStatLocal) -> boolean, - visitLocalDeclarationEnd: (luau.AstStatLocal) -> (), - visitFor: (luau.AstStatFor) -> boolean, - visitForIn: (luau.AstStatForIn) -> boolean, - visitAssign: (luau.AstStatAssign) -> boolean, - visitCompoundAssign: (luau.AstStatCompoundAssign) -> boolean, - visitFunction: (luau.AstStatFunction) -> boolean, - visitLocalFunction: (luau.AstStatLocalFunction) -> boolean, - visitTypeAlias: (luau.AstStatTypeAlias) -> boolean, - visitStatTypeFunction: (luau.AstStatTypeFunction) -> boolean, - - visitExpression: (luau.AstExpr) -> boolean, - visitExpressionEnd: (luau.AstExpr) -> (), - visitLocalReference: (luau.AstExprLocal) -> boolean, - visitGlobal: (luau.AstExprGlobal) -> boolean, - visitCall: (luau.AstExprCall) -> boolean, - visitUnary: (luau.AstExprUnary) -> boolean, - visitBinary: (luau.AstExprBinary) -> boolean, - visitAnonymousFunction: (luau.AstExprAnonymousFunction) -> boolean, - visitTableItem: (luau.AstExprTableItem) -> boolean, - visitTable: (luau.AstExprTable) -> boolean, - visitIndexName: (luau.AstExprIndexName) -> boolean, - visitIndexExpr: (luau.AstExprIndexExpr) -> boolean, - visitGroup: (luau.AstExprGroup) -> boolean, - visitInterpolatedString: (luau.AstExprInterpString) -> boolean, - visitTypeAssertion: (luau.AstExprTypeAssertion) -> boolean, - visitIfExpression: (luau.AstExprIfElse) -> boolean, - - visitTypeReference: (luau.AstTypeReference) -> boolean, - visitTypeBoolean: (luau.AstTypeSingletonBool) -> boolean, - visitTypeString: (luau.AstTypeSingletonString) -> boolean, - visitTypeTypeof: (luau.AstTypeTypeof) -> boolean, - visitTypeGroup: (luau.AstTypeGroup) -> boolean, - visitTypeUnion: (luau.AstTypeUnion) -> boolean, - visitTypeIntersection: (luau.AstTypeIntersection) -> boolean, - visitTypeArray: (luau.AstTypeArray) -> boolean, - visitTypeTable: (luau.AstTypeTable) -> boolean, - visitTypeFunction: (luau.AstTypeFunction) -> boolean, - - visitTypePackExplicit: (luau.AstTypePackExplicit) -> boolean, - visitTypePackGeneric: (luau.AstTypePackGeneric) -> boolean, - visitTypePackVariadic: (luau.AstTypePackVariadic) -> boolean, - - visitToken: (luau.Token) -> boolean, - visitNil: (luau.AstExprConstantNil) -> boolean, - visitString: (luau.AstExprConstantString) -> boolean, - visitBoolean: (luau.AstExprConstantBool) -> boolean, - visitNumber: (luau.AstExprConstantNumber) -> boolean, - visitLocal: (luau.AstLocal) -> boolean, - visitVarargs: (luau.AstExprVarargs) -> boolean, + visitBlock: (types.AstStatBlock) -> boolean, + visitBlockEnd: (types.AstStatBlock) -> (), + visitIf: (types.AstStatIf) -> boolean, + visitWhile: (types.AstStatWhile) -> boolean, + visitRepeat: (types.AstStatRepeat) -> boolean, + visitReturn: (types.AstStatReturn) -> boolean, + visitLocalDeclaration: (types.AstStatLocal) -> boolean, + visitLocalDeclarationEnd: (types.AstStatLocal) -> (), + visitFor: (types.AstStatFor) -> boolean, + visitForIn: (types.AstStatForIn) -> boolean, + visitAssign: (types.AstStatAssign) -> boolean, + visitCompoundAssign: (types.AstStatCompoundAssign) -> boolean, + visitFunction: (types.AstStatFunction) -> boolean, + visitLocalFunction: (types.AstStatLocalFunction) -> boolean, + visitTypeAlias: (types.AstStatTypeAlias) -> boolean, + visitStatTypeFunction: (types.AstStatTypeFunction) -> boolean, + + visitExpression: (types.AstExpr) -> boolean, + visitExpressionEnd: (types.AstExpr) -> (), + visitLocalReference: (types.AstExprLocal) -> boolean, + visitGlobal: (types.AstExprGlobal) -> boolean, + visitCall: (types.AstExprCall) -> boolean, + visitUnary: (types.AstExprUnary) -> boolean, + visitBinary: (types.AstExprBinary) -> boolean, + visitAnonymousFunction: (types.AstExprAnonymousFunction) -> boolean, + visitTableItem: (types.AstExprTableItem) -> boolean, + visitTable: (types.AstExprTable) -> boolean, + visitIndexName: (types.AstExprIndexName) -> boolean, + visitIndexExpr: (types.AstExprIndexExpr) -> boolean, + visitGroup: (types.AstExprGroup) -> boolean, + visitInterpolatedString: (types.AstExprInterpString) -> boolean, + visitTypeAssertion: (types.AstExprTypeAssertion) -> boolean, + visitIfExpression: (types.AstExprIfElse) -> boolean, + + visitTypeReference: (types.AstTypeReference) -> boolean, + visitTypeBoolean: (types.AstTypeSingletonBool) -> boolean, + visitTypeString: (types.AstTypeSingletonString) -> boolean, + visitTypeTypeof: (types.AstTypeTypeof) -> boolean, + visitTypeGroup: (types.AstTypeGroup) -> boolean, + visitTypeUnion: (types.AstTypeUnion) -> boolean, + visitTypeIntersection: (types.AstTypeIntersection) -> boolean, + visitTypeArray: (types.AstTypeArray) -> boolean, + visitTypeTable: (types.AstTypeTable) -> boolean, + visitTypeFunction: (types.AstTypeFunction) -> boolean, + + visitTypePackExplicit: (types.AstTypePackExplicit) -> boolean, + visitTypePackGeneric: (types.AstTypePackGeneric) -> boolean, + visitTypePackVariadic: (types.AstTypePackVariadic) -> boolean, + + visitToken: (types.Token) -> boolean, + visitNil: (types.AstExprConstantNil) -> boolean, + visitString: (types.AstExprConstantString) -> boolean, + visitBoolean: (types.AstExprConstantBool) -> boolean, + visitNumber: (types.AstExprConstantNumber) -> boolean, + visitLocal: (types.AstLocal) -> boolean, + visitVarargs: (types.AstExprVarargs) -> boolean, } local function alwaysVisit(...: any) @@ -132,11 +132,11 @@ local function exhaustiveMatch(value: never): never error(`Unknown value in exhaustive match: {value}`) end -local function visitToken(token: luau.Token, visitor: Visitor) +local function visitToken(token: types.Token, visitor: Visitor) visitor.visitToken(token) end -local function visitPunctuated(list: luau.Punctuated, visitor: Visitor, apply: (T, Visitor) -> ()) +local function visitPunctuated(list: types.Punctuated, visitor: Visitor, apply: (T, Visitor) -> ()) for _, item in list do apply(item.node, visitor) if item.separator then @@ -145,7 +145,7 @@ local function visitPunctuated(list: luau.Punctuated end end -local function visitLocal(node: luau.AstLocal, visitor: Visitor) +local function visitLocal(node: types.AstLocal, visitor: Visitor) if visitor.visitLocal(node) then visitToken(node.name, visitor) if node.colon then @@ -157,7 +157,7 @@ local function visitLocal(node: luau.AstLocal, visitor: Visitor) end end -local function visitBlock(block: luau.AstStatBlock, visitor: Visitor) +local function visitBlock(block: types.AstStatBlock, visitor: Visitor) if visitor.visitBlock(block) then for _, statement in block.statements do visitStatement(statement, visitor) @@ -167,7 +167,7 @@ local function visitBlock(block: luau.AstStatBlock, visitor: Visitor) end end -local function visitIf(node: luau.AstStatIf, visitor: Visitor) +local function visitIf(node: types.AstStatIf, visitor: Visitor) if visitor.visitIf(node) then visitToken(node.ifkeyword, visitor) visitExpression(node.condition, visitor) @@ -189,7 +189,7 @@ local function visitIf(node: luau.AstStatIf, visitor: Visitor) end end -local function visitWhile(node: luau.AstStatWhile, visitor: Visitor) +local function visitWhile(node: types.AstStatWhile, visitor: Visitor) if visitor.visitWhile(node) then visitToken(node.whilekeyword, visitor) visitExpression(node.condition, visitor) @@ -199,7 +199,7 @@ local function visitWhile(node: luau.AstStatWhile, visitor: Visitor) end end -local function visitRepeat(node: luau.AstStatRepeat, visitor: Visitor) +local function visitRepeat(node: types.AstStatRepeat, visitor: Visitor) if visitor.visitRepeat(node) then visitToken(node.repeatKeyword, visitor) visitBlock(node.body, visitor) @@ -208,14 +208,14 @@ local function visitRepeat(node: luau.AstStatRepeat, visitor: Visitor) end end -local function visitReturn(node: luau.AstStatReturn, visitor: Visitor) +local function visitReturn(node: types.AstStatReturn, visitor: Visitor) if visitor.visitReturn(node) then visitToken(node.returnkeyword, visitor) visitPunctuated(node.expressions, visitor, visitExpression) end end -local function visitLocalStatement(node: luau.AstStatLocal, visitor: Visitor) +local function visitLocalStatement(node: types.AstStatLocal, visitor: Visitor) if visitor.visitLocalDeclaration(node) then visitToken(node.localkeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) @@ -228,7 +228,7 @@ local function visitLocalStatement(node: luau.AstStatLocal, visitor: Visitor) end end -local function visitFor(node: luau.AstStatFor, visitor: Visitor) +local function visitFor(node: types.AstStatFor, visitor: Visitor) if visitor.visitFor(node) then visitToken(node.forkeyword, visitor) visitLocal(node.variable, visitor) @@ -248,7 +248,7 @@ local function visitFor(node: luau.AstStatFor, visitor: Visitor) end end -local function visitForIn(node: luau.AstStatForIn, visitor: Visitor) +local function visitForIn(node: types.AstStatForIn, visitor: Visitor) if visitor.visitForIn(node) then visitToken(node.forkeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) @@ -260,7 +260,7 @@ local function visitForIn(node: luau.AstStatForIn, visitor: Visitor) end end -local function visitAssign(node: luau.AstStatAssign, visitor: Visitor) +local function visitAssign(node: types.AstStatAssign, visitor: Visitor) if visitor.visitAssign(node) then visitPunctuated(node.variables, visitor, visitExpression) visitToken(node.equals, visitor) @@ -268,7 +268,7 @@ local function visitAssign(node: luau.AstStatAssign, visitor: Visitor) end end -local function visitCompoundAssign(node: luau.AstStatCompoundAssign, visitor: Visitor) +local function visitCompoundAssign(node: types.AstStatCompoundAssign, visitor: Visitor) if visitor.visitCompoundAssign(node) then visitExpression(node.variable, visitor) visitToken(node.operand, visitor) @@ -276,7 +276,7 @@ local function visitCompoundAssign(node: luau.AstStatCompoundAssign, visitor: Vi end end -local function visitGeneric(node: luau.AstGenericType, visitor: Visitor) +local function visitGeneric(node: types.AstGenericType, visitor: Visitor) visitToken(node.name, visitor) if node.equals then visitToken(node.equals, visitor) @@ -286,7 +286,7 @@ local function visitGeneric(node: luau.AstGenericType, visitor: Visitor) end end -local function visitGenericPack(node: luau.AstGenericTypePack, visitor: Visitor) +local function visitGenericPack(node: types.AstGenericTypePack, visitor: Visitor) visitToken(node.name, visitor) visitToken(node.ellipsis, visitor) if node.equals then @@ -297,7 +297,7 @@ local function visitGenericPack(node: luau.AstGenericTypePack, visitor: Visitor) end end -local function visitTypeAlias(node: luau.AstStatTypeAlias, visitor: Visitor) +local function visitTypeAlias(node: types.AstStatTypeAlias, visitor: Visitor) if visitor.visitTypeAlias(node) then if node.export then visitToken(node.export, visitor) @@ -321,49 +321,49 @@ local function visitTypeAlias(node: luau.AstStatTypeAlias, visitor: Visitor) end end -local function visitString(node: luau.AstExprConstantString, visitor: Visitor) +local function visitString(node: types.AstExprConstantString, visitor: Visitor) if visitor.visitString(node) then visitor.visitToken(node) end end -local function visitNil(node: luau.AstExprConstantNil, visitor: Visitor) +local function visitNil(node: types.AstExprConstantNil, visitor: Visitor) if visitor.visitNil(node) then visitToken(node, visitor) end end -local function visitBoolean(node: luau.AstExprConstantBool, visitor: Visitor) +local function visitBoolean(node: types.AstExprConstantBool, visitor: Visitor) if visitor.visitBoolean(node) then visitToken(node, visitor) end end -local function visitNumber(node: luau.AstExprConstantNumber, visitor: Visitor) +local function visitNumber(node: types.AstExprConstantNumber, visitor: Visitor) if visitor.visitNumber(node) then visitToken(node, visitor) end end -local function visitLocalReference(node: luau.AstExprLocal, visitor: Visitor) +local function visitLocalReference(node: types.AstExprLocal, visitor: Visitor) if visitor.visitLocalReference(node) then visitor.visitToken(node.token) end end -local function visitGlobal(node: luau.AstExprGlobal, visitor: Visitor) +local function visitGlobal(node: types.AstExprGlobal, visitor: Visitor) if visitor.visitGlobal(node) then visitor.visitToken(node.name) end end -local function visitVarargs(node: luau.AstExprVarargs, visitor: Visitor) +local function visitVarargs(node: types.AstExprVarargs, visitor: Visitor) if visitor.visitVarargs(node) then visitToken(node, visitor) end end -local function visitCall(node: luau.AstExprCall, visitor: Visitor) +local function visitCall(node: types.AstExprCall, visitor: Visitor) if visitor.visitCall(node) then visitExpression(node.func, visitor) if node.openparens then @@ -376,14 +376,14 @@ local function visitCall(node: luau.AstExprCall, visitor: Visitor) end end -local function visitUnary(node: luau.AstExprUnary, visitor: Visitor) +local function visitUnary(node: types.AstExprUnary, visitor: Visitor) if visitor.visitUnary(node) then visitToken(node.operator, visitor) visitExpression(node.operand, visitor) end end -local function visitBinary(node: luau.AstExprBinary, visitor: Visitor) +local function visitBinary(node: types.AstExprBinary, visitor: Visitor) if visitor.visitBinary(node) then visitExpression(node.lhsoperand, visitor) visitToken(node.operator, visitor) @@ -391,7 +391,7 @@ local function visitBinary(node: luau.AstExprBinary, visitor: Visitor) end end -local function visitFunctionBody(node: luau.AstFunctionBody, visitor: Visitor) +local function visitFunctionBody(node: types.AstFunctionBody, visitor: Visitor) if node.opengenerics then visitToken(node.opengenerics, visitor) end @@ -426,11 +426,11 @@ local function visitFunctionBody(node: luau.AstFunctionBody, visitor: Visitor) visitToken(node.endkeyword, visitor) end -local function visitAttribute(node: luau.AstAttribute, visitor) +local function visitAttribute(node: types.AstAttribute, visitor) visitToken(node, visitor) end -local function visitAnonymousFunction(node: luau.AstExprAnonymousFunction, visitor: Visitor) +local function visitAnonymousFunction(node: types.AstExprAnonymousFunction, visitor: Visitor) if visitor.visitAnonymousFunction(node) then for _, attribute in node.attributes do visitAttribute(attribute, visitor) @@ -440,7 +440,7 @@ local function visitAnonymousFunction(node: luau.AstExprAnonymousFunction, visit end end -local function visitFunction(node: luau.AstStatFunction, visitor: Visitor) +local function visitFunction(node: types.AstStatFunction, visitor: Visitor) if visitor.visitFunction(node) then for _, attribute in node.attributes do visitAttribute(attribute, visitor) @@ -451,7 +451,7 @@ local function visitFunction(node: luau.AstStatFunction, visitor: Visitor) end end -local function visitLocalFunction(node: luau.AstStatLocalFunction, visitor: Visitor) +local function visitLocalFunction(node: types.AstStatLocalFunction, visitor: Visitor) if visitor.visitLocalFunction(node) then for _, attribute in node.attributes do visitAttribute(attribute, visitor) @@ -463,7 +463,7 @@ local function visitLocalFunction(node: luau.AstStatLocalFunction, visitor: Visi end end -local function visitStatTypeFunction(node: luau.AstStatTypeFunction, visitor: Visitor) +local function visitStatTypeFunction(node: types.AstStatTypeFunction, visitor: Visitor) if visitor.visitStatTypeFunction(node) then if node.export then visitToken(node.export, visitor) @@ -475,7 +475,7 @@ local function visitStatTypeFunction(node: luau.AstStatTypeFunction, visitor: Vi end end -local function visitTableItem(node: luau.AstExprTableItem, visitor: Visitor) +local function visitTableItem(node: types.AstExprTableItem, visitor: Visitor) if visitor.visitTableItem(node) then if node.kind == "list" then visitExpression(node.value, visitor) @@ -499,7 +499,7 @@ local function visitTableItem(node: luau.AstExprTableItem, visitor: Visitor) end end -local function visitTable(node: luau.AstExprTable, visitor: Visitor) +local function visitTable(node: types.AstExprTable, visitor: Visitor) if visitor.visitTable(node) then visitToken(node.openbrace, visitor) for _, item in node.entries do @@ -509,7 +509,7 @@ local function visitTable(node: luau.AstExprTable, visitor: Visitor) end end -local function visitIndexName(node: luau.AstExprIndexName, visitor: Visitor) +local function visitIndexName(node: types.AstExprIndexName, visitor: Visitor) if visitor.visitIndexName(node) then visitExpression(node.expression, visitor) visitToken(node.accessor, visitor) @@ -517,7 +517,7 @@ local function visitIndexName(node: luau.AstExprIndexName, visitor: Visitor) end end -local function visitIndexExpr(node: luau.AstExprIndexExpr, visitor: Visitor) +local function visitIndexExpr(node: types.AstExprIndexExpr, visitor: Visitor) if visitor.visitIndexExpr(node) then visitExpression(node.expression, visitor) visitToken(node.openbrackets, visitor) @@ -526,7 +526,7 @@ local function visitIndexExpr(node: luau.AstExprIndexExpr, visitor: Visitor) end end -local function visitGroup(node: luau.AstExprGroup, visitor: Visitor) +local function visitGroup(node: types.AstExprGroup, visitor: Visitor) if visitor.visitGroup(node) then visitToken(node.openparens, visitor) visitExpression(node.expression, visitor) @@ -534,7 +534,7 @@ local function visitGroup(node: luau.AstExprGroup, visitor: Visitor) end end -local function visitInterpolatedString(node: luau.AstExprInterpString, visitor: Visitor) +local function visitInterpolatedString(node: types.AstExprInterpString, visitor: Visitor) if visitor.visitInterpolatedString(node) then for i = 1, #node.strings do visitToken(node.strings[i], visitor) @@ -545,7 +545,7 @@ local function visitInterpolatedString(node: luau.AstExprInterpString, visitor: end end -local function visitTypeAssertion(node: luau.AstExprTypeAssertion, visitor: Visitor) +local function visitTypeAssertion(node: types.AstExprTypeAssertion, visitor: Visitor) if visitor.visitTypeAssertion(node) then visitExpression(node.operand, visitor) visitToken(node.operator, visitor) @@ -553,7 +553,7 @@ local function visitTypeAssertion(node: luau.AstExprTypeAssertion, visitor: Visi end end -local function visitIfExpression(node: luau.AstExprIfElse, visitor: Visitor) +local function visitIfExpression(node: types.AstExprIfElse, visitor: Visitor) if visitor.visitIfExpression(node) then visitToken(node.ifkeyword, visitor) visitExpression(node.condition, visitor) @@ -570,7 +570,7 @@ local function visitIfExpression(node: luau.AstExprIfElse, visitor: Visitor) end end -local function visitTypeOrPack(node: luau.AstType | luau.AstTypePack, visitor: Visitor) +local function visitTypeOrPack(node: types.AstType | types.AstTypePack, visitor: Visitor) if node.tag == "explicit" or node.tag == "generic" or node.tag == "variadic" then visitTypePack(node, visitor) else @@ -578,7 +578,7 @@ local function visitTypeOrPack(node: luau.AstType | luau.AstTypePack, visitor: V end end -local function visitTypeReference(node: luau.AstTypeReference, visitor: Visitor) +local function visitTypeReference(node: types.AstTypeReference, visitor: Visitor) if visitor.visitTypeReference(node) then if node.prefix then visitToken(node.prefix, visitor) @@ -599,19 +599,19 @@ local function visitTypeReference(node: luau.AstTypeReference, visitor: Visitor) end end -local function visitTypeBoolean(node: luau.AstTypeSingletonBool, visitor: Visitor) +local function visitTypeBoolean(node: types.AstTypeSingletonBool, visitor: Visitor) if visitor.visitTypeBoolean(node) then visitToken(node, visitor) end end -local function visitTypeString(node: luau.AstTypeSingletonString, visitor: Visitor) +local function visitTypeString(node: types.AstTypeSingletonString, visitor: Visitor) if visitor.visitTypeString(node) then visitToken(node, visitor) end end -local function visitTypeTypeof(node: luau.AstTypeTypeof, visitor: Visitor) +local function visitTypeTypeof(node: types.AstTypeTypeof, visitor: Visitor) if visitor.visitTypeTypeof(node) then visitToken(node.typeof, visitor) visitToken(node.openparens, visitor) @@ -620,7 +620,7 @@ local function visitTypeTypeof(node: luau.AstTypeTypeof, visitor: Visitor) end end -local function visitTypeGroup(node: luau.AstTypeGroup, visitor: Visitor) +local function visitTypeGroup(node: types.AstTypeGroup, visitor: Visitor) if visitor.visitTypeGroup(node) then visitToken(node.openparens, visitor) visitType(node.type, visitor) @@ -628,7 +628,7 @@ local function visitTypeGroup(node: luau.AstTypeGroup, visitor: Visitor) end end -local function visitTypeUnion(node: luau.AstTypeUnion, visitor: Visitor) +local function visitTypeUnion(node: types.AstTypeUnion, visitor: Visitor) if visitor.visitTypeUnion(node) then if node.leading then visitToken(node.leading, visitor) @@ -637,7 +637,7 @@ local function visitTypeUnion(node: luau.AstTypeUnion, visitor: Visitor) end end -local function visitTypeIntersection(node: luau.AstTypeIntersection, visitor: Visitor) +local function visitTypeIntersection(node: types.AstTypeIntersection, visitor: Visitor) if visitor.visitTypeIntersection(node) then if node.leading then visitToken(node.leading, visitor) @@ -646,7 +646,7 @@ local function visitTypeIntersection(node: luau.AstTypeIntersection, visitor: Vi end end -local function visitTypeArray(node: luau.AstTypeArray, visitor: Visitor) +local function visitTypeArray(node: types.AstTypeArray, visitor: Visitor) if visitor.visitTypeArray(node) then visitToken(node.openbrace, visitor) if node.access then @@ -657,7 +657,7 @@ local function visitTypeArray(node: luau.AstTypeArray, visitor: Visitor) end end -local function visitTypeTable(node: luau.AstTypeTable, visitor: Visitor) +local function visitTypeTable(node: types.AstTypeTable, visitor: Visitor) if visitor.visitTypeTable(node) then visitToken(node.openbrace, visitor) for _, entry in node.entries do @@ -685,7 +685,7 @@ local function visitTypeTable(node: luau.AstTypeTable, visitor: Visitor) end end -local function visitTypeFunctionParameter(node: luau.AstTypeFunctionParameter, visitor) +local function visitTypeFunctionParameter(node: types.AstTypeFunctionParameter, visitor) if node.name then visitToken(node.name, visitor) end @@ -695,7 +695,7 @@ local function visitTypeFunctionParameter(node: luau.AstTypeFunctionParameter, v visitType(node.type, visitor) end -local function visitTypeFunction(node: luau.AstTypeFunction, visitor: Visitor) +local function visitTypeFunction(node: types.AstTypeFunction, visitor: Visitor) if visitor.visitTypeFunction(node) then if node.opengenerics then visitToken(node.opengenerics, visitor) @@ -720,7 +720,7 @@ local function visitTypeFunction(node: luau.AstTypeFunction, visitor: Visitor) end end -local function visitTypePackExplicit(node: luau.AstTypePackExplicit, visitor: Visitor) +local function visitTypePackExplicit(node: types.AstTypePackExplicit, visitor: Visitor) if visitor.visitTypePackExplicit(node) then if node.openparens then visitToken(node.openparens, visitor) @@ -735,14 +735,14 @@ local function visitTypePackExplicit(node: luau.AstTypePackExplicit, visitor: Vi end end -local function visitTypePackGeneric(node: luau.AstTypePackGeneric, visitor: Visitor) +local function visitTypePackGeneric(node: types.AstTypePackGeneric, visitor: Visitor) if visitor.visitTypePackGeneric(node) then visitToken(node.name, visitor) visitToken(node.ellipsis, visitor) end end -local function visitTypePackVariadic(node: luau.AstTypePackVariadic, visitor: Visitor) +local function visitTypePackVariadic(node: types.AstTypePackVariadic, visitor: Visitor) if visitor.visitTypePackVariadic(node) then if node.ellipsis then visitToken(node.ellipsis, visitor) @@ -751,7 +751,7 @@ local function visitTypePackVariadic(node: luau.AstTypePackVariadic, visitor: Vi end end -function visitExpression(expression: luau.AstExpr, visitor: Visitor) +function visitExpression(expression: types.AstExpr, visitor: Visitor) if visitor.visitExpression(expression) then if expression.tag == "nil" then visitNil(expression, visitor) @@ -797,7 +797,7 @@ function visitExpression(expression: luau.AstExpr, visitor: Visitor) end end -function visitStatement(statement: luau.AstStat, visitor: Visitor) +function visitStatement(statement: types.AstStat, visitor: Visitor) if statement.tag == "block" then visitBlock(statement, visitor) elseif statement.tag == "conditional" then @@ -837,7 +837,7 @@ function visitStatement(statement: luau.AstStat, visitor: Visitor) end end -function visitType(type: luau.AstType, visitor: Visitor) +function visitType(type: types.AstType, visitor: Visitor) if type.tag == "reference" then visitTypeReference(type, visitor) elseif type.tag == "boolean" then @@ -865,7 +865,7 @@ function visitType(type: luau.AstType, visitor: Visitor) end end -function visitTypePack(type: luau.AstTypePack, visitor: Visitor) +function visitTypePack(type: types.AstTypePack, visitor: Visitor) if type.tag == "explicit" then visitTypePackExplicit(type, visitor) elseif type.tag == "generic" then diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 00694126b..11cd425ef 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -1,10 +1,10 @@ local fs = require("@std/fs") -local luau = require("@std/luau") local path = require("@std/path") local test = require("@std/test") local asserts = require("@std/test/assert") local parser = require("@std/syntax/parser") local printer = require("@std/syntax/printer") +local syntax = require("@std/syntax") local T = require("@std/luau") local function assertEqualsLocation( @@ -23,13 +23,13 @@ end test.suite("Parser", function(suite) suite:case("tokenContainsLeadingSpaces", function(assert) - local block = luau.parse(" local x = 1").root + local block = parser.parse(" local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: luau.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localkeyword assert.eq(#token.leadingtrivia, 1) assert.eq(token.leadingtrivia[1].tag, "whitespace") assert.eq(token.leadingtrivia[1].text, " ") @@ -37,13 +37,13 @@ test.suite("Parser", function(suite) end) suite:case("tokenContainsLeadingNewline", function(assert) - local block = luau.parse("\n" .. "local x = 1").root + local block = parser.parse("\n" .. "local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: luau.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localkeyword assert.eq(#token.leadingtrivia, 1) assert.eq(token.leadingtrivia[1].tag, "whitespace") assert.eq(token.leadingtrivia[1].text, "\n") @@ -51,13 +51,13 @@ test.suite("Parser", function(suite) end) suite:case("tokenContainsLeadingSingleLineComment", function(assert) - local block = luau.parse("-- comment\n" .. "local x = 1").root + local block = parser.parse("-- comment\n" .. "local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: luau.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localkeyword assert.eq(#token.leadingtrivia, 2) assert.eq(token.leadingtrivia[1].tag, "comment") assert.eq(token.leadingtrivia[1].text, "-- comment") @@ -68,13 +68,13 @@ test.suite("Parser", function(suite) end) suite:case("tokenContainsLeadingBlockComment", function(assert) - local block = luau.parse("--[[ comment ]] local x = 1").root + local block = parser.parse("--[[ comment ]] local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: luau.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localkeyword assert.eq(#token.leadingtrivia, 2) assert.eq(token.leadingtrivia[1].tag, "blockcomment") assert.eq(token.leadingtrivia[1].text, "--[[ comment ]]") @@ -85,13 +85,13 @@ test.suite("Parser", function(suite) end) suite:case("tokenizeWhitespace", function(assert) - local block = luau.parse(" \n\t\t\n\n" .. "local x = 1").root + local block = parser.parse(" \n\t\t\n\n" .. "local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: luau.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localkeyword assert.eq(#token.leadingtrivia, 3) assert.eq(token.leadingtrivia[1].text, " \n") assert.eq(token.leadingtrivia[2].text, "\t\t\n") @@ -99,15 +99,15 @@ test.suite("Parser", function(suite) end) suite:case("triviaSplitBetweenLeadingAndTrailing", function(assert) - local block = luau.parse("local x = 'test' -- comment\n" .. "-- comment 2\nlocal y = 'value'").root + local block = parser.parse("local x = 'test' -- comment\n" .. "-- comment 2\nlocal y = 'value'") assert.eq(#block.statements, 2) local firstStmt = block.statements[1] assert.eq(firstStmt.tag, "local") - local trailingToken: luau.AstExpr = (firstStmt :: luau.AstStatLocal).values[1].node + local trailingToken: syntax.AstExpr = (firstStmt :: syntax.AstStatLocal).values[1].node assert.eq(trailingToken.tag, "string") - local expr = trailingToken :: luau.AstExprConstantString + local expr = trailingToken :: syntax.AstExprConstantString assert.eq(#expr.trailingtrivia, 3) assert.eq(expr.trailingtrivia[1].text, " ") assert.eq(expr.trailingtrivia[2].text, "-- comment") @@ -116,7 +116,7 @@ test.suite("Parser", function(suite) local secondStmt = block.statements[2] assert.eq(secondStmt.tag, "local") - local leadingToken = (secondStmt :: luau.AstStatLocal).localkeyword + local leadingToken = (secondStmt :: syntax.AstStatLocal).localkeyword assert.eq(#leadingToken.leadingtrivia, 2) assert.eq(leadingToken.leadingtrivia[1].text, "-- comment 2") assert.eq(leadingToken.leadingtrivia[2].text, "\n") @@ -142,14 +142,14 @@ test.suite("Parser", function(suite) suite:case("lineOffsetsField", function(assert) local singleLineCode = "local x = 1" - local result = luau.parse(singleLineCode) + local result = parser.parsefile(singleLineCode) assert.eq(type(result.lineoffsets), "table") assert.eq(#result.lineoffsets, 1) assert.eq(result.lineoffsets[1], 0) local multiLineCode = "local x = 1\nlocal y = 2\nlocal z = 3" - local multiResult = luau.parse(multiLineCode) + local multiResult = parser.parsefile(multiLineCode) assert.eq(type(multiResult.lineoffsets), "table") assert.eq(#multiResult.lineoffsets, 3) @@ -158,7 +158,7 @@ test.suite("Parser", function(suite) assert.eq(multiResult.lineoffsets[3], 24) local emptyLineCode = "local x = 1\n\nlocal y = 2" - local emptyResult = luau.parse(emptyLineCode) + local emptyResult = parser.parsefile(emptyLineCode) assert.eq(type(emptyResult.lineoffsets), "table") assert.eq(#emptyResult.lineoffsets, 3) @@ -180,13 +180,13 @@ test.suite("Parser", function(suite) } for _, subcase in ipairs(subcases) do - local block = luau.parse(subcase.code).root + local block = parser.parse(subcase.code) assert.eq(#block.statements, 1) local l = block.statements[1] assert.eq(l.tag, "localfunction") - local lf = l :: luau.AstStatLocalFunction + local lf = l :: syntax.AstStatLocalFunction assert.eq(lf.attributes[1].text, subcase.attr) end end) @@ -197,7 +197,7 @@ test.suite("parseExpr", function(suite) local groupExpr = parser.parseexpr("(x)") assert.eq(groupExpr.tag, "group") assert.eq(groupExpr.kind, "expr") - local expr = groupExpr :: luau.AstExprGroup + local expr = groupExpr :: syntax.AstExprGroup assert.eq(expr.openparens.text, "(") assert.eq(expr.closeparens.text, ")") assert.eq(expr.expression.tag, "global") @@ -207,118 +207,118 @@ test.suite("parseExpr", function(suite) local expr = parser.parseexpr("nil") assert.eq(expr.tag, "nil") assert.eq(expr.kind, "expr") - assert.eq((expr :: luau.AstExprConstantNil).text, "nil") + assert.eq((expr :: syntax.AstExprConstantNil).text, "nil") end) suite:case("parseExprBoolean", function(assert) local trueExpr = parser.parseexpr("true") assert.eq(trueExpr.tag, "boolean") assert.eq(trueExpr.kind, "expr") - assert.eq((trueExpr :: luau.AstExprConstantBool).text, "true") - assert.eq((trueExpr :: luau.AstExprConstantBool).value, true) + assert.eq((trueExpr :: syntax.AstExprConstantBool).text, "true") + assert.eq((trueExpr :: syntax.AstExprConstantBool).value, true) local falseExpr = parser.parseexpr("false") assert.eq(falseExpr.tag, "boolean") assert.eq(falseExpr.kind, "expr") - assert.eq((falseExpr :: luau.AstExprConstantBool).text, "false") - assert.eq((falseExpr :: luau.AstExprConstantBool).value, false) + assert.eq((falseExpr :: syntax.AstExprConstantBool).text, "false") + assert.eq((falseExpr :: syntax.AstExprConstantBool).value, false) end) suite:case("parseExprNumber", function(assert) local numberExpr = parser.parseexpr("42") assert.eq(numberExpr.tag, "number") assert.eq(numberExpr.kind, "expr") - assert.eq((numberExpr :: luau.AstExprConstantNumber).text, "42") - assert.eq((numberExpr :: luau.AstExprConstantNumber).value, 42) + assert.eq((numberExpr :: syntax.AstExprConstantNumber).text, "42") + assert.eq((numberExpr :: syntax.AstExprConstantNumber).value, 42) local floatExpr = parser.parseexpr("3.14") assert.eq(floatExpr.tag, "number") assert.eq(floatExpr.kind, "expr") - assert.eq((floatExpr :: luau.AstExprConstantNumber).text, "3.14") - assert.eq((floatExpr :: luau.AstExprConstantNumber).value, 3.14) + assert.eq((floatExpr :: syntax.AstExprConstantNumber).text, "3.14") + assert.eq((floatExpr :: syntax.AstExprConstantNumber).value, 3.14) end) suite:case("parseExprString", function(assert) local singleQuoteExpr = parser.parseexpr("'hello'") assert.eq(singleQuoteExpr.tag, "string") assert.eq(singleQuoteExpr.kind, "expr") - assert.eq((singleQuoteExpr :: luau.AstExprConstantString).text, "hello") - assert.eq((singleQuoteExpr :: luau.AstExprConstantString).quotestyle, "single") + assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).text, "hello") + assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).quotestyle, "single") local doubleQuoteExpr = parser.parseexpr('"world"') assert.eq(doubleQuoteExpr.tag, "string") assert.eq(doubleQuoteExpr.kind, "expr") - assert.eq((doubleQuoteExpr :: luau.AstExprConstantString).text, "world") - assert.eq((doubleQuoteExpr :: luau.AstExprConstantString).quotestyle, "double") + assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).text, "world") + assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).quotestyle, "double") local blockQuoteExpr = parser.parseexpr("[[world]]") assert.eq(blockQuoteExpr.tag, "string") assert.eq(blockQuoteExpr.kind, "expr") - assert.eq((blockQuoteExpr :: luau.AstExprConstantString).text, "world") - assert.eq((blockQuoteExpr :: luau.AstExprConstantString).quotestyle, "block") + assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).text, "world") + assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).quotestyle, "block") local interpExpr = parser.parseexpr("`foobar`") assert.eq(interpExpr.tag, "string") assert.eq(interpExpr.kind, "expr") - assert.eq((interpExpr :: luau.AstExprConstantString).text, "foobar") - assert.eq((interpExpr :: luau.AstExprConstantString).quotestyle, "interp") + assert.eq((interpExpr :: syntax.AstExprConstantString).text, "foobar") + assert.eq((interpExpr :: syntax.AstExprConstantString).quotestyle, "interp") end) suite:case("parseExprLocal", function(assert) local localExpr = parser.parseexpr("x") assert.eq(localExpr.tag, "global") assert.eq(localExpr.kind, "expr") - assert.eq((localExpr :: luau.AstExprGlobal).name.text, "x") + assert.eq((localExpr :: syntax.AstExprGlobal).name.text, "x") end) suite:case("parseExprGlobal", function(assert) local globalExpr = parser.parseexpr("_G") assert.eq(globalExpr.tag, "global") assert.eq(globalExpr.kind, "expr") - assert.eq((globalExpr :: luau.AstExprGlobal).name.text, "_G") + assert.eq((globalExpr :: syntax.AstExprGlobal).name.text, "_G") end) suite:case("parseExprVarargs", function(assert) local varargsExpr = parser.parseexpr("...") assert.eq(varargsExpr.tag, "vararg") assert.eq(varargsExpr.kind, "expr") - assert.eq((varargsExpr :: luau.AstExprVarargs).text, "...") + assert.eq((varargsExpr :: syntax.AstExprVarargs).text, "...") end) suite:case("parseExprCall", function(assert) local callExpr = parser.parseexpr("foo()") assert.eq(callExpr.tag, "call") assert.eq(callExpr.kind, "expr") - assert.eq((callExpr :: luau.AstExprCall).func.tag, "global") - assert.eq(((callExpr :: luau.AstExprCall).func :: luau.AstExprGlobal).name.text, "foo") - assert.eq((callExpr :: luau.AstExprCall).self, false) - assert.eq(#(callExpr :: luau.AstExprCall).arguments, 0) + assert.eq((callExpr :: syntax.AstExprCall).func.tag, "global") + assert.eq(((callExpr :: syntax.AstExprCall).func :: syntax.AstExprGlobal).name.text, "foo") + assert.eq((callExpr :: syntax.AstExprCall).self, false) + assert.eq(#(callExpr :: syntax.AstExprCall).arguments, 0) local callWithArgsExpr = parser.parseexpr("bar(1, 2)") assert.eq(callWithArgsExpr.tag, "call") assert.eq(callWithArgsExpr.kind, "expr") - assert.eq(#(callWithArgsExpr :: luau.AstExprCall).arguments, 2) - assert.eq((callWithArgsExpr :: luau.AstExprCall).arguments[1].node.tag, "number") - assert.eq((callWithArgsExpr :: luau.AstExprCall).arguments[2].node.tag, "number") + assert.eq(#(callWithArgsExpr :: syntax.AstExprCall).arguments, 2) + assert.eq((callWithArgsExpr :: syntax.AstExprCall).arguments[1].node.tag, "number") + assert.eq((callWithArgsExpr :: syntax.AstExprCall).arguments[2].node.tag, "number") end) suite:case("parseExprIndexName", function(assert) local indexExpr = parser.parseexpr("obj.field") assert.eq(indexExpr.tag, "indexname") assert.eq(indexExpr.kind, "expr") - assert.eq((indexExpr :: luau.AstExprIndexName).expression.tag, "global") - assert.eq((indexExpr :: luau.AstExprIndexName).accessor.text, ".") - assert.eq((indexExpr :: luau.AstExprIndexName).index.text, "field") + assert.eq((indexExpr :: syntax.AstExprIndexName).expression.tag, "global") + assert.eq((indexExpr :: syntax.AstExprIndexName).accessor.text, ".") + assert.eq((indexExpr :: syntax.AstExprIndexName).index.text, "field") end) suite:case("parseExprIndexExpr", function(assert) local indexExpr = parser.parseexpr("arr[1]") assert.eq(indexExpr.tag, "index") assert.eq(indexExpr.kind, "expr") - assert.eq((indexExpr :: luau.AstExprIndexExpr).expression.tag, "global") - assert.eq((indexExpr :: luau.AstExprIndexExpr).index.tag, "number") - assert.eq((indexExpr :: luau.AstExprIndexExpr).openbrackets.text, "[") - assert.eq((indexExpr :: luau.AstExprIndexExpr).closebrackets.text, "]") + assert.eq((indexExpr :: syntax.AstExprIndexExpr).expression.tag, "global") + assert.eq((indexExpr :: syntax.AstExprIndexExpr).index.tag, "number") + assert.eq((indexExpr :: syntax.AstExprIndexExpr).openbrackets.text, "[") + assert.eq((indexExpr :: syntax.AstExprIndexExpr).closebrackets.text, "]") end) suite:case("parseExprAnonymousFunction", function(assert) @@ -326,7 +326,7 @@ test.suite("parseExpr", function(suite) assert.eq(anonFuncExpr.tag, "function") assert.eq(anonFuncExpr.kind, "expr") - local funcExpr = anonFuncExpr :: luau.AstExprAnonymousFunction + local funcExpr = anonFuncExpr :: syntax.AstExprAnonymousFunction assert.eq(#funcExpr.attributes, 0) assert.eq(funcExpr.functionkeyword.text, "function") @@ -352,7 +352,7 @@ test.suite("parseExpr", function(suite) assert.eq(anonFuncExpr.tag, "function") assert.eq(anonFuncExpr.kind, "expr") - funcExpr = anonFuncExpr :: luau.AstExprAnonymousFunction + funcExpr = anonFuncExpr :: syntax.AstExprAnonymousFunction assert.eq(#funcExpr.attributes, 0) assert.eq(funcExpr.functionkeyword.text, "function") @@ -360,22 +360,22 @@ test.suite("parseExpr", function(suite) assert.neq(funcBody.opengenerics, nil) assert.neq(funcBody.generics, nil) - assert.eq(#funcBody.generics :: luau.Punctuated, 1) - local gen = (funcBody.generics :: luau.Punctuated)[1].node + assert.eq(#funcBody.generics :: syntax.Punctuated, 1) + local gen = (funcBody.generics :: syntax.Punctuated)[1].node assert.eq(gen.name.text, "A") assert.eq(gen.equals, nil) assert.eq(gen.default, nil) assert.neq(funcBody.genericpacks, nil) - assert.eq(#funcBody.genericpacks :: luau.Punctuated, 1) - local genPack = (funcBody.genericpacks :: luau.Punctuated)[1].node + assert.eq(#funcBody.genericpacks :: syntax.Punctuated, 1) + local genPack = (funcBody.genericpacks :: syntax.Punctuated)[1].node assert.eq(genPack.name.text, "B") assert.eq(genPack.ellipsis.text, "...") assert.eq(genPack.equals, nil) assert.eq(genPack.default, nil) assert.neq(funcBody.closegenerics, nil) - assert.eq((funcBody.closegenerics :: luau.Token<">">).text, ">") + assert.eq((funcBody.closegenerics :: syntax.Token<">">).text, ">") assert.eq(funcBody.openparens.text, "(") assert.eq(#funcBody.parameters, 1) @@ -383,29 +383,29 @@ test.suite("parseExpr", function(suite) local param = funcBody.parameters[1].node assert.eq(param.name.text, "a") assert.neq(param.colon, nil) - assert.eq((param.colon :: luau.Token<":">).text, ":") + assert.eq((param.colon :: syntax.Token<":">).text, ":") assert.neq(param.annotation, nil) - assert.eq((param.annotation :: luau.AstType).tag, "reference") + assert.eq((param.annotation :: syntax.AstType).tag, "reference") assert.neq(funcBody.vararg, nil) - assert.eq((funcBody.vararg :: luau.Token<"...">).text, "...") + assert.eq((funcBody.vararg :: syntax.Token<"...">).text, "...") assert.neq(funcBody.varargcolon, nil) - assert.eq((funcBody.varargcolon :: luau.Token<":">).text, ":") + assert.eq((funcBody.varargcolon :: syntax.Token<":">).text, ":") assert.neq(funcBody.varargannotation, nil) - assert.eq((funcBody.varargannotation :: luau.AstTypePack).tag, "generic") + assert.eq((funcBody.varargannotation :: syntax.AstTypePack).tag, "generic") assert.eq(funcBody.closeparens.text, ")") assert.neq(funcBody.returnspecifier, nil) - assert.eq((funcBody.returnspecifier :: luau.Token<":">).text, ":") + assert.eq((funcBody.returnspecifier :: syntax.Token<":">).text, ":") assert.neq(funcBody.returnannotation, nil) - assert.eq((funcBody.returnannotation :: luau.AstTypePack).tag, "explicit") + assert.eq((funcBody.returnannotation :: syntax.AstTypePack).tag, "explicit") end) suite:case("parseExprTable", function(assert) local emptyTableExpr = parser.parseexpr("{}") assert.eq(emptyTableExpr.tag, "table") assert.eq(emptyTableExpr.kind, "expr") - local tableExpr = emptyTableExpr :: luau.AstExprTable + local tableExpr = emptyTableExpr :: syntax.AstExprTable assert.eq(tableExpr.openbrace.text, "{") assert.eq(tableExpr.closebrace.text, "}") assert.eq(#tableExpr.entries, 0) @@ -413,14 +413,14 @@ test.suite("parseExpr", function(suite) local mixedTable = parser.parseexpr("{1, foo = bar; [2] = baz}") assert.eq(mixedTable.tag, "table") assert.eq(mixedTable.kind, "expr") - local mixedTableExpr = mixedTable :: luau.AstExprTable + local mixedTableExpr = mixedTable :: syntax.AstExprTable assert.eq(#mixedTableExpr.entries, 3) local entry1 = mixedTableExpr.entries[1] assert.eq(entry1.kind, "list") assert.eq(entry1.value.tag, "number") assert.neq(entry1.separator, nil) - assert.eq((entry1.separator :: luau.Token<"," | ";">).text, ",") + assert.eq((entry1.separator :: syntax.Token<"," | ";">).text, ",") local entry2: any = mixedTableExpr.entries[2] assert.eq(entry2.kind, "record") @@ -428,7 +428,7 @@ test.suite("parseExpr", function(suite) assert.eq(entry2.equals.text, "=") assert.eq(entry2.value.tag, "global") assert.neq(entry2.separator, nil) - assert.eq((entry2.separator :: luau.Token<"," | ";">).text, ";") + assert.eq((entry2.separator :: syntax.Token<"," | ";">).text, ";") local entry3: any = mixedTableExpr.entries[3] assert.eq(entry3.kind, "general") @@ -444,21 +444,21 @@ test.suite("parseExpr", function(suite) local notExpr = parser.parseexpr("not x") assert.eq(notExpr.tag, "unary") assert.eq(notExpr.kind, "expr") - local expr = notExpr :: luau.AstExprUnary + local expr = notExpr :: syntax.AstExprUnary assert.eq(expr.operator.text, "not") assert.eq(expr.operand.tag, "global") local negExpr = parser.parseexpr("-42") assert.eq(negExpr.tag, "unary") assert.eq(negExpr.kind, "expr") - expr = negExpr :: luau.AstExprUnary + expr = negExpr :: syntax.AstExprUnary assert.eq(expr.operator.text, "-") assert.eq(expr.operand.tag, "number") local lenExpr = parser.parseexpr("#str") assert.eq(lenExpr.tag, "unary") assert.eq(lenExpr.kind, "expr") - expr = lenExpr :: luau.AstExprUnary + expr = lenExpr :: syntax.AstExprUnary assert.eq(expr.operator.text, "#") assert.eq(expr.operand.tag, "global") end) @@ -467,7 +467,7 @@ test.suite("parseExpr", function(suite) local addExpr = parser.parseexpr("1 + 2") assert.eq(addExpr.tag, "binary") assert.eq(addExpr.kind, "expr") - local expr = addExpr :: luau.AstExprBinary + local expr = addExpr :: syntax.AstExprBinary assert.eq(expr.lhsoperand.tag, "number") assert.eq(expr.operator.text, "+") assert.eq(expr.rhsoperand.tag, "number") @@ -477,7 +477,7 @@ test.suite("parseExpr", function(suite) local interpExpr = parser.parseexpr("`Hello, {name}! Today is {dayOfWeek}.`") assert.eq(interpExpr.tag, "interpolatedstring") assert.eq(interpExpr.kind, "expr") - local expr = interpExpr :: luau.AstExprInterpString + local expr = interpExpr :: syntax.AstExprInterpString assert.eq(#expr.strings, 3) assert.eq(expr.strings[1].text, "Hello, ") @@ -493,7 +493,7 @@ test.suite("parseExpr", function(suite) local castExpr = parser.parseexpr("x :: string") assert.eq(castExpr.tag, "cast") assert.eq(castExpr.kind, "expr") - local expr = castExpr :: luau.AstExprTypeAssertion + local expr = castExpr :: syntax.AstExprTypeAssertion assert.eq(expr.operand.tag, "global") assert.eq(expr.operator.text, "::") assert.eq(expr.annotation.tag, "reference") @@ -517,7 +517,7 @@ test.suite("parse", function(suite) parser.parse("if x > 0 then print('positive') elseif x < 0 then print('negative') else print('zero') end") assert.eq(#block.statements, 1) - local ifStat = block.statements[1] :: luau.AstStatIf + local ifStat = block.statements[1] :: syntax.AstStatIf assert.eq(ifStat.tag, "conditional") assert.eq(ifStat.ifkeyword.text, "if") assert.eq(ifStat.condition.tag, "binary") @@ -535,14 +535,14 @@ test.suite("parse", function(suite) checkIsBlock(elseIf.thenblock, assert) assert.neq(ifStat.elsekeyword, nil) - assert.eq((ifStat.elsekeyword :: luau.Token<"else">).text, "else") + assert.eq((ifStat.elsekeyword :: syntax.Token<"else">).text, "else") assert.neq(ifStat.elseblock, nil) checkIsBlock(ifStat.elseblock, assert) assert.eq(ifStat.endkeyword.text, "end") block = parser.parse("if condition then doSomething() end") assert.eq(#block.statements, 1) - local simpleIfStat = block.statements[1] :: luau.AstStatIf + local simpleIfStat = block.statements[1] :: syntax.AstStatIf assert.eq(simpleIfStat.tag, "conditional") assert.eq(simpleIfStat.ifkeyword.text, "if") assert.eq(simpleIfStat.condition.tag, "global") @@ -560,7 +560,7 @@ test.suite("parse", function(suite) checkIsBlock(block, assert) assert.eq(#block.statements, 1) - local whileStat = block.statements[1] :: luau.AstStatWhile + local whileStat = block.statements[1] :: syntax.AstStatWhile assert.eq(whileStat.tag, "while") assert.eq(whileStat.whilekeyword.text, "while") assert.eq(whileStat.condition.tag, "binary") @@ -573,7 +573,7 @@ test.suite("parse", function(suite) checkIsBlock(block, assert) assert.eq(#block.statements, 1) - whileStat = block.statements[1] :: luau.AstStatWhile + whileStat = block.statements[1] :: syntax.AstStatWhile assert.eq(whileStat.tag, "while") assert.eq(whileStat.whilekeyword.text, "while") assert.eq(whileStat.condition.tag, "boolean") @@ -581,9 +581,9 @@ test.suite("parse", function(suite) checkIsBlock(whileStat.body, assert) assert.eq(#whileStat.body.statements, 1) - local breakStat = whileStat.body.statements[1] :: luau.AstStat + local breakStat = whileStat.body.statements[1] :: syntax.AstStat assert.eq(breakStat.kind, "stat") - breakStat = breakStat :: luau.AstStatBreak + breakStat = breakStat :: syntax.AstStatBreak assert.eq(breakStat.tag, "break") assert.eq(breakStat.text, "break") @@ -593,7 +593,7 @@ test.suite("parse", function(suite) checkIsBlock(block, assert) assert.eq(#block.statements, 1) - whileStat = block.statements[1] :: luau.AstStatWhile + whileStat = block.statements[1] :: syntax.AstStatWhile assert.eq(whileStat.tag, "while") assert.eq(whileStat.whilekeyword.text, "while") assert.eq(whileStat.condition.tag, "boolean") @@ -601,9 +601,9 @@ test.suite("parse", function(suite) checkIsBlock(whileStat.body, assert) assert.eq(#whileStat.body.statements, 1) - local continueStat = whileStat.body.statements[1] :: luau.AstStat + local continueStat = whileStat.body.statements[1] :: syntax.AstStat assert.eq(continueStat.kind, "stat") - continueStat = continueStat :: luau.AstStatContinue + continueStat = continueStat :: syntax.AstStatContinue assert.eq(continueStat.tag, "continue") assert.eq(continueStat.text, "continue") @@ -615,7 +615,7 @@ test.suite("parse", function(suite) checkIsBlock(block, assert) assert.eq(#block.statements, 1) - local repeatStat = block.statements[1] :: luau.AstStatRepeat + local repeatStat = block.statements[1] :: syntax.AstStatRepeat assert.eq(repeatStat.tag, "repeat") assert.eq(repeatStat.repeatKeyword.text, "repeat") checkIsBlock(repeatStat.body, assert) @@ -630,21 +630,21 @@ test.suite("parse", function(suite) assert.eq(#block.statements, 1) assert.eq(block.statements[1].kind, "stat") - local l = block.statements[1] :: luau.AstStatLocal + local l = block.statements[1] :: syntax.AstStatLocal assert.eq(l.tag, "local") assert.eq(l.localkeyword.text, "local") assert.eq(#l.variables, 2) assert.eq(l.variables[1].node.name.text, "b") assert.neq(l.variables[1].separator, nil) - assert.eq((l.variables[1].separator :: luau.Token<",">).text, ",") + assert.eq((l.variables[1].separator :: syntax.Token<",">).text, ",") assert.eq(l.variables[2].node.name.text, "c") assert.eq(l.variables[2].separator, nil) assert.neq(l.equals, nil) - assert.eq((l.equals :: luau.Token<"=">).text, "=") + assert.eq((l.equals :: syntax.Token<"=">).text, "=") assert.eq(#l.values, 2) assert.eq(l.values[1].node.tag, "number") assert.neq(l.values[1].separator, nil) - assert.eq((l.values[1].separator :: luau.Token<",">).text, ",") + assert.eq((l.values[1].separator :: syntax.Token<",">).text, ",") assert.eq(l.values[2].node.tag, "number") assert.eq(l.values[2].separator, nil) @@ -653,7 +653,7 @@ test.suite("parse", function(suite) assert.eq(#block.statements, 1) assert.eq(block.statements[1].kind, "stat") - l = block.statements[1] :: luau.AstStatLocal + l = block.statements[1] :: syntax.AstStatLocal assert.eq(l.tag, "local") assert.eq(l.localkeyword.text, "local") assert.eq(#l.variables, 1) @@ -668,7 +668,7 @@ test.suite("parse", function(suite) assert.eq(block.tag, "block") assert.eq(#block.statements, 1) - local forStat = block.statements[1] :: luau.AstStatFor + local forStat = block.statements[1] :: syntax.AstStatFor assert.eq(forStat.tag, "for") assert.eq(forStat.forkeyword.text, "for") assert.eq(forStat.variable.name.text, "i") @@ -677,9 +677,9 @@ test.suite("parse", function(suite) assert.eq(forStat.tocomma.text, ",") assert.eq(forStat.to.tag, "number") assert.neq(forStat.stepcomma, nil) - assert.eq((forStat.stepcomma :: luau.Token<",">).text, ",") + assert.eq((forStat.stepcomma :: syntax.Token<",">).text, ",") assert.neq(forStat.step, nil) - assert.eq(((forStat.step :: any) :: luau.AstExpr).tag, "number") + assert.eq(((forStat.step :: any) :: syntax.AstExpr).tag, "number") assert.eq(forStat.dokeyword.text, "do") checkIsBlock(forStat.body, assert) assert.eq(#forStat.body.statements, 1) @@ -689,7 +689,7 @@ test.suite("parse", function(suite) assert.eq(block.tag, "block") assert.eq(#block.statements, 1) - forStat = block.statements[1] :: luau.AstStatFor + forStat = block.statements[1] :: syntax.AstStatFor assert.eq(forStat.tag, "for") assert.eq(forStat.forkeyword.text, "for") assert.eq(forStat.variable.name.text, "j") @@ -710,7 +710,7 @@ test.suite("parse", function(suite) assert.eq(block.tag, "block") assert.eq(#block.statements, 1) - local forInStat = block.statements[1] :: luau.AstStatForIn + local forInStat = block.statements[1] :: syntax.AstStatForIn assert.eq(forInStat.tag, "forin") assert.eq(forInStat.forkeyword.text, "for") assert.eq(#forInStat.variables, 2) @@ -730,7 +730,7 @@ test.suite("parse", function(suite) assert.eq(block.tag, "block") assert.eq(#block.statements, 1) - local assign = block.statements[1] :: luau.AstStatAssign + local assign = block.statements[1] :: syntax.AstStatAssign assert.eq(assign.tag, "assign") assert.eq(#assign.variables, 1) assert.eq(assign.variables[1].node.tag, "global") @@ -744,7 +744,7 @@ test.suite("parse", function(suite) assert.eq(block.tag, "block") assert.eq(#block.statements, 1) - local compound = block.statements[1] :: luau.AstStatCompoundAssign + local compound = block.statements[1] :: syntax.AstStatCompoundAssign assert.eq(compound.tag, "compoundassign") assert.eq(compound.variable.tag, "global") assert.eq(compound.operand.text, "+=") @@ -756,15 +756,15 @@ test.suite("parse", function(suite) checkIsBlock(block, assert) assert.eq(#block.statements, 1) - local funcStat = block.statements[1] :: luau.AstStatFunction + local funcStat = block.statements[1] :: syntax.AstStatFunction assert.eq(funcStat.tag, "function") assert.eq(#funcStat.attributes, 3) - assert.eq((funcStat.attributes[1] :: luau.AstAttribute).text, "@checked") - assert.eq((funcStat.attributes[2] :: luau.AstAttribute).text, "@native") - assert.eq((funcStat.attributes[3] :: luau.AstAttribute).text, "@deprecated") + assert.eq((funcStat.attributes[1] :: syntax.AstAttribute).text, "@checked") + assert.eq((funcStat.attributes[2] :: syntax.AstAttribute).text, "@native") + assert.eq((funcStat.attributes[3] :: syntax.AstAttribute).text, "@deprecated") assert.eq(funcStat.functionkeyword.text, "function") assert.eq(funcStat.name.tag, "global") - assert.eq((funcStat.name :: luau.AstExprGlobal).name.text, "add") + assert.eq((funcStat.name :: syntax.AstExprGlobal).name.text, "add") -- body parsing tested in parseExprAnonymousFunction end) @@ -773,7 +773,7 @@ test.suite("parse", function(suite) checkIsBlock(block, assert) assert.eq(#block.statements, 1) - local localFuncStat = block.statements[1] :: luau.AstStatLocalFunction + local localFuncStat = block.statements[1] :: syntax.AstStatLocalFunction assert.eq(localFuncStat.tag, "localfunction") assert.eq(#localFuncStat.attributes, 0) assert.eq(localFuncStat.localkeyword.text, "local") @@ -786,7 +786,7 @@ test.suite("parse", function(suite) assert.eq(block.tag, "block") assert.eq(#block.statements, 1) - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.tag, "typealias") assert.eq(typeAlias.export, nil) assert.eq(typeAlias.typeToken.text, "type") @@ -802,17 +802,17 @@ test.suite("parse", function(suite) checkIsBlock(block, assert) assert.eq(#block.statements, 1) - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.neq(typeAlias.export, nil) - assert.eq((typeAlias.export :: luau.Token<"export">).text, "export") + assert.eq((typeAlias.export :: syntax.Token<"export">).text, "export") assert.neq(typeAlias.opengenerics, nil) - assert.eq((typeAlias.opengenerics :: luau.Token<"<">).text, "<") + assert.eq((typeAlias.opengenerics :: syntax.Token<"<">).text, "<") assert.neq(typeAlias.generics, nil) - assert.eq(#typeAlias.generics :: luau.Punctuated, 1) + assert.eq(#typeAlias.generics :: syntax.Punctuated, 1) assert.neq(typeAlias.genericpacks, nil) - assert.eq(#typeAlias.genericpacks :: luau.Punctuated, 1) + assert.eq(#typeAlias.genericpacks :: syntax.Punctuated, 1) assert.neq(typeAlias.closegenerics, nil) - assert.eq((typeAlias.closegenerics :: luau.Token<">">).text, ">") + assert.eq((typeAlias.closegenerics :: syntax.Token<">">).text, ">") end) suite:case("parseStatTypeFunction", function(assert) @@ -820,7 +820,7 @@ test.suite("parse", function(suite) assert.eq(block.tag, "block") assert.eq(#block.statements, 1) - local typeFunc = block.statements[1] :: luau.AstStatTypeFunction + local typeFunc = block.statements[1] :: syntax.AstStatTypeFunction assert.eq(typeFunc.tag, "typefunction") assert.eq(typeFunc.export, nil) assert.eq(typeFunc.type.text, "type") @@ -831,9 +831,9 @@ test.suite("parse", function(suite) assert.eq(block.tag, "block") assert.eq(#block.statements, 1) - typeFunc = block.statements[1] :: luau.AstStatTypeFunction + typeFunc = block.statements[1] :: syntax.AstStatTypeFunction assert.neq(typeFunc.export, nil) - assert.eq((typeFunc.export :: luau.Token<"export">).text, "export") + assert.eq((typeFunc.export :: syntax.Token<"export">).text, "export") end) end) @@ -841,10 +841,10 @@ test.suite("parse types", function(suite) -- AstGenericType and AstGenericTypePack are tested in parseExprAnonymousFunction suite:case("testTypeReference", function(assert) local block = parser.parse("type MyString = string") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "reference") - local typeRef = typeAlias.type :: luau.AstTypeReference + local typeRef = typeAlias.type :: syntax.AstTypeReference assert.eq(typeRef.prefix, nil) assert.eq(typeRef.prefixpoint, nil) assert.eq(typeRef.name.text, "string") @@ -853,67 +853,67 @@ test.suite("parse types", function(suite) assert.eq(typeRef.closeparameters, nil) block = parser.parse("type MyTable = MyModule.Table") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "reference") - typeRef = typeAlias.type :: luau.AstTypeReference + typeRef = typeAlias.type :: syntax.AstTypeReference assert.neq(typeRef.prefix, nil) - assert.eq((typeRef.prefix :: luau.Token).text, "MyModule") + assert.eq((typeRef.prefix :: syntax.Token).text, "MyModule") assert.neq(typeRef.prefixpoint, nil) - assert.eq((typeRef.prefixpoint :: luau.Token<".">).text, ".") + assert.eq((typeRef.prefixpoint :: syntax.Token<".">).text, ".") assert.eq(typeRef.name.text, "Table") assert.neq(typeRef.openparameters, nil) - assert.eq((typeRef.openparameters :: luau.Token<"<">).text, "<") + assert.eq((typeRef.openparameters :: syntax.Token<"<">).text, "<") assert.neq(typeRef.parameters, nil) - assert.eq(#typeRef.parameters :: luau.Punctuated, 2) - assert.eq((typeRef.parameters :: luau.Punctuated)[1].node.tag, "reference") - assert.eq((typeRef.parameters :: luau.Punctuated)[2].node.tag, "reference") + assert.eq(#typeRef.parameters :: syntax.Punctuated, 2) + assert.eq((typeRef.parameters :: syntax.Punctuated)[1].node.tag, "reference") + assert.eq((typeRef.parameters :: syntax.Punctuated)[2].node.tag, "reference") assert.neq(typeRef.closeparameters, nil) - assert.eq((typeRef.closeparameters :: luau.Token<">">).text, ">") + assert.eq((typeRef.closeparameters :: syntax.Token<">">).text, ">") end) suite:case("testTypeSingletonBoolean", function(assert) local block = parser.parse("type TrueType = true") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "boolean") - local singletonBool = typeAlias.type :: luau.AstTypeSingletonBool + local singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool assert.eq(singletonBool.text, "true") assert.eq(singletonBool.value, true) block = parser.parse("type FalseType = false") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "boolean") - singletonBool = typeAlias.type :: luau.AstTypeSingletonBool + singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool assert.eq(singletonBool.text, "false") assert.eq(singletonBool.value, false) end) suite:case("testTypeSingletonString", function(assert) local block = parser.parse("type HelloType = 'hello'") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "string") - local singletonStr = typeAlias.type :: luau.AstTypeSingletonString + local singletonStr = typeAlias.type :: syntax.AstTypeSingletonString assert.eq(singletonStr.text, "hello") assert.eq(singletonStr.quotestyle, "single") block = parser.parse('type WorldType = "world"') - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "string") - singletonStr = typeAlias.type :: luau.AstTypeSingletonString + singletonStr = typeAlias.type :: syntax.AstTypeSingletonString assert.eq(singletonStr.text, "world") assert.eq(singletonStr.quotestyle, "double") end) suite:case("testTypeTypeof", function(assert) local block = parser.parse("type T = typeof(someVariable)") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "typeof") - local typeofType = typeAlias.type :: luau.AstTypeTypeof + local typeofType = typeAlias.type :: syntax.AstTypeTypeof assert.eq(typeofType.typeof.text, "typeof") assert.eq(typeofType.openparens.text, "(") assert.eq(typeofType.expression.tag, "global") @@ -922,10 +922,10 @@ test.suite("parse types", function(suite) suite:case("testTypeGroup", function(assert) local block = parser.parse("type T = (string)") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "group") - local groupType = typeAlias.type :: luau.AstTypeGroup + local groupType = typeAlias.type :: syntax.AstTypeGroup assert.eq(groupType.openparens.text, "(") assert.eq(groupType.type.tag, "reference") assert.eq(groupType.closeparens.text, ")") @@ -933,10 +933,10 @@ test.suite("parse types", function(suite) suite:case("testTypeUnion", function(assert) local block = parser.parse("type T = string | number | boolean") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "union") - local unionType = typeAlias.type :: luau.AstTypeUnion + local unionType = typeAlias.type :: syntax.AstTypeUnion assert.eq(unionType.leading, nil) assert.eq(#unionType.types, 3) assert.eq(unionType.types[1].node.tag, "reference") @@ -944,24 +944,24 @@ test.suite("parse types", function(suite) assert.eq(unionType.types[3].node.tag, "reference") block = parser.parse("type T = | number?") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "union") - unionType = typeAlias.type :: luau.AstTypeUnion + unionType = typeAlias.type :: syntax.AstTypeUnion assert.neq(unionType.leading, nil) - assert.eq((unionType.leading :: luau.Token<"|">).text, "|") + assert.eq((unionType.leading :: syntax.Token<"|">).text, "|") assert.eq(#unionType.types, 2) assert.eq(unionType.types[1].node.tag, "reference") assert.eq(unionType.types[2].node.tag, "optional") - assert.eq((unionType.types[2].node :: luau.AstTypeOptional).text, "?") + assert.eq((unionType.types[2].node :: syntax.AstTypeOptional).text, "?") end) suite:case("testTypeUnionWithOptionalPart", function(assert) local block = parser.parse("type T = number? | string") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "union") - local unionType = typeAlias.type :: luau.AstTypeUnion + local unionType = typeAlias.type :: syntax.AstTypeUnion assert.eq(unionType.leading, nil) assert.eq(#unionType.types, 3) @@ -972,7 +972,7 @@ test.suite("parse types", function(suite) local pair2 = unionType.types[2] assert.eq(pair2.node.tag, "optional") assert.neq(pair2.separator, nil) - assert.eq((pair2.separator :: luau.Token<"|">).text, "|") + assert.eq((pair2.separator :: syntax.Token<"|">).text, "|") local pair3 = unionType.types[3] assert.eq(pair3.node.tag, "reference") @@ -981,10 +981,10 @@ test.suite("parse types", function(suite) suite:case("testTypeIntersection", function(assert) local block = parser.parse("type T = A & B & C") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "intersection") - local intersectionType = typeAlias.type :: luau.AstTypeIntersection + local intersectionType = typeAlias.type :: syntax.AstTypeIntersection assert.eq(intersectionType.leading, nil) assert.eq(#intersectionType.types, 3) assert.eq(intersectionType.types[1].node.tag, "reference") @@ -992,59 +992,59 @@ test.suite("parse types", function(suite) assert.eq(intersectionType.types[3].node.tag, "reference") block = parser.parse("type T = & B") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "intersection") - intersectionType = typeAlias.type :: luau.AstTypeIntersection + intersectionType = typeAlias.type :: syntax.AstTypeIntersection assert.neq(intersectionType.leading, nil) - assert.eq((intersectionType.leading :: luau.Token<"&">).text, "&") + assert.eq((intersectionType.leading :: syntax.Token<"&">).text, "&") assert.eq(#intersectionType.types, 1) assert.eq(intersectionType.types[1].node.tag, "reference") end) suite:case("testTypeArray", function(assert) local block = parser.parse("type T = { number }") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "array") - local arrayType = typeAlias.type :: luau.AstTypeArray + local arrayType = typeAlias.type :: syntax.AstTypeArray assert.eq(arrayType.openbrace.text, "{") assert.eq(arrayType.access, nil) assert.eq(arrayType.type.tag, "reference") assert.eq(arrayType.closebrace.text, "}") block = parser.parse("type T = { read string }") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "array") - arrayType = typeAlias.type :: luau.AstTypeArray + arrayType = typeAlias.type :: syntax.AstTypeArray assert.neq(arrayType.access, nil) - assert.eq((arrayType.access :: luau.Token<"read" | "write">).text, "read") + assert.eq((arrayType.access :: syntax.Token<"read" | "write">).text, "read") block = parser.parse("type T = { write number }") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "array") - arrayType = typeAlias.type :: luau.AstTypeArray + arrayType = typeAlias.type :: syntax.AstTypeArray assert.neq(arrayType.access, nil) - assert.eq((arrayType.access :: luau.Token<"read" | "write">).text, "write") + assert.eq((arrayType.access :: syntax.Token<"read" | "write">).text, "write") end) suite:case("parseTypeTable", function(assert) local block = parser.parse("type T = {}") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") - local tableType = typeAlias.type :: luau.AstTypeTable + local tableType = typeAlias.type :: syntax.AstTypeTable assert.eq(tableType.openbrace.text, "{") assert.eq(#tableType.entries, 0) assert.eq(tableType.closebrace.text, "}") block = parser.parse("type T = {[number] : string}") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") - tableType = typeAlias.type :: luau.AstTypeTable + tableType = typeAlias.type :: syntax.AstTypeTable assert.eq(#tableType.entries, 1) local tableTypeItem: any = tableType.entries[1] assert.eq(tableTypeItem.kind, "indexer") @@ -1057,69 +1057,69 @@ test.suite("parse types", function(suite) assert.eq(tableTypeItem.separator, nil) block = parser.parse("type T = {['hello'] : 'world', read ['foo'] : number; write ['bar'] : boolean}") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") - tableType = typeAlias.type :: luau.AstTypeTable + tableType = typeAlias.type :: syntax.AstTypeTable assert.eq(#tableType.entries, 3) tableTypeItem = tableType.entries[1] assert.eq(tableTypeItem.kind, "stringproperty") assert.eq(tableTypeItem.access, nil) assert.eq(tableTypeItem.indexeropen.text, "[") - assert.eq((tableTypeItem.key :: luau.AstTypeSingletonString).tag, "string") - assert.eq((tableTypeItem.key :: luau.AstTypeSingletonString).text, "hello") + assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).tag, "string") + assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).text, "hello") assert.eq(tableTypeItem.indexerclose.text, "]") assert.eq(tableTypeItem.colon.text, ":") assert.eq(tableTypeItem.value.tag, "string") assert.neq(tableTypeItem.separator, nil) - assert.eq((tableTypeItem.separator :: luau.Token<"," | ";">).text, ",") + assert.eq((tableTypeItem.separator :: syntax.Token<"," | ";">).text, ",") tableTypeItem = tableType.entries[2] assert.eq(tableTypeItem.kind, "stringproperty") assert.neq(tableTypeItem.access, nil) - assert.eq((tableTypeItem.access :: luau.Token<"read" | "write">).text, "read") + assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "read") assert.neq(tableTypeItem.separator, nil) - assert.eq((tableTypeItem.separator :: luau.Token<"," | ";">).text, ";") + assert.eq((tableTypeItem.separator :: syntax.Token<"," | ";">).text, ";") tableTypeItem = tableType.entries[3] assert.eq(tableTypeItem.kind, "stringproperty") assert.neq(tableTypeItem.access, nil) - assert.eq((tableTypeItem.access :: luau.Token<"read" | "write">).text, "write") + assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "write") assert.eq(tableTypeItem.separator, nil) block = parser.parse("type T = { hello: 'world'; read foo: number, write bar: boolean }") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") - tableType = typeAlias.type :: luau.AstTypeTable + tableType = typeAlias.type :: syntax.AstTypeTable assert.eq(#tableType.entries, 3) tableTypeItem = tableType.entries[1] assert.eq(tableTypeItem.kind, "property") assert.eq(tableTypeItem.access, nil) - assert.eq((tableTypeItem.key :: luau.Token).text, "hello") + assert.eq((tableTypeItem.key :: syntax.Token).text, "hello") assert.eq(tableTypeItem.colon.text, ":") assert.eq(tableTypeItem.value.tag, "string") assert.neq(tableTypeItem.separator, nil) - assert.eq((tableTypeItem.separator :: luau.Token<"," | ";">).text, ";") + assert.eq((tableTypeItem.separator :: syntax.Token<"," | ";">).text, ";") tableTypeItem = tableType.entries[2] assert.eq(tableTypeItem.kind, "property") assert.neq(tableTypeItem.access, nil) - assert.eq((tableTypeItem.access :: luau.Token<"read" | "write">).text, "read") + assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "read") assert.neq(tableTypeItem.separator, nil) - assert.eq((tableTypeItem.separator :: luau.Token<"," | ";">).text, ",") + assert.eq((tableTypeItem.separator :: syntax.Token<"," | ";">).text, ",") tableTypeItem = tableType.entries[3] assert.eq(tableTypeItem.kind, "property") assert.neq(tableTypeItem.access, nil) - assert.eq((tableTypeItem.access :: luau.Token<"read" | "write">).text, "write") + assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "write") assert.eq(tableTypeItem.separator, nil) end) suite:case("parseTypeFunction", function(assert) local block = parser.parse("type T = (n : number, string) -> boolean") - local typeAlias = block.statements[1] :: luau.AstStatTypeAlias + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") - local funcType = typeAlias.type :: luau.AstTypeFunction + local funcType = typeAlias.type :: syntax.AstTypeFunction assert.eq(funcType.opengenerics, nil) assert.eq(funcType.generics, nil) assert.eq(funcType.genericpacks, nil) @@ -1129,9 +1129,9 @@ test.suite("parse types", function(suite) local param = funcType.parameters[1].node assert.neq(param.name, nil) - assert.eq((param.name :: luau.Token).text, "n") + assert.eq((param.name :: syntax.Token).text, "n") assert.neq(param.colon, nil) - assert.eq((param.colon :: luau.Token<":">).text, ":") + assert.eq((param.colon :: syntax.Token<":">).text, ":") assert.eq(param.type.tag, "reference") param = funcType.parameters[2].node @@ -1145,37 +1145,37 @@ test.suite("parse types", function(suite) assert.eq(funcType.returntypes.kind, "typepack") block = parser.parse("type T = (item: T, ...U) -> ()") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") - funcType = typeAlias.type :: luau.AstTypeFunction + funcType = typeAlias.type :: syntax.AstTypeFunction assert.neq(funcType.opengenerics, nil) - assert.eq((funcType.opengenerics :: luau.Token<"<">).text, "<") + assert.eq((funcType.opengenerics :: syntax.Token<"<">).text, "<") assert.neq(funcType.generics, nil) - assert.eq(#funcType.generics :: luau.Punctuated, 1) + assert.eq(#funcType.generics :: syntax.Punctuated, 1) assert.neq(funcType.genericpacks, nil) - assert.eq(#funcType.genericpacks :: luau.Punctuated, 1) - assert.eq((funcType.genericpacks :: luau.Punctuated)[1].node.tag, "generic") + assert.eq(#funcType.genericpacks :: syntax.Punctuated, 1) + assert.eq((funcType.genericpacks :: syntax.Punctuated)[1].node.tag, "generic") - local genericTypePack = (funcType.genericpacks :: luau.Punctuated)[1].node + local genericTypePack = (funcType.genericpacks :: syntax.Punctuated)[1].node assert.eq(genericTypePack.name.text, "U") assert.eq(genericTypePack.ellipsis.text, "...") assert.neq(funcType.closegenerics, nil) - assert.eq((funcType.closegenerics :: luau.Token<">">).text, ">") + assert.eq((funcType.closegenerics :: syntax.Token<">">).text, ">") assert.neq(funcType.vararg, nil) - assert.eq((funcType.vararg :: luau.AstTypePack).kind, "typepack") + assert.eq((funcType.vararg :: syntax.AstTypePack).kind, "typepack") block = parser.parse("type T = () -> ...number") - typeAlias = block.statements[1] :: luau.AstStatTypeAlias + typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") - funcType = typeAlias.type :: luau.AstTypeFunction + funcType = typeAlias.type :: syntax.AstTypeFunction assert.eq(funcType.returntypes.kind, "typepack") assert.eq(funcType.returntypes.tag, "variadic") - local variadicType: luau.AstTypePackVariadic = funcType.returntypes :: any + local variadicType: syntax.AstTypePackVariadic = funcType.returntypes :: any assert.neq(variadicType.ellipsis, nil) - assert.eq((variadicType.ellipsis :: luau.Token<"...">).text, "...") + assert.eq((variadicType.ellipsis :: syntax.Token<"...">).text, "...") assert.eq(variadicType.type.tag, "reference") end) end) diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index 62e248ef7..54a18ecc0 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -1,12 +1,12 @@ local luau = require("@std/luau") -local parser = require("@std/syntax/parser") +local syntax = require("@std/syntax") local query = require("@std/syntax/query") local test = require("@std/test") local utils = require("@std/syntax/utils") test.suite("AST Query", function(suite) suite:case("filter", function(assert) - local testQuery = query.selectFromRoot(parser.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.selectFromRoot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local filteredQuery = testQuery:filter(function(n) return n.value > 2 @@ -19,7 +19,7 @@ test.suite("AST Query", function(suite) end) suite:case("replace", function(assert) - local testQuery = query.selectFromRoot(parser.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.selectFromRoot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local replacements = testQuery:replace(function(n): string? if n.value > 0 then @@ -46,9 +46,9 @@ test.suite("AST Query", function(suite) end) suite:case("map", function(assert) - local testQuery = query.selectFromRoot(parser.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.selectFromRoot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field - local mappedQuery = query.map(testQuery, function(n: luau.AstExprConstantNumber): number? + local mappedQuery = query.map(testQuery, function(n: syntax.AstExprConstantNumber): number? if n.value % 2 == 0 then return n.value * 10 end @@ -62,10 +62,10 @@ test.suite("AST Query", function(suite) end) suite:case("forEach", function(assert) - local testQuery = query.selectFromRoot(parser.parse("local _ = {0, 1, 2}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.selectFromRoot(syntax.parse("local _ = {0, 1, 2}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local sum = 0 - testQuery:forEach(function(n: luau.AstExprConstantNumber) + testQuery:forEach(function(n: syntax.AstExprConstantNumber) sum = sum + n.value end) @@ -73,10 +73,10 @@ test.suite("AST Query", function(suite) end) suite:case("select_nums", function(assert) - local ast = luau.parse("print(1 + 2)") - local queryResult: query.query = query.selectFromRoot( - ast.root, -- LUAUFIX: Complains that AstStatBlock doesn't have location? field - function(n: luau.AstNode): luau.AstExprConstantNumber? + local ast = syntax.parse("print(1 + 2)") + local queryResult: query.query = query.selectFromRoot( + ast, -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + function(n: syntax.AstNode): syntax.AstExprConstantNumber? if n.kind == "expr" and n.tag == "number" then return n end @@ -85,30 +85,30 @@ test.suite("AST Query", function(suite) ) assert.eq(#queryResult.nodes, 2) - local num: luau.AstExpr = queryResult.nodes[1] + local num: syntax.AstExpr = queryResult.nodes[1] assert.eq(num.kind, "expr") assert.eq(num.tag, "number") - assert.eq((num :: luau.AstExprConstantNumber).value, 1) + assert.eq((num :: syntax.AstExprConstantNumber).value, 1) num = queryResult.nodes[2] assert.eq(num.kind, "expr") assert.eq(num.tag, "number") - assert.eq((num :: luau.AstExprConstantNumber).value, 2) + assert.eq((num :: syntax.AstExprConstantNumber).value, 2) end) suite:case("select_utils", function(assert) - local ast = luau.parse("print(1 + 2)") - local queryResult: query.query = - query.selectFromRoot(ast.root :: luau.AstNode, utils.isExprConstantNumber) + local ast = syntax.parse("print(1 + 2)") + local queryResult: query.query = + query.selectFromRoot(ast, utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field assert.eq(#queryResult.nodes, 2) - local num: luau.AstExpr = queryResult.nodes[1] + local num: syntax.AstExpr = queryResult.nodes[1] assert.eq(num.kind, "expr") assert.eq(num.tag, "number") - assert.eq((num :: luau.AstExprConstantNumber).value, 1) + assert.eq((num :: syntax.AstExprConstantNumber).value, 1) num = queryResult.nodes[2] assert.eq(num.kind, "expr") assert.eq(num.tag, "number") - assert.eq((num :: luau.AstExprConstantNumber).value, 2) + assert.eq((num :: syntax.AstExprConstantNumber).value, 2) end) end) From d7ca1cce90312b61ac0cda51af21a5c458f93262 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 12 Nov 2025 10:17:28 -0800 Subject: [PATCH 154/642] bugfix: remaining failing windows tests (#560) This PR fixes the remaining windows tests that fail due to platform specific new line conventions. Inside of process.cpp, we now convert CRLF line endings to LF endings automatically. Finally, some of tasks display diverging behaviour on git bash vs command prompt. I've update the job to run tests on `cmd` in windows and `bash` on posix systems. --- .github/workflows/ci.yml | 11 +++++++++-- lute/process/src/process.cpp | 15 ++++++++++++++- tests/cli/test.test.luau | 4 ++-- tests/std/process.test.luau | 9 +++++++-- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3777c1a46..ddd0928f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,8 +42,15 @@ jobs: - name: Use CI .luaurc file run: rm .luaurc && mv .luaurc.ci .luaurc - - name: Run Luau Tests - run: find tests -name "*.test.luau" | xargs -I {} ${{ steps.build_lute.outputs.exe_path }} run {} + - name: Run Luau Tests (POSIX) + if: runner.os != 'Windows' + shell: bash + run: find tests -name "*.test.luau" | xargs -I {} "${{ steps.build_lute.outputs.exe_path }}" run {} + + - name: Run Luau Tests (Windows) + if: runner.os == 'Windows' + shell: cmd + run: for /r tests %%f in (*.test.luau) do "${{ steps.build_lute.outputs.exe_path }}" run "%%f" run-lute-cxx-unittests: runs-on: ${{ matrix.os }} diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index a539a1263..665bcef5d 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -26,6 +26,18 @@ namespace process { +void convertCRLFtoLF(std::string& str) +{ + size_t writePos = 0; + for (size_t readPos = 0; readPos < str.size(); ++readPos) + { + if (str[readPos] == '\r' && readPos + 1 < str.size() && str[readPos + 1] == '\n') + continue; // Skip the '\r' in CRLF + str[writePos++] = str[readPos]; + } + str.resize(writePos); +} + struct ProcessHandle { uv_process_t process; @@ -96,7 +108,8 @@ struct ProcessHandle std::string finalStdout = stdoutData; std::string finalStderr = stderrData; std::string finalSignalStr = finalTermSignal ? std::to_string(finalTermSignal) : ""; - + convertCRLFtoLF(finalStdout); + convertCRLFtoLF(finalStderr); resumeToken->complete( [=](lua_State* L) { diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 97706bf01..1c7128a93 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -75,7 +75,7 @@ test.run() result.stdout:find(`Failed with: Runtime error in nil_field_suite.access_field_of_nil in:`, 1, true), nil ) - assert.neq(result.stdout:find(`{tostring(testFilePath)}:6`, 1, true), nil) + assert.neq(result.stdout:find(`{path.format(testFilePath)}:6`, 1, true), nil) fs.remove(testFilePath) end) @@ -107,7 +107,7 @@ test.run() assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number assert.neq(result.stdout:find(`Failed with: Runtime error in access_field_of_nil in:`, 1, true), nil) - assert.neq(result.stdout:find(`{tostring(testFilePath)}:5`, 1, true), nil) + assert.neq(result.stdout:find(`{path.format(testFilePath)}:5`, 1, true), nil) fs.remove(testFilePath) end) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 4ff9d2a79..f197bba1d 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -1,7 +1,7 @@ local pathlib = require("@std/path") local process = require("@std/process") local test = require("@std/test") - +local system = require("@std/system") test.suite("ProcessSuite", function(suite) suite:case("homedir_and_cwd_and_execpath", function(assert) local hd = process.homedir() @@ -20,7 +20,12 @@ test.suite("ProcessSuite", function(suite) end) suite:case("run_shell_and_cwd", function(assert) - local r = process.run({ "pwd" }, { cwd = "/" }) + local rootdir: string = "/" + if system.win32 then + local root = pathlib.win32.drive(process.cwd()) + rootdir = pathlib.format(root) + end + local r = process.run({ "pwd" }, { cwd = rootdir }) assert.tableeq(r, { exitcode = 0, stdout = "/\n", From bcb476914816da1efc7fdc111d835c9264a0a3b5 Mon Sep 17 00:00:00 2001 From: ariel Date: Wed, 12 Nov 2025 10:36:06 -0800 Subject: [PATCH 155/642] crypto: implement a secretbox interface for authenticated encryption (#552) The last remaining bit of work to close out #161 was the desire to have some kind of API for authenticated encryption (AE) or authenticated encrypted with additional data (AEAD). I worked a bit on trying to provide the full AEAD interface from boringssl, but it felt clunky and bad to use in Luau. I don't want to commit to the runtime providing a standard interface that is literally reproducing openssl's headers, that's something that can be done later with FFI. Instead, I want the crypto library to feel more in the spirit of libsodium, providing opinionated default ways of solving common cryptography tasks. The secretbox interface is a well-understood metaphor for AE(AD), and one that works quite nicely. To mitigate the likelihood of nonce or key reuse, the interface provided right now seals a message providing the key and nonce as output. We can extend the API in the future with optional arguments for nonce and key as needed. --- definitions/crypto.luau | 37 ++++++--- examples/secretbox.luau | 20 +++++ lute/crypto/include/lute/crypto.h | 12 +++ lute/crypto/src/crypto.cpp | 102 +++++++++++++++++++++-- lute/runtime/include/lute/userdatas.h | 3 +- lute/std/libs/test/assert.luau | 24 ++++++ tests/runtime/crypto.test.luau | 113 ++++++++++++++++++++++++++ 7 files changed, 294 insertions(+), 17 deletions(-) create mode 100644 examples/secretbox.luau create mode 100644 tests/runtime/crypto.test.luau diff --git a/definitions/crypto.luau b/definitions/crypto.luau index 821e95548..37629f491 100644 --- a/definitions/crypto.luau +++ b/definitions/crypto.luau @@ -1,20 +1,37 @@ local crypto = {} -export type Hash = { __hash: T } - -crypto.hash = { - md5 = {} :: Hash<"md5">, - sha1 = {} :: Hash<"sha1">, - sha256 = {} :: Hash<"sha256">, - sha512 = {} :: Hash<"sha512">, - blake2b256 = {} :: Hash<"blake2b256">, +export type hash = { __hash: T } + +crypto.hash = table.freeze({ + md5 = {} :: hash<"md5">, + sha1 = {} :: hash<"sha1">, + sha256 = {} :: hash<"sha256">, + sha512 = {} :: hash<"sha512">, + blake2b256 = {} :: hash<"blake2b256">, +}) + +function crypto.digest(hash: hash, message: string | buffer): buffer + error("not implemented") +end + +crypto.secretbox = {} + +export type secretbox = { + read ciphertext: buffer, + read nonce: buffer, + read key: buffer, } -crypto.password = {} -function crypto.digest(hash: Hash, message: string | buffer): buffer +function crypto.secretbox.seal(message: string | buffer): secretbox + error("not implemented") +end + +function crypto.secretbox.open(box: secretbox): buffer error("not implemented") end +crypto.password = {} + function crypto.password.hash(password: string): buffer error("not implemented") end diff --git a/examples/secretbox.luau b/examples/secretbox.luau new file mode 100644 index 000000000..50332fde7 --- /dev/null +++ b/examples/secretbox.luau @@ -0,0 +1,20 @@ +local crypto = require("@lute/crypto") + +local message = "Hello, world!" +local box = crypto.secretbox.seal(message) + +function hexify(digest) + local hex = {} + for i = 1, buffer.len(digest) do + hex[i] = string.format("%02x", buffer.readu8(digest, i - 1)) + end + return table.concat(hex) +end + +print(hexify(box.ciphertext)) + +local opened = crypto.secretbox.open(box) +local output = buffer.readstring(opened, 0, buffer.len(opened)) + +print(output) +assert(output == message) diff --git a/lute/crypto/include/lute/crypto.h b/lute/crypto/include/lute/crypto.h index e8c3711f7..4df06a889 100644 --- a/lute/crypto/include/lute/crypto.h +++ b/lute/crypto/include/lute/crypto.h @@ -14,11 +14,22 @@ namespace crypto { static const char kHashProperty[] = "hash"; +static const char kSecretboxProperty[] = "secretbox"; static const char kPasswordProperty[] = "password"; static const char kDigestName[] = "digest"; int lua_digest(lua_State* L); +static const char kCiphertextField[] = "ciphertext"; +static const char kKeyField[] = "key"; +static const char kNonceField[] = "nonce"; + +static const char kSealName[] = "seal"; +int lua_seal(lua_State* L); + +static const char kOpenName[] = "open"; +int lua_open(lua_State* L); + static const char kPasswordHashName[] = "hash"; int lua_pwhash(lua_State* L); @@ -29,6 +40,7 @@ static const luaL_Reg lib[] = {{kDigestName, lua_digest}, {nullptr, nullptr}}; static const std::string properties[] = { kHashProperty, + kSecretboxProperty, kPasswordProperty, }; diff --git a/lute/crypto/src/crypto.cpp b/lute/crypto/src/crypto.cpp index c5bb54c0e..42caf11c2 100644 --- a/lute/crypto/src/crypto.cpp +++ b/lute/crypto/src/crypto.cpp @@ -1,10 +1,13 @@ #include "lute/crypto.h" +#include "lute/userdatas.h" #include "lua.h" #include "lualib.h" #include "openssl/digest.h" #include "sodium/crypto_pwhash.h" +#include "sodium/crypto_secretbox.h" +#include "sodium/randombytes.h" #include #include @@ -18,8 +21,6 @@ struct HashFunction const env_md_st* md; }; -static const int kHashFunctionTag = 81; - static const HashFunction hashFunctions[] = { {"md5", EVP_md5()}, {"sha1", EVP_sha1()}, @@ -34,7 +35,7 @@ int makeHashFunctionMap(lua_State* L) for (auto& [name, md] : hashFunctions) { - lua_pushlightuserdatatagged(L, (void*)md, kHashFunctionTag); + lua_pushlightuserdatatagged(L, (void*) md, kHashFunctionTag); lua_setfield(L, -2, name.c_str()); } @@ -80,6 +81,7 @@ BinaryData extractData(lua_State* L, int idx) luaL_error(L, "failed to extract binary data from stack: %d", idx); } +// digest(hash: hash, message: string | buffer): buffer int lua_digest(lua_State* L) { int argumentCount = lua_gettop(L); @@ -89,13 +91,96 @@ int lua_digest(lua_State* L) const env_md_st* hashFunction = getHashFunction(L, 1); BinaryData message = extractData(L, 2); - void* buffer = lua_newbuffer(L, EVP_MD_size(hashFunction)); - if (EVP_Digest(message.data, message.length, (uint8_t*)buffer, nullptr, hashFunction, nullptr) == 0) + uint8_t* buffer = static_cast(lua_newbuffer(L, EVP_MD_size(hashFunction))); + if (EVP_Digest(message.data, message.length, buffer, nullptr, hashFunction, nullptr) == 0) luaL_error(L, "%s: failed to compute hash", kDigestName); return 1; } +// seal(message: string | buffer): { ciphertext: buffer, key: buffer, nonce: buffer } +int lua_secretbox_seal(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 1) + luaL_error(L, "%s: expected 1 argument, but got %d", kSealName, argumentCount); + + BinaryData message = extractData(L, 1); + + lua_createtable(L, 0, 3); + + // ciphertext + uint8_t* buffer = static_cast(lua_newbuffer(L, crypto_secretbox_macbytes() + message.length)); + lua_setfield(L, -2, kCiphertextField); + + // key + uint8_t* key = static_cast(lua_newbuffer(L, crypto_secretbox_keybytes())); + crypto_secretbox_keygen(key); + lua_setfield(L, -2, kKeyField); + + // nonce + uint8_t* nonce = static_cast(lua_newbuffer(L, crypto_secretbox_noncebytes())); + randombytes_buf(nonce, crypto_secretbox_noncebytes()); + lua_setfield(L, -2, kNonceField); + + // compute the ciphertext based on the message, nonce, and key + crypto_secretbox_easy(buffer, static_cast(message.data), message.length, nonce, key); + + // freeze the result + lua_setreadonly(L, -1, true); + + return 1; +} + +// open(secretbox: { ciphertext: buffer, key: buffer, nonce: buffer }): buffer +int lua_secretbox_open(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 1) + luaL_error(L, "%s: expected 1 argument, but got %d", kOpenName, argumentCount); + + // read all three of the required fields out of the table + lua_getfield(L, 1, "ciphertext"); + lua_getfield(L, 1, "nonce"); + lua_getfield(L, 1, "key"); + + size_t ciphertextLength = 0; + uint8_t* ciphertext = static_cast(luaL_checkbuffer(L, 2, &ciphertextLength)); + + size_t nonceLength = 0; + uint8_t* nonce = static_cast(luaL_checkbuffer(L, 3, &nonceLength)); + if (nonceLength != crypto_secretbox_noncebytes()) + luaL_error(L, "%s: nonce buffer should be %d bytes", kOpenName, int(crypto_secretbox_noncebytes())); + + size_t keyLength = 0; + uint8_t* key = static_cast(luaL_checkbuffer(L, 4, &keyLength)); + if (keyLength != crypto_secretbox_keybytes()) + luaL_error(L, "%s: keys buffer should be %d bytes", kOpenName, int(crypto_secretbox_keybytes())); + + uint8_t* buffer = static_cast(lua_newbuffer(L, ciphertextLength - crypto_secretbox_macbytes())); + if (crypto_secretbox_open_easy(buffer, ciphertext, ciphertextLength, nonce, key) != 0) + luaL_error(L, "%s: failed to verify the message", kOpenName); + + return 1; +} + +int makeSecretboxLibrary(lua_State* L) +{ + lua_createtable(L, 0, 2); + + // seal + lua_pushcfunction(L, lua_secretbox_seal, kSealName); + lua_setfield(L, -2, kSealName); + + // open + lua_pushcfunction(L, lua_secretbox_open, kOpenName); + lua_setfield(L, -2, kOpenName); + + lua_setreadonly(L, -1, true); + + return 1; +} + // hash(password: string): buffer int lua_pwhash(lua_State* L) { @@ -145,6 +230,8 @@ int makePasswordHashLibrary(lua_State* L) lua_pushcfunction(L, lua_pwhash_verify, kVerifyPasswordHashName); lua_setfield(L, -2, kVerifyPasswordHashName); + lua_setreadonly(L, -1, true); + return 1; } @@ -174,10 +261,13 @@ int luteopen_crypto(lua_State* L) crypto::makeHashFunctionMap(L); lua_setfield(L, -2, crypto::kHashProperty); + crypto::makeSecretboxLibrary(L); + lua_setfield(L, -2, crypto::kSecretboxProperty); + crypto::makePasswordHashLibrary(L); lua_setfield(L, -2, crypto::kPasswordProperty); - lua_setreadonly(L, -1, 1); + lua_setreadonly(L, -1, true); return 1; } diff --git a/lute/runtime/include/lute/userdatas.h b/lute/runtime/include/lute/userdatas.h index 2986772b5..5cc6153d9 100644 --- a/lute/runtime/include/lute/userdatas.h +++ b/lute/runtime/include/lute/userdatas.h @@ -3,4 +3,5 @@ // all tags count down from 128 constexpr int kDurationTag = 127; constexpr int kInstantTag = 126; -constexpr int kWatchHandleTag = 125; \ No newline at end of file +constexpr int kWatchHandleTag = 125; +constexpr int kHashFunctionTag = 124; diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index fe130e876..d8affc1c2 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -81,6 +81,30 @@ function assertions.tableeq(lhs: { [unknown]: unknown }, rhs: { [unknown]: unkno end end +function assertions.buffereq(lhs: buffer, rhs: buffer) + if typeof(lhs) ~= "buffer" then + error(`lhs is of type {typeof(lhs)}, not buffer`) + end + + if typeof(rhs) ~= "buffer" then + error(`rhs is of type {typeof(rhs)}, not buffer`) + end + + if buffer.len(lhs) ~= buffer.len(rhs) then + error("buffers are of unequal length") + end + + local len = buffer.len(lhs) + for offset = 0, len - 1 do + local left = buffer.readu8(lhs, offset) + local right = buffer.readu8(rhs, offset) + + if left ~= right then + error(string.format("lhs[%d] = %02x, but rhs[%d] = %02x", offset, left, offset, right)) + end + end +end + export type asserts = typeof(assertions) return table.freeze(assertions) diff --git a/tests/runtime/crypto.test.luau b/tests/runtime/crypto.test.luau new file mode 100644 index 000000000..8013ab2f9 --- /dev/null +++ b/tests/runtime/crypto.test.luau @@ -0,0 +1,113 @@ +local test = require("@std/test") + +local crypto = require("@lute/crypto") + +test.suite("CryptoRuntimeTests", function(suite) + suite:case("secretbox_is_a_roundtrip_strings", function(assert) + local message = "Remember, remember the Fifth of November." + local box = crypto.secretbox.seal(message) + + local opened = crypto.secretbox.open(box) + local output = buffer.readstring(opened, 0, buffer.len(opened)) + + assert.eq(output, message) + end) + + suite:case("secretbox_is_a_roundtrip_buffers", function(assert) + local message = "Gunpowder, treason, and plot." + local buf = buffer.create(#message) + buffer.writestring(buf, 0, message) + + local box = crypto.secretbox.seal(buf) + local opened = crypto.secretbox.open(box) + + assert.buffereq(opened, buf) + end) + + suite:case("secretbox_errors_on_tampering", function(assert) + local message = "Gunpowder, treason, and plot." + local buf = buffer.create(#message) + buffer.writestring(buf, 0, message) + + local box = crypto.secretbox.seal(buf) + buffer.writeu32(box.ciphertext, 2, 10_11_2025) + + assert.errors(function() + crypto.secretbox.open(box) + end) + end) + + suite:case("secretbox_no_open_string", function(assert) + local message = "I see no reason why gunpowder treason should ever be forgot." + + local buf = buffer.create(#message) + buffer.writestring(buf, 0, message) + + local box = crypto.secretbox.seal(buf) + local other = { + ciphertext = buffer.readstring(box.ciphertext, 0, #message), + nonce = box.nonce, + key = box.key, + } + + assert.errors(function() + crypto.secretbox.open(other) + end) + end) + + suite:case("secretbox_missized_key", function(assert) + local message = "I see no reason why gunpowder treason should ever be forgot." + + local buf = buffer.create(#message) + buffer.writestring(buf, 0, message) + + local box = crypto.secretbox.seal(buf) + local other = { + ciphertext = box.ciphertext, + nonce = box.nonce, + key = buffer.create(5), + } + + assert.errors(function() + crypto.secretbox.open(other) + end) + end) + + suite:case("secretbox_on_ill_typed_inputs", function(assert) + local message = "I see no reason why gunpowder treason should ever be forgot." + + assert.errors(function() + crypto.secretbox.seal({ message } :: any) + end) + + assert.errors(function() + crypto.secretbox.seal({ message, message, message } :: any) + end) + + assert.errors(function() + crypto.secretbox.seal({ ciphertext = message, key = message, nonce = message } :: any) + end) + + assert.errors(function() + crypto.secretbox.open(message :: any) + end) + + assert.errors(function() + crypto.secretbox.open({ ciphertext = message, key = buffer.create(), nonce = "woof" } :: any) + end) + + assert.errors(function() + crypto.secretbox.open({ ciphertext = message, key = "woof", nonce = buffer.create() } :: any) + end) + + assert.errors(function() + crypto.secretbox.open({ ciphertext = message } :: any) + end) + + assert.errors(function() + crypto.secretbox.open(nil :: any) + end) + end) +end) + +test.run() From 755d11be61444b0a50d189dd04cb928bd5b41412 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 12 Nov 2025 11:00:17 -0800 Subject: [PATCH 156/642] stdlib: Update utils type annotations (#570) This got missed during the refactor I did yesterday (#565), probably because utils was requiring `@lute/luau` rather than `@std/luau` --- lute/std/libs/syntax/utils.luau | 116 ++++++++++++++++---------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/lute/std/libs/syntax/utils.luau b/lute/std/libs/syntax/utils.luau index 4c570cb9e..85f1b9966 100644 --- a/lute/std/libs/syntax/utils.luau +++ b/lute/std/libs/syntax/utils.luau @@ -1,232 +1,232 @@ -local luau = require("@lute/luau") +local types = require("./types") local utils = {} -function utils.isLocal(n: luau.AstNode): luau.AstLocal? +function utils.isLocal(n: types.AstNode): types.AstLocal? return if n.kind == "local" then n else nil end -function utils.isExprGroup(n: luau.AstNode): luau.AstExprGroup? +function utils.isExprGroup(n: types.AstNode): types.AstExprGroup? return if n.kind == "expr" and n.tag == "group" then n else nil end -function utils.isExprConstantNil(n: luau.AstNode): luau.AstExprConstantNil? +function utils.isExprConstantNil(n: types.AstNode): types.AstExprConstantNil? return if n.kind == "expr" and n.tag == "nil" then n else nil end -function utils.isExprConstantBool(n: luau.AstNode): luau.AstExprConstantBool? +function utils.isExprConstantBool(n: types.AstNode): types.AstExprConstantBool? return if n.kind == "expr" and n.tag == "bool" then n else nil end -function utils.isExprConstantNumber(n: luau.AstNode): luau.AstExprConstantNumber? +function utils.isExprConstantNumber(n: types.AstNode): types.AstExprConstantNumber? return if n.kind == "expr" and n.tag == "number" then n else nil end -function utils.isExprConstantString(n: luau.AstNode): luau.AstExprConstantString? +function utils.isExprConstantString(n: types.AstNode): types.AstExprConstantString? return if n.kind == "expr" and n.tag == "string" then n else nil end -function utils.isExprLocal(n: luau.AstNode): luau.AstExprLocal? +function utils.isExprLocal(n: types.AstNode): types.AstExprLocal? return if n.kind == "expr" and n.tag == "local" then n else nil end -function utils.isExprGlobal(n: luau.AstNode): luau.AstExprGlobal? +function utils.isExprGlobal(n: types.AstNode): types.AstExprGlobal? return if n.kind == "expr" and n.tag == "global" then n else nil end -function utils.isExprVarargs(n: luau.AstNode): luau.AstExprVarargs? +function utils.isExprVarargs(n: types.AstNode): types.AstExprVarargs? return if n.kind == "expr" and n.tag == "vararg" then n else nil end -function utils.isExprCall(n: luau.AstNode): luau.AstExprCall? +function utils.isExprCall(n: types.AstNode): types.AstExprCall? return if n.kind == "expr" and n.tag == "call" then n else nil end -function utils.isExprIndexName(n: luau.AstNode): luau.AstExprIndexName? +function utils.isExprIndexName(n: types.AstNode): types.AstExprIndexName? return if n.kind == "expr" and n.tag == "indexname" then n else nil end -function utils.isExprIndexExpr(n: luau.AstNode): luau.AstExprIndexExpr? +function utils.isExprIndexExpr(n: types.AstNode): types.AstExprIndexExpr? return if n.kind == "expr" and n.tag == "index" then n else nil end -function utils.isExprAnonymousFunction(n: luau.AstNode): luau.AstExprAnonymousFunction? +function utils.isExprAnonymousFunction(n: types.AstNode): types.AstExprAnonymousFunction? return if n.kind == "expr" and n.tag == "function" then n else nil end -function utils.isExprTable(n: luau.AstNode): luau.AstExprTable? +function utils.isExprTable(n: types.AstNode): types.AstExprTable? return if n.kind == "expr" and n.tag == "table" then n else nil end -function utils.isExprUnary(n: luau.AstNode): luau.AstExprUnary? +function utils.isExprUnary(n: types.AstNode): types.AstExprUnary? return if n.kind == "expr" and n.tag == "unary" then n else nil end -function utils.isExprBinary(n: luau.AstNode): luau.AstExprBinary? +function utils.isExprBinary(n: types.AstNode): types.AstExprBinary? return if n.kind == "expr" and n.tag == "binary" then n else nil end -function utils.isExprInterpString(n: luau.AstNode): luau.AstExprInterpString? +function utils.isExprInterpString(n: types.AstNode): types.AstExprInterpString? return if n.kind == "expr" and n.tag == "interpolatedstring" then n else nil end -function utils.isExprTypeAssertion(n: luau.AstNode): luau.AstExprTypeAssertion? +function utils.isExprTypeAssertion(n: types.AstNode): types.AstExprTypeAssertion? return if n.kind == "expr" and n.tag == "cast" then n else nil end -function utils.isExprIfElse(n: luau.AstNode): luau.AstExprIfElse? +function utils.isExprIfElse(n: types.AstNode): types.AstExprIfElse? return if n.kind == "expr" and n.tag == "conditional" then n else nil end -function utils.isExpr(n: luau.AstNode): luau.AstExpr? +function utils.isExpr(n: types.AstNode): types.AstExpr? return if n.kind == "expr" then n else nil end -function utils.isStatBlock(n: luau.AstNode): luau.AstStatBlock? +function utils.isStatBlock(n: types.AstNode): types.AstStatBlock? return if n.kind == "stat" and n.tag == "block" then n else nil end -function utils.isStatIf(n: luau.AstNode): luau.AstStatIf? +function utils.isStatIf(n: types.AstNode): types.AstStatIf? return if n.kind == "stat" and n.tag == "conditional" then n else nil end -function utils.isStatWhile(n: luau.AstNode): luau.AstStatWhile? +function utils.isStatWhile(n: types.AstNode): types.AstStatWhile? return if n.kind == "stat" and n.tag == "while" then n else nil end -function utils.isStatRepeat(n: luau.AstNode): luau.AstStatRepeat? +function utils.isStatRepeat(n: types.AstNode): types.AstStatRepeat? return if n.kind == "stat" and n.tag == "repeat" then n else nil end -function utils.isStatBreak(n: luau.AstNode): luau.AstStatBreak? +function utils.isStatBreak(n: types.AstNode): types.AstStatBreak? return if n.kind == "stat" and n.tag == "break" then n else nil end -function utils.isStatContinue(n: luau.AstNode): luau.AstStatContinue? +function utils.isStatContinue(n: types.AstNode): types.AstStatContinue? return if n.kind == "stat" and n.tag == "continue" then n else nil end -function utils.isStatReturn(n: luau.AstNode): luau.AstStatReturn? +function utils.isStatReturn(n: types.AstNode): types.AstStatReturn? return if n.kind == "stat" and n.tag == "return" then n else nil end -function utils.isStatExpr(n: luau.AstNode): luau.AstStatExpr? +function utils.isStatExpr(n: types.AstNode): types.AstStatExpr? return if n.kind == "stat" and n.tag == "expression" then n else nil end -function utils.isStatLocal(n: luau.AstNode): luau.AstStatLocal? +function utils.isStatLocal(n: types.AstNode): types.AstStatLocal? return if n.kind == "stat" and n.tag == "local" then n else nil end -function utils.isStatFor(n: luau.AstNode): luau.AstStatFor? +function utils.isStatFor(n: types.AstNode): types.AstStatFor? return if n.kind == "stat" and n.tag == "for" then n else nil end -function utils.isStatForIn(n: luau.AstNode): luau.AstStatForIn? +function utils.isStatForIn(n: types.AstNode): types.AstStatForIn? return if n.kind == "stat" and n.tag == "forin" then n else nil end -function utils.isStatAssign(n: luau.AstNode): luau.AstStatAssign? +function utils.isStatAssign(n: types.AstNode): types.AstStatAssign? return if n.kind == "stat" and n.tag == "assign" then n else nil end -function utils.isStatCompoundAssign(n: luau.AstNode): luau.AstStatCompoundAssign? +function utils.isStatCompoundAssign(n: types.AstNode): types.AstStatCompoundAssign? return if n.kind == "stat" and n.tag == "compoundassign" then n else nil end -function utils.isStatFunction(n: luau.AstNode): luau.AstStatFunction? +function utils.isStatFunction(n: types.AstNode): types.AstStatFunction? return if n.kind == "stat" and n.tag == "function" then n else nil end -function utils.isStatLocalFunction(n: luau.AstNode): luau.AstStatLocalFunction? +function utils.isStatLocalFunction(n: types.AstNode): types.AstStatLocalFunction? return if n.kind == "stat" and n.tag == "localfunction" then n else nil end -function utils.isStatTypeAlias(n: luau.AstNode): luau.AstStatTypeAlias? +function utils.isStatTypeAlias(n: types.AstNode): types.AstStatTypeAlias? return if n.kind == "stat" and n.tag == "typealias" then n else nil end -function utils.isStatTypeFunction(n: luau.AstNode): luau.AstStatTypeFunction? +function utils.isStatTypeFunction(n: types.AstNode): types.AstStatTypeFunction? return if n.kind == "stat" and n.tag == "typefunction" then n else nil end -function utils.isStat(n: luau.AstNode): luau.AstStat? +function utils.isStat(n: types.AstNode): types.AstStat? return if n.kind == "stat" then n else nil end -function utils.isGenericType(n: luau.AstNode): luau.AstGenericType? +function utils.isGenericType(n: types.AstNode): types.AstGenericType? return if n.kind == "type" and n.tag == "generic" then n else nil end -function utils.isGenericTypePack(n: luau.AstNode): luau.AstGenericTypePack? +function utils.isGenericTypePack(n: types.AstNode): types.AstGenericTypePack? return if n.kind == "typepack" and n.tag == "genericpack" then n else nil end -function utils.isTypeReference(n: luau.AstNode): luau.AstTypeReference? +function utils.isTypeReference(n: types.AstNode): types.AstTypeReference? return if n.kind == "type" and n.tag == "reference" then n else nil end -function utils.isTypeSingletonBool(n: luau.AstNode): luau.AstTypeSingletonBool? +function utils.isTypeSingletonBool(n: types.AstNode): types.AstTypeSingletonBool? return if n.kind == "type" and n.tag == "boolean" then n else nil end -function utils.isTypeSingletonString(n: luau.AstNode): luau.AstTypeSingletonString? +function utils.isTypeSingletonString(n: types.AstNode): types.AstTypeSingletonString? return if n.kind == "type" and n.tag == "string" then n else nil end -function utils.isTypeTypeof(n: luau.AstNode): luau.AstTypeTypeof? +function utils.isTypeTypeof(n: types.AstNode): types.AstTypeTypeof? return if n.kind == "type" and n.tag == "typeof" then n else nil end -function utils.isTypeGroup(n: luau.AstNode): luau.AstTypeGroup? +function utils.isTypeGroup(n: types.AstNode): types.AstTypeGroup? return if n.kind == "type" and n.tag == "group" then n else nil end -function utils.isTypeOptional(n: luau.AstNode): luau.AstTypeOptional? +function utils.isTypeOptional(n: types.AstNode): types.AstTypeOptional? return if n.kind == "type" and n.tag == "optional" then n else nil end -function utils.isTypeUnion(n: luau.AstNode): luau.AstTypeUnion? +function utils.isTypeUnion(n: types.AstNode): types.AstTypeUnion? return if n.kind == "type" and n.tag == "union" then n else nil end -function utils.isTypeIntersection(n: luau.AstNode): luau.AstTypeIntersection? +function utils.isTypeIntersection(n: types.AstNode): types.AstTypeIntersection? return if n.kind == "type" and n.tag == "intersection" then n else nil end -function utils.isTypeArray(n: luau.AstNode): luau.AstTypeArray? +function utils.isTypeArray(n: types.AstNode): types.AstTypeArray? return if n.kind == "type" and n.tag == "array" then n else nil end -function utils.isTypeTable(n: luau.AstNode): luau.AstTypeTable? +function utils.isTypeTable(n: types.AstNode): types.AstTypeTable? return if n.kind == "type" and n.tag == "table" then n else nil end -function utils.isTypeFunction(n: luau.AstNode): luau.AstTypeFunction? +function utils.isTypeFunction(n: types.AstNode): types.AstTypeFunction? return if n.kind == "type" and n.tag == "function" then n else nil end -function utils.isType(n: luau.AstNode): luau.AstType? +function utils.isType(n: types.AstNode): types.AstType? return if n.kind == "type" then n else nil end -function utils.isTypePackExplicit(n: luau.AstNode): luau.AstTypePackExplicit? +function utils.isTypePackExplicit(n: types.AstNode): types.AstTypePackExplicit? return if n.kind == "typepack" and n.tag == "explicit" then n else nil end -function utils.isTypePackVariadic(n: luau.AstNode): luau.AstTypePackVariadic? +function utils.isTypePackVariadic(n: types.AstNode): types.AstTypePackVariadic? return if n.kind == "typepack" and n.tag == "variadic" then n else nil end -function utils.isTypePackGeneric(n: luau.AstNode): luau.AstTypePackGeneric? +function utils.isTypePackGeneric(n: types.AstNode): types.AstTypePackGeneric? return if n.kind == "typepack" and n.tag == "generic" then n else nil end -function utils.isTypePack(n: luau.AstNode): luau.AstTypePack? +function utils.isTypePack(n: types.AstNode): types.AstTypePack? return if n.kind == "typepack" then n else nil end -function utils.isToken(n: luau.AstNode): luau.Token? +function utils.isToken(n: types.AstNode): types.Token? return if n.kind == "token" then n else nil end From fc347ac95ffaa7a7748e5322350311efca1f9e5c Mon Sep 17 00:00:00 2001 From: ariel Date: Wed, 12 Nov 2025 11:13:36 -0800 Subject: [PATCH 157/642] crypto: include the ability to separately generate a key before sealing (#571) The initial `secretbox` API provided opinionatedly generates keys and nonces on your behalf in `seal`. I believe this makes sense for one-off situations, but there's also definitely situations where people would reasonably want to reuse the key across a series of messages being sent with authenticated encryption. We should clearly make that possible. --- definitions/crypto.luau | 4 +++ lute/crypto/include/lute/crypto.h | 7 +++-- lute/crypto/src/crypto.cpp | 49 +++++++++++++++++++++++++------ tests/runtime/crypto.test.luau | 12 ++++++++ 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/definitions/crypto.luau b/definitions/crypto.luau index 37629f491..dad1227d6 100644 --- a/definitions/crypto.luau +++ b/definitions/crypto.luau @@ -22,6 +22,10 @@ export type secretbox = { read key: buffer, } +function crypto.secretbox.keygen(): buffer + error("not implemented") +end + function crypto.secretbox.seal(message: string | buffer): secretbox error("not implemented") end diff --git a/lute/crypto/include/lute/crypto.h b/lute/crypto/include/lute/crypto.h index 4df06a889..eda8835d1 100644 --- a/lute/crypto/include/lute/crypto.h +++ b/lute/crypto/include/lute/crypto.h @@ -24,11 +24,14 @@ static const char kCiphertextField[] = "ciphertext"; static const char kKeyField[] = "key"; static const char kNonceField[] = "nonce"; +static const char kKeygenName[] = "keygen"; +int lua_secretbox_keygen(lua_State* L); + static const char kSealName[] = "seal"; -int lua_seal(lua_State* L); +int lua_secretbox_seal(lua_State* L); static const char kOpenName[] = "open"; -int lua_open(lua_State* L); +int lua_secretbox_open(lua_State* L); static const char kPasswordHashName[] = "hash"; int lua_pwhash(lua_State* L); diff --git a/lute/crypto/src/crypto.cpp b/lute/crypto/src/crypto.cpp index 42caf11c2..3ed774070 100644 --- a/lute/crypto/src/crypto.cpp +++ b/lute/crypto/src/crypto.cpp @@ -98,12 +98,25 @@ int lua_digest(lua_State* L) return 1; } -// seal(message: string | buffer): { ciphertext: buffer, key: buffer, nonce: buffer } +// keygen(): buffer +int lua_secretbox_keygen(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 0) + luaL_error(L, "%s: expected no arguments, but got %d", kKeygenName, argumentCount); + + uint8_t* key = static_cast(lua_newbuffer(L, crypto_secretbox_keybytes())); + crypto_secretbox_keygen(key); + + return 1; +} + +// seal(message: string | buffer, key: buffer?): { ciphertext: buffer, key: buffer, nonce: buffer } int lua_secretbox_seal(lua_State* L) { int argumentCount = lua_gettop(L); - if (argumentCount != 1) - luaL_error(L, "%s: expected 1 argument, but got %d", kSealName, argumentCount); + if (argumentCount != 1 && argumentCount != 2) + luaL_error(L, "%s: expected 1 or 2 arguments, but got %d", kSealName, argumentCount); BinaryData message = extractData(L, 1); @@ -113,11 +126,25 @@ int lua_secretbox_seal(lua_State* L) uint8_t* buffer = static_cast(lua_newbuffer(L, crypto_secretbox_macbytes() + message.length)); lua_setfield(L, -2, kCiphertextField); - // key - uint8_t* key = static_cast(lua_newbuffer(L, crypto_secretbox_keybytes())); - crypto_secretbox_keygen(key); - lua_setfield(L, -2, kKeyField); + uint8_t* key; + // user provided a key + if (argumentCount == 2) + { + size_t keyLength = 0; + key = static_cast(luaL_checkbuffer(L, 2, &keyLength)); + if (keyLength != crypto_secretbox_keybytes()) + luaL_error(L, "%s: key buffer should be %d bytes", kOpenName, int(crypto_secretbox_keybytes())); + lua_pushvalue(L, 2); + lua_setfield(L, -2, kKeyField); + } + else + { + key = static_cast(lua_newbuffer(L, crypto_secretbox_keybytes())); + crypto_secretbox_keygen(key); + lua_setfield(L, -2, kKeyField); + } + // nonce uint8_t* nonce = static_cast(lua_newbuffer(L, crypto_secretbox_noncebytes())); randombytes_buf(nonce, crypto_secretbox_noncebytes()); @@ -155,7 +182,7 @@ int lua_secretbox_open(lua_State* L) size_t keyLength = 0; uint8_t* key = static_cast(luaL_checkbuffer(L, 4, &keyLength)); if (keyLength != crypto_secretbox_keybytes()) - luaL_error(L, "%s: keys buffer should be %d bytes", kOpenName, int(crypto_secretbox_keybytes())); + luaL_error(L, "%s: key buffer should be %d bytes", kOpenName, int(crypto_secretbox_keybytes())); uint8_t* buffer = static_cast(lua_newbuffer(L, ciphertextLength - crypto_secretbox_macbytes())); if (crypto_secretbox_open_easy(buffer, ciphertext, ciphertextLength, nonce, key) != 0) @@ -166,7 +193,11 @@ int lua_secretbox_open(lua_State* L) int makeSecretboxLibrary(lua_State* L) { - lua_createtable(L, 0, 2); + lua_createtable(L, 0, 3); + + // keygen + lua_pushcfunction(L, lua_secretbox_keygen, kKeygenName); + lua_setfield(L, -2, kKeygenName); // seal lua_pushcfunction(L, lua_secretbox_seal, kSealName); diff --git a/tests/runtime/crypto.test.luau b/tests/runtime/crypto.test.luau index 8013ab2f9..5062f7aaf 100644 --- a/tests/runtime/crypto.test.luau +++ b/tests/runtime/crypto.test.luau @@ -24,6 +24,18 @@ test.suite("CryptoRuntimeTests", function(suite) assert.buffereq(opened, buf) end) + suite:case("secretbox_separate_keygen", function(assert) + local key = crypto.secretbox.keygen() + + local message = "Remember, remember the Fifth of November." + local box = crypto.secretbox.seal(message, key) + + local opened = crypto.secretbox.open(box) + local output = buffer.readstring(opened, 0, buffer.len(opened)) + + assert.eq(output, message) + end) + suite:case("secretbox_errors_on_tampering", function(assert) local message = "Gunpowder, treason, and plot." local buf = buffer.create(#message) From 24491fbc7e6fbf79f05ad761def8b1bf7f8e5935 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 12 Nov 2025 15:34:51 -0800 Subject: [PATCH 158/642] lute stdlib: Update createVisitor to take a default callback (#569) Creating visitors today can be a bit of a pain if you want to override most of the default visitor's fields. I added an optional visit callback to make life a bit easier. Also found some incorrect type annotations and unused requires along the way. --- lute/std/libs/syntax/printer.luau | 75 +++++-------------------------- lute/std/libs/syntax/query.luau | 73 +++++------------------------- lute/std/libs/syntax/visitor.luau | 69 ++++++++++++++++++++++++++-- tests/std/syntax/query.test.luau | 1 - 4 files changed, 87 insertions(+), 131 deletions(-) diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 52c1f7ea4..ad1da1acf 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -11,12 +11,12 @@ type PrintVisitor = visitor.Visitor & { cursor: number, replacements: types.replacements, write: (self: PrintVisitor, str: string) -> (), - printTrivia: (self: PrintVisitor, trivia: luau.Trivia) -> (), - printTriviaList: (self: PrintVisitor, trivia: { luau.Trivia }) -> (), - printToken: (self: PrintVisitor, token: luau.Token) -> (), - printString: (self: PrintVisitor, expr: luau.AstExprConstantString | luau.AstTypeSingletonString) -> (), - printInterpolatedString: (self: PrintVisitor, expr: luau.AstExprInterpString) -> (), - printReplacement: (self: PrintVisitor, node: luau.AstNode, replacement: types.replacement) -> (), + printTrivia: (self: PrintVisitor, trivia: types.Trivia) -> (), + printTriviaList: (self: PrintVisitor, trivia: { types.Trivia }) -> (), + printToken: (self: PrintVisitor, token: types.Token) -> (), + printString: (self: PrintVisitor, expr: types.AstExprConstantString | types.AstTypeSingletonString) -> (), + printInterpolatedString: (self: PrintVisitor, expr: types.AstExprInterpString) -> (), + printReplacement: (self: PrintVisitor, node: types.AstNode, replacement: types.replacement) -> (), } local function exhaustiveMatch(value: never): never error(`Unknown value in exhaustive match: {value}`) @@ -82,7 +82,7 @@ local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprIn end end -local function printReplacement(self: PrintVisitor, node: luau.AstNode, replacement: types.replacement) +local function printReplacement(self: PrintVisitor, node: types.AstNode, replacement: types.replacement) if typeof(replacement) == "string" then self:write(replacement) elseif replacement.kind ~= nil then -- LUAUFIX: type solver upset comparing string singleton union to nil @@ -130,9 +130,11 @@ local function printVisitor(replacements: types.replacements?) return false end + printer = visitor.create(maybeReplace) :: PrintVisitor + printer.result = result printer.cursor = cursor - printer.replacements = if replacements ~= nil then replacements else {} + printer.replacements = if replacements ~= nil then replacements else {} :: types.replacements printer.write = write printer.printTrivia = printTrivia printer.printTriviaList = printTriviaList @@ -140,80 +142,27 @@ local function printVisitor(replacements: types.replacements?) printer.printString = printString printer.printInterpolatedString = printInterpolatedString printer.printReplacement = printReplacement - - printer.visitBlock = maybeReplace - printer.visitBlockEnd = maybeReplace - printer.visitIf = maybeReplace - printer.visitWhile = maybeReplace - printer.visitRepeat = maybeReplace - printer.visitReturn = maybeReplace - printer.visitLocalDeclaration = maybeReplace - printer.visitLocalDeclarationEnd = maybeReplace - printer.visitFor = maybeReplace - printer.visitForIn = maybeReplace - printer.visitAssign = maybeReplace - printer.visitCompoundAssign = maybeReplace - printer.visitFunction = maybeReplace - printer.visitLocalFunction = maybeReplace - printer.visitTypeAlias = maybeReplace - printer.visitStatTypeFunction = maybeReplace - - printer.visitExpression = maybeReplace - printer.visitExpressionEnd = maybeReplace - printer.visitLocalReference = maybeReplace - printer.visitGlobal = maybeReplace - printer.visitCall = maybeReplace - printer.visitUnary = maybeReplace - printer.visitBinary = maybeReplace - printer.visitAnonymousFunction = maybeReplace - printer.visitTableItem = maybeReplace - printer.visitTable = maybeReplace - printer.visitIndexName = maybeReplace - printer.visitIndexExpr = maybeReplace - printer.visitGroup = maybeReplace - printer.visitInterpolatedString = function(node: luau.AstExprInterpString) + printer.visitInterpolatedString = function(node: types.AstExprInterpString) printer:printInterpolatedString(node) return false end - printer.visitTypeAssertion = maybeReplace - printer.visitIfExpression = maybeReplace - - printer.visitTypeReference = maybeReplace - printer.visitTypeBoolean = maybeReplace printer.visitTypeString = function(node: types.AstTypeSingletonString) printer:printString(node) return false end - printer.visitTypeTypeof = maybeReplace - printer.visitTypeGroup = maybeReplace - printer.visitTypeUnion = maybeReplace - printer.visitTypeIntersection = maybeReplace - printer.visitTypeArray = maybeReplace - printer.visitTypeTable = maybeReplace - printer.visitTypeFunction = maybeReplace - - printer.visitTypePackExplicit = maybeReplace - printer.visitTypePackGeneric = maybeReplace - printer.visitTypePackVariadic = maybeReplace - printer.visitToken = function(node: types.Token) printer:printToken(node) return false end - printer.visitNil = maybeReplace printer.visitString = function(node: types.AstExprConstantString) printer:printString(node) return false end - printer.visitBoolean = maybeReplace - printer.visitNumber = maybeReplace - printer.visitLocal = maybeReplace - printer.visitVarargs = maybeReplace return printer end -function printerLib.printnode(node: luau.AstNode, replacements: types.replacements?): string +function printerLib.printnode(node: types.AstNode, replacements: types.replacements?): string local printer = printVisitor(replacements) visitor.visit(node, printer) return buffer.readstring(printer.result, 0, printer.cursor) diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index d56dd3d53..000ee3b42 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -2,7 +2,6 @@ -- @std/syntax/query -- Provides utility functions for querying Luau AST nodes -local luau = require("@std/luau") local printer = require("./printer") local types = require("./types") local visitor = require("./visitor") @@ -76,68 +75,16 @@ function queryLib.selectFromRoot(ast: node, fn: (node) -> T?): query return true end - local selectVisitor: visitor.Visitor = { - visitBlock = visit, - visitBlockEnd = function(n) end, - visitIf = visit, - visitWhile = visit, - visitRepeat = visit, - visitReturn = visit, - visitLocalDeclaration = visit, - visitLocalDeclarationEnd = function(n) end, - visitFor = visit, - visitForIn = visit, - visitAssign = visit, - visitCompoundAssign = visit, - visitFunction = visit, - visitLocalFunction = visit, - visitTypeAlias = visit, - visitStatTypeFunction = visit, - - visitExpression = function(n) - return true - end, - visitExpressionEnd = function(n) end, - visitLocalReference = visit, - visitGlobal = visit, - visitCall = visit, - visitUnary = visit, - visitBinary = visit, - visitAnonymousFunction = visit, - visitTableItem = visit, - visitTable = visit, - visitIndexName = visit, - visitIndexExpr = visit, - visitGroup = visit, - visitInterpolatedString = visit, - visitTypeAssertion = visit, - visitIfExpression = visit, - - visitTypeReference = visit, - visitTypeBoolean = visit, - visitTypeString = visit, - visitTypeTypeof = visit, - visitTypeGroup = visit, - visitTypeUnion = visit, - visitTypeIntersection = visit, - visitTypeArray = visit, - visitTypeTable = visit, - visitTypeFunction = visit, - - visitTypePackExplicit = visit, - visitTypePackGeneric = visit, - visitTypePackVariadic = visit, - - visitToken = function(n) - return true - end, - visitNil = visit, - visitString = visit, - visitBoolean = visit, - visitNumber = visit, - visitLocal = visit, - visitVarargs = visit, - } + local selectVisitor = visitor.create(visit) + selectVisitor.visitBlockEnd = function(n) end + selectVisitor.visitLocalDeclarationEnd = function(n) end + selectVisitor.visitExpression = function(n) + return true + end + selectVisitor.visitExpressionEnd = function(n) end + selectVisitor.visitToken = function(n) + return true + end visitor.visit(ast, selectVisitor) diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 298a30c60..3fa244de3 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -571,7 +571,7 @@ local function visitIfExpression(node: types.AstExprIfElse, visitor: Visitor) end local function visitTypeOrPack(node: types.AstType | types.AstTypePack, visitor: Visitor) - if node.tag == "explicit" or node.tag == "generic" or node.tag == "variadic" then + if node.kind == "typepack" then visitTypePack(node, visitor) else visitType(node, visitor) @@ -877,11 +877,72 @@ function visitTypePack(type: types.AstTypePack, visitor: Visitor) end end -local function create() - return table.clone(defaultVisitor) +local function create(visit: ((types.AstNode) -> boolean)?): Visitor + if visit then + return { + visitBlock = visit, + visitBlockEnd = visit :: any, + visitIf = visit, + visitWhile = visit, + visitRepeat = visit, + visitReturn = visit, + visitLocalDeclaration = visit, + visitLocalDeclarationEnd = visit, + visitFor = visit, + visitForIn = visit, + visitAssign = visit, + visitCompoundAssign = visit, + visitFunction = visit, + visitLocalFunction = visit, + visitTypeAlias = visit, + visitStatTypeFunction = visit, + + visitExpression = visit, + visitExpressionEnd = visit, + visitLocalReference = visit, + visitGlobal = visit, + visitCall = visit, + visitUnary = visit, + visitBinary = visit, + visitAnonymousFunction = visit, + visitTableItem = visit, + visitTable = visit, + visitIndexName = visit, + visitIndexExpr = visit, + visitGroup = visit, + visitInterpolatedString = visit, + visitTypeAssertion = visit, + visitIfExpression = visit, + + visitTypeReference = visit, + visitTypeBoolean = visit, + visitTypeString = visit, + visitTypeTypeof = visit, + visitTypeGroup = visit, + visitTypeUnion = visit, + visitTypeIntersection = visit, + visitTypeArray = visit, + visitTypeTable = visit, + visitTypeFunction = visit, + + visitTypePackExplicit = visit, + visitTypePackGeneric = visit, + visitTypePackVariadic = visit, + + visitToken = visit, + visitNil = visit, + visitString = visit, + visitBoolean = visit, + visitNumber = visit, + visitLocal = visit, + visitVarargs = visit, + } + else + return table.clone(defaultVisitor) + end end -local function visit(node: luau.AstNode, visitor: Visitor) +local function visit(node: types.AstNode, visitor: Visitor) if node.kind == "stat" then visitStatement(node, visitor) elseif node.kind == "expr" then diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index 54a18ecc0..34b06b9d6 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -1,4 +1,3 @@ -local luau = require("@std/luau") local syntax = require("@std/syntax") local query = require("@std/syntax/query") local test = require("@std/test") From 9cb35ed348091696ec26b79f4b12e91ba3f8db5a Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:50:38 -0800 Subject: [PATCH 159/642] Support executing `require` in dynamically generated, library-aware virtual filesystems (#339) This PR introduces `Package::RequireVfs` as an alternative to `RequireVfs` (the current abstraction around the filesystem, `@std` libraries, `@lute` libraries, and [optionally] CLI commands and embedded/bundled VFS). To accomplish this, we introduce `IRequireVfs` as a shared interface in `require.h` that both `RequireVfs` and `Library::RequireVfs` implement; `RequireCtx` now simply contains a `std::unique_ptr`. At a surface level, `Package::RequireVfs`'s logic is fairly similar to `RequireVfs`'s. Our `RequireVfs` implementation can be in any of the following VFS types at a given moment: `Disk`, `Std`, `Cli`, `Lute`. Similarly, `Library::RequireVfs` can be in any of the following: `UserlandVfs`, `Std`, `Lute`. The default VFS type (for example, when code execution of a project begins) for `Library::RequireVfs` is `UserlandVfs`. In this case, `Package::RequireVfs` manages an internal `Package::UserlandVfs`, which itself can be in one of two VFS states: `Disk` or `Subtree`. Conceptually, the `Disk` case is used to execute user code, and `Subtree` is used for executing code in installed libraries. To enable executing code that relies on installed libraries, `Package::UserlandVfs` must be constructed with two arguments: 1. `directDependencies`: the list of all libraries that user code directly depends on. 2. `libraries`: the list of all transitive dependencies of the user code, with their dependencies on each other also passed in explicitly. What this enables: when executing user code (in `Disk` mode), we are able to dynamically inject aliases to the dependencies in `directDependencies` that, when followed, transition `Package::UserlandVfs` into `Subtree` mode. Then, when executing dependency code (in `Subtree` mode), we are able to use the transitive dependency information passed in via the `libraries` constructor argument to inject aliases to each dependency's direct dependencies. Because this happens on a per-dependency basis, an alias `@dep` doesn't need to mean the same thing in `library1` and `library2`. Although it is still true that user code and any individual library cannot _directly_ depend on multiple versions of a dependency, multiple versions are completely fine in the set of transitive dependencies. --- lute/cli/src/climain.cpp | 8 +- lute/require/CMakeLists.txt | 4 + lute/require/include/lute/packagerequirevfs.h | 55 +++ lute/require/include/lute/require.h | 37 +- lute/require/include/lute/requirevfs.h | 29 +- lute/require/include/lute/userlandvfs.h | 95 ++++++ lute/require/src/packagerequirevfs.cpp | 261 ++++++++++++++ lute/require/src/require.cpp | 42 +-- lute/require/src/userlandvfs.cpp | 318 ++++++++++++++++++ lute/vm/src/spawn.cpp | 5 +- tests/CMakeLists.txt | 16 +- tests/src/libraryrequire.test.cpp | 88 +++++ tests/src/packages/dep/module.luau | 3 + tests/src/packages/internaldep/init.luau | 1 + tests/src/packages/packageentry/entry.luau | 7 + 15 files changed, 909 insertions(+), 60 deletions(-) create mode 100644 lute/require/include/lute/packagerequirevfs.h create mode 100644 lute/require/include/lute/userlandvfs.h create mode 100644 lute/require/src/packagerequirevfs.cpp create mode 100644 lute/require/src/userlandvfs.cpp create mode 100644 tests/src/libraryrequire.test.cpp create mode 100644 tests/src/packages/dep/module.luau create mode 100644 tests/src/packages/internaldep/init.luau create mode 100644 tests/src/packages/packageentry/entry.luau diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 92025059c..3d2ea24a3 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -15,6 +15,7 @@ #include "lute/ref.h" #include "lute/reporter.h" #include "lute/require.h" +#include "lute/requirevfs.h" #include "lute/runtime.h" #include "lute/staticrequires.h" #include "lute/system.h" @@ -36,6 +37,8 @@ #include "uv.h" +#include + #ifdef _WIN32 #include #endif @@ -113,7 +116,7 @@ void* createCliRequireContext(lua_State* L) if (!ctx) luaL_error(L, "unable to allocate RequireCtx"); - ctx = new (ctx) RequireCtx{CliVfs{}}; + ctx = new (ctx) RequireCtx{std::make_unique(CliVfs{})}; // Store RequireCtx in the registry to keep it alive for the lifetime of // this lua_State. Memory address is used as a key to avoid collisions. @@ -161,8 +164,7 @@ void* createBundleRequireContext(lua_State* L, Luau::DenseHashMap(BundleVfs{std::move(bundleMap)})}; // Store RequireCtx in the registry to keep it alive for the lifetime of // this lua_State. Memory address is used as a key to avoid collisions. diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index f4ad8dc01..2999e3a5d 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -6,18 +6,22 @@ target_sources(Lute.Require PRIVATE include/lute/filevfs.h include/lute/modulepath.h include/lute/options.h + include/lute/packagerequirevfs.h include/lute/require.h include/lute/requirevfs.h include/lute/stdlibvfs.h + include/lute/userlandvfs.h src/bundlevfs.cpp src/clivfs.cpp src/filevfs.cpp src/modulepath.cpp src/options.cpp + src/packagerequirevfs.cpp src/require.cpp src/requirevfs.cpp src/stdlibvfs.cpp + src/userlandvfs.cpp ) target_compile_features(Lute.Require PUBLIC cxx_std_17) diff --git a/lute/require/include/lute/packagerequirevfs.h b/lute/require/include/lute/packagerequirevfs.h new file mode 100644 index 000000000..f67645090 --- /dev/null +++ b/lute/require/include/lute/packagerequirevfs.h @@ -0,0 +1,55 @@ +#pragma once + +#include "lute/require.h" +#include "lute/stdlibvfs.h" +#include "lute/userlandvfs.h" + +namespace Package +{ + +class RequireVfs : public IRequireVfs +{ +public: + RequireVfs(UserlandVfs); + + bool isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const override; + + NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) override; + NavigationStatus jumpToAlias(lua_State* L, std::string_view path) override; + + NavigationStatus toParent(lua_State* L) override; + NavigationStatus toChild(lua_State* L, std::string_view name) override; + + bool isModulePresent(lua_State* L) const override; + std::string getContents(lua_State* L, const std::string& loadname) const override; + + std::string getChunkname(lua_State* L) const override; + std::string getLoadname(lua_State* L) const override; + std::string getCacheKey(lua_State* L) const override; + + ConfigStatus getConfigStatus(lua_State* L) const override; + std::string getConfig(lua_State* L) const override; + + bool isPrecompiled() const override + { + return false; + } + +private: + enum class VFSType + { + Userland, + Std, + Lute, + }; + + VFSType vfsType = VFSType::Userland; + + Package::UserlandVfs userlandVfs; + StdLibVfs stdLibVfs; + std::string lutePath; + + bool atFakeRoot = false; +}; + +} // namespace Package diff --git a/lute/require/include/lute/require.h b/lute/require/include/lute/require.h index 4be08c9b0..99bfcac52 100644 --- a/lute/require/include/lute/require.h +++ b/lute/require/include/lute/require.h @@ -1,20 +1,43 @@ #pragma once -#include "lute/bundlevfs.h" -#include "lute/clivfs.h" -#include "lute/requirevfs.h" +#include "lute/modulepath.h" #include "Luau/Require.h" +#include #include void requireConfigInit(luarequire_Configuration* config); +class IRequireVfs +{ +public: + virtual ~IRequireVfs() = default; + + virtual bool isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const = 0; + + virtual NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) = 0; + virtual NavigationStatus jumpToAlias(lua_State* L, std::string_view path) = 0; + + virtual NavigationStatus toParent(lua_State* L) = 0; + virtual NavigationStatus toChild(lua_State* L, std::string_view name) = 0; + + virtual bool isModulePresent(lua_State* L) const = 0; + virtual std::string getContents(lua_State* L, const std::string& loadname) const = 0; + + virtual std::string getChunkname(lua_State* L) const = 0; + virtual std::string getLoadname(lua_State* L) const = 0; + virtual std::string getCacheKey(lua_State* L) const = 0; + + virtual ConfigStatus getConfigStatus(lua_State* L) const = 0; + virtual std::string getConfig(lua_State* L) const = 0; + + virtual bool isPrecompiled() const = 0; +}; + struct RequireCtx { - RequireCtx(); - RequireCtx(CliVfs cliVfs); - RequireCtx(BundleVfs bundleVfs); + RequireCtx(std::unique_ptr vfs); - RequireVfs vfs; + std::unique_ptr vfs; }; diff --git a/lute/require/include/lute/requirevfs.h b/lute/require/include/lute/requirevfs.h index 1540835a0..552e47802 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -4,6 +4,7 @@ #include "lute/clivfs.h" #include "lute/filevfs.h" #include "lute/modulepath.h" +#include "lute/require.h" #include "lute/stdlibvfs.h" #include "lua.h" @@ -11,32 +12,32 @@ #include #include -class RequireVfs +class RequireVfs : public IRequireVfs { public: RequireVfs() = default; RequireVfs(CliVfs cliVfs); RequireVfs(BundleVfs bundleVfs); - bool isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const; + bool isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const override; - NavigationStatus reset(lua_State* L, std::string_view requirerChunkname); - NavigationStatus jumpToAlias(lua_State* L, std::string_view path); + NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) override; + NavigationStatus jumpToAlias(lua_State* L, std::string_view path) override; - NavigationStatus toParent(lua_State* L); - NavigationStatus toChild(lua_State* L, std::string_view name); + NavigationStatus toParent(lua_State* L) override; + NavigationStatus toChild(lua_State* L, std::string_view name) override; - bool isModulePresent(lua_State* L) const; - std::string getContents(lua_State* L, const std::string& loadname) const; + bool isModulePresent(lua_State* L) const override; + std::string getContents(lua_State* L, const std::string& loadname) const override; - std::string getChunkname(lua_State* L) const; - std::string getLoadname(lua_State* L) const; - std::string getCacheKey(lua_State* L) const; + std::string getChunkname(lua_State* L) const override; + std::string getLoadname(lua_State* L) const override; + std::string getCacheKey(lua_State* L) const override; - ConfigStatus getConfigStatus(lua_State* L) const; - std::string getConfig(lua_State* L) const; + ConfigStatus getConfigStatus(lua_State* L) const override; + std::string getConfig(lua_State* L) const override; - bool isPrecompiled() const + bool isPrecompiled() const override { return vfsType == VFSType::Bundle; }; diff --git a/lute/require/include/lute/userlandvfs.h b/lute/require/include/lute/userlandvfs.h new file mode 100644 index 000000000..0b1192323 --- /dev/null +++ b/lute/require/include/lute/userlandvfs.h @@ -0,0 +1,95 @@ +#pragma once + +#include "lute/filevfs.h" +#include "lute/modulepath.h" + +#include +#include +#include +#include +#include + +namespace Package +{ + +struct Identifier +{ + std::string name; + std::string version; + + bool operator<(const Identifier& other) const + { + return std::tie(name, version) < std::tie(other.name, other.version); + } +}; + +struct Info +{ + std::string rootDirectory; + std::string entryFile; + std::vector dependencies; +}; + +class Subtree +{ +public: + static std::optional create(Info info); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& name); + + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; + + bool isModulePresent() const; + + std::string getCurrentPath() const; + +private: + Subtree(ModulePath currentModulePath, std::string generatedRootLuaurc); + + ModulePath currentModulePath; + std::string generatedRootLuaurc; + bool atGeneratedRoot = false; +}; + +class UserlandVfs +{ +public: + static UserlandVfs create(std::vector directDependencies, std::vector> libraries); + + NavigationStatus resetToPath(const std::string& path); + NavigationStatus jumpToDependencySubtree(const std::string& identifierStringified); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& name); + + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; + + bool isModulePresent() const; + std::optional getContents(const std::string& path) const; + + std::string getCurrentPath() const; + +private: + UserlandVfs(std::map libraries, std::string generatedRootLuaurc); + NavigationStatus jumpToDependencySubtreeImpl(Identifier identifier); + + enum class VFSType + { + Disk, + Subtree, + }; + + VFSType vfsType = VFSType::Disk; + + FileVfs fileVfs; + std::optional currentSubtree = std::nullopt; + std::map libraries; + + bool atDiskFakeRoot = false; + std::string generatedRootLuaurc; +}; + +} // namespace Package diff --git a/lute/require/src/packagerequirevfs.cpp b/lute/require/src/packagerequirevfs.cpp new file mode 100644 index 000000000..245eb90a6 --- /dev/null +++ b/lute/require/src/packagerequirevfs.cpp @@ -0,0 +1,261 @@ +#include "lute/packagerequirevfs.h" + +#include "Luau/FileUtils.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include +#include + +namespace Package +{ + +RequireVfs::RequireVfs(UserlandVfs userlandVfs) + : userlandVfs(std::move(userlandVfs)) +{ +} + +bool RequireVfs::isRequireAllowed(lua_State* L, std::string_view requirerChunkname) const +{ + bool isFile = (!requirerChunkname.empty() && requirerChunkname[0] == '@'); + bool isStdLibFile = (requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@std/"); + return isFile || isStdLibFile; +} + +NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkname) +{ + atFakeRoot = false; + + if ((requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@std/")) + { + vfsType = VFSType::Std; + return stdLibVfs.resetToPath(std::string(requirerChunkname.substr(1))); + } + + vfsType = VFSType::Userland; + if (requirerChunkname.empty() || requirerChunkname[0] != '@') + return NavigationStatus::NotFound; + + if (!isAbsolutePath(requirerChunkname.substr(1))) + { + // For now, we only support absolute paths. + return NavigationStatus::NotFound; + } + + return userlandVfs.resetToPath(std::string(requirerChunkname.substr(1))); +} + +NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) +{ + if (path == "$std") + { + atFakeRoot = false; + vfsType = VFSType::Std; + return stdLibVfs.resetToPath("@std"); + } + else if (path == "$lute") + { + vfsType = VFSType::Lute; + lutePath = "@lute"; + return NavigationStatus::Success; + } + else if (!path.empty() && path[0] == '$') + { + // "$name:version" is interpreted as an identifier. + vfsType = VFSType::Userland; + return userlandVfs.jumpToDependencySubtree(std::string(path)); + } + + NavigationStatus status = NavigationStatus::NotFound; + switch (vfsType) + { + case VFSType::Userland: + status = userlandVfs.resetToPath(std::string(path)); + break; + case VFSType::Std: + status = stdLibVfs.resetToPath(std::string(path)); + break; + case VFSType::Lute: + break; + } + return status; +} + +NavigationStatus RequireVfs::toParent(lua_State* L) +{ + NavigationStatus status = NavigationStatus::NotFound; + switch (vfsType) + { + case VFSType::Userland: + status = userlandVfs.toParent(); + break; + case VFSType::Std: + status = stdLibVfs.toParent(); + break; + case VFSType::Lute: + luaL_error(L, "cannot get the parent of @lute"); + } + + if (status == NavigationStatus::NotFound) + { + if (atFakeRoot) + return NavigationStatus::NotFound; + + atFakeRoot = true; + return NavigationStatus::Success; + } + + return status; +} + +NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) +{ + atFakeRoot = false; + + switch (vfsType) + { + case VFSType::Userland: + return userlandVfs.toChild(std::string(name)); + case VFSType::Std: + return stdLibVfs.toChild(std::string(name)); + case VFSType::Lute: + luaL_error(L, "'%s' is not a lute library", std::string(name).c_str()); + } + + return NavigationStatus::NotFound; +} + +bool RequireVfs::isModulePresent(lua_State* L) const +{ + switch (vfsType) + { + case VFSType::Userland: + return userlandVfs.isModulePresent(); + case VFSType::Std: + return stdLibVfs.isModulePresent(); + case VFSType::Lute: + luaL_error(L, "@lute is not requirable"); + } + + return false; +} + +std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) const +{ + std::optional contents; + switch (vfsType) + { + case VFSType::Userland: + contents = userlandVfs.getContents(loadname); + break; + case VFSType::Std: + contents = stdLibVfs.getContents(loadname); + break; + case VFSType::Lute: + break; + } + return contents ? *contents : ""; +} + +std::string RequireVfs::getChunkname(lua_State* L) const +{ + std::string chunkname; + switch (vfsType) + { + case VFSType::Userland: + chunkname = "@" + userlandVfs.getCurrentPath(); + break; + case VFSType::Std: + chunkname = "@" + stdLibVfs.getIdentifier(); + break; + case VFSType::Lute: + break; + } + return chunkname; +} + +std::string RequireVfs::getLoadname(lua_State* L) const +{ + std::string loadname; + switch (vfsType) + { + case VFSType::Userland: + loadname = userlandVfs.getCurrentPath(); + break; + case VFSType::Std: + loadname = stdLibVfs.getIdentifier(); + break; + case VFSType::Lute: + break; + } + return loadname; +} + +std::string RequireVfs::getCacheKey(lua_State* L) const +{ + std::string cacheKey; + switch (vfsType) + { + case VFSType::Userland: + cacheKey = userlandVfs.getCurrentPath(); + break; + case VFSType::Std: + cacheKey = stdLibVfs.getIdentifier(); + break; + case VFSType::Lute: + break; + } + return cacheKey; +} + +ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const +{ + if (atFakeRoot) + return ConfigStatus::PresentJson; + + ConfigStatus status = ConfigStatus::Ambiguous; + switch (vfsType) + { + case VFSType::Userland: + status = userlandVfs.getConfigStatus(); + break; + case VFSType::Std: + status = stdLibVfs.getConfigStatus(); + break; + case VFSType::Lute: + break; + } + return status; +} + +std::string RequireVfs::getConfig(lua_State* L) const +{ + if (atFakeRoot) + { + std::string globalConfig = "{\n" + " \"aliases\": {\n" + " \"std\": \"$std\",\n" + " \"lute\": \"$lute\",\n" + " }\n" + "}\n"; + return globalConfig; + } + + std::optional configContents; + switch (vfsType) + { + case VFSType::Userland: + configContents = userlandVfs.getConfig(); + break; + case VFSType::Std: + configContents = stdLibVfs.getConfig(); + break; + case VFSType::Lute: + break; + } + return configContents ? *configContents : ""; +} + +} // namespace Package diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index 887ef9ee2..9706edeb0 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -1,13 +1,11 @@ #include "lute/require.h" -#include "lute/clivfs.h" #include "lute/modulepath.h" #include "lute/options.h" #include "Luau/CodeGen.h" #include "Luau/Compiler.h" #include "Luau/Require.h" -#include "Luau/StringUtils.h" #include "lua.h" #include "lualib.h" @@ -74,67 +72,67 @@ static luarequire_ConfigStatus convert(ConfigStatus status) static bool is_require_allowed(lua_State* L, void* ctx, const char* requirer_chunkname) { RequireCtx* reqCtx = static_cast(ctx); - return reqCtx->vfs.isRequireAllowed(L, requirer_chunkname); + return reqCtx->vfs->isRequireAllowed(L, requirer_chunkname); } static luarequire_NavigateResult reset(lua_State* L, void* ctx, const char* requirer_chunkname) { RequireCtx* reqCtx = static_cast(ctx); - return convert(reqCtx->vfs.reset(L, requirer_chunkname)); + return convert(reqCtx->vfs->reset(L, requirer_chunkname)); } static luarequire_NavigateResult jump_to_alias(lua_State* L, void* ctx, const char* path) { RequireCtx* reqCtx = static_cast(ctx); - return convert(reqCtx->vfs.jumpToAlias(L, path)); + return convert(reqCtx->vfs->jumpToAlias(L, path)); } static luarequire_NavigateResult to_parent(lua_State* L, void* ctx) { RequireCtx* reqCtx = static_cast(ctx); - return convert(reqCtx->vfs.toParent(L)); + return convert(reqCtx->vfs->toParent(L)); } static luarequire_NavigateResult to_child(lua_State* L, void* ctx, const char* name) { RequireCtx* reqCtx = static_cast(ctx); - return convert(reqCtx->vfs.toChild(L, name)); + return convert(reqCtx->vfs->toChild(L, name)); } static bool is_module_present(lua_State* L, void* ctx) { RequireCtx* reqCtx = static_cast(ctx); - return reqCtx->vfs.isModulePresent(L); + return reqCtx->vfs->isModulePresent(L); } static luarequire_WriteResult get_chunkname(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) { RequireCtx* reqCtx = static_cast(ctx); - return write(reqCtx->vfs.getChunkname(L), buffer, buffer_size, size_out); + return write(reqCtx->vfs->getChunkname(L), buffer, buffer_size, size_out); } static luarequire_WriteResult get_loadname(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) { RequireCtx* reqCtx = static_cast(ctx); - return write(reqCtx->vfs.getLoadname(L), buffer, buffer_size, size_out); + return write(reqCtx->vfs->getLoadname(L), buffer, buffer_size, size_out); } static luarequire_WriteResult get_cache_key(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) { RequireCtx* reqCtx = static_cast(ctx); - return write(reqCtx->vfs.getCacheKey(L), buffer, buffer_size, size_out); + return write(reqCtx->vfs->getCacheKey(L), buffer, buffer_size, size_out); } static luarequire_ConfigStatus get_config_status(lua_State* L, void* ctx) { RequireCtx* reqCtx = static_cast(ctx); - return convert(reqCtx->vfs.getConfigStatus(L)); + return convert(reqCtx->vfs->getConfigStatus(L)); } static luarequire_WriteResult get_config(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) { RequireCtx* reqCtx = static_cast(ctx); - return write(reqCtx->vfs.getConfig(L), buffer, buffer_size, size_out); + return write(reqCtx->vfs->getConfig(L), buffer, buffer_size, size_out); } static int load(lua_State* L, void* ctx, const char* path, const char* chunkname, const char* loadname) @@ -149,12 +147,12 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname luaL_sandboxthread(ML); RequireCtx* reqCtx = static_cast(ctx); - std::optional contents = reqCtx->vfs.getContents(L, loadname); + std::optional contents = reqCtx->vfs->getContents(L, loadname); if (!contents) luaL_error(L, "could not read file '%s'", loadname); // now we can compile & run module on the new thread - std::string bytecode = reqCtx->vfs.isPrecompiled() ? *contents : Luau::compile(*contents, copts()); + std::string bytecode = reqCtx->vfs->isPrecompiled() ? *contents : Luau::compile(*contents, copts()); bool errored = true; if (luau_load(ML, chunkname, bytecode.data(), bytecode.size(), 0) == 0) { @@ -218,17 +216,7 @@ void requireConfigInit(luarequire_Configuration* config) config->load = load; } -RequireCtx::RequireCtx() - : vfs() -{ -} - -RequireCtx::RequireCtx(CliVfs cliVfs) - : vfs(cliVfs) -{ -} - -RequireCtx::RequireCtx(BundleVfs bundleVfs) - : vfs(bundleVfs) +RequireCtx::RequireCtx(std::unique_ptr vfs) + : vfs(std::move(vfs)) { } diff --git a/lute/require/src/userlandvfs.cpp b/lute/require/src/userlandvfs.cpp new file mode 100644 index 000000000..a1c4756db --- /dev/null +++ b/lute/require/src/userlandvfs.cpp @@ -0,0 +1,318 @@ +#include "lute/userlandvfs.h" + +#include "lute/modulepath.h" + +#include "Luau/Common.h" +#include "Luau/Config.h" +#include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" + +#include +#include + +namespace Package +{ + +static std::string generateRootLuaurc(const std::vector& dependencies) +{ + std::string rootLuaurc = "{\n \"aliases\": {\n"; + for (const Identifier& dep : dependencies) + { + rootLuaurc += " \"" + dep.name + "\": \"$" + dep.name + ":" + dep.version + "\",\n"; + } + rootLuaurc += " }\n}\n"; + return rootLuaurc; +} + +std::optional Subtree::create(Info info) +{ + std::string_view entryFile = info.entryFile; + if (entryFile.find(info.rootDirectory) != 0) + return std::nullopt; + + if (entryFile.size() <= info.rootDirectory.size()) + return std::nullopt; + + if (entryFile[info.rootDirectory.size()] != '/' && entryFile[info.rootDirectory.size()] != '\\') + return std::nullopt; + + std::string entryFileWithoutRoot = info.entryFile.substr(info.rootDirectory.size() + 1); + + std::optional currentModulePath = ModulePath::create(info.rootDirectory, entryFileWithoutRoot, isFile, isDirectory); + if (!currentModulePath) + return std::nullopt; + + return Subtree{std::move(*currentModulePath), generateRootLuaurc(info.dependencies)}; +} + +Subtree::Subtree(ModulePath currentModulePath, std::string generatedRootLuaurc) + : currentModulePath(std::move(currentModulePath)) + , generatedRootLuaurc(std::move(generatedRootLuaurc)) +{ +} + +NavigationStatus Subtree::toParent() +{ + NavigationStatus status = currentModulePath.toParent(); + if (status == NavigationStatus::NotFound) + { + if (atGeneratedRoot) + return NavigationStatus::NotFound; + + atGeneratedRoot = true; + return NavigationStatus::Success; + } + + return status; +} + +NavigationStatus Subtree::toChild(const std::string& name) +{ + atGeneratedRoot = false; + return currentModulePath.toChild(name); +} + +ConfigStatus Subtree::getConfigStatus() const +{ + if (atGeneratedRoot) + return ConfigStatus::PresentJson; + + bool luaurcExists = isFile(currentModulePath.getPotentialConfigPath(Luau::kConfigName)); + bool luauConfigExists = isFile(currentModulePath.getPotentialConfigPath(Luau::kLuauConfigName)); + + if (luaurcExists && luauConfigExists) + return ConfigStatus::Ambiguous; + else if (luauConfigExists) + return ConfigStatus::PresentLuau; + else if (luaurcExists) + return ConfigStatus::PresentJson; + + return ConfigStatus::Absent; +} + +std::optional Subtree::getConfig() const +{ + if (atGeneratedRoot) + return generatedRootLuaurc; + + ConfigStatus status = getConfigStatus(); + LUAU_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); + + if (status == ConfigStatus::PresentJson) + return readFile(currentModulePath.getPotentialConfigPath(Luau::kConfigName)); + else if (status == ConfigStatus::PresentLuau) + return readFile(currentModulePath.getPotentialConfigPath(Luau::kLuauConfigName)); + + LUAU_UNREACHABLE(); +} + +bool Subtree::isModulePresent() const +{ + if (atGeneratedRoot) + return false; + + ResolvedRealPath result = currentModulePath.getRealPath(); + if (result.status != NavigationStatus::Success) + return false; + + return isFile(result.realPath); +} + +std::string Subtree::getCurrentPath() const +{ + ResolvedRealPath result = currentModulePath.getRealPath(); + if (result.status != NavigationStatus::Success) + return ""; + + return result.realPath; +} + +UserlandVfs UserlandVfs::create(std::vector directDependencies, std::vector> libraries) +{ + std::map librariesMap; + for (auto& [identifier, info] : libraries) + { + librariesMap[std::move(identifier)] = std::move(info); + } + + return UserlandVfs{std::move(librariesMap), generateRootLuaurc(directDependencies)}; +} + +UserlandVfs::UserlandVfs(std::map libraries, std::string generatedRootLuaurc) + : libraries(std::move(libraries)) + , generatedRootLuaurc(std::move(generatedRootLuaurc)) +{ +} + +NavigationStatus UserlandVfs::resetToPath(const std::string& path) +{ + for (const auto& [identifier, info] : libraries) + { + if (path.find(info.rootDirectory) == 0) + { + return jumpToDependencySubtreeImpl(identifier); + } + } + + currentSubtree = std::nullopt; + vfsType = VFSType::Disk; + atDiskFakeRoot = false; + + return fileVfs.resetToPath(path); +} + +NavigationStatus UserlandVfs::jumpToDependencySubtree(const std::string& identifierStringified) +{ + if (identifierStringified.empty() || identifierStringified[0] != '$') + return NavigationStatus::NotFound; + + Identifier identifier; + size_t colonPos = identifierStringified.find(':'); + if (colonPos == std::string::npos) + return NavigationStatus::NotFound; + + identifier.name = identifierStringified.substr(1, colonPos - 1); + identifier.version = identifierStringified.substr(colonPos + 1); + + return jumpToDependencySubtreeImpl(identifier); +} + +NavigationStatus UserlandVfs::jumpToDependencySubtreeImpl(Identifier identifier) +{ + if (libraries.find(identifier) == libraries.end()) + return NavigationStatus::NotFound; + + std::optional st = Subtree::create(libraries.at(identifier)); + if (!st) + return NavigationStatus::NotFound; + + currentSubtree = std::move(*st); + vfsType = VFSType::Subtree; + atDiskFakeRoot = false; + + return NavigationStatus::Success; +} + +NavigationStatus UserlandVfs::toParent() +{ + NavigationStatus status = NavigationStatus::NotFound; + switch (vfsType) + { + case VFSType::Disk: + status = fileVfs.toParent(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + status = currentSubtree->toParent(); + break; + } + + if (status == NavigationStatus::NotFound) + { + if (atDiskFakeRoot) + return NavigationStatus::NotFound; + + atDiskFakeRoot = true; + return NavigationStatus::Success; + } + + return status; +} + +NavigationStatus UserlandVfs::toChild(const std::string& name) +{ + atDiskFakeRoot = false; + + NavigationStatus status = NavigationStatus::NotFound; + switch (vfsType) + { + case VFSType::Disk: + status = fileVfs.toChild(name); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + status = currentSubtree->toChild(name); + break; + } + return status; +} + +ConfigStatus UserlandVfs::getConfigStatus() const +{ + if (atDiskFakeRoot) + return ConfigStatus::PresentJson; + + ConfigStatus status = ConfigStatus::Ambiguous; + switch (vfsType) + { + case VFSType::Disk: + status = fileVfs.getConfigStatus(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + status = currentSubtree->getConfigStatus(); + break; + } + return status; +} + +std::optional UserlandVfs::getConfig() const +{ + if (atDiskFakeRoot) + return generatedRootLuaurc; + + std::optional config; + switch (vfsType) + { + case VFSType::Disk: + config = fileVfs.getConfig(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + config = currentSubtree->getConfig(); + break; + } + return config; +} + +bool UserlandVfs::isModulePresent() const +{ + if (atDiskFakeRoot) + return false; + + bool isPresent = false; + switch (vfsType) + { + case VFSType::Disk: + isPresent = fileVfs.isModulePresent(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + isPresent = currentSubtree->isModulePresent(); + break; + } + return isPresent; +} + +std::optional UserlandVfs::getContents(const std::string& path) const +{ + return readFile(path); +} + +std::string UserlandVfs::getCurrentPath() const +{ + std::string path; + switch (vfsType) + { + case VFSType::Disk: + path = fileVfs.getAbsoluteFilePath(); + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + path = currentSubtree->getCurrentPath(); + break; + } + return path; +} + +} // namespace Package diff --git a/lute/vm/src/spawn.cpp b/lute/vm/src/spawn.cpp index 50eec4366..29bf4f659 100644 --- a/lute/vm/src/spawn.cpp +++ b/lute/vm/src/spawn.cpp @@ -2,6 +2,7 @@ #include "lute/ref.h" #include "lute/require.h" +#include "lute/requirevfs.h" #include "lute/runtime.h" #include "Luau/Require.h" @@ -185,7 +186,7 @@ static void* createChildVmRequireContext(lua_State* L) if (!ctx) luaL_error(L, "unable to allocate RequireCtx"); - ctx = new (ctx) RequireCtx{}; + ctx = new (ctx) RequireCtx{std::make_unique()}; // Store RequireCtx in the registry to keep it alive for the lifetime of // this lua_State. Memory address is used as a key to avoid collisions. @@ -214,7 +215,7 @@ int lua_spawn(lua_State* L) lua_getinfo(L, 1, "s", &ar); // Require the target module - RequireCtx ctx{}; + RequireCtx ctx{std::make_unique()}; luarequire_pushproxyrequire(child->GL, requireConfigInit, &ctx); lua_pushstring(child->GL, file); lua_pushstring(child->GL, ar.source); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 48d98d001..0de66c096 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,20 +3,22 @@ add_executable(Lute.Test) target_sources(Lute.Test PRIVATE src/doctest.h src/main.cpp - src/cliruntimefixture.h - src/cliruntimefixture.cpp + # Test fixtures and utilities + src/cliruntimefixture.h src/lutefixture.h - src/lutefixture.cpp - src/luteprojectroot.h + src/testreporter.h + + src/cliruntimefixture.cpp + src/lutefixture.cpp src/luteprojectroot.cpp + src/testreporter.cpp + # Test files src/compile.test.cpp src/configresolver.test.cpp - src/testreporter.h - src/testreporter.cpp - + src/libraryrequire.test.cpp src/modulepath.test.cpp src/moduleresolver.test.cpp src/require.test.cpp diff --git a/tests/src/libraryrequire.test.cpp b/tests/src/libraryrequire.test.cpp new file mode 100644 index 000000000..30f7ad22a --- /dev/null +++ b/tests/src/libraryrequire.test.cpp @@ -0,0 +1,88 @@ +#include "lute/climain.h" +#include "lute/options.h" +#include "lute/packagerequirevfs.h" +#include "lute/require.h" +#include "lute/runtime.h" +#include "lute/userlandvfs.h" + +#include "Luau/Compiler.h" +#include "Luau/FileUtils.h" +#include "Luau/Require.h" + +#include "lua.h" +#include "lualib.h" + +#include + +#include "doctest.h" +#include "lutefixture.h" +#include "luteprojectroot.h" + +TEST_CASE_FIXTURE(LuteFixture, "package_aware_require") +{ + Runtime runtime; + + lua_State* L = setupState( + runtime, + [](lua_State* L) + { + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string entryRoot = luteProjectRoot + '/' + "tests/src/packages/packageentry"; + + std::vector directDependencies = { + {"dep", "2.0.0"}, + }; + + std::string librariesRoot = luteProjectRoot + '/' + "tests/src/packages"; + std::vector> libraries; + libraries.push_back( + {{"dep", "1.0.0"}, + { + librariesRoot + '/' + "internaldep", + librariesRoot + '/' + "internaldep/init.luau", + {}, + }} + ); + libraries.push_back( + {{"dep", "2.0.0"}, + { + librariesRoot + '/' + "dep", + librariesRoot + '/' + "dep/module.luau", + {{"dep", "1.0.0"}}, + }} + ); + + Package::UserlandVfs libraryVfs = Package::UserlandVfs::create(std::move(directDependencies), std::move(libraries)); + + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + static_cast(ptr)->~RequireCtx(); + } + ); + + if (!ctx) + luaL_errorL(L, "unable to allocate RequireCtx"); + + ctx = new (ctx) RequireCtx{std::make_unique(std::move(libraryVfs))}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + luaopen_require(L, requireConfigInit, ctx); + } + ); + + std::string path = getLuteProjectRootAbsolute() + "/tests/src/packages/packageentry/entry.luau"; + std::optional contents = readFile(path); + REQUIRE(contents); + + std::string bytecode = Luau::compile(*contents, copts()); + bool success = runBytecode(runtime, bytecode, "@" + path, L, 0, nullptr, getReporter()); + CHECK(success); +} diff --git a/tests/src/packages/dep/module.luau b/tests/src/packages/dep/module.luau new file mode 100644 index 000000000..a18e7b423 --- /dev/null +++ b/tests/src/packages/dep/module.luau @@ -0,0 +1,3 @@ +local result = require("@dep") +result[#result + 1] = "external" +return result diff --git a/tests/src/packages/internaldep/init.luau b/tests/src/packages/internaldep/init.luau new file mode 100644 index 000000000..c351ed5a0 --- /dev/null +++ b/tests/src/packages/internaldep/init.luau @@ -0,0 +1 @@ +return { "internal" } diff --git a/tests/src/packages/packageentry/entry.luau b/tests/src/packages/packageentry/entry.luau new file mode 100644 index 000000000..d95ed26a3 --- /dev/null +++ b/tests/src/packages/packageentry/entry.luau @@ -0,0 +1,7 @@ +--!strict +local result = require("@dep") :: { string } +assert(type(result) == "table") +assert(#result == 2) + +assert(result[1] == "internal") +assert(result[2] == "external") From caf674a9d6f3bc4398b86bcdfd11f62f3b661bf9 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 12 Nov 2025 17:00:34 -0800 Subject: [PATCH 160/642] bugfix: lute check supports ~ in luaurc paths (#573) Users have been having issues with the `.luaurc` generated by the `lute setup` command. This sets certain aliases up in a path with `~` in them but the require rfc didn't support navigating to aliases with `~` in them. This PR fixes this by allowing the FileVFS to navigate to aliases that represent paths with `~` in them. Annoyingly, I had to use the filesystem header to handle the test, because it sets up a directory under `~` called `lute_test_special` and writes a file there - if yall have suggestions for how to fix this, please let me know. --- lute/require/CMakeLists.txt | 2 +- lute/require/src/filevfs.cpp | 23 ++++++++++- tests/src/require.test.cpp | 39 +++++++++++++++++++ .../require/config_tests/tilde_config/.luaurc | 5 +++ .../config_tests/tilde_config/main.luau | 1 + 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 tests/src/require/config_tests/tilde_config/.luaurc create mode 100644 tests/src/require/config_tests/tilde_config/main.luau diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index 2999e3a5d..3c090c850 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -26,5 +26,5 @@ target_sources(Lute.Require PRIVATE target_compile_features(Lute.Require PUBLIC cxx_std_17) target_include_directories(Lute.Require PUBLIC include) -target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Std Lute.CLI.Commands) +target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Std Lute.CLI.Commands Lute.Runtime) target_compile_options(Lute.Require PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/require/src/filevfs.cpp b/lute/require/src/filevfs.cpp index 72fc81bb6..6cab7f446 100644 --- a/lute/require/src/filevfs.cpp +++ b/lute/require/src/filevfs.cpp @@ -1,12 +1,15 @@ #include "lute/filevfs.h" #include "lute/modulepath.h" +#include "lute/uvutils.h" #include "Luau/Common.h" #include "Luau/Config.h" #include "Luau/FileUtils.h" #include "Luau/LuauConfig.h" +#include "uv.h" + #include #include #include @@ -26,7 +29,25 @@ NavigationStatus FileVfs::resetToStdIn() NavigationStatus FileVfs::resetToPath(const std::string& path) { - std::string normalizedPath = normalizePath(path); + std::string pathToProcess = path; + + // Handle tilde expansion for home directory + if (!path.empty() && path[0] == '~') + { + auto result = uvutils::getStringFromUv(uv_os_homedir); + if (result.get_if() != nullptr) + return NavigationStatus::NotFound; + + std::string* homeDir = result.get_if(); + + // Replace ~ with home directory + if (path.size() == 1) + pathToProcess = *homeDir; + else if (path[1] == '/' || path[1] == '\\') + pathToProcess = *homeDir + path.substr(1); + } + + std::string normalizedPath = normalizePath(pathToProcess); if (isAbsolutePath(normalizedPath)) { diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index 49b544575..127614110 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -1,9 +1,15 @@ #include "lute/climain.h" +#include "lute/uvutils.h" #include "Luau/FileUtils.h" #include "lua.h" +#include "uv.h" + +#include +#include + #include "cliruntimefixture.h" #include "doctest.h" #include "lutefixture.h" @@ -223,3 +229,36 @@ TEST_CASE_FIXTURE(LuteFixture, "require_by_string_semantics_in_cli") CHECK_NE(cliMain(argv.size(), argv.data(), getReporter()), 0); } } + +TEST_CASE_FIXTURE(LuteFixture, "require_check_tilde_path") +{ + char executablePlaceholder[] = "lute"; + char subCommand[] = "check"; + // Get home directory + auto result = uvutils::getStringFromUv(uv_os_homedir); + REQUIRE(result.get_if() != nullptr); + std::string homeDir = *result.get_if(); + + // Create test directory and test file + std::filesystem::path testDir = std::filesystem::path(homeDir) / "lute_test_special"; + std::filesystem::path testFile = testDir / "foo.luau"; + std::filesystem::create_directories(testDir); + + // Write test module file + std::ofstream file(testFile); + REQUIRE(file.is_open()); + file << "return { foo = \"bar\" }\n"; + file.close(); + + // Run the test + for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) + { + std::string mainPath = joinPaths(luteProjectRoot, "tests/src/require/config_tests/tilde_config/main.luau"); + std::vector argv = {executablePlaceholder, subCommand, mainPath.data()}; + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); + } + + // Clean up + std::filesystem::remove(testFile); + std::filesystem::remove(testDir); +} diff --git a/tests/src/require/config_tests/tilde_config/.luaurc b/tests/src/require/config_tests/tilde_config/.luaurc new file mode 100644 index 000000000..690dc6af4 --- /dev/null +++ b/tests/src/require/config_tests/tilde_config/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "tilde": "~/lute_test_special" + } +} diff --git a/tests/src/require/config_tests/tilde_config/main.luau b/tests/src/require/config_tests/tilde_config/main.luau new file mode 100644 index 000000000..b0f6c9f22 --- /dev/null +++ b/tests/src/require/config_tests/tilde_config/main.luau @@ -0,0 +1 @@ +local x = require("@tilde/foo") From 827638ead18d1c25c713e4a7de2c56e146b5f9b8 Mon Sep 17 00:00:00 2001 From: Andrew Zhilin Date: Thu, 13 Nov 2025 16:49:08 -0300 Subject: [PATCH 161/642] bugfix: initialize status variable (#576) Fixes Release build failure due to uninitialized variable warning with -O3 optimization level. The status variable wasn't assigned when vfsType == VFSType::Lute before use. https://github.com/luau-lang/lute/issues/575 --- lute/require/src/requirevfs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index daa295aae..1a17039d5 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -102,7 +102,7 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) NavigationStatus RequireVfs::toParent(lua_State* L) { - NavigationStatus status; + NavigationStatus status = NavigationStatus::NotFound; switch (vfsType) { From d81aeba71cfdf62ff549fe7aa2af77656bbcc4bc Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:51:40 -0800 Subject: [PATCH 162/642] Fix missed `Package::RequireVfs` renames from #339 (#578) Missed a few places in #339 when removing "library"/"libraries" from this part of our code. --- lute/require/include/lute/userlandvfs.h | 6 +++--- lute/require/src/userlandvfs.cpp | 20 +++++++++---------- tests/CMakeLists.txt | 2 +- ...quire.test.cpp => packagerequire.test.cpp} | 20 +++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) rename tests/src/{libraryrequire.test.cpp => packagerequire.test.cpp} (79%) diff --git a/lute/require/include/lute/userlandvfs.h b/lute/require/include/lute/userlandvfs.h index 0b1192323..d9f173dcf 100644 --- a/lute/require/include/lute/userlandvfs.h +++ b/lute/require/include/lute/userlandvfs.h @@ -56,7 +56,7 @@ class Subtree class UserlandVfs { public: - static UserlandVfs create(std::vector directDependencies, std::vector> libraries); + static UserlandVfs create(std::vector directDependencies, std::vector> allDependencies); NavigationStatus resetToPath(const std::string& path); NavigationStatus jumpToDependencySubtree(const std::string& identifierStringified); @@ -73,7 +73,7 @@ class UserlandVfs std::string getCurrentPath() const; private: - UserlandVfs(std::map libraries, std::string generatedRootLuaurc); + UserlandVfs(std::map allDependencies, std::string generatedRootLuaurc); NavigationStatus jumpToDependencySubtreeImpl(Identifier identifier); enum class VFSType @@ -86,7 +86,7 @@ class UserlandVfs FileVfs fileVfs; std::optional currentSubtree = std::nullopt; - std::map libraries; + std::map allDependencies; bool atDiskFakeRoot = false; std::string generatedRootLuaurc; diff --git a/lute/require/src/userlandvfs.cpp b/lute/require/src/userlandvfs.cpp index a1c4756db..65e921bc8 100644 --- a/lute/require/src/userlandvfs.cpp +++ b/lute/require/src/userlandvfs.cpp @@ -127,26 +127,26 @@ std::string Subtree::getCurrentPath() const return result.realPath; } -UserlandVfs UserlandVfs::create(std::vector directDependencies, std::vector> libraries) +UserlandVfs UserlandVfs::create(std::vector directDependencies, std::vector> allDependencies) { - std::map librariesMap; - for (auto& [identifier, info] : libraries) + std::map allDependenciesMap; + for (auto& [identifier, info] : allDependencies) { - librariesMap[std::move(identifier)] = std::move(info); + allDependenciesMap[std::move(identifier)] = std::move(info); } - return UserlandVfs{std::move(librariesMap), generateRootLuaurc(directDependencies)}; + return UserlandVfs{std::move(allDependenciesMap), generateRootLuaurc(directDependencies)}; } -UserlandVfs::UserlandVfs(std::map libraries, std::string generatedRootLuaurc) - : libraries(std::move(libraries)) +UserlandVfs::UserlandVfs(std::map allDependencies, std::string generatedRootLuaurc) + : allDependencies(std::move(allDependencies)) , generatedRootLuaurc(std::move(generatedRootLuaurc)) { } NavigationStatus UserlandVfs::resetToPath(const std::string& path) { - for (const auto& [identifier, info] : libraries) + for (const auto& [identifier, info] : allDependencies) { if (path.find(info.rootDirectory) == 0) { @@ -179,10 +179,10 @@ NavigationStatus UserlandVfs::jumpToDependencySubtree(const std::string& identif NavigationStatus UserlandVfs::jumpToDependencySubtreeImpl(Identifier identifier) { - if (libraries.find(identifier) == libraries.end()) + if (allDependencies.find(identifier) == allDependencies.end()) return NavigationStatus::NotFound; - std::optional st = Subtree::create(libraries.at(identifier)); + std::optional st = Subtree::create(allDependencies.at(identifier)); if (!st) return NavigationStatus::NotFound; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0de66c096..4b7cf783e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,9 +18,9 @@ target_sources(Lute.Test PRIVATE # Test files src/compile.test.cpp src/configresolver.test.cpp - src/libraryrequire.test.cpp src/modulepath.test.cpp src/moduleresolver.test.cpp + src/packagerequire.test.cpp src/require.test.cpp src/staticrequires.test.cpp src/stdsystem.test.cpp) diff --git a/tests/src/libraryrequire.test.cpp b/tests/src/packagerequire.test.cpp similarity index 79% rename from tests/src/libraryrequire.test.cpp rename to tests/src/packagerequire.test.cpp index 30f7ad22a..421630aa8 100644 --- a/tests/src/libraryrequire.test.cpp +++ b/tests/src/packagerequire.test.cpp @@ -33,26 +33,26 @@ TEST_CASE_FIXTURE(LuteFixture, "package_aware_require") {"dep", "2.0.0"}, }; - std::string librariesRoot = luteProjectRoot + '/' + "tests/src/packages"; - std::vector> libraries; - libraries.push_back( + std::string packagesRoot = luteProjectRoot + '/' + "tests/src/packages"; + std::vector> allDependencies; + allDependencies.push_back( {{"dep", "1.0.0"}, { - librariesRoot + '/' + "internaldep", - librariesRoot + '/' + "internaldep/init.luau", + packagesRoot + '/' + "internaldep", + packagesRoot + '/' + "internaldep/init.luau", {}, }} ); - libraries.push_back( + allDependencies.push_back( {{"dep", "2.0.0"}, { - librariesRoot + '/' + "dep", - librariesRoot + '/' + "dep/module.luau", + packagesRoot + '/' + "dep", + packagesRoot + '/' + "dep/module.luau", {{"dep", "1.0.0"}}, }} ); - Package::UserlandVfs libraryVfs = Package::UserlandVfs::create(std::move(directDependencies), std::move(libraries)); + Package::UserlandVfs userlandVfs = Package::UserlandVfs::create(std::move(directDependencies), std::move(allDependencies)); void* ctx = lua_newuserdatadtor( L, @@ -66,7 +66,7 @@ TEST_CASE_FIXTURE(LuteFixture, "package_aware_require") if (!ctx) luaL_errorL(L, "unable to allocate RequireCtx"); - ctx = new (ctx) RequireCtx{std::make_unique(std::move(libraryVfs))}; + ctx = new (ctx) RequireCtx{std::make_unique(std::move(userlandVfs))}; // Store RequireCtx in the registry to keep it alive for the lifetime of // this lua_State. Memory address is used as a key to avoid collisions. From 22a1628919f479ae4e0fc1970701b5d78f77f4c1 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Thu, 13 Nov 2025 18:35:54 -0500 Subject: [PATCH 163/642] Support token selection in selectFromRoot (#577) There is currently no way to query for generic `Token` nodes via the `query` library. Tokens are a little annoying to work with ``visitor``'s ``visitToken`` is called for each of the following types: - `AstExprConstantNil` (extension of token) - `AstExprConstantNumber` (extension of token) - `AstExprConstantBool` (extension of token) - `AstExprConstantString` (extension of token) - `Token` In a full pass of `visitor`, each of the token extension types (`AstExprConstant...`) is additionally visited by their respective `visitExprConstant...` function. Thus, if we were to override `selectVisitor`'s `visitToken` function with our generic `visit` function (calls `fn` on node, selects node if result is not nil), we would end up double counting (selecting same node twice) these `AstExprConstant...` nodes. This PR adds a utility `isBaseToken` to distinguish between `AstExprConstant...` nodes and the rudimentary `Token` node. We then use this utility in `selectFromRoot`'s `selectVisitor` to avoid double counting the `AstExprConstant...` nodes. Also, fixes some small bugs along the way: - `utils.isToken` was performing an improper check; the `Token` type does not have a `kind` field - `utils.isExprConstantBool` was performing an improper check; `AstExprConstantBool.kind` is always "boolean" not "bool" --- lute/std/libs/syntax/query.luau | 5 +++++ lute/std/libs/syntax/utils.luau | 8 ++++++-- tests/std/syntax/query.test.luau | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 000ee3b42..9f0ad18d7 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -5,6 +5,7 @@ local printer = require("./printer") local types = require("./types") local visitor = require("./visitor") +local utils = require("./utils") type node = types.AstNode @@ -83,6 +84,10 @@ function queryLib.selectFromRoot(ast: node, fn: (node) -> T?): query end selectVisitor.visitExpressionEnd = function(n) end selectVisitor.visitToken = function(n) + if utils.isBaseToken(n) then + -- only visit if it is a base token, so we don't double count + return visit(n) + end return true end diff --git a/lute/std/libs/syntax/utils.luau b/lute/std/libs/syntax/utils.luau index 85f1b9966..a5f7b8229 100644 --- a/lute/std/libs/syntax/utils.luau +++ b/lute/std/libs/syntax/utils.luau @@ -15,7 +15,7 @@ function utils.isExprConstantNil(n: types.AstNode): types.AstExprConstantNil? end function utils.isExprConstantBool(n: types.AstNode): types.AstExprConstantBool? - return if n.kind == "expr" and n.tag == "bool" then n else nil + return if n.kind == "expr" and n.tag == "boolean" then n else nil end function utils.isExprConstantNumber(n: types.AstNode): types.AstExprConstantNumber? @@ -227,7 +227,11 @@ function utils.isTypePack(n: types.AstNode): types.AstTypePack? end function utils.isToken(n: types.AstNode): types.Token? - return if n.kind == "token" then n else nil + return if n.istoken then n else nil +end + +function utils.isBaseToken(n: types.AstNode): types.Token? + return if n.istoken and not n.kind then n else nil end return table.freeze(utils) diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index 34b06b9d6..6b16d4416 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -109,6 +109,22 @@ test.suite("AST Query", function(suite) assert.eq(num.tag, "number") assert.eq((num :: syntax.AstExprConstantNumber).value, 2) end) + + suite:case("select_tokens", function(assert) + local ast = syntax.parse("local x = 1 or nil") + -- verify that query doesn't doubly count tokens + local queryResult = query.selectFromRoot(ast, utils.isToken) + assert.eq(#queryResult.nodes, 6) + queryResult:forEach(function(node) + assert.eq(node.istoken, true) + end) + local constNums = query.selectFromRoot(ast, utils.isExprConstantNumber) + assert.eq(#constNums.nodes, 1) + local constNils = query.selectFromRoot(ast, utils.isExprConstantNil) + assert.eq(#constNils.nodes, 1) + local baseTokens = query.selectFromRoot(ast, utils.isBaseToken) + assert.eq(#baseTokens.nodes, 4) + end) end) test.run() From 9f576a20899abadb82db49e74ac2637e3fc62ca8 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 13 Nov 2025 15:40:02 -0800 Subject: [PATCH 164/642] bugfix: lute setup should destroy the typedefs folder (#574) Lute setup had a bug whereby it would write new typedefs into the typedefs folder, but sometimes these could conflict with stale typedefs, causing ambiguous requires. For example, moving "posix.luau" into `posix/init.luau` but keeping `posix.luau` around will lead to an ambiguous require. To do this, lute setup will call a new standard library method - `fs.removedirectory` with the `{ recursive = true}` option. --- lute/cli/commands/setup/init.luau | 3 ++ lute/std/libs/fs.luau | 24 +++++++++++++ tests/std/fs.test.luau | 58 +++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 072e0689e..492feeb35 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -18,6 +18,9 @@ local args = { ... } -- TODO we're assuming the files are completely unmodified, but we really shouldn't do that. print(`Writing definitions at {BASE_PATH_PRETTY}`) +if fs.exists(BASE_PATH) then + fs.removedirectory(BASE_PATH, { recursive = true }) +end fs.createdirectory(BASE_PATH, { makeparents = true }) for key, value in definitions :: { [string]: string } do diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 25ca1f2c6..44918a2d3 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -22,6 +22,10 @@ export type createdirectoryoptions = { makeparents: boolean?, } +export type removedirectoryoptions = { + recursive: boolean?, +} + function fslib.open(path: pathlike, mode: handlemode?): filehandle return fs.open(pathlib.format(path), mode) end @@ -117,4 +121,24 @@ function fslib.createdirectory(path: pathlike, options: createdirectoryoptions?) end end +function fslib.removedirectory(path: pathlike, options: removedirectoryoptions?): () + if options and options.recursive then + -- Recursively delete all contents first + if fslib.exists(path) and fslib.type(path) == "dir" then + local entries = fslib.listdir(path) + for _, entry in entries do + local entryPath = pathlib.join(path, entry.name) + if entry.type == "dir" then + fslib.removedirectory(entryPath, { recursive = true }) + else + fslib.remove(entryPath) + end + end + fs.rmdir(pathlib.format(path)) + end + else + fs.rmdir(pathlib.format(path)) + end +end + return table.freeze(fslib) diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 2a8934108..bfc152943 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -167,6 +167,64 @@ test.suite("FsSuite", function(suite) fs.remove(file) end) + + suite:case("removedirectory_non_recursive", function(assert) + local dir = path.join(tmpdir, "empty_dir") + fs.createdirectory(dir, { makeparents = true }) + assert.eq(fs.exists(dir), true) + + fs.removedirectory(dir) + assert.eq(fs.exists(dir), false) + end) + + suite:case("removedirectory_recursive", function(assert) + local dir = path.join(tmpdir, "recursive_test") + local subdir1 = path.join(dir, "subdir1") + local subdir2 = path.join(dir, "subdir2") + local nestedDir = path.join(subdir1, "nested") + + fs.createdirectory(nestedDir, { makeparents = true }) + fs.createdirectory(subdir2, { makeparents = true }) + + local file1 = path.join(dir, "file1.txt") + local file2 = path.join(subdir1, "file2.txt") + local file3 = path.join(nestedDir, "file3.txt") + + fs.writestringtofile(file1, "content1") + fs.writestringtofile(file2, "content2") + fs.writestringtofile(file3, "content3") + + assert.eq(fs.exists(dir), true) + assert.eq(fs.exists(file1), true) + assert.eq(fs.exists(file2), true) + assert.eq(fs.exists(file3), true) + + fs.removedirectory(dir, { recursive = true }) + + assert.eq(fs.exists(dir), false) + assert.eq(fs.exists(file1), false) + assert.eq(fs.exists(file2), false) + assert.eq(fs.exists(file3), false) + end) + + suite:case("removedirectory_recursive_false_with_contents", function(assert) + local dir = path.join(tmpdir, "non_empty_dir") + fs.createdirectory(dir, { makeparents = true }) + + local file = path.join(dir, "file.txt") + fs.writestringtofile(file, "content") + + local success, err = pcall(function() + fs.removedirectory(dir, { recursive = false }) + end) + + assert.neq(success, true) + assert.neq(err, nil) + + -- Cleanup + fs.remove(file) + fs.rmdir(dir) + end) end) test.run() From 5ff55dc169622b2fcca9fc0f97fbd72dc073ccdd Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 14 Nov 2025 13:19:49 -0800 Subject: [PATCH 165/642] stdlib: Printing AST replacements preserves trivia (#580) Implemented trivia preservation for AST node replacements by grabbing the leftmost and rightmost trivia of the node being replaced. Along the way, I discovered that trivia is often bound to the EOF token, rather than AST nodes, so I had to make a change to make sure we pass that EOF token around inside of query. To increase the chances that this token is present, I want to encourage codemod authors to use the parse function which returns `ParseResult` (which includes EOF), so I renamed `parse(...) -> AstStatBlock` to `parseBlock`, and `parseFile(...) -> ParseResult` to `parse`. The bulk of this PR is the machinery for getting that trivia, as well as the tests making sure we correctly reserialize trivia for every AST node. While writing those tests, I also found a bunch of other bugs. --- examples/query_transformer.luau | 5 +- lute/cli/commands/transform/init.luau | 27 +- lute/luau/src/luau.cpp | 4 +- lute/std/libs/syntax/init.luau | 2 +- lute/std/libs/syntax/parser.luau | 4 +- lute/std/libs/syntax/printer.luau | 17 + lute/std/libs/syntax/query.luau | 17 +- lute/std/libs/syntax/types.luau | 2 +- .../syntax/{utils.luau => utils/init.luau} | 4 + lute/std/libs/syntax/utils/trivia.luau | 52 ++ lute/std/libs/syntax/visitor.luau | 58 +- tests/std/syntax/parser.test.luau | 111 +-- tests/std/syntax/printer.test.luau | 861 ++++++++++++++++-- tests/std/syntax/query.test.luau | 2 +- 14 files changed, 1014 insertions(+), 152 deletions(-) rename lute/std/libs/syntax/{utils.luau => utils/init.luau} (98%) create mode 100644 lute/std/libs/syntax/utils/trivia.luau diff --git a/examples/query_transformer.luau b/examples/query_transformer.luau index 39a01638b..e50c4382a 100644 --- a/examples/query_transformer.luau +++ b/examples/query_transformer.luau @@ -1,5 +1,5 @@ -local luau = require("@std/luau") local query = require("@std/syntax/query") +local syntax = require("@std/syntax") local syntax_utils = require("@std/syntax/utils") local function transform(ctx) @@ -12,9 +12,8 @@ local function transform(ctx) and bin.lhsoperand.token.text == bin.rhsoperand.token.text end) :replace(function(bin) - return `math.isnan({(bin.lhsoperand :: luau.AstExprLocal).token.text})` + return `math.isnan({(bin.lhsoperand :: syntax.AstExprLocal).token.text})` end) - :print() end return transform diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index 7fa60abaa..ec598fc63 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -1,7 +1,9 @@ local fs = require("@lute/fs") local luau = require("@std/luau") local pathLib = require("@std/path") +local printer = require("@std/syntax/printer") local syntax = require("@std/syntax") +local syntaxTypes = require("@std/syntax/types") local arguments = require("@self/lib/arguments") local files = require("@self/lib/files") @@ -99,7 +101,7 @@ local function applyMigration( local source = fs.readfiletostring(pathStr) - local parseresult = syntax.parsefile(source) + local parseresult = syntax.parse(source) local ctx = { path = pathStr, @@ -111,13 +113,24 @@ local function applyMigration( -- TODO: should we wrap in pcall? For now we don't do this to preserve stack trace local result = migration.transform(ctx) - assert(typeof(result) == "string", `Transformed expected to return string, but got {typeof(result)}`) + if typeof(result) == "string" then + if result == types.DELETION_MARKER then + print(`Marking {pathStr} for deletion`) + table.insert(deletedPaths, pathStr) + elseif result ~= source and not dryRun then + fs.writestringtofile(pathStr, result) + end + else + local replacements = result :: syntaxTypes.replacements - if result == types.DELETION_MARKER then - print(`Marking {pathStr} for deletion`) - table.insert(deletedPaths, pathStr) - elseif result ~= source and not dryRun then - fs.writestringtofile(pathStr, result) + local serialized = printer.printfile(parseresult, replacements) + + if serialized == types.DELETION_MARKER then + print(`Marking {pathStr} for deletion`) + table.insert(deletedPaths, pathStr) + elseif serialized ~= source and not dryRun then + fs.writestringtofile(pathStr, serialized) + end end end diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index bd134bf92..01a048182 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1957,7 +1957,9 @@ struct AstSerialize : public Luau::AstVisitor if (node->types.data[i]->is()) { - serializeToken(node->types.data[i]->location.begin, "?", 1); + serializeToken(node->types.data[i]->location.begin, "?", 2); + lua_pushstring(L, "type"); + lua_setfield(L, -2, "kind"); lua_pushstring(L, "optional"); lua_setfield(L, -2, "tag"); lua_setfield(L, -2, "node"); diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index 9ba850e4c..68bb61c32 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -159,4 +159,4 @@ export type AstNode = types.AstNode export type ParseResult = types.ParseResult -return table.freeze({ parse = parser.parse, parseexpr = parser.parseexpr, parsefile = parser.parsefile }) +return table.freeze({ parseblock = parser.parseblock, parseexpr = parser.parseexpr, parse = parser.parse }) diff --git a/lute/std/libs/syntax/parser.luau b/lute/std/libs/syntax/parser.luau index 2ad15a371..713383574 100644 --- a/lute/std/libs/syntax/parser.luau +++ b/lute/std/libs/syntax/parser.luau @@ -8,7 +8,7 @@ local types = require("./types") local parser = {} --- Parses Luau source code into an AstStatBlock -function parser.parse(source: string): types.AstStatBlock +function parser.parseblock(source: string): types.AstStatBlock return luau.parse(source).root end @@ -16,7 +16,7 @@ function parser.parseexpr(source: string): types.AstExpr return luau.parseexpr(source) end -function parser.parsefile(source: string): types.ParseResult +function parser.parse(source: string): types.ParseResult return luau.parse(source) end diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index ad1da1acf..1fd61410a 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -1,7 +1,9 @@ --!strict -- @std/syntax/printer +local triviaUtils = require("./utils/trivia") local types = require("./types") +local utils = require("./utils") local visitor = require("./visitor") local printerLib = {} @@ -43,6 +45,11 @@ local function printToken(self: PrintVisitor, token: types.Token) end local function printString(self: PrintVisitor, expr: types.AstExprConstantString | types.AstTypeSingletonString) + if self.replacements[expr] ~= nil then + self:printReplacement(expr, self.replacements[expr]) + return + end + self:printTriviaList(expr.leadingtrivia) if expr.quotestyle == "single" then @@ -83,6 +90,11 @@ local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprIn end local function printReplacement(self: PrintVisitor, node: types.AstNode, replacement: types.replacement) + local leftmostTrivia = triviaUtils.leftmosttrivia(node) + if leftmostTrivia ~= nil then + self:printTriviaList(leftmostTrivia) + end + if typeof(replacement) == "string" then self:write(replacement) elseif replacement.kind ~= nil then -- LUAUFIX: type solver upset comparing string singleton union to nil @@ -91,6 +103,11 @@ local function printReplacement(self: PrintVisitor, node: types.AstNode, replace else error("Unsupported replacement type") end + + local rightmostTrivia = triviaUtils.rightmosttrivia(node) + if rightmostTrivia ~= nil then + self:printTriviaList(rightmostTrivia) + end end local function write(self: PrintVisitor, str: string) diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 9f0ad18d7..26eead2f6 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -2,7 +2,6 @@ -- @std/syntax/query -- Provides utility functions for querying Luau AST nodes -local printer = require("./printer") local types = require("./types") local visitor = require("./visitor") local utils = require("./utils") @@ -15,7 +14,6 @@ export type query = { replace: (self: query, repl: (T) -> types.replacement?) -> types.replacements, map: (self: query, fn: (T) -> U?) -> query, -- recursion violation reported forEach: (self: query, callback: (T) -> ()) -> (), - __root: node, } -- TODO: Add a select: (self: query, fn: (node) -> U?) -> query method and a flatMap: (self: query, fn: (T) -> { U }) -> query method local queryLib = {} @@ -31,12 +29,8 @@ function queryLib.filter(self: query, pred: (T) -> boolean): query return self end -local function print(self: types.replacements): string - return printer.printnode(self.__root, self) -end - function queryLib.replace(self: query, repl: (T) -> types.replacement?): types.replacements - local replacements = { print = print, __root = self.__root } + local replacements = {} for _, node in self.nodes do local r = repl(node) if r ~= nil then @@ -59,13 +53,14 @@ function queryLib.map(self: query, fn: (T) -> U?): query return self end -function queryLib.forEach(self: query, callback: (T) -> ()): () +function queryLib.forEach(self: query, callback: (T) -> ()): query for _, node in self.nodes do callback(node) end + return self end -function queryLib.selectFromRoot(ast: node, fn: (node) -> T?): query +function queryLib.selectFromRoot(ast: types.ParseResult | node, fn: (node) -> T?): query local nodes: { T } = {} local function visit(n: node) -- LUAUFIX: type checker doesn't like assigning visit in the visitor fields with n: node @@ -91,7 +86,8 @@ function queryLib.selectFromRoot(ast: node, fn: (node) -> T?): query return true end - visitor.visit(ast, selectVisitor) + local root: node = if ast.root ~= nil then ast.root else ast + visitor.visit(root, selectVisitor) return { nodes = nodes, @@ -99,7 +95,6 @@ function queryLib.selectFromRoot(ast: node, fn: (node) -> T?): query replace = queryLib.replace, map = queryLib.map, forEach = queryLib.forEach, - __root = ast, } -- LUAUFIX: queryLib.map has generics quantified at a different level than expected end diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index 47ba6b61a..f344e7161 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -159,6 +159,6 @@ export type AstNode = luau.AstNode export type ParseResult = luau.ParseResult export type replacement = string | AstNode -export type replacements = { [AstNode]: replacement, print: (self: replacements) -> string, __root: AstNode } +export type replacements = { [AstNode]: replacement } return {} diff --git a/lute/std/libs/syntax/utils.luau b/lute/std/libs/syntax/utils/init.luau similarity index 98% rename from lute/std/libs/syntax/utils.luau rename to lute/std/libs/syntax/utils/init.luau index a5f7b8229..ba813250f 100644 --- a/lute/std/libs/syntax/utils.luau +++ b/lute/std/libs/syntax/utils/init.luau @@ -234,4 +234,8 @@ function utils.isBaseToken(n: types.AstNode): types.Token? return if n.istoken and not n.kind then n else nil end +function utils.isAttribute(n: types.AstAttribute): types.AstAttribute? + return if n.kind == "attribute" then n else nil +end + return table.freeze(utils) diff --git a/lute/std/libs/syntax/utils/trivia.luau b/lute/std/libs/syntax/utils/trivia.luau new file mode 100644 index 000000000..fe6a475d7 --- /dev/null +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -0,0 +1,52 @@ +local query = require("../query") +local types = require("../types") + +local retrieverLib = {} + +function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } + local q: query.query = query.selectFromRoot(n, function(n) + return if n.istoken ~= nil and n.istoken then n else nil + end) + + local leftmostToken: types.Token? = nil + + for _, token in q.nodes do + if leftmostToken == nil then + leftmostToken = token + elseif token.position.line < leftmostToken.position.line then + leftmostToken = token + elseif + token.position.line == leftmostToken.position.line + and token.position.column < leftmostToken.position.column + then + leftmostToken = token + end + end + + return if leftmostToken ~= nil then leftmostToken.leadingtrivia else {} +end + +function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } + local q: query.query = query.selectFromRoot(n, function(n) + return if n.istoken ~= nil and n.istoken then n else nil + end) + + local rightmostToken: types.Token? = nil + + for _, token in q.nodes do + if rightmostToken == nil then + rightmostToken = token + elseif token.position.line > rightmostToken.position.line then + rightmostToken = token + elseif + token.position.line == rightmostToken.position.line + and token.position.column > rightmostToken.position.column + then + rightmostToken = token + end + end + + return if rightmostToken ~= nil then rightmostToken.trailingtrivia else {} +end + +return table.freeze(retrieverLib) diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 3fa244de3..c6d91c056 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -12,6 +12,8 @@ export type Visitor = { visitIf: (types.AstStatIf) -> boolean, visitWhile: (types.AstStatWhile) -> boolean, visitRepeat: (types.AstStatRepeat) -> boolean, + visitBreak: (types.AstStatBreak) -> boolean, + visitContinue: (types.AstStatContinue) -> boolean, visitReturn: (types.AstStatReturn) -> boolean, visitLocalDeclaration: (types.AstStatLocal) -> boolean, visitLocalDeclarationEnd: (types.AstStatLocal) -> (), @@ -23,6 +25,7 @@ export type Visitor = { visitLocalFunction: (types.AstStatLocalFunction) -> boolean, visitTypeAlias: (types.AstStatTypeAlias) -> boolean, visitStatTypeFunction: (types.AstStatTypeFunction) -> boolean, + visitStatExpr: (types.AstStatExpr) -> boolean, visitExpression: (types.AstExpr) -> boolean, visitExpressionEnd: (types.AstExpr) -> (), @@ -46,6 +49,7 @@ export type Visitor = { visitTypeString: (types.AstTypeSingletonString) -> boolean, visitTypeTypeof: (types.AstTypeTypeof) -> boolean, visitTypeGroup: (types.AstTypeGroup) -> boolean, + visitTypeOptional: (types.AstTypeOptional) -> boolean, visitTypeUnion: (types.AstTypeUnion) -> boolean, visitTypeIntersection: (types.AstTypeIntersection) -> boolean, visitTypeArray: (types.AstTypeArray) -> boolean, @@ -63,6 +67,8 @@ export type Visitor = { visitNumber: (types.AstExprConstantNumber) -> boolean, visitLocal: (types.AstLocal) -> boolean, visitVarargs: (types.AstExprVarargs) -> boolean, + + visitAttribute: (types.AstAttribute) -> boolean, } local function alwaysVisit(...: any) @@ -75,6 +81,8 @@ local defaultVisitor: Visitor = { visitIf = alwaysVisit :: any, visitWhile = alwaysVisit :: any, visitRepeat = alwaysVisit :: any, + visitBreak = alwaysVisit :: any, + visitContinue = alwaysVisit :: any, visitReturn = alwaysVisit :: any, visitLocalDeclaration = alwaysVisit :: any, visitLocalDeclarationEnd = alwaysVisit :: any, @@ -86,6 +94,7 @@ local defaultVisitor: Visitor = { visitLocalFunction = alwaysVisit :: any, visitTypeAlias = alwaysVisit :: any, visitStatTypeFunction = alwaysVisit :: any, + visitStatExpr = alwaysVisit :: any, visitExpression = alwaysVisit :: any, visitExpressionEnd = alwaysVisit :: any, @@ -109,6 +118,7 @@ local defaultVisitor: Visitor = { visitTypeString = alwaysVisit :: any, visitTypeTypeof = alwaysVisit :: any, visitTypeGroup = alwaysVisit :: any, + visitTypeOptional = alwaysVisit :: any, visitTypeUnion = alwaysVisit :: any, visitTypeIntersection = alwaysVisit :: any, visitTypeArray = alwaysVisit :: any, @@ -126,6 +136,8 @@ local defaultVisitor: Visitor = { visitNumber = alwaysVisit :: any, visitLocal = alwaysVisit :: any, visitVarargs = alwaysVisit :: any, + + visitAttribute = alwaysVisit :: any, } local function exhaustiveMatch(value: never): never @@ -208,6 +220,18 @@ local function visitRepeat(node: types.AstStatRepeat, visitor: Visitor) end end +local function visitBreak(node: types.AstStatBreak, visitor: Visitor) + if visitor.visitBreak(node) then + visitToken(node, visitor) + end +end + +local function visitContinue(node: types.AstStatContinue, visitor: Visitor) + if visitor.visitContinue(node) then + visitToken(node, visitor) + end +end + local function visitReturn(node: types.AstStatReturn, visitor: Visitor) if visitor.visitReturn(node) then visitToken(node.returnkeyword, visitor) @@ -321,6 +345,12 @@ local function visitTypeAlias(node: types.AstStatTypeAlias, visitor: Visitor) end end +local function visitStatExpr(node: types.AstStatExpr, visitor: Visitor) + if visitor.visitStatExpr(node) then + visitExpression(node.expression, visitor) + end +end + local function visitString(node: types.AstExprConstantString, visitor: Visitor) if visitor.visitString(node) then visitor.visitToken(node) @@ -426,8 +456,10 @@ local function visitFunctionBody(node: types.AstFunctionBody, visitor: Visitor) visitToken(node.endkeyword, visitor) end -local function visitAttribute(node: types.AstAttribute, visitor) - visitToken(node, visitor) +local function visitAttribute(node: types.AstAttribute, visitor: Visitor) + if visitor.visitAttribute(node) then + visitToken(node, visitor) + end end local function visitAnonymousFunction(node: types.AstExprAnonymousFunction, visitor: Visitor) @@ -628,6 +660,12 @@ local function visitTypeGroup(node: types.AstTypeGroup, visitor: Visitor) end end +local function visitTypeOptional(node: types.AstTypeOptional, visitor: Visitor) + if visitor.visitTypeOptional(node) then + visitToken(node, visitor) + end +end + local function visitTypeUnion(node: types.AstTypeUnion, visitor: Visitor) if visitor.visitTypeUnion(node) then if node.leading then @@ -803,7 +841,7 @@ function visitStatement(statement: types.AstStat, visitor: Visitor) elseif statement.tag == "conditional" then visitIf(statement, visitor) elseif statement.tag == "expression" then - visitExpression(statement.expression, visitor) + visitStatExpr(statement, visitor) elseif statement.tag == "local" then visitLocalStatement(statement, visitor) elseif statement.tag == "return" then @@ -811,9 +849,9 @@ function visitStatement(statement: types.AstStat, visitor: Visitor) elseif statement.tag == "while" then visitWhile(statement, visitor) elseif statement.tag == "break" then - visitToken(statement, visitor) + visitBreak(statement, visitor) elseif statement.tag == "continue" then - visitToken(statement, visitor) + visitContinue(statement, visitor) elseif statement.tag == "repeat" then visitRepeat(statement, visitor) elseif statement.tag == "for" then @@ -853,7 +891,7 @@ function visitType(type: types.AstType, visitor: Visitor) elseif type.tag == "intersection" then visitTypeIntersection(type, visitor) elseif type.tag == "optional" then - visitToken(type, visitor) + visitTypeOptional(type, visitor) elseif type.tag == "array" then visitTypeArray(type, visitor) elseif type.tag == "table" then @@ -881,10 +919,12 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor if visit then return { visitBlock = visit, - visitBlockEnd = visit :: any, + visitBlockEnd = visit, visitIf = visit, visitWhile = visit, visitRepeat = visit, + visitBreak = visit, + visitContinue = visit, visitReturn = visit, visitLocalDeclaration = visit, visitLocalDeclarationEnd = visit, @@ -896,6 +936,7 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitLocalFunction = visit, visitTypeAlias = visit, visitStatTypeFunction = visit, + visitStatExpr = visit, visitExpression = visit, visitExpressionEnd = visit, @@ -919,6 +960,7 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitTypeString = visit, visitTypeTypeof = visit, visitTypeGroup = visit, + visitTypeOptional = visit, visitTypeUnion = visit, visitTypeIntersection = visit, visitTypeArray = visit, @@ -936,6 +978,8 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitNumber = visit, visitLocal = visit, visitVarargs = visit, + + visitAttribute = visit, } else return table.clone(defaultVisitor) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 11cd425ef..59738bfdd 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -23,7 +23,7 @@ end test.suite("Parser", function(suite) suite:case("tokenContainsLeadingSpaces", function(assert) - local block = parser.parse(" local x = 1") + local block = parser.parseblock(" local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -37,7 +37,7 @@ test.suite("Parser", function(suite) end) suite:case("tokenContainsLeadingNewline", function(assert) - local block = parser.parse("\n" .. "local x = 1") + local block = parser.parseblock("\n" .. "local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -51,7 +51,7 @@ test.suite("Parser", function(suite) end) suite:case("tokenContainsLeadingSingleLineComment", function(assert) - local block = parser.parse("-- comment\n" .. "local x = 1") + local block = parser.parseblock("-- comment\n" .. "local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -68,7 +68,7 @@ test.suite("Parser", function(suite) end) suite:case("tokenContainsLeadingBlockComment", function(assert) - local block = parser.parse("--[[ comment ]] local x = 1") + local block = parser.parseblock("--[[ comment ]] local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -85,7 +85,7 @@ test.suite("Parser", function(suite) end) suite:case("tokenizeWhitespace", function(assert) - local block = parser.parse(" \n\t\t\n\n" .. "local x = 1") + local block = parser.parseblock(" \n\t\t\n\n" .. "local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -99,7 +99,7 @@ test.suite("Parser", function(suite) end) suite:case("triviaSplitBetweenLeadingAndTrailing", function(assert) - local block = parser.parse("local x = 'test' -- comment\n" .. "-- comment 2\nlocal y = 'value'") + local block = parser.parseblock("local x = 'test' -- comment\n" .. "-- comment 2\nlocal y = 'value'") assert.eq(#block.statements, 2) local firstStmt = block.statements[1] @@ -130,7 +130,7 @@ test.suite("Parser", function(suite) end local p = path.join(directory, entry.name) local source = fs.readfiletostring(path.format(p)) - local result = printer.printfile(parser.parsefile(source)) + local result = printer.printfile(parser.parse(source)) assert.eq(source, result) end @@ -142,14 +142,14 @@ test.suite("Parser", function(suite) suite:case("lineOffsetsField", function(assert) local singleLineCode = "local x = 1" - local result = parser.parsefile(singleLineCode) + local result = parser.parse(singleLineCode) assert.eq(type(result.lineoffsets), "table") assert.eq(#result.lineoffsets, 1) assert.eq(result.lineoffsets[1], 0) local multiLineCode = "local x = 1\nlocal y = 2\nlocal z = 3" - local multiResult = parser.parsefile(multiLineCode) + local multiResult = parser.parse(multiLineCode) assert.eq(type(multiResult.lineoffsets), "table") assert.eq(#multiResult.lineoffsets, 3) @@ -158,7 +158,7 @@ test.suite("Parser", function(suite) assert.eq(multiResult.lineoffsets[3], 24) local emptyLineCode = "local x = 1\n\nlocal y = 2" - local emptyResult = parser.parsefile(emptyLineCode) + local emptyResult = parser.parse(emptyLineCode) assert.eq(type(emptyResult.lineoffsets), "table") assert.eq(#emptyResult.lineoffsets, 3) @@ -180,7 +180,7 @@ test.suite("Parser", function(suite) } for _, subcase in ipairs(subcases) do - local block = parser.parse(subcase.code) + local block = parser.parseblock(subcase.code) assert.eq(#block.statements, 1) local l = block.statements[1] @@ -507,14 +507,15 @@ end test.suite("parse", function(suite) suite:case("parseEmptyProgram", function(assert) - local block = parser.parse("") + local block = parser.parseblock("") checkIsBlock(block, assert) assert.eq(#block.statements, 0) end) suite:case("parseIfStatement", function(assert) - local block = - parser.parse("if x > 0 then print('positive') elseif x < 0 then print('negative') else print('zero') end") + local block = parser.parseblock( + "if x > 0 then print('positive') elseif x < 0 then print('negative') else print('zero') end" + ) assert.eq(#block.statements, 1) local ifStat = block.statements[1] :: syntax.AstStatIf @@ -540,7 +541,7 @@ test.suite("parse", function(suite) checkIsBlock(ifStat.elseblock, assert) assert.eq(ifStat.endkeyword.text, "end") - block = parser.parse("if condition then doSomething() end") + block = parser.parseblock("if condition then doSomething() end") assert.eq(#block.statements, 1) local simpleIfStat = block.statements[1] :: syntax.AstStatIf assert.eq(simpleIfStat.tag, "conditional") @@ -556,7 +557,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatWhile", function(assert) - local block = parser.parse("while i <= 10 do print(i); i = i + 1 end") + local block = parser.parseblock("while i <= 10 do print(i); i = i + 1 end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -569,7 +570,7 @@ test.suite("parse", function(suite) assert.eq(#whileStat.body.statements, 2) assert.eq(whileStat.endkeyword.text, "end") - block = parser.parse("while true do break end") + block = parser.parseblock("while true do break end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -589,7 +590,7 @@ test.suite("parse", function(suite) assert.eq(whileStat.endkeyword.text, "end") - block = parser.parse("while true do continue end") + block = parser.parseblock("while true do continue end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -611,7 +612,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatRepeat", function(assert) - local block = parser.parse("repeat i = i + 1; print(i) until i > 5") + local block = parser.parseblock("repeat i = i + 1; print(i) until i > 5") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -625,7 +626,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatLocal", function(assert) - local block = parser.parse("local b, c = 1, 2") + local block = parser.parseblock("local b, c = 1, 2") checkIsBlock(block, assert) assert.eq(#block.statements, 1) assert.eq(block.statements[1].kind, "stat") @@ -648,7 +649,7 @@ test.suite("parse", function(suite) assert.eq(l.values[2].node.tag, "number") assert.eq(l.values[2].separator, nil) - block = parser.parse("local x") + block = parser.parseblock("local x") checkIsBlock(block, assert) assert.eq(#block.statements, 1) assert.eq(block.statements[1].kind, "stat") @@ -664,7 +665,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatFor", function(assert) - local block = parser.parse("for i = 1, 10, 2 do print(i) end") + local block = parser.parseblock("for i = 1, 10, 2 do print(i) end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -685,7 +686,7 @@ test.suite("parse", function(suite) assert.eq(#forStat.body.statements, 1) assert.eq(forStat.endkeyword.text, "end") - block = parser.parse("for j = 0, 5 do print(j) end") + block = parser.parseblock("for j = 0, 5 do print(j) end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -706,7 +707,7 @@ test.suite("parse", function(suite) end) suite:case("parseForInLoop", function(assert) - local block = parser.parse("for key, value in arr do print(key, value) end") + local block = parser.parseblock("for key, value in arr do print(key, value) end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -726,7 +727,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatAssign", function(assert) - local block = parser.parse("x = 1") + local block = parser.parseblock("x = 1") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -740,7 +741,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatCompoundAssign", function(assert) - local block = parser.parse("x += 5") + local block = parser.parseblock("x += 5") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -752,7 +753,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatFunction", function(assert) - local block = parser.parse("@checked @native @deprecated function add(a, b) return a + b end") + local block = parser.parseblock("@checked @native @deprecated function add(a, b) return a + b end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -769,7 +770,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatLocalFunction", function(assert) - local block = parser.parse("local function multiply(x, y) return x * y end") + local block = parser.parseblock("local function multiply(x, y) return x * y end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -782,7 +783,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatTypeAlias", function(assert) - local block = parser.parse("type Vector3 = {x: number, y: number, z: number}") + local block = parser.parseblock("type Vector3 = {x: number, y: number, z: number}") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -798,7 +799,7 @@ test.suite("parse", function(suite) assert.eq(typeAlias.type.kind, "type") assert.eq(typeAlias.type.tag, "table") - block = parser.parse("export type Point = {x: T, y: T, f: (U...) -> ()}") + block = parser.parseblock("export type Point = {x: T, y: T, f: (U...) -> ()}") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -816,7 +817,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatTypeFunction", function(assert) - local block = parser.parse("type function foo() return types.number end") + local block = parser.parseblock("type function foo() return types.number end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -827,7 +828,7 @@ test.suite("parse", function(suite) assert.eq(typeFunc.functionkeyword.text, "function") assert.eq(typeFunc.name.text, "foo") - block = parser.parse("export type function foo() return types.number end") + block = parser.parseblock("export type function foo() return types.number end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -840,7 +841,7 @@ end) test.suite("parse types", function(suite) -- AstGenericType and AstGenericTypePack are tested in parseExprAnonymousFunction suite:case("testTypeReference", function(assert) - local block = parser.parse("type MyString = string") + local block = parser.parseblock("type MyString = string") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "reference") @@ -852,7 +853,7 @@ test.suite("parse types", function(suite) assert.eq(typeRef.parameters, nil) assert.eq(typeRef.closeparameters, nil) - block = parser.parse("type MyTable = MyModule.Table") + block = parser.parseblock("type MyTable = MyModule.Table") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "reference") @@ -873,7 +874,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeSingletonBoolean", function(assert) - local block = parser.parse("type TrueType = true") + local block = parser.parseblock("type TrueType = true") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "boolean") @@ -881,7 +882,7 @@ test.suite("parse types", function(suite) assert.eq(singletonBool.text, "true") assert.eq(singletonBool.value, true) - block = parser.parse("type FalseType = false") + block = parser.parseblock("type FalseType = false") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "boolean") @@ -891,7 +892,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeSingletonString", function(assert) - local block = parser.parse("type HelloType = 'hello'") + local block = parser.parseblock("type HelloType = 'hello'") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "string") @@ -899,7 +900,7 @@ test.suite("parse types", function(suite) assert.eq(singletonStr.text, "hello") assert.eq(singletonStr.quotestyle, "single") - block = parser.parse('type WorldType = "world"') + block = parser.parseblock('type WorldType = "world"') typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "string") @@ -909,7 +910,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeTypeof", function(assert) - local block = parser.parse("type T = typeof(someVariable)") + local block = parser.parseblock("type T = typeof(someVariable)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "typeof") @@ -921,7 +922,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeGroup", function(assert) - local block = parser.parse("type T = (string)") + local block = parser.parseblock("type T = (string)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "group") @@ -932,7 +933,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeUnion", function(assert) - local block = parser.parse("type T = string | number | boolean") + local block = parser.parseblock("type T = string | number | boolean") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "union") @@ -943,7 +944,7 @@ test.suite("parse types", function(suite) assert.eq(unionType.types[2].node.tag, "reference") assert.eq(unionType.types[3].node.tag, "reference") - block = parser.parse("type T = | number?") + block = parser.parseblock("type T = | number?") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "union") @@ -957,7 +958,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeUnionWithOptionalPart", function(assert) - local block = parser.parse("type T = number? | string") + local block = parser.parseblock("type T = number? | string") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "union") @@ -980,7 +981,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeIntersection", function(assert) - local block = parser.parse("type T = A & B & C") + local block = parser.parseblock("type T = A & B & C") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "intersection") @@ -991,7 +992,7 @@ test.suite("parse types", function(suite) assert.eq(intersectionType.types[2].node.tag, "reference") assert.eq(intersectionType.types[3].node.tag, "reference") - block = parser.parse("type T = & B") + block = parser.parseblock("type T = & B") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "intersection") @@ -1003,7 +1004,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeArray", function(assert) - local block = parser.parse("type T = { number }") + local block = parser.parseblock("type T = { number }") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "array") @@ -1013,7 +1014,7 @@ test.suite("parse types", function(suite) assert.eq(arrayType.type.tag, "reference") assert.eq(arrayType.closebrace.text, "}") - block = parser.parse("type T = { read string }") + block = parser.parseblock("type T = { read string }") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "array") @@ -1021,7 +1022,7 @@ test.suite("parse types", function(suite) assert.neq(arrayType.access, nil) assert.eq((arrayType.access :: syntax.Token<"read" | "write">).text, "read") - block = parser.parse("type T = { write number }") + block = parser.parseblock("type T = { write number }") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "array") @@ -1031,7 +1032,7 @@ test.suite("parse types", function(suite) end) suite:case("parseTypeTable", function(assert) - local block = parser.parse("type T = {}") + local block = parser.parseblock("type T = {}") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") @@ -1040,7 +1041,7 @@ test.suite("parse types", function(suite) assert.eq(#tableType.entries, 0) assert.eq(tableType.closebrace.text, "}") - block = parser.parse("type T = {[number] : string}") + block = parser.parseblock("type T = {[number] : string}") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") @@ -1056,7 +1057,7 @@ test.suite("parse types", function(suite) assert.eq(tableTypeItem.value.tag, "reference") assert.eq(tableTypeItem.separator, nil) - block = parser.parse("type T = {['hello'] : 'world', read ['foo'] : number; write ['bar'] : boolean}") + block = parser.parseblock("type T = {['hello'] : 'world', read ['foo'] : number; write ['bar'] : boolean}") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") tableType = typeAlias.type :: syntax.AstTypeTable @@ -1086,7 +1087,7 @@ test.suite("parse types", function(suite) assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "write") assert.eq(tableTypeItem.separator, nil) - block = parser.parse("type T = { hello: 'world'; read foo: number, write bar: boolean }") + block = parser.parseblock("type T = { hello: 'world'; read foo: number, write bar: boolean }") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") tableType = typeAlias.type :: syntax.AstTypeTable @@ -1115,7 +1116,7 @@ test.suite("parse types", function(suite) end) suite:case("parseTypeFunction", function(assert) - local block = parser.parse("type T = (n : number, string) -> boolean") + local block = parser.parseblock("type T = (n : number, string) -> boolean") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") @@ -1144,7 +1145,7 @@ test.suite("parse types", function(suite) assert.eq(funcType.returnarrow.text, "->") assert.eq(funcType.returntypes.kind, "typepack") - block = parser.parse("type T = (item: T, ...U) -> ()") + block = parser.parseblock("type T = (item: T, ...U) -> ()") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") @@ -1166,7 +1167,7 @@ test.suite("parse types", function(suite) assert.neq(funcType.vararg, nil) assert.eq((funcType.vararg :: syntax.AstTypePack).kind, "typepack") - block = parser.parse("type T = () -> ...number") + block = parser.parseblock("type T = () -> ...number") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 50cc51962..8b961f88a 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -1,111 +1,846 @@ -local luau = require("@std/luau") -local parser = require("@std/syntax/parser") +local assertLib = require("@std/test/assert") local printer = require("@std/syntax/printer") local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local syntaxTypes = require("@std/syntax/types") local syntaxUtils = require("@std/syntax/utils") local test = require("@std/test") +local function compareStrings(result: string, expected: string) + for i = 1, math.max(#result, #expected) do + local rChar = result:sub(i, i):byte() + local eChar = expected:sub(i, i):byte() + if rChar ~= eChar then + print(`Difference at index {i}: result='{rChar}', expected='{eChar}'`) + end + end +end + +local function checkReplacement( + source: string, + expected: string, + transformFn: (syntaxTypes.AstNode) -> syntaxTypes.replacements?, + assert: assertLib.asserts +) + local ast = syntax.parse(source) + local replacements = transformFn(ast.root) + + local result = printer.printfile(ast, replacements) + + assert.eq(result, expected) +end + test.suite("syntax_printer", function(suite) suite:case("string_replacement", function(assert) local source = [[ local name = "World" - ]] +]] - local ast = parser.parse(source) - local replacements = query.selectFromRoot(ast, syntaxUtils.isExprConstantString):replace(function(s) - return if s.text == "World" then "1 + 2" else nil - end) + local expected = " local name = 1 + 2\n" - local result = printer.printnode(ast, replacements) - - local expected = " local name = 1 + 2" -- TODO: preserve trivia so trailing newline is preserved - - assert.eq(result, expected) + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprConstantString):replace(function(s) + return if s.text == "World" then "1 + 2" else nil + end) + end, assert) end) suite:case("expr_replacement", function(assert) local source = [[ local name = "World" - ]] - - local ast = parser.parse(source) - local replacements = query.selectFromRoot(ast, syntaxUtils.isExprConstantString):replace(function(s) - return if s.text == "World" then parser.parseexpr("1 + 2") else nil -- LUAUFIX: I think this is because AstExpr doesn't subtype against AstNode - end) +]] - local result = printer.printnode(ast, replacements) + local expected = " local name = 1 + 2\n" - local expected = " local name = 1 + 2" -- TODO: preserve trivia so trailing newline is preserved - - assert.eq(result, expected) + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprConstantString):replace(function(s) + return if s.text == "World" then syntax.parseexpr("1 + 2") else nil + end) + end, assert) end) suite:case("stat_replacement", function(assert) local source = [[ local name = "World" - ]] - - local ast = parser.parse(source) - local replacements = query.selectFromRoot(ast, syntaxUtils.isStatLocal):replace(function(s) - return parser.parse("local name = 1 + 2") - end) - - local result = printer.printnode(ast, replacements) +]] - local expected = "local name = 1 + 2" -- TODO: preserve trivia so indentation and trailing newline is preserved + local expected = " local name = 1 + 2\n" - assert.eq(result, expected) + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatLocal):replace(function(s) + return syntax.parseblock("local name = 1 + 2") + end) + end, assert) end) suite:case("type_replacement", function(assert) local source = [[local name: number = "World"]] + local expected = [[local name: string = "World"]] + + local stat = syntax.parseblock("local x: string") + local ann = (stat.statements[1] :: syntaxTypes.AstStatLocal).variables[1].node.annotation - local stat = parser.parse("local x: string") - local ann = (stat.statements[1] :: luau.AstStatLocal).variables[1].node.annotation - assert.neq(ann, nil) + checkReplacement(source, expected, function(ast) + return query + .selectFromRoot(ast, syntaxUtils.isStatLocal) + :map(function(assign) + return assign.variables[1].node.annotation + end) + :replace(function(t) + return ann + end) + end, assert) + end) - local ast = parser.parse(source) - local replacements = query - .selectFromRoot(ast, syntaxUtils.isStatLocal) - :map(function(assign) - local ann = assign.variables[1].node.annotation - assert.neq(ann, nil) - return ann + suite:case("type_pack_replacement", function(assert) + local source = [[type t = (...number) -> ()]] + local expected = [[type t = (...number) -> string, ...string)]] + + local stat = syntax.parseblock("type t = () -> (string, ...string)") + local tp = ((stat.statements[1] :: syntaxTypes.AstStatTypeAlias).type :: syntaxTypes.AstTypeFunction).returntypes + + checkReplacement(source, expected, function(ast) + return query + .selectFromRoot(ast, syntaxUtils.isStatTypeAlias) + :map(function(ta) + if ta.type.tag == "function" then + return ta.type.returntypes + end + end) + :replace(function(t) + return tp + end) + end, assert) + end) + + suite:case("exprgroup_trivia", function(assert) + local source = [[local x = +(1 + 2) -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprGroup):replace(function() + return "replaced" end) - :replace(function(t) - return ann + end, assert) + end) + + suite:case("exprnil_trivia", function(assert) + local source = [[local x = +nil -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprConstantNil):replace(function() + return "replaced" end) + end, assert) + end) - local result = printer.printnode(ast, replacements) + suite:case("exprbool_trivia", function(assert) + local source = [[local x = +true -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprConstantBool):replace(function() + return "replaced" + end) + end, assert) + end) - local expected = [[local name: string= "World"]] -- TODO: preserve trivia so space after type is preserved + suite:case("exprnumber_trivia", function(assert) + local source = [[local x = +235 -- after]] + local expected = "local x =\nreplaced -- after" - assert.eq(result, expected) + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprConstantNumber):replace(function() + return "replaced" + end) + end, assert) end) - suite:case("type_pack_replacement", function(assert) - local source = [[type t = (...number) -> ()]] + suite:case("exprlocal_trivia", function(assert) + local source = [[local y = 3 + local x = +y -- after]] + local expected = "local y = 3\n\t\tlocal x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprLocal):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprglobal_trivia", function(assert) + local source = [[local x = +y -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprGlobal):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprvarargs_trivia", function(assert) + local source = [[local x = +... -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprVarargs):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprcall_trivia", function(assert) + local source = [[local x = +print() -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprCall):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprindexname_trivia", function(assert) + local source = [[local x = +hello.world -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprIndexName):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprindexexpr_trivia", function(assert) + local source = [[local x = +hello['world'] -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprIndexExpr):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("expranonymousfunction_trivia", function(assert) + local source = [[local x = +function() return end -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprAnonymousFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprtable_trivia", function(assert) + local source = [[local x = +{} -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprTable):replace(function() + return "replaced" + end) + end, assert) + end) - local stat = parser.parse("type t = () -> (string, ...string)") - local tp = ((stat.statements[1] :: luau.AstStatTypeAlias).type :: luau.AstTypeFunction).returntypes + suite:case("exprunary_trivia", function(assert) + local source = [[local x = +#t -- after]] + local expected = "local x =\nreplaced -- after" - local ast = parser.parse(source) - local replacements = query - .selectFromRoot(ast, syntaxUtils.isStatTypeAlias) - :map(function(ta) - if ta.type.tag == "function" then - return ta.type.returntypes - end + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprUnary):replace(function() + return "replaced" end) - :replace(function(t) - return tp + end, assert) + end) + + suite:case("exprbinary_trivia", function(assert) + local source = [[local x = +3 + 2 -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprBinary):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprinterpstring_trivia", function(assert) + local source = [[local x = +`hello {"world"}` -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprInterpString):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprtypeassertion_trivia", function(assert) + local source = [[local x = +y :: number -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprTypeAssertion):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("exprifelse_trivia", function(assert) + local source = [[local x = +if true then 'hello' else 'world' -- after]] + local expected = "local x =\nreplaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExprIfElse):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("expr_trivia", function(assert) + local source = [[local x, y = +if true then 'hello' else 'world', z :: string -- after]] + local expected = "local x, y =\nreplaced, replaced -- after" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isExpr):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statblock_trivia", function(assert) + local source = [[-- hello +local y = 1 +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatBlock):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statif_trivia", function(assert) + local source = [[-- hello +if true then + return 1 +else + return 2 +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatIf):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statwhile_trivia", function(assert) + local source = [[-- hello +while true do + print("hello") +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatWhile):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statrepeat_trivia", function(assert) + local source = [[-- hello +repeat + print("hello") +until true +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatRepeat):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statbreak_trivia", function(assert) + local source = [[-- hello +while true do + break +end +-- world]] + local expected = "-- hello\nwhile true do\n\treplaced\nend\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatBreak):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statcontinue_trivia", function(assert) + local source = [[-- hello +while true do + continue +end +-- world]] + local expected = "-- hello\nwhile true do\n\treplaced\nend\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatContinue):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statreturn_trivia", function(assert) + local source = [[-- hello +function foo() return 1, 2 end +-- world]] + local expected = "-- hello\nfunction foo() replaced end\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatReturn):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statexpr_trivia", function(assert) + local source = [[-- hello +foobar() +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatExpr):replace(function() + return "replaced" end) + end, assert) + end) + + suite:case("statlocal_trivia", function(assert) + local source = [[-- hello +local x = 1 +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatLocal):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statfor_trivia", function(assert) + local source = [[-- hello +for i = 1, 10 do + print(i) +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatFor):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statforin_trivia", function(assert) + local source = [[-- hello +for i in {1, 2, 3} do + print(i) +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatForIn):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statassign_trivia", function(assert) + local source = [[-- hello +local y = 2 +y = 1 +-- world]] + local expected = "-- hello\nlocal y = 2\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatAssign):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statcompoundassign_trivia", function(assert) + local source = [[-- hello +local y = 2 +y -= 1 +-- world]] + local expected = "-- hello\nlocal y = 2\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatCompoundAssign):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statfunction_trivia", function(assert) + local source = [[-- hello +function foo() + return 1 +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("statlocalfunction_trivia", function(assert) + local source = [[-- hello +local function foo() + return 1 +end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatLocalFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("stattypealias_trivia", function(assert) + local source = [[-- hello +type foo = number +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatTypeAlias):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("stattypefunction_trivia", function(assert) + local source = [[-- hello +type function foo() return number end +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStatTypeFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("stat_trivia", function(assert) + local source = [[-- hello +local x = 1 +local y = 2 +-- world]] + local expected = "-- hello\nreplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isStat):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typereference_trivia", function(assert) + local source = [[-- hello +type foo = number +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeReference):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typesingletonbool_trivia", function(assert) + local source = [[-- hello +type foo = true +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeSingletonBool):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typesingletonstring_trivia", function(assert) + local source = [[-- hello +local x : "hello" +-- world]] + local expected = "-- hello\nlocal x : replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeSingletonString):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typetypeof_trivia", function(assert) + local source = [[-- hello +type foo = typeof(123) +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeTypeof):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typegroup_trivia", function(assert) + local source = [[-- hello +type foo = (number) +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeGroup):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typeoptional_trivia", function(assert) + local source = [[-- hello +type foo = true? +-- world]] + local expected = "-- hello\ntype foo = truereplaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeOptional):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typeunion_trivia", function(assert) + local source = [[-- hello +type foo = true? | false +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeUnion):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typeintersection_trivia", function(assert) + local source = [[-- hello +type foo = true & false +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeIntersection):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typearray_trivia", function(assert) + local source = [[-- hello +type foo = { number } +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeArray):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typetable_trivia", function(assert) + local source = [[-- hello +type foo = {} +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeTable):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typefunction_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> () +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypeFunction):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("type_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> () +-- world]] + local expected = "-- hello\ntype foo = replaced\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isType):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typepackexplicit_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> (number, ...string) +-- world]] + local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypePackExplicit):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typepackgeneric_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> (number, G...) +-- world]] + local expected = "-- hello\ntype foo = (number) -> (number, replaced)\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypePackGeneric):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typepackvariadic_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> (...number) +-- world]] + local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypePackVariadic):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("typepack_trivia", function(assert) + local source = [[-- hello +type foo = (number) -> (number, ...string) +-- world]] + local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isTypePack):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("local_trivia", function(assert) + local source = [[-- hello +local x, y = 1, 2 +-- world]] + local expected = "-- hello\nlocal replaced, replaced = 1, 2\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isLocal):replace(function() + return "replaced" + end) + end, assert) + end) + + suite:case("attribute_trivia", function(assert) + local source = [[-- hello +@checked +function foo() return end +-- world]] + local expected = "-- hello\nreplaced\nfunction foo() return end\n-- world" + + checkReplacement(source, expected, function(ast) + return query.selectFromRoot(ast, syntaxUtils.isAttribute):replace(function() + return "replaced" + end) + end, assert) + end) - local result = printer.printnode(ast, replacements) + suite:case("comments_in_call", function(assert) + local source = [[foobar( + foo, -- what about this + bar, -- and this + baz -- and this?? +)]] - local expected = [[type t = (...number) -> string, ...string)]] -- TODO: bug in the parser? AstTypePackExplicit is missing opening + local expected = "foobar(\n foo, -- what about this\n bar, -- and this\n quxx -- and this??\n)" - assert.eq(result, expected) + checkReplacement(source, expected, function(ast) + return query + .selectFromRoot(ast, syntaxUtils.isExprGlobal) + :filter(function(g) + return g.name.text == "baz" + end) + :replace(function() + return "quxx" + end) + end, assert) end) end) diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index 6b16d4416..21195b2ab 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -41,7 +41,7 @@ test.suite("AST Query", function(suite) for _, _ in replacements do count += 1 end - assert.eq(count, 6) -- 4 non-nil replacements + print + __root + assert.eq(count, 4) -- 4 non-nil replacements end) suite:case("map", function(assert) From e6dbdfce07add9b701e32bd2e7fb99f340c05c10 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:29:17 +0000 Subject: [PATCH 166/642] Update Luau to 0.700 (#586) **Luau**: Updated from `0.699` to `0.700` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.700 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: vrn-sn <61795485+vrn-sn@users.noreply.github.com> --- extern/luau.tune | 4 ++-- lute/luau/include/lute/configresolver.h | 3 ++- lute/luau/include/lute/moduleresolver.h | 2 +- lute/luau/src/configresolver.cpp | 2 +- lute/luau/src/moduleresolver.cpp | 2 +- tests/src/configresolver.test.cpp | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index f5e80e34f..7816cc340 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.699" -revision = "994b6416f1a2d16ac06c52b4e574bad5d8749053" +branch = "0.700" +revision = "3e1c94ec2c1a077497b7ac21f580745c7aeeefae" diff --git a/lute/luau/include/lute/configresolver.h b/lute/luau/include/lute/configresolver.h index bb9dbe84b..4b6a9f1a2 100644 --- a/lute/luau/include/lute/configresolver.h +++ b/lute/luau/include/lute/configresolver.h @@ -1,6 +1,7 @@ #pragma once #include "Luau/Config.h" +#include "Luau/ConfigResolver.h" namespace Luau { @@ -14,7 +15,7 @@ struct LuteConfigResolver : Luau::ConfigResolver LuteConfigResolver(Luau::Mode mode); - const Luau::Config& getConfig(const Luau::ModuleName& name) const override; + const Luau::Config& getConfig(const Luau::ModuleName& name, const TypeCheckLimits& limits) const override; const Luau::Config& readConfigRec(const std::string& path) const; }; diff --git a/lute/luau/include/lute/moduleresolver.h b/lute/luau/include/lute/moduleresolver.h index 64a8051d8..c023346fd 100644 --- a/lute/luau/include/lute/moduleresolver.h +++ b/lute/luau/include/lute/moduleresolver.h @@ -13,7 +13,7 @@ struct LuteModuleResolver : Luau::FileResolver std::optional readSource(const Luau::ModuleName& name) override; // We are currently resolving modules and requires only, and will add support for Roblox globals / types in a subsequent PR. - std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node) override; + std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const TypeCheckLimits& limits) override; }; } // namespace Luau diff --git a/lute/luau/src/configresolver.cpp b/lute/luau/src/configresolver.cpp index 8e381990d..c5305df20 100644 --- a/lute/luau/src/configresolver.cpp +++ b/lute/luau/src/configresolver.cpp @@ -12,7 +12,7 @@ LuteConfigResolver::LuteConfigResolver(Luau::Mode mode) defaultConfig.mode = mode; } -const Luau::Config& LuteConfigResolver::getConfig(const Luau::ModuleName& name) const +const Luau::Config& LuteConfigResolver::getConfig(const Luau::ModuleName& name, const TypeCheckLimits& limits) const { std::optional path = getParentPath(name); if (!path) diff --git a/lute/luau/src/moduleresolver.cpp b/lute/luau/src/moduleresolver.cpp index b8c5ab9b6..0f141b1e7 100644 --- a/lute/luau/src/moduleresolver.cpp +++ b/lute/luau/src/moduleresolver.cpp @@ -16,7 +16,7 @@ std::optional LuteModuleResolver::readSource(const Luau::Modul } // We are currently resolving modules and requires only, and will add support for Roblox globals / types in a subsequent PR. -std::optional LuteModuleResolver::resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node) +std::optional LuteModuleResolver::resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const TypeCheckLimits& limits) { if (auto expr = node->as()) { diff --git a/tests/src/configresolver.test.cpp b/tests/src/configresolver.test.cpp index 0d7d912b3..691a4ed88 100644 --- a/tests/src/configresolver.test.cpp +++ b/tests/src/configresolver.test.cpp @@ -14,7 +14,7 @@ TEST_CASE("configresolver") // There is a .luaurc in tests/src/resolver; verify resolver reads it without crashing Luau::LuteConfigResolver configResolver(Luau::Mode::Nonstrict); - const Luau::Config& cfg = configResolver.getConfig(file); + const Luau::Config& cfg = configResolver.getConfig(file, {}); // check that mode was set to strict per .luaurc CHECK(cfg.mode == Luau::Mode::Strict); From 4d3203d037a7a024034dffa3c3f1db2638f1079c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:31:38 +0000 Subject: [PATCH 167/642] Update Lute to 0.1.0-nightly.20251114 (#587) **Lute**: Updated from `0.1.0-nightly.20251107` to `0.1.0-nightly.20251114` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20251114 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: vrn-sn <61795485+vrn-sn@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 764af7d4f..633b6ec08 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251107" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251114" } diff --git a/rokit.toml b/rokit.toml index 9cb34eb15..6a7c7366c 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20251107" +lute = "luau-lang/lute@0.1.0-nightly.20251114" From e6148d4ab6d5a6acd185ce6ebeb2f02de7eeb995 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 14 Nov 2025 16:11:25 -0800 Subject: [PATCH 168/642] stdlib: luacase some ast node fields (#588) A while back, I went through and luacased the AST node fields. It looks like I missed a few on that pass, so fixing them here. --- definitions/luau.luau | 6 +++--- lute/luau/src/luau.cpp | 6 +++--- lute/std/libs/syntax/visitor.luau | 6 +++--- tests/std/syntax/parser.test.luau | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 1d3a5a9d8..d726c2fde 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -270,9 +270,9 @@ export type AstStatWhile = { export type AstStatRepeat = { kind: "stat", tag: "repeat", - repeatKeyword: Token<"repeat">, + repeatkeyword: Token<"repeat">, body: AstStatBlock, - untilKeyword: Token<"until">, + untilkeyword: Token<"until">, condition: AstExpr, } @@ -367,7 +367,7 @@ export type AstStatTypeAlias = { kind: "stat", tag: "typealias", export: Token<"export">?, - typeToken: Token<"type">, + typetoken: Token<"type">, name: Token, opengenerics: Token<"<">?, generics: Punctuated?, diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 01a048182..dd5cf9656 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1259,14 +1259,14 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "repeat", "stat"); serializeToken(node->location.begin, "repeat"); - lua_setfield(L, -2, "repeatKeyword"); + lua_setfield(L, -2, "repeatkeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); auto cstNode = lookupCstNode(node); serializeToken(cstNode->untilPosition, "until"); - lua_setfield(L, -2, "untilKeyword"); + lua_setfield(L, -2, "untilkeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1532,7 +1532,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "export"); serializeToken(cstNode->typeKeywordPosition, "type"); - lua_setfield(L, -2, "typeToken"); + lua_setfield(L, -2, "typetoken"); serializeToken(node->nameLocation.begin, node->name.value); lua_setfield(L, -2, "name"); diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index c6d91c056..39e77e629 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -213,9 +213,9 @@ end local function visitRepeat(node: types.AstStatRepeat, visitor: Visitor) if visitor.visitRepeat(node) then - visitToken(node.repeatKeyword, visitor) + visitToken(node.repeatkeyword, visitor) visitBlock(node.body, visitor) - visitToken(node.untilKeyword, visitor) + visitToken(node.untilkeyword, visitor) visitExpression(node.condition, visitor) end end @@ -326,7 +326,7 @@ local function visitTypeAlias(node: types.AstStatTypeAlias, visitor: Visitor) if node.export then visitToken(node.export, visitor) end - visitToken(node.typeToken, visitor) + visitToken(node.typetoken, visitor) visitToken(node.name, visitor) if node.opengenerics then visitToken(node.opengenerics, visitor) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 59738bfdd..0d9821f24 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -618,10 +618,10 @@ test.suite("parse", function(suite) local repeatStat = block.statements[1] :: syntax.AstStatRepeat assert.eq(repeatStat.tag, "repeat") - assert.eq(repeatStat.repeatKeyword.text, "repeat") + assert.eq(repeatStat.repeatkeyword.text, "repeat") checkIsBlock(repeatStat.body, assert) assert.eq(#repeatStat.body.statements, 2) - assert.eq(repeatStat.untilKeyword.text, "until") + assert.eq(repeatStat.untilkeyword.text, "until") assert.eq(repeatStat.condition.tag, "binary") end) @@ -790,7 +790,7 @@ test.suite("parse", function(suite) local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.tag, "typealias") assert.eq(typeAlias.export, nil) - assert.eq(typeAlias.typeToken.text, "type") + assert.eq(typeAlias.typetoken.text, "type") assert.eq(typeAlias.name.text, "Vector3") assert.eq(typeAlias.opengenerics, nil) assert.eq(typeAlias.generics, nil) From 018efd96b8afa133f657622316e6afd8bdabf924 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:46:17 -0800 Subject: [PATCH 169/642] stdlib: fs.walk (#583) We previously did not have a way to recursively iterate through a directory and its subdirectories. This PR introduces `fs.walk(path, options?)` that allows us to walk through a directory, with an optional boolean to specify recursive or not. API: ```luau export type walkdirectoryoptions = { recursive: boolean?, } function fslib.walk(path: pathlike, options: walkdirectoryoptions?): () -> pathlike? ``` Example: ```luau local fs = require("@std/fs") local iterator = fslib.walk("some/directory", { recursive = true }) local path = iterator() while path do print(path) path = iterator() end ``` See `examples/walk_directory.luau` as well. Note: Because `for` loops don't support yielding generalized iterators yet, we cannot use `fs.walk` as ``` for path in fs.walk(...) do ... end ``` --- examples/walk_directory.luau | 44 ++++++++++++++++++++++++++++++++++++ lute/std/libs/fs.luau | 30 ++++++++++++++++++++++++ tests/std/fs.test.luau | 42 ++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 examples/walk_directory.luau diff --git a/examples/walk_directory.luau b/examples/walk_directory.luau new file mode 100644 index 000000000..fc0638ed3 --- /dev/null +++ b/examples/walk_directory.luau @@ -0,0 +1,44 @@ +local fslib = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") + +local tmpdir = system.tmpdir() +local baseDir = path.join(tmpdir, "walk_directory_example") + +-- Setup: create a directory structure +if not fslib.exists(baseDir) then + fslib.createdirectory(baseDir, { makeparents = true }) +end + +local subdirs = { + "subdir1", + "subdir2/nested1", +} + +for _, subdir in subdirs do + local fullPath = path.join(baseDir, subdir) + fslib.createdirectory(fullPath, { makeparents = true }) +end + +local files = { + "file1.txt", + "subdir1/file2.txt", + "subdir2/nested1/file3.txt", +} + +for _, file in files do + local fullPath = path.join(baseDir, file) + fslib.writestringtofile(fullPath, "hello lute") +end + +-- Walk the directory recursively +print("Walking directory:", baseDir) +local it = fslib.walk(baseDir, { recursive = true }) +local walker = it() +while walker do + print("Found:", walker) + walker = it() +end + +-- Cleanup: remove the created directory structure +fslib.removedirectory(baseDir, { recursive = true }) diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 44918a2d3..1e3b52d9f 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -26,6 +26,10 @@ export type removedirectoryoptions = { recursive: boolean?, } +export type walkoptions = { + recursive: boolean?, +} + function fslib.open(path: pathlike, mode: handlemode?): filehandle return fs.open(pathlib.format(path), mode) end @@ -141,4 +145,30 @@ function fslib.removedirectory(path: pathlike, options: removedirectoryoptions?) end end +-- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. +function fslib.walk(path: pathlike, options: walkoptions?): () -> path? + local queue = { pathlib.parse(path) } + + return function() + while #queue > 0 do + local current = table.remove(queue, 1) + if not current then + return nil + end + + if fslib.type(current) == "dir" and options and options.recursive then + local entries = fslib.listdir(current) + for _, entry in entries do + local fullPath = pathlib.join(current, entry.name) + table.insert(queue, fullPath) + end + end + + return current + end + + return nil + end +end + return table.freeze(fslib) diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index bfc152943..eab5dd309 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -225,6 +225,48 @@ test.suite("FsSuite", function(suite) fs.remove(file) fs.rmdir(dir) end) + + suite:case("walk_directory_recursive", function(assert) + local baseDir = path.join(tmpdir, "walktest") + local subDir = path.join(baseDir, "subdir") + fs.createdirectory(subDir, { makeparents = true }) + + local file1 = path.join(baseDir, "file1.txt") + local f1 = fs.open(file1, "w+") + fs.close(f1) + + local file2 = path.join(subDir, "file2.txt") + local f2 = fs.open(file2, "w+") + fs.close(f2) + + local expectedPaths = { + tostring(baseDir), + tostring(file1), + tostring(subDir), + tostring(file2), + } + local foundFiles = {} + + local it = fs.walk(baseDir, { recursive = true }) + local p = it() + while p do + table.insert(foundFiles, tostring(p)) + p = it() + end + + for _, expected in expectedPaths do + local found = false + for _, foundPath in foundFiles do + if foundPath == expected then + found = true + break + end + end + assert.eq(found, true) + end + + fs.removedirectory(baseDir, { recursive = true }) + end) end) test.run() From 31626ba31ad120e2eff3d81d2db977436077a097 Mon Sep 17 00:00:00 2001 From: ariel Date: Mon, 17 Nov 2025 04:27:06 -0800 Subject: [PATCH 170/642] crypto: add optional key argument for seal in definition files. (#590) In #571, we provided the ability for the secretbox api to use a separately generated key, but we forgot to update the definition file to appropriately describe that part of the interface. --- definitions/crypto.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/definitions/crypto.luau b/definitions/crypto.luau index dad1227d6..b3a37fab3 100644 --- a/definitions/crypto.luau +++ b/definitions/crypto.luau @@ -26,7 +26,7 @@ function crypto.secretbox.keygen(): buffer error("not implemented") end -function crypto.secretbox.seal(message: string | buffer): secretbox +function crypto.secretbox.seal(message: string | buffer, key: buffer?): secretbox error("not implemented") end From fe2f923b77ea13427abd72e5ac5e3db43658f27d Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 17 Nov 2025 13:39:39 -0800 Subject: [PATCH 171/642] stdlib: Fix Lute test assertions (#592) `lute test` wasn't correctly reporting the values from failed `assert.eq` and `assert.neq` values due to a recent refactoring. @~Vighnesh-V authored the fix, and I've completed the PR with some tests in his absence. --------- Co-authored-by: Vighnesh Vijay --- lute/std/libs/test/reporter.luau | 2 +- lute/std/libs/test/runner.luau | 6 +- lute/std/libs/test/types.luau | 2 +- tests/cli/test.test.luau | 104 +++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/lute/std/libs/test/reporter.luau b/lute/std/libs/test/reporter.luau index 11d952ef5..eb36aa5ed 100644 --- a/lute/std/libs/test/reporter.luau +++ b/lute/std/libs/test/reporter.luau @@ -15,7 +15,7 @@ local function printFailedTest(failed: FailedTest) print(`❌ {failed.test}`) if failed.assertion ~= nil then print(` {failed.filename}:{failed.linenumber}`) - print(` {failed.assertion}: {failed.message}`) + print(` {failed.assertion}: {failed.error}`) else print(` Failed with: Runtime error in {failed.test} in:`) print(` {failed.filename}:{failed.linenumber}`) diff --git a/lute/std/libs/test/runner.luau b/lute/std/libs/test/runner.luau index 64291f706..5f7090c25 100644 --- a/lute/std/libs/test/runner.luau +++ b/lute/std/libs/test/runner.luau @@ -3,7 +3,7 @@ -- Standard test runner library for Luau local types = require("./types") -local assert = require("./assert") +local asserts = require("./assert") type Test = types.Test type TestCase = types.TestCase @@ -110,7 +110,7 @@ function runner.run(env: TestEnvironment): TestRunResult failed += 1 end - local success = xpcall(tc.case, handlefailure, assert) + local success = xpcall(tc.case, handlefailure, asserts) if success then passed += 1 end @@ -156,7 +156,7 @@ function runner.run(env: TestEnvironment): TestRunResult end end - local success = xpcall(tc.case, handlefailure, assert) + local success = xpcall(tc.case, handlefailure, asserts) -- Run aftereach hook if it exists (runs even if test failed) if suite._aftereach then local afterSuccess = xpcall(suite._aftereach, afterachErrorHandler(testFullName)) diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index 817d5d55b..243642dd0 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -26,7 +26,7 @@ export type testsuite = { export type failedtest = { test: string, -- name of the test case that failed assertion: string?, -- if an assertion failed, name of the assertion that failed (e.g., "assert.eq") - message: string?, -- the error message to report with a failed assertion + error: string?, -- the error message to report with a failed assertion filename: string, -- source file where the failure occurred linenumber: number, -- line number of the failure } diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 1c7128a93..63a65947e 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -111,6 +111,110 @@ test.run() fs.remove(testFilePath) end) + + suite:case("assert.eq_error_message_in_case", function(assert) + local testFilePath = path.join(tmpDir, "assert_eq_fails.test.luau") + local handle = fs.open(testFilePath, "w+") + fs.write( + handle, + [[ +local test = require("@std/test") + +test.case("eq_error", function(assert) + assert.eq(1, 2) +end) + +test.run() + ]] + ) + fs.close(handle) + + -- Run lute on the created test file + local result = process.run({ lutePath, tostring(testFilePath) }) + + -- Check that the output specifies the inequal values + assert.eq(result.exitcode, 1) + assert.neq(result.stdout:find("eq: 1 ~= 2", 1, true), nil) + end) + + suite:case("assert.eq_error_message_in_suite", function(assert) + local testFilePath = path.join(tmpDir, "assert_eq_fails.test.luau") + local handle = fs.open(testFilePath, "w+") + fs.write( + handle, + [[ +local test = require("@std/test") + +test.suite("eq_failure_suite", function(suite) + suite:case("eq_error", function(assert) + assert.eq(1, 2) + end) +end) + +test.run() + ]] + ) + fs.close(handle) + + -- Run lute on the created test file + local result = process.run({ lutePath, tostring(testFilePath) }) + + -- Check that the output specifies the inequal values + assert.eq(result.exitcode, 1) + assert.neq(result.stdout:find("eq: 1 ~= 2", 1, true), nil) + end) + + suite:case("assert.neq_error_message_in_case", function(assert) + local testFilePath = path.join(tmpDir, "assert_neq_fails.test.luau") + local handle = fs.open(testFilePath, "w+") + fs.write( + handle, + [[ +local test = require("@std/test") + +test.case("neq_error", function(assert) + assert.neq(1, 1) +end) + +test.run() + ]] + ) + fs.close(handle) + + -- Run lute on the created test file + local result = process.run({ lutePath, tostring(testFilePath) }) + + -- Check that the output specifies the inequal values + assert.eq(result.exitcode, 1) + assert.neq(result.stdout:find("neq: 1 == 1", 1, true), nil) + end) + + suite:case("assert.neq_error_message_in_suite", function(assert) + local testFilePath = path.join(tmpDir, "assert_neq_fails.test.luau") + local handle = fs.open(testFilePath, "w+") + fs.write( + handle, + [[ +local test = require("@std/test") + +test.suite("neq_failure_suite", function(suite) + suite:case("neq_error", function(assert) + assert.neq(1, 1) + end) +end) + +test.run() + ]] + ) + fs.close(handle) + + -- Run lute on the created test file + local result = process.run({ lutePath, tostring(testFilePath) }) + + -- Check that the output specifies the inequal values + assert.eq(result.exitcode, 1) + assert.neq(result.stdout:find("neq: 1 == 1", 1, true), nil) + end) end) test.run() From b217954671311048a312a0b8b8206c73e9b61a3c Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 17 Nov 2025 16:13:34 -0800 Subject: [PATCH 172/642] stdlib: query:findall and query:flatmap (#591) Follow up work to AST querying: - Select provides the same functionality as `selectFromRoot`, but rooted on each node contained in the query. - flatMap allows programmers to return array-like tables in their lambdas, which are then flattened and collected to form the next collection of nodes. flatMap also allowed me to write an additional unit test which revealed a bug with how token replacements were being handled. --- examples/query.luau | 10 +- examples/query_transformer.luau | 2 +- lute/std/libs/syntax/printer.luau | 11 +- lute/std/libs/syntax/query.luau | 51 ++++++-- lute/std/libs/syntax/utils/trivia.luau | 4 +- lute/std/libs/syntax/visitor.luau | 2 + tests/std/syntax/printer.test.luau | 155 +++++++++++++++---------- tests/std/syntax/query.test.luau | 115 +++++++++++++++--- 8 files changed, 258 insertions(+), 92 deletions(-) diff --git a/examples/query.luau b/examples/query.luau index 99760385e..68360f8a6 100644 --- a/examples/query.luau +++ b/examples/query.luau @@ -1,11 +1,11 @@ -local luau = require("@std/luau") +local syntax = require("@std/syntax") local query = require("@std/syntax/query") local utils = require("@std/syntax/utils") -local ast = luau.parseexpr("require(OldComponent)") +local ast = syntax.parseexpr("require(OldComponent)") local p = query - .select(ast, utils.isExprCall) + .findallfromroot(ast, utils.isExprCall) :filter(function(call) local calledFunc = call.func return calledFunc.tag == "global" and calledFunc.name.text == "require" @@ -16,8 +16,8 @@ local p = query end) -- Can we improve the type solver here so we don't need to add this annotation? -local argIndexNames = query.map(p, function(call: luau.AstExprCall) - return call.arguments[1].node :: luau.AstExprIndexName +local argIndexNames = query.map(p, function(call: syntax.AstExprCall) + return call.arguments[1].node :: syntax.AstExprIndexName end) local replacements = argIndexNames:replace(function(node) diff --git a/examples/query_transformer.luau b/examples/query_transformer.luau index e50c4382a..41f918e03 100644 --- a/examples/query_transformer.luau +++ b/examples/query_transformer.luau @@ -4,7 +4,7 @@ local syntax_utils = require("@std/syntax/utils") local function transform(ctx) return query - .selectFromRoot(ctx.parseresult.root, syntax_utils.isExprBinary) + .findallfromroot(ctx.parseresult.root, syntax_utils.isExprBinary) :filter(function(bin) return bin.operator.text == "~=" and bin.lhsoperand.tag == "local" diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 1fd61410a..aa185e02b 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -3,7 +3,6 @@ local triviaUtils = require("./utils/trivia") local types = require("./types") -local utils = require("./utils") local visitor = require("./visitor") local printerLib = {} @@ -39,6 +38,11 @@ local function printTriviaList(self: PrintVisitor, trivia: { types.Trivia }) end local function printToken(self: PrintVisitor, token: types.Token) + if self.replacements[token] ~= nil then + self:printReplacement(token, self.replacements[token]) + return + end + self:printTriviaList(token.leadingtrivia) self:write(token.text) self:printTriviaList(token.trailingtrivia) @@ -69,6 +73,11 @@ local function printString(self: PrintVisitor, expr: types.AstExprConstantString end local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprInterpString) + if self.replacements[expr] ~= nil then + self:printReplacement(expr, self.replacements[expr]) + return + end + for i = 1, #expr.strings do self:printTriviaList(expr.strings[i].leadingtrivia) if i == 1 then diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 26eead2f6..68106455a 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -2,6 +2,7 @@ -- @std/syntax/query -- Provides utility functions for querying Luau AST nodes +local tableext = require("@std/tableext") local types = require("./types") local visitor = require("./visitor") local utils = require("./utils") @@ -13,8 +14,10 @@ export type query = { filter: (self: query, pred: (T) -> boolean) -> query, replace: (self: query, repl: (T) -> types.replacement?) -> types.replacements, map: (self: query, fn: (T) -> U?) -> query, -- recursion violation reported - forEach: (self: query, callback: (T) -> ()) -> (), -} -- TODO: Add a select: (self: query, fn: (node) -> U?) -> query method and a flatMap: (self: query, fn: (T) -> { U }) -> query method + foreach: (self: query, callback: (T) -> ()) -> query, + findall: (self: query, fn: (node) -> U?) -> query, + flatmap: (self: query, fn: (T) -> { U }) -> query, +} local queryLib = {} @@ -53,16 +56,14 @@ function queryLib.map(self: query, fn: (T) -> U?): query return self end -function queryLib.forEach(self: query, callback: (T) -> ()): query +function queryLib.foreach(self: query, callback: (T) -> ()): query for _, node in self.nodes do callback(node) end return self end -function queryLib.selectFromRoot(ast: types.ParseResult | node, fn: (node) -> T?): query - local nodes: { T } = {} - +local function newSelectVisitor(nodes: { T }, fn: (node) -> T?): visitor.Visitor local function visit(n: node) -- LUAUFIX: type checker doesn't like assigning visit in the visitor fields with n: node local selected = fn(n) if selected ~= nil then @@ -86,6 +87,40 @@ function queryLib.selectFromRoot(ast: types.ParseResult | node, fn: (node) -> return true end + return selectVisitor +end + +function queryLib.findall(self: query, fn: (node) -> U?): query + local selectedNodes: { U } = {} + + local selectVisitor = newSelectVisitor(selectedNodes, fn) + + for _, node in self.nodes do + visitor.visit(node, selectVisitor) + end + + self.nodes = selectedNodes + + return self +end + +function queryLib.flatmap(self: query, fn: (T) -> { U }): query + local newNodes: { U } = {} + for _, node in self.nodes do + local mapped = fn(node) + if #mapped > 0 then + tableext.extend(newNodes, mapped) + end + end + self.nodes = newNodes + return self +end + +function queryLib.findallfromroot(ast: types.ParseResult | node, fn: (node) -> T?): query + local nodes: { T } = {} + + local selectVisitor = newSelectVisitor(nodes, fn) + local root: node = if ast.root ~= nil then ast.root else ast visitor.visit(root, selectVisitor) @@ -94,7 +129,9 @@ function queryLib.selectFromRoot(ast: types.ParseResult | node, fn: (node) -> filter = queryLib.filter, replace = queryLib.replace, map = queryLib.map, - forEach = queryLib.forEach, + foreach = queryLib.foreach, + findall = queryLib.findall, + flatmap = queryLib.flatmap, } -- LUAUFIX: queryLib.map has generics quantified at a different level than expected end diff --git a/lute/std/libs/syntax/utils/trivia.luau b/lute/std/libs/syntax/utils/trivia.luau index fe6a475d7..68110cb54 100644 --- a/lute/std/libs/syntax/utils/trivia.luau +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -4,7 +4,7 @@ local types = require("../types") local retrieverLib = {} function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } - local q: query.query = query.selectFromRoot(n, function(n) + local q: query.query = query.findallfromroot(n, function(n) return if n.istoken ~= nil and n.istoken then n else nil end) @@ -27,7 +27,7 @@ function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } end function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } - local q: query.query = query.selectFromRoot(n, function(n) + local q: query.query = query.findallfromroot(n, function(n) return if n.istoken ~= nil and n.istoken then n else nil end) diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 39e77e629..b56a22608 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -999,6 +999,8 @@ local function visit(node: types.AstNode, visitor: Visitor) visitLocal(node, visitor) elseif node.kind == "attribute" then visitAttribute(node, visitor) + elseif node.istoken then + visitToken(node, visitor) else exhaustiveMatch(node.kind) end diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 8b961f88a..0dd2a9b78 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -39,7 +39,7 @@ test.suite("syntax_printer", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprConstantString):replace(function(s) + return query.findallfromroot(ast, syntaxUtils.isExprConstantString):replace(function(s) return if s.text == "World" then "1 + 2" else nil end) end, assert) @@ -53,7 +53,7 @@ test.suite("syntax_printer", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprConstantString):replace(function(s) + return query.findallfromroot(ast, syntaxUtils.isExprConstantString):replace(function(s) return if s.text == "World" then syntax.parseexpr("1 + 2") else nil end) end, assert) @@ -67,7 +67,7 @@ test.suite("syntax_printer", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatLocal):replace(function(s) + return query.findallfromroot(ast, syntaxUtils.isStatLocal):replace(function(s) return syntax.parseblock("local name = 1 + 2") end) end, assert) @@ -82,7 +82,7 @@ test.suite("syntax_printer", function(suite) checkReplacement(source, expected, function(ast) return query - .selectFromRoot(ast, syntaxUtils.isStatLocal) + .findallfromroot(ast, syntaxUtils.isStatLocal) :map(function(assign) return assign.variables[1].node.annotation end) @@ -101,7 +101,7 @@ test.suite("syntax_printer", function(suite) checkReplacement(source, expected, function(ast) return query - .selectFromRoot(ast, syntaxUtils.isStatTypeAlias) + .findallfromroot(ast, syntaxUtils.isStatTypeAlias) :map(function(ta) if ta.type.tag == "function" then return ta.type.returntypes @@ -119,7 +119,7 @@ test.suite("syntax_printer", function(suite) local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprGroup):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprGroup):replace(function() return "replaced" end) end, assert) @@ -131,7 +131,7 @@ nil -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprConstantNil):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprConstantNil):replace(function() return "replaced" end) end, assert) @@ -143,7 +143,7 @@ true -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprConstantBool):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprConstantBool):replace(function() return "replaced" end) end, assert) @@ -155,7 +155,7 @@ true -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprConstantNumber):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprConstantNumber):replace(function() return "replaced" end) end, assert) @@ -168,7 +168,7 @@ y -- after]] local expected = "local y = 3\n\t\tlocal x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprLocal):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprLocal):replace(function() return "replaced" end) end, assert) @@ -180,7 +180,7 @@ y -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprGlobal):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprGlobal):replace(function() return "replaced" end) end, assert) @@ -192,7 +192,7 @@ y -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprVarargs):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprVarargs):replace(function() return "replaced" end) end, assert) @@ -204,7 +204,7 @@ print() -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprCall):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprCall):replace(function() return "replaced" end) end, assert) @@ -216,7 +216,7 @@ hello.world -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprIndexName):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprIndexName):replace(function() return "replaced" end) end, assert) @@ -228,7 +228,7 @@ hello['world'] -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprIndexExpr):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprIndexExpr):replace(function() return "replaced" end) end, assert) @@ -240,7 +240,7 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprAnonymousFunction):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprAnonymousFunction):replace(function() return "replaced" end) end, assert) @@ -252,7 +252,7 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprTable):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprTable):replace(function() return "replaced" end) end, assert) @@ -264,7 +264,7 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprUnary):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprUnary):replace(function() return "replaced" end) end, assert) @@ -276,7 +276,7 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprBinary):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprBinary):replace(function() return "replaced" end) end, assert) @@ -288,7 +288,7 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprInterpString):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprInterpString):replace(function() return "replaced" end) end, assert) @@ -300,7 +300,7 @@ y :: number -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprTypeAssertion):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprTypeAssertion):replace(function() return "replaced" end) end, assert) @@ -312,7 +312,7 @@ if true then 'hello' else 'world' -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExprIfElse):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprIfElse):replace(function() return "replaced" end) end, assert) @@ -324,7 +324,7 @@ if true then 'hello' else 'world', z :: string -- after]] local expected = "local x, y =\nreplaced, replaced -- after" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isExpr):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExpr):replace(function() return "replaced" end) end, assert) @@ -337,7 +337,7 @@ local y = 1 local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatBlock):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatBlock):replace(function() return "replaced" end) end, assert) @@ -354,7 +354,7 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatIf):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatIf):replace(function() return "replaced" end) end, assert) @@ -369,7 +369,7 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatWhile):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatWhile):replace(function() return "replaced" end) end, assert) @@ -384,7 +384,7 @@ until true local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatRepeat):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatRepeat):replace(function() return "replaced" end) end, assert) @@ -399,7 +399,7 @@ end local expected = "-- hello\nwhile true do\n\treplaced\nend\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatBreak):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatBreak):replace(function() return "replaced" end) end, assert) @@ -414,7 +414,7 @@ end local expected = "-- hello\nwhile true do\n\treplaced\nend\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatContinue):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatContinue):replace(function() return "replaced" end) end, assert) @@ -427,7 +427,7 @@ function foo() return 1, 2 end local expected = "-- hello\nfunction foo() replaced end\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatReturn):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatReturn):replace(function() return "replaced" end) end, assert) @@ -440,7 +440,7 @@ foobar() local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatExpr):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatExpr):replace(function() return "replaced" end) end, assert) @@ -453,7 +453,7 @@ local x = 1 local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatLocal):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatLocal):replace(function() return "replaced" end) end, assert) @@ -468,7 +468,7 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatFor):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatFor):replace(function() return "replaced" end) end, assert) @@ -483,7 +483,7 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatForIn):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatForIn):replace(function() return "replaced" end) end, assert) @@ -497,7 +497,7 @@ y = 1 local expected = "-- hello\nlocal y = 2\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatAssign):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatAssign):replace(function() return "replaced" end) end, assert) @@ -511,7 +511,7 @@ y -= 1 local expected = "-- hello\nlocal y = 2\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatCompoundAssign):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatCompoundAssign):replace(function() return "replaced" end) end, assert) @@ -526,7 +526,7 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatFunction):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatFunction):replace(function() return "replaced" end) end, assert) @@ -541,7 +541,7 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatLocalFunction):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatLocalFunction):replace(function() return "replaced" end) end, assert) @@ -554,7 +554,7 @@ type foo = number local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatTypeAlias):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatTypeAlias):replace(function() return "replaced" end) end, assert) @@ -567,7 +567,7 @@ type function foo() return number end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStatTypeFunction):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStatTypeFunction):replace(function() return "replaced" end) end, assert) @@ -581,7 +581,7 @@ local y = 2 local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isStat):replace(function() + return query.findallfromroot(ast, syntaxUtils.isStat):replace(function() return "replaced" end) end, assert) @@ -594,7 +594,7 @@ type foo = number local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeReference):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeReference):replace(function() return "replaced" end) end, assert) @@ -607,7 +607,7 @@ type foo = true local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeSingletonBool):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeSingletonBool):replace(function() return "replaced" end) end, assert) @@ -620,7 +620,7 @@ local x : "hello" local expected = "-- hello\nlocal x : replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeSingletonString):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeSingletonString):replace(function() return "replaced" end) end, assert) @@ -633,7 +633,7 @@ type foo = typeof(123) local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeTypeof):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeTypeof):replace(function() return "replaced" end) end, assert) @@ -646,7 +646,7 @@ type foo = (number) local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeGroup):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeGroup):replace(function() return "replaced" end) end, assert) @@ -659,7 +659,7 @@ type foo = true? local expected = "-- hello\ntype foo = truereplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeOptional):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeOptional):replace(function() return "replaced" end) end, assert) @@ -672,7 +672,7 @@ type foo = true? | false local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeUnion):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeUnion):replace(function() return "replaced" end) end, assert) @@ -685,7 +685,7 @@ type foo = true & false local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeIntersection):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeIntersection):replace(function() return "replaced" end) end, assert) @@ -698,7 +698,7 @@ type foo = { number } local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeArray):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeArray):replace(function() return "replaced" end) end, assert) @@ -711,7 +711,7 @@ type foo = {} local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeTable):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeTable):replace(function() return "replaced" end) end, assert) @@ -724,7 +724,7 @@ type foo = (number) -> () local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypeFunction):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypeFunction):replace(function() return "replaced" end) end, assert) @@ -737,7 +737,7 @@ type foo = (number) -> () local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isType):replace(function() + return query.findallfromroot(ast, syntaxUtils.isType):replace(function() return "replaced" end) end, assert) @@ -750,7 +750,7 @@ type foo = (number) -> (number, ...string) local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypePackExplicit):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypePackExplicit):replace(function() return "replaced" end) end, assert) @@ -763,7 +763,7 @@ type foo = (number) -> (number, G...) local expected = "-- hello\ntype foo = (number) -> (number, replaced)\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypePackGeneric):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypePackGeneric):replace(function() return "replaced" end) end, assert) @@ -776,7 +776,7 @@ type foo = (number) -> (...number) local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypePackVariadic):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypePackVariadic):replace(function() return "replaced" end) end, assert) @@ -789,7 +789,7 @@ type foo = (number) -> (number, ...string) local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isTypePack):replace(function() + return query.findallfromroot(ast, syntaxUtils.isTypePack):replace(function() return "replaced" end) end, assert) @@ -802,7 +802,7 @@ local x, y = 1, 2 local expected = "-- hello\nlocal replaced, replaced = 1, 2\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isLocal):replace(function() + return query.findallfromroot(ast, syntaxUtils.isLocal):replace(function() return "replaced" end) end, assert) @@ -816,7 +816,7 @@ function foo() return end local expected = "-- hello\nreplaced\nfunction foo() return end\n-- world" checkReplacement(source, expected, function(ast) - return query.selectFromRoot(ast, syntaxUtils.isAttribute):replace(function() + return query.findallfromroot(ast, syntaxUtils.isAttribute):replace(function() return "replaced" end) end, assert) @@ -833,7 +833,7 @@ function foo() return end checkReplacement(source, expected, function(ast) return query - .selectFromRoot(ast, syntaxUtils.isExprGlobal) + .findallfromroot(ast, syntaxUtils.isExprGlobal) :filter(function(g) return g.name.text == "baz" end) @@ -842,6 +842,41 @@ function foo() return end end) end, assert) end) + + suite:case("comments_in_table", function(assert) + local source = [[local x = { + foo = -- what about this + 1, + bar = 2, + -- and this + + baz = 3 -- and this?? +}]] + + local expected = [[local x = { + replaced = -- what about this + 1, + replaced = 2, + -- and this + + replaced = 3 -- and this?? +}]] + + checkReplacement(source, expected, function(ast) + return query + .findallfromroot(ast, syntaxUtils.isExprTable) + :flatmap(function(t) + local l = {} + for _, entry in t.entries do + table.insert(l, entry.key) + end + return l + end) + :replace(function() + return "replaced" + end) + end, assert) + end) end) test.run() diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index 21195b2ab..11e41040a 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -5,7 +5,7 @@ local utils = require("@std/syntax/utils") test.suite("AST Query", function(suite) suite:case("filter", function(assert) - local testQuery = query.selectFromRoot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local filteredQuery = testQuery:filter(function(n) return n.value > 2 @@ -18,7 +18,7 @@ test.suite("AST Query", function(suite) end) suite:case("replace", function(assert) - local testQuery = query.selectFromRoot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local replacements = testQuery:replace(function(n): string? if n.value > 0 then @@ -45,7 +45,7 @@ test.suite("AST Query", function(suite) end) suite:case("map", function(assert) - local testQuery = query.selectFromRoot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local mappedQuery = query.map(testQuery, function(n: syntax.AstExprConstantNumber): number? if n.value % 2 == 0 then @@ -60,20 +60,20 @@ test.suite("AST Query", function(suite) assert.eq(mappedQuery.nodes[3], 40) end) - suite:case("forEach", function(assert) - local testQuery = query.selectFromRoot(syntax.parse("local _ = {0, 1, 2}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + suite:case("foreach", function(assert) + local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local sum = 0 - testQuery:forEach(function(n: syntax.AstExprConstantNumber) + testQuery:foreach(function(n: syntax.AstExprConstantNumber) sum = sum + n.value end) assert.eq(sum, 3) -- 0 + 1 + 2 = 3 end) - suite:case("select_nums", function(assert) + suite:case("findall_nums", function(assert) local ast = syntax.parse("print(1 + 2)") - local queryResult: query.query = query.selectFromRoot( + local queryResult: query.query = query.findallfromroot( ast, -- LUAUFIX: Complains that AstStatBlock doesn't have location? field function(n: syntax.AstNode): syntax.AstExprConstantNumber? if n.kind == "expr" and n.tag == "number" then @@ -94,10 +94,10 @@ test.suite("AST Query", function(suite) assert.eq((num :: syntax.AstExprConstantNumber).value, 2) end) - suite:case("select_utils", function(assert) + suite:case("findall_utils", function(assert) local ast = syntax.parse("print(1 + 2)") local queryResult: query.query = - query.selectFromRoot(ast, utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + query.findallfromroot(ast, utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field assert.eq(#queryResult.nodes, 2) local num: syntax.AstExpr = queryResult.nodes[1] @@ -110,21 +110,104 @@ test.suite("AST Query", function(suite) assert.eq((num :: syntax.AstExprConstantNumber).value, 2) end) - suite:case("select_tokens", function(assert) + suite:case("findall_tokens", function(assert) local ast = syntax.parse("local x = 1 or nil") -- verify that query doesn't doubly count tokens - local queryResult = query.selectFromRoot(ast, utils.isToken) + local queryResult = query.findallfromroot(ast, utils.isToken) assert.eq(#queryResult.nodes, 6) - queryResult:forEach(function(node) + queryResult:foreach(function(node) assert.eq(node.istoken, true) end) - local constNums = query.selectFromRoot(ast, utils.isExprConstantNumber) + local constNums = query.findallfromroot(ast, utils.isExprConstantNumber) assert.eq(#constNums.nodes, 1) - local constNils = query.selectFromRoot(ast, utils.isExprConstantNil) + local constNils = query.findallfromroot(ast, utils.isExprConstantNil) assert.eq(#constNils.nodes, 1) - local baseTokens = query.selectFromRoot(ast, utils.isBaseToken) + local baseTokens = query.findallfromroot(ast, utils.isBaseToken) assert.eq(#baseTokens.nodes, 4) end) + + suite:case("findall", function(assert) + -- Test selecting from a query of binary expressions to find their operands + local ast = syntax.parse("local x = 1 + 2") + local nums = query.findallfromroot(ast, utils.isExprBinary):findall(utils.isExprConstantNumber) + + assert.eq(#nums.nodes, 2) + assert.eq(nums.nodes[1].value, 1) + assert.eq(nums.nodes[2].value, 2) + end) + + suite:case("findall", function(assert) + -- Test selecting through nested structures + local ast = syntax.parse("local x = {a = 1 + 2, b = 3 + 4}") + local tables = query.findallfromroot(ast, utils.isExprTable) + + -- Select all binary expressions within the table + local binaryExprs = tables:findall(utils.isExprBinary) + + assert.eq(#binaryExprs.nodes, 2) + + -- Now select all numbers from those binary expressions + local numbers = binaryExprs:findall(utils.isExprConstantNumber) + + assert.eq(#numbers.nodes, 4) + assert.eq(numbers.nodes[1].value, 1) + assert.eq(numbers.nodes[2].value, 2) + assert.eq(numbers.nodes[3].value, 3) + assert.eq(numbers.nodes[4].value, 4) + end) + + suite:case("flatmap", function(assert) + -- Test flatmap to collect multiple items from each node + local ast = syntax.parse("local x = 1 + 2; local y = 3 + 4") + local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l) + local values = {} + for _, v in l.values do + table.insert(values, v.node) + end + return values + end) + + assert.eq(#locals.nodes, 2) -- Two binary expressions + assert.eq(locals.nodes[1].kind, "expr") + assert.eq(locals.nodes[1].tag, "binary") + assert.eq(locals.nodes[2].kind, "expr") + assert.eq(locals.nodes[2].tag, "binary") + end) + + suite:case("flatmap_empty", function(assert) + -- Test flatmap with some nodes returning empty arrays + local ast = syntax.parse("local x; local y = 1; local z") + local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l) + local values = {} + for _, v in l.values do + table.insert(values, v.node) + end + return values + end) + + assert.eq(#locals.nodes, 1) -- Only 'local y = 1' has a value + assert.eq(locals.nodes[1].kind, "expr") + assert.eq(locals.nodes[1].tag, "number") + assert.eq((locals.nodes[1] :: syntax.AstExprConstantNumber).value, 1) + end) + + suite:case("flatmap_multiple", function(assert) + -- Test flatmap with nodes that return multiple items + local ast = syntax.parse("local a, b = 1, 2; local c, d = 3, 4, 5") + local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l: syntax.AstStatLocal) + local vars = {} + for _, v in l.variables do + table.insert(vars, v.node) + end + return vars + end) + + assert.eq(#locals.nodes, 4) -- a, b, c, d + assert.eq(locals.nodes[1].name.text, "a") + assert.eq(locals.nodes[2].name.text, "b") + assert.eq(locals.nodes[3].name.text, "c") + assert.eq(locals.nodes[4].name.text, "d") + end) end) test.run() From 946a6912a1fc01eb69b7c5fe9bf5ee4ba15b29c2 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:23:38 -0500 Subject: [PATCH 173/642] std/tableext: implement `combine`, `foreach`, and `keys` (#584) This PR adds two methods to `tableext`: - `combine`: merges keyed tables, with keys in last-passed tables taking precedence - `foreach`: runs callback on each item in table - `keys`: returns array-like of all the keys in the passed table The PR also modifies the `extend` signature, to accept a variable number of extendee tables --------- Co-authored-by: ariel --- lute/std/libs/tableext.luau | 35 ++++++++++++++++++++++++++++++++--- tests/std/tableext.test.luau | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau index e48fa268a..1062f18ba 100644 --- a/lute/std/libs/tableext.luau +++ b/lute/std/libs/tableext.luau @@ -40,10 +40,39 @@ function tableext.fold(table: { [K]: V }, f: (A, V) -> A, initial: A): return acc end -function tableext.extend(tbl: array, other: array) - for _, entry in other do - table.insert(tbl, entry) +function tableext.extend(tbl: array, ...: array) + local extensions = table.pack(...) + extensions.n = nil :: any + for _, extension in extensions do + for _, entry in extension do + table.insert(tbl, entry) + end + end +end + +function tableext.combine(tbl: { [K]: V }, ...: { [K]: V }): { [K]: V } + local tables = table.pack(...) + tables.n = nil :: any + for _, tocombine in tables do + for key, val in tocombine do + tbl[key] = val + end + end + return tbl +end + +function tableext.foreach(tbl: { [K]: V }, f: (K, V) -> ()) + for k, v in tbl do + f(k, v) + end +end + +function tableext.keys(tbl: { [K]: V }): { K } + local keys = {} + for k, _ in tbl do + table.insert(keys, k) end + return keys end return table.freeze(tableext) diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index ea615de10..b1e43872e 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -30,10 +30,42 @@ test.suite("TableExt", function(suite) suite:case("extend", function(assert) local a: { number } = { 1, 2 } local b: { number } = { 3, 4 } - tableext.extend(a, b) - assert.eq(#a, 4) + local c: { number } = { 5, 6 } + tableext.extend(a, b, c) + assert.eq(#a, 6) assert.eq(a[3], 3) assert.eq(a[4], 4) + assert.eq(a[5], 5) + assert.eq(a[6], 6) + end) + + suite:case("combine", function(assert) + local a = { ["a"] = 1, ["b"] = 2 } + local b = { ["b"] = 3, ["c"] = 4 } + local c = { ["c"] = 1, ["d"] = 5 } + tableext.combine(a, b, c) + assert.eq(a["a"], 1) + assert.eq(a["b"], 3) + assert.eq(a["c"], 1) + assert.eq(a["d"], 5) + end) + + suite:case("foreach", function(assert) + local a = { 1, 2, 3 } + local numCalls = 0 + local function callback() + numCalls += 1 + end + tableext.foreach(a, callback) + assert.eq(numCalls, #a) + end) + + suite:case("keys", function(assert) + local a = { a = 1, b = 2, c = 3 } + local keys = tableext.keys(a) + for _, key in keys do + assert.neq(a[key], nil) + end end) end) From d2179be99590f246a5f6fb35cbc486d1ce25336d Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:01:11 -0800 Subject: [PATCH 174/642] docs: support doc comments in lute doc gen (#589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ‼️ disclaimer - this code is pretty gross and is 100% just an intermediate step in the Luau doc gen plan while I'm still working on the implementation. This entire file should be deleted when we have implementations for parsing a module's return type and generating docs from that instead of this line-by-line pattern matching 😃👍 ‼️ Adding support for adding all sorts of doc comments to functions that are captured by the doc gen script for https://lute.luau.org/! Only detects comments that are right above the function signature, but you can write - [single line comments](https://create.roblox.com/docs/luau/comments#single-line-comments) `---` or - [block comments](https://create.roblox.com/docs/luau/comments#block-comments) with `--[=[ ]=]` ^this is because we don't have a standardized way of writing doc comments yet in Luau, so I'm just supporting these two for now There is a mock module file and output documentation file in `docs/test/` that you can see what the doc gen script `reference.luau` generates! They're the `test_module.luau` and `test_module.md` files. --- docs/scripts/reference.luau | 339 ++++++++++++++++++++++-------------- docs/test/test_module.luau | 40 +++++ docs/test/test_module.md | 43 +++++ 3 files changed, 290 insertions(+), 132 deletions(-) create mode 100644 docs/test/test_module.luau create mode 100755 docs/test/test_module.md diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 83ef654b0..1a9852d59 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -1,59 +1,176 @@ local fs = require("@std/fs") local process = require("@std/process") local path = require("@std/path") +local stringext = require("@std/stringext") -local function extractPropertySignature(module: string, line: string): (string?, string?) - -- Match property declarations like: process.env = {} :: { [string]: string } +local STDLIB_MODULE_ALIASES = { + assert = "assertions", + fs = "fslib", + io = "iolib", + process = "processlib", + task = "tasklib", + path = "pathlib", + system = "systemlib", + trivia = "retrieverLib", + visitor = "visitorlib", + query = "queryLib", + printer = "printerLib", +} + +local tripleDashCommentPattern = "^%s*%-%-%-%s?(.*)$" +local blockCommentStartPattern = "^%s*%-%-%[(=+)%[" +local whitespacePattern = "^%s*$" + +type PropertySignature = { + name: string?, + signature: string?, + doc: string?, + pendingDoc: { string }, + inBlock: boolean, + blockEq: string?, +} + +-- extractPropertySignature: (this function runs on one line at a time so we need to keep some states for block comments) +-- module (module name) +-- line (current line) +-- pendingDoc (accumulated doc lines so far) +-- inBlock (are we currently inside a block comment?) +-- blockEq (the "=" sequence used for block comment delimiting, e.g. "=" for --[=[ ; nil when not inside) +local function extractPropertySignature( + module: string, + line: string, + pendingDoc: { string }, + inBlock: boolean, + blockEq: string? +): PropertySignature + -- 1) If we're *not* in a block, detect block start that may include = signs: + if not inBlock then + local eq = string.match(line, blockCommentStartPattern) + if eq ~= nil then + -- start of block comment, capture part after the opening delimiter, if any + local after = string.match(line, "^%s*%-%-%[" .. eq .. "%[(.*)$") + if after then + after = stringext.trim(after) + if after ~= "" then + table.insert(pendingDoc, after) + end + end + + return { + pendingDoc = pendingDoc, + inBlock = true, + blockEq = eq, -- remember the = count for closing pattern + } + end + end + + -- 2) If we're inside a block, accumulate until we find the matching close + if inBlock then + local eq = blockEq or "" + -- find the closing pattern "]=]" that matches the # of "=" used in opening + local closingPattern = "%]" .. eq .. "%]" + local before = string.match(line, "^(.-)" .. closingPattern) + if before then + -- closing found on this line + before = stringext.trim(before) + if before ~= "" then + table.insert(pendingDoc, before) + end + + -- there might be trailing comment markers like "]]" on the same line; + -- we've already captured inner content, so we just exit block mode. + inBlock = false + blockEq = nil + else + -- still inside block: append whole line (trimmed) + local trimmed = stringext.trim(line) + if trimmed ~= "" then + table.insert(pendingDoc, trimmed) + end + end + + return { + pendingDoc = pendingDoc, + inBlock = inBlock, + blockEq = blockEq, + } + end + + -- 3) Triple dash comment? (---) + local triple = string.match(line, tripleDashCommentPattern) + if triple then + local txt = stringext.trim(triple) + if txt ~= "" then + table.insert(pendingDoc, txt) + end + return { + pendingDoc = pendingDoc, + inBlock = inBlock, + blockEq = blockEq, + } + end + + -- 4) Match property declarations like: process.env = {} :: { [string]: string } local propName, propType = string.match(line, `^{module}%.([%w_]+)%s*=%s*.-::%s*(.+)$`) if propName and propType then - return propName, propType + return { + name = propName, + signature = propType, + doc = if #pendingDoc > 0 then table.concat(pendingDoc, "\n\n") else nil, + pendingDoc = {}, + inBlock = inBlock, + blockEq = blockEq, + } end - -- Match function declarations like: function crypto.password.hash(password: string): buffer + -- 5) Match function declarations like: function crypto.password.hash(password: string): buffer local fullName, params, _return, returnType = string.match(line, `^function%s+{module}%.(.-)(%b())(:?)%s*(.*)$`) if fullName then fullName = string.match(fullName, "^(.*)%b<>$") or fullName returnType = if returnType == "" then "()" else returnType - local signature = `{params} -> {returnType}` - return fullName, signature - end - - return nil, nil -end - --- docs for definitions/*.luau -- -local function parseDefinitionFile( - module: path.pathlike, - filePath: path.pathlike -): { { name: string, signature: string } } - local content = fs.readfiletostring(filePath) - local lines = string.split(content, "\n") - local definitions = {} - - for _, line in lines do - local name, signature = extractPropertySignature(tostring(module), line) + return { + name = fullName, + signature = `{params} -> {returnType}`, + doc = if #pendingDoc > 0 then table.concat(pendingDoc, "\n\n") else nil, + pendingDoc = {}, + inBlock = inBlock, + blockEq = blockEq, + } + end - if name and signature then - table.insert(definitions, { - name = name, - signature = signature, - }) - end + -- 6) If we reach here and there's any non-whitespace (i.e., code), reset pendingDoc to clear old comments + if not string.match(line, whitespacePattern) then + pendingDoc = {} end - return definitions + -- nothing found on this line + return { + pendingDoc = pendingDoc, + inBlock = inBlock, + blockEq = blockEq, + } end -local function generateDefinitionsMarkdown( +local function generateMarkdown( moduleName: string, - definitions: { { name: string, signature: string } } + definitions: { { name: string, signature: string, doc: string? } }, + filePath: path.pathlike, + libraryPath: path.pathlike, + requireLibraryAlias: string ): string + local libPathStr = tostring(libraryPath) + local filePathStr = tostring(filePath) + + local relativePath = string.sub(filePathStr, #libPathStr + 2) -- gets the path relative to the require path (only relevant for subdirs like std/libs) + relativePath = string.gsub(relativePath, "%.luau$", "") + local requirePath = string.gsub(relativePath, "/init$", "") -- remove trailing /init and use the module dir name + local lines = { `# {moduleName}`, "", "```luau", - `local {moduleName} = require("@lute/{moduleName}")`, + `local {moduleName} = require("@{requireLibraryAlias}/{requirePath}")`, "```", "", } @@ -63,7 +180,12 @@ local function generateDefinitionsMarkdown( end) for _, def in definitions do - table.insert(lines, `## {moduleName}.{def.name}`) + table.insert(lines, `## {moduleName}.{def.name}\n`) + + -- Insert the documentation if it exists + if def.doc then + table.insert(lines, `{def.doc}\n`) + end table.insert(lines, "```luau") table.insert(lines, def.signature) table.insert(lines, "```") @@ -73,138 +195,91 @@ local function generateDefinitionsMarkdown( return table.concat(lines, "\n") end -local function processDefinitionsDir(definitionsPath: path.pathlike, referencePath: path.pathlike) - fs.createdirectory(referencePath, { makeparents = true }) - - local definitionFiles = fs.listdir(definitionsPath) - - for _, file in definitionFiles do - if file.type == "file" and string.find(file.name, "%.luau$") then - local moduleName = string.gsub(file.name, "%.luau$", "") - local definitionFilePath = path.join(definitionsPath, file.name) - local referenceFilePath = path.join(referencePath, moduleName .. ".md") - - print(`Processing {moduleName}...`) - - local definitions = parseDefinitionFile(moduleName, definitionFilePath) - local markdown = generateDefinitionsMarkdown(moduleName, definitions) - - fs.writestringtofile(referenceFilePath, markdown) - - print(`Generated {tostring(referenceFilePath)}`) - end - end -end - --- docs for lute/std/libs/*.luau -- - -local STDLIB_MODULE_ALIASES = { - assert = "assertions", - fs = "fslib", - io = "iolib", - process = "processlib", - task = "tasklib", - path = "pathlib", - system = "systemlib", -} - -local function parseStdlibFile(module: path.pathlike, filePath: path.pathlike): { { name: string, signature: string } } +local function parseModule( + module: path.pathlike, + filePath: path.pathlike +): { { name: string, signature: string, doc: string? } } local content = fs.readfiletostring(filePath) local lines = string.split(content, "\n") - local stdlibDefinitions = {} - for _, line in lines do - local mod = STDLIB_MODULE_ALIASES[tostring(module)] or tostring(module) - - local name, signature = extractPropertySignature(mod, line) + local moduleDefinitions = {} + local pendingDoc = {} + local inBlock = false + local blockEq = nil - if name and signature then - table.insert(stdlibDefinitions, { - name = name, - signature = signature, + for _, line in lines do + local ps = extractPropertySignature(tostring(module), line, pendingDoc, inBlock, blockEq) + + -- update state carried across lines + pendingDoc = ps.pendingDoc + inBlock = ps.inBlock + blockEq = ps.blockEq + + if ps.name and ps.signature then + table.insert(moduleDefinitions, { + name = ps.name, + signature = ps.signature, + doc = ps.doc, }) end end - return stdlibDefinitions -end - -local function generateStdLibMarkdown( - moduleName: string, - definitions: { { name: string, signature: string } }, - stdlibFilePath: path.pathlike -): string - local referencePath = path.join(process.cwd(), "..", "lute", "std", "libs") - - local refPathStr = tostring(referencePath) - local stdlibFilePathStr = tostring(stdlibFilePath) - - local relativePath = string.sub(stdlibFilePathStr, #refPathStr + 2) -- gets the path relative to std/libs for the require path - relativePath = string.gsub(relativePath, "%.luau$", "") - local requirePath = string.gsub(relativePath, "/init$", "") - - local lines = { - `# {moduleName}`, - "", - "```luau", - `local {moduleName} = require("@std/{requirePath}")`, - "```", - "", - } - - table.sort(definitions, function(a, b) - return a.name < b.name - end) - - for _, def in definitions do - table.insert(lines, `## {moduleName}.{def.name}`) - table.insert(lines, "```luau") - table.insert(lines, def.signature) - table.insert(lines, "```") - table.insert(lines, "") - end - - return table.concat(lines, "\n") + return moduleDefinitions end -local function processStdlibDir(sourceDir: path.pathlike, referenceDir: path.pathlike) - fs.createdirectory(referenceDir, { makeparents = true }) +local function processDirectory( + modulePath: path.pathlike, + documentationPath: path.pathlike, + libraryPath: path.pathlike, + requireLibraryAlias: string +) + fs.createdirectory(documentationPath, { makeparents = true }) - local entries = fs.listdir(sourceDir) + local entries = fs.listdir(modulePath) for _, entry in entries do - local sourcePath = path.join(sourceDir, entry.name) - local referencePath = path.join(referenceDir, entry.name) + local entryPath = path.join(modulePath, entry.name) + local docPath = path.join(documentationPath, entry.name) if entry.type == "dir" then - fs.createdirectory(referencePath, { makeparents = true }) - processStdlibDir(sourcePath, referencePath) + fs.createdirectory(docPath, { makeparents = true }) + processDirectory(entryPath, docPath, libraryPath, requireLibraryAlias) elseif entry.type == "file" and string.find(entry.name, "%.luau$") then local moduleName = string.gsub(entry.name, "%.luau$", "") if moduleName == "init" then - moduleName = string.match(tostring(sourceDir), "([^/\\]+)$") or moduleName + moduleName = string.match(tostring(modulePath), "([^/\\]+)$") or moduleName end - local referenceFilePath = path.join(referenceDir, moduleName .. ".md") + local documentationFilePath = path.join(documentationPath, moduleName .. ".md") print(`Processing {moduleName}...`) - local stdlibDefinitions = parseStdlibFile(moduleName, sourcePath) - local markdown = generateStdLibMarkdown(moduleName, stdlibDefinitions, sourcePath) + if requireLibraryAlias == "std" then + moduleName = STDLIB_MODULE_ALIASES[moduleName] or moduleName + end + + local definitions = parseModule(moduleName, entryPath) + local markdown = generateMarkdown(moduleName, definitions, entryPath, libraryPath, requireLibraryAlias) - fs.writestringtofile(referenceFilePath, markdown) + fs.writestringtofile(documentationFilePath, markdown) - print(`Generated {tostring(referenceFilePath)}`) + print(`Generated {tostring(documentationFilePath)}`) end end end local definitionsPath = path.join(process.cwd(), "..", "definitions") local referencePath = path.join(process.cwd(), "reference", "definitions") -processDefinitionsDir(definitionsPath, referencePath) +processDirectory(definitionsPath, referencePath, definitionsPath, "lute") local stdlibPath = path.join(process.cwd(), "..", "lute", "std", "libs") local stdlibReferencePath = path.join(process.cwd(), "reference", "std") -processStdlibDir(stdlibPath, stdlibReferencePath) +processDirectory(stdlibPath, stdlibReferencePath, stdlibPath, "std") + +-- Mock module in test/ for testing purposes + +-- local testPath = path.join(process.cwd(), "test") +-- local testReferencePath = path.join(process.cwd(), "test") +-- processDirectory(testPath, testReferencePath, testPath, "test") print("Reference documentation generation complete!") diff --git a/docs/test/test_module.luau b/docs/test/test_module.luau new file mode 100644 index 000000000..3d35c1b10 --- /dev/null +++ b/docs/test/test_module.luau @@ -0,0 +1,40 @@ +-- This file creates a mock module called test_module with random mock functions and different types of comments to test the reference doc generator reference.luau +-- See output documentation file generated in this folder `test_module.md` + +local test_module = {} + +--- Property declaration test: declare process.env as a map from string to string +test_module.property = test_module.property or {} :: { [string]: string } +test_module.property["EXAMPLE_VAR"] = "value" + +--- Simple function with triple dashes comment that returns hello +function test_module.triple_dash(name: string): string + return `Hello, {name}!` +end + +--- Multiple triple-dash comments (---) above function declaration +--- Should capture both of these lines + +--- Even if there are blank lines between them +function test_module.multiple_triple_dashes(y: number): number + return y + 1 +end + +-- this comment should not be captured because it uses double dashes + +--[[ + This block comment is also not captured because there are no = signs. +]] + +--- this should also not be captured because there is non function or property code +local ignore = "value" + +--[=[ + Block comment with equals above function declaration + This tests nested bracket delimiters being recognized properly. +]=] +function test_module.block_with_equals(name: string): string + return "This is a nested bracket block comment: " .. name +end + +return table.freeze(test_module) diff --git a/docs/test/test_module.md b/docs/test/test_module.md new file mode 100755 index 000000000..5d688c460 --- /dev/null +++ b/docs/test/test_module.md @@ -0,0 +1,43 @@ +# test_module + +```luau +local test_module = require("@test/test_module") +``` + +## test_module.block_with_equals + +Block comment with equals above function declaration + +This tests nested bracket delimiters being recognized properly. + +```luau +(name: string) -> string +``` + +## test_module.multiple_triple_dashes + +Multiple triple-dash comments (---) above function declaration + +Should capture both of these lines + +Even if there are blank lines between them + +```luau +(y: number) -> number +``` + +## test_module.property + +Property declaration test: declare process.env as a map from string to string + +```luau +{ [string]: string } +``` + +## test_module.triple_dash + +Simple function with triple dashes comment that returns hello + +```luau +(name: string) -> string +``` From c1b25b4400e5d1784ef0fceb4cadfa0fd449eea8 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Tue, 18 Nov 2025 19:12:18 -0500 Subject: [PATCH 175/642] std/syntax: `visitor.visit` should support table items (`luau.AstExprTableItem`) (#594) `visitor.visit` will currently fallback to `exhaustiveMatch` and error if passed an `AstExprTableItem` node. This causes `printnode` to fail if passed a `replacements` map containing any `AstExprTableItem` node, preventing table entries from being replaced. You can minimally repro this with: ```luau local printer = require("@std/syntax/printer") local parser = require("@std/syntax/parser") local query = require("@std/syntax/query") local utils = require("@std/syntax/utils") local demo = [[ local tbl = { a = 1, } ]] local ast = parser.parseblock(demo) local replacements = {} local tbl = query.selectFromRoot(ast, utils.isExprTable):forEach(function(n) for i, entry in n.entries do replacements[entry] = `a{i} = "hello",` end end) print(printer.printnode(ast, replacements)) ``` This PR ensures `visitor.visit` supports table entries and that we can replace them in codemods --- definitions/luau.luau | 4 +++- lute/luau/src/luau.cpp | 15 ++++++++++++--- lute/std/libs/syntax/utils/init.luau | 4 ++++ lute/std/libs/syntax/visitor.luau | 2 ++ tests/std/syntax/printer.test.luau | 16 ++++++++++++++++ 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index d726c2fde..bdb2c2dd2 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -135,7 +135,7 @@ export type AstExprAnonymousFunction = { } -- helper types for items contained in an AstExprTable, not actually AstExprs themselves -export type AstExprTableItemList = { kind: "list", value: AstExpr, separator: Token<"," | ";">? } +export type AstExprTableItemList = { kind: "list", value: AstExpr, separator: Token<"," | ";">?, istableitem: true } export type AstExprTableItemRecord = { kind: "record", @@ -143,6 +143,7 @@ export type AstExprTableItemRecord = { equals: Token<"=">, value: AstExpr, separator: Token<"," | ";">?, + istableitem: true, } export type AstExprTableItemGeneral = { @@ -153,6 +154,7 @@ export type AstExprTableItemGeneral = { equals: Token<"=">, value: AstExpr, separator: Token<"," | ";">?, + istableitem: true, } export type AstExprTableItem = AstExprTableItemList | AstExprTableItemRecord | AstExprTableItemGeneral diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index dd5cf9656..4ee0a4a10 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -371,19 +371,25 @@ struct AstSerialize : public Luau::AstVisitor if (item.kind == Luau::AstExprTable::Item::List) { - lua_createtable(L, 0, 3); + lua_createtable(L, 0, 4); lua_pushstring(L, "list"); lua_setfield(L, -2, "kind"); + lua_pushboolean(L, 1); + lua_setfield(L, -2, "istableitem"); + visit(item.value); lua_setfield(L, -2, "value"); } else if (item.kind == Luau::AstExprTable::Item::Record) { - lua_createtable(L, 0, 5); + lua_createtable(L, 0, 6); lua_pushstring(L, "record"); lua_setfield(L, -2, "kind"); + lua_pushboolean(L, 1); + lua_setfield(L, -2, "istableitem"); + const auto& value = item.key->as()->value; serializeToken(item.key->location.begin, std::string(value.data, value.size).data()); lua_setfield(L, -2, "key"); @@ -397,10 +403,13 @@ struct AstSerialize : public Luau::AstVisitor } else if (item.kind == Luau::AstExprTable::Item::General) { - lua_createtable(L, 0, 7); + lua_createtable(L, 0, 8); lua_pushstring(L, "general"); lua_setfield(L, -2, "kind"); + lua_pushboolean(L, 1); + lua_setfield(L, -2, "istableitem"); + LUAU_ASSERT(cstNode->indexerOpenPosition); serializeToken(*cstNode->indexerOpenPosition, "["); lua_setfield(L, -2, "indexeropen"); diff --git a/lute/std/libs/syntax/utils/init.luau b/lute/std/libs/syntax/utils/init.luau index ba813250f..1518450b1 100644 --- a/lute/std/libs/syntax/utils/init.luau +++ b/lute/std/libs/syntax/utils/init.luau @@ -58,6 +58,10 @@ function utils.isExprTable(n: types.AstNode): types.AstExprTable? return if n.kind == "expr" and n.tag == "table" then n else nil end +function utils.isExprTableItem(n: types.AstNode): types.AstExprTableItem? + return if n.istableitem then n else nil +end + function utils.isExprUnary(n: types.AstNode): types.AstExprUnary? return if n.kind == "expr" and n.tag == "unary" then n else nil end diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index b56a22608..e228a6903 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -1001,6 +1001,8 @@ local function visit(node: types.AstNode, visitor: Visitor) visitAttribute(node, visitor) elseif node.istoken then visitToken(node, visitor) + elseif node.istableitem then + visitTableItem(node, visitor) else exhaustiveMatch(node.kind) end diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 0dd2a9b78..ef9cc08ca 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -113,6 +113,22 @@ test.suite("syntax_printer", function(suite) end, assert) end) + suite:case("expr_table_item_replacement", function(assert) + local source = "local tbl = { a = 1, }" + local expected = "local tbl = { b = 2, }" + + checkReplacement(source, expected, function(ast) + return query + .findallfromroot(ast, syntaxUtils.isExprTable) + :map(function(node) + return node.entries[1] + end) + :replace(function(entry) + return "b = 2," + end) + end, assert) + end) + suite:case("exprgroup_trivia", function(assert) local source = [[local x = (1 + 2) -- after]] From ea169e7499c2156f067a15d2549c8af2983cc73e Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:14:07 -0800 Subject: [PATCH 176/642] fix: exclude `docs/test/` from doc gen and fix file naming (#596) `test/` folder was being published to the site and the filenames were the internal table variable names (i.e. `printerlib`) instead of the intended module names (i.e. `printer`) --- docs/.vitepress/config.mts | 1 + docs/scripts/reference.luau | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 82142c258..57e05a37d 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -16,6 +16,7 @@ export default withSidebar( { icon: 'github', link: 'https://github.com/luau-lang/lute' }, ], }, + srcExclude: ['/test/**' ], }), { // ============ [ SIDEBAR OPTIONS ] ============ diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 1a9852d59..7dbee7f07 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -254,11 +254,11 @@ local function processDirectory( print(`Processing {moduleName}...`) - if requireLibraryAlias == "std" then - moduleName = STDLIB_MODULE_ALIASES[moduleName] or moduleName - end + local moduleAlias = if requireLibraryAlias == "std" + then STDLIB_MODULE_ALIASES[moduleName] or moduleName + else moduleName - local definitions = parseModule(moduleName, entryPath) + local definitions = parseModule(moduleAlias, entryPath) local markdown = generateMarkdown(moduleName, definitions, entryPath, libraryPath, requireLibraryAlias) fs.writestringtofile(documentationFilePath, markdown) From 9bec3e9200f117c5b0d43792daf03968cf0be3b8 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:00:53 -0800 Subject: [PATCH 177/642] fix: correctly exclude test folder for docgen + require build-docs-site success in CI to merge (#598) Apparently vitepress sidebar extension shadows the vitepress exclude directories somehow, so fixing that and making sure `build-docs-site` CI test needs to succeed before github can merge I tested it correctly this time and `test/test_module.md` does not get generated on the sidebar image --- .github/workflows/ci.yml | 4 ++-- docs/.vitepress/config.mts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddd0928f9..944c7e707 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,12 +134,12 @@ jobs: aggregator: name: Gated Commits runs-on: ubuntu-latest - needs: [run-lutecli-luau-tests, run-lute-cxx-unittests, check-format] + needs: [run-lutecli-luau-tests, run-lute-cxx-unittests, check-format, build-docs-site] if: ${{ always() }} steps: - name: Aggregator run: | - if [ "${{ needs.run-lutecli-luau-tests.result }}" == "success" ] && [ "${{ needs.run-lute-cxx-unittests.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ]; then + if [ "${{ needs.run-lutecli-luau-tests.result }}" == "success" ] && [ "${{ needs.run-lute-cxx-unittests.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ] && [ "${{ needs.build-docs-site.result }}" == "success" ]; then echo "All jobs succeeded" else echo "One or more jobs failed" diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 57e05a37d..1ca1c6f54 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -16,7 +16,6 @@ export default withSidebar( { icon: 'github', link: 'https://github.com/luau-lang/lute' }, ], }, - srcExclude: ['/test/**' ], }), { // ============ [ SIDEBAR OPTIONS ] ============ @@ -26,5 +25,6 @@ export default withSidebar( hyphenToSpace: true, underscoreToSpace: true, sortMenusByName: true, + excludeByGlobPattern: ['**/test/**'], } ) From 0becd2770346fda88afcfdd8952af1c21f307466 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:11:59 -0800 Subject: [PATCH 178/642] docs: change sidebar naming from "definitions" to "lute" (#599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 😊 --- docs/.vitepress/config.mts | 2 +- docs/index.md | 2 +- docs/scripts/reference.luau | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 1ca1c6f54..d6e99a05d 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -9,7 +9,7 @@ export default withSidebar( themeConfig: { nav: [ { text: 'Guide', link: '/guide/installation' }, - { text: 'Reference', link: '/reference/definitions/crypto' }, + { text: 'Reference', link: '/reference/lute/crypto' }, ], search: { provider: 'local' }, socialLinks: [ diff --git a/docs/index.md b/docs/index.md index f88023c4c..2ae69a9d1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ hero: link: /guide/installation - theme: alt text: Reference - link: /reference/definitions/crypto + link: /reference/lute/crypto # TODO: add buzz words # features: diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 7dbee7f07..83a552457 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -269,7 +269,7 @@ local function processDirectory( end local definitionsPath = path.join(process.cwd(), "..", "definitions") -local referencePath = path.join(process.cwd(), "reference", "definitions") +local referencePath = path.join(process.cwd(), "reference", "lute") processDirectory(definitionsPath, referencePath, definitionsPath, "lute") local stdlibPath = path.join(process.cwd(), "..", "lute", "std", "libs") From eba310dc6e640bc5670371e9d43e27230c452d58 Mon Sep 17 00:00:00 2001 From: ariel Date: Tue, 18 Nov 2025 17:14:37 -0800 Subject: [PATCH 179/642] lute/luau: implement a `span` userdata to replace serializing location and position as tables. (#595) Resolves #572 by replacing the location and position tables with a single userdata, `span`, that should be defined as: ```luau declare extern type span with beginline: number, begincolumn: number, endline: number, endcolumn: number end ``` This includes a constructor `span.create` that takes a table so that you can use punned syntax to construct a span easily, e.g. ```luau local location = span.create { beginline = 5, begincolumn = 6, endline = 5, endcolumn = 7, } ``` My informal measurements showed a 30-40% speedup, but we should probably try to put together more formal benchmarks for parsing in a follow-up work before we do any direct optimization of parsing. --------- Co-authored-by: Sora Kanosue --- definitions/luau.luau | 32 +++-- examples/transformer.luau | 40 ++++-- lute/luau/include/lute/luau.h | 8 ++ lute/luau/src/luau.cpp | 183 +++++++++++++++++++------ lute/runtime/include/lute/userdatas.h | 1 + lute/std/libs/syntax/init.luau | 11 +- lute/std/libs/syntax/types.luau | 3 +- lute/std/libs/syntax/utils/trivia.luau | 12 +- tests/std/syntax/parser.test.luau | 22 +-- 9 files changed, 216 insertions(+), 96 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index bdb2c2dd2..deb398818 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -1,30 +1,34 @@ local luau = {} -export type Position = { - line: number, - column: number, +-- this is a userdata, not a table, but it has this interface +export type span = { + beginline: number, + begincolumn: number, + endline: number, + endcolumn: number, } -export type Location = { - begin: Position, - ["end"]: Position, -- TODO: do we really want to have to use brackets everywhere? -} +luau.span = {} + +function luau.span.create(tbl: { beginline: number, begincolumn: number, endline: number, endcolumn: number }): span + error("not implemented") +end export type Whitespace = { tag: "whitespace", - location: Location, + location: span, text: string, } export type SingleLineComment = { tag: "comment", - location: Location, + location: span, text: string, } export type MultiLineComment = { tag: "blockcomment", - location: Location, + location: span, text: string, -- TODO: depth: number, } @@ -33,7 +37,7 @@ export type Trivia = Whitespace | SingleLineComment | MultiLineComment export type Token = { leadingtrivia: { Trivia }, - position: Position, + location: span, text: Kind, trailingtrivia: { Trivia }, istoken: true, @@ -86,7 +90,7 @@ export type AstExprCall = { arguments: Punctuated, closeparens: Token<")">?, self: boolean, - argLocation: Location, + argLocation: span, } export type AstExprIndexName = { @@ -95,7 +99,7 @@ export type AstExprIndexName = { expression: AstExpr, accessor: Token<"." | ":">, index: Token, - indexlocation: Location, + indexlocation: span, } export type AstExprIndexExpr = { @@ -579,7 +583,7 @@ export type AstTypePackVariadic = { export type AstTypePack = { kind: "typepack" } & (AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic) -export type AstNode = { location: Location? } & (AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute) +export type AstNode = { location: span? } & (AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute) export type ParseResult = { root: AstStatBlock, diff --git a/examples/transformer.luau b/examples/transformer.luau index ba8126a6b..5aa276f40 100644 --- a/examples/transformer.luau +++ b/examples/transformer.luau @@ -21,7 +21,7 @@ local function transformation(ctx) local operand: syntax.AstExprLocal = node.lhsoperand operand.token.leadingtrivia = {} operand.token.trailingtrivia = {} - local openingPosition: syntax.Position = operand.token.position; + local location: syntax.span = operand.token.location; -- transform node into an AstExprCall (node :: any).operator = nil @@ -30,11 +30,13 @@ local function transformation(ctx) ((node :: any) :: syntax.AstExprCall).tag = "call" - local func: syntax.AstExpr = { + local func: syntax.AstExprGlobal = { + kind = "expr", tag = "global", name = { + istoken = true, leadingtrivia = {}, - position = openingPosition, + location = location, text = "math.isnan", trailingtrivia = {}, }, @@ -42,8 +44,14 @@ local function transformation(ctx) ((node :: any) :: syntax.AstExprCall).func = func local openparens: syntax.Token<"("> = { + istoken = true, leadingtrivia = {}, - position = { line = openingPosition.line, column = openingPosition.column + #"math.isnan" }, + location = syntax.span.create({ + beginline = location.beginline, + begincolumn = location.begincolumn + #"math.isnan", + endline = location.endline, + endcolumn = location.endcolumn + #"math.isnan" + 1, + }), text = "(", trailingtrivia = {}, } @@ -53,11 +61,14 @@ local function transformation(ctx) ((node :: any) :: syntax.AstExprCall).arguments = arguments local closeparens: syntax.Token<")"> = { + istoken = true, leadingtrivia = {}, - position = { - line = openingPosition.line, - column = openingPosition.column + #"math.isnan(" + #operand.token.text, - }, + location = syntax.span.create({ + beginline = location.beginline, + begincolumn = location.begincolumn + #"math.isnan(" + #operand.token.text, + endline = location.endline, + endcolumn = location.endcolumn + #"math.isnan(" + #operand.token.text + 1, + }), text = ")", trailingtrivia = {}, } @@ -65,13 +76,12 @@ local function transformation(ctx) ((node :: any) :: syntax.AstExprCall).self = false - local argLocation: syntax.Location = { - begin = { line = openingPosition.line, column = openingPosition.column + #"math.isnan(" }, - ["end"] = { - line = openingPosition.line, - column = openingPosition.column + #"math.isnan(" + #operand.token.text, - }, - } + local argLocation: syntax.span = syntax.span.create({ + beginline = location.beginline, + begincolumn = location.begincolumn + #"math.isnan(", + endline = location.endline, + endcolumn = location.begincolumn + #"math.isnan(" + #operand.token.text, + }); ((node :: any) :: syntax.AstExprCall).argLocation = argLocation return false diff --git a/lute/luau/include/lute/luau.h b/lute/luau/include/lute/luau.h index 97b0a7d8d..204f23c60 100644 --- a/lute/luau/include/lute/luau.h +++ b/lute/luau/include/lute/luau.h @@ -13,6 +13,10 @@ int luteopen_luau(lua_State* L); namespace luau { +static const char kSpanType[] = "span"; +static const char kCompileResultType[] = "CompileResult"; +static const char kSpanCreateName[] = "span.create"; + int luau_parse(lua_State* L); int luau_parseexpr(lua_State* L); @@ -33,4 +37,8 @@ static const luaL_Reg lib[] = { {nullptr, nullptr}, }; +static const std::string properties[] = { + kSpanType, +}; + } // namespace luau diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 4ee0a4a10..f7ecc11bb 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -2,6 +2,7 @@ #include "lute/configresolver.h" #include "lute/moduleresolver.h" +#include "lute/userdatas.h" #include "Luau/Ast.h" #include "Luau/BuiltinDefinitions.h" @@ -23,8 +24,6 @@ #include #include -const char* COMPILE_RESULT_TYPE = "CompileResult"; - namespace luau { @@ -120,6 +119,88 @@ struct Trivia std::string_view text; }; +// the userdata version of `Luau::Location` because exposing this as a table was, unfortunately, very impractical +// it happens too much all over the entire AST to do reasonably. +struct Span +{ + uint32_t beginLine; + uint32_t beginColumn; + uint32_t endLine; + uint32_t endColumn; +}; + +static int createSpan(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 1) + luaL_error(L, "%s: expected 1 argument, but got %d", kSpanCreateName, argumentCount); + + // read all three of the required fields out of the table + lua_getfield(L, 1, "beginline"); + lua_getfield(L, 1, "begincolumn"); + lua_getfield(L, 1, "endline"); + lua_getfield(L, 1, "endcolumn"); + + double beginline = luaL_checknumber(L, 2); + double begincolumn = luaL_checknumber(L, 3); + double endline = luaL_checknumber(L, 4); + double endcolumn = luaL_checknumber(L, 5); + + Span* span = static_cast(lua_newuserdatatagged(L, sizeof(Span), kSpanTag)); + + span->beginLine = static_cast(beginline); + span->beginColumn = static_cast(begincolumn); + span->endLine = static_cast(endline); + span->endColumn = static_cast(endcolumn); + + luaL_getmetatable(L, kSpanType); + lua_setmetatable(L, -2); + + return 1; +} + +static int makeSpanLibrary(lua_State* L) +{ + lua_createtable(L, 0, 1); + + lua_pushcfunction(L, luau::createSpan, "create"); + lua_setfield(L, -2, "create"); + + lua_setreadonly(L, -1, 1); + + return 1; +} + +static int indexSpan(lua_State* L) +{ + const Span* span = static_cast(luaL_checkudata(L, 1, kSpanType)); + + const char* fieldName = luaL_checkstring(L, 2); + + if (std::strcmp(fieldName, "beginline") == 0) + { + lua_pushnumber(L, span->beginLine); + return 1; + } + else if (std::strcmp(fieldName, "begincolumn") == 0) + { + lua_pushnumber(L, span->beginColumn); + return 1; + } + else if (std::strcmp(fieldName, "endline") == 0) + { + lua_pushnumber(L, span->endLine); + return 1; + } + else if (std::strcmp(fieldName, "endcolumn") == 0) + { + lua_pushnumber(L, span->endColumn); + return 1; + } + + return 0; +} + struct AstSerialize : public Luau::AstVisitor { lua_State* L; @@ -281,28 +362,19 @@ struct AstSerialize : public Luau::AstVisitor return {std::vector(trivia.begin(), middleIter), std::vector(middleIter, trivia.end())}; } - void serialize(Luau::Position position) - { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 2); - - lua_pushnumber(L, position.line); - lua_setfield(L, -2, "line"); - - lua_pushnumber(L, position.column); - lua_setfield(L, -2, "column"); - } - void serialize(Luau::Location location) { lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 2); - serialize(location.begin); - lua_setfield(L, -2, "begin"); + Span* span = static_cast(lua_newuserdatatagged(L, sizeof(Span), kSpanTag)); + + span->beginLine = location.begin.line + 1; + span->beginColumn = location.begin.column + 1; + span->endLine = location.end.line + 1; + span->endColumn = location.end.column + 1; - serialize(location.end); - lua_setfield(L, -2, "end"); + luaL_getmetatable(L, kSpanType); + lua_setmetatable(L, -2); } void serialize(Luau::AstName& name) @@ -527,10 +599,13 @@ struct AstSerialize : public Luau::AstVisitor LUAU_ASSERT(lua_istable(L, -2)); lua_setfield(L, -2, "leadingtrivia"); - serialize(position); - lua_setfield(L, -2, "position"); + size_t textLength = strlen(text); - lua_pushstring(L, text); + Luau::Position endPosition{ position.line, position.column + static_cast(textLength) }; + serialize(Luau::Location { position, endPosition }); + lua_setfield(L, -2, "location"); + + lua_pushlstring(L, text, textLength); lua_setfield(L, -2, "text"); advancePosition(text); @@ -2689,15 +2764,29 @@ int compile_luau(lua_State* L) new (userdata) std::string(std::move(bytecode)); - luaL_getmetatable(L, COMPILE_RESULT_TYPE); + luaL_getmetatable(L, kCompileResultType); lua_setmetatable(L, -2); return 1; } +static int indexCompileResult(lua_State* L) +{ + const std::string* bytecode_string = static_cast(luaL_checkudata(L, 1, kCompileResultType)); + + if (std::strcmp(luaL_checkstring(L, 2), "bytecode") == 0) + { + lua_pushlstring(L, bytecode_string->c_str(), bytecode_string->size()); + + return 1; + } + + return 0; +} + int load_luau(lua_State* L) { - const std::string* bytecodeString = static_cast(luaL_checkudata(L, 1, COMPILE_RESULT_TYPE)); + const std::string* bytecodeString = static_cast(luaL_checkudata(L, 1, kCompileResultType)); const char* chunkname = luaL_checkstring(L, 2); int envIndex = lua_isnoneornil(L, 3) ? 0 : 3; @@ -2742,50 +2831,54 @@ int typeofmodule_luau(lua_State* L) return 1; } - -} // namespace luau - -static int index_result(lua_State* L) +// perform type mt registration, etc +static int initLuauLibrary(lua_State* L) { - const std::string* bytecode_string = static_cast(luaL_checkudata(L, 1, COMPILE_RESULT_TYPE)); + luaL_newmetatable(L, kCompileResultType); - if (std::strcmp(luaL_checkstring(L, 2), "bytecode") == 0) - { - lua_pushlstring(L, bytecode_string->c_str(), bytecode_string->size()); + // Set __type + lua_pushstring(L, kCompileResultType); + lua_setfield(L, -2, "__type"); - return 1; - } + lua_pushcfunction(L, luau::indexCompileResult, "CompilerResult.__index"); + lua_setfield(L, -2, "__index"); - return 0; -} + lua_setreadonly(L, -1, 1); -// perform type mt registration, etc -static int init_luau_lib(lua_State* L) -{ - luaL_newmetatable(L, COMPILE_RESULT_TYPE); + lua_pop(L, 1); + + luaL_newmetatable(L, kSpanType); // Set __type - lua_pushstring(L, "CompilerResult"); + lua_pushstring(L, kSpanType); lua_setfield(L, -2, "__type"); - lua_pushcfunction(L, index_result, "CompilerResult.__index"); + lua_pushcfunction(L, luau::indexSpan, "span.__index"); lua_setfield(L, -2, "__index"); + lua_setreadonly(L, -1, 1); + lua_pop(L, 1); return 1; } +} // namespace luau + int luaopen_luau(lua_State* L) { luaL_register(L, "luau", luau::lib); - return init_luau_lib(L); + return luau::initLuauLibrary(L); } int luteopen_luau(lua_State* L) { - lua_createtable(L, 0, std::size(luau::lib)); + lua_createtable(L, 0, std::size(luau::lib) + std::size(luau::properties)); + + // span library + luau::makeSpanLibrary(L); + lua_setfield(L, -2, luau::kSpanType); for (auto& [name, func] : luau::lib) { @@ -2798,5 +2891,5 @@ int luteopen_luau(lua_State* L) lua_setreadonly(L, -1, 1); - return init_luau_lib(L); + return luau::initLuauLibrary(L); } diff --git a/lute/runtime/include/lute/userdatas.h b/lute/runtime/include/lute/userdatas.h index 5cc6153d9..b0eb73c5d 100644 --- a/lute/runtime/include/lute/userdatas.h +++ b/lute/runtime/include/lute/userdatas.h @@ -5,3 +5,4 @@ constexpr int kDurationTag = 127; constexpr int kInstantTag = 126; constexpr int kWatchHandleTag = 125; constexpr int kHashFunctionTag = 124; +constexpr int kSpanTag = 123; diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index 68bb61c32..cfb9b5178 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -1,8 +1,13 @@ local parser = require("@self/parser") local types = require("@self/types") -export type Position = types.Position -export type Location = types.Location +local luau = require("@lute/luau") + +export type span = types.span + +local span = table.freeze({ + create = luau.span.create, +}) export type Whitespace = types.Whitespace export type SingleLineComment = types.SingleLineComment @@ -159,4 +164,4 @@ export type AstNode = types.AstNode export type ParseResult = types.ParseResult -return table.freeze({ parseblock = parser.parseblock, parseexpr = parser.parseexpr, parse = parser.parse }) +return table.freeze({ parseblock = parser.parseblock, parseexpr = parser.parseexpr, parse = parser.parse, span = span }) diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index f344e7161..29026454a 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -1,7 +1,6 @@ local luau = require("@lute/luau") -export type Position = luau.Position -export type Location = luau.Location +export type span = luau.span export type Whitespace = luau.Whitespace export type SingleLineComment = luau.SingleLineComment diff --git a/lute/std/libs/syntax/utils/trivia.luau b/lute/std/libs/syntax/utils/trivia.luau index 68110cb54..4f9a81a32 100644 --- a/lute/std/libs/syntax/utils/trivia.luau +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -13,11 +13,11 @@ function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } for _, token in q.nodes do if leftmostToken == nil then leftmostToken = token - elseif token.position.line < leftmostToken.position.line then + elseif token.location.beginline < leftmostToken.location.beginline then leftmostToken = token elseif - token.position.line == leftmostToken.position.line - and token.position.column < leftmostToken.position.column + token.location.beginline == leftmostToken.location.beginline + and token.location.begincolumn < leftmostToken.location.begincolumn then leftmostToken = token end @@ -36,11 +36,11 @@ function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } for _, token in q.nodes do if rightmostToken == nil then rightmostToken = token - elseif token.position.line > rightmostToken.position.line then + elseif token.location.beginline > rightmostToken.location.beginline then rightmostToken = token elseif - token.position.line == rightmostToken.position.line - and token.position.column > rightmostToken.position.column + token.location.beginline == rightmostToken.location.beginline + and token.location.begincolumn > rightmostToken.location.begincolumn then rightmostToken = token end diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 0d9821f24..5d06d1878 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -9,16 +9,16 @@ local T = require("@std/luau") local function assertEqualsLocation( assert: asserts.Asserts, - actual: T.Location, + actual: T.span, startLine: number, startColumn: number, endLine: number, endColumn: number ) - assert.eq(actual.begin.line, startLine) - assert.eq(actual.begin.column, startColumn) - assert.eq(actual["end"].line, endLine) - assert.eq(actual["end"].column, endColumn) + assert.eq(actual.beginline, startLine) + assert.eq(actual.begincolumn, startColumn) + assert.eq(actual.endline, endLine) + assert.eq(actual.endcolumn, endColumn) end test.suite("Parser", function(suite) @@ -33,7 +33,7 @@ test.suite("Parser", function(suite) assert.eq(#token.leadingtrivia, 1) assert.eq(token.leadingtrivia[1].tag, "whitespace") assert.eq(token.leadingtrivia[1].text, " ") - assertEqualsLocation(assert, token.leadingtrivia[1].location, 0, 0, 0, 2) + assertEqualsLocation(assert, token.leadingtrivia[1].location, 1, 1, 1, 3) end) suite:case("tokenContainsLeadingNewline", function(assert) @@ -47,7 +47,7 @@ test.suite("Parser", function(suite) assert.eq(#token.leadingtrivia, 1) assert.eq(token.leadingtrivia[1].tag, "whitespace") assert.eq(token.leadingtrivia[1].text, "\n") - assertEqualsLocation(assert, token.leadingtrivia[1].location, 0, 0, 1, 0) + assertEqualsLocation(assert, token.leadingtrivia[1].location, 1, 1, 2, 1) end) suite:case("tokenContainsLeadingSingleLineComment", function(assert) @@ -61,10 +61,10 @@ test.suite("Parser", function(suite) assert.eq(#token.leadingtrivia, 2) assert.eq(token.leadingtrivia[1].tag, "comment") assert.eq(token.leadingtrivia[1].text, "-- comment") - assertEqualsLocation(assert, token.leadingtrivia[1].location, 0, 0, 0, 10) + assertEqualsLocation(assert, token.leadingtrivia[1].location, 1, 1, 1, 11) assert.eq(token.leadingtrivia[2].tag, "whitespace") assert.eq(token.leadingtrivia[2].text, "\n") - assertEqualsLocation(assert, token.leadingtrivia[2].location, 0, 10, 1, 0) + assertEqualsLocation(assert, token.leadingtrivia[2].location, 1, 11, 2, 1) end) suite:case("tokenContainsLeadingBlockComment", function(assert) @@ -78,10 +78,10 @@ test.suite("Parser", function(suite) assert.eq(#token.leadingtrivia, 2) assert.eq(token.leadingtrivia[1].tag, "blockcomment") assert.eq(token.leadingtrivia[1].text, "--[[ comment ]]") - assertEqualsLocation(assert, token.leadingtrivia[1].location, 0, 0, 0, 15) + assertEqualsLocation(assert, token.leadingtrivia[1].location, 1, 1, 1, 16) assert.eq(token.leadingtrivia[2].tag, "whitespace") assert.eq(token.leadingtrivia[2].text, " ") - assertEqualsLocation(assert, token.leadingtrivia[2].location, 0, 15, 0, 16) + assertEqualsLocation(assert, token.leadingtrivia[2].location, 1, 16, 1, 17) end) suite:case("tokenizeWhitespace", function(assert) From 0236055e486a42685663be90a3805d2d196cbadd Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:03:13 -0800 Subject: [PATCH 180/642] stdlib: remove vectorext (#602) As part of the API audit/cleanup, we remove `vectorext` from `@std` --- lute/std/libs/vectorext.luau | 20 -------------------- tests/src/require/lute/std.luau | 2 -- tests/std/vectorext.test.luau | 25 ------------------------- 3 files changed, 47 deletions(-) delete mode 100644 lute/std/libs/vectorext.luau delete mode 100644 tests/std/vectorext.test.luau diff --git a/lute/std/libs/vectorext.luau b/lute/std/libs/vectorext.luau deleted file mode 100644 index 6560845dc..000000000 --- a/lute/std/libs/vectorext.luau +++ /dev/null @@ -1,20 +0,0 @@ ---!strict --- @std/vectorext --- stdlib for an extension of the built-in vector library in Luau --- Provides additional utility functions for vector operations - -local vectorext = {} - -function vectorext.withx(vec: vector, x: number): vector - return vector.create(x, vec.y, vec.z) -end - -function vectorext.withy(vec: vector, y: number): vector - return vector.create(vec.x, y, vec.z) -end - -function vectorext.withz(vec: vector, z: number): vector - return vector.create(vec.x, vec.y, z) -end - -return table.freeze(vectorext) diff --git a/tests/src/require/lute/std.luau b/tests/src/require/lute/std.luau index 2a5cefffc..1eb4033b3 100644 --- a/tests/src/require/lute/std.luau +++ b/tests/src/require/lute/std.luau @@ -1,13 +1,11 @@ local tableext = require("@std/tableext") local task = require("@std/task") local time = require("@std/time") -local vectorext = require("@std/vectorext") local system = require("@std/system") assert(type(tableext) == "table") assert(type(task) == "table") assert(type(time) == "table") -assert(type(vectorext) == "table") assert(type(system) == "table") return { "successfully required @std modules" } diff --git a/tests/std/vectorext.test.luau b/tests/std/vectorext.test.luau deleted file mode 100644 index 1b567999f..000000000 --- a/tests/std/vectorext.test.luau +++ /dev/null @@ -1,25 +0,0 @@ -local test = require("@std/test") -local vectorext = require("@std/vectorext") - -test.suite("VectorExt", function(suite) - suite:case("withx_withy_withz", function(assert) - local v = vector.create(1, 2, 3) - - local vx = vectorext.withx(v, 10) - assert.eq(vx.x, 10) - assert.eq(vx.y, 2) - assert.eq(vx.z, 3) - - local vy = vectorext.withy(v, 20) - assert.eq(vy.x, 1) - assert.eq(vy.y, 20) - assert.eq(vy.z, 3) - - local vz = vectorext.withz(v, 30) - assert.eq(vz.x, 1) - assert.eq(vz.y, 2) - assert.eq(vz.z, 30) - end) -end) - -test.run() From 683df4e15374e887bfa68473e9203776388799c3 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:53:22 -0800 Subject: [PATCH 181/642] stdlib: update tableext by removing unnecessary funcs and types (#603) As part of the API audit/cleanup, we remove `fold`, `foreach`, `array` and `dictionary` from `tableext` and any use cases --- lute/std/libs/tableext.luau | 21 +-------------------- tests/src/resolver/mainmodule.luau | 2 +- tests/std/tableext.test.luau | 18 ------------------ 3 files changed, 2 insertions(+), 39 deletions(-) diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau index 1062f18ba..eeda367a5 100644 --- a/lute/std/libs/tableext.luau +++ b/lute/std/libs/tableext.luau @@ -5,9 +5,6 @@ local tableext = {} -export type array = { T } -export type dictionary = { [string]: T } - function tableext.map(table: { [K]: A }, f: (A) -> B): { [K]: B } local new = {} @@ -30,17 +27,7 @@ function tableext.filter(table: { [K]: V }, predicate: (V) -> boolean): { return new end -function tableext.fold(table: { [K]: V }, f: (A, V) -> A, initial: A): A - local acc = initial - - for _, v in table do - acc = f(acc, v) - end - - return acc -end - -function tableext.extend(tbl: array, ...: array) +function tableext.extend(tbl: { T }, ...: { T }) local extensions = table.pack(...) extensions.n = nil :: any for _, extension in extensions do @@ -61,12 +48,6 @@ function tableext.combine(tbl: { [K]: V }, ...: { [K]: V }): { [K]: V } return tbl end -function tableext.foreach(tbl: { [K]: V }, f: (K, V) -> ()) - for k, v in tbl do - f(k, v) - end -end - function tableext.keys(tbl: { [K]: V }): { K } local keys = {} for k, _ in tbl do diff --git a/tests/src/resolver/mainmodule.luau b/tests/src/resolver/mainmodule.luau index 6633c4fc9..64ddaa13c 100644 --- a/tests/src/resolver/mainmodule.luau +++ b/tests/src/resolver/mainmodule.luau @@ -7,7 +7,7 @@ local testFilter: { [string]: number } = { } local a = {} -type tb = tableext.dictionary +type tb = { [string]: number } function a._a(a: number) return function(s: string): types.test diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index b1e43872e..0f8925bbb 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -19,14 +19,6 @@ test.suite("TableExt", function(suite) assert.eq(filtered.c, 3) end) - suite:case("fold", function(assert) - local arr: { number } = { 1, 2, 3, 4 } - local sum = tableext.fold(arr, function(acc: number, v: number) - return acc + v - end, 0) - assert.eq(sum, 10) - end) - suite:case("extend", function(assert) local a: { number } = { 1, 2 } local b: { number } = { 3, 4 } @@ -50,16 +42,6 @@ test.suite("TableExt", function(suite) assert.eq(a["d"], 5) end) - suite:case("foreach", function(assert) - local a = { 1, 2, 3 } - local numCalls = 0 - local function callback() - numCalls += 1 - end - tableext.foreach(a, callback) - assert.eq(numCalls, #a) - end) - suite:case("keys", function(assert) local a = { a = 1, b = 2, c = 3 } local keys = tableext.keys(a) From a849b5467e6a125416acb315e0daa58c2efc2cbd Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:53:33 -0800 Subject: [PATCH 182/642] stdlib: update stringext funcs to hasprefix and hassuffix (#604) As part of the API audit/cleanup, we rename `startswith` to `hasprefix` and `endswith` to `hassuffix` --- lute/cli/commands/test/finder.luau | 2 +- lute/cli/commands/transform/lib/arguments.luau | 2 +- lute/cli/commands/transform/lib/files.luau | 2 +- lute/cli/commands/transform/lib/ignore.luau | 4 ++-- lute/std/libs/stringext.luau | 8 ++++---- tests/std/stringext.test.luau | 10 +++++----- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index 413427a89..9aa2a0ab2 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -5,7 +5,7 @@ local stringext = require("@std/stringext") local tableext = require("@std/tableext") local function istestfile(filename: string): boolean - return stringext.endswith(filename, ".test.luau") or stringext.endswith(filename, ".spec.luau") + return stringext.hassuffix(filename, ".test.luau") or stringext.hassuffix(filename, ".spec.luau") end local function findtestfilesrec(directory: path.path): { path.path } diff --git a/lute/cli/commands/transform/lib/arguments.luau b/lute/cli/commands/transform/lib/arguments.luau index 896cbd468..337a9d492 100644 --- a/lute/cli/commands/transform/lib/arguments.luau +++ b/lute/cli/commands/transform/lib/arguments.luau @@ -31,7 +31,7 @@ local function parse(arguments: { string }): Config while i <= #arguments do local argument = arguments[i] - if stringext.startswith(argument, "--") then + if stringext.hasprefix(argument, "--") then local name = stringext.removeprefix(argument, "--") if config.migrationPath == nil then diff --git a/lute/cli/commands/transform/lib/files.luau b/lute/cli/commands/transform/lib/files.luau index 09580b8f4..2cdfc9186 100644 --- a/lute/cli/commands/transform/lib/files.luau +++ b/lute/cli/commands/transform/lib/files.luau @@ -21,7 +21,7 @@ local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: tableext.extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver)) else -- TODO: allow customisation of the filter when traversing a directory. e.g., globs? file endings? ignore paths? - if stringext.endswith(entry.name, ".luau") or stringext.endswith(entry.name, ".lua") then + if stringext.hassuffix(entry.name, ".luau") or stringext.hassuffix(entry.name, ".lua") then if ignoreResolver:isIgnoredFile(fullPath) then print("Skipping", fullPath) continue diff --git a/lute/cli/commands/transform/lib/ignore.luau b/lute/cli/commands/transform/lib/ignore.luau index b7c3652d5..7c8a5d266 100644 --- a/lute/cli/commands/transform/lib/ignore.luau +++ b/lute/cli/commands/transform/lib/ignore.luau @@ -123,11 +123,11 @@ local function parseGitignoreContents(location: string, contents: string): Gitig for _, line in lines do line = stringext.trim(line) - if line == "" or stringext.startswith(line, "#") then + if line == "" or stringext.hasprefix(line, "#") then continue end - if stringext.startswith(line, "!") then + if stringext.hasprefix(line, "!") then local globPattern = stringext.removeprefix(line, "!") local glob = parseGlob(globPattern) table.insert(ignoreData.whitelists, glob) diff --git a/lute/std/libs/stringext.luau b/lute/std/libs/stringext.luau index 65d1633d7..04b4797cb 100644 --- a/lute/std/libs/stringext.luau +++ b/lute/std/libs/stringext.luau @@ -5,7 +5,7 @@ local stringext = {} -function stringext.startswith(str: string, prefix: string): boolean +function stringext.hasprefix(str: string, prefix: string): boolean if #prefix > #str then return false end @@ -14,19 +14,19 @@ function stringext.startswith(str: string, prefix: string): boolean end function stringext.removeprefix(str: string, prefix: string): string - if not stringext.startswith(str, prefix) then + if not stringext.hasprefix(str, prefix) then return str end return str:sub(#prefix + 1) end -function stringext.endswith(str: string, suffix: string): boolean +function stringext.hassuffix(str: string, suffix: string): boolean return str:sub(-#suffix) == suffix end function stringext.removesuffix(str: string, suffix: string): string - if not stringext.endswith(str, suffix) then + if not stringext.hassuffix(str, suffix) then return str end diff --git a/tests/std/stringext.test.luau b/tests/std/stringext.test.luau index 877ebbba4..f2add781c 100644 --- a/tests/std/stringext.test.luau +++ b/tests/std/stringext.test.luau @@ -2,12 +2,12 @@ local stringext = require("@std/stringext") local test = require("@std/test") test.suite("StringExt", function(suite) - suite:case("startswith_and_endswith", function(assert) - assert.eq(stringext.startswith("hello", "he"), true) - assert.eq(stringext.startswith("hi", "hello"), false) + suite:case("hasprefix_and_hassuffix", function(assert) + assert.eq(stringext.hasprefix("hello", "he"), true) + assert.eq(stringext.hasprefix("hi", "hello"), false) - assert.eq(stringext.endswith("foobar", "bar"), true) - assert.eq(stringext.endswith("foo", "foobar"), false) + assert.eq(stringext.hassuffix("foobar", "bar"), true) + assert.eq(stringext.hassuffix("foo", "foobar"), false) end) suite:case("removeprefix_and_removesuffix", function(assert) From 1f5dd8ff25449db7457e99e0aa7a23792125dab1 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 20 Nov 2025 11:23:57 -0800 Subject: [PATCH 183/642] actually report failing windows tests (#601) A while back, we ensured that Windows tests actually run in CI, but due to how Command Prompt for loops work, test failures didn't actually cause the status check to fail. This PR fixes that and also fixes the tests that were failing unreported. --- .github/workflows/ci.yml | 6 +++++- lute/process/src/process.cpp | 5 ++--- tests/cli/test.test.luau | 20 +++++++++++++++++--- tests/std/process.test.luau | 32 ++++++++++++++++++++++---------- 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 944c7e707..dc6b6e6a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,11 @@ jobs: - name: Run Luau Tests (Windows) if: runner.os == 'Windows' shell: cmd - run: for /r tests %%f in (*.test.luau) do "${{ steps.build_lute.outputs.exe_path }}" run "%%f" + run: | + set _errorFlag=0 + for /r tests %%f in (*.test.luau) do "${{ steps.build_lute.outputs.exe_path }}" run "%%f" || (set _errorFlag=1) + if %_errorFlag% neq 0 exit /b 1 + run-lute-cxx-unittests: runs-on: ${{ matrix.os }} diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 665bcef5d..ad4affef2 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -32,12 +32,12 @@ void convertCRLFtoLF(std::string& str) for (size_t readPos = 0; readPos < str.size(); ++readPos) { if (str[readPos] == '\r' && readPos + 1 < str.size() && str[readPos + 1] == '\n') - continue; // Skip the '\r' in CRLF + continue; // Skip the '\r' in CRLF str[writePos++] = str[readPos]; } str.resize(writePos); } - + struct ProcessHandle { uv_process_t process; @@ -457,7 +457,6 @@ int exitFunc(lua_State* L) { int exitCode = luaL_optinteger(L, 1, 0); - // Exit with the provided code std::exit(exitCode); diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 63a65947e..a68aa45ae 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -3,7 +3,7 @@ local path = require("@std/path") local process = require("@std/process") local system = require("@std/system") local test = require("@std/test") -local fs = require("@std/fs") + local lutePath = path.format(process.execpath()) local tmpDir = system.tmpdir() @@ -75,7 +75,14 @@ test.run() result.stdout:find(`Failed with: Runtime error in nil_field_suite.access_field_of_nil in:`, 1, true), nil ) - assert.neq(result.stdout:find(`{path.format(testFilePath)}:6`, 1, true), nil) + assert.neq( + result.stdout:find( -- filename comes from debug.info, which uses forward slashes even on Windows + `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:6`, + 1, + true + ), + nil + ) fs.remove(testFilePath) end) @@ -107,7 +114,14 @@ test.run() assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number assert.neq(result.stdout:find(`Failed with: Runtime error in access_field_of_nil in:`, 1, true), nil) - assert.neq(result.stdout:find(`{path.format(testFilePath)}:5`, 1, true), nil) + assert.neq( + result.stdout:find( -- filename comes from debug.info, which uses forward slashes even on Windows + `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:5`, + 1, + true + ), + nil + ) fs.remove(testFilePath) end) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index f197bba1d..ab52b7b75 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -1,7 +1,8 @@ local pathlib = require("@std/path") local process = require("@std/process") -local test = require("@std/test") local system = require("@std/system") +local test = require("@std/test") + test.suite("ProcessSuite", function(suite) suite:case("homedir_and_cwd_and_execpath", function(assert) local hd = process.homedir() @@ -19,22 +20,33 @@ test.suite("ProcessSuite", function(suite) assert.eq(r.stdout, "hello-from-lute\n") end) - suite:case("run_shell_and_cwd", function(assert) + suite:case("run_shell_and_cwd", function(check) local rootdir: string = "/" + local root: pathlib.path? = nil if system.win32 then - local root = pathlib.win32.drive(process.cwd()) + root = pathlib.win32.drive(process.cwd()) rootdir = pathlib.format(root) end local r = process.run({ "pwd" }, { cwd = rootdir }) - assert.tableeq(r, { - exitcode = 0, - stdout = "/\n", - stderr = "", - ok = true, - }) + if system.win32 then + assert(root ~= nil and root.driveLetter ~= nil) + check.tableeq(r, { + exitcode = 0, + stdout = `/{root.driveLetter:lower()}\n`, + stderr = "", + ok = true, + }) + else + check.tableeq(r, { + exitcode = 0, + stdout = "/\n", + stderr = "", + ok = true, + }) + end local r2 = process.run({ "echo hello!" }, { shell = true }) - assert.tableeq(r2, { + check.tableeq(r2, { exitcode = 0, stdout = "hello!\n", stderr = "", From 3a25262d3f4cad6127b2e353f67534fa007f2f24 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:52:08 -0800 Subject: [PATCH 184/642] stdlib: luacase @std/luau (#605) As part of the API audit/cleanup, we are luacasing `@std/luau`!! --- lute/std/libs/luau.luau | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 464146c75..f09822489 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -10,13 +10,13 @@ local path = require("@std/path") local luau = {} -export type Bytecode = luteLuau.Bytecode +export type bytecode = luteLuau.Bytecode -function luau.compile(source: string): Bytecode +function luau.compile(source: string): bytecode return luteLuau.compile(source) end -function luau.load(bytecode: Bytecode, chunkname: string?, env: { [any]: any }?): (...any) -> ...any +function luau.load(bytecode: bytecode, chunkname: string?, env: { [any]: any }?): (...any) -> ...any return luteLuau.load(bytecode, if chunkname ~= nil then `@{chunkname}` else "=luau.load", env) end From dc6b1780b674586e6b9ebfc64b0250b5aab3d398 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 20 Nov 2025 14:05:44 -0800 Subject: [PATCH 185/642] First version of lute lint (#597) Initial implementation of `lute lint`. It currently supports exactly one rule and one source file, with multiple rules and source files coming in a future PR. By default, it pretty prints reported violations (barring tab shenanigans). Some notes: - I had to copy `cli.luau` over from batteries because `@batteries` isn't embedded with lute commands. - I added a `@commands` alias so that the lint rules I wrote could have access to lint-related types. Longer term, we should probably figure out a better place to put those types. --- .luaurc | 3 +- .luaurc.ci | 1 + batteries/cli.luau | 2 +- examples/lints/almost_swapped.luau | 105 +++++++++++++++++ examples/lints/divide_by_zero.luau | 39 +++++++ lute/cli/commands/cli.luau | 175 ++++++++++++++++++++++++++++ lute/cli/commands/lint/init.luau | 87 ++++++++++++++ lute/cli/commands/lint/printer.luau | 71 +++++++++++ lute/cli/commands/lint/types.luau | 19 +++ lute/std/libs/syntax/query.luau | 13 +++ tests/cli/lint.test.luau | 157 +++++++++++++++++++++++++ tests/std/syntax/query.test.luau | 35 ++++++ 12 files changed, 705 insertions(+), 2 deletions(-) create mode 100644 examples/lints/almost_swapped.luau create mode 100644 examples/lints/divide_by_zero.luau create mode 100644 lute/cli/commands/cli.luau create mode 100644 lute/cli/commands/lint/init.luau create mode 100644 lute/cli/commands/lint/printer.luau create mode 100644 lute/cli/commands/lint/types.luau create mode 100644 tests/cli/lint.test.luau diff --git a/.luaurc b/.luaurc index b928cef88..01c26f8f6 100644 --- a/.luaurc +++ b/.luaurc @@ -3,6 +3,7 @@ "aliases": { "batteries": "./batteries", "std": "./lute/std/libs", - "lute": "./definitions" + "lute": "./definitions", + "commands": "./lute/cli/commands" } } diff --git a/.luaurc.ci b/.luaurc.ci index 7882318d8..99ef608b7 100644 --- a/.luaurc.ci +++ b/.luaurc.ci @@ -2,5 +2,6 @@ "languageMode": "strict", "aliases": { "batteries": "./batteries", + "commands": "./lute/cli/commands" } } diff --git a/batteries/cli.luau b/batteries/cli.luau index 28018634d..178637f82 100644 --- a/batteries/cli.luau +++ b/batteries/cli.luau @@ -151,7 +151,7 @@ function cli.parse(self: Parser, args: { string }): () end end -function cli.get(self: Parser, name: string): string +function cli.get(self: Parser, name: string): string? return self.parsed.values[name] end diff --git a/examples/lints/almost_swapped.luau b/examples/lints/almost_swapped.luau new file mode 100644 index 000000000..dc543c397 --- /dev/null +++ b/examples/lints/almost_swapped.luau @@ -0,0 +1,105 @@ +local lintTypes = require("@commands/lint/types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "almost_swapped" +local message = "This looks like a failed attempt to swap." + +local compFuncs = {} + +function compFuncs.exprLocalsSame(a: syntax.AstExprLocal, b: syntax.AstExprLocal): boolean + return a["local"] == b["local"] +end + +function compFuncs.exprGlobalsSame(a: syntax.AstExprGlobal, b: syntax.AstExprGlobal): boolean + return a.name.text == b.name.text +end + +function compFuncs.exprIndexNamesSame(a: syntax.AstExprIndexName, b: syntax.AstExprIndexName): boolean + if a.index.text ~= b.index.text then + return false + end + + return compFuncs.refExprsSame(a.expression, b.expression) +end + +function compFuncs.exprIndexExprsSame(a: syntax.AstExprIndexExpr, b: syntax.AstExprIndexExpr): boolean + if a.index.tag == "string" and b.index.tag == "string" then + if a.index.text ~= b.index.text then + return false + else + return compFuncs.refExprsSame(a.expression, b.expression) + end + else + return compFuncs.refExprsSame(a.expression, b.expression) and compFuncs.refExprsSame(a.index, b.index) + end +end + +function compFuncs.refExprsSame(a: syntax.AstExpr, b: syntax.AstExpr): boolean + if a.tag ~= b.tag then + return false + end + + if a.tag == "local" then + return compFuncs.exprLocalsSame(a, b :: syntax.AstExprLocal) + elseif a.tag == "global" then + return compFuncs.exprGlobalsSame(a, b :: syntax.AstExprGlobal) + elseif a.tag == "indexname" then + return compFuncs.exprIndexNamesSame(a, b :: syntax.AstExprIndexName) + elseif a.tag == "index" then + return compFuncs.exprIndexExprsSame(a, b :: syntax.AstExprIndexExpr) + else + return false + end +end + +-- Report instances of attempted swaps like: +-- a = b; b = a +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } + local violations = {} + + local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes + + for _, block in nodes do + for i = 1, #block.statements - 1 do + local currStat = block.statements[i] + if currStat.tag ~= "assign" or #currStat.values ~= 1 or #currStat.variables ~= 1 then + continue + end + + local nextStat = block.statements[i + 1] + if nextStat.tag ~= "assign" or #nextStat.values ~= 1 or #nextStat.variables ~= 1 then + continue + end + + local currVar, currVal = currStat.variables[1].node, currStat.values[1].node + local nextVar, nextVal = nextStat.variables[1].node, nextStat.values[1].node + + if compFuncs.refExprsSame(currVar, nextVal) and compFuncs.refExprsSame(nextVar, currVal) then + table.insert(violations, { -- LUAUFIX: severity isn't inferred as a singleton, so table.insert is mad + lintname = name, + location = syntax.span.create({ + beginline = currStat.location.beginline, + begincolumn = currStat.location.begincolumn, + endline = nextStat.location.endline, + endcolumn = nextStat.location.endcolumn, + }), + message = message, + severity = "warning", + sourcepath = sourcepath, + }) + end + end + end + + return violations +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/examples/lints/divide_by_zero.luau b/examples/lints/divide_by_zero.luau new file mode 100644 index 000000000..f9ffc8051 --- /dev/null +++ b/examples/lints/divide_by_zero.luau @@ -0,0 +1,39 @@ +local lintTypes = require("@commands/lint/types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "divide_by_zero" +local message = "Division by zero detected." + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } + return query + .findallfromroot(ast, utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" + end) + :filter(function(bin) + return bin.rhsoperand.kind == "expr" and bin.rhsoperand.tag == "number" and bin.rhsoperand.value == 0 + end) + :maptoarray( + function( + n: syntax.AstExprBinary + ): lintTypes.LintViolation -- LUAUFIX: Bidiretional inference of generics should let us not need this annotation + return { + lintname = name, + location = n.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + } + end + ) +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/cli.luau b/lute/cli/commands/cli.luau new file mode 100644 index 000000000..178637f82 --- /dev/null +++ b/lute/cli/commands/cli.luau @@ -0,0 +1,175 @@ +local cli = {} +cli.__index = cli + +type ArgKind = "positional" | "flag" | "option" +type ArgOptions = { + help: string?, + aliases: { string }?, + default: string?, + required: boolean?, +} + +type ArgData = { + name: string, + kind: ArgKind, + options: ArgOptions, +} + +type ParseResult = { + values: { [string]: string }, + flags: { [string]: boolean }, + fwdArgs: { string }, +} + +type ParserData = { + arguments: { [number]: ArgData }, + positional: { [number]: ArgData }, + parsed: ParseResult, +} + +type ParserInterface = typeof(cli) +export type Parser = setmetatable + +function cli.parser(): Parser + local self = { + arguments = {}, + positional = {}, + parsed = { values = {}, flags = {}, fwdArgs = {} }, + } + + return setmetatable(self, cli) +end + +function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions): () + local argument = { + name = name, + kind = kind, + options = options or { aliases = {}, required = false }, + } + + table.insert(self.arguments, argument) + if kind == "positional" then + table.insert(self.positional, argument) + end +end + +function cli.parse(self: Parser, args: { string }): () + local i = 0 + local pos_index = 1 + + while i < #args do + i += 1 + + local arg = args[i] + + -- if the argument is exactly "--", we pass everything along + if arg == "--" then + table.move(args, i + 1, #args, 1, self.parsed.fwdArgs) + break + end + + -- if the argument starts with two dashes, we're parsing either a flag or an option + if string.sub(arg, 1, 2) == "--" then + local name = string.sub(arg, 3) + local found = false + + for _, argument in self.arguments do + local aliases = argument.options.aliases or {} + + if argument.name == name or table.find(aliases, name) then + found = true + + if argument.kind == "option" then + -- advance past the argument + i += 1 + + assert(i <= #args, "Missing value for argument: " .. argument.name) + self.parsed.values[argument.name] = args[i] + + break + end + + self.parsed.flags[argument.name] = true + break + end + end + + assert(found, "Unknown argument: " .. name) + continue + end + + -- if the argument starts with a single dash, we're parsing a flag + if string.sub(arg, 1, 1) == "-" then + local flags = string.sub(arg, 2) + + for j = 1, #flags do + local name = string.sub(flags, j, j) + local found = false + for _, argument in self.arguments do + local aliases = argument.options.aliases or {} + if argument.name == name or table.find(aliases, name) then + found = true + if argument.kind == "option" then + i += 1 + assert(i <= #args, "Missing value for argument: " .. argument.name) + self.parsed.values[argument.name] = args[i] + else + self.parsed.flags[argument.name] = true + end + break + end + end + + assert(found, "Unknown argument: " .. name) + end + + continue + end + + -- if we have positional arguments left, we can take this argument as one + if pos_index <= #self.positional then + self.parsed.values[self.positional[pos_index].name] = arg + pos_index += 1 + continue + end + + -- otherwise, the argument is forwarded on + table.insert(self.parsed.fwdArgs, arg) + end + + -- check that all required arguments are present, and set any default values as needed + for _, argument in self.arguments do + assert(argument) + + if argument.options.required and self.parsed.values[argument.name] == nil then + assert(self.parsed.values[argument.name], "Missing required argument: " .. argument.name) + end + + if self.parsed.values[argument.name] == nil and argument.options.default then + self.parsed.values[argument.name] = argument.options.default + end + end +end + +function cli.get(self: Parser, name: string): string? + return self.parsed.values[name] +end + +function cli.has(self: Parser, name: string): boolean + return self.parsed.flags[name] ~= nil +end + +function cli.forwarded(self: Parser): { string }? + return self.parsed.fwdArgs +end + +function cli.help(self: Parser): () + print("Usage:") + for _, argument in self.arguments do + local aliasStr = table.concat(argument.options.aliases or {}, ", ") + local reqStr = if argument.options.required then "(required) " else "" + print(string.format(" --%s (-%s) - %s%s", argument.name, aliasStr, reqStr, argument.options.help or "")) + end +end + +return table.freeze(cli) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau new file mode 100644 index 000000000..38fb3e3c3 --- /dev/null +++ b/lute/cli/commands/lint/init.luau @@ -0,0 +1,87 @@ +local cli = require("./cli") +local fs = require("@std/fs") +local luau = require("@std/luau") +local path = require("@std/path") +local printer = require("@self/printer") +local syntax = require("@std/syntax") +local types = require("@self/types") + +local USAGE = "Usage: lute lint [OPTIONS] [RULE] [PATH]" + +local function printHelp() + print(USAGE) + print([[ +Lint the specified Luau file using the specified lint rule. + +OPTIONS: + -h, --help Show this help message + +RULE: + Path to a Luau file defining a lint rule. + +PATH: + Path to the Luau file to be linted. + +EXAMPLES: + lute lint examples/lints/almost_swapped.luau bad_swap.luau +]]) +end + +local function loadLintRule(path: path.pathlike): types.LintRule + local loaded = luau.loadbypath(path) + assert(loaded, `{path} failed to require`) + assert(typeof(loaded) == "table", `{path} must return a table`) + assert(typeof(loaded.name) == "string", `{path} must return a table with a 'name' string property`) + assert( + typeof(loaded.lint) == "function", -- LUAUFIX: type error on loaded.lint because loaded has been refined to { read name: string } + `{path} must return a table with a 'lint' function property` + ) + + return loaded :: types.LintRule -- We have to cast because we can't assert that loaded.lint is a more specific functio type +end + +local function main(...: string) + local args = cli.parser() + + args:add("rule", "positional", { help = "Linting rule to apply" }) + args:add("input", "positional", { help = "Input file to be linted" }) + args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) + -- TODO: handle multiple rules, multiple input files, ignore blobs, json output option + + args:parse({ ... }) + + if args:has("help") then + printHelp() + return + end + + local rulePath = args:get("rule") + if rulePath == nil then + print("Error: No lint rule specified.\n\n" .. USAGE .. "\nUse --help for more information.") + return + end + + local inputFile = args:get("input") + if inputFile == nil then + print("Error: No input file specified.\n\n" .. USAGE .. "\nUse --help for more information.") + return + end + local inputFilePath = path.parse(inputFile) + + print(`Loading lint rule from '{rulePath}'`) + local lintRule = loadLintRule(rulePath) + + print(`Reading input file '{inputFile}'`) + local fileContent = fs.readfiletostring(inputFilePath) + + print(`Parsing input file '{inputFile}'`) + local ast = syntax.parseblock(fileContent) + + print("Applying lint rule") + local violations = lintRule.lint(ast, inputFilePath) + + print(`Printing {#violations} violations\n`) + printer.printLints(violations, fileContent) +end + +main(...) diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau new file mode 100644 index 000000000..18e1f0299 --- /dev/null +++ b/lute/cli/commands/lint/printer.luau @@ -0,0 +1,71 @@ +local path = require("@std/path") +local types = require("./types") + +local printerLib = {} + +local function printLint(lint: types.LintViolation, sourceLines: { string }): () + print(`{lint.severity}[{lint.lintname}]: {lint.message}\n`) + + local location = lint.location + local filepath = path.format(lint.sourcepath) + + if location.beginline == location.endline then + local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number + local blankGutter = string.rep(" ", gutterSize) + + local sourceLine = ` {location.beginline} │ {sourceLines[location.beginline]}` + + local filepathLine = + `{blankGutter}┌── {filepath}:{location.beginline}:{location.begincolumn}-{location.endcolumn} ──` + if #filepathLine < #sourceLine then + filepathLine ..= string.rep("─", #sourceLine - #filepathLine) + end + + print(filepathLine) + print(`{blankGutter}│`) + print(sourceLine) + print( + `{blankGutter}│ {string.rep(" ", location.begincolumn - 1)}{string.rep( + "^", + location.endcolumn - location.begincolumn + )}` + ) + print(`{blankGutter}│`) + print() + else + local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number + local blankGutter = string.rep(" ", gutterSize) + + local renderedSourceLines = { ` {location.beginline} │ ╭ {sourceLines[location.beginline]}` } + local maxSourceLineLength = #renderedSourceLines[1] + for lineNum = location.beginline + 1, location.endline do + table.insert(renderedSourceLines, ` {lineNum} │ │ {sourceLines[lineNum]}`) + maxSourceLineLength = math.max(maxSourceLineLength, #renderedSourceLines[#renderedSourceLines]) + end + + local filepathLine = + `{blankGutter}┌── {filepath}:{location.beginline}:{location.begincolumn}-{location.endline}:{location.endcolumn} ──` + if #filepathLine < maxSourceLineLength then + filepathLine ..= string.rep("─", maxSourceLineLength - #filepathLine) + end + + print(filepathLine) + print(`{blankGutter}│`) + for _, line in renderedSourceLines do + print(line) + end + print(`{blankGutter}│ ╰─{string.rep("─", location.endcolumn - 2)}^`) + print(`{blankGutter}│`) + print() + end +end + +function printerLib.printLints(lints: { types.LintViolation }, sourceContent: string): () + local sourceLines = sourceContent:split("\n") + + for _, lint in lints do + printLint(lint, sourceLines) + end +end + +return table.freeze(printerLib) diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau new file mode 100644 index 000000000..cae182961 --- /dev/null +++ b/lute/cli/commands/lint/types.luau @@ -0,0 +1,19 @@ +local syntax = require("@std/syntax") +local path = require("@std/path") + +export type severity = "error" | "warning" | "info" | "hint" + +export type LintViolation = { + read lintname: string, + read location: syntax.span, + read message: string, + read severity: severity, + read sourcepath: path.path, +} -- TODO: add suggested fix + +export type LintRule = { + read lint: (ast: syntax.AstStatBlock, sourcepath: path.path) -> { LintViolation }, + read name: string, +} + +return {} diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 68106455a..b8193e6ca 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -17,6 +17,7 @@ export type query = { foreach: (self: query, callback: (T) -> ()) -> query, findall: (self: query, fn: (node) -> U?) -> query, flatmap: (self: query, fn: (T) -> { U }) -> query, + maptoarray: (self: query, fn: (T) -> U?) -> { U }, } local queryLib = {} @@ -116,6 +117,17 @@ function queryLib.flatmap(self: query, fn: (T) -> { U }): query return self end +function queryLib.maptoarray(self: query, fn: (T) -> U?): { U } + local result = {} + for _, node in self.nodes do + local mapped = fn(node) + if mapped ~= nil then + table.insert(result, mapped) + end + end + return result +end + function queryLib.findallfromroot(ast: types.ParseResult | node, fn: (node) -> T?): query local nodes: { T } = {} @@ -132,6 +144,7 @@ function queryLib.findallfromroot(ast: types.ParseResult | node, fn: (node) - foreach = queryLib.foreach, findall = queryLib.findall, flatmap = queryLib.flatmap, + maptoarray = queryLib.maptoarray, } -- LUAUFIX: queryLib.map has generics quantified at a different level than expected end diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau new file mode 100644 index 000000000..b797261e0 --- /dev/null +++ b/tests/cli/lint.test.luau @@ -0,0 +1,157 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local lutePath = path.format(process.execpath()) +local tmpDir = system.tmpdir() + +test.suite("lute lint", function(suite) + suite:case("lute lint help message", function(assert) + local result = process.run({ lutePath, "lint", "-h" }) + + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Usage: lute lint [OPTIONS] [RULE] [PATH]", 1, true), nil) + + result = process.run({ lutePath, "lint", "--h" }) + + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Usage: lute lint [OPTIONS] [RULE] [PATH]", 1, true), nil) + + result = process.run({ lutePath, "lint", "--help" }) + + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Usage: lute lint [OPTIONS] [RULE] [PATH]", 1, true), nil) + end) + + suite:case("lute lint no rule", function(assert) + local result = process.run({ lutePath, "lint" }) + + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Error: No lint rule specified.", 1, true), nil) + end) + + suite:case("lute lint no rule", function(assert) + local result = process.run({ lutePath, "lint", "a_rule" }) + + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Error: No input file specified.", 1, true), nil) + end) + + suite:case("divide_by_zero", function(assert) + -- Setup + local rule = path.format(path.join("examples", "lints", "divide_by_zero.luau")) + + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", rule, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.neq(result.stdout:find("warning[divide_by_zero]: Division by zero detected.", 1, true), nil) + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("almost_swapped", function(assert) + -- Setup + local rule = path.format(path.join("examples", "lints", "almost_swapped.luau")) + + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +a = b +b = a + +local x = 0 +local y = "hello" +x = y +y = x + +local t = { a = 1, b = 2 } +t.a = t.b +t.b = t.a + +if true then + t["a"] = t["b"] + t["b"] = t["a"] +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", rule, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + -- We expect 4 warnings, so stdout should be split into 5 parts + assert.eq(#result.stdout:split("warning[almost_swapped]: This looks like a failed attempt to swap."), 5) + local expected = [[ +violator.luau:1:1-2:6 ── + │ + 1 │ ╭ a = b + 2 │ │ b = a + │ ╰─────^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + expected = [[ +violator.luau:6:1-7:6 ── + │ + 6 │ ╭ x = y + 7 │ │ y = x + │ ╰─────^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + expected = [[ +violator.luau:10:1-11:10 ── + │ + 10 │ ╭ t.a = t.b + 11 │ │ t.b = t.a + │ ╰─────────^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + expected = [[ +violator.luau:14:2-15:17 ── + │ + 14 │ ╭ t["a"] = t["b"] + 15 │ │ t["b"] = t["a"] + │ ╰────────────────^ + │ +]] -- todo: address tab shenanigans + assert.neq(result.stdout:find(expected, 1, true), nil) + + -- Teardown + fs.remove(violatorPath) + end) +end) + +test.run() diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index 11e41040a..e6f78fbf4 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -208,6 +208,41 @@ test.suite("AST Query", function(suite) assert.eq(locals.nodes[3].name.text, "c") assert.eq(locals.nodes[4].name.text, "d") end) + + suite:case("maptoarray", function(assert) + -- Test maptoarray to convert query nodes to a simple array + local ast = syntax.parse("local _ = {0, 1, 2, 3, 4}") + local nums = query.findallfromroot(ast, utils.isExprConstantNumber) + + local doubled = nums:maptoarray(function(n: syntax.AstExprConstantNumber): number + return n.value * 2 + end) + + assert.eq(#doubled, 5) + assert.eq(doubled[1], 0) + assert.eq(doubled[2], 2) + assert.eq(doubled[3], 4) + assert.eq(doubled[4], 6) + assert.eq(doubled[5], 8) + end) + + suite:case("maptoarray_with_nil", function(assert) + -- Test maptoarray filtering out nil values + local ast = syntax.parse("local _ = {0, 1, 2, 3, 4}") + local nums = query.findallfromroot(ast, utils.isExprConstantNumber) + + local evenOnly = nums:maptoarray(function(n: syntax.AstExprConstantNumber): number? + if n.value % 2 == 0 then + return n.value + end + return nil + end) + + assert.eq(#evenOnly, 3) + assert.eq(evenOnly[1], 0) + assert.eq(evenOnly[2], 2) + assert.eq(evenOnly[3], 4) + end) end) test.run() From 668b6c12bb16a1852d09edf8adaa3880b8d81bd7 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:51:14 -0800 Subject: [PATCH 186/642] stdlib: cleanup @std/fs (renaming/removing APIs) (#606) As part of the API audit/cleanup, we are fixing `@std/fs` APIs. This is mostly renaming and removing unnecessary functions, gonna leave `fs.watch` to be its separate PR Parallel changes to `@lute/fs` will be in a subsequent PR after we've agreed on how @std should look like --- docs/scripts/reference.luau | 2 +- examples/async_read.luau | 16 ----------- examples/create_directory.luau | 2 +- lute/cli/commands/test/finder.luau | 2 +- lute/std/libs/fs.luau | 43 +++++++++++++----------------- tests/cli/loadbypath.test.luau | 2 +- tests/std/fs.test.luau | 24 +++++------------ tests/std/syntax/parser.test.luau | 2 +- 8 files changed, 30 insertions(+), 63 deletions(-) delete mode 100644 examples/async_read.luau diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 83a552457..06267992c 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -235,7 +235,7 @@ local function processDirectory( ) fs.createdirectory(documentationPath, { makeparents = true }) - local entries = fs.listdir(modulePath) + local entries = fs.listdirectory(modulePath) for _, entry in entries do local entryPath = path.join(modulePath, entry.name) diff --git a/examples/async_read.luau b/examples/async_read.luau deleted file mode 100644 index 3038459a9..000000000 --- a/examples/async_read.luau +++ /dev/null @@ -1,16 +0,0 @@ -local fs = require("@std/fs") -local task = require("@std/task") - --- blocking -local f = fs.open("temp", "w+") -local x = "This is a string I am writing to a file" -fs.write(f, x) -fs.close(f) - -local t = task.create(function() - return fs.readasync("temp") -end) - -local t2 = task.await(t) -print(t2) -print(t2 == x) diff --git a/examples/create_directory.luau b/examples/create_directory.luau index 31357a5c2..e647f3a79 100644 --- a/examples/create_directory.luau +++ b/examples/create_directory.luau @@ -13,7 +13,7 @@ else print("Failed to create directory") end -fs.rmdir(directory) +fs.removedirectory(directory) if not fs.exists(directory) then print("Directory successfully removed") diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index 9aa2a0ab2..53de45e3d 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -11,7 +11,7 @@ end local function findtestfilesrec(directory: path.path): { path.path } local results = {} - for _, entry in fs.listdir(directory) do + for _, entry in fs.listdirectory(directory) do local fullPath = path.join(directory, entry.name) if entry.type == "dir" then diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 1e3b52d9f..573b955dc 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -10,13 +10,14 @@ local sys = require("@std/system") local fslib = {} export type handlemode = fs.HandleMode -export type filehandle = fs.FileHandle -export type filetype = fs.FileType -export type filemetadata = fs.FileMetadata +export type file = fs.FileHandle +export type type = fs.FileType +export type metadata = fs.FileMetadata export type directoryentry = fs.DirectoryEntry export type watchhandle = fs.WatchHandle export type watchevent = fs.WatchEvent -export type pathlike = pathlib.pathlike + +type pathlike = pathlib.pathlike export type createdirectoryoptions = { makeparents: boolean?, @@ -30,31 +31,31 @@ export type walkoptions = { recursive: boolean?, } -function fslib.open(path: pathlike, mode: handlemode?): filehandle +function fslib.open(path: pathlike, mode: handlemode?): file return fs.open(pathlib.format(path), mode) end -function fslib.read(handle: filehandle): string - return fs.read(handle) +function fslib.read(file: file): string + return fs.read(file) end -function fslib.write(handle: filehandle, contents: string): () - return fs.write(handle, contents) +function fslib.write(file: file, contents: string): () + return fs.write(file, contents) end -function fslib.close(handle: filehandle): () - return fs.close(handle) +function fslib.close(file: file): () + return fs.close(file) end function fslib.remove(path: pathlike): () return fs.remove(pathlib.format(path)) end -function fslib.stat(path: pathlike): filemetadata +function fslib.metadata(path: pathlike): metadata return fs.stat(pathlib.format(path)) end -function fslib.type(path: pathlike): filetype +function fslib.type(path: pathlike): type return fs.type(pathlib.format(path)) end @@ -62,7 +63,7 @@ function fslib.link(src: pathlike, dest: pathlike): () return fs.link(pathlib.format(src), pathlib.format(dest)) end -function fslib.symlink(src: pathlike, dest: pathlike): () +function fslib.symboliclink(src: pathlike, dest: pathlike): () return fs.symlink(pathlib.format(src), pathlib.format(dest)) end @@ -78,14 +79,10 @@ function fslib.copy(src: pathlike, dest: pathlike): () return fs.copy(pathlib.format(src), pathlib.format(dest)) end -function fslib.listdir(path: pathlike): { directoryentry } +function fslib.listdirectory(path: pathlike): { directoryentry } return fs.listdir(pathlib.format(path)) end -function fslib.rmdir(path: pathlike): () - return fs.rmdir(pathlib.format(path)) -end - function fslib.readfiletostring(filepath: pathlike): string return fs.readfiletostring(pathlib.format(filepath)) end @@ -94,10 +91,6 @@ function fslib.writestringtofile(filepath: pathlike, contents: string): () return fs.writestringtofile(pathlib.format(filepath), contents) end -function fslib.readasync(filepath: pathlike): string - return fs.readasync(pathlib.format(filepath)) -end - function fslib.createdirectory(path: pathlike, options: createdirectoryoptions?): () if options and options.makeparents then local parsed = pathlib.parse(path) @@ -129,7 +122,7 @@ function fslib.removedirectory(path: pathlike, options: removedirectoryoptions?) if options and options.recursive then -- Recursively delete all contents first if fslib.exists(path) and fslib.type(path) == "dir" then - local entries = fslib.listdir(path) + local entries = fslib.listdirectory(path) for _, entry in entries do local entryPath = pathlib.join(path, entry.name) if entry.type == "dir" then @@ -157,7 +150,7 @@ function fslib.walk(path: pathlike, options: walkoptions?): () -> path? end if fslib.type(current) == "dir" and options and options.recursive then - local entries = fslib.listdir(current) + local entries = fslib.listdirectory(current) for _, entry in entries do local fullPath = pathlib.join(current, entry.name) table.insert(queue, fullPath) diff --git a/tests/cli/loadbypath.test.luau b/tests/cli/loadbypath.test.luau index b5c9601f8..fcb029ad7 100644 --- a/tests/cli/loadbypath.test.luau +++ b/tests/cli/loadbypath.test.luau @@ -52,7 +52,7 @@ test.suite("Lute CLI Run", function(suite) suite:afterall(function() if fs.exists(testDirStr) then - fs.rmdir(testDirStr) + fs.removedirectory(testDirStr) end end) end) diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index eab5dd309..853fc171e 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -14,7 +14,7 @@ test.suite("FsSuite", function(suite) fs.write(h, "abc") fs.close(h) - local m = fs.stat(file) + local m = fs.metadata(file) assert.eq(m.size, 3) local hr = fs.open(file, "r") @@ -55,7 +55,7 @@ test.suite("FsSuite", function(suite) assert.eq(fs.exists(dir), true) assert.eq(fs.type(dir), "dir") - local entries = fs.listdir(tmpdir) + local entries = fs.listdirectory(tmpdir) local found = false for _, e in entries do if e.name == "subdir" then @@ -63,17 +63,7 @@ test.suite("FsSuite", function(suite) end end assert.eq(found, true) - fs.rmdir(dir) - end) - - suite:case("readasync_and_writestring", function(assert) - local file = path.join(tmpdir, "async.txt") - fs.writestringtofile(file, "async content") - - local got = fs.readasync(file) - assert.eq(got, "async content") - - fs.remove(file) + fs.removedirectory(dir) end) suite:case("link_and_symlink_and_copy", function(assert) @@ -92,7 +82,7 @@ test.suite("FsSuite", function(suite) fs.remove(dstcopy) local dstsymlink = path.join(tmpdir, "dstsymlink") - fs.symlink(srcfile, dstsymlink) + fs.symboliclink(srcfile, dstsymlink) assert.eq(fs.readfiletostring(dstsymlink), "linkcontent") fs.remove(dstsymlink) @@ -131,8 +121,8 @@ test.suite("FsSuite", function(suite) fs.createdirectory(nestedDir, { makeparents = true }) assert.eq(fs.exists(nestedDir), true) - fs.rmdir(nestedDir) - fs.rmdir(path.join(tmpdir, "nested")) + fs.removedirectory(nestedDir) + fs.removedirectory(path.join(tmpdir, "nested")) end) suite:case("createdirectory_makeparents_false", function(assert) @@ -223,7 +213,7 @@ test.suite("FsSuite", function(suite) -- Cleanup fs.remove(file) - fs.rmdir(dir) + fs.removedirectory(dir) end) suite:case("walk_directory_recursive", function(assert) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 5d06d1878..38d65ccf4 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -124,7 +124,7 @@ test.suite("Parser", function(suite) suite:case("roundtrippableAst", function(assert) local function visitDirectory(directory: string) - for _, entry in fs.listdir(directory) do + for _, entry in fs.listdirectory(directory) do if entry.type ~= "file" then continue end From 26b8e251acd9f6009c64ae982f04471ee6d9a5af Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:08:34 -0800 Subject: [PATCH 187/642] feature: add location field to Luau AST types (#608) Requiring `location` field in AST types per @skanosue 's request; fixes some type errors too! --- definitions/luau.luau | 92 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index deb398818..225d0ef2f 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -49,6 +49,7 @@ export type Pair = { node: T, separator: Token? } export type Punctuated = { Pair } export type AstLocal = { + location: span, kind: "local", name: Token, colon: Token<":">?, @@ -57,32 +58,43 @@ export type AstLocal = { } export type AstExprGroup = { + location: span, tag: "group", openparens: Token<"(">, expression: AstExpr, closeparens: Token<")">, } -export type AstExprConstantNil = Token<"nil"> & { kind: "expr", tag: "nil" } +export type AstExprConstantNil = Token<"nil"> & { location: span, kind: "expr", tag: "nil" } -export type AstExprConstantBool = Token<"true" | "false"> & { kind: "expr", tag: "boolean", value: boolean } - -export type AstExprConstantNumber = Token & { kind: "expr", tag: "number", value: number } +export type AstExprConstantBool = + Token<"true" | "false"> + & { location: span, kind: "expr", tag: "boolean", value: boolean } +export type AstExprConstantNumber = Token & { location: span, kind: "expr", tag: "number", value: number } export type AstExprConstantString = Token & { + location: span, kind: "expr", tag: "string", quotestyle: "single" | "double" | "block" | "interp", blockdepth: number, } -export type AstExprLocal = { kind: "expr", tag: "local", token: Token, ["local"]: AstLocal, upvalue: boolean } +export type AstExprLocal = { + location: span, + kind: "expr", + tag: "local", + token: Token, + ["local"]: AstLocal, + upvalue: boolean, +} -export type AstExprGlobal = { kind: "expr", tag: "global", name: Token } +export type AstExprGlobal = { location: span, kind: "expr", tag: "global", name: Token } -export type AstExprVarargs = Token<"..."> & { kind: "expr", tag: "vararg" } +export type AstExprVarargs = Token<"..."> & { location: span, kind: "expr", tag: "vararg" } export type AstExprCall = { + location: span, kind: "expr", tag: "call", func: AstExpr, @@ -94,6 +106,7 @@ export type AstExprCall = { } export type AstExprIndexName = { + location: span, kind: "expr", tag: "indexname", expression: AstExpr, @@ -103,6 +116,7 @@ export type AstExprIndexName = { } export type AstExprIndexExpr = { + location: span, kind: "expr", tag: "index", expression: AstExpr, @@ -131,6 +145,7 @@ export type AstFunctionBody = { } export type AstExprAnonymousFunction = { + location: span, kind: "expr", tag: "function", attributes: { AstAttribute }, @@ -164,6 +179,7 @@ export type AstExprTableItemGeneral = { export type AstExprTableItem = AstExprTableItemList | AstExprTableItemRecord | AstExprTableItemGeneral export type AstExprTable = { + location: span, kind: "expr", tag: "table", openbrace: Token<"{">, @@ -171,9 +187,16 @@ export type AstExprTable = { closebrace: Token<"}">, } -export type AstExprUnary = { kind: "expr", tag: "unary", operator: Token<"not" | "-" | "#">, operand: AstExpr } +export type AstExprUnary = { + location: span, + kind: "expr", + tag: "unary", + operator: Token<"not" | "-" | "#">, + operand: AstExpr, +} export type AstExprBinary = { + location: span, kind: "expr", tag: "binary", lhsoperand: AstExpr, @@ -182,6 +205,7 @@ export type AstExprBinary = { } export type AstExprInterpString = { + location: span, kind: "expr", tag: "interpolatedstring", strings: { Token }, @@ -189,6 +213,7 @@ export type AstExprInterpString = { } export type AstExprTypeAssertion = { + location: span, kind: "expr", tag: "cast", operand: AstExpr, @@ -205,6 +230,7 @@ export type AstElseIfExpr = { } export type AstExprIfElse = { + location: span, kind: "expr", tag: "conditional", ifkeyword: Token<"if">, @@ -238,6 +264,7 @@ export type AstExpr = { kind: "expr" } & ( ) export type AstStatBlock = { + location: span, kind: "stat", tag: "block", statements: { AstStat }, @@ -251,6 +278,7 @@ export type AstElseIfStat = { } export type AstStatIf = { + location: span, kind: "stat", tag: "conditional", ifkeyword: Token<"if">, @@ -264,6 +292,7 @@ export type AstStatIf = { } export type AstStatWhile = { + location: span, kind: "stat", tag: "while", whilekeyword: Token<"while">, @@ -274,6 +303,7 @@ export type AstStatWhile = { } export type AstStatRepeat = { + location: span, kind: "stat", tag: "repeat", repeatkeyword: Token<"repeat">, @@ -282,20 +312,22 @@ export type AstStatRepeat = { condition: AstExpr, } -export type AstStatBreak = Token<"break"> & { kind: "stat", tag: "break" } +export type AstStatBreak = Token<"break"> & { location: span, kind: "stat", tag: "break" } -export type AstStatContinue = Token<"continue"> & { kind: "stat", tag: "continue" } +export type AstStatContinue = Token<"continue"> & { location: span, kind: "stat", tag: "continue" } export type AstStatReturn = { + location: span, kind: "stat", tag: "return", returnkeyword: Token<"return">, expressions: Punctuated, } -export type AstStatExpr = { kind: "stat", tag: "expression", expression: AstExpr } +export type AstStatExpr = { location: span, kind: "stat", tag: "expression", expression: AstExpr } export type AstStatLocal = { + location: span, kind: "stat", tag: "local", localkeyword: Token<"local">, @@ -305,6 +337,7 @@ export type AstStatLocal = { } export type AstStatFor = { + location: span, kind: "stat", tag: "for", forkeyword: Token<"for">, @@ -321,6 +354,7 @@ export type AstStatFor = { } export type AstStatForIn = { + location: span, kind: "stat", tag: "forin", forkeyword: Token<"for">, @@ -333,6 +367,7 @@ export type AstStatForIn = { } export type AstStatAssign = { + location: span, kind: "stat", tag: "assign", variables: Punctuated, @@ -341,6 +376,7 @@ export type AstStatAssign = { } export type AstStatCompoundAssign = { + location: span, kind: "stat", tag: "compoundassign", variable: AstExpr, @@ -348,9 +384,10 @@ export type AstStatCompoundAssign = { value: AstExpr, } -export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { kind: "attribute" } +export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { location: span, kind: "attribute" } export type AstStatFunction = { + location: span, kind: "stat", tag: "function", attributes: { AstAttribute }, @@ -360,6 +397,7 @@ export type AstStatFunction = { } export type AstStatLocalFunction = { + location: span, kind: "stat", tag: "localfunction", attributes: { AstAttribute }, @@ -370,6 +408,7 @@ export type AstStatLocalFunction = { } export type AstStatTypeAlias = { + location: span, kind: "stat", tag: "typealias", export: Token<"export">?, @@ -384,6 +423,7 @@ export type AstStatTypeAlias = { } export type AstStatTypeFunction = { + location: span, kind: "stat", tag: "typefunction", export: Token<"export">?, @@ -429,6 +469,7 @@ export type AstGenericTypePack = { } export type AstTypeReference = { + location: span, kind: "type", tag: "reference", prefix: Token?, @@ -439,11 +480,19 @@ export type AstTypeReference = { closeparameters: Token<">">?, } -export type AstTypeSingletonBool = Token<"true" | "false"> & { kind: "type", tag: "boolean", value: boolean } +export type AstTypeSingletonBool = + Token<"true" | "false"> + & { location: span, kind: "type", tag: "boolean", value: boolean } -export type AstTypeSingletonString = Token & { kind: "type", tag: "string", quotestyle: "single" | "double" } +export type AstTypeSingletonString = Token & { + location: span, + kind: "type", + tag: "string", + quotestyle: "single" | "double", +} export type AstTypeTypeof = { + location: span, kind: "type", tag: "typeof", typeof: Token<"typeof">, @@ -453,6 +502,7 @@ export type AstTypeTypeof = { } export type AstTypeGroup = { + location: span, kind: "type", tag: "group", openparens: Token<"(">, @@ -460,9 +510,10 @@ export type AstTypeGroup = { closeparens: Token<")">, } -export type AstTypeOptional = Token<"?"> & { kind: "type", tag: "optional" } +export type AstTypeOptional = Token<"?"> & { location: span, kind: "type", tag: "optional" } export type AstTypeUnion = { + location: span, kind: "type", tag: "union", leading: Token<"|">?, @@ -471,6 +522,7 @@ export type AstTypeUnion = { } export type AstTypeIntersection = { + location: span, kind: "type", tag: "intersection", leading: Token<"&">?, @@ -478,6 +530,7 @@ export type AstTypeIntersection = { } export type AstTypeArray = { + location: span, kind: "type", tag: "array", openbrace: Token<"{">, @@ -520,6 +573,7 @@ export type AstTypeTableItemProperty = { export type AstTypeTableItem = AstTypeTableItemIndexer | AstTypeTableItemStringProperty | AstTypeTableItemProperty export type AstTypeTable = { + location: span, kind: "type", tag: "table", openbrace: Token<"{">, @@ -528,12 +582,14 @@ export type AstTypeTable = { } export type AstTypeFunctionParameter = { + location: span, name: Token?, colon: Token<":">?, type: AstType, } export type AstTypeFunction = { + location: span, kind: "type", tag: "function", opengenerics: Token<"<">?, @@ -563,6 +619,7 @@ export type AstType = { kind: "type" } & ( ) export type AstTypePackExplicit = { + location: span, kind: "typepack", tag: "explicit", openparens: Token<"(">?, @@ -571,9 +628,10 @@ export type AstTypePackExplicit = { closeparens: Token<")">?, } -export type AstTypePackGeneric = { kind: "typepack", tag: "generic", name: Token, ellipsis: Token<"..."> } +export type AstTypePackGeneric = { location: span, kind: "typepack", tag: "generic", name: Token, ellipsis: Token<"..."> } export type AstTypePackVariadic = { + location: span, kind: "typepack", tag: "variadic", --- May be nil when present as the vararg annotation in a function body @@ -583,7 +641,7 @@ export type AstTypePackVariadic = { export type AstTypePack = { kind: "typepack" } & (AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic) -export type AstNode = { location: span? } & (AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute) +export type AstNode = { location: span } & (AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute) export type ParseResult = { root: AstStatBlock, From efd3d9851090d0fd3c6c7cc6fcabb9cbbeb5a5ca Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 24 Nov 2025 14:56:29 -0800 Subject: [PATCH 188/642] transform: Add output directory optional argument (#610) Resolves #607 Rather than specifying a single output file as requested, I decided to allow specifying a directory instead, since `lute transform` can take any number of files to be transformed. --- lute/cli/commands/transform/init.luau | 40 +++++----- .../cli/commands/transform/lib/arguments.luau | 11 +++ lute/cli/commands/transform/lib/types.luau | 3 +- tests/cli/transform.test.luau | 73 +++++++++++++++++++ 4 files changed, 108 insertions(+), 19 deletions(-) diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index ec598fc63..f6c1d55f6 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -1,9 +1,8 @@ -local fs = require("@lute/fs") +local fs = require("@std/fs") local luau = require("@std/luau") local pathLib = require("@std/path") local printer = require("@std/syntax/printer") local syntax = require("@std/syntax") -local syntaxTypes = require("@std/syntax/types") local arguments = require("@self/lib/arguments") local files = require("@self/lib/files") @@ -82,7 +81,8 @@ local function applyMigration( migration: types.Migration, paths: { pathLib.path }, options: { [string]: string | boolean }, - dryRun: boolean + dryRun: boolean, + outputPath: string? ) local deletedPaths = {} @@ -94,6 +94,8 @@ local function applyMigration( migration.initialize(ctx) end + local outputFilePath = if outputPath then pathLib.parse(outputPath) else nil + for _, pathObj in paths do local pathStr = pathLib.format(pathObj) @@ -113,23 +115,21 @@ local function applyMigration( -- TODO: should we wrap in pcall? For now we don't do this to preserve stack trace local result = migration.transform(ctx) - if typeof(result) == "string" then - if result == types.DELETION_MARKER then - print(`Marking {pathStr} for deletion`) - table.insert(deletedPaths, pathStr) - elseif result ~= source and not dryRun then - fs.writestringtofile(pathStr, result) - end - else - local replacements = result :: syntaxTypes.replacements - - local serialized = printer.printfile(parseresult, replacements) + local toWrite = if typeof(result) == "string" then result else printer.printfile(parseresult, result) - if serialized == types.DELETION_MARKER then + if toWrite == types.DELETION_MARKER then + if outputFilePath then + print(`Skipping writing {pathStr} as it was marked for deletion`) + else print(`Marking {pathStr} for deletion`) table.insert(deletedPaths, pathStr) - elseif serialized ~= source and not dryRun then - fs.writestringtofile(pathStr, serialized) + end + elseif not dryRun then + if outputFilePath then + assert(outputFilePath ~= nil) + fs.writestringtofile(outputFilePath, toWrite) + elseif toWrite ~= source then + fs.writestringtofile(pathStr, toWrite) end end end @@ -149,6 +149,10 @@ local function main(...: string) print("Executing in dry run mode") end + if args.outputFile then + print(`Output path specified: '{args.outputFile}'`) + end + print(`Loading migration '{args.migrationPath}'`) local migration = loadMigration(args.migrationPath) @@ -162,7 +166,7 @@ local function main(...: string) local migrationOptions = processMigrationOptions(migration, args.migrationOptions) print("Applying migration") - applyMigration(migration, files, migrationOptions, args.dryRun) + applyMigration(migration, files, migrationOptions, args.dryRun, args.outputFile) print(`Processed {#files} files!`) end diff --git a/lute/cli/commands/transform/lib/arguments.luau b/lute/cli/commands/transform/lib/arguments.luau index 337a9d492..833a4e6ce 100644 --- a/lute/cli/commands/transform/lib/arguments.luau +++ b/lute/cli/commands/transform/lib/arguments.luau @@ -10,6 +10,8 @@ type Config = { --- List of file paths to process --- Note: no directory traversal or filtering has been applied on this list filePaths: { string }, + --- Path to output file (only works if a single input file is specified) + outputFile: string?, } local function parse(arguments: { string }): Config @@ -25,6 +27,7 @@ local function parse(arguments: { string }): Config migrationPath = nil :: string?, migrationOptions = {}, filePaths = {}, + outputFile = nil, } local i = 1 @@ -38,6 +41,10 @@ local function parse(arguments: { string }): Config -- Options before the codemod file are parsed as options for the tool itself if name == "dry-run" then config.dryRun = true + elseif name == "output" then + i += 1 + assert(i <= #arguments, `Missing value for '--output'`) + config.outputFile = arguments[i] else error(`Unknown flag '--{name}'`) end @@ -60,6 +67,10 @@ local function parse(arguments: { string }): Config end assert(config.migrationPath, "ASSERTION FAILED: codemodPath ~= nil") + assert( + if config.outputFile ~= nil then #config.filePaths == 1 else true, + "ASSERTION FAILED: When specifying an output file, only one input file is allowed" + ) return config end diff --git a/lute/cli/commands/transform/lib/types.luau b/lute/cli/commands/transform/lib/types.luau index 45ad573c7..ebb3f3942 100644 --- a/lute/cli/commands/transform/lib/types.luau +++ b/lute/cli/commands/transform/lib/types.luau @@ -1,4 +1,5 @@ local syntax = require("@std/syntax") +local syntaxTypes = require("@std/syntax/types") export type Context = { path: string, @@ -7,7 +8,7 @@ export type Context = { options: Options, } -export type Transformer = (Context) -> string +export type Transformer = (Context) -> string | syntaxTypes.replacements export type ConfigOption = { diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index 5b0fe2c0d..c4e571ee6 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -12,6 +12,7 @@ local b = x ~= x local lutePath = process.execpath() local tmpDir = system.tmpdir() local transformeePath = path.format(path.join(tmpDir, "transformee.luau")) +local outputPath = path.format(path.join(tmpDir, "transformee_output.luau")) test.suite("lute transform", function(suite) suite:beforeeach(function() @@ -24,6 +25,9 @@ test.suite("lute transform", function(suite) suite:aftereach(function() -- Remove the transformee file fs.remove(transformeePath) + if fs.exists(outputPath) then + fs.remove(outputPath) + end end) suite:case("transform visitor style", function(assert) @@ -73,6 +77,75 @@ test.suite("lute transform", function(suite) -- Teardown fs.remove(transformerDest) end) + + suite:case("transform output directory happy path", function(assert) + -- Setup + -- Copy examples/transformer.luau to build/transform_tests/transformer.luau + local transformerExample = path.format(path.join("examples", "query_transformer.luau")) + local transformerDest = path.format(path.join(tmpDir, "query_transformer.luau")) + fs.copy(transformerExample, transformerDest) + + -- Create output file + local h = fs.open(outputPath, "w+") + fs.close(h) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "transform", "--output", outputPath, transformerDest, transformeePath }) + -- Check + assert.eq(result.exitcode, 0) + + -- Check that the original file is unchanged + local transformeeHandle = fs.open(transformeePath, "r") + local transformeeContent = fs.read(transformeeHandle) + assert.eq(transformeeContent, TRANSFORMEE_CONTENT) + fs.close(transformeeHandle) + + -- Check that the output file has been written to + h = fs.open(outputPath, "r") + local outputContent = fs.read(h) + assert.eq(outputContent:find("x ~= x"), nil) + assert.neq(outputContent:find("math.isnan(x)", 1, true), nil) + fs.close(h) + + -- Teardown + fs.remove(transformerDest) + fs.remove(outputPath) + end) + + suite:case("transform output creates file if it doesn't exist", function(assert) + -- Setup + -- Copy examples/transformer.luau to build/transform_tests/transformer.luau + local transformerExample = path.format(path.join("examples", "query_transformer.luau")) + local transformerDest = path.format(path.join(tmpDir, "query_transformer.luau")) + fs.copy(transformerExample, transformerDest) + + -- Create output directory + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "transform", "--output", outputPath, transformerDest, transformeePath }) + -- Check + assert.eq(result.exitcode, 0) + + -- Check that the original file is unchanged + local transformeeHandle = fs.open(transformeePath, "r") + local transformeeContent = fs.read(transformeeHandle) + assert.eq(transformeeContent, TRANSFORMEE_CONTENT) + fs.close(transformeeHandle) + + -- Check that the output file has been created and written to + assert.eq(fs.exists(outputPath), true) + local h = fs.open(outputPath, "r") + local outputContent = fs.read(h) + assert.eq(outputContent:find("x ~= x"), nil) + assert.neq(outputContent:find("math.isnan(x)", 1, true), nil) + fs.close(h) + + -- Teardown + fs.remove(transformerDest) + fs.remove(outputPath) + end) end) test.run() From 40e09cf638539a0c9a7f47b7e5b9632a0565a7a8 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:05:58 -0800 Subject: [PATCH 189/642] refactor: split process.run and process.shell into separate functions (#609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As part of the API audit/cleanup, we separate `run` and `shell` execution in `@std/process` to be separate functions for more intuitive APIs 😊 Also add `assert.erroreq` function to compare a function's error message with the expected error msg --- definitions/process.luau | 17 +- examples/process.luau | 10 +- lute/process/include/lute/process.h | 2 + lute/process/src/process.cpp | 271 +++++++++++++++------------- lute/std/libs/process.luau | 7 +- lute/std/libs/test/assert.luau | 14 ++ tests/std/process.test.luau | 76 +++++++- 7 files changed, 260 insertions(+), 137 deletions(-) diff --git a/definitions/process.luau b/definitions/process.luau index a2f1069ee..9a21bad1e 100644 --- a/definitions/process.luau +++ b/definitions/process.luau @@ -1,7 +1,14 @@ -export type StdioKind = "default" | "inherit" | "none" | "" +export type StdioKind = "default" | "inherit" | "none" export type ProcessRunOptions = { - shell: (string | boolean)?, + cwd: string?, + stdio: StdioKind?, + + env: { [string]: string }?, +} + +export type ProcessSystemOptions = { + system: string?, cwd: string?, stdio: StdioKind?, @@ -29,7 +36,11 @@ function process.cwd(): string error("not implemented") end -function process.run(args: string | { string }, options: ProcessRunOptions?): ProcessResult +function process.run(args: { string }, options: ProcessRunOptions?): ProcessResult + error("not implemented") +end + +function process.system(command: string, options: ProcessSystemOptions?): ProcessResult error("not implemented") end diff --git a/examples/process.luau b/examples/process.luau index b2825d4c4..c1b5dd4f4 100644 --- a/examples/process.luau +++ b/examples/process.luau @@ -1,6 +1,8 @@ local process = require("@std/process") local task = require("@std/task") +process.system("echo hello", { cwd = {} }) + local result = process.run({ "echo", "Hello, lute!" }) print(result.exitcode) @@ -16,19 +18,19 @@ print(r1.stdout) print(r2.stdout) print(r3.stdout) -local r4 = process.run("echo Hello, lute!", { shell = true }) +local r4 = process.system("echo Hello, lute!") print(r4.stdout) -local r5 = process.run({ "echo", "$HOME" }, { env = { HOME = "/home/lute" }, shell = true }) +local r5 = process.system("echo $HOME", { env = { HOME = "/home/lute" } }) print(r5.stdout) local r6 = process.run({ "pwd" }, { cwd = "/" }) print(r6.stdout) -local r7 = process.run("echo $0", { shell = true }) +local r7 = process.system("echo $0") print(r7.stdout) -local r8 = process.run("echo $0", { shell = "/bin/sh" }) +local r8 = process.system("echo $0", { system = "/bin/sh" }) print(r8.stdout) process.exit(0) diff --git a/lute/process/include/lute/process.h b/lute/process/include/lute/process.h index 04de45120..d163b5ff1 100644 --- a/lute/process/include/lute/process.h +++ b/lute/process/include/lute/process.h @@ -15,6 +15,7 @@ namespace process { int run(lua_State* L); +int system(lua_State* L); int homedir(lua_State* L); int cwd(lua_State* L); @@ -26,6 +27,7 @@ int execpath(lua_State* L); static const luaL_Reg lib[] = { {"run", run}, + {"system", system}, {"homedir", homedir}, {"cwd", cwd}, {"exit", exitFunc}, diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index ad4affef2..701513fca 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -152,6 +152,14 @@ struct ProcessHandle } }; +struct ProcessOptions +{ + std::string cwd; + std::string stdioKind; + std::map env; + std::string customShell; // only used by system() +}; + static void onProcessExit(uv_process_t* process, int64_t exitStatus, int termSignal) { ProcessHandle* handle = static_cast(process->data); @@ -213,117 +221,9 @@ const std::string kStdioKindNone = "none"; // TODO: add forwarding // const std::string kStdioKindForward = "forward"; -int run(lua_State* L) +// helper function for run() and system() +int executionHelper(lua_State* L, std::vector args, ProcessOptions opts) { - std::vector args; - if (lua_istable(L, 1)) - { - int len = lua_objlen(L, 1); - for (int i = 1; i <= len; i++) - { - lua_rawgeti(L, 1, i); - args.push_back(lua_tostring(L, -1)); - lua_pop(L, 1); - } - } - else - { - args.push_back(lua_tostring(L, 1)); - } - - if (args.empty() || args[0].empty()) - { - luaL_error(L, "process.create requires a non-empty command"); - return 0; - } - - bool useShell = false; - std::string customShell; - std::string cwd; - std::string stdioKind; - std::map env; - - if (lua_istable(L, 2)) - { - lua_getfield(L, 2, "shell"); - if (lua_isboolean(L, -1)) - { - useShell = lua_toboolean(L, -1); - } - else if (lua_isstring(L, -1)) - { - customShell = lua_tostring(L, -1); - useShell = true; - } - - lua_pop(L, 1); - - lua_getfield(L, 2, "cwd"); - if (!lua_isnil(L, -1)) - cwd = lua_tostring(L, -1); - - lua_pop(L, 1); - - lua_getfield(L, 2, "stdio"); - if (lua_isstring(L, -1)) - { - stdioKind = lua_tostring(L, -1); - // TODO: support stdin and separate stdout/stderr kinds - } - lua_pop(L, 1); - - lua_getfield(L, 2, "env"); - if (lua_istable(L, -1)) - { - lua_pushnil(L); - while (lua_next(L, -2)) - { - env[luaL_checkstring(L, -2)] = luaL_checkstring(L, -1); - lua_pop(L, 1); - } - } - lua_pop(L, 1); - } - - if (useShell) - { - std::string commandStr = args[0]; - - for (size_t i = 1; i < args.size(); ++i) - { - commandStr += " "; - commandStr += args[i]; - } - -#ifdef _WIN32 - const char* shellVar = "COMSPEC"; - const char* shellFallback = "cmd.exe"; - const char* shellArg = "/c"; -#else - const char* shellVar = "SHELL"; - const char* shellFallback = "/bin/sh"; - const char* shellArg = "-c"; -#endif - - std::string resolvedShell; - if (customShell.empty()) - { - char shellBuffer[1024]; - size_t shellSize = sizeof(shellBuffer); - int result = uv_os_getenv(shellVar, shellBuffer, &shellSize); - resolvedShell = result == 0 ? shellBuffer : shellFallback; - } - else - { - resolvedShell = customShell; - } - - args.clear(); - args.emplace_back(resolvedShell); - args.emplace_back(shellArg); - args.emplace_back(commandStr); - } - auto handle = std::make_shared(); handle->loop = uv_default_loop(); handle->self = handle; @@ -342,7 +242,7 @@ int run(lua_State* L) std::vector envStrings; std::vector envPtr; - if (!env.empty()) + if (!opts.env.empty()) { // Copy current environment into the new environment uv_env_item_t* currentEnvItems; @@ -350,23 +250,22 @@ int run(lua_State* L) int err = uv_os_environ(¤tEnvItems, ¤tEnvCount); if (err != 0) { - luaL_error(L, "Failed to get current environment: %s", uv_strerror(err)); uv_os_free_environ(currentEnvItems, currentEnvCount); - return 0; + luaL_error(L, "Failed to get current environment: %s", uv_strerror(err)); } for (int i = 0; i < currentEnvCount; i++) { - if (currentEnvItems[i].name && currentEnvItems[i].value && env.find(currentEnvItems[i].name) == env.end()) + if (currentEnvItems[i].name && currentEnvItems[i].value && opts.env.find(currentEnvItems[i].name) == opts.env.end()) { - env[currentEnvItems[i].name] = currentEnvItems[i].value; + opts.env[currentEnvItems[i].name] = currentEnvItems[i].value; } } uv_os_free_environ(currentEnvItems, currentEnvCount); // Turn the new environment into a char** array - envStrings.reserve(env.size()); - envPtr.reserve(env.size() + 1); - for (const auto& pair : env) + envStrings.reserve(opts.env.size()); + envPtr.reserve(opts.env.size() + 1); + for (const auto& pair : opts.env) { envStrings.push_back(pair.first + "=" + pair.second); } @@ -378,9 +277,9 @@ int run(lua_State* L) options.env = envPtr.data(); } - if (!cwd.empty()) + if (!opts.cwd.empty()) { - options.cwd = cwd.c_str(); + options.cwd = opts.cwd.c_str(); } uv_pipe_init(handle->loop, &handle->stdoutPipe, 0); @@ -389,19 +288,19 @@ int run(lua_State* L) options.stdio_count = 3; uv_stdio_container_t stdio[3]; stdio[0].flags = UV_IGNORE; - if (stdioKind == kStdioKindNone) + if (opts.stdioKind == kStdioKindNone) { stdio[1].flags = UV_IGNORE; stdio[2].flags = UV_IGNORE; } - else if (stdioKind == kStdioKindInherit) + else if (opts.stdioKind == kStdioKindInherit) { stdio[1].flags = UV_INHERIT_FD; stdio[1].data.fd = fileno(stdout); stdio[2].flags = UV_INHERIT_FD; stdio[2].data.fd = fileno(stderr); } - else if (stdioKind == kStdioKindDefault || stdioKind.empty()) + else if (opts.stdioKind == kStdioKindDefault || opts.stdioKind.empty()) { stdio[1].flags = static_cast(UV_CREATE_PIPE | UV_WRITABLE_PIPE); stdio[1].data.stream = (uv_stream_t*)&handle->stdoutPipe; @@ -410,8 +309,7 @@ int run(lua_State* L) } else { - luaL_error(L, "Invalid stdio kind: %s", stdioKind.c_str()); - return 0; + luaL_error(L, "Invalid stdio kind: %s", opts.stdioKind.c_str()); } options.stdio = stdio; @@ -433,7 +331,6 @@ int run(lua_State* L) handle->closeHandles(); luaL_error(L, "Failed to spawn process: %s", uv_strerror(spawnResult)); - return 0; } uv_read_start((uv_stream_t*)&handle->stdoutPipe, allocBuffer, onPipeRead); @@ -442,6 +339,128 @@ int run(lua_State* L) return lua_yield(L, 0); } +ProcessOptions parseOptions(lua_State* L, int index) +{ + ProcessOptions opts; + + if (lua_isnoneornil(L, index)) + { + return opts; // use defaults + } + + if (!lua_istable(L, index)) + { + luaL_error(L, "process options must be a table"); + } + + lua_getfield(L, index, "system"); + if (!lua_isnil(L, -1)) + { + opts.customShell = luaL_checkstring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, index, "cwd"); + if (!lua_isnil(L, -1)) + { + opts.cwd = luaL_checkstring(L,-1); + } + lua_pop(L, 1); + + lua_getfield(L, index, "stdio"); + if (!lua_isnil(L, -1)) + { + opts.stdioKind = luaL_checkstring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, index, "env"); + if (!lua_isnil(L, -1)) + { + if (lua_istable(L, -1)) + { + lua_pushnil(L); + while (lua_next(L, -2)) + { + opts.env[luaL_checkstring(L, -2)] = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + } + else + { + luaL_error(L, "process option 'env' must be a table"); + } + } + lua_pop(L, 1); + + return opts; +} + +int run(lua_State* L) +{ + if (!lua_istable(L, 1)) + { + luaL_error(L, "process.run expects a table of arguments as the first parameter"); + } + + std::vector args; + int len = lua_objlen(L, 1); + for (int i = 1; i <= len; i++) + { + lua_rawgeti(L, 1, i); + args.push_back(luaL_checkstring(L, -1)); + lua_pop(L, 1); + } + + if (args.empty()) + { + luaL_error(L, "process.run requires a non-empty table of arguments"); + } + if (args[0].empty()) + { + luaL_error(L, "process.run requires a non-empty command as the first argument"); + } + + ProcessOptions opts = parseOptions(L, 2); + return executionHelper(L, args, opts); +} + +int system(lua_State* L) +{ + std::string command = luaL_checkstring(L, 1); + if (command.empty()) + { + luaL_error(L, "process.system requires a non-empty string as the command"); + } + + ProcessOptions opts = parseOptions(L, 2); + +#ifdef _WIN32 + const char* shellVar = "COMSPEC"; + const char* shellFallback = "cmd.exe"; + const char* shellArg = "/c"; +#else + const char* shellVar = "SHELL"; + const char* shellFallback = "/bin/sh"; + const char* shellArg = "-c"; +#endif + + std::string resolvedShell; + if (opts.customShell.empty()) + { + char shellBuffer[1024]; + size_t shellSize = sizeof(shellBuffer); + int result = uv_os_getenv(shellVar, shellBuffer, &shellSize); + resolvedShell = result == 0 ? shellBuffer : shellFallback; + } + else + { + resolvedShell = opts.customShell; + } + + return executionHelper(L, { resolvedShell, shellArg, command }, opts); +} + int homedir(lua_State* L) { auto result = uvutils::getStringFromUv(uv_os_homedir); diff --git a/lute/std/libs/process.luau b/lute/std/libs/process.luau index 671063a10..8f8a8883b 100644 --- a/lute/std/libs/process.luau +++ b/lute/std/libs/process.luau @@ -10,6 +10,7 @@ local processlib = {} export type stdiokind = process.StdioKind export type processrunoptions = process.ProcessRunOptions +export type processsystemoptions = process.ProcessSystemOptions export type processresult = process.ProcessResult export type path = pathlib.path export type pathlike = pathlib.pathlike @@ -22,10 +23,14 @@ function processlib.cwd(): path return pathlib.parse(process.cwd()) end -function processlib.run(args: string | { string }, options: processrunoptions?): processresult +function processlib.run(args: { string }, options: processrunoptions?): processresult return process.run(args, options) end +function processlib.system(command: string, options: processsystemoptions?): processresult + return process.system(command, options) +end + function processlib.exit(exitcode: number): never return process.exit(exitcode) end diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index d8affc1c2..d144c4f61 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -1,6 +1,7 @@ --!strict -- @std/test/assert -- Standard assertion library for Luau +local stringext = require("@std/stringext") local assertions = {} @@ -105,6 +106,19 @@ function assertions.buffereq(lhs: buffer, rhs: buffer) end end +function assertions.erroreq(func: () -> (), expectedErrorMessage: string) + local success, err = pcall(func) + if success then + error({ msg = `Function did not error as expected.` }) + end + + local actualErrorMessage = tostring(err) + -- pcall appends filename and line number of the error, so we check suffix only + if not stringext.hassuffix(actualErrorMessage, expectedErrorMessage) then + error({ msg = `Expected suffix of error message "{expectedErrorMessage}", but got "{actualErrorMessage}"` }) + end +end + export type asserts = typeof(assertions) return table.freeze(assertions) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index ab52b7b75..1a0a99e3f 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -15,12 +15,17 @@ test.suite("ProcessSuite", function(suite) assert.neq(pathlib.format(ex), "") end) - suite:case("run_echo_array", function(assert) + suite:case("run_echo_array_via_run", function(assert) local r = process.run({ "echo", "hello-from-lute" }) assert.eq(r.stdout, "hello-from-lute\n") end) - suite:case("run_shell_and_cwd", function(check) + suite:case("run_echo_array_via_system", function(assert) + local r = process.system("echo hello-from-lute") + assert.eq(r.stdout, "hello-from-lute\n") + end) + + suite:case("run_system_and_cwd", function(check) local rootdir: string = "/" local root: pathlib.path? = nil if system.win32 then @@ -45,7 +50,7 @@ test.suite("ProcessSuite", function(suite) }) end - local r2 = process.run({ "echo hello!" }, { shell = true }) + local r2 = process.system("echo hello!") check.tableeq(r2, { exitcode = 0, stdout = "hello!\n", @@ -63,6 +68,71 @@ test.suite("ProcessSuite", function(suite) ok = false, }) end) + + suite:case("process_run_options_error_cases", function(assert) + assert.erroreq(function() + process.run({}) + end, "process.run requires a non-empty table of arguments") + + assert.erroreq(function() + process.run({ "echo", "hello" }, { cwd = {} }) + end, "invalid argument #-1 to 'run' (string expected, got table)") + + assert.erroreq(function() + process.run({ "echo", "hello" }, { stdio = {} }) + end, "invalid argument #-1 to 'run' (string expected, got table)") + + assert.erroreq(function() + process.run({ "echo", "hello" }, { env = "not-a-table" }) + end, "process option 'env' must be a table") + + assert.erroreq(function() + process.run({ "echo", "hello" }, "invalid_option") + end, "process options must be a table") + + assert.erroreq(function() + process.run({ "echo", "hello" }, "not-a-table") + end, "process options must be a table") + + assert.erroreq(function() + process.run({ huh = 42 }) + end, "process.run requires a non-empty table of arguments") + end) + + suite:case("process_run_arraylike_table", function(assert) + -- We expect this to still work and ignore the non-string keys + local r = process.run({ "echo", "hello", foo = "bar" }) + assert.eq(r.stdout, "hello\n") + end) + + suite:case("process_system_options_error_cases", function(assert) + assert.erroreq(function() + process.system() + end, "invalid argument #1 to 'system' (string expected, got nil)") + + assert.erroreq(function() + process.system({}) + end, "invalid argument #1 to 'system' (string expected, got table)") + + assert.erroreq(function() + process.system("echo hello", { system = {} }) + end, "invalid argument #-1 to 'system' (string expected, got table)") + + assert.erroreq(function() + process.system("echo hello", { cwd = {} }) + end, "invalid argument #-1 to 'system' (string expected, got table)") + assert.erroreq(function() + process.system("echo hello", { stdio = {} }) + end, "invalid argument #-1 to 'system' (string expected, got table)") + + assert.erroreq(function() + process.system("echo hello", { env = "not-a-table" }) + end, "process option 'env' must be a table") + + assert.erroreq(function() + process.system("echo hello", "invalid_option") + end, "process options must be a table") + end) end) test.run() From 03b635c34e531f8ad80678de51f8e2c0d68b7678 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 25 Nov 2025 09:52:28 -0800 Subject: [PATCH 190/642] lute lint: support multiple lint rules (#615) Adds support for passing multiple lint rules to `lute lint` by supporting either passing a single lint rule file or a directory containing multiple rules. I converted the rule positional argument into an optional one since at some point we'll presumably want to allow calling `lute lint` and getting a bunch of default rules. --- definitions/luau.luau | 6 +- lute/cli/commands/lint/init.luau | 74 +++++++++++++++++++------ lute/luau/src/luau.cpp | 28 +++++++++- lute/std/libs/fs.luau | 5 +- tests/cli/lint.test.luau | 94 ++++++++++++++++++++++++++++---- 5 files changed, 174 insertions(+), 33 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 225d0ef2f..38bb34a55 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -1,12 +1,16 @@ local luau = {} -- this is a userdata, not a table, but it has this interface -export type span = { +type spandata = { beginline: number, begincolumn: number, endline: number, endcolumn: number, } +type spanMT = { + __lt: (a: spandata, b: spandata) -> boolean, +} +export type span = setmetatable luau.span = {} diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 38fb3e3c3..09c4bd866 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -1,33 +1,33 @@ local cli = require("./cli") local fs = require("@std/fs") local luau = require("@std/luau") -local path = require("@std/path") +local pathLib = require("@std/path") local printer = require("@self/printer") local syntax = require("@std/syntax") +local tableext = require("@std/tableext") local types = require("@self/types") -local USAGE = "Usage: lute lint [OPTIONS] [RULE] [PATH]" +local USAGE = "Usage: lute lint [OPTIONS] [PATH]" local function printHelp() print(USAGE) print([[ -Lint the specified Luau file using the specified lint rule. +Lint the specified Luau file using the specified lint rule(s). OPTIONS: -h, --help Show this help message - -RULE: - Path to a Luau file defining a lint rule. + -r, --rules [RULE] Path to a single lint rule or a folder containing lint rules. If a folder is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated as individual lint rules. PATH: Path to the Luau file to be linted. EXAMPLES: - lute lint examples/lints/almost_swapped.luau bad_swap.luau + lute lint -r examples/lints/almost_swapped.luau bad_swap.luau + lute lint -r examples/lints/ lintee.luau ]]) end -local function loadLintRule(path: path.pathlike): types.LintRule +local function loadLintRule(path: string): types.LintRule local loaded = luau.loadbypath(path) assert(loaded, `{path} failed to require`) assert(typeof(loaded) == "table", `{path} must return a table`) @@ -37,13 +37,43 @@ local function loadLintRule(path: path.pathlike): types.LintRule `{path} must return a table with a 'lint' function property` ) - return loaded :: types.LintRule -- We have to cast because we can't assert that loaded.lint is a more specific functio type + return loaded :: types.LintRule -- We have to cast because we can only assert that loaded.lint is the top function type, rather than a more specific function type like (ast: syntax.AstStatBlock, sourcepath: path.path) -> { LintViolation } +end + +local function loadLintRules(path: string): { types.LintRule } + assert(fs.exists(path), `Lint rule at path '{path}' does not exist`) + + local rules = {} + + local queue: { string } = { path } + while #queue > 0 do + local currentPath = table.remove(queue, 1) + assert(currentPath, "We should never pop from an empty queue") + + if fs.metadata(currentPath).type == "dir" then + local currentPathObj = pathLib.parse(currentPath) + local initPath = pathLib.format(pathLib.join(currentPathObj, "init.luau")) + if fs.exists(initPath) then + table.insert(rules, loadLintRule(initPath)) + else + local entries = fs.listdirectory(currentPath) + local childPaths = tableext.map(entries, function(entry) + return pathLib.format(pathLib.join(currentPathObj, entry.name)) + end) + tableext.extend(queue, childPaths) + end + elseif pathLib.extname(currentPath) == ".luau" then + table.insert(rules, loadLintRule(currentPath)) + end + end + + return rules end local function main(...: string) local args = cli.parser() - args:add("rule", "positional", { help = "Linting rule to apply" }) + args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) args:add("input", "positional", { help = "Input file to be linted" }) args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) -- TODO: handle multiple rules, multiple input files, ignore blobs, json output option @@ -55,9 +85,10 @@ local function main(...: string) return end - local rulePath = args:get("rule") + local rulePath = args:get("rules") if rulePath == nil then - print("Error: No lint rule specified.\n\n" .. USAGE .. "\nUse --help for more information.") + -- TODO: support default rules + print("Error: No lint rules specified.\n\n" .. USAGE .. "\nUse --help for more information.") return end @@ -66,10 +97,10 @@ local function main(...: string) print("Error: No input file specified.\n\n" .. USAGE .. "\nUse --help for more information.") return end - local inputFilePath = path.parse(inputFile) + local inputFilePath = pathLib.parse(inputFile) - print(`Loading lint rule from '{rulePath}'`) - local lintRule = loadLintRule(rulePath) + print(`Loading lint rule(s) from '{rulePath}'`) + local lintRules = loadLintRules(rulePath) print(`Reading input file '{inputFile}'`) local fileContent = fs.readfiletostring(inputFilePath) @@ -77,8 +108,17 @@ local function main(...: string) print(`Parsing input file '{inputFile}'`) local ast = syntax.parseblock(fileContent) - print("Applying lint rule") - local violations = lintRule.lint(ast, inputFilePath) + print("Applying lint rules") + local violations: { types.LintViolation } = {} + for _, rule in lintRules do + tableext.extend(violations, rule.lint(ast, inputFilePath)) + end + table.sort( + violations, + function(a: types.LintViolation, b: types.LintViolation) -- LUAUFIX: bidirecitonal inference should mean that annotations on a and b aren't needed + return a.location < b.location + end + ) print(`Printing {#violations} violations\n`) printer.printLints(violations, fileContent) diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index f7ecc11bb..ca8a27d71 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -201,6 +201,27 @@ static int indexSpan(lua_State* L) return 0; } +static int ltSpan(lua_State* L) +{ + const Span* lhs = static_cast(luaL_checkudata(L, 1, kSpanType)); + const Span* rhs = static_cast(luaL_checkudata(L, 2, kSpanType)); + + // Compare beginnings, and if they're equal, compare ends + if (lhs->beginLine < rhs->beginLine || (lhs->beginLine == rhs->beginLine && lhs->beginColumn < rhs->beginColumn)) + lua_pushboolean(L, 1); + else if (lhs->beginLine == rhs->beginLine && lhs->beginColumn == rhs->beginColumn) + { + if (lhs->endLine < rhs->endLine || (lhs->endLine == rhs->endLine && lhs->endColumn < rhs->endColumn)) + lua_pushboolean(L, 1); + else + lua_pushboolean(L, 0); + } + else + lua_pushboolean(L, 0); + + return 1; +} + struct AstSerialize : public Luau::AstVisitor { lua_State* L; @@ -601,8 +622,8 @@ struct AstSerialize : public Luau::AstVisitor size_t textLength = strlen(text); - Luau::Position endPosition{ position.line, position.column + static_cast(textLength) }; - serialize(Luau::Location { position, endPosition }); + Luau::Position endPosition{position.line, position.column + static_cast(textLength)}; + serialize(Luau::Location{position, endPosition}); lua_setfield(L, -2, "location"); lua_pushlstring(L, text, textLength); @@ -2856,6 +2877,9 @@ static int initLuauLibrary(lua_State* L) lua_pushcfunction(L, luau::indexSpan, "span.__index"); lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, luau::ltSpan, "span.__lt"); + lua_setfield(L, -2, "__lt"); + lua_setreadonly(L, -1, 1); lua_pop(L, 1); diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 573b955dc..445ef6d35 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -18,6 +18,7 @@ export type watchhandle = fs.WatchHandle export type watchevent = fs.WatchEvent type pathlike = pathlib.pathlike +type path = pathlib.path export type createdirectoryoptions = { makeparents: boolean?, @@ -140,7 +141,7 @@ end -- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. function fslib.walk(path: pathlike, options: walkoptions?): () -> path? - local queue = { pathlib.parse(path) } + local queue = { path } return function() while #queue > 0 do @@ -149,7 +150,7 @@ function fslib.walk(path: pathlike, options: walkoptions?): () -> path? return nil end - if fslib.type(current) == "dir" and options and options.recursive then + if fslib.type(current) == "dir" and options and options.recursive then -- LUAUFIX: error on options.recursive because it thinks options could still be nil local entries = fslib.listdirectory(current) for _, entry in entries do local fullPath = pathlib.join(current, entry.name) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index b797261e0..a7e5c8c9f 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -6,34 +6,46 @@ local test = require("@std/test") local lutePath = path.format(process.execpath()) local tmpDir = system.tmpdir() +local rulesDir = path.format(path.join(".", "lints")) +local almostSwappedExample = path.format(path.join("examples", "lints", "almost_swapped.luau")) +local divideByZeroExample = path.format(path.join("examples", "lints", "divide_by_zero.luau")) + +local USAGE = "Usage: lute lint [OPTIONS] [PATH]" test.suite("lute lint", function(suite) + suite:beforeeach(function() + -- A test that creates rulesDir which fails won't cleanup after itself + if fs.exists(rulesDir) then + fs.removedirectory(rulesDir, { recursive = true }) + end + end) + suite:case("lute lint help message", function(assert) local result = process.run({ lutePath, "lint", "-h" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Usage: lute lint [OPTIONS] [RULE] [PATH]", 1, true), nil) + assert.neq(result.stdout:find(USAGE, 1, true), nil) result = process.run({ lutePath, "lint", "--h" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Usage: lute lint [OPTIONS] [RULE] [PATH]", 1, true), nil) + assert.neq(result.stdout:find(USAGE, 1, true), nil) result = process.run({ lutePath, "lint", "--help" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Usage: lute lint [OPTIONS] [RULE] [PATH]", 1, true), nil) + assert.neq(result.stdout:find(USAGE, 1, true), nil) end) suite:case("lute lint no rule", function(assert) local result = process.run({ lutePath, "lint" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Error: No lint rule specified.", 1, true), nil) + assert.neq(result.stdout:find("Error: No lint rules specified.", 1, true), nil) end) suite:case("lute lint no rule", function(assert) - local result = process.run({ lutePath, "lint", "a_rule" }) + local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) assert.eq(result.exitcode, 0) assert.neq(result.stdout:find("Error: No input file specified.", 1, true), nil) @@ -41,8 +53,6 @@ test.suite("lute lint", function(suite) suite:case("divide_by_zero", function(assert) -- Setup - local rule = path.format(path.join("examples", "lints", "divide_by_zero.luau")) - -- Create a file that violates the rule local violatorPath = path.format(path.join(tmpDir, "violator.luau")) fs.writestringtofile( @@ -54,7 +64,7 @@ local x = 1 / 0 -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", rule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) -- Check assert.eq(result.exitcode, 0) @@ -75,8 +85,6 @@ violator.luau:1:11-16 ── suite:case("almost_swapped", function(assert) -- Setup - local rule = path.format(path.join("examples", "lints", "almost_swapped.luau")) - -- Create a file that violates the rule local violatorPath = path.format(path.join(tmpDir, "violator.luau")) fs.writestringtofile( @@ -103,7 +111,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", rule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", almostSwappedExample, violatorPath }) -- Check assert.eq(result.exitcode, 0) @@ -152,6 +160,70 @@ violator.luau:14:2-15:17 ── -- Teardown fs.remove(violatorPath) end) + + suite:case("lute lint multiple rules", function(assert) + -- Setup + fs.createdirectory(rulesDir) + -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau + fs.copy(almostSwappedExample, path.join(rulesDir, "almost_swapped.luau")) + -- Copy divide_by_zero to rulesDir/divide_by_zero/init.luau + local divideByZeroDir = path.join(rulesDir, "divide_by_zero") + fs.createdirectory(divideByZeroDir) + fs.copy(divideByZeroExample, path.join(divideByZeroDir, "init.luau")) + fs.writestringtofile( + path.join(rulesDir, "bogus.txt"), + [[ +not a luau file +]] + ) + + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", rulesDir, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.neq(result.stdout:find("warning[divide_by_zero]: Division by zero detected.", 1, true), nil) + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + assert.neq( + result.stdout:find("warning[almost_swapped]: This looks like a failed attempt to swap.", 1, true), + nil + ) + expected = [[ +violator.luau:3:1-4:6 ── + │ + 3 │ ╭ a = b + 4 │ │ b = a + │ ╰─────^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + -- Teardown + fs.remove(violatorPath) + fs.removedirectory(rulesDir, { recursive = true }) + end) end) test.run() From 4cca9b84c8ccd2ff69c25ff790c86e5ef29d63cc Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:53:45 -0500 Subject: [PATCH 191/642] Add optional `msg` param to asserts (#614) Supporting a custom ``msg`` in the ``assert`` functions that the ``test`` framework leans on would add nice flexibility for users. I ended up hacking this into a separate project, so I figure it just makes sense to add --- lute/std/libs/test/assert.luau | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index d144c4f61..87081755f 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -5,22 +5,22 @@ local stringext = require("@std/stringext") local assertions = {} -function assertions.eq(lhs: T, rhs: T) +function assertions.eq(lhs: T, rhs: T, msg: string?) if lhs ~= rhs then - error({ msg = `{lhs} ~= {rhs}` }) + error({ msg = msg or `{lhs} ~= {rhs}` }) end end -function assertions.neq(lhs: T, rhs: T) +function assertions.neq(lhs: T, rhs: T, msg: string?) if lhs == rhs then - error({ msg = `{lhs} == {rhs}` }) + error({ msg = msg or `{lhs} == {rhs}` }) end end -function assertions.errors(callback: () -> ()) +function assertions.errors(callback: () -> (), msg: string?) local success = pcall(callback) if success then - error({ msg = `{callback} did not throw error.` }) + error({ msg = msg or `{callback} did not throw error.` }) end end From 010c26049fe00a69e4dbd947103ce094dda7339b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:56:46 +0000 Subject: [PATCH 192/642] Update Luau to 0.701 (#612) **Luau**: Updated from `0.700` to `0.701` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.701 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> --- extern/luau.tune | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 7816cc340..3ef75a384 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.700" -revision = "3e1c94ec2c1a077497b7ac21f580745c7aeeefae" +branch = "0.701" +revision = "535f92589bdc304c8a2012d6cfad5e7b9faff2f7" From e97e76f74fca3a70c01367eadf10f1ec75dbec43 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:57:03 +0000 Subject: [PATCH 193/642] Update Lute to 0.1.0-nightly.20251121 (#613) **Lute**: Updated from `0.1.0-nightly.20251114` to `0.1.0-nightly.20251121` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20251121 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Annie Tang <98965493+annieetang@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 633b6ec08..2a9a55dab 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251114" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251121" } diff --git a/rokit.toml b/rokit.toml index 6a7c7366c..dc3c878c6 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20251114" +lute = "luau-lang/lute@0.1.0-nightly.20251121" From 1a77294c89c7163449543d24f353b139f792e230 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 25 Nov 2025 14:19:43 -0800 Subject: [PATCH 194/642] lute lint: gracefully handle rules that error (#618) Currently, if a single lint rule errors, it'll exit the whole lint. If we wrap the lint rule calls in `pcall` instead, we can gracefully continue calling the other lint rules too. --- lute/cli/commands/lint/init.luau | 10 ++++-- tests/cli/lint.test.luau | 62 ++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 09c4bd866..41f2683a0 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -111,11 +111,17 @@ local function main(...: string) print("Applying lint rules") local violations: { types.LintViolation } = {} for _, rule in lintRules do - tableext.extend(violations, rule.lint(ast, inputFilePath)) + local success, err = pcall(rule.lint, ast, inputFilePath) + if success then + -- On success, err contains the returned violations + tableext.extend(violations, err) + else + print(`Error applying lint rule '{rule.name}': {err}`) + end end table.sort( violations, - function(a: types.LintViolation, b: types.LintViolation) -- LUAUFIX: bidirecitonal inference should mean that annotations on a and b aren't needed + function(a: types.LintViolation, b: types.LintViolation) -- LUAUFIX: bidirectional inference should mean that annotations on a and b aren't needed return a.location < b.location end ) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index a7e5c8c9f..a1f0a9ce5 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -224,6 +224,68 @@ violator.luau:3:1-4:6 ── fs.remove(violatorPath) fs.removedirectory(rulesDir, { recursive = true }) end) + + suite:case("lute lint multiple rules but one errors", function(assert) + -- Setup + fs.createdirectory(rulesDir) + -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau + fs.copy(almostSwappedExample, path.join(rulesDir, "almost_swapped.luau")) + -- Create a rule that fails + fs.writestringtofile( + path.join(rulesDir, "erroring_rule.luau"), + [[ +return table.freeze({ + name = "AlwaysErrors", + lint = function(ast, sourcepath) + error("This rule always errors") + end, +}) +]] + ) + + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", rulesDir, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.neq( + result.stdout:find("warning[almost_swapped]: This looks like a failed attempt to swap.", 1, true), + nil + ) + local expected = [[ +violator.luau:1:1-2:6 ── + │ + 1 │ ╭ a = b + 2 │ │ b = a + │ ╰─────^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + assert.neq( + result.stdout:find( + "Error applying lint rule 'AlwaysErrors': %.[/\\]lints[/\\]erroring_rule%.luau:4: This rule always errors", + 1 + ), + nil + ) + + -- Teardown + fs.remove(violatorPath) + fs.removedirectory(rulesDir, { recursive = true }) + end) end) test.run() From 802532bcb2865ed1313ff9ea6f98bcd4a884b906 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 25 Nov 2025 15:05:00 -0800 Subject: [PATCH 195/642] lute lint: Support multiple lintee files (#617) Update lute lint to support multiple input files by assuming that all non option arguments are paths to be linted. If a directory is passed, we recursively walk the directory for all the files to be linted. We error handle any files that error while linting so we can keep linting other files. --- lute/cli/commands/lint/init.luau | 93 +++++++++++++------ lute/cli/commands/lint/printer.luau | 7 +- tests/cli/lint.test.luau | 136 +++++++++++++++++++++++++++- 3 files changed, 202 insertions(+), 34 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 41f2683a0..64cb8132e 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -7,7 +7,7 @@ local syntax = require("@std/syntax") local tableext = require("@std/tableext") local types = require("@self/types") -local USAGE = "Usage: lute lint [OPTIONS] [PATH]" +local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" local function printHelp() print(USAGE) @@ -18,12 +18,12 @@ OPTIONS: -h, --help Show this help message -r, --rules [RULE] Path to a single lint rule or a folder containing lint rules. If a folder is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated as individual lint rules. -PATH: - Path to the Luau file to be linted. +PATHS: + Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. EXAMPLES: lute lint -r examples/lints/almost_swapped.luau bad_swap.luau - lute lint -r examples/lints/ lintee.luau + lute lint -r examples/lints/ lintee.luau src_code/ ]]) end @@ -70,11 +70,39 @@ local function loadLintRules(path: string): { types.LintRule } return rules end +local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule }): { types.LintViolation } + print(`Reading input file '{inputFilePath}'`) + local fileContent = fs.readfiletostring(inputFilePath) + + print(`Parsing input file '{inputFilePath}'`) + local ast = syntax.parseblock(fileContent) + + print("Applying lint rules") + local violations: { types.LintViolation } = {} + for _, rule in lintRules do + local success, err = pcall(rule.lint, ast, inputFilePath) + if success then + -- On success, err contains the returned violations + tableext.extend(violations, err) + else + print(`Error applying lint rule '{rule.name}': {err}`) + end + end + + table.sort( + violations, + function(a: types.LintViolation, b: types.LintViolation) -- LUAUFIX: bidirecitonal inference should mean that annotations on a and b aren't needed + return a.location < b.location + end + ) + + return violations +end + local function main(...: string) local args = cli.parser() args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) - args:add("input", "positional", { help = "Input file to be linted" }) args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) -- TODO: handle multiple rules, multiple input files, ignore blobs, json output option @@ -92,42 +120,47 @@ local function main(...: string) return end - local inputFile = args:get("input") - if inputFile == nil then - print("Error: No input file specified.\n\n" .. USAGE .. "\nUse --help for more information.") + local inputFiles = args:forwarded() + if inputFiles == nil or #inputFiles == 0 then + print("Error: No input files specified.\n\n" .. USAGE .. "\nUse --help for more information.") return end - local inputFilePath = pathLib.parse(inputFile) print(`Loading lint rule(s) from '{rulePath}'`) local lintRules = loadLintRules(rulePath) - print(`Reading input file '{inputFile}'`) - local fileContent = fs.readfiletostring(inputFilePath) + local allViolations = {} - print(`Parsing input file '{inputFile}'`) - local ast = syntax.parseblock(fileContent) + for _, inputPath in inputFiles do + local walker = fs.walk(inputPath, { recursive = true }) - print("Applying lint rules") - local violations: { types.LintViolation } = {} - for _, rule in lintRules do - local success, err = pcall(rule.lint, ast, inputFilePath) - if success then - -- On success, err contains the returned violations - tableext.extend(violations, err) - else - print(`Error applying lint rule '{rule.name}': {err}`) + local curr = walker() + while curr ~= nil do + local inputFilePath = pathLib.parse(curr) + + local extension = pathLib.extname(inputFilePath) + if extension == ".luau" or extension == ".lua" then + local success, err = pcall(lintFile, inputFilePath, lintRules) + if success then + -- Violations are returned as the second return value from pcall + allViolations[inputFilePath] = err + else + print(`Error linting file '{inputFilePath}': {err}`) + end + else + print(`Skipping non-Luau file '{inputFilePath}'`) + end + + curr = walker() end end - table.sort( - violations, - function(a: types.LintViolation, b: types.LintViolation) -- LUAUFIX: bidirectional inference should mean that annotations on a and b aren't needed - return a.location < b.location - end - ) - print(`Printing {#violations} violations\n`) - printer.printLints(violations, fileContent) + print(`Printing violations from {#allViolations} files\n`) + for sourcePath, violations in allViolations do + if #violations > 0 then + printer.printLints(violations, sourcePath) + end + end end main(...) diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau index 18e1f0299..73362598e 100644 --- a/lute/cli/commands/lint/printer.luau +++ b/lute/cli/commands/lint/printer.luau @@ -1,3 +1,4 @@ +local fs = require("@std/fs") local path = require("@std/path") local types = require("./types") @@ -60,7 +61,7 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () end end -function printerLib.printLints(lints: { types.LintViolation }, sourceContent: string): () +local function printLints(lints: { types.LintViolation }, sourceContent: string): () local sourceLines = sourceContent:split("\n") for _, lint in lints do @@ -68,4 +69,8 @@ function printerLib.printLints(lints: { types.LintViolation }, sourceContent: st end end +function printerLib.printLints(lints: { types.LintViolation }, sourcePath: path.path): () + printLints(lints, fs.readfiletostring(sourcePath)) +end + return table.freeze(printerLib) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index a1f0a9ce5..50011df3f 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -10,7 +10,7 @@ local rulesDir = path.format(path.join(".", "lints")) local almostSwappedExample = path.format(path.join("examples", "lints", "almost_swapped.luau")) local divideByZeroExample = path.format(path.join("examples", "lints", "divide_by_zero.luau")) -local USAGE = "Usage: lute lint [OPTIONS] [PATH]" +local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" test.suite("lute lint", function(suite) suite:beforeeach(function() @@ -18,6 +18,11 @@ test.suite("lute lint", function(suite) if fs.exists(rulesDir) then fs.removedirectory(rulesDir, { recursive = true }) end + + local modulePath = path.join(tmpDir, "module") + if fs.exists(modulePath) then + fs.removedirectory(modulePath, { recursive = true }) + end end) suite:case("lute lint help message", function(assert) @@ -44,11 +49,11 @@ test.suite("lute lint", function(suite) assert.neq(result.stdout:find("Error: No lint rules specified.", 1, true), nil) end) - suite:case("lute lint no rule", function(assert) + suite:case("lute lint no input file", function(assert) local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Error: No input file specified.", 1, true), nil) + assert.neq(result.stdout:find("Error: No input files specified.", 1, true), nil) end) suite:case("divide_by_zero", function(assert) @@ -286,6 +291,131 @@ violator.luau:1:1-2:6 ── fs.remove(violatorPath) fs.removedirectory(rulesDir, { recursive = true }) end) + + suite:case("lute lint multiple input files", function(assert) + -- Setup + -- Create multiple files that violate the rule + local violatorPath1 = path.format(path.join(tmpDir, "violator1.luau")) + fs.writestringtofile( + violatorPath1, + [[ +local x = 1 / 0 + +a = b +b = a + ]] + ) + + local modulePath = path.join(tmpDir, "module") + fs.createdirectory(modulePath) + local violatorPath2 = path.format(path.join(modulePath, "violator2.lua")) + fs.writestringtofile( + violatorPath2, + [[ +local y = 10 / 0 + +local x = 0 +local y = "hello" +x = y +y = x + ]] + ) + fs.writestringtofile(path.format(path.join(modulePath, "not_luau.txt")), "blablabla") + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", "examples/lints", violatorPath1, path.format(modulePath) }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.eq(#result.stdout:split("warning[divide_by_zero]: Division by zero detected."), 3) + local expected = [[ +violator1.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + expected = [[ +violator2.lua:1:11-17 ── + │ + 1 │ local y = 10 / 0 + │ ^^^^^^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + assert.eq(#result.stdout:split("warning[almost_swapped]: This looks like a failed attempt to swap."), 3) + expected = [[ +violator1.luau:3:1-4:6 ── + │ + 3 │ ╭ a = b + 4 │ │ b = a + │ ╰─────^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + expected = [[ +violator2.lua:5:1-6:6 ── + │ + 5 │ ╭ x = y + 6 │ │ y = x + │ ╰─────^ + │ +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + expected = "Skipping non%-Luau file '.+[/\\]module[/\\]not_luau%.txt'" + assert.neq(result.stdout:find(expected, 1, false), nil) + + -- Teardown + fs.remove(violatorPath1) + fs.removedirectory(modulePath, { recursive = true }) + end) + + suite:case("lute lint multiple files with errors recovers", function(assert) + -- Setup + -- Create multiple files that violate the rule + local lintee = path.format(path.join(tmpDir, "lintee.luau")) + fs.writestringtofile(lintee, "bogus") + + local violatorPath = path.format(path.join(tmpDir, "violator1.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", "examples/lints", lintee, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + local expected = [[ +lintee.luau': @std/syntax/parser.luau:12: parsing failed: +(0, 0) - (0, 5): Incomplete statement: expected assignment or a function call +]] + assert.neq(result.stdout:find(expected, 1, true), nil) + + assert.neq(result.stdout:find("warning[divide_by_zero]: Division by zero detected.", 1, true), nil) + assert.neq( + result.stdout:find("warning[almost_swapped]: This looks like a failed attempt to swap.", 1, true), + nil + ) + + -- Teardown + fs.remove(lintee) + fs.remove(violatorPath) + end) end) test.run() From 32d689e42023f7c739d2240c31dc2765a07581b6 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 26 Nov 2025 11:15:51 -0800 Subject: [PATCH 196/642] add assert.strcontains (#619) what the title says also updated test files which were using the `assert.neq(str:find(...), nil)` pattern to use the new helper instead --- lute/std/libs/test/assert.luau | 18 +++++++++++ tests/cli/lint.test.luau | 59 ++++++++++++++-------------------- tests/cli/test.test.luau | 37 ++++++++------------- tests/cli/transform.test.luau | 8 ++--- 4 files changed, 60 insertions(+), 62 deletions(-) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index 87081755f..728d31e5a 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -119,6 +119,24 @@ function assertions.erroreq(func: () -> (), expectedErrorMessage: string) end end +function assertions.strcontains(haystack: string, needle: string, msg: string?, instances: number?) + if instances == nil then + instances = 1 + end + + assert(instances > 0, "instances must be greater than 0") -- LUAUFIX: Type stating should mean this comparison doesn't error. + + if instances == 1 then + if haystack:find(needle, 1, true) == nil then + error(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) + end + else + if #haystack:split(needle) ~= instances + 1 then -- LUAUFIX: Type stating should mean this addition doesn't error. + error(if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.`) + end + end +end + export type asserts = typeof(assertions) return table.freeze(assertions) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 50011df3f..3c7d18245 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -29,31 +29,31 @@ test.suite("lute lint", function(suite) local result = process.run({ lutePath, "lint", "-h" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find(USAGE, 1, true), nil) + assert.strcontains(result.stdout, USAGE) result = process.run({ lutePath, "lint", "--h" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find(USAGE, 1, true), nil) + assert.strcontains(result.stdout, USAGE) result = process.run({ lutePath, "lint", "--help" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find(USAGE, 1, true), nil) + assert.strcontains(result.stdout, USAGE) end) suite:case("lute lint no rule", function(assert) local result = process.run({ lutePath, "lint" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Error: No lint rules specified.", 1, true), nil) + assert.strcontains(result.stdout, "Error: No lint rules specified.") end) suite:case("lute lint no input file", function(assert) local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Error: No input files specified.", 1, true), nil) + assert.strcontains(result.stdout, "Error: No input files specified.") end) suite:case("divide_by_zero", function(assert) @@ -74,7 +74,7 @@ local x = 1 / 0 -- Check assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("warning[divide_by_zero]: Division by zero detected.", 1, true), nil) + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") local expected = [[ violator.luau:1:11-16 ── │ @@ -82,7 +82,7 @@ violator.luau:1:11-16 ── │ ^^^^^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) -- Teardown fs.remove(violatorPath) @@ -121,7 +121,7 @@ end -- Check assert.eq(result.exitcode, 0) -- We expect 4 warnings, so stdout should be split into 5 parts - assert.eq(#result.stdout:split("warning[almost_swapped]: This looks like a failed attempt to swap."), 5) + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.", nil, 4) local expected = [[ violator.luau:1:1-2:6 ── │ @@ -130,7 +130,7 @@ violator.luau:1:1-2:6 ── │ ╰─────^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) expected = [[ violator.luau:6:1-7:6 ── @@ -140,7 +140,7 @@ violator.luau:6:1-7:6 ── │ ╰─────^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) expected = [[ violator.luau:10:1-11:10 ── @@ -150,7 +150,7 @@ violator.luau:10:1-11:10 ── │ ╰─────────^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) expected = [[ violator.luau:14:2-15:17 ── @@ -160,7 +160,7 @@ violator.luau:14:2-15:17 ── │ ╰────────────────^ │ ]] -- todo: address tab shenanigans - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) -- Teardown fs.remove(violatorPath) @@ -201,7 +201,7 @@ b = a -- Check assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("warning[divide_by_zero]: Division by zero detected.", 1, true), nil) + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") local expected = [[ violator.luau:1:11-16 ── │ @@ -209,12 +209,9 @@ violator.luau:1:11-16 ── │ ^^^^^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) - assert.neq( - result.stdout:find("warning[almost_swapped]: This looks like a failed attempt to swap.", 1, true), - nil - ) + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") expected = [[ violator.luau:3:1-4:6 ── │ @@ -223,7 +220,7 @@ violator.luau:3:1-4:6 ── │ ╰─────^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) -- Teardown fs.remove(violatorPath) @@ -265,10 +262,7 @@ b = a -- Check assert.eq(result.exitcode, 0) - assert.neq( - result.stdout:find("warning[almost_swapped]: This looks like a failed attempt to swap.", 1, true), - nil - ) + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") local expected = [[ violator.luau:1:1-2:6 ── │ @@ -277,7 +271,7 @@ violator.luau:1:1-2:6 ── │ ╰─────^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) assert.neq( result.stdout:find( @@ -337,7 +331,7 @@ violator1.luau:1:11-16 ── │ ^^^^^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) expected = [[ violator2.lua:1:11-17 ── @@ -346,7 +340,7 @@ violator2.lua:1:11-17 ── │ ^^^^^^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) assert.eq(#result.stdout:split("warning[almost_swapped]: This looks like a failed attempt to swap."), 3) expected = [[ @@ -357,7 +351,7 @@ violator1.luau:3:1-4:6 ── │ ╰─────^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) expected = [[ violator2.lua:5:1-6:6 ── @@ -367,7 +361,7 @@ violator2.lua:5:1-6:6 ── │ ╰─────^ │ ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) expected = "Skipping non%-Luau file '.+[/\\]module[/\\]not_luau%.txt'" assert.neq(result.stdout:find(expected, 1, false), nil) @@ -404,13 +398,10 @@ b = a lintee.luau': @std/syntax/parser.luau:12: parsing failed: (0, 0) - (0, 5): Incomplete statement: expected assignment or a function call ]] - assert.neq(result.stdout:find(expected, 1, true), nil) + assert.strcontains(result.stdout, expected) - assert.neq(result.stdout:find("warning[divide_by_zero]: Division by zero detected.", 1, true), nil) - assert.neq( - result.stdout:find("warning[almost_swapped]: This looks like a failed attempt to swap.", 1, true), - nil - ) + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") -- Teardown fs.remove(lintee) diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index a68aa45ae..ade9a9f30 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -23,7 +23,7 @@ test.suite("lute test", function(suite) -- Check that it exits successfully and prints help assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Usage:", 1, true), nil) + assert.strcontains(result.stdout, "Usage:") end) suite:case("lute test runs successfully", function(assert) @@ -71,17 +71,10 @@ test.run() assert.eq(result.exitcode, 1) assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number - assert.neq( - result.stdout:find(`Failed with: Runtime error in nil_field_suite.access_field_of_nil in:`, 1, true), - nil - ) - assert.neq( - result.stdout:find( -- filename comes from debug.info, which uses forward slashes even on Windows - `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:6`, - 1, - true - ), - nil + assert.strcontains(result.stdout, `Failed with: Runtime error in nil_field_suite.access_field_of_nil in:`) + assert.strcontains( + result.stdout, + `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:6` ) fs.remove(testFilePath) @@ -113,14 +106,10 @@ test.run() assert.eq(result.exitcode, 1) assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number - assert.neq(result.stdout:find(`Failed with: Runtime error in access_field_of_nil in:`, 1, true), nil) - assert.neq( - result.stdout:find( -- filename comes from debug.info, which uses forward slashes even on Windows - `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:5`, - 1, - true - ), - nil + assert.strcontains(result.stdout, `Failed with: Runtime error in access_field_of_nil in:`) + assert.strcontains( + result.stdout, + `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:5` ) fs.remove(testFilePath) @@ -148,7 +137,7 @@ test.run() -- Check that the output specifies the inequal values assert.eq(result.exitcode, 1) - assert.neq(result.stdout:find("eq: 1 ~= 2", 1, true), nil) + assert.strcontains(result.stdout, "eq: 1 ~= 2") end) suite:case("assert.eq_error_message_in_suite", function(assert) @@ -175,7 +164,7 @@ test.run() -- Check that the output specifies the inequal values assert.eq(result.exitcode, 1) - assert.neq(result.stdout:find("eq: 1 ~= 2", 1, true), nil) + assert.strcontains(result.stdout, "eq: 1 ~= 2") end) suite:case("assert.neq_error_message_in_case", function(assert) @@ -200,7 +189,7 @@ test.run() -- Check that the output specifies the inequal values assert.eq(result.exitcode, 1) - assert.neq(result.stdout:find("neq: 1 == 1", 1, true), nil) + assert.strcontains(result.stdout, "neq: 1 == 1") end) suite:case("assert.neq_error_message_in_suite", function(assert) @@ -227,7 +216,7 @@ test.run() -- Check that the output specifies the inequal values assert.eq(result.exitcode, 1) - assert.neq(result.stdout:find("neq: 1 == 1", 1, true), nil) + assert.strcontains(result.stdout, "neq: 1 == 1") end) end) diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index c4e571ee6..405baa92a 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -47,7 +47,7 @@ test.suite("lute transform", function(suite) local transformeeHandle = fs.open(transformeePath, "r") local transformeeContent = fs.read(transformeeHandle) assert.eq(transformeeContent:find("x ~= x"), nil) - assert.neq(transformeeContent:find("math.isnan(x)", 1, true), nil) + assert.strcontains(transformeeContent, "math.isnan(x)") fs.close(transformeeHandle) -- Teardown @@ -71,7 +71,7 @@ test.suite("lute transform", function(suite) local transformeeHandle = fs.open(transformeePath, "r") local transformeeContent = fs.read(transformeeHandle) assert.eq(transformeeContent:find("x ~= x"), nil) - assert.neq(transformeeContent:find("math.isnan(x)", 1, true), nil) + assert.strcontains(transformeeContent, "math.isnan(x)") fs.close(transformeeHandle) -- Teardown @@ -105,7 +105,7 @@ test.suite("lute transform", function(suite) h = fs.open(outputPath, "r") local outputContent = fs.read(h) assert.eq(outputContent:find("x ~= x"), nil) - assert.neq(outputContent:find("math.isnan(x)", 1, true), nil) + assert.strcontains(outputContent, "math.isnan(x)") fs.close(h) -- Teardown @@ -139,7 +139,7 @@ test.suite("lute transform", function(suite) local h = fs.open(outputPath, "r") local outputContent = fs.read(h) assert.eq(outputContent:find("x ~= x"), nil) - assert.neq(outputContent:find("math.isnan(x)", 1, true), nil) + assert.strcontains(outputContent, "math.isnan(x)") fs.close(h) -- Teardown From 537a042c055a0791426f6d0589f5ca76072321d9 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 1 Dec 2025 11:13:07 -0800 Subject: [PATCH 197/642] stdlib: Some fixups to assert functions (#623) - Update `strcontains` to use coalescing to assign to `instances`. This was a fix suggested by @~hgoldstein in another PR which got automerged over the break. - Also updates `erroreq` to handle the cases where we actually want to pass stuff to `func` --- lute/std/libs/test/assert.luau | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index 728d31e5a..ae5a92bb4 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -106,8 +106,8 @@ function assertions.buffereq(lhs: buffer, rhs: buffer) end end -function assertions.erroreq(func: () -> (), expectedErrorMessage: string) - local success, err = pcall(func) +function assertions.erroreq(func: (A...) -> ...unknown, expectedErrorMessage: string, ...: A...) + local success, err = pcall(func, ...) if success then error({ msg = `Function did not error as expected.` }) end @@ -120,18 +120,16 @@ function assertions.erroreq(func: () -> (), expectedErrorMessage: string) end function assertions.strcontains(haystack: string, needle: string, msg: string?, instances: number?) - if instances == nil then - instances = 1 - end + instances = instances or 1 - assert(instances > 0, "instances must be greater than 0") -- LUAUFIX: Type stating should mean this comparison doesn't error. + assert(instances > 0, "instances must be greater than 0") if instances == 1 then if haystack:find(needle, 1, true) == nil then error(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) end else - if #haystack:split(needle) ~= instances + 1 then -- LUAUFIX: Type stating should mean this addition doesn't error. + if #haystack:split(needle) ~= instances + 1 then error(if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.`) end end From c5cd640210085783e03ebecb56b8f9a14b5fd4d4 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 1 Dec 2025 14:36:28 -0800 Subject: [PATCH 198/642] add transform and lint options to lute --help (#625) title says it all --- lute/cli/src/climain.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 3d2ea24a3..90144af3b 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -55,6 +55,8 @@ static const char* HELP_STRING = R"(Usage: lute [options] [arguments.. check Type check Luau files. compile Compile a Luau script into a standalone executable. setup Generate type definition files for the language server. + transform Run a specified code transformation on specified Luau files. + lint Run linting rules on specified Luau files. Run Options (when using 'run' or no command): lute [run] [args...] @@ -71,7 +73,20 @@ Compile Options: Setup Options: lute setup Generates type definition files for the language server. - --with-luaurc Defines aliases to the type definition files in the working directory's luaurc file. + --with-luaurc Defines aliases to the type definition files in the working directory's luaurc file. + +Transform Options: + lute transform [options...] + Runs the specified code transformation on the provided Luau files. + --dry-run Runs the transformation without actually overwriting or deleting any files. + --output Specifies an output file for a transformed file. Only valid when + transforming a single file. If not specified, files are overwritten in place. + +Lint Options: + lute lint [options...] + Runs linting rules on the specified Luau files. + --rules Path to a single lint rule or a directory containing multiple lint rules. + If not specified, default lint rules are used. General Options: -h, --help Display this usage message. From 637f247f320af18c1de40c818628859cdce125fa Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 1 Dec 2025 14:36:55 -0800 Subject: [PATCH 199/642] lute lint: default lint rules (#624) Implement behavior for `lute lint` to use default lint rules if none are specified. After discussing with @~Vighnesh-V, we've decided to keep the example rules in `examples` as-is, but keep the actual lint rules used inside `lute/cli/commands/lint/rules`, which will also hold any lint rules added in the future. Also deleted the `linter.luau` example in case people go thinking that's the linter instead. --- examples/linter.luau | 94 ---------------- examples/lints/almost_swapped.luau | 2 + examples/lints/divide_by_zero.luau | 2 + lute/cli/commands/lint/init.luau | 57 ++++++---- lute/cli/commands/lint/rules/README.md | 1 + .../commands/lint/rules/almost_swapped.luau | 105 ++++++++++++++++++ .../commands/lint/rules/divide_by_zero.luau | 39 +++++++ tests/cli/lint.test.luau | 63 +++++++++-- 8 files changed, 238 insertions(+), 125 deletions(-) delete mode 100644 examples/linter.luau create mode 100644 lute/cli/commands/lint/rules/README.md create mode 100644 lute/cli/commands/lint/rules/almost_swapped.luau create mode 100644 lute/cli/commands/lint/rules/divide_by_zero.luau diff --git a/examples/linter.luau b/examples/linter.luau deleted file mode 100644 index ba91f249e..000000000 --- a/examples/linter.luau +++ /dev/null @@ -1,94 +0,0 @@ -local fs = require("@std/fs") -local syntax = require("@std/syntax") - -local pp = require("@batteries/pp") - -local function select(node, predicate: ({ [string]: any }) -> T?): { T } - local nodes = {} - - local function helper(n) - if typeof(n) ~= "table" then - return - end - - local result = predicate(n) - if result ~= nil then - table.insert(nodes, result) - end - - for key, value in n :: { unknown } do - if key == "tag" or key == "location" then - continue - end - - helper(value) - end - end - - helper(node) - return nodes -end - -local path = tostring(...) -local source = fs.readfiletostring(path) - -local prog = syntax.parse(source) - -local function lintForDivZeroByZero(prog) - local function unwrapParens(n) - if n.tag == "group" then - return unwrapParens(n.expression) - end - return n - end - - local function isZero(n) - return n.tag == "number" and n.value == 0 - end - - local function isZeroDivZero(n) - return n.tag == "binary" - and n.operator == "/" - and isZero(unwrapParens(n.lhsoperand)) - and isZero(unwrapParens(n.rhsoperand)) - end - - local function isComparisonToZeroZero(n) - if not (n.operator == "~=" or n.operator == "==") then - return nil - end - - local lhs = unwrapParens(n.lhsoperand) - local rhs = unwrapParens(n.rhsoperand) - - if isZeroDivZero(lhs) then - return rhs - end - if isZeroDivZero(rhs) then - return lhs - end - - return nil - end - - local allBinaryOperators = select(prog, function(n) - return if n.tag == "binary" then n else nil - end) - - local violations = {} - for _, node in allBinaryOperators do - local expr = isComparisonToZeroZero(node) - if expr ~= nil then - local op_as_string = node.operator - table.insert(violations, { - severity = "err", - node = node, - message = `Don't compare things to 0/0, try: expr {op_as_string} expr`, - }) - end - end - - return violations -end - -print(pp(lintForDivZeroByZero(prog))) diff --git a/examples/lints/almost_swapped.luau b/examples/lints/almost_swapped.luau index dc543c397..4cce1e31e 100644 --- a/examples/lints/almost_swapped.luau +++ b/examples/lints/almost_swapped.luau @@ -1,3 +1,5 @@ +-- Check out lute/cli/commands/lint/rules for the default rules used by lute lint! + local lintTypes = require("@commands/lint/types") local path = require("@std/path") local query = require("@std/syntax/query") diff --git a/examples/lints/divide_by_zero.luau b/examples/lints/divide_by_zero.luau index f9ffc8051..d4e6068fa 100644 --- a/examples/lints/divide_by_zero.luau +++ b/examples/lints/divide_by_zero.luau @@ -1,3 +1,5 @@ +-- Check out lute/cli/commands/lint/rules for the default rules used by lute lint! + local lintTypes = require("@commands/lint/types") local path = require("@std/path") local query = require("@std/syntax/query") diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 64cb8132e..34e4ec5b6 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -7,16 +7,20 @@ local syntax = require("@std/syntax") local tableext = require("@std/tableext") local types = require("@self/types") +local DEFAULT_RULES = { "almost_swapped", "divide_by_zero" } local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" local function printHelp() print(USAGE) print([[ -Lint the specified Luau file using the specified lint rule(s). +Lint the specified Luau file using the specified lint rule(s) or using the default rules. OPTIONS: -h, --help Show this help message - -r, --rules [RULE] Path to a single lint rule or a folder containing lint rules. If a folder is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated as individual lint rules. + -r, --rules [RULE] Path to a single lint rule or a folder containing lint rules. If a folder + is provided, any subfolders containing init.luau files will be treated as + modules exporting lint rules, while all other .luau files will be treated + as individual lint rules. If unspecified, the default lint rules are used. PATHS: Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. @@ -27,17 +31,21 @@ EXAMPLES: ]]) end -local function loadLintRule(path: string): types.LintRule - local loaded = luau.loadbypath(path) - assert(loaded, `{path} failed to require`) - assert(typeof(loaded) == "table", `{path} must return a table`) - assert(typeof(loaded.name) == "string", `{path} must return a table with a 'name' string property`) +local function assertIsLintRule(rule: unknown, path: string) + assert(rule, `{path} failed to require`) + assert(typeof(rule) == "table", `{path} must return a table`) + assert(typeof(rule.name) == "string", `{path} must return a table with a 'name' string property`) assert( - typeof(loaded.lint) == "function", -- LUAUFIX: type error on loaded.lint because loaded has been refined to { read name: string } + typeof(rule.lint) == "function", -- LUAUFIX: type error on loaded.lint because loaded has been refined to { read name: string } `{path} must return a table with a 'lint' function property` ) +end + +local function loadLintRule(path: string): types.LintRule + local loaded = luau.loadbypath(path) + assertIsLintRule(loaded, path) - return loaded :: types.LintRule -- We have to cast because we can only assert that loaded.lint is the top function type, rather than a more specific function type like (ast: syntax.AstStatBlock, sourcepath: path.path) -> { LintViolation } + return loaded -- Typecast isn't needed because loaded is refined to any bc of type error in assertIsLintRule end local function loadLintRules(path: string): { types.LintRule } @@ -70,6 +78,17 @@ local function loadLintRules(path: string): { types.LintRule } return rules end +local function loadDefaultRules(): { types.LintRule } + local rules = {} + for _, ruleName in DEFAULT_RULES do + local path = `@self/rules/{ruleName}` + local rule = require(path) + assertIsLintRule(rule, path) + table.insert(rules, rule) + end + return rules +end + local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule }): { types.LintViolation } print(`Reading input file '{inputFilePath}'`) local fileContent = fs.readfiletostring(inputFilePath) @@ -104,7 +123,7 @@ local function main(...: string) args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) - -- TODO: handle multiple rules, multiple input files, ignore blobs, json output option + -- TODO: ignore blobs, json output option args:parse({ ... }) @@ -113,21 +132,21 @@ local function main(...: string) return end - local rulePath = args:get("rules") - if rulePath == nil then - -- TODO: support default rules - print("Error: No lint rules specified.\n\n" .. USAGE .. "\nUse --help for more information.") - return - end - local inputFiles = args:forwarded() if inputFiles == nil or #inputFiles == 0 then print("Error: No input files specified.\n\n" .. USAGE .. "\nUse --help for more information.") return end - print(`Loading lint rule(s) from '{rulePath}'`) - local lintRules = loadLintRules(rulePath) + local lintRules: { types.LintRule } + local rulePath = args:get("rules") + if rulePath == nil then + print("Using default lint rules.") + lintRules = loadDefaultRules() + else + print(`Loading lint rule(s) from '{rulePath}'`) + lintRules = loadLintRules(rulePath) + end local allViolations = {} diff --git a/lute/cli/commands/lint/rules/README.md b/lute/cli/commands/lint/rules/README.md new file mode 100644 index 000000000..cd3b3a505 --- /dev/null +++ b/lute/cli/commands/lint/rules/README.md @@ -0,0 +1 @@ +To add a new default lint rule to `lute lint`, create a module or luau file defining the rule in this folder. Then, add the name of the rule to the DEFAULT_RULES constant in `lint/init.luau`, and add a test for the rule in `tests/cli/lint.test.luau`. The expected type of a lint rule is specified in `lint/types.luau`. \ No newline at end of file diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau new file mode 100644 index 000000000..c9342de98 --- /dev/null +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -0,0 +1,105 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "almost_swapped" +local message = "This looks like a failed attempt to swap." + +local compFuncs = {} + +function compFuncs.exprLocalsSame(a: syntax.AstExprLocal, b: syntax.AstExprLocal): boolean + return a["local"] == b["local"] +end + +function compFuncs.exprGlobalsSame(a: syntax.AstExprGlobal, b: syntax.AstExprGlobal): boolean + return a.name.text == b.name.text +end + +function compFuncs.exprIndexNamesSame(a: syntax.AstExprIndexName, b: syntax.AstExprIndexName): boolean + if a.index.text ~= b.index.text then + return false + end + + return compFuncs.refExprsSame(a.expression, b.expression) +end + +function compFuncs.exprIndexExprsSame(a: syntax.AstExprIndexExpr, b: syntax.AstExprIndexExpr): boolean + if a.index.tag == "string" and b.index.tag == "string" then + if a.index.text ~= b.index.text then + return false + else + return compFuncs.refExprsSame(a.expression, b.expression) + end + else + return compFuncs.refExprsSame(a.expression, b.expression) and compFuncs.refExprsSame(a.index, b.index) + end +end + +function compFuncs.refExprsSame(a: syntax.AstExpr, b: syntax.AstExpr): boolean + if a.tag ~= b.tag then + return false + end + + if a.tag == "local" then + return compFuncs.exprLocalsSame(a, b :: syntax.AstExprLocal) + elseif a.tag == "global" then + return compFuncs.exprGlobalsSame(a, b :: syntax.AstExprGlobal) + elseif a.tag == "indexname" then + return compFuncs.exprIndexNamesSame(a, b :: syntax.AstExprIndexName) + elseif a.tag == "index" then + return compFuncs.exprIndexExprsSame(a, b :: syntax.AstExprIndexExpr) + else + return false + end +end + +-- Report instances of attempted swaps like: +-- a = b; b = a +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } + local violations = {} + + local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes + + for _, block in nodes do + for i = 1, #block.statements - 1 do + local currStat = block.statements[i] + if currStat.tag ~= "assign" or #currStat.values ~= 1 or #currStat.variables ~= 1 then + continue + end + + local nextStat = block.statements[i + 1] + if nextStat.tag ~= "assign" or #nextStat.values ~= 1 or #nextStat.variables ~= 1 then + continue + end + + local currVar, currVal = currStat.variables[1].node, currStat.values[1].node + local nextVar, nextVal = nextStat.variables[1].node, nextStat.values[1].node + + if compFuncs.refExprsSame(currVar, nextVal) and compFuncs.refExprsSame(nextVar, currVal) then + table.insert(violations, { -- LUAUFIX: severity isn't inferred as a singleton, so table.insert is mad + lintname = name, + location = syntax.span.create({ + beginline = currStat.location.beginline, + begincolumn = currStat.location.begincolumn, + endline = nextStat.location.endline, + endcolumn = nextStat.location.endcolumn, + }), + message = message, + severity = "warning", + sourcepath = sourcepath, + }) + end + end + end + + return violations +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau new file mode 100644 index 000000000..dbf67e5d1 --- /dev/null +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -0,0 +1,39 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "divide_by_zero" +local message = "Division by zero detected." + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } + return query + .findallfromroot(ast, utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" + end) + :filter(function(bin) + return bin.rhsoperand.kind == "expr" and bin.rhsoperand.tag == "number" and bin.rhsoperand.value == 0 + end) + :maptoarray( + function( + n: syntax.AstExprBinary + ): lintTypes.LintViolation -- LUAUFIX: Bidiretional inference of generics should let us not need this annotation + return { + lintname = name, + location = n.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + } + end + ) +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 3c7d18245..941f1b6ef 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -7,8 +7,8 @@ local test = require("@std/test") local lutePath = path.format(process.execpath()) local tmpDir = system.tmpdir() local rulesDir = path.format(path.join(".", "lints")) -local almostSwappedExample = path.format(path.join("examples", "lints", "almost_swapped.luau")) -local divideByZeroExample = path.format(path.join("examples", "lints", "divide_by_zero.luau")) +local almostSwappedExample = path.format(path.join("lute", "cli", "commands", "lint", "rules", "almost_swapped.luau")) +local divideByZeroExample = path.format(path.join("lute", "cli", "commands", "lint", "rules", "divide_by_zero.luau")) local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" @@ -42,13 +42,6 @@ test.suite("lute lint", function(suite) assert.strcontains(result.stdout, USAGE) end) - suite:case("lute lint no rule", function(assert) - local result = process.run({ lutePath, "lint" }) - - assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, "Error: No lint rules specified.") - end) - suite:case("lute lint no input file", function(assert) local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) @@ -170,11 +163,12 @@ violator.luau:14:2-15:17 ── -- Setup fs.createdirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau - fs.copy(almostSwappedExample, path.join(rulesDir, "almost_swapped.luau")) + -- Because the default rules require types using a relative path, we copy from examples, which uses an absolute require path + fs.copy(path.join("examples", "lints", "almost_swapped.luau"), path.join(rulesDir, "almost_swapped.luau")) -- Copy divide_by_zero to rulesDir/divide_by_zero/init.luau local divideByZeroDir = path.join(rulesDir, "divide_by_zero") fs.createdirectory(divideByZeroDir) - fs.copy(divideByZeroExample, path.join(divideByZeroDir, "init.luau")) + fs.copy(path.join("examples", "lints", "divide_by_zero.luau"), path.join(divideByZeroDir, "init.luau")) fs.writestringtofile( path.join(rulesDir, "bogus.txt"), [[ @@ -231,7 +225,7 @@ violator.luau:3:1-4:6 ── -- Setup fs.createdirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau - fs.copy(almostSwappedExample, path.join(rulesDir, "almost_swapped.luau")) + fs.copy(path.join("examples", "lints", "almost_swapped.luau"), path.join(rulesDir, "almost_swapped.luau")) -- Create a rule that fails fs.writestringtofile( path.join(rulesDir, "erroring_rule.luau"), @@ -407,6 +401,51 @@ lintee.luau': @std/syntax/parser.luau:12: parsing failed: fs.remove(lintee) fs.remove(violatorPath) end) + + suite:case("lute lint default rules", function(assert) + -- Create a file that violates the default almost_swapped and divide_by_zero rules + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + expected = [[ +violator.luau:3:1-4:6 ── + │ + 3 │ ╭ a = b + 4 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) end) test.run() From ae14f3f5929371ae49b8e3cd76173c92b7e9d34d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 23:03:08 +0000 Subject: [PATCH 200/642] Update Lute to 0.1.0-nightly.20251127 (#622) **Lute**: Updated from `0.1.0-nightly.20251121` to `0.1.0-nightly.20251127` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20251127 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 2a9a55dab..7de88a7f4 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251121" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251127" } diff --git a/rokit.toml b/rokit.toml index dc3c878c6..a9c0c07e9 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20251121" +lute = "luau-lang/lute@0.1.0-nightly.20251127" From 0a85afb03e54f42ac2cdcd1b769c2f1710c3b866 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:05:19 -0800 Subject: [PATCH 201/642] Infra: opt out of concurrency grouping for CI running on the `primary` branch (#629) If two PRs are merged in to `primary` close to each other, our CI badge temporarily switches to a failing icon because our concurrency grouping logic cancels the first commit's CI run (treated as a failure). Instead, we opt out of concurrency grouping using the run's unique `run_id` if the current ref matches the ref pointed at by `primary`. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc6b6e6a2..6749dc566 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,10 @@ on: pull_request: branches: ["primary"] +# CI on the "primary" branch opts out of concurrency grouping by using the run's +# unique run_id. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/primary' && github.run_id || github.ref }} cancel-in-progress: true jobs: From a81d354a6e835ade8d5a1c588a974101915b8cbb Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:06:38 -0800 Subject: [PATCH 202/642] Add `resolverequire` API to standard library with filepath semantics (#630) Internally, we simply need to prepend a `@` to the filepath to create a valid chunkname, but the new API does this automatically to hide the chunkname implementation details from users. --- lute/std/libs/luau.luau | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index f09822489..36374fb01 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -38,4 +38,9 @@ function luau.typeofmodule(modulepath: path.pathlike): string return luteLuau.typeofmodule(modulePathStr) end +function luau.resolverequire(modulepath: string, frompath: path.pathlike): string + local frompathString = if typeof(frompath) == "string" then frompath else path.format(frompath) + return luteLuau.resolverequire(modulepath, `@{frompathString}`) +end + return luau From 9e48899e807da01434c0c0431e26c985e7bc57b8 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 2 Dec 2025 10:59:06 -0800 Subject: [PATCH 203/642] tidy: Lute shouldn't pull in the filesystem header (#628) This [PR](https://github.com/luau-lang/lute/pull/573) introduced a dependency on `std::filesystem` in the `Lute.Test` target. There are some known problems with this header, and it was suggested in that PR by @aatxe that we should manually implement the platform specific file I/O operations that we need as part of a `FileUtils` library in lute, so that we can remove our dependency on this header. This PR implements the necessary operations that we need and removes our dependency on `std::filesystem`. --- lute/cli/CMakeLists.txt | 2 + lute/cli/include/lute/fileutils.h | 28 ++++++ lute/cli/src/fileutils.cpp | 146 ++++++++++++++++++++++++++++++ tests/src/require.test.cpp | 19 ++-- 4 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 lute/cli/include/lute/fileutils.h create mode 100644 lute/cli/src/fileutils.cpp diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 2aaa97889..6a61f6c2c 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -25,6 +25,7 @@ add_library(Lute.CLI.lib STATIC) target_sources(Lute.CLI.lib PRIVATE include/lute/climain.h include/lute/compile.h + include/lute/fileutils.h include/lute/luauflags.h include/lute/reporter.h include/lute/staticrequires.h @@ -33,6 +34,7 @@ target_sources(Lute.CLI.lib PRIVATE src/climain.cpp src/compile.cpp + src/fileutils.cpp src/luauflags.cpp src/staticrequires.cpp src/tc.cpp diff --git a/lute/cli/include/lute/fileutils.h b/lute/cli/include/lute/fileutils.h new file mode 100644 index 000000000..15a150a07 --- /dev/null +++ b/lute/cli/include/lute/fileutils.h @@ -0,0 +1,28 @@ +// This file is part of the Lute programming language and is licensed under MIT License +#pragma once + +#include +#include + +namespace Lute +{ + +// Open a file with platform-specific handling (wfopen on Windows, fopen elsewhere) +FILE* openFile(const std::string& path, const char* mode); + +// Write string content to a file +bool writeFile(const std::string& path, const std::string& content); + +// Create a directory (non-recursive) +bool createDirectory(const std::string& path); + +// Create directories recursively (equivalent to mkdir -p) +bool createDirectories(const std::string& path); + +// Remove a file +bool removeFile(const std::string& path); + +// Remove a directory (must be empty) +bool removeDirectory(const std::string& path); + +} // namespace Lute diff --git a/lute/cli/src/fileutils.cpp b/lute/cli/src/fileutils.cpp new file mode 100644 index 000000000..215f10c7e --- /dev/null +++ b/lute/cli/src/fileutils.cpp @@ -0,0 +1,146 @@ +// This file is part of the Lute programming language and is licensed under MIT License +#include "lute/fileutils.h" + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include + +#include +#endif + +#include +#include "Luau/FileUtils.h" + +namespace Lute +{ + +#ifdef _WIN32 +static std::wstring fromUtf8(const std::string& path) +{ + if (path.empty()) + return std::wstring(); + + size_t result = MultiByteToWideChar(CP_UTF8, 0, path.data(), int(path.size()), nullptr, 0); + if (result == 0) + return std::wstring(); + + std::wstring buf(result, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, path.data(), int(path.size()), &buf[0], int(buf.size())); + + return buf; +} +#endif + +FILE* openFile(const std::string& path, const char* mode) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + std::wstring wmode = fromUtf8(mode); + if (wpath.empty() || wmode.empty()) + return nullptr; + return _wfopen(wpath.c_str(), wmode.c_str()); +#else + return fopen(path.c_str(), mode); +#endif +} + +bool writeFile(const std::string& path, const std::string& content) +{ + FILE* file = openFile(path, "wb"); + if (!file) + return false; + + bool success = fwrite(content.data(), 1, content.size(), file) == content.size(); + fclose(file); + return success; +} + +bool createDirectory(const std::string& path) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + if (wpath.empty()) + return false; + return _wmkdir(wpath.c_str()) == 0 || errno == EEXIST; +#else + return mkdir(path.c_str(), 0755) == 0 || errno == EEXIST; +#endif +} + +bool createDirectories(const std::string& path) +{ + if (path.empty()) + return false; + + std::vector components = splitPath(path); + std::string currentPath; + size_t offset = 0; + + // Handle absolute paths - extract the root/prefix + if (isAbsolutePath(path)) + { +#ifdef _WIN32 + // Check if path starts with drive letter (e.g., "C:") + if (path.size() >= 2 && path[1] == ':') + { + currentPath = path.substr(0, 2); + offset = 1; // Skip the drive letter component in the loop + } + else if (path.size() >= 1 && (path[0] == '/' || path[0] == '\\')) + { + currentPath = path.substr(0, 1); + } +#else + currentPath = "/"; +#endif + } + + for (size_t i = offset; i < components.size(); ++i) + { + const std::string_view component = components[i]; + currentPath = joinPaths(currentPath, component); + + if (!createDirectory(currentPath)) + { + // Check if the directory already exists + if (errno != EEXIST) + return false; + } + } + + return true; +} + +bool removeFile(const std::string& path) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + if (wpath.empty()) + return false; + return _wunlink(wpath.c_str()) == 0; +#else + return unlink(path.c_str()) == 0; +#endif +} + +bool removeDirectory(const std::string& path) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + if (wpath.empty()) + return false; + return _wrmdir(wpath.c_str()) == 0; +#else + return rmdir(path.c_str()) == 0; +#endif +} + +} // namespace Lute diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index 127614110..fc636040d 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -1,4 +1,5 @@ #include "lute/climain.h" +#include "lute/fileutils.h" #include "lute/uvutils.h" #include "Luau/FileUtils.h" @@ -7,9 +8,6 @@ #include "uv.h" -#include -#include - #include "cliruntimefixture.h" #include "doctest.h" #include "lutefixture.h" @@ -240,15 +238,12 @@ TEST_CASE_FIXTURE(LuteFixture, "require_check_tilde_path") std::string homeDir = *result.get_if(); // Create test directory and test file - std::filesystem::path testDir = std::filesystem::path(homeDir) / "lute_test_special"; - std::filesystem::path testFile = testDir / "foo.luau"; - std::filesystem::create_directories(testDir); + std::string testDir = joinPaths(homeDir, "lute_test_special"); + std::string testFile = joinPaths(testDir, "foo.luau"); + REQUIRE(Lute::createDirectories(testDir)); // Write test module file - std::ofstream file(testFile); - REQUIRE(file.is_open()); - file << "return { foo = \"bar\" }\n"; - file.close(); + REQUIRE(Lute::writeFile(testFile, "return { foo = \"bar\" }\n")); // Run the test for (const std::string& luteProjectRoot : {getLuteProjectRootRelative(), getLuteProjectRootAbsolute()}) @@ -259,6 +254,6 @@ TEST_CASE_FIXTURE(LuteFixture, "require_check_tilde_path") } // Clean up - std::filesystem::remove(testFile); - std::filesystem::remove(testDir); + Lute::removeFile(testFile); + Lute::removeDirectory(testDir); } From 24a30f85b6ad269225bcea932d396f47f534b83e Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:10:46 -0800 Subject: [PATCH 204/642] Add a test for `luau.resolverequire` API (#631) --- tests/std/luau.test.luau | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/std/luau.test.luau b/tests/std/luau.test.luau index 37491cbaf..6c9182e54 100644 --- a/tests/std/luau.test.luau +++ b/tests/std/luau.test.luau @@ -32,6 +32,18 @@ test.suite("LuauTypeOfModuleSuite", function(suite) -- Basic check for now until the data structure is created assert.eq(moduleTypeString, expectedOutputString) end) + + suite:case("resolverequire", function(assert) + local testPath = path.join(tmpDir, "resolverequire_test.luau") + local testFile = fs.open(testPath, "w+") + + fs.write(testFile, 'print("Hello, World!")') + local resolvedPath = luau.resolverequire("@self", testPath) + fs.close(testFile) + + local expected = path.format(testPath):gsub("\\", "/") + assert.eq(resolvedPath, expected) + end) end) test.run() From 4c5adb2b91d0c5d59f727d1616d6a06ae0fe242b Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 2 Dec 2025 13:04:18 -0800 Subject: [PATCH 205/642] stdlib: assert tests + fixes (#632) I noticed `assert.strcontains` wasn't giving good error messages so I added some tests and fixed some assert bugs --- lute/std/libs/test/assert.luau | 18 ++- lute/std/libs/test/runner.luau | 14 +- tests/cli/test.test.luau | 267 ++++++++++++++++++++++++++------- 3 files changed, 226 insertions(+), 73 deletions(-) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index ae5a92bb4..65d679c66 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -84,15 +84,15 @@ end function assertions.buffereq(lhs: buffer, rhs: buffer) if typeof(lhs) ~= "buffer" then - error(`lhs is of type {typeof(lhs)}, not buffer`) + error({ msg = `lhs is of type {typeof(lhs)}, not buffer` }) end if typeof(rhs) ~= "buffer" then - error(`rhs is of type {typeof(rhs)}, not buffer`) + error({ msg = `rhs is of type {typeof(rhs)}, not buffer` }) end if buffer.len(lhs) ~= buffer.len(rhs) then - error("buffers are of unequal length") + error({ msg = "buffers are of unequal length" }) end local len = buffer.len(lhs) @@ -101,7 +101,7 @@ function assertions.buffereq(lhs: buffer, rhs: buffer) local right = buffer.readu8(rhs, offset) if left ~= right then - error(string.format("lhs[%d] = %02x, but rhs[%d] = %02x", offset, left, offset, right)) + error({ msg = string.format("lhs[%d] = %02x, but rhs[%d] = %02x", offset, left, offset, right) }) end end end @@ -109,7 +109,7 @@ end function assertions.erroreq(func: (A...) -> ...unknown, expectedErrorMessage: string, ...: A...) local success, err = pcall(func, ...) if success then - error({ msg = `Function did not error as expected.` }) + error({ msg = "Function did not error as expected." }) end local actualErrorMessage = tostring(err) @@ -122,15 +122,17 @@ end function assertions.strcontains(haystack: string, needle: string, msg: string?, instances: number?) instances = instances or 1 - assert(instances > 0, "instances must be greater than 0") + if instances <= 0 then + error({ msg = "instances must be greater than 0" }) + end if instances == 1 then if haystack:find(needle, 1, true) == nil then - error(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) + error({ msg = if msg then msg else `Expected "{haystack}" to contain "{needle}".` }) end else if #haystack:split(needle) ~= instances + 1 then - error(if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.`) + error({ msg = if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.` }) end end end diff --git a/lute/std/libs/test/runner.luau b/lute/std/libs/test/runner.luau index 5f7090c25..42c972123 100644 --- a/lute/std/libs/test/runner.luau +++ b/lute/std/libs/test/runner.luau @@ -5,13 +5,13 @@ local types = require("./types") local asserts = require("./assert") -type Test = types.Test -type TestCase = types.TestCase -type TestSuite = types.TestSuite -type TestEnvironment = types.TestEnvironment -type TestReporter = types.TestReporter -type FailedTest = types.FailedTest -type TestRunResult = types.TestRunResult +type Test = types.test +type TestCase = types.testcase +type TestSuite = types.testsuite +type TestEnvironment = types.testenvironment +type TestReporter = types.testreporter +type FailedTest = types.failedtest +type TestRunResult = types.testrunresult local runner = {} diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index ade9a9f30..ac3d5abc3 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -1,3 +1,4 @@ +local assertions = require("@std/test/assert") local fs = require("@std/fs") local path = require("@std/path") local process = require("@std/process") @@ -16,6 +17,23 @@ SmokeSuite: make_pass ]] +local function assertErrorsWithMsg( + testName: string, + testContents: string, + expectedErrMsg: string, + assert: assertions.asserts +) + local testFilePath = path.join(tmpDir, `{testName}.test.luau`) + fs.writestringtofile(testFilePath, testContents) + + -- Run lute on the created test file + local result = process.run({ lutePath, tostring(testFilePath) }) + + -- Check that the output specifies the inequal values + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, expectedErrMsg) +end + test.suite("lute test", function(suite) suite:case("lute test help message", function(assert) -- Run lute test with -h flag @@ -46,9 +64,8 @@ test.suite("lute test", function(suite) suite:case("lute doesn't report xpcall as error when accessing field of nil in suite", function(assert) -- Setup local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") - local handle = fs.open(testFilePath, "w+") - fs.write( - handle, + fs.writestringtofile( + testFilePath, [[ local test = require("@std/test") @@ -62,7 +79,6 @@ end) test.run() ]] ) - fs.close(handle) -- Run lute on the created test file local result = process.run({ lutePath, tostring(testFilePath) }) @@ -83,9 +99,8 @@ test.run() suite:case("lute doesn't report xpcall as error when accessing field of nil in case", function(assert) -- Setup local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") - local handle = fs.open(testFilePath, "w+") - fs.write( - handle, + fs.writestringtofile( + testFilePath, [[ local test = require("@std/test") @@ -97,7 +112,6 @@ end) test.run() ]] ) - fs.close(handle) -- Run lute on the created test file local result = process.run({ lutePath, tostring(testFilePath) }) @@ -116,10 +130,8 @@ test.run() end) suite:case("assert.eq_error_message_in_case", function(assert) - local testFilePath = path.join(tmpDir, "assert_eq_fails.test.luau") - local handle = fs.open(testFilePath, "w+") - fs.write( - handle, + assertErrorsWithMsg( + "assert_eq_fails", [[ local test = require("@std/test") @@ -128,23 +140,15 @@ test.case("eq_error", function(assert) end) test.run() - ]] + ]], + "eq: 1 ~= 2", + assert ) - fs.close(handle) - - -- Run lute on the created test file - local result = process.run({ lutePath, tostring(testFilePath) }) - - -- Check that the output specifies the inequal values - assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "eq: 1 ~= 2") end) suite:case("assert.eq_error_message_in_suite", function(assert) - local testFilePath = path.join(tmpDir, "assert_eq_fails.test.luau") - local handle = fs.open(testFilePath, "w+") - fs.write( - handle, + assertErrorsWithMsg( + "assert_eq_fails", [[ local test = require("@std/test") @@ -155,23 +159,15 @@ test.suite("eq_failure_suite", function(suite) end) test.run() - ]] + ]], + "eq: 1 ~= 2", + assert ) - fs.close(handle) - - -- Run lute on the created test file - local result = process.run({ lutePath, tostring(testFilePath) }) - - -- Check that the output specifies the inequal values - assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "eq: 1 ~= 2") end) suite:case("assert.neq_error_message_in_case", function(assert) - local testFilePath = path.join(tmpDir, "assert_neq_fails.test.luau") - local handle = fs.open(testFilePath, "w+") - fs.write( - handle, + assertErrorsWithMsg( + "assert_neq_fails", [[ local test = require("@std/test") @@ -180,23 +176,15 @@ test.case("neq_error", function(assert) end) test.run() - ]] + ]], + "neq: 1 == 1", + assert ) - fs.close(handle) - - -- Run lute on the created test file - local result = process.run({ lutePath, tostring(testFilePath) }) - - -- Check that the output specifies the inequal values - assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "neq: 1 == 1") end) suite:case("assert.neq_error_message_in_suite", function(assert) - local testFilePath = path.join(tmpDir, "assert_neq_fails.test.luau") - local handle = fs.open(testFilePath, "w+") - fs.write( - handle, + assertErrorsWithMsg( + "assert_neq_fails", [[ local test = require("@std/test") @@ -207,16 +195,179 @@ test.suite("neq_failure_suite", function(suite) end) test.run() - ]] + ]], + "neq: 1 == 1", + assert ) - fs.close(handle) + end) - -- Run lute on the created test file - local result = process.run({ lutePath, tostring(testFilePath) }) + suite:case("assert.tableeq_error_message_in_case", function(assert) + assertErrorsWithMsg( + "assert_tableeq_fails", + [[ +local test = require("@std/test") - -- Check that the output specifies the inequal values - assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "neq: 1 == 1") +test.case("tableeq_error", function(assert) + assert.tableeq({ a = 1 }, { a = 2 }) +end) + +test.run() + ]], + "tableeq: lhs[a] = 1 but rhs[a] = 2", + assert + ) + end) + + suite:case("assert.tableeq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_tableeq_fails", + [[ +local test = require("@std/test") + +test.suite("tableeq_failure_suite", function(suite) + suite:case("tableeq_error", function(assert) + assert.tableeq({ a = 1 }, { a = 2 }) + end) +end) + +test.run() + ]], + "tableeq: lhs[a] = 1 but rhs[a] = 2", + assert + ) + end) + + suite:case("assert.buffereq_unequal_length", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.case("buffereq_error", function(assert) + local buf1 = buffer.create(1) + local buf2 = buffer.create(2) + assert.buffereq(buf1, buf2) +end) + +test.run() + ]], + "buffers are of unequal length", + assert + ) + end) + + suite:case("assert.buffereq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("buffereq_failure_suite", function(suite) + suite:case("buffereq_error", function(assert) + local buf1 = buffer.create(2) + local buf2 = buffer.create(2) + buffer.writeu8(buf1, 0, 84) + buffer.writeu8(buf2, 0, 85) + assert.buffereq(buf1, buf2) + end) +end) + +test.run() + ]], + "buffereq: lhs[0] = 54, but rhs[0] = 55", + assert + ) + end) + + suite:case("assert.erroreq_no_error", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.case("erroreq_error", function(assert) + assert.erroreq(function() return end, "expected message") +end) + +test.run() + ]], + "Function did not error as expected.", + assert + ) + end) + + suite:case("assert.erroreq_error_message", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("erroreq_failure_suite", function(suite) + suite:case("erroreq_error", function(assert) + assert.erroreq(function() error("wrong message") end, "expected message") + end) +end) + +test.run() + ]], + 'Expected suffix of error message "expected message", but got ', -- the full message contains tmpdir path to test file + assert + ) + end) + + suite:case("assert.strcontains_instances_negative", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.case("strcontains_error", function(assert) + assert.strcontains("hello world", "goodbye", nil, -1) +end) + +test.run() + ]], + "instances must be greater than 0", + assert + ) + end) + + suite:case("assert.strcontains_single_instance_fails", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("strcontains_failure_suite", function(suite) + suite:case("strcontains_error", function(assert) + assert.strcontains("hello world", "goodbye") + end) +end) + +test.run() + ]], + 'Expected "hello world" to contain "goodbye".', + assert + ) + end) + + suite:case("assert.strcontains_multiple_instance_fails", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("strcontains_failure_suite", function(suite) + suite:case("strcontains_error", function(assert) + assert.strcontains("muahahahaha", "ha", nil, 5) + end) +end) + +test.run() + ]], + 'Expected "muahahahaha" to contain "ha" 5 times.', + assert + ) end) end) From 29914634c92c29727cb58abd1054da69bf96a39c Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:55:32 -0800 Subject: [PATCH 206/642] fix: fix heap-use-after-free in `task.wait` (#633) Splitting from https://github.com/luau-lang/lute/pull/616 --- lute/task/src/task.cpp | 22 +++++++++++++++++++--- tests/lute/task.test.luau | 29 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 tests/lute/task.test.luau diff --git a/lute/task/src/task.cpp b/lute/task/src/task.cpp index 59c9bcab7..23e1c8658 100644 --- a/lute/task/src/task.cpp +++ b/lute/task/src/task.cpp @@ -25,8 +25,23 @@ struct WaitData uint64_t startedAtMs; + bool closed = false; bool putDeltaTimeOnStack; int nargs; + + void close() + { + if (!closed) + { + uv_timer_stop(&uvTimer); + closed = true; + } + } + + ~WaitData() + { + close(); + } }; static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaTimeOnStack, int nargs) @@ -54,12 +69,13 @@ static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaT if (yield->putDeltaTimeOnStack) lua_pushnumber(L, static_cast(uv_now(uv_default_loop()) - yield->startedAtMs) / 1000.0); - delete yield; + uv_close(reinterpret_cast(&yield->uvTimer), [](uv_handle_t* handle) { + WaitData* yield = static_cast(handle->data); + delete yield; + }); return stackReturnAmount; } ); - - uv_timer_stop(&yield->uvTimer); }, milliseconds, 0 diff --git a/tests/lute/task.test.luau b/tests/lute/task.test.luau new file mode 100644 index 000000000..274e69eb0 --- /dev/null +++ b/tests/lute/task.test.luau @@ -0,0 +1,29 @@ +local test = require("@std/test") +local task = require("@lute/task") + +test.suite("LuteTaskSuite", function(suite) + suite:case("simple_task_wait", function(assert) + local startTime = os.clock() + task.wait(0.5) + local endTime = os.clock() + + print("Elapsed time: ", endTime - startTime) + + -- task.wait(0.5) actually waits ~0.48 seconds :) + assert.eq(math.ceil(endTime - startTime) >= 0.5, true) + end) + + suite:case("task_wait_in_loop", function(assert) + local startTime = os.clock() + local elapsed = 0 + while elapsed < 0.5 do + task.wait(0.1) + elapsed = os.clock() - startTime + end + local endTime = os.clock() + + assert.eq(endTime - startTime >= 0.5, true) + end) +end) + +test.run() From b66569d5fcb94773032b71716c7fe0c56925778b Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:18:50 -0800 Subject: [PATCH 207/642] stdlib: re-implement `fs.watch` as an iterator (#616) As part of the API audit/cleanup, we reimplement `fs.watch` as an iterator instead of passing in a callback so you can use `fs.watch` on a task cooperatively. This will then have the issue that `fs.walk` has where for loops do not support yielding generalized iterators, so we can only implement with a while loop for now. Usage could be something like ```lua local watcher = fs.watch(tmpdir) fs.writestringtofile(watched, "x") local event local start = os.clock() repeat event = watcher:next() if not event then task.wait(0.01) end until event or (os.clock() - start > 2) if event then print("Event detected:", event.change and "change" or "rename") else print("No event detected within the timeout period.") end watcher:close() ``` See full examples in `examples/watch_directory.luau` or `fs.test.luau:watch_iterator_on_change` This PR also fixes a use-after-free in `task.cpp`- see https://github.com/luau-lang/lute/pull/616#issuecomment-3599127135 for details --- examples/watch_directory.luau | 41 +++++++++++++++++++++++++++++++++++ lute/fs/src/fs.cpp | 25 ++++----------------- lute/std/libs/fs.luau | 32 ++++++++++++++++++++++++--- tests/lute/task.test.luau | 2 -- tests/std/fs.test.luau | 30 ++++++++++++++----------- 5 files changed, 91 insertions(+), 39 deletions(-) create mode 100644 examples/watch_directory.luau diff --git a/examples/watch_directory.luau b/examples/watch_directory.luau new file mode 100644 index 000000000..7db44daea --- /dev/null +++ b/examples/watch_directory.luau @@ -0,0 +1,41 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") +local task = require("@lute/task") + +-- Setup, get a temporary directory to watch +local tmpdir = system.tmpdir() + +local watchedFileName = "watched.txt" +local watched = path.join(tmpdir, watchedFileName) +if fs.exists(watched) then + fs.remove(watched) +end + +local watcher = fs.watch(tmpdir) + +-- Trigger a file change event +fs.writestringtofile(watched, "x") + +-- Poll the iterator with a timeout of 2 seconds +local event +local start = os.clock() + +repeat + event = watcher:next() + if not event then + task.wait(0.01) + end +until event or (os.clock() - start > 2) + +if event then + print("Event detected:", event.change and "change" or "rename") +else + print("No event detected within the timeout period.") +end + +-- Cleanup, close the watcher and remove the watched file +watcher:close() +if fs.exists(watched) then + fs.remove(watched) +end diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 34cde2b34..a20cabc5c 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -636,27 +636,10 @@ int fs_watch(lua_State* L) // events lua_createtable(L, 0, 2); - if ((events & UV_RENAME) == UV_RENAME) - { - lua_pushboolean(L, true); - lua_setfield(L, -2, "rename"); - } - else - { - lua_pushboolean(L, false); - lua_setfield(L, -2, "rename"); - } - - if ((events & UV_CHANGE) == UV_CHANGE) - { - lua_pushboolean(L, true); - lua_setfield(L, -2, "change"); - } - else - { - lua_pushboolean(L, false); - lua_setfield(L, -2, "change"); - } + lua_pushboolean(L, (events & UV_RENAME) != 0); + lua_setfield(L, -2, "rename"); + lua_pushboolean(L, (events & UV_CHANGE) != 0); + lua_setfield(L, -2, "change"); return 2; } diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 445ef6d35..def4b9386 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -32,6 +32,11 @@ export type walkoptions = { recursive: boolean?, } +type watcher = { + next: (watcher) -> watchevent?, + close: (self: watcher) -> (), +} + function fslib.open(path: pathlike, mode: handlemode?): file return fs.open(pathlib.format(path), mode) end @@ -68,8 +73,29 @@ function fslib.symboliclink(src: pathlike, dest: pathlike): () return fs.symlink(pathlib.format(src), pathlib.format(dest)) end -function fslib.watch(path: pathlike, callback: (filename: pathlike, event: watchevent) -> ()): watchhandle - return fs.watch(pathlib.format(path), callback) +--- Iterator function that yields filename and event pairs when changes occur in the watched path. +--- Also provides a `close` method to stop watching. +--- Note: for loops do not support yielding generalized iterators, so we cannot use fs.watch as `for _ in fs.watch(...) do` directly. A while loop can be used instead. See example/watch_directory.luau for usage. +function fslib.watch(path: pathlike): watcher + local queue = {} + local handle = fs.watch(pathlib.format(path), function(filename: string, event: watchevent) + table.insert(queue, { filename = pathlib.parse(filename), event = event }) + end) + + return { + next = function(self: watcher): watchevent? + if #queue == 0 then + return nil + end + local item = table.remove(queue, 1) + return item.event + end, + close = function(self: watcher): () + if handle then + handle:close() + end + end, + } end function fslib.exists(path: pathlike): boolean @@ -139,7 +165,7 @@ function fslib.removedirectory(path: pathlike, options: removedirectoryoptions?) end end --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. +--- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. See example/walk_directory.luau for usage. function fslib.walk(path: pathlike, options: walkoptions?): () -> path? local queue = { path } diff --git a/tests/lute/task.test.luau b/tests/lute/task.test.luau index 274e69eb0..12a5da513 100644 --- a/tests/lute/task.test.luau +++ b/tests/lute/task.test.luau @@ -7,8 +7,6 @@ test.suite("LuteTaskSuite", function(suite) task.wait(0.5) local endTime = os.clock() - print("Elapsed time: ", endTime - startTime) - -- task.wait(0.5) actually waits ~0.48 seconds :) assert.eq(math.ceil(endTime - startTime) >= 0.5, true) end) diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 853fc171e..96e5857cc 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -2,6 +2,7 @@ local fs = require("@std/fs") local path = require("@std/path") local system = require("@std/system") local test = require("@std/test") +local task = require("@lute/task") local tmpdir = system.tmpdir() @@ -89,28 +90,31 @@ test.suite("FsSuite", function(suite) fs.remove(src) end) - suite:case("watch_callback_on_change", function(assert) - local watched = path.join(tmpdir, "watched.txt") + suite:case("watch_iterator_on_change", function(assert) + local watchedFileName = "watched.txt" + local watched = path.join(tmpdir, watchedFileName) if fs.exists(watched) then fs.remove(watched) end - local triggered = false - local handle = fs.watch(tmpdir, function(filename, event) - if tostring(filename):find("watched.txt") then - triggered = true - end - end) + local watcher = fs.watch(tmpdir) fs.writestringtofile(watched, "x") - local start = os.time() - while not triggered and os.time() - start < 2 do - end + local start = os.clock() + local event + + repeat + event = watcher:next() + if not event then + task.wait(0.01) + end + until event or (os.clock() - start > 2) - assert.eq(type(triggered) == "boolean", true) + assert.neq(event, nil) + assert.eq(event.change or event.rename, true) - handle:close() + watcher:close() if fs.exists(watched) then fs.remove(watched) end From 5b0676bdd23721dd7da5975c03b20e0413151699 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 2 Dec 2025 18:24:50 -0800 Subject: [PATCH 208/642] feature: Lute test runs all the tests (#557) Run all the tests in your project via: ``` lute test ``` Run some of the tests in a directory via: ``` lute test filter/path/directory ``` --------- Co-authored-by: Annie Tang <98965493+annieetang@users.noreply.github.com> --- lute/cli/commands/test/finder.luau | 4 ++-- lute/cli/commands/test/init.luau | 16 ++++++++++++++++ lute/std/libs/test/runner.luau | 5 +++-- tests/cli/test.test.luau | 13 ++++++------- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index 53de45e3d..c13b2b025 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -17,7 +17,7 @@ local function findtestfilesrec(directory: path.path): { path.path } if entry.type == "dir" then tableext.extend(results, findtestfilesrec(fullPath)) elseif istestfile(entry.name) then - table.insert(results, fullPath) + table.insert(results, path.normalize(fullPath)) end end @@ -37,7 +37,7 @@ local function findtestfiles(paths: { string }): { path.path } if fs.type(pathObj) == "dir" then tableext.extend(files, findtestfilesrec(pathObj)) elseif istestfile(filepath) then - table.insert(files, pathObj) + table.insert(files, path.normalize(pathObj)) end end diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index 49fddc0e6..477813acb 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -1,6 +1,10 @@ local finder = require("@self/finder") local luau = require("@std/luau") local path = require("@std/path") +local ps = require("@std/process") +local testtypes = require("@std/test/types") +local runner = require("@std/test/runner") +local reporter = require("@std/test/reporter") local test = require("@std/test") local function printHelp() @@ -85,6 +89,15 @@ local function listtests() end end +local function runtests(env: testtypes.testenvironment, runner: testtypes.testrunner, reporter: testtypes.testreporter) + local result = runner(env) + reporter(result) + if result.failed ~= 0 then + ps.exit(1) + end + ps.exit(0) +end + local function main(...: string) local args = { ... } local testPaths = {} @@ -105,6 +118,9 @@ local function main(...: string) if isListMode then listtests() + else + local env = test._registered() + runtests(env, runner.run, reporter.simple) end end diff --git a/lute/std/libs/test/runner.luau b/lute/std/libs/test/runner.luau index 42c972123..2c1be5934 100644 --- a/lute/std/libs/test/runner.luau +++ b/lute/std/libs/test/runner.luau @@ -4,6 +4,7 @@ local types = require("./types") local asserts = require("./assert") +local path = require("@std/path") type Test = types.test type TestCase = types.testcase @@ -103,7 +104,7 @@ function runner.run(env: TestEnvironment): TestRunResult test = tc.name, assertion = if assertName == "xpcall" then nil else assertName, error = if assertName == "xpcall" then nil else err.msg, - filename = fileName, + filename = path.format(fileName), linenumber = lineNumber, } table.insert(failures, failedtest) @@ -142,7 +143,7 @@ function runner.run(env: TestEnvironment): TestRunResult test = testFullName, assertion = if assertName == "xpcall" then nil else assertName, error = if assertName == "xpcall" then nil else err.msg, - filename = fileName, + filename = path.format(fileName), linenumber = lineNumber, } table.insert(failures, failedtest) diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index ac3d5abc3..452e316d8 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -44,11 +44,11 @@ test.suite("lute test", function(suite) assert.strcontains(result.stdout, "Usage:") end) - suite:case("lute test runs successfully", function(assert) - -- Run lute test without arguments - local result = process.run({ lutePath, "test" }) + suite:case("lute test runs tests in discovery folder", function(assert) + -- Run lute test on tests/cli/discovery folder + local result = process.run({ lutePath, "test", "tests/cli/discovery" }) - -- Check that it succeeds + -- Check that it runs tests and succeeds assert.eq(result.exitcode, 0) end) @@ -81,8 +81,7 @@ test.run() ) -- Run lute on the created test file - local result = process.run({ lutePath, tostring(testFilePath) }) - + local result = process.run({ lutePath, path.format(testFilePath) }) -- Check that the output doesn't include xpcall assert.eq(result.exitcode, 1) assert.eq(result.stdout:find("xpcall", 1, true), nil) @@ -114,7 +113,7 @@ test.run() ) -- Run lute on the created test file - local result = process.run({ lutePath, tostring(testFilePath) }) + local result = process.run({ lutePath, path.format(testFilePath) }) -- Check that the output doesn't include xpcall assert.eq(result.exitcode, 1) From 89c09d3071ce3e33a43ede399d543362d7643b34 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 3 Dec 2025 10:25:40 -0800 Subject: [PATCH 209/642] lute lint: warning suppression comments (#627) Implement warning suppression by checking if any ancestor nodes contain leading trivia with comments which suppress the relevant rule. This lets us do nice things like suppress in the following case: ```luau -- lute-lint-ignore(divide_by_zero) if true then local y = 10 / 0 end ``` The story is a bit more complicated with violations which span multiple nodes: We check if there are any suppressing nodes which subsume the violation, _or_ whether the first node completely contained in the violation suppresses the warning. This is roughly similar to what Selene does, and allows suppressing cases like the following: ```luau -- lute-lint-ignore(almost_swapped) a = b b = a ``` We'll need some follow up work to do (file) global warning suppression. --- lute/cli/commands/lint/init.luau | 94 ++++++- lute/cli/commands/lint/printer.luau | 8 +- lute/std/libs/syntax/init.luau | 24 +- lute/std/libs/test/assert.luau | 6 + tests/cli/lint.test.luau | 383 ++++++++++++++++++++++++++++ 5 files changed, 508 insertions(+), 7 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 34e4ec5b6..aa8cb01f2 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -5,7 +5,9 @@ local pathLib = require("@std/path") local printer = require("@self/printer") local syntax = require("@std/syntax") local tableext = require("@std/tableext") +local triviaUtils = require("@std/syntax/utils/trivia") local types = require("@self/types") +local visitor = require("@std/syntax/visitor") local DEFAULT_RULES = { "almost_swapped", "divide_by_zero" } local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" @@ -82,13 +84,96 @@ local function loadDefaultRules(): { types.LintRule } local rules = {} for _, ruleName in DEFAULT_RULES do local path = `@self/rules/{ruleName}` - local rule = require(path) + local rule: types.LintRule = require(path) :: any -- Anycast to silence error from dynamic require assertIsLintRule(rule, path) table.insert(rules, rule) end return rules end +local function parseSuppressions(node: syntax.AstNode): { [string]: boolean } + local suppressions: { [string]: boolean } = {} + + local leadingTrivia = triviaUtils.leftmosttrivia(node) + for _, trivia in leadingTrivia do + if trivia.tag == "blockcomment" or trivia.tag == "comment" then + for rule in trivia.text:gmatch("lute%-lint%-ignore%((.+)%)") do + suppressions[rule] = true + end + end + end + + return suppressions +end + +local function maybeParseSuppressions( + node: syntax.AstNode, + cache: { [syntax.AstNode]: { [string]: boolean } } +): { [string]: boolean } + local rules = cache[node] + if rules == nil then + rules = parseSuppressions(node) + + if node.kind == "stat" and node.tag == "block" then + -- The logic here is a little different, since we only want suppression to apply to the first statement + -- Store a sentinel value at this node to avoid reparsing suppressions + cache[node] = {} + -- Parse the rules that this node suppresses and attach them to the first statement of the block + cache[node.statements[1]] = rules + end + + cache[node] = rules + end + return rules +end + +local function violationIsSuppressed( + violation: types.LintViolation, + ast: syntax.AstStatBlock, + suppressionCache: { [syntax.AstNode]: { [string]: boolean } } +): boolean + local violationSuppressed = false + + local function nodeIsSuppressed(node: syntax.AstNode): boolean + if syntax.span.subsumes(violation.location, node.location) then + -- The first node which is fully contained within a violation can suppress it + local rules = maybeParseSuppressions(node, suppressionCache) + if rules[violation.lintname] then + violationSuppressed = true + end + -- No other fully contained nodes can suppress, so we can stop recursing + return false + end + + if not syntax.span.subsumes(node.location, violation.location) then + -- Violation doesn't come from this node, so don't recurse deeper + return false + end + + local rules = maybeParseSuppressions(node, suppressionCache) + + -- AstStatBlock suppressions should only apply to the first statement, not the block itself + if node.kind == "stat" and node.tag == "block" then + return true + end + + if rules[violation.lintname] then + violationSuppressed = true + -- If the node is suppressed, we don't need to recurse deeper + return false + else + -- A deeper node might suppress this rule + return true + end + end + + local suppressionVisitor = visitor.create(nodeIsSuppressed) + + visitor.visitblock(ast, suppressionVisitor) + + return violationSuppressed +end + local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule }): { types.LintViolation } print(`Reading input file '{inputFilePath}'`) local fileContent = fs.readfiletostring(inputFilePath) @@ -98,11 +183,16 @@ local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule print("Applying lint rules") local violations: { types.LintViolation } = {} + local suppressionCache = {} for _, rule in lintRules do local success, err = pcall(rule.lint, ast, inputFilePath) if success then -- On success, err contains the returned violations - tableext.extend(violations, err) + for _, violation in err do + if not violationIsSuppressed(violation, ast, suppressionCache) then + table.insert(violations, violation) + end + end else print(`Error applying lint rule '{rule.name}': {err}`) end diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau index 73362598e..1c0ed1540 100644 --- a/lute/cli/commands/lint/printer.luau +++ b/lute/cli/commands/lint/printer.luau @@ -37,10 +37,14 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number local blankGutter = string.rep(" ", gutterSize) - local renderedSourceLines = { ` {location.beginline} │ ╭ {sourceLines[location.beginline]}` } + local renderedSourceLines = + { ` {string.format(`%-{gutterSize - 2}d`, location.beginline)} │ ╭ {sourceLines[location.beginline]}` } local maxSourceLineLength = #renderedSourceLines[1] for lineNum = location.beginline + 1, location.endline do - table.insert(renderedSourceLines, ` {lineNum} │ │ {sourceLines[lineNum]}`) + table.insert( + renderedSourceLines, + ` {string.format(`%-{gutterSize - 2}d`, lineNum)} │ │ {sourceLines[lineNum]}` + ) maxSourceLineLength = math.max(maxSourceLineLength, #renderedSourceLines[#renderedSourceLines]) end diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index cfb9b5178..5090315d1 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -5,9 +5,22 @@ local luau = require("@lute/luau") export type span = types.span -local span = table.freeze({ +local span = { create = luau.span.create, -}) +} + +function span.subsumes(haystack: span, needle: span): boolean + return ( + if haystack.beginline == needle.beginline + then haystack.begincolumn <= needle.begincolumn + else haystack.beginline < needle.beginline + ) + and ( + if haystack.endline == needle.endline + then haystack.endcolumn >= needle.endcolumn + else haystack.endline > needle.endline + ) +end export type Whitespace = types.Whitespace export type SingleLineComment = types.SingleLineComment @@ -164,4 +177,9 @@ export type AstNode = types.AstNode export type ParseResult = types.ParseResult -return table.freeze({ parseblock = parser.parseblock, parseexpr = parser.parseexpr, parse = parser.parse, span = span }) +return table.freeze({ + parseblock = parser.parseblock, + parseexpr = parser.parseexpr, + parse = parser.parse, + span = table.freeze(span), +}) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index 65d679c66..813aae4eb 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -137,6 +137,12 @@ function assertions.strcontains(haystack: string, needle: string, msg: string?, end end +function assertions.strnotcontains(haystack: string, needle: string, msg: string?) + if haystack:find(needle, 1, true) ~= nil then + error({ msg = if msg then msg else `Expected "{haystack}" to not contain "{needle}".` }) + end +end + export type asserts = typeof(assertions) return table.freeze(assertions) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 941f1b6ef..035db0d4f 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -446,6 +446,389 @@ violator.luau:3:1-4:6 ── -- Teardown fs.remove(violatorPath) end) + + suite:case("lute lint warning suppression", function(assert) + -- Create a file that violates the default almost_swapped and divide_by_zero rules + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(divide_by_zero) +local x = 1 / 0 + +-- lute-lint-ignore(divide_by_zero) +if true then + local y = 10 / 0 +end + +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + local expected = [[ +violator.luau:9:1-10:6 ── + │ + 9 │ ╭ a = b + 10 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(divide_by_zero) +local x = 1 / 0 + +if true then + local y = 10 / 0 +end + +local z = 1 / 0 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) + local expected = [[ +violator.luau:5:12-18 ── + │ + 5 │ local y = 10 / 0 + │ ^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:8:11-16 ── + │ + 8 │ local z = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement2", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + +-- lute-lint-ignore(divide_by_zero) +if true then + local y = 10 / 0 +end + +local z = 1 / 0 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:8:11-16 ── + │ + 8 │ local z = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement3", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local function foo() + -- lute-lint-ignore(divide_by_zero) + local y = 3 / 0 + + return 3 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator.luau:5:9-14 ── + │ + 5 │ return 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement4", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +if true then + -- lute-lint-ignore(divide_by_zero) + local y = 3 / 0 + + return 3 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator.luau:5:9-14 ── + │ + 5 │ return 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement5", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +while true do + -- lute-lint-ignore(divide_by_zero) + local y = 3 / 0 + + return 3 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator.luau:5:9-14 ── + │ + 5 │ return 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression is limited to following statement6", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local t = {} +for _, _ in t do + -- lute-lint-ignore(divide_by_zero) + local y = 3 / 0 + + return 3 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator.luau:6:9-14 ── + │ + 6 │ return 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression for multi node violation", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(almost_swapped) +a = b +b = a + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("suppression for multi node violation", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(almost_swapped) +a = b +b = a +a = b + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.", nil, 1) + + local expected = [[ +violator.luau:3:1-4:6 ── + │ + 3 │ ╭ b = a + 4 │ │ a = b + │ ╰─────^ + │ +]] + + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("nested suppressions", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-ignore(divide_by_zero) +local function foo() + local x = 1/0 + local function bar() + -- lute-lint-ignore(almost_swapped) + a = b + b = a + end +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + assert.strnotcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + + -- Teardown + fs.remove(violatorPath) + end) end) test.run() From 2c84896035f19a1460f76dc864b4f2c6f0254efe Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 3 Dec 2025 15:14:47 -0800 Subject: [PATCH 210/642] lute lint: fix tab pretty printing (#638) Fix tab pretty printing by substituting them with 4 spaces + other adjustments. I added `stringext.count` along the way --- lute/cli/commands/lint/printer.luau | 26 +++++++++++++++++++------- lute/std/libs/stringext.luau | 18 ++++++++++++++++++ tests/cli/lint.test.luau | 26 +++++++++++++------------- tests/std/stringext.test.luau | 11 +++++++++++ 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau index 1c0ed1540..922033102 100644 --- a/lute/cli/commands/lint/printer.luau +++ b/lute/cli/commands/lint/printer.luau @@ -1,9 +1,12 @@ local fs = require("@std/fs") local path = require("@std/path") +local stringext = require("@std/stringext") local types = require("./types") local printerLib = {} +local TAB_SIZE = 4 + local function printLint(lint: types.LintViolation, sourceLines: { string }): () print(`{lint.severity}[{lint.lintname}]: {lint.message}\n`) @@ -14,7 +17,13 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number local blankGutter = string.rep(" ", gutterSize) - local sourceLine = ` {location.beginline} │ {sourceLines[location.beginline]}` + -- Compute the number of tabs before the violation and within the violation + local prelintTabs = stringext.count(sourceLines[location.beginline], "\t", 1, location.begincolumn - 1) + local lintTabs = + stringext.count(sourceLines[location.beginline], "\t", location.begincolumn, location.endcolumn) + -- Convert tabs in the source line to spaces + local sourceLine = + ` {location.beginline} │ {sourceLines[location.beginline]:gsub("\t", string.rep(" ", TAB_SIZE))}` local filepathLine = `{blankGutter}┌── {filepath}:{location.beginline}:{location.begincolumn}-{location.endcolumn} ──` @@ -26,9 +35,9 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () print(`{blankGutter}│`) print(sourceLine) print( - `{blankGutter}│ {string.rep(" ", location.begincolumn - 1)}{string.rep( + `{blankGutter}│ {string.rep(" ", location.begincolumn - 1 + (prelintTabs * (TAB_SIZE - 1)))}{string.rep( "^", - location.endcolumn - location.begincolumn + location.endcolumn - location.begincolumn + (lintTabs * (TAB_SIZE - 1)) )}` ) print(`{blankGutter}│`) @@ -37,13 +46,16 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number local blankGutter = string.rep(" ", gutterSize) - local renderedSourceLines = - { ` {string.format(`%-{gutterSize - 2}d`, location.beginline)} │ ╭ {sourceLines[location.beginline]}` } + local subbedSourceLine = sourceLines[location.beginline]:gsub("\t", string.rep(" ", TAB_SIZE)) + local renderedSourceLines = { + ` {string.format(`%-{gutterSize - 2}d`, location.beginline)} │ ╭ {subbedSourceLine}`, + } local maxSourceLineLength = #renderedSourceLines[1] for lineNum = location.beginline + 1, location.endline do + subbedSourceLine = sourceLines[lineNum]:gsub("\t", string.rep(" ", TAB_SIZE)) table.insert( renderedSourceLines, - ` {string.format(`%-{gutterSize - 2}d`, lineNum)} │ │ {sourceLines[lineNum]}` + ` {string.format(`%-{gutterSize - 2}d`, lineNum)} │ │ {subbedSourceLine}` ) maxSourceLineLength = math.max(maxSourceLineLength, #renderedSourceLines[#renderedSourceLines]) end @@ -59,7 +71,7 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () for _, line in renderedSourceLines do print(line) end - print(`{blankGutter}│ ╰─{string.rep("─", location.endcolumn - 2)}^`) + print(`{blankGutter}│ ╰{string.rep("─", maxSourceLineLength - gutterSize - 8)}^`) print(`{blankGutter}│`) print() end diff --git a/lute/std/libs/stringext.luau b/lute/std/libs/stringext.luau index 04b4797cb..ead98abb5 100644 --- a/lute/std/libs/stringext.luau +++ b/lute/std/libs/stringext.luau @@ -38,4 +38,22 @@ function stringext.trim(str: string): string return str:match("^%s*(.-)%s*$") :: string end +-- Counts the number of occurrences of pattern in str between startPos and endPos (inclusive) +function stringext.count(str: string, pattern: string, startPos: number?, endPos: number?): number + if startPos and endPos then + str = str:sub(startPos, endPos) + elseif startPos then + str = str:sub(startPos) + elseif endPos then + str = str:sub(1, endPos) + end + + local count = 0 + for _ in str:gmatch(pattern) do + count += 1 + end + + return count +end + return table.freeze(stringext) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 035db0d4f..cef758519 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -148,9 +148,9 @@ violator.luau:10:1-11:10 ── expected = [[ violator.luau:14:2-15:17 ── │ - 14 │ ╭ t["a"] = t["b"] - 15 │ │ t["b"] = t["a"] - │ ╰────────────────^ + 14 │ ╭ t["a"] = t["b"] + 15 │ │ t["b"] = t["a"] + │ ╰───────────────────^ │ ]] -- todo: address tab shenanigans assert.strcontains(result.stdout, expected) @@ -519,8 +519,8 @@ local z = 1 / 0 local expected = [[ violator.luau:5:12-18 ── │ - 5 │ local y = 10 / 0 - │ ^^^^^^ + 5 │ local y = 10 / 0 + │ ^^^^^^ │ ]] assert.strcontains(result.stdout, expected) @@ -613,8 +613,8 @@ end local expected = [[ violator.luau:5:9-14 ── │ - 5 │ return 3 / 0 - │ ^^^^^ + 5 │ return 3 / 0 + │ ^^^^^ │ ]] assert.strcontains(result.stdout, expected) @@ -650,8 +650,8 @@ end local expected = [[ violator.luau:5:9-14 ── │ - 5 │ return 3 / 0 - │ ^^^^^ + 5 │ return 3 / 0 + │ ^^^^^ │ ]] assert.strcontains(result.stdout, expected) @@ -687,8 +687,8 @@ end local expected = [[ violator.luau:5:9-14 ── │ - 5 │ return 3 / 0 - │ ^^^^^ + 5 │ return 3 / 0 + │ ^^^^^ │ ]] assert.strcontains(result.stdout, expected) @@ -725,8 +725,8 @@ end local expected = [[ violator.luau:6:9-14 ── │ - 6 │ return 3 / 0 - │ ^^^^^ + 6 │ return 3 / 0 + │ ^^^^^ │ ]] assert.strcontains(result.stdout, expected) diff --git a/tests/std/stringext.test.luau b/tests/std/stringext.test.luau index f2add781c..7fdb5b620 100644 --- a/tests/std/stringext.test.luau +++ b/tests/std/stringext.test.luau @@ -24,6 +24,17 @@ test.suite("StringExt", function(suite) assert.eq(stringext.trim(""), "") assert.eq(stringext.trim(" "), "") end) + + suite:case("count", function(assert) + assert.eq(stringext.count("hello world", "o"), 2) + assert.eq(stringext.count("hello world", "l", 4), 2) -- from position 4 to end + assert.eq(stringext.count("hello world", "l", nil, 5), 2) -- from start to position 5 + assert.eq(stringext.count("hello world", "l", 4, 9), 1) -- from position 4 to position 9 + assert.eq(stringext.count("aaaaaa", "aa"), 3) -- overlapping patterns + assert.eq(stringext.count("no matches here", "z"), 0) + assert.eq(stringext.count("hello world", "%s"), 1) -- patterns supported too + assert.eq(stringext.count("hello world", "[hw]"), 2) -- count h and w + end) end) test.run() From b6843725ce094f20861f6c6307d06e969f0f9ca7 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 3 Dec 2025 16:21:25 -0800 Subject: [PATCH 211/642] lute lint: add verbosity option (#641) title says it all --- lute/cli/commands/lint/init.luau | 31 +++++++++++++++++----- tests/cli/lint.test.luau | 45 +++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index aa8cb01f2..b2396ad9d 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -11,6 +11,7 @@ local visitor = require("@std/syntax/visitor") local DEFAULT_RULES = { "almost_swapped", "divide_by_zero" } local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" +local VERBOSE = false local function printHelp() print(USAGE) @@ -19,6 +20,7 @@ Lint the specified Luau file using the specified lint rule(s) or using the defau OPTIONS: -h, --help Show this help message + -v, --verbose Enable verbose output -r, --rules [RULE] Path to a single lint rule or a folder containing lint rules. If a folder is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated @@ -175,13 +177,19 @@ local function violationIsSuppressed( end local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule }): { types.LintViolation } - print(`Reading input file '{inputFilePath}'`) + if VERBOSE then + print(`Reading input file '{inputFilePath}'`) + end local fileContent = fs.readfiletostring(inputFilePath) - print(`Parsing input file '{inputFilePath}'`) + if VERBOSE then + print(`Parsing input file '{inputFilePath}'`) + end local ast = syntax.parseblock(fileContent) - print("Applying lint rules") + if VERBOSE then + print("Applying lint rules") + end local violations: { types.LintViolation } = {} local suppressionCache = {} for _, rule in lintRules do @@ -213,6 +221,7 @@ local function main(...: string) args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) + args:add("verbose", "flag", { help = "Enable verbose output", aliases = { "v" } }) -- TODO: ignore blobs, json output option args:parse({ ... }) @@ -222,6 +231,8 @@ local function main(...: string) return end + VERBOSE = args:has("verbose") + local inputFiles = args:forwarded() if inputFiles == nil or #inputFiles == 0 then print("Error: No input files specified.\n\n" .. USAGE .. "\nUse --help for more information.") @@ -231,10 +242,14 @@ local function main(...: string) local lintRules: { types.LintRule } local rulePath = args:get("rules") if rulePath == nil then - print("Using default lint rules.") + if VERBOSE then + print("Using default lint rules.") + end lintRules = loadDefaultRules() else - print(`Loading lint rule(s) from '{rulePath}'`) + if VERBOSE then + print(`Loading lint rule(s) from '{rulePath}'`) + end lintRules = loadLintRules(rulePath) end @@ -256,7 +271,7 @@ local function main(...: string) else print(`Error linting file '{inputFilePath}': {err}`) end - else + elseif VERBOSE then print(`Skipping non-Luau file '{inputFilePath}'`) end @@ -264,7 +279,9 @@ local function main(...: string) end end - print(`Printing violations from {#allViolations} files\n`) + if VERBOSE then + print(`Printing violations from {#allViolations} files\n`) + end for sourcePath, violations in allViolations do if #violations > 0 then printer.printLints(violations, sourcePath) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index cef758519..a0f231889 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -49,6 +49,48 @@ test.suite("lute lint", function(suite) assert.strcontains(result.stdout, "Error: No input files specified.") end) + suite:case("lute lint non verbose", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile(violatorPath, "") + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.eq(result.stdout, "") + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("lute lint verbose", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile(violatorPath, "") + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-v", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "Using default lint rules.") + assert.neq(result.stdout:find("Reading input file '.+violator%.luau'"), nil) + assert.neq(result.stdout:find("Parsing input file '.+violator%.luau'"), nil) + assert.strcontains(result.stdout, "Applying lint rules") + assert.strcontains(result.stdout, "Printing violations from 0 files") + + -- Teardown + fs.remove(violatorPath) + end) + suite:case("divide_by_zero", function(assert) -- Setup -- Create a file that violates the rule @@ -312,7 +354,8 @@ y = x -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", "examples/lints", violatorPath1, path.format(modulePath) }) + local result = + process.run({ lutePath, "lint", "-r", "examples/lints", "-v", violatorPath1, path.format(modulePath) }) -- Check assert.eq(result.exitcode, 0) From b4e920849223e6496a609e7391c8d529e9625c9a Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:58:00 -0500 Subject: [PATCH 212/642] Deque Battery (#636) Adds a `deque` battery serving as the first member of a `collections` module. --- batteries/collections/deque.luau | 70 +++++++++++++++ tests/batteries/collections/deque.test.luau | 97 +++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 batteries/collections/deque.luau create mode 100644 tests/batteries/collections/deque.test.luau diff --git a/batteries/collections/deque.luau b/batteries/collections/deque.luau new file mode 100644 index 000000000..a78d085b1 --- /dev/null +++ b/batteries/collections/deque.luau @@ -0,0 +1,70 @@ +export type Deque = { + _head: number, + _tail: number, + _values: { [number]: T }, + pushfront: (self: Deque, value: T) -> (), + pushback: (self: Deque, value: T) -> (), + popfront: (self: Deque) -> T, + popback: (self: Deque) -> T, + peekfront: (self: Deque) -> T?, + peekback: (self: Deque) -> T?, +} + +local deque = {} +deque.__index = deque +deque.__len = function(self: Deque) + return self._tail - self._head + 1 +end + +local dequeLib = {} + +function dequeLib.new(initValue: T?): Deque + local self = {} :: Deque + self._values = {} + self._head, self._tail = 0, if initValue then 0 else -1 + if initValue then + self._values[self._head] = initValue + end + + return setmetatable(self, deque) +end + +function deque.pushfront(self: Deque, value: T) + self._head -= 1 + self._values[self._head] = value +end + +function deque.pushback(self: Deque, value: T) + self._tail += 1 + self._values[self._tail] = value +end + +function deque.popfront(self: Deque): T + if self._tail < self._head then + error("Popping from empty deque") + end + local toReturn = self._values[self._head] + self._values[self._head] = nil + self._head += 1 + return toReturn +end + +function deque.popback(self: Deque): T + if self._tail < self._head then + error("Popping from empty deque") + end + local toReturn = self._values[self._tail] + self._values[self._tail] = nil + self._tail -= 1 + return toReturn +end + +function deque.peekfront(self: Deque) + return self._values[self._head] +end + +function deque.peekback(self: Deque) + return self._values[self._tail] +end + +return table.freeze(dequeLib) diff --git a/tests/batteries/collections/deque.test.luau b/tests/batteries/collections/deque.test.luau new file mode 100644 index 000000000..f3e013f6d --- /dev/null +++ b/tests/batteries/collections/deque.test.luau @@ -0,0 +1,97 @@ +local deque = require("@batteries/collections/deque") +local test = require("@std/test") + +test.suite("deque", function(suite) + suite:case("deque.new with no value creates empty deque", function(assert) + local deq = deque.new() + assert.eq(deq:peekfront(), nil) + assert.eq(deq:peekback(), nil) + assert.eq(#deq, 0) + end) + + suite:case("deque.new with values creates deque with one element", function(assert) + local deq = deque.new(1) + assert.eq(deq:peekfront(), 1) + assert.eq(deq:peekback(), 1) + assert.eq(#deq, 1) + end) + + suite:case("deque:pushfront", function(assert) + local deq = deque.new(2) + deq:pushfront(1) + assert.eq(deq:peekfront(), 1) + assert.eq(deq:peekback(), 2) + assert.eq(#deq, 2) + end) + + suite:case("deque:pushback", function(assert) + local deq = deque.new(2) + deq:pushback(1) + assert.eq(deq:peekfront(), 2) + assert.eq(deq:peekback(), 1) + assert.eq(#deq, 2) + end) + + suite:case("deque:popfront", function(assert) + local deq = deque.new(2) + local popped = deq:popfront() + assert.eq(popped, 2) + assert.eq(#deq, 0) + assert.eq(deq:peekfront(), nil) + assert.eq(deq:peekback(), nil) + end) + + suite:case("deque:popback", function(assert) + local deq = deque.new(2) + local popped = deq:popback() + assert.eq(popped, 2) + assert.eq(#deq, 0) + assert.eq(deq:peekfront(), nil) + assert.eq(deq:peekback(), nil) + end) + + suite:case("deque:peekfront", function(assert) + local deq = deque.new(1) + assert.eq(#deq, 1) + assert.eq(deq:peekfront(), 1) + end) + + suite:case("deque:peekback", function(assert) + local deq = deque.new(1) + assert.eq(#deq, 1) + assert.eq(deq:peekback(), 1) + end) + + suite:case("popping from empty deque", function(assert) + local deq = deque.new() + assert.erroreq(function() + deq:popback() + end, "Popping from empty deque") + assert.erroreq(function() + deq:popfront() + end, "Popping from empty deque") + end) + + suite:case("e2e", function(assert) + local deq = deque.new() + deq:pushback(1) + deq:pushback(2) + deq:pushback(3) + + assert.eq(#deq, 3) + assert.eq(deq:peekfront(), 1) + assert.eq(deq:peekback(), 3) + + assert.eq(deq:popfront(), 1) + assert.eq(#deq, 2) + assert.eq(deq:peekfront(), 2) + assert.eq(deq:peekback(), 3) + + assert.eq(deq:popback(), 3) + assert.eq(#deq, 1) + assert.eq(deq:peekfront(), 2) + assert.eq(deq:peekback(), 2) + end) +end) + +test.run() From 0a2fa0f47ef35b7feda302f786024c80bffb0da6 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 4 Dec 2025 09:58:40 -0800 Subject: [PATCH 213/642] lute lint: json output (#635) Add an option for json output that roughly models the LSP Diagnostic format, with the hope that it'll be consumed by `luau-lsp` at some point. --- lute/cli/commands/lint/init.luau | 24 ++++++--- lute/cli/commands/lint/lsp/init.luau | 73 +++++++++++++++++++++++++++ lute/cli/commands/lint/lsp/types.luau | 22 ++++++++ lute/cli/commands/lint/types.luau | 2 + tests/cli/lint.test.luau | 46 +++++++++++++++++ 5 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 lute/cli/commands/lint/lsp/init.luau create mode 100644 lute/cli/commands/lint/lsp/types.luau diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index b2396ad9d..34f4632af 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -1,5 +1,7 @@ local cli = require("./cli") local fs = require("@std/fs") +local json = require("@std/json") +local lsp = require("@self/lsp") local luau = require("@std/luau") local pathLib = require("@std/path") local printer = require("@self/printer") @@ -25,6 +27,7 @@ OPTIONS: is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated as individual lint rules. If unspecified, the default lint rules are used. + -j, --json Output lint violations in JSON format matching the LSP diagnostic spec. PATHS: Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. @@ -222,7 +225,8 @@ local function main(...: string) args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) args:add("verbose", "flag", { help = "Enable verbose output", aliases = { "v" } }) - -- TODO: ignore blobs, json output option + args:add("json", "flag", { help = "Output lint violations in JSON format", aliases = { "j" } }) + -- TODO: ignore blobs args:parse({ ... }) @@ -253,7 +257,7 @@ local function main(...: string) lintRules = loadLintRules(rulePath) end - local allViolations = {} + local allViolations: { [pathLib.path]: { types.LintViolation } } = {} for _, inputPath in inputFiles do local walker = fs.walk(inputPath, { recursive = true }) @@ -279,12 +283,16 @@ local function main(...: string) end end - if VERBOSE then - print(`Printing violations from {#allViolations} files\n`) - end - for sourcePath, violations in allViolations do - if #violations > 0 then - printer.printLints(violations, sourcePath) + if args:has("json") then + print(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.object, true)) + else + if VERBOSE then + print(`Printing violations from {#allViolations} files\n`) + end + for sourcePath, violations in allViolations do + if #violations > 0 then + printer.printLints(violations, sourcePath) + end end end end diff --git a/lute/cli/commands/lint/lsp/init.luau b/lute/cli/commands/lint/lsp/init.luau new file mode 100644 index 000000000..fc3a16249 --- /dev/null +++ b/lute/cli/commands/lint/lsp/init.luau @@ -0,0 +1,73 @@ +local pathLib = require("@std/path") +local lintTypes = require("./types") +local lspTypes = require("@self/types") +local tableext = require("@std/tableext") + +local lsp = {} + +local function severity(sev: lintTypes.severity): number + if sev == "error" then + return 1 + elseif sev == "warning" then + return 2 + elseif sev == "info" then + return 3 + else + return 4 + end +end + +local function tag(t: lintTypes.tag): number + if t == "unnecessary" then + return 1 + else + return 2 + end +end + +local function diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic + return table.freeze({ + range = table.freeze({ + start = table.freeze({ + line = lint.location.beginline - 1, + character = lint.location.begincolumn - 1, + }), + ["end"] = table.freeze({ + line = lint.location.endline - 1, + character = lint.location.endcolumn - 1, + }), + }), + severity = severity(lint.severity), + code = lint.lintname, + source = "lute lint", -- LUAUFIX: Errors because this isn't inferred as a singleton + message = lint.message, + tags = if lint.tags then table.freeze(tableext.map(lint.tags, tag)) else nil, + }) +end + +local function workspaceDocumentDiagnosticReport( + path: pathLib.path, + violations: { lintTypes.LintViolation } +): lspTypes.WorkspaceDocumentDiagnosticReport + local report = { items = {}, uri = pathLib.format(path) } + for _, violation in violations do + table.insert(report.items, diagnostic(violation)) + end + return table.freeze(report) +end + +function lsp.workspaceDiagnosticReport( + violations: { [pathLib.path]: { lintTypes.LintViolation } } +): lspTypes.WorkspaceDiagnosticReport + local report = { + items = {}, + } + + for path, pathViolations in violations do + table.insert(report.items, workspaceDocumentDiagnosticReport(path, pathViolations)) + end + + return table.freeze(report) +end + +return table.freeze(lsp) diff --git a/lute/cli/commands/lint/lsp/types.luau b/lute/cli/commands/lint/lsp/types.luau new file mode 100644 index 000000000..c9fbe176c --- /dev/null +++ b/lute/cli/commands/lint/lsp/types.luau @@ -0,0 +1,22 @@ +export type Diagnostic = { + read range: { + read start: { read line: number, read character: number }, + read ["end"]: { read line: number, read character: number }, + }, -- zero-based + read severity: number, -- 1: Error, 2: Warning, 3: Information, 4: Hint + read code: string, -- lint name + read source: "lute lint", + read message: string, + read tags: { number }?, -- 1: Unnecessary, 2: Deprecated +} + +export type WorkspaceDocumentDiagnosticReport = { + read items: { Diagnostic }, + read uri: string, -- Note: Any lsp consumers will need to ensure this is a proper file URI. For now, we just use the file path. +} + +export type WorkspaceDiagnosticReport = { + read items: { WorkspaceDocumentDiagnosticReport }, +} + +return table.freeze({}) diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index cae182961..c3601ab65 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -2,6 +2,7 @@ local syntax = require("@std/syntax") local path = require("@std/path") export type severity = "error" | "warning" | "info" | "hint" +export type tag = "unnecessary" | "deprecated" export type LintViolation = { read lintname: string, @@ -9,6 +10,7 @@ export type LintViolation = { read message: string, read severity: severity, read sourcepath: path.path, + read tags: { tag }?, } -- TODO: add suggested fix export type LintRule = { diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index a0f231889..cd23d8da6 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -872,6 +872,52 @@ end -- Teardown fs.remove(violatorPath) end) + + suite:case("json output", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-j", violatorPath }) + -- Check + assert.eq(result.exitcode, 0) + + local expected = [[ +{ + "items": [ + { + "items": [ + { + "message": "Division by zero detected.", + "source": "lute lint", + "code": "divide_by_zero", + "severity": 2, + "range": { + "start": { + "character": 10, + "line": 0 + }, + "end": { + "character": 15, + "line": 0 + } + } + } + ], + "uri":]] -- URI has absolute tmpdir path, so we cut off here + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) end) test.run() From a56da82aa0b9412df8b26f00639627cb18bf09f1 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 4 Dec 2025 10:07:36 -0800 Subject: [PATCH 214/642] lute lint: respect gitignores (#644) Update lute lint to respect .gitignore files using the same infrastructure that `lute transform` already uses. I moved some files so that `lint` wasn't dependent on `transform` --- lute/cli/commands/{ => lib}/cli.luau | 0 .../commands/{transform => }/lib/files.luau | 26 +++++++--- .../commands/{transform => }/lib/ignore.luau | 0 lute/cli/commands/lint/init.luau | 27 +++++----- lute/cli/commands/transform/init.luau | 2 +- tests/cli/lint.test.luau | 51 +++++++++++++++++++ 6 files changed, 85 insertions(+), 21 deletions(-) rename lute/cli/commands/{ => lib}/cli.luau (100%) rename lute/cli/commands/{transform => }/lib/files.luau (74%) rename lute/cli/commands/{transform => }/lib/ignore.luau (100%) diff --git a/lute/cli/commands/cli.luau b/lute/cli/commands/lib/cli.luau similarity index 100% rename from lute/cli/commands/cli.luau rename to lute/cli/commands/lib/cli.luau diff --git a/lute/cli/commands/transform/lib/files.luau b/lute/cli/commands/lib/files.luau similarity index 74% rename from lute/cli/commands/transform/lib/files.luau rename to lute/cli/commands/lib/files.luau index 2cdfc9186..8ed07dd10 100644 --- a/lute/cli/commands/transform/lib/files.luau +++ b/lute/cli/commands/lib/files.luau @@ -6,7 +6,11 @@ local path = require("@std/path") local stringext = require("@std/stringext") local tableext = require("@std/tableext") -local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: ignore.IgnoreResolver): { path.path } +local function traverseDirectoryRecursive( + directory: path.path, + ignoreResolver: ignore.IgnoreResolver, + verbose: boolean? +): { path.path } local results = {} for _, entry in fs.listdir(path.format(directory)) do @@ -14,19 +18,25 @@ local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: if entry.type == "dir" then if ignoreResolver:isIgnoredDirectory(fullPath) then - print("Skipping", fullPath) + if verbose then + print("Skipping", fullPath) + end continue end - tableext.extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver)) + tableext.extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver, verbose)) else -- TODO: allow customisation of the filter when traversing a directory. e.g., globs? file endings? ignore paths? if stringext.hassuffix(entry.name, ".luau") or stringext.hassuffix(entry.name, ".lua") then if ignoreResolver:isIgnoredFile(fullPath) then - print("Skipping", fullPath) + if verbose then + print("Skipping", fullPath) + end continue end table.insert(results, fullPath) + elseif verbose then + print(`Skipping non-Luau file '{fullPath}'`) end end end @@ -34,7 +44,7 @@ local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: return results end -local function getSourceFiles(paths: { string }): { path.path } +local function getSourceFiles(paths: { string }, verbose: boolean?): { path.path } local files = {} local ignoreResolver = ignore.new() @@ -47,9 +57,11 @@ local function getSourceFiles(paths: { string }): { path.path } end filepath = path.format(pathObj) - print(filepath) + if verbose then + print(filepath) + end if fs.type(filepath) == "dir" then - tableext.extend(files, traverseDirectoryRecursive(pathObj, ignoreResolver)) + tableext.extend(files, traverseDirectoryRecursive(pathObj, ignoreResolver, verbose)) else table.insert(files, pathObj) end diff --git a/lute/cli/commands/transform/lib/ignore.luau b/lute/cli/commands/lib/ignore.luau similarity index 100% rename from lute/cli/commands/transform/lib/ignore.luau rename to lute/cli/commands/lib/ignore.luau diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 34f4632af..6888ec631 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -1,4 +1,5 @@ -local cli = require("./cli") +local cli = require("./lib/cli") +local files = require("./lib/files") local fs = require("@std/fs") local json = require("@std/json") local lsp = require("@self/lsp") @@ -243,6 +244,11 @@ local function main(...: string) return end + if VERBOSE then + print("Filtering input files by gitignore") + end + local inputFilePaths = files.getSourceFiles(inputFiles, VERBOSE) + local lintRules: { types.LintRule } local rulePath = args:get("rules") if rulePath == nil then @@ -259,24 +265,19 @@ local function main(...: string) local allViolations: { [pathLib.path]: { types.LintViolation } } = {} - for _, inputPath in inputFiles do + for _, inputPath in inputFilePaths do local walker = fs.walk(inputPath, { recursive = true }) local curr = walker() while curr ~= nil do local inputFilePath = pathLib.parse(curr) - local extension = pathLib.extname(inputFilePath) - if extension == ".luau" or extension == ".lua" then - local success, err = pcall(lintFile, inputFilePath, lintRules) - if success then - -- Violations are returned as the second return value from pcall - allViolations[inputFilePath] = err - else - print(`Error linting file '{inputFilePath}': {err}`) - end - elseif VERBOSE then - print(`Skipping non-Luau file '{inputFilePath}'`) + local success, err = pcall(lintFile, inputFilePath, lintRules) + if success then + -- Violations are returned as the second return value from pcall + allViolations[inputFilePath] = err + else + print(`Error linting file '{inputFilePath}': {err}`) end curr = walker() diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index f6c1d55f6..5a9bb7863 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -5,7 +5,7 @@ local printer = require("@std/syntax/printer") local syntax = require("@std/syntax") local arguments = require("@self/lib/arguments") -local files = require("@self/lib/files") +local files = require("./lib/files") local types = require("@self/lib/types") local function exhaustiveMatch(value: never): never diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index cd23d8da6..359a8a79e 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -918,6 +918,57 @@ local x = 1 / 0 -- Teardown fs.remove(violatorPath) end) + + suite:case("lute lint respects gitignore", function(assert) + -- Setup + -- Create a module directory with a .gitignore that ignores one file + local modulePath = path.join(tmpDir, "module") + fs.createdirectory(modulePath) + local violatorPath1 = path.format(path.join(modulePath, "violator1.luau")) + fs.writestringtofile( + violatorPath1, + [[ +local x = 10 / 0 + ]] + ) + local violatorPath2 = path.format(path.join(modulePath, "violator2.luau")) + fs.writestringtofile( + violatorPath2, + [[ +local y = 10 / 0 + ]] + ) + fs.writestringtofile(path.format(path.join(modulePath, ".gitignore")), "violator2.luau\n") + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", path.format(modulePath) }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + local expected = [[ +violator1.luau:1:11-17 ── + │ + 1 │ local x = 10 / 0 + │ ^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator2.lua:1:11-17 ── + │ + 1 │ local y = 10 / 0 + │ ^^^^^^ + │ +]] + assert.strnotcontains(result.stdout, expected) + + -- Teardown + fs.removedirectory(modulePath, { recursive = true }) + end) end) test.run() From 13522a686c0479c3772697836a79c9a2fb272f4b Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:40:09 -0800 Subject: [PATCH 215/642] fix: fs.watch test (#640) fixes #639 --- tests/std/fs.test.luau | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 96e5857cc..ab1fb309b 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -93,24 +93,23 @@ test.suite("FsSuite", function(suite) suite:case("watch_iterator_on_change", function(assert) local watchedFileName = "watched.txt" local watched = path.join(tmpdir, watchedFileName) - if fs.exists(watched) then - fs.remove(watched) - end local watcher = fs.watch(tmpdir) - fs.writestringtofile(watched, "x") + fs.writestringtofile(watched, "hi") + -- Note: we had a timeout for this loop before, but it was leading to a flaky test where event = nil, so we're removing it. + -- If this hangs indefinitely at, we'll see this test fail later and revisit. local start = os.clock() local event - repeat event = watcher:next() if not event then task.wait(0.01) end - until event or (os.clock() - start > 2) + until event + print("Watch iterator time taken: " .. (os.clock() - start)) assert.neq(event, nil) assert.eq(event.change or event.rename, true) From bf1faee68cc5c8f523e961024aa7ff600aa2577e Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 4 Dec 2025 15:13:12 -0800 Subject: [PATCH 216/642] lute lint: add report directive (#637) Adds a report directive which allows overriding an ignore directive from an outer scope. See test for example usage --- lute/cli/commands/lint/init.luau | 61 +++++++++++++++++--------------- tests/cli/lint.test.luau | 46 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 6888ec631..175365ab1 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -97,57 +97,61 @@ local function loadDefaultRules(): { types.LintRule } return rules end -local function parseSuppressions(node: syntax.AstNode): { [string]: boolean } - local suppressions: { [string]: boolean } = {} +local function parseDirectives(node: syntax.AstNode): { [string]: boolean } + local directives: { [string]: boolean } = {} local leadingTrivia = triviaUtils.leftmosttrivia(node) for _, trivia in leadingTrivia do if trivia.tag == "blockcomment" or trivia.tag == "comment" then for rule in trivia.text:gmatch("lute%-lint%-ignore%((.+)%)") do - suppressions[rule] = true + directives[rule] = true -- True means ignore + end + for rule in trivia.text:gmatch("lute%-lint%-report%((.+)%)") do + directives[rule] = false -- False means report end end end - return suppressions + return directives end -local function maybeParseSuppressions( +local function maybeParseDirectives( node: syntax.AstNode, cache: { [syntax.AstNode]: { [string]: boolean } } ): { [string]: boolean } - local rules = cache[node] - if rules == nil then - rules = parseSuppressions(node) + local directives = cache[node] + if directives == nil then + directives = parseDirectives(node) if node.kind == "stat" and node.tag == "block" then -- The logic here is a little different, since we only want suppression to apply to the first statement -- Store a sentinel value at this node to avoid reparsing suppressions cache[node] = {} -- Parse the rules that this node suppresses and attach them to the first statement of the block - cache[node.statements[1]] = rules + cache[node.statements[1]] = directives end - cache[node] = rules + cache[node] = directives end - return rules + return directives end local function violationIsSuppressed( violation: types.LintViolation, ast: syntax.AstStatBlock, - suppressionCache: { [syntax.AstNode]: { [string]: boolean } } + directiveCache: { [syntax.AstNode]: { [string]: boolean } } ): boolean local violationSuppressed = false + -- We want to find the lint directive with the tightest scope that applies to this violation local function nodeIsSuppressed(node: syntax.AstNode): boolean if syntax.span.subsumes(violation.location, node.location) then - -- The first node which is fully contained within a violation can suppress it - local rules = maybeParseSuppressions(node, suppressionCache) - if rules[violation.lintname] then - violationSuppressed = true - end - -- No other fully contained nodes can suppress, so we can stop recursing + -- The first node which is fully contained within a violation has the tightest relevant directive + local directives = maybeParseDirectives(node, directiveCache) + violationSuppressed = if directives[violation.lintname] ~= nil + then directives[violation.lintname] + else violationSuppressed + -- No other fully contained nodes can affect whether we suppress, so we can stop recursing return false end @@ -156,21 +160,20 @@ local function violationIsSuppressed( return false end - local rules = maybeParseSuppressions(node, suppressionCache) + local directives = maybeParseDirectives(node, directiveCache) -- AstStatBlock suppressions should only apply to the first statement, not the block itself if node.kind == "stat" and node.tag == "block" then return true end - if rules[violation.lintname] then - violationSuppressed = true - -- If the node is suppressed, we don't need to recurse deeper - return false - else - -- A deeper node might suppress this rule - return true - end + -- Use any directives found on this node to update suppression status + violationSuppressed = if directives[violation.lintname] ~= nil + then directives[violation.lintname] + else violationSuppressed + + -- A deeper node might affect whether we suppress, so continue recursing + return true end local suppressionVisitor = visitor.create(nodeIsSuppressed) @@ -195,13 +198,13 @@ local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule print("Applying lint rules") end local violations: { types.LintViolation } = {} - local suppressionCache = {} + local directiveCache = {} for _, rule in lintRules do local success, err = pcall(rule.lint, ast, inputFilePath) if success then -- On success, err contains the returned violations for _, violation in err do - if not violationIsSuppressed(violation, ast, suppressionCache) then + if not violationIsSuppressed(violation, ast, directiveCache) then table.insert(violations, violation) end end diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 359a8a79e..7e1bce0f9 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -969,6 +969,52 @@ violator2.lua:1:11-17 ── -- Teardown fs.removedirectory(modulePath, { recursive = true }) end) + + suite:case("lute lint report directive", function(assert) + -- Create a file that violates the default almost_swapped and divide_by_zero rules + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 +-- lute-lint-ignore(divide_by_zero) +if true then + local z = 3 / 0 + -- lute-lint-report(divide_by_zero) + local y = 10 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) + + local expected = [[ +violator.luau:1:11-16 ── + │ + 1 │ local x = 1 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:6:12-18 ── + │ + 6 │ local y = 10 / 0 + │ ^^^^^^ + │ +]] + + -- Teardown + fs.remove(violatorPath) + end) end) test.run() From c76f1af9e2d82457643a4f4f02637a1d4a9b047a Mon Sep 17 00:00:00 2001 From: othomson-roblox <64273528+othomson-roblox@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:17:36 -0800 Subject: [PATCH 217/642] Support negative numbers in the json std lib (#649) The `json` std lib does not support negative numbers currently. --- lute/std/libs/json.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 7396a1762..63a6795ff 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -453,7 +453,7 @@ deserialize = function(state: DeserializerState): value elseif string.sub(state.src, state.cursor, state.cursor + 4) == "false" then state.cursor += 5 return false - elseif string.match(state.src, "^[%d%.]", state.cursor) then + elseif string.match(state.src, "^[%-%d%.]", state.cursor) then -- number return deserializeNumber(state) elseif string.byte(state.src, state.cursor) == string.byte('"') then From e14bfb80f3b62c58eb556ab23e1eeb0b2cd6f2f6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:45:19 +0000 Subject: [PATCH 218/642] Update Lute to 0.1.0-nightly.20251204 (#646) **Lute**: Updated from `0.1.0-nightly.20251127` to `0.1.0-nightly.20251204` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20251204 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: skberkeley <66887530+skberkeley@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 7de88a7f4..6497c4b61 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251127" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251204" } diff --git a/rokit.toml b/rokit.toml index a9c0c07e9..b8ece9ebe 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20251127" +lute = "luau-lang/lute@0.1.0-nightly.20251204" From 32045b7eab23a6604b3a5668fba236e478883d28 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:48:25 -0800 Subject: [PATCH 219/642] Lute.Require: delete all "fake root" logic and replace with new `to_alias_fallback` Luau.Require API (#647) ### Problem The "fake root" hack we use in the `require` virtual filesystem needs to be removed. 1. It's inefficient: it requires us to repeatedly copy an injected `.luaurc` around to require built-in libraries like `@std`. 2. It's difficult to reason about: forces our virtual filesystems to diverge from the structures they're meant to represent. 3. It's semantically incorrect: - The presence of a fake root node allows users to `require` "up" (using `../`) into fake filesystem nodes. - The existing hack also forces us to inject `.luaurc` files that don't conform to the official spec for `.luaurc` files (e.g. mapping `@std` to `$std` isn't permitted by the spec since aliases must map to paths, but the implementation allows this to avoid being opinionated on what a "valid path" could look like in an abstract context). ### Solution In [Luau release 0.701](https://github.com/luau-lang/luau/releases/tag/0.701), we introduced a new `to_alias_fallback` API to natively support host-defined aliases, such as Lute's `@std` alias. This PR migrates Lute's require implementations to this new API. This includes `Package::RequireVfs`, which can now automatically redirect aliases to pre-declared dependencies when executing code in the package-aware virtual filesystem. --- lute/require/include/lute/packagerequirevfs.h | 3 +- lute/require/include/lute/require.h | 1 + lute/require/include/lute/requirevfs.h | 3 +- lute/require/include/lute/userlandvfs.h | 36 ++++-- lute/require/src/clivfs.cpp | 3 + lute/require/src/packagerequirevfs.cpp | 64 +++------- lute/require/src/require.cpp | 7 ++ lute/require/src/requirevfs.cpp | 60 +++------ lute/require/src/userlandvfs.cpp | 119 ++++++------------ 9 files changed, 110 insertions(+), 186 deletions(-) diff --git a/lute/require/include/lute/packagerequirevfs.h b/lute/require/include/lute/packagerequirevfs.h index f67645090..959ce8e24 100644 --- a/lute/require/include/lute/packagerequirevfs.h +++ b/lute/require/include/lute/packagerequirevfs.h @@ -16,6 +16,7 @@ class RequireVfs : public IRequireVfs NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) override; NavigationStatus jumpToAlias(lua_State* L, std::string_view path) override; + NavigationStatus toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) override; NavigationStatus toParent(lua_State* L) override; NavigationStatus toChild(lua_State* L, std::string_view name) override; @@ -48,8 +49,6 @@ class RequireVfs : public IRequireVfs Package::UserlandVfs userlandVfs; StdLibVfs stdLibVfs; std::string lutePath; - - bool atFakeRoot = false; }; } // namespace Package diff --git a/lute/require/include/lute/require.h b/lute/require/include/lute/require.h index 99bfcac52..755041969 100644 --- a/lute/require/include/lute/require.h +++ b/lute/require/include/lute/require.h @@ -18,6 +18,7 @@ class IRequireVfs virtual NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) = 0; virtual NavigationStatus jumpToAlias(lua_State* L, std::string_view path) = 0; + virtual NavigationStatus toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) = 0; virtual NavigationStatus toParent(lua_State* L) = 0; virtual NavigationStatus toChild(lua_State* L, std::string_view name) = 0; diff --git a/lute/require/include/lute/requirevfs.h b/lute/require/include/lute/requirevfs.h index 552e47802..88afd763e 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -23,6 +23,7 @@ class RequireVfs : public IRequireVfs NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) override; NavigationStatus jumpToAlias(lua_State* L, std::string_view path) override; + NavigationStatus toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) override; NavigationStatus toParent(lua_State* L) override; NavigationStatus toChild(lua_State* L, std::string_view name) override; @@ -59,6 +60,4 @@ class RequireVfs : public IRequireVfs std::optional cliVfs = std::nullopt; std::optional bundleVfs = std::nullopt; std::string lutePath; - - bool atFakeRoot = false; }; diff --git a/lute/require/include/lute/userlandvfs.h b/lute/require/include/lute/userlandvfs.h index d9f173dcf..264ddbe1a 100644 --- a/lute/require/include/lute/userlandvfs.h +++ b/lute/require/include/lute/userlandvfs.h @@ -3,7 +3,9 @@ #include "lute/filevfs.h" #include "lute/modulepath.h" -#include +#include "Luau/DenseHash.h" +#include "Luau/StringUtils.h" + #include #include #include @@ -17,9 +19,17 @@ struct Identifier std::string name; std::string version; - bool operator<(const Identifier& other) const + bool operator==(const Identifier& other) const + { + return std::tie(name, version) == std::tie(other.name, other.version); + } +}; + +struct IdentifierHashDefault +{ + size_t operator()(const Identifier& id) const { - return std::tie(name, version) < std::tie(other.name, other.version); + return Luau::detail::DenseHashDefault()(Luau::format("%s:%s", id.name.c_str(), id.version.c_str())); } }; @@ -44,13 +54,13 @@ class Subtree bool isModulePresent() const; std::string getCurrentPath() const; + Info getInfo() const; private: - Subtree(ModulePath currentModulePath, std::string generatedRootLuaurc); + Subtree(ModulePath currentModulePath, Info info); ModulePath currentModulePath; - std::string generatedRootLuaurc; - bool atGeneratedRoot = false; + Info info; }; class UserlandVfs @@ -59,7 +69,7 @@ class UserlandVfs static UserlandVfs create(std::vector directDependencies, std::vector> allDependencies); NavigationStatus resetToPath(const std::string& path); - NavigationStatus jumpToDependencySubtree(const std::string& identifierStringified); + NavigationStatus toAliasFallback(std::string_view aliasUnprefixed); NavigationStatus toParent(); NavigationStatus toChild(const std::string& name); @@ -73,8 +83,11 @@ class UserlandVfs std::string getCurrentPath() const; private: - UserlandVfs(std::map allDependencies, std::string generatedRootLuaurc); - NavigationStatus jumpToDependencySubtreeImpl(Identifier identifier); + using DependencyMap = Luau::DenseHashMap; + + UserlandVfs(std::vector directDependencies, DependencyMap allDependencies); + + NavigationStatus jumpToDependencySubtree(const Identifier& dependency); enum class VFSType { @@ -86,10 +99,9 @@ class UserlandVfs FileVfs fileVfs; std::optional currentSubtree = std::nullopt; - std::map allDependencies; - bool atDiskFakeRoot = false; - std::string generatedRootLuaurc; + std::vector directDependencies; + DependencyMap allDependencies; }; } // namespace Package diff --git a/lute/require/src/clivfs.cpp b/lute/require/src/clivfs.cpp index 314b8e838..ce3c7f6b7 100644 --- a/lute/require/src/clivfs.cpp +++ b/lute/require/src/clivfs.cpp @@ -23,6 +23,9 @@ static std::optional readCliModule(const std::string& path) static bool isCliDirectory(const std::string& path) { + if (path == "@cli") + return true; + CliModuleResult result = getCliModule(path); return result.type == CliModuleType::Directory; } diff --git a/lute/require/src/packagerequirevfs.cpp b/lute/require/src/packagerequirevfs.cpp index 245eb90a6..49dbebd78 100644 --- a/lute/require/src/packagerequirevfs.cpp +++ b/lute/require/src/packagerequirevfs.cpp @@ -26,8 +26,6 @@ bool RequireVfs::isRequireAllowed(lua_State* L, std::string_view requirerChunkna NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkname) { - atFakeRoot = false; - if ((requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@std/")) { vfsType = VFSType::Std; @@ -49,25 +47,6 @@ NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkn NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) { - if (path == "$std") - { - atFakeRoot = false; - vfsType = VFSType::Std; - return stdLibVfs.resetToPath("@std"); - } - else if (path == "$lute") - { - vfsType = VFSType::Lute; - lutePath = "@lute"; - return NavigationStatus::Success; - } - else if (!path.empty() && path[0] == '$') - { - // "$name:version" is interpreted as an identifier. - vfsType = VFSType::Userland; - return userlandVfs.jumpToDependencySubtree(std::string(path)); - } - NavigationStatus status = NavigationStatus::NotFound; switch (vfsType) { @@ -83,6 +62,24 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) return status; } +NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) +{ + if (aliasUnprefixed == "std") + { + vfsType = VFSType::Std; + return stdLibVfs.resetToPath("@std"); + } + else if (aliasUnprefixed == "lute") + { + vfsType = VFSType::Lute; + lutePath = "@lute"; + return NavigationStatus::Success; + } + + vfsType = VFSType::Userland; + return userlandVfs.toAliasFallback(aliasUnprefixed); +} + NavigationStatus RequireVfs::toParent(lua_State* L) { NavigationStatus status = NavigationStatus::NotFound; @@ -98,22 +95,11 @@ NavigationStatus RequireVfs::toParent(lua_State* L) luaL_error(L, "cannot get the parent of @lute"); } - if (status == NavigationStatus::NotFound) - { - if (atFakeRoot) - return NavigationStatus::NotFound; - - atFakeRoot = true; - return NavigationStatus::Success; - } - return status; } NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) { - atFakeRoot = false; - switch (vfsType) { case VFSType::Userland: @@ -212,9 +198,6 @@ std::string RequireVfs::getCacheKey(lua_State* L) const ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const { - if (atFakeRoot) - return ConfigStatus::PresentJson; - ConfigStatus status = ConfigStatus::Ambiguous; switch (vfsType) { @@ -232,17 +215,6 @@ ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const std::string RequireVfs::getConfig(lua_State* L) const { - if (atFakeRoot) - { - std::string globalConfig = "{\n" - " \"aliases\": {\n" - " \"std\": \"$std\",\n" - " \"lute\": \"$lute\",\n" - " }\n" - "}\n"; - return globalConfig; - } - std::optional configContents; switch (vfsType) { diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index 9706edeb0..40267c14a 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -87,6 +87,12 @@ static luarequire_NavigateResult jump_to_alias(lua_State* L, void* ctx, const ch return convert(reqCtx->vfs->jumpToAlias(L, path)); } +static luarequire_NavigateResult to_alias_fallback(lua_State* L, void* ctx, const char* alias_unprefixed) +{ + RequireCtx* reqCtx = static_cast(ctx); + return convert(reqCtx->vfs->toAliasFallback(L, alias_unprefixed)); +} + static luarequire_NavigateResult to_parent(lua_State* L, void* ctx) { RequireCtx* reqCtx = static_cast(ctx); @@ -205,6 +211,7 @@ void requireConfigInit(luarequire_Configuration* config) config->is_require_allowed = is_require_allowed; config->reset = reset; config->jump_to_alias = jump_to_alias; + config->to_alias_fallback = to_alias_fallback; config->to_parent = to_parent; config->to_child = to_child; config->is_module_present = is_module_present; diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 1a17039d5..16ffdccfd 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -30,8 +30,6 @@ bool RequireVfs::isRequireAllowed(lua_State* L, std::string_view requirerChunkna NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkname) { - atFakeRoot = false; - if ((requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@std/")) { vfsType = VFSType::Std; @@ -64,19 +62,6 @@ NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkn NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) { - if (path == "$std") - { - atFakeRoot = false; - vfsType = VFSType::Std; - return stdLibVfs.resetToPath("@std"); - } - else if (path == "$lute") - { - vfsType = VFSType::Lute; - lutePath = "@lute"; - return NavigationStatus::Success; - } - NavigationStatus status = NavigationStatus::NotFound; switch (vfsType) { @@ -100,10 +85,26 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) return status; } +NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) +{ + if (aliasUnprefixed == "std") + { + vfsType = VFSType::Std; + return stdLibVfs.resetToPath("@std"); + } + else if (aliasUnprefixed == "lute") + { + vfsType = VFSType::Lute; + lutePath = "@lute"; + return NavigationStatus::Success; + } + + return NavigationStatus::NotFound; +} + NavigationStatus RequireVfs::toParent(lua_State* L) { NavigationStatus status = NavigationStatus::NotFound; - switch (vfsType) { case VFSType::Disk: @@ -124,23 +125,11 @@ NavigationStatus RequireVfs::toParent(lua_State* L) luaL_error(L, "cannot get the parent of @lute"); break; } - - if (status == NavigationStatus::NotFound) - { - if (atFakeRoot) - return NavigationStatus::NotFound; - - atFakeRoot = true; - return NavigationStatus::Success; - } - return status; } NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) { - atFakeRoot = false; - switch (vfsType) { case VFSType::Disk: @@ -157,7 +146,6 @@ NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) luaL_error(L, "'%s' is not a lute library", std::string(name).c_str()); break; } - return NavigationStatus::NotFound; } @@ -287,9 +275,6 @@ std::string RequireVfs::getCacheKey(lua_State* L) const ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const { - if (atFakeRoot) - return ConfigStatus::PresentJson; - ConfigStatus status = ConfigStatus::Absent; switch (vfsType) { @@ -315,17 +300,6 @@ ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const std::string RequireVfs::getConfig(lua_State* L) const { - if (atFakeRoot) - { - std::string globalConfig = "{\n" - " \"aliases\": {\n" - " \"std\": \"$std\",\n" - " \"lute\": \"$lute\",\n" - " }\n" - "}\n"; - return globalConfig; - } - std::optional configContents; switch (vfsType) { diff --git a/lute/require/src/userlandvfs.cpp b/lute/require/src/userlandvfs.cpp index 65e921bc8..c857aa9ea 100644 --- a/lute/require/src/userlandvfs.cpp +++ b/lute/require/src/userlandvfs.cpp @@ -13,21 +13,10 @@ namespace Package { -static std::string generateRootLuaurc(const std::vector& dependencies) -{ - std::string rootLuaurc = "{\n \"aliases\": {\n"; - for (const Identifier& dep : dependencies) - { - rootLuaurc += " \"" + dep.name + "\": \"$" + dep.name + ":" + dep.version + "\",\n"; - } - rootLuaurc += " }\n}\n"; - return rootLuaurc; -} - std::optional Subtree::create(Info info) { std::string_view entryFile = info.entryFile; - if (entryFile.find(info.rootDirectory) != 0) + if (entryFile.rfind(info.rootDirectory, 0) != 0) return std::nullopt; if (entryFile.size() <= info.rootDirectory.size()) @@ -42,41 +31,27 @@ std::optional Subtree::create(Info info) if (!currentModulePath) return std::nullopt; - return Subtree{std::move(*currentModulePath), generateRootLuaurc(info.dependencies)}; + return Subtree{std::move(*currentModulePath), std::move(info)}; } -Subtree::Subtree(ModulePath currentModulePath, std::string generatedRootLuaurc) +Subtree::Subtree(ModulePath currentModulePath, Info info) : currentModulePath(std::move(currentModulePath)) - , generatedRootLuaurc(std::move(generatedRootLuaurc)) + , info(std::move(info)) { } NavigationStatus Subtree::toParent() { - NavigationStatus status = currentModulePath.toParent(); - if (status == NavigationStatus::NotFound) - { - if (atGeneratedRoot) - return NavigationStatus::NotFound; - - atGeneratedRoot = true; - return NavigationStatus::Success; - } - - return status; + return currentModulePath.toParent(); } NavigationStatus Subtree::toChild(const std::string& name) { - atGeneratedRoot = false; return currentModulePath.toChild(name); } ConfigStatus Subtree::getConfigStatus() const { - if (atGeneratedRoot) - return ConfigStatus::PresentJson; - bool luaurcExists = isFile(currentModulePath.getPotentialConfigPath(Luau::kConfigName)); bool luauConfigExists = isFile(currentModulePath.getPotentialConfigPath(Luau::kLuauConfigName)); @@ -92,9 +67,6 @@ ConfigStatus Subtree::getConfigStatus() const std::optional Subtree::getConfig() const { - if (atGeneratedRoot) - return generatedRootLuaurc; - ConfigStatus status = getConfigStatus(); LUAU_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); @@ -108,9 +80,6 @@ std::optional Subtree::getConfig() const bool Subtree::isModulePresent() const { - if (atGeneratedRoot) - return false; - ResolvedRealPath result = currentModulePath.getRealPath(); if (result.status != NavigationStatus::Success) return false; @@ -127,20 +96,25 @@ std::string Subtree::getCurrentPath() const return result.realPath; } +Info Subtree::getInfo() const +{ + return info; +} + UserlandVfs UserlandVfs::create(std::vector directDependencies, std::vector> allDependencies) { - std::map allDependenciesMap; + DependencyMap allDependenciesMap{{}}; for (auto& [identifier, info] : allDependencies) { allDependenciesMap[std::move(identifier)] = std::move(info); } - return UserlandVfs{std::move(allDependenciesMap), generateRootLuaurc(directDependencies)}; + return UserlandVfs{std::move(directDependencies), std::move(allDependenciesMap)}; } -UserlandVfs::UserlandVfs(std::map allDependencies, std::string generatedRootLuaurc) - : allDependencies(std::move(allDependencies)) - , generatedRootLuaurc(std::move(generatedRootLuaurc)) +UserlandVfs::UserlandVfs(std::vector directDependencies, DependencyMap allDependencies) + : directDependencies(std::move(directDependencies)) + , allDependencies(std::move(allDependencies)) { } @@ -148,47 +122,50 @@ NavigationStatus UserlandVfs::resetToPath(const std::string& path) { for (const auto& [identifier, info] : allDependencies) { - if (path.find(info.rootDirectory) == 0) - { - return jumpToDependencySubtreeImpl(identifier); - } + if (path.rfind(info.rootDirectory, 0) == 0) + return jumpToDependencySubtree(identifier); } currentSubtree = std::nullopt; vfsType = VFSType::Disk; - atDiskFakeRoot = false; return fileVfs.resetToPath(path); } -NavigationStatus UserlandVfs::jumpToDependencySubtree(const std::string& identifierStringified) +NavigationStatus UserlandVfs::toAliasFallback(std::string_view aliasUnprefixed) { - if (identifierStringified.empty() || identifierStringified[0] != '$') - return NavigationStatus::NotFound; - - Identifier identifier; - size_t colonPos = identifierStringified.find(':'); - if (colonPos == std::string::npos) - return NavigationStatus::NotFound; + std::vector availableDependencies; + switch (vfsType) + { + case VFSType::Disk: + availableDependencies = directDependencies; + break; + case VFSType::Subtree: + LUAU_ASSERT(currentSubtree); + availableDependencies = currentSubtree->getInfo().dependencies; + } - identifier.name = identifierStringified.substr(1, colonPos - 1); - identifier.version = identifierStringified.substr(colonPos + 1); + for (const Identifier& identifier : availableDependencies) + { + if (identifier.name == aliasUnprefixed) + return jumpToDependencySubtree(identifier); + } - return jumpToDependencySubtreeImpl(identifier); + return NavigationStatus::NotFound; } -NavigationStatus UserlandVfs::jumpToDependencySubtreeImpl(Identifier identifier) +NavigationStatus UserlandVfs::jumpToDependencySubtree(const Identifier& dependency) { - if (allDependencies.find(identifier) == allDependencies.end()) + Info* info = allDependencies.find(dependency); + if (!info) return NavigationStatus::NotFound; - std::optional st = Subtree::create(allDependencies.at(identifier)); + std::optional st = Subtree::create(*info); if (!st) return NavigationStatus::NotFound; currentSubtree = std::move(*st); vfsType = VFSType::Subtree; - atDiskFakeRoot = false; return NavigationStatus::Success; } @@ -207,22 +184,11 @@ NavigationStatus UserlandVfs::toParent() break; } - if (status == NavigationStatus::NotFound) - { - if (atDiskFakeRoot) - return NavigationStatus::NotFound; - - atDiskFakeRoot = true; - return NavigationStatus::Success; - } - return status; } NavigationStatus UserlandVfs::toChild(const std::string& name) { - atDiskFakeRoot = false; - NavigationStatus status = NavigationStatus::NotFound; switch (vfsType) { @@ -239,9 +205,6 @@ NavigationStatus UserlandVfs::toChild(const std::string& name) ConfigStatus UserlandVfs::getConfigStatus() const { - if (atDiskFakeRoot) - return ConfigStatus::PresentJson; - ConfigStatus status = ConfigStatus::Ambiguous; switch (vfsType) { @@ -258,9 +221,6 @@ ConfigStatus UserlandVfs::getConfigStatus() const std::optional UserlandVfs::getConfig() const { - if (atDiskFakeRoot) - return generatedRootLuaurc; - std::optional config; switch (vfsType) { @@ -277,9 +237,6 @@ std::optional UserlandVfs::getConfig() const bool UserlandVfs::isModulePresent() const { - if (atDiskFakeRoot) - return false; - bool isPresent = false; switch (vfsType) { From ccf62ed4d722551008d2c0acb7121b06808562a6 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 5 Dec 2025 16:30:30 -0800 Subject: [PATCH 220/642] tidy: Split the tests in `test.test.luau` into two different test suites (#656) These tests currently mix the tests associated with the cli command `lute test` and the tests associated with the standard libraries test framework. This PR separates these out since that file is becoming a bit hairy to debug. --- tests/cli/test.test.luau | 330 +------------------------------------- tests/std/test.test.luau | 338 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+), 329 deletions(-) create mode 100644 tests/std/test.test.luau diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 452e316d8..98d5dfefb 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -1,12 +1,9 @@ -local assertions = require("@std/test/assert") -local fs = require("@std/fs") local path = require("@std/path") local process = require("@std/process") local system = require("@std/system") local test = require("@std/test") local lutePath = path.format(process.execpath()) -local tmpDir = system.tmpdir() local expected = [[Anonymous: Passing @@ -17,24 +14,7 @@ SmokeSuite: make_pass ]] -local function assertErrorsWithMsg( - testName: string, - testContents: string, - expectedErrMsg: string, - assert: assertions.asserts -) - local testFilePath = path.join(tmpDir, `{testName}.test.luau`) - fs.writestringtofile(testFilePath, testContents) - - -- Run lute on the created test file - local result = process.run({ lutePath, tostring(testFilePath) }) - - -- Check that the output specifies the inequal values - assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, expectedErrMsg) -end - -test.suite("lute test", function(suite) +test.suite("LuteTestCommand", function(suite) suite:case("lute test help message", function(assert) -- Run lute test with -h flag local result = process.run({ lutePath, "test", "-h" }) @@ -60,314 +40,6 @@ test.suite("lute test", function(suite) assert.eq(result.exitcode, 0) assert.eq(expected, result.stdout) end) - - suite:case("lute doesn't report xpcall as error when accessing field of nil in suite", function(assert) - -- Setup - local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") - fs.writestringtofile( - testFilePath, - [[ -local test = require("@std/test") - -test.suite("nil_field_suite", function(suite) - suite:case("access_field_of_nil", function(assert) - local t = nil - local value = t.field -- This will cause an error - end) -end) - -test.run() - ]] - ) - - -- Run lute on the created test file - local result = process.run({ lutePath, path.format(testFilePath) }) - -- Check that the output doesn't include xpcall - assert.eq(result.exitcode, 1) - assert.eq(result.stdout:find("xpcall", 1, true), nil) - -- Check that the error points to filepath + correct line number - assert.strcontains(result.stdout, `Failed with: Runtime error in nil_field_suite.access_field_of_nil in:`) - assert.strcontains( - result.stdout, - `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:6` - ) - - fs.remove(testFilePath) - end) - - suite:case("lute doesn't report xpcall as error when accessing field of nil in case", function(assert) - -- Setup - local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") - fs.writestringtofile( - testFilePath, - [[ -local test = require("@std/test") - -test.case("access_field_of_nil", function(assert) - local t = nil - local value = t.field -- This will cause an error -end) - -test.run() - ]] - ) - - -- Run lute on the created test file - local result = process.run({ lutePath, path.format(testFilePath) }) - - -- Check that the output doesn't include xpcall - assert.eq(result.exitcode, 1) - assert.eq(result.stdout:find("xpcall", 1, true), nil) - -- Check that the error points to filepath + correct line number - assert.strcontains(result.stdout, `Failed with: Runtime error in access_field_of_nil in:`) - assert.strcontains( - result.stdout, - `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:5` - ) - - fs.remove(testFilePath) - end) - - suite:case("assert.eq_error_message_in_case", function(assert) - assertErrorsWithMsg( - "assert_eq_fails", - [[ -local test = require("@std/test") - -test.case("eq_error", function(assert) - assert.eq(1, 2) -end) - -test.run() - ]], - "eq: 1 ~= 2", - assert - ) - end) - - suite:case("assert.eq_error_message_in_suite", function(assert) - assertErrorsWithMsg( - "assert_eq_fails", - [[ -local test = require("@std/test") - -test.suite("eq_failure_suite", function(suite) - suite:case("eq_error", function(assert) - assert.eq(1, 2) - end) -end) - -test.run() - ]], - "eq: 1 ~= 2", - assert - ) - end) - - suite:case("assert.neq_error_message_in_case", function(assert) - assertErrorsWithMsg( - "assert_neq_fails", - [[ -local test = require("@std/test") - -test.case("neq_error", function(assert) - assert.neq(1, 1) -end) - -test.run() - ]], - "neq: 1 == 1", - assert - ) - end) - - suite:case("assert.neq_error_message_in_suite", function(assert) - assertErrorsWithMsg( - "assert_neq_fails", - [[ -local test = require("@std/test") - -test.suite("neq_failure_suite", function(suite) - suite:case("neq_error", function(assert) - assert.neq(1, 1) - end) -end) - -test.run() - ]], - "neq: 1 == 1", - assert - ) - end) - - suite:case("assert.tableeq_error_message_in_case", function(assert) - assertErrorsWithMsg( - "assert_tableeq_fails", - [[ -local test = require("@std/test") - -test.case("tableeq_error", function(assert) - assert.tableeq({ a = 1 }, { a = 2 }) -end) - -test.run() - ]], - "tableeq: lhs[a] = 1 but rhs[a] = 2", - assert - ) - end) - - suite:case("assert.tableeq_error_message_in_suite", function(assert) - assertErrorsWithMsg( - "assert_tableeq_fails", - [[ -local test = require("@std/test") - -test.suite("tableeq_failure_suite", function(suite) - suite:case("tableeq_error", function(assert) - assert.tableeq({ a = 1 }, { a = 2 }) - end) -end) - -test.run() - ]], - "tableeq: lhs[a] = 1 but rhs[a] = 2", - assert - ) - end) - - suite:case("assert.buffereq_unequal_length", function(assert) - assertErrorsWithMsg( - "assert_buffereq_fails", - [[ -local test = require("@std/test") - -test.case("buffereq_error", function(assert) - local buf1 = buffer.create(1) - local buf2 = buffer.create(2) - assert.buffereq(buf1, buf2) -end) - -test.run() - ]], - "buffers are of unequal length", - assert - ) - end) - - suite:case("assert.buffereq_error_message_in_suite", function(assert) - assertErrorsWithMsg( - "assert_buffereq_fails", - [[ -local test = require("@std/test") - -test.suite("buffereq_failure_suite", function(suite) - suite:case("buffereq_error", function(assert) - local buf1 = buffer.create(2) - local buf2 = buffer.create(2) - buffer.writeu8(buf1, 0, 84) - buffer.writeu8(buf2, 0, 85) - assert.buffereq(buf1, buf2) - end) -end) - -test.run() - ]], - "buffereq: lhs[0] = 54, but rhs[0] = 55", - assert - ) - end) - - suite:case("assert.erroreq_no_error", function(assert) - assertErrorsWithMsg( - "assert_buffereq_fails", - [[ -local test = require("@std/test") - -test.case("erroreq_error", function(assert) - assert.erroreq(function() return end, "expected message") -end) - -test.run() - ]], - "Function did not error as expected.", - assert - ) - end) - - suite:case("assert.erroreq_error_message", function(assert) - assertErrorsWithMsg( - "assert_buffereq_fails", - [[ -local test = require("@std/test") - -test.suite("erroreq_failure_suite", function(suite) - suite:case("erroreq_error", function(assert) - assert.erroreq(function() error("wrong message") end, "expected message") - end) -end) - -test.run() - ]], - 'Expected suffix of error message "expected message", but got ', -- the full message contains tmpdir path to test file - assert - ) - end) - - suite:case("assert.strcontains_instances_negative", function(assert) - assertErrorsWithMsg( - "assert_buffereq_fails", - [[ -local test = require("@std/test") - -test.case("strcontains_error", function(assert) - assert.strcontains("hello world", "goodbye", nil, -1) -end) - -test.run() - ]], - "instances must be greater than 0", - assert - ) - end) - - suite:case("assert.strcontains_single_instance_fails", function(assert) - assertErrorsWithMsg( - "assert_buffereq_fails", - [[ -local test = require("@std/test") - -test.suite("strcontains_failure_suite", function(suite) - suite:case("strcontains_error", function(assert) - assert.strcontains("hello world", "goodbye") - end) -end) - -test.run() - ]], - 'Expected "hello world" to contain "goodbye".', - assert - ) - end) - - suite:case("assert.strcontains_multiple_instance_fails", function(assert) - assertErrorsWithMsg( - "assert_buffereq_fails", - [[ -local test = require("@std/test") - -test.suite("strcontains_failure_suite", function(suite) - suite:case("strcontains_error", function(assert) - assert.strcontains("muahahahaha", "ha", nil, 5) - end) -end) - -test.run() - ]], - 'Expected "muahahahaha" to contain "ha" 5 times.', - assert - ) - end) end) test.run() diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau new file mode 100644 index 000000000..6c00a5179 --- /dev/null +++ b/tests/std/test.test.luau @@ -0,0 +1,338 @@ +local assertions = require("@std/test/assert") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local lutePath = path.format(process.execpath()) +local tmpDir = system.tmpdir() + +local function assertErrorsWithMsg( + testName: string, + testContents: string, + expectedErrMsg: string, + assert: assertions.asserts +) + local testFilePath = path.join(tmpDir, `{testName}.test.luau`) + fs.writestringtofile(testFilePath, testContents) + + -- Run lute on the created test file + local result = process.run({ lutePath, tostring(testFilePath) }) + + -- Check that the output specifies the inequal values + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, expectedErrMsg) +end + +test.suite("LuteStdTestFramework", function(suite) + suite:case("lute doesn't report xpcall as error when accessing field of nil in suite", function(assert) + -- Setup + local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") + fs.writestringtofile( + testFilePath, + [[ +local test = require("@std/test") + +test.suite("nil_field_suite", function(suite) + suite:case("access_field_of_nil", function(assert) + local t = nil + local value = t.field -- This will cause an error + end) +end) + +test.run() + ]] + ) + + -- Run lute on the created test file + local result = process.run({ lutePath, path.format(testFilePath) }) + -- Check that the output doesn't include xpcall + assert.eq(result.exitcode, 1) + assert.eq(result.stdout:find("xpcall", 1, true), nil) + -- Check that the error points to filepath + correct line number + assert.strcontains(result.stdout, `Failed with: Runtime error in nil_field_suite.access_field_of_nil in:`) + assert.strcontains( + result.stdout, + `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:6` + ) + + fs.remove(testFilePath) + end) + + suite:case("lute doesn't report xpcall as error when accessing field of nil in case", function(assert) + -- Setup + local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") + fs.writestringtofile( + testFilePath, + [[ +local test = require("@std/test") + +test.case("access_field_of_nil", function(assert) + local t = nil + local value = t.field -- This will cause an error +end) + +test.run() + ]] + ) + + -- Run lute on the created test file + local result = process.run({ lutePath, path.format(testFilePath) }) + + -- Check that the output doesn't include xpcall + assert.eq(result.exitcode, 1) + assert.eq(result.stdout:find("xpcall", 1, true), nil) + -- Check that the error points to filepath + correct line number + assert.strcontains(result.stdout, `Failed with: Runtime error in access_field_of_nil in:`) + assert.strcontains( + result.stdout, + `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:5` + ) + + fs.remove(testFilePath) + end) + + suite:case("assert.eq_error_message_in_case", function(assert) + assertErrorsWithMsg( + "assert_eq_fails", + [[ +local test = require("@std/test") + +test.case("eq_error", function(assert) + assert.eq(1, 2) +end) + +test.run() + ]], + "eq: 1 ~= 2", + assert + ) + end) + + suite:case("assert.eq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_eq_fails", + [[ +local test = require("@std/test") + +test.suite("eq_failure_suite", function(suite) + suite:case("eq_error", function(assert) + assert.eq(1, 2) + end) +end) + +test.run() + ]], + "eq: 1 ~= 2", + assert + ) + end) + + suite:case("assert.neq_error_message_in_case", function(assert) + assertErrorsWithMsg( + "assert_neq_fails", + [[ +local test = require("@std/test") + +test.case("neq_error", function(assert) + assert.neq(1, 1) +end) + +test.run() + ]], + "neq: 1 == 1", + assert + ) + end) + + suite:case("assert.neq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_neq_fails", + [[ +local test = require("@std/test") + +test.suite("neq_failure_suite", function(suite) + suite:case("neq_error", function(assert) + assert.neq(1, 1) + end) +end) + +test.run() + ]], + "neq: 1 == 1", + assert + ) + end) + + suite:case("assert.tableeq_error_message_in_case", function(assert) + assertErrorsWithMsg( + "assert_tableeq_fails", + [[ +local test = require("@std/test") + +test.case("tableeq_error", function(assert) + assert.tableeq({ a = 1 }, { a = 2 }) +end) + +test.run() + ]], + "tableeq: lhs[a] = 1 but rhs[a] = 2", + assert + ) + end) + + suite:case("assert.tableeq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_tableeq_fails", + [[ +local test = require("@std/test") + +test.suite("tableeq_failure_suite", function(suite) + suite:case("tableeq_error", function(assert) + assert.tableeq({ a = 1 }, { a = 2 }) + end) +end) + +test.run() + ]], + "tableeq: lhs[a] = 1 but rhs[a] = 2", + assert + ) + end) + + suite:case("assert.buffereq_unequal_length", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.case("buffereq_error", function(assert) + local buf1 = buffer.create(1) + local buf2 = buffer.create(2) + assert.buffereq(buf1, buf2) +end) + +test.run() + ]], + "buffers are of unequal length", + assert + ) + end) + + suite:case("assert.buffereq_error_message_in_suite", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("buffereq_failure_suite", function(suite) + suite:case("buffereq_error", function(assert) + local buf1 = buffer.create(2) + local buf2 = buffer.create(2) + buffer.writeu8(buf1, 0, 84) + buffer.writeu8(buf2, 0, 85) + assert.buffereq(buf1, buf2) + end) +end) + +test.run() + ]], + "buffereq: lhs[0] = 54, but rhs[0] = 55", + assert + ) + end) + + suite:case("assert.erroreq_no_error", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.case("erroreq_error", function(assert) + assert.erroreq(function() return end, "expected message") +end) + +test.run() + ]], + "Function did not error as expected.", + assert + ) + end) + + suite:case("assert.erroreq_error_message", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("erroreq_failure_suite", function(suite) + suite:case("erroreq_error", function(assert) + assert.erroreq(function() error("wrong message") end, "expected message") + end) +end) + +test.run() + ]], + 'Expected suffix of error message "expected message", but got ', -- the full message contains tmpdir path to test file + assert + ) + end) + + suite:case("assert.strcontains_instances_negative", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.case("strcontains_error", function(assert) + assert.strcontains("hello world", "goodbye", nil, -1) +end) + +test.run() + ]], + "instances must be greater than 0", + assert + ) + end) + + suite:case("assert.strcontains_single_instance_fails", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("strcontains_failure_suite", function(suite) + suite:case("strcontains_error", function(assert) + assert.strcontains("hello world", "goodbye") + end) +end) + +test.run() + ]], + 'Expected "hello world" to contain "goodbye".', + assert + ) + end) + + suite:case("assert.strcontains_multiple_instance_fails", function(assert) + assertErrorsWithMsg( + "assert_buffereq_fails", + [[ +local test = require("@std/test") + +test.suite("strcontains_failure_suite", function(suite) + suite:case("strcontains_error", function(assert) + assert.strcontains("muahahahaha", "ha", nil, 5) + end) +end) + +test.run() + ]], + 'Expected "muahahahaha" to contain "ha" 5 times.', + assert + ) + end) +end) + +test.run() From 365a86d91b62e5d2b666ca37e87b7ab766f4db5e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:44:25 -0800 Subject: [PATCH 221/642] Update Luau to 0.702 (#645) **Luau**: Updated from `0.701` to `0.702` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.702 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: skberkeley --- extern/luau.tune | 4 ++-- lute/luau/src/luau.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 3ef75a384..05fe1c586 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.701" -revision = "535f92589bdc304c8a2012d6cfad5e7b9faff2f7" +branch = "0.702" +revision = "c836feb2450a074581010f84da2eadeea38a3d55" diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index ca8a27d71..3979dc3b5 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -55,7 +55,7 @@ struct ExprResult std::shared_ptr allocator; std::shared_ptr names; - Luau::ParseExprResult parseResult; + Luau::ParseNodeResult parseResult; }; static ExprResult parseExpr(std::string& source) @@ -2739,7 +2739,7 @@ int luau_parseexpr(lua_State* L) } AstSerialize serializer{L, source, result.parseResult.cstNodeMap, result.parseResult.commentLocations}; - serializer.visit(result.parseResult.expr); + serializer.visit(result.parseResult.root); return 1; } From 081f421a978b3d8cc2844905cbc6a28ada747801 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:44:37 -0500 Subject: [PATCH 222/642] Battery for Text Diffing (#620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `diffText` battery, that implements a myers-diff esque algorithm using the notion of an edit graph. An edit graph can be described as: - src string along x axis - destination string along y axis - each edge in the graph represents a diff operation: - (n, n) -> (n+1, n) represents a deletion of src[n+1] - (n, n) -> (n, n+1) represents an addition of destination[n+1] - (n, n) -> (n+1, n+1) exists only if src[n+1] == destination[n+1] - The shortest path from (0, 0) -> (len(src), len(destination)) represents the shortest-edit-sequence. Myers-diff works by greedily following diagonal edges (n,n -> n+1,n+1) but the algorithm is not super intuitive / readable imo. This implementation stays consistent with the greedy nature but varies slightly in how the graph is traversed. We use a deque to BFS the edit graph in a greedy fashion: whenever a node has a diagonal edge, we push it to the front of the deque, while other edges are always pushed to the back. Here is what the output of `examples/diffText.luau` looks like: Screenshot 2025-12-04 at 6 02 45 PM **Follow up work:** support side-by-side diff printing --- batteries/difftext/init.luau | 19 ++ batteries/difftext/myersdiff.luau | 135 +++++++++ batteries/difftext/printdiff.luau | 106 +++++++ batteries/difftext/types.luau | 12 + examples/difftext.luau | 32 +++ lute/std/libs/tableext.luau | 11 + tests/batteries/difftext.test.luau | 438 +++++++++++++++++++++++++++++ tests/std/tableext.test.luau | 13 + 8 files changed, 766 insertions(+) create mode 100644 batteries/difftext/init.luau create mode 100644 batteries/difftext/myersdiff.luau create mode 100644 batteries/difftext/printdiff.luau create mode 100644 batteries/difftext/types.luau create mode 100644 examples/difftext.luau create mode 100644 tests/batteries/difftext.test.luau diff --git a/batteries/difftext/init.luau b/batteries/difftext/init.luau new file mode 100644 index 000000000..4838d2b17 --- /dev/null +++ b/batteries/difftext/init.luau @@ -0,0 +1,19 @@ +local myersdiff = require("@self/myersdiff") +local printdiff = require("@self/printdiff") +local types = require("@self/types") + +export type diff = types.diff +export type diffoptions = types.diffoptions + +local function diff(a: string, b: string, options: diffoptions): diff + if options and options.byChar then + return myersdiff.myersdiffbychar(a, b) + else + return myersdiff.myersdiffbyline(a, b) + end +end + +return { + diff = diff, + prettydiff = printdiff, +} diff --git a/batteries/difftext/myersdiff.luau b/batteries/difftext/myersdiff.luau new file mode 100644 index 000000000..4ca0411bb --- /dev/null +++ b/batteries/difftext/myersdiff.luau @@ -0,0 +1,135 @@ +local deque = require("@batteries/collections/deque") +local tableext = require("@std/tableext") +local types = require("./types") + +type diffoperation = types.diffoperation +type diff = types.diff + +type EditGraphNode = { + x: number, + y: number, + prev: EditGraphNode?, +} + +local function editGraphNode(x: number, y: number, prev: EditGraphNode?): EditGraphNode + return { + x = x, + y = y, + prev = prev, + } +end + +-- true if a[x+1] == b[y+1]; indicates an EQUAL operation (no diff) is valid +local function hasDiagonalEdge(node: EditGraphNode, a: string | { string }, b: string | { string }): boolean + local x, y = node.x, node.y + if typeof(a) == "table" then + if typeof(b) == "table" then + return x < #a and y < #b and a[x + 1] == b[y + 1] + else + error("hasDiagonalEdge inputs should have the same type.") + end + elseif typeof(b) == "table" then + error("hasDiagonalEdge inputs should have the same type.") + end + + return x < #a and y < #b and a:sub(x + 1, x + 1) == b:sub(y + 1, y + 1) +end + +local function edgeTodiff(curNode: EditGraphNode, a: string | { string }, b: string | { string }): diffoperation? + -- maps stepping from curNode.prev -> curNode to a diff operation + -- (n-1, n) -> (n, n) == DELETION of a[n] + -- (n, n-1) -> (n, n) == ADDITION of b[n] + -- (n-1, n-1) -> (n, n) == EQUAL (a[n] == b[n]) + if not curNode.prev then + return nil + end + + local prev = curNode.prev + local xdiff = curNode.x - prev.x + local ydiff = curNode.y - prev.y + if xdiff == 1 and ydiff == 1 then + return { + key = "EQUAL", + text = if typeof(a) == "table" then a[curNode.x] else a:sub(curNode.x, curNode.x), + } + elseif xdiff == 1 then + return { + key = "DELETE", + text = if typeof(a) == "table" then a[curNode.x] else a:sub(curNode.x, curNode.x), + } + else + return { + key = "ADD", + text = if typeof(b) == "table" then b[curNode.y] else b:sub(curNode.y, curNode.y), + } + end +end + +local function myersdiff(a: string | { string }, b: string | { string }): diff + local start = editGraphNode(0, 0) + local editGraphDeque = deque.new(start) + + local diff = {} :: diff + local seen = {} :: { [string]: true } + seen["0,0"] = true + + while #editGraphDeque do + local curNode = editGraphDeque:popfront() + + -- we've reached bottom right vertex; indicates full edit from a -> b + if curNode.x == #a and curNode.y == #b then + -- reconstruct diff operations by tracing path from termination node to start + local edgeOp = edgeTodiff(curNode, a, b) + while edgeOp do + table.insert(diff, edgeOp) + curNode = curNode.prev + edgeOp = if curNode then edgeTodiff(curNode, a, b) else nil + end + break + end + + -- add neighbors (diag prioritzed, then deletion, then insertion) + if hasDiagonalEdge(curNode, a, b) then + -- algorithm optimizes for diagonal edges since they represent no diff. We want the shortest-edit-sequence (least # of diff operations) + -- so diagonal edges representing equality in a[n+1] & b[n+1] is always preferred + local edgePositionStr = `{curNode.x + 1},{curNode.y + 1}` + if not seen[edgePositionStr] then + seen[edgePositionStr] = true + editGraphDeque:pushfront(editGraphNode(curNode.x + 1, curNode.y + 1, curNode)) + end + continue + end + + if curNode.x < #a then + local edgePositionStr = `{curNode.x + 1},{curNode.y}` + if not seen[edgePositionStr] then + seen[edgePositionStr] = true + editGraphDeque:pushback(editGraphNode(curNode.x + 1, curNode.y, curNode)) + end + end + + if curNode.y < #b then + local edgePositionStr = `{curNode.x},{curNode.y + 1}` + if not seen[edgePositionStr] then + seen[edgePositionStr] = true + editGraphDeque:pushback(editGraphNode(curNode.x, curNode.y + 1, curNode)) + end + end + end + + tableext.reverse(diff, true) + return diff +end + +local function myersdiffbyline(a: string, b: string): diff + return myersdiff(a:split("\n"), b:split("\n")) +end + +local function myersdiffbychar(a: string, b: string): diff + return myersdiff(a, b) +end + +return table.freeze({ + myersdiffbyline = myersdiffbyline, + myersdiffbychar = myersdiffbychar, +}) diff --git a/batteries/difftext/printdiff.luau b/batteries/difftext/printdiff.luau new file mode 100644 index 000000000..b8a833593 --- /dev/null +++ b/batteries/difftext/printdiff.luau @@ -0,0 +1,106 @@ +local richterm = require("@batteries/richterm") +local myersdiff = require("./myersdiff") +local myersdiffbychar = myersdiff.myersdiffbychar +local myersdiffbyline = myersdiff.myersdiffbyline +local types = require("./types") + +type diff = types.diff + +-- Collects consecutive DELETEs and/or ADDs starting at index i +-- Returns: deletes array, adds array, new index position +local function collectConsecutiveChanges(diff: diff, startIndex: number): ({ string }, { string }, number) + local i = startIndex + local deletes = {} + while i <= #diff and diff[i].key == "DELETE" do + table.insert(deletes, diff[i].text) + i += 1 + end + + local adds = {} + while i <= #diff and diff[i].key == "ADD" do + table.insert(adds, diff[i].text) + i += 1 + end + + return deletes, adds, i +end + +local function visualizeCharDiff(a: string, b: string): (string, string) + local charOps = myersdiffbychar(a, b) + -- diffs two strings by char and returns colored visual for src and destination text + local srcVisual = {} + local destVisual = {} + + for _, op in charOps do + if op.key == "EQUAL" then + table.insert(srcVisual, richterm.brightRed(op.text)) + table.insert(destVisual, richterm.brightGreen(op.text)) + elseif op.key == "DELETE" then + table.insert(srcVisual, richterm.bgRed(op.text)) + elseif op.key == "ADD" then + table.insert(destVisual, richterm.bgGreen(op.text)) + end + end + + return table.concat(srcVisual, ""), table.concat(destVisual, "") +end + +local function printDiffByLineDetailed(a: string, b: string): string + local diff = myersdiffbyline(a, b) + local i = 1 + local result: { string } = {} + while i <= #diff do + local op = diff[i] + + if op.key == "DELETE" or op.key == "ADD" then + local deletes, adds + deletes, adds, i = collectConsecutiveChanges(diff, i) + local pair = #deletes == 1 and #adds == 1 + if pair then + local srcLine, destLine = visualizeCharDiff(deletes[1], adds[1]) + table.insert(result, richterm.brightRed("- ") .. srcLine) + table.insert(result, richterm.brightGreen("+ ") .. destLine) + else + for j, deleted in deletes do + table.insert(result, richterm.brightRed(`- {deleted}`)) + end + for j, added in adds do + table.insert(result, richterm.brightGreen(`+ {added}`)) + end + end + else -- EQUAL + table.insert(result, op.text) + i += 1 + end + end + return table.concat(result, "\n") +end + +local function prettydiff( + a: string, + b: string, + options: { + detailed: boolean?, + }? +): string + if options then + if options.detailed then + return printDiffByLineDetailed(a, b) + end + end + + local result = {} :: { string } + local diff = myersdiffbyline(a, b) + for i, op in diff do + table.insert( + result, + if op.key == "ADD" + then richterm.brightGreen(`+ {op.text}`) + elseif op.key == "DELETE" then richterm.brightRed(`- {op.text}`) + else `{op.text}` + ) + end + return table.concat(result, "\n") +end + +return prettydiff diff --git a/batteries/difftext/types.luau b/batteries/difftext/types.luau new file mode 100644 index 000000000..ce2e57abb --- /dev/null +++ b/batteries/difftext/types.luau @@ -0,0 +1,12 @@ +export type diffoperation = { + key: "EQUAL" | "ADD" | "DELETE", + text: string, +} + +export type diff = { diffoperation } + +export type diffoptions = { + byChar: boolean?, +}? + +return table.freeze({}) diff --git a/examples/difftext.luau b/examples/difftext.luau new file mode 100644 index 000000000..2aee1dc30 --- /dev/null +++ b/examples/difftext.luau @@ -0,0 +1,32 @@ +local difftext = require("@batteries/difftext") +local richterm = require("@batteries/richterm") + +local src = [[ +local originalVariable = 1 +local same = 1 + +function doSomething() -- comment + -- removed + print(originalVariable * 2) +end + +print() +]] + +local destination = [[ +local newVariable = 2 +local same = 1 + +function doSomething() + print(newVariable * 2) +end + +doSomething() +]] + +print(richterm.bold("Diff")) +print(difftext.prettydiff(src, destination)) +print(richterm.bold("Diff (Detailed)")) +print(difftext.prettydiff(src, destination, { + detailed = true, +})) diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau index eeda367a5..a2a626909 100644 --- a/lute/std/libs/tableext.luau +++ b/lute/std/libs/tableext.luau @@ -56,4 +56,15 @@ function tableext.keys(tbl: { [K]: V }): { K } return keys end +function tableext.reverse(tbl: { T }, inplace: boolean?) + local new = if inplace then tbl else {} + local i, j = 1, #tbl + while i <= j do + new[i], new[j] = tbl[j], tbl[i] + i += 1 + j -= 1 + end + return new +end + return table.freeze(tableext) diff --git a/tests/batteries/difftext.test.luau b/tests/batteries/difftext.test.luau new file mode 100644 index 000000000..b4025e65d --- /dev/null +++ b/tests/batteries/difftext.test.luau @@ -0,0 +1,438 @@ +local difftext = require("@batteries/difftext") +local richterm = require("@batteries/richterm") +local test = require("@std/test") + +local diff = difftext.diff + +-- Each diff operation has structure: { key: "EQUAL" | "ADD" | "DELETE", text: string } +-- Order matters - the sequence of operations defines the transformation + +test.suite("myersdiff - bychar", function(suite) + suite:case("identical strings", function(assert) + local a = "abc" + local b = "abc" + local diffResult = diff(a, b, { byChar = true }) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("single character change in middle", function(assert) + local a = "abc" + local b = "axc" + local diffResult = diff(a, b, { byChar = true }) + -- a(EQUAL), b(DELETE), x(ADD), c(EQUAL) + assert.eq(#diffResult, 4) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "x") + assert.eq(diffResult[4].key, "EQUAL") + assert.eq(diffResult[4].text, "c") + end) + + suite:case("character addition at start", function(assert) + local a = "bc" + local b = "abc" + local diffResult = diff(a, b, { byChar = true }) + -- a(ADD), b(EQUAL), c(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "ADD") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("character addition at end", function(assert) + local a = "ab" + local b = "abc" + local diffResult = diff(a, b, { byChar = true }) + -- a(EQUAL), b(EQUAL), c(ADD) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("character deletion at start", function(assert) + local a = "abc" + local b = "bc" + local diffResult = diff(a, b, { byChar = true }) + -- a(DELETE), b(EQUAL), c(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("character deletion at end", function(assert) + local a = "abc" + local b = "ab" + local diffResult = diff(a, b, { byChar = true }) + -- a(EQUAL), b(EQUAL), c(DELETE) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "DELETE") + assert.eq(diffResult[3].text, "c") + end) + + suite:case("multiple consecutive changes", function(assert) + local a = "abc" + local b = "xyz" + local diffResult = diff(a, b, { byChar = true }) + -- a(DELETE), b(DELETE), c(DELETE), x(ADD), y(ADD), z(ADD) + assert.eq(#diffResult, 6) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "a") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "b") + assert.eq(diffResult[3].key, "DELETE") + assert.eq(diffResult[3].text, "c") + assert.eq(diffResult[4].key, "ADD") + assert.eq(diffResult[4].text, "x") + assert.eq(diffResult[5].key, "ADD") + assert.eq(diffResult[5].text, "y") + assert.eq(diffResult[6].key, "ADD") + assert.eq(diffResult[6].text, "z") + end) + + suite:case("empty to non-empty", function(assert) + local a = "" + local b = "hi" + local diffResult = diff(a, b, { byChar = true }) + -- h(ADD), i(ADD) + assert.eq(#diffResult, 2) + assert.eq(diffResult[1].key, "ADD") + assert.eq(diffResult[1].text, "h") + assert.eq(diffResult[2].key, "ADD") + assert.eq(diffResult[2].text, "i") + end) + + suite:case("non-empty to empty", function(assert) + local a = "hi" + local b = "" + local diffResult = diff(a, b, { byChar = true }) + -- h(DELETE), i(DELETE) + assert.eq(#diffResult, 2) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "h") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "i") + end) +end) + +test.suite("myersdiff - byline", function(suite) + suite:case("identical multiline strings", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline2\nline3" + local diffResult = diff(a, b) + -- All lines should be EQUAL + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("add line at start", function(assert) + local a = "line2\nline3" + local b = "line1\nline2\nline3" + local diffResult = diff(a, b) + -- line1(ADD), line2(EQUAL), line3(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "ADD") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("add line at end", function(assert) + local a = "line1\nline2" + local b = "line1\nline2\nline3" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(EQUAL), line3(ADD) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("add line in middle", function(assert) + local a = "line1\nline3" + local b = "line1\nline2\nline3" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(ADD), line3(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "ADD") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("delete line at start", function(assert) + local a = "line1\nline2\nline3" + local b = "line2\nline3" + local diffResult = diff(a, b) + -- line1(DELETE), line2(EQUAL), line3(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("delete line at end", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline2" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(EQUAL), line3(DELETE) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "EQUAL") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "DELETE") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("delete line in middle", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline3" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(DELETE), line3(EQUAL) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "EQUAL") + assert.eq(diffResult[3].text, "line3") + end) + + suite:case("modify single line", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nmodified\nline3" + local diffResult = diff(a, b) + -- line1(EQUAL), line2(DELETE), modified(ADD), line3(EQUAL) + assert.eq(#diffResult, 4) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "modified") + assert.eq(diffResult[4].key, "EQUAL") + assert.eq(diffResult[4].text, "line3") + end) + + suite:case("multiple line changes", function(assert) + local a = "A\nB\nC\nD" + local b = "A\nX\nC\nY\nD" + local diffResult = diff(a, b) + -- A(EQUAL), B(DELETE), X(ADD), C(EQUAL), Y(ADD), D(EQUAL) + assert.eq(#diffResult, 6) + assert.eq(diffResult[1].key, "EQUAL") + assert.eq(diffResult[1].text, "A") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "B") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "X") + assert.eq(diffResult[4].key, "EQUAL") + assert.eq(diffResult[4].text, "C") + assert.eq(diffResult[5].key, "ADD") + assert.eq(diffResult[5].text, "Y") + assert.eq(diffResult[6].key, "EQUAL") + assert.eq(diffResult[6].text, "D") + end) + + suite:case("empty to multiline", function(assert) + local a = "" + local b = "line1\nline2" + local diffResult = diff(a, b) + -- When splitting "" by "\n", we get {""}, so it's: ""(DELETE), line1(ADD), line2(ADD) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "") + assert.eq(diffResult[2].key, "ADD") + assert.eq(diffResult[2].text, "line1") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "line2") + end) + + suite:case("multiline to empty", function(assert) + local a = "line1\nline2" + local b = "" + local diffResult = diff(a, b) + -- line1(DELETE), line2(DELETE), ""(ADD) + assert.eq(#diffResult, 3) + assert.eq(diffResult[1].key, "DELETE") + assert.eq(diffResult[1].text, "line1") + assert.eq(diffResult[2].key, "DELETE") + assert.eq(diffResult[2].text, "line2") + assert.eq(diffResult[3].key, "ADD") + assert.eq(diffResult[3].text, "") + end) +end) + +test.suite("printdiff", function(suite: any) + local function composeDetailedRemoval(str: string, changedCharPos: { [number]: true }) + local composed = richterm.brightRed("- ") + for i = 1, #str do + if changedCharPos[i] then + composed ..= richterm.bgRed(str:sub(i, i)) + else + composed ..= richterm.brightRed(str:sub(i, i)) + end + end + return composed + end + + local function composeDetailedAddition(str: string, changedCharPos: { [number]: true }) + local composed = richterm.brightGreen("+ ") + for i = 1, #str do + if changedCharPos[i] then + composed ..= richterm.bgGreen(str:sub(i, i)) + else + composed ..= richterm.brightGreen(str:sub(i, i)) + end + end + return composed + end + + suite:case("printdiff default - simple addition", function(assert) + local a = "line1\nline2" + local b = "line1\nline2\nline3" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 3) + assert.eq(lines[1], "line1") + assert.eq(lines[2], "line2") + assert.eq(lines[3], richterm.brightGreen("+ line3")) + end) + + suite:case("printdiff default - simple deletion", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline3" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 3) + assert.eq(lines[1], "line1") + assert.eq(lines[2], richterm.brightRed("- line2")) + assert.eq(lines[3], "line3") + end) + + suite:case("printdiff default - modification", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nmodified\nline3" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 4) + assert.eq(lines[1], "line1") + assert.eq(lines[2], richterm.brightRed("- line2")) + assert.eq(lines[3], richterm.brightGreen("+ modified")) + assert.eq(lines[4], "line3") + end) + + suite:case("printdiff default - unchanged lines", function(assert) + local a = "line1\nline2\nline3" + local b = "line1\nline2\nline3" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 3) + assert.eq(lines[1], "line1") + assert.eq(lines[2], "line2") + assert.eq(lines[3], "line3") + end) + + suite:case("printdiff default - multiple changes", function(assert) + local a = "A\nB\nC\nD" + local b = "A\nX\nC\nY\nD" + local result = difftext.prettydiff(a, b) + local lines = result:split("\n") + + assert.eq(#lines, 6) + assert.eq(lines[1], "A") + assert.eq(lines[2], richterm.brightRed("- B")) + assert.eq(lines[3], richterm.brightGreen("+ X")) + assert.eq(lines[4], "C") + assert.eq(lines[5], richterm.brightGreen("+ Y")) + assert.eq(lines[6], "D") + end) + + suite:case("printdiff detailed - single line change", function(assert) + local a = "hello world" + local b = "hello wirld" + local result = difftext.prettydiff(a, b, { detailed = true }) + local lines = result:split("\n") + + -- Detailed mode shows both old and new lines with character-level highlighting + assert.eq(#lines, 2) + assert.eq(lines[1], composeDetailedRemoval("hello world", { [8] = true })) + assert.eq(lines[2], composeDetailedAddition("hello wirld", { [8] = true })) + end) + + suite:case("printdiff detailed - multiple line changes", function(assert) + local a = "line1\nold line\nline3" + local b = "line1\nnew line\nline3" + local result = difftext.prettydiff(a, b, { detailed = true }) + local lines = result:split("\n") + + local changedChars = { + [1] = true, + [2] = true, + [3] = true, + } + + -- Should have: line1, - old line, + new line, line3 + assert.eq(#lines, 4) + assert.eq(lines[1], "line1") + assert.eq(lines[2], composeDetailedRemoval("old line", changedChars)) + assert.eq(lines[3], composeDetailedAddition("new line", changedChars)) + assert.eq(lines[4], "line3") + end) + + suite:case("printdiff detailed - unchanged lines", function(assert) + local a = "line1\nline2" + local b = "line1\nline2" + local result = difftext.prettydiff(a, b, { detailed = true }) + local lines = result:split("\n") + + assert.eq(#lines, 2) + assert.eq(lines[1], "line1") + assert.eq(lines[2], "line2") + end) +end) + +test.run() diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index 0f8925bbb..266d6e346 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -49,6 +49,19 @@ test.suite("TableExt", function(suite) assert.neq(a[key], nil) end end) + + suite:case("reverse", function(assert) + -- in-place + local a = { 1, 2, 3 } + tableext.reverse(a, true) + assert.tableeq(a, { 3, 2, 1 }) + + -- return new + local b = { 4, 5, 6 } + local c = tableext.reverse(b) + assert.tableeq(b, { 4, 5, 6 }) + assert.tableeq(c, { 6, 5, 4 }) + end) end) test.run() From 8fa3cc2aa9c58ff23f10b29e65e75065e6d1f795 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:46:28 +0000 Subject: [PATCH 223/642] Update Lute to 0.1.0-nightly.20251205 (#655) **Lute**: Updated from `0.1.0-nightly.20251204` to `0.1.0-nightly.20251205` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20251205 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: skberkeley --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 6497c4b61..f6e686a53 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251204" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251205" } diff --git a/rokit.toml b/rokit.toml index b8ece9ebe..3a1f07b74 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20251204" +lute = "luau-lang/lute@0.1.0-nightly.20251205" From e4df9d8664d1eac301343cf59991f5834cd1d970 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 9 Dec 2025 11:15:04 -0800 Subject: [PATCH 224/642] Updates CI to automatically generate release notes (#663) This PR adds support for auto-generating release notes for Lute. I've also updated the contribution guidelines to suggest a) making PR titles descriptive, user-facing one liners, and b) adding labels to PR's so that the release notes will place changes in the appropriate categories. --- .github/workflows/release.yml | 1 + CONTRIBUTING.md | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a6368f60..c198d413d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,6 +161,7 @@ jobs: name: ${{ steps.tag_release.outputs.tag }} draft: true prerelease: ${{ github.event.inputs.nightly || github.event_name == 'schedule' }} + generate_release_notes: true files: release/*.zip - name: Wait for Tag Creation in GitHub diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e1650632..bbb44a400 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,6 +60,25 @@ There are some additional style considerations that we do not have automated enf 5. Small, incremental contributions are always preferred over sweeping changes. You should expect that any large sweeping change will be rejected summarily without review. If you're interested in working on something that _requires_ such a change, you should open an issue first to discuss the idea and get buy-in from the team. +## Pull Request Guidelines + +When submitting a pull request: + +1. **PR Title:** Your PR title should be a clear, one-line sentence that makes sense as a changelog entry. This title will appear in the release notes, so write it to be informative to users. Focus on what changed from a user's perspective. + - Good: "Adds support for custom error handlers in the CLI" + - Good: "Fixes memory leak in filesystem operations" + - Bad: "Update code" + - Bad: "WIP: testing changes" + +2. **PR Labels:** Please label your PR with one of the following labels so it appears in the correct section of the release notes: + - `documentation` - Documentation improvements + - `std` - Changes to the standard library + - `batteries` - Changes to batteries (additional libraries that are useful for writing lute code) + - `cli` - Changes to the command line - either the c++ subcommands (run, compile, etc) or lute tooling (lint, transform, test) + - `runtime` - Changes to the C++ runtime + - `infra` - Changes to CI/CD, build system, or project infrastructure + - `bug` - Bug fixes + ## Licensing By providing code in an issue or opening a pull request, you agree to license that code under the MIT License, and indicate that you have the legal right to do so. From a65a478755d8852cf210119c75faf9d2907568c5 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 9 Dec 2025 13:31:56 -0800 Subject: [PATCH 225/642] Adds support for luaurc files in executables from `lute compile` (#626) The `lute compile` command is built on the `resolveRequire` api. This is `.luaurc` aware, which means that we can resolve require statements that have aliases in them. However, while running the binary that `lute compile` emits, we aren't able to resolve these aliased requires at runtime, because we don't have access to the `.luaurc`. This PR adds support for this by: 1. Collecting the paths to .luaurc during static require tracing 2. Injecting this configuration into the final executable. 3. Reading the config back out when running the embedded scripts and navigating to directories based on the aliases stored. Currently doesn't support config.luau. --------- Co-authored-by: ariel --- lute/cli/include/lute/compile.h | 8 ++ lute/cli/include/lute/staticrequires.h | 8 ++ lute/cli/src/climain.cpp | 45 ++++---- lute/cli/src/compile.cpp | 94 +++++++++++++++- lute/cli/src/staticrequires.cpp | 95 +++++++++++++++++ lute/require/include/lute/bundlevfs.h | 3 +- lute/require/src/bundlevfs.cpp | 68 +++++++++--- tests/src/compile.test.cpp | 44 ++++++++ tests/src/staticrequires.test.cpp | 100 +++++++++++++++++- tests/src/staticrequires/.luaurc | 5 + tests/src/staticrequires/dep/option.luau | 3 + tests/src/staticrequires/main.luau | 6 ++ tests/src/staticrequires/other/.luaurc | 5 + .../src/staticrequires/other/lib/example.luau | 3 + tests/src/staticrequires/other/module.luau | 7 ++ 15 files changed, 456 insertions(+), 38 deletions(-) create mode 100644 tests/src/staticrequires/.luaurc create mode 100644 tests/src/staticrequires/dep/option.luau create mode 100644 tests/src/staticrequires/other/.luaurc create mode 100644 tests/src/staticrequires/other/lib/example.luau create mode 100644 tests/src/staticrequires/other/module.luau diff --git a/lute/cli/include/lute/compile.h b/lute/cli/include/lute/compile.h index fb8ecefe6..ab161f6c5 100644 --- a/lute/cli/include/lute/compile.h +++ b/lute/cli/include/lute/compile.h @@ -21,6 +21,12 @@ struct LuteEncodeResult * Represents a bundle of compiled Luau files ready for injection. * * Uncompressed bundle format (before compression): + * [num_config_entries: uint32_t] + * For each config entry: + * [path_length: uint32_t] + * [path_val: char[path_length]] + * [luaurc_length: uint32_t] + * [luaurc_contents: char[luaurc_length]] * For each file: * [path_length: uint32_t] * [path_string: char[path_length]] @@ -40,12 +46,14 @@ struct LuteExePayload { LuteExePayload(LuteReporter& reporter); void add(const std::string& bundlePath, const std::string& sourcePath); + void setLuauConfig(const Luau::DenseHashMap& configs); std::optional encode(); static std::optional decode(const std::string_view binary, LuteReporter& reporter); std::string entryPointPath; Luau::DenseHashMap filePathToBytecode{""}; // path -> bytecode + Luau::DenseHashMap luauConfigFiles{""}; // path -> config private: LuteReporter& reporter; diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h index 426db0f31..d7653eac9 100644 --- a/lute/cli/include/lute/staticrequires.h +++ b/lute/cli/include/lute/staticrequires.h @@ -29,6 +29,13 @@ class StaticRequireTracer return lowestCommonRoot; } + // Get discovered .luaurc files as a map of (lcrPath -> content) + // lcrPath is the absolute path with the lowest common prefix stripped + const Luau::DenseHashMap& getLuaurcFiles() const + { + return luaurcFiles; + } + void printRequireGraph() const; // Find the lowest common root directory from a collection of absolute paths static std::string findLowestCommonRoot(const std::vector& paths); @@ -37,6 +44,7 @@ class StaticRequireTracer Luau::DenseHashSet visited{""}; std::vector discovered; // Absolute paths Luau::DenseHashMap> requireGraph{""}; // Absolute paths + Luau::DenseHashMap luaurcFiles{""}; // LCR-relative path -> content std::string lowestCommonRoot; // Extract all require() paths from source code diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 90144af3b..f3846e7d9 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -166,7 +166,11 @@ static void luteopen_libs(lua_State* L) } } -void* createBundleRequireContext(lua_State* L, Luau::DenseHashMap bundleMap) +void* createBundleRequireContext( + lua_State* L, + Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap bundleMap +) { void* ctx = lua_newuserdatadtor( L, @@ -179,7 +183,7 @@ void* createBundleRequireContext(lua_State* L, Luau::DenseHashMap(BundleVfs{std::move(bundleMap)})}; + ctx = new (ctx) RequireCtx{std::make_unique(BundleVfs{std::move(luaurcFiles), std::move(bundleMap)})}; // Store RequireCtx in the registry to keep it alive for the lifetime of // this lua_State. Memory address is used as a key to avoid collisions. @@ -208,17 +212,21 @@ lua_State* setupCliState(Runtime& runtime, std::function preSa ); } -lua_State* setupBundleState(Runtime& runtime, Luau::DenseHashMap bundleMap) +lua_State* setupBundleState( + Runtime& runtime, + Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap bundleMap +) { return setupState( runtime, - [bundleMap = std::move(bundleMap)](lua_State* L) + [luaurcFiles = std::move(luaurcFiles), bundleMap = std::move(bundleMap)](lua_State* L) { luteopen_libs(L); if (Luau::CodeGen::isSupported()) Luau::CodeGen::create(L); - luaopen_require(L, requireConfigInit, createBundleRequireContext(L, std::move(bundleMap))); + luaopen_require(L, requireConfigInit, createBundleRequireContext(L, std::move(luaurcFiles), std::move(bundleMap))); } ); } @@ -558,6 +566,9 @@ int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& rep payload.add(bundle, absolute); } + // Add the discovered luaurc configuration + payload.setLuauConfig(tracer.getLuaurcFiles()); + // Encode the payload reporter.reportOutput("Compiling and bundling bytecode..."); @@ -622,21 +633,19 @@ int cliMain(int argc, char** argv, LuteReporter& reporter) setLuauFlags(); std::string err = ""; - if (auto exePath = process::getExecPath(&err)) + + LuteExecutable exe{argv[0], reporter}; + if (auto payload = exe.extract()) { - LuteExecutable exe{*exePath, reporter}; - if (auto payload = exe.extract()) - { - Runtime runtime; + Runtime runtime; - lua_State* GL = setupBundleState(runtime, payload->filePathToBytecode); - std::string entryPoint = payload->entryPointPath; - auto entryModule = payload->filePathToBytecode.find(entryPoint); - if (entryModule != nullptr) - { - bool success = runBytecode(runtime, *entryModule, "@@bundle/" + entryPoint, GL, argc, argv, reporter); - return success ? 0 : 1; - } + lua_State* GL = setupBundleState(runtime, payload->luauConfigFiles, payload->filePathToBytecode); + std::string entryPoint = payload->entryPointPath; + auto entryModule = payload->filePathToBytecode.find(entryPoint); + if (entryModule != nullptr) + { + bool success = runBytecode(runtime, *entryModule, "@@bundle/" + entryPoint, GL, argc, argv, reporter); + return success ? 0 : 1; } } diff --git a/lute/cli/src/compile.cpp b/lute/cli/src/compile.cpp index 7931d274d..f957a5991 100644 --- a/lute/cli/src/compile.cpp +++ b/lute/cli/src/compile.cpp @@ -36,6 +36,11 @@ void LuteExePayload::add(const std::string& bundlePath, const std::string& sourc sourceToBundlePath[sourcePath] = bundlePath; } +void LuteExePayload::setLuauConfig(const Luau::DenseHashMap& configs) +{ + luauConfigFiles = configs; +} + std::optional LuteExePayload::encode() { // Encoding an empty payload is an error @@ -46,9 +51,32 @@ std::optional LuteExePayload::encode() } LuteEncodeResult result; - // Step 1: Build uncompressed bytecode bundle - // Format: For each file, append [path_len][path][bytecode_size][bytecode] + // Step 1: Build uncompressed bundle + // Format: [num_config_entries][config entries...][file entries...] std::string uncompressedBundle; + + // Step 1a: Write .luaurc config files first + uint32_t numConfigEntries = static_cast(luauConfigFiles.size()); + uncompressedBundle.append(reinterpret_cast(&numConfigEntries), sizeof(uint32_t)); + + for (const auto& [configPath, configContent] : luauConfigFiles) + { + // Append path_length field (uint32_t, 4 bytes) + uint32_t pathLength = static_cast(configPath.size()); + uncompressedBundle.append(reinterpret_cast(&pathLength), sizeof(uint32_t)); + + // Append path_val field (variable length) + uncompressedBundle.append(configPath); + + // Append luaurc_length field (uint32_t, 4 bytes) + uint32_t luaurcLength = static_cast(configContent.size()); + uncompressedBundle.append(reinterpret_cast(&luaurcLength), sizeof(uint32_t)); + + // Append luaurc_contents field (variable length) + uncompressedBundle.append(configContent); + } + + // Step 1b: Write bytecode files for (const auto& sourcePath : filePaths) { // Get the bundle path (rooted path for the bundle) @@ -275,7 +303,69 @@ bool LuteExePayload::parseFromDecompressedBundle(std::string_view decompressedBu { size_t offset = 0; filePathToBytecode.clear(); + luauConfigFiles.clear(); + + // Step 1: Read num_config_entries + if (offset + sizeof(uint32_t) > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete num_config_entries field"); + return false; + } + + uint32_t numConfigEntries; + memcpy(&numConfigEntries, decompressedBundle.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + + // Step 2: Read each config entry + for (uint32_t i = 0; i < numConfigEntries; ++i) + { + // Read path length + if (offset + sizeof(uint32_t) > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete config path length field"); + return false; + } + + uint32_t pathLength; + memcpy(&pathLength, decompressedBundle.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + + // Read path string + if (offset + pathLength > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete config path string"); + return false; + } + + std::string configPath(decompressedBundle.data() + offset, pathLength); + offset += pathLength; + + // Read luaurc length + if (offset + sizeof(uint32_t) > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete luaurc length field"); + return false; + } + + uint32_t luaurcLength; + memcpy(&luaurcLength, decompressedBundle.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + + // Read luaurc content + if (offset + luaurcLength > decompressedBundle.size()) + { + reporter.reportError("Invalid bundle: incomplete luaurc content"); + return false; + } + + std::string luaurcContent(decompressedBundle.data() + offset, luaurcLength); + offset += luaurcLength; + + // Store in map + luauConfigFiles[configPath] = luaurcContent; + } + // Step 3: Read bytecode compiled scripts while (offset < decompressedBundle.size()) { // Read path length diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index a01101e56..0a43f4941 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -5,6 +5,7 @@ #include "lute/staticrequires.h" #include "Luau/Ast.h" +#include "Luau/Config.h" #include "Luau/FileUtils.h" #include "Luau/Parser.h" #include "Luau/VecDeque.h" @@ -44,6 +45,7 @@ void StaticRequireTracer::trace(const std::string& entryPoint) visited.clear(); discovered.clear(); requireGraph.clear(); + luaurcFiles.clear(); if (!isAbsolutePath(entryPoint)) { @@ -51,6 +53,21 @@ void StaticRequireTracer::trace(const std::string& entryPoint) return; } + // Calculate the entry point directory - we should not look for .luaurc files beyond this directory + std::string entryPointDir = entryPoint; + size_t lastSlash = entryPointDir.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + entryPointDir = entryPointDir.substr(0, lastSlash); + } + else + { + entryPointDir = ""; + } + + // Temporary set to collect absolute paths to .luaurc files + Luau::DenseHashSet luaurcAbsolutePaths{""}; + Luau::VecDeque toProcess; toProcess.push_back(entryPoint); @@ -74,6 +91,35 @@ void StaticRequireTracer::trace(const std::string& entryPoint) // Add to discovered list (use the relative path from rootDirectory) discovered.push_back(filePath); + // Discover .luaurc files in this file's directory tree + std::string dir = filePath; + size_t lastSlash = dir.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + dir = dir.substr(0, lastSlash); + + // Walk up the directory tree looking for .luaurc files, but stop at the entry point directory + while (!dir.empty()) + { + std::string luaurcPath = dir + "/" + Luau::kConfigName; + if (isFile(luaurcPath)) + { + luaurcAbsolutePaths.insert(luaurcPath); + break; + } + + // Stop if we've reached the entry point directory + if (dir == entryPointDir) + break; + + // Move to parent directory + size_t parentSlash = dir.find_last_of("/\\"); + if (parentSlash == std::string::npos || parentSlash == 0) + break; + dir = dir.substr(0, parentSlash); + } + } + std::vector requiresInFile = extractRequires(*source); std::vector resolvedDeps; @@ -109,6 +155,40 @@ void StaticRequireTracer::trace(const std::string& entryPoint) } lowestCommonRoot = findLowestCommonRoot(discovered); + + // Convert absolute .luaurc paths to LCR-relative .luaurc paths and read their content + size_t commonRootLen = lowestCommonRoot.empty() ? 0 : lowestCommonRoot.length() + 1; // +1 for the trailing slash + + for (const auto& absolutePath : luaurcAbsolutePaths) + { + // Get the directory containing the .luaurc file and append .luaurc + std::string absoluteDir = absolutePath; + size_t lastSlash = absoluteDir.find_last_of("/\\"); + if (lastSlash != std::string::npos) + { + absoluteDir = absoluteDir.substr(0, lastSlash); + } + + // Convert to relative path and append .luaurc + std::string relativeLuaurc = ".luaurc"; + if (commonRootLen > 0 && absoluteDir.length() > commonRootLen) + { + std::string relativeDir = absoluteDir.substr(commonRootLen); + relativeLuaurc = relativeDir + "/.luaurc"; + } + + // Read the .luaurc file content + std::optional content = readFile(absolutePath); + if (content) + { + // Store using the .luaurc file path (e.g., "dir/.luaurc" or just ".luaurc") + luaurcFiles[relativeLuaurc] = *content; + } + else + { + reporter.formatError("Warning: Could not read .luaurc file '%s'\n", absolutePath.c_str()); + } + } } std::vector StaticRequireTracer::extractRequires(const std::string& source) @@ -190,6 +270,21 @@ void StaticRequireTracer::printRequireGraph() const reporter.reportOutput("\t\t(no dependencies)"); } } + + // Print luaurc files found + if (!luaurcFiles.empty()) + { + reporter.reportOutput("\n.luaurc files found:"); + for (const auto& [configDir, content] : luaurcFiles) + { + reporter.formatOutput("\t%s", configDir.c_str()); + } + } + else + { + reporter.reportOutput("\nNo .luaurc files found"); + } + reporter.reportOutput(""); } diff --git a/lute/require/include/lute/bundlevfs.h b/lute/require/include/lute/bundlevfs.h index b638ca94c..601c19d75 100644 --- a/lute/require/include/lute/bundlevfs.h +++ b/lute/require/include/lute/bundlevfs.h @@ -11,7 +11,7 @@ class BundleVfs { public: - BundleVfs(Luau::DenseHashMap bundleMap); + BundleVfs(Luau::DenseHashMap luaurcFiles, Luau::DenseHashMap bundleMap); NavigationStatus resetToPath(const std::string& path); @@ -27,5 +27,6 @@ class BundleVfs private: const Luau::DenseHashMap filePathToBytecode; + const Luau::DenseHashMap luaurcFiles; std::optional modulePath; }; diff --git a/lute/require/src/bundlevfs.cpp b/lute/require/src/bundlevfs.cpp index c52f79419..f1634dffd 100644 --- a/lute/require/src/bundlevfs.cpp +++ b/lute/require/src/bundlevfs.cpp @@ -6,19 +6,30 @@ #include #include -constexpr std::string_view kBundlePrefix = "@bundle/"; +constexpr std::string_view kBundlePrefix = "@bundle"; +constexpr std::string_view kBundlePrefixPath = "@bundle/"; -BundleVfs::BundleVfs(Luau::DenseHashMap bundleMap) +BundleVfs::BundleVfs(Luau::DenseHashMap luaurcFiles, Luau::DenseHashMap bundleMap) : filePathToBytecode(std::move(bundleMap)) + , luaurcFiles(std::move(luaurcFiles)) { } static bool isBundleModule(const Luau::DenseHashMap& bundleMap, const std::string& path) { + // The bundle root (@bundle or @bundle/) should never be treated as a file + if (path == kBundlePrefix || path == kBundlePrefixPath) + return false; + // Strip @bundle/ prefix if present std::string lookupPath = path; - if (path.rfind(kBundlePrefix, 0) == 0) - lookupPath = path.substr(kBundlePrefix.size()); + if (path.rfind(kBundlePrefixPath, 0) == 0) + lookupPath = path.substr(kBundlePrefixPath.size()); + + // The bundle root should never be treated as a file, always as a directory + // This prevents ambiguity when there's an init.lua at the root + if (lookupPath == "init.lua" || lookupPath == "init.luau") + return false; // Check direct file match if (bundleMap.find(lookupPath) != nullptr) @@ -29,10 +40,18 @@ static bool isBundleModule(const Luau::DenseHashMap& b static bool isBundleDirectory(const Luau::DenseHashMap& bundleMap, const std::string& path) { + // Handle the root directory - both "@bundle" and "@bundle/" should be treated as the root + if (path == kBundlePrefix || path == kBundlePrefixPath) + return !bundleMap.empty(); + // Strip @bundle/ prefix if present std::string lookupPath = path; - if (path.rfind(kBundlePrefix, 0) == 0) - lookupPath = path.substr(kBundlePrefix.size()); + if (path.rfind(kBundlePrefixPath, 0) == 0) + lookupPath = path.substr(kBundlePrefixPath.size()); + + // The root directory (@bundle/) exists if the bundle has any files + if (lookupPath.empty()) + return !bundleMap.empty(); // A directory exists if any file in the bundle starts with this path followed by a slash std::string prefix = lookupPath + "/"; @@ -66,11 +85,11 @@ NavigationStatus BundleVfs::resetToPath(const std::string& path) } // Handle "@bundle/path/to/file" - if (path.rfind(kBundlePrefix, 0) != 0) + if (path.rfind(kBundlePrefixPath, 0) != 0) return NavigationStatus::NotFound; // Strip "@bundle/" to get the actual path - std::string filePath = path.substr(kBundlePrefix.size()); + std::string filePath = path.substr(kBundlePrefixPath.size()); modulePath = ModulePath::create( "@bundle", @@ -117,8 +136,8 @@ std::optional BundleVfs::getContents(const std::string& path) const { // Strip @bundle/ prefix if present std::string lookupPath = path; - if (path.rfind(kBundlePrefix, 0) == 0) - lookupPath = path.substr(kBundlePrefix.size()); + if (path.rfind(kBundlePrefixPath, 0) == 0) + lookupPath = path.substr(kBundlePrefixPath.size()); // Try direct lookup const std::string* value = filePathToBytecode.find(lookupPath); @@ -130,12 +149,37 @@ std::optional BundleVfs::getContents(const std::string& path) const ConfigStatus BundleVfs::getConfigStatus() const { - // Currently, we do not support .luaurc files in bundles. + LUAU_ASSERT(modulePath); + + // Get the potential config path for the current module path + std::string configPath = modulePath->getPotentialConfigPath(".luaurc"); + + // Strip @bundle/ prefix if present + if (configPath.rfind(kBundlePrefixPath, 0) == 0) + configPath = configPath.substr(kBundlePrefixPath.size()); + + // Check if this config file exists in our luaurc map + if (luaurcFiles.find(configPath) != nullptr) + return ConfigStatus::PresentJson; + return ConfigStatus::Absent; } std::optional BundleVfs::getConfig() const { - // Currently, we do not support .luaurc files in bundles. + LUAU_ASSERT(modulePath); + + // Get the potential config path for the current module path + std::string configPath = modulePath->getPotentialConfigPath(".luaurc"); + + // Strip @bundle/ prefix if present + if (configPath.rfind(kBundlePrefixPath, 0) == 0) + configPath = configPath.substr(kBundlePrefixPath.size()); + + // Look up the config content in our luaurc map + const std::string* configContent = luaurcFiles.find(configPath); + if (configContent != nullptr) + return *configContent; + return std::nullopt; } diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp index 65fe79d23..e26f15682 100644 --- a/tests/src/compile.test.cpp +++ b/tests/src/compile.test.cpp @@ -23,6 +23,11 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_single_file_roundtrip") LuteExePayload originalPayload{getReporter()}; originalPayload.add(testFilePath, testFilePath); + // Add luaurc config + Luau::DenseHashMap configs{""}; + configs[".luaurc"] = "{\"aliases\":{\"example\":\"./dep\"}}"; + originalPayload.setLuauConfig(configs); + // Encode auto encodeResult = originalPayload.encode(); REQUIRE(encodeResult.has_value()); @@ -56,6 +61,12 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_single_file_roundtrip") auto originalIt = originalPayload.filePathToBytecode.find(testFilePath); REQUIRE(originalIt != nullptr); CHECK(*it == *originalIt); + + // Verify luaurc config was preserved + REQUIRE(decodeResult->payload.luauConfigFiles.size() == 1); + auto configIt = decodeResult->payload.luauConfigFiles.find(".luaurc"); + REQUIRE(configIt != nullptr); + CHECK(configIt->find("aliases") != std::string::npos); } TEST_CASE_FIXTURE(LuteFixture, "lutepayload_multiple_files_roundtrip") @@ -75,6 +86,11 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_multiple_files_roundtrip") originalPayload.add(file, file); } + // Add luaurc config + Luau::DenseHashMap configs{""}; + configs[".luaurc"] = "{\"aliases\":{\"example\":\"./dep\"}}"; + originalPayload.setLuauConfig(configs); + // First file should be the entry point CHECK(originalPayload.entryPointPath == testFiles[0]); @@ -102,6 +118,12 @@ TEST_CASE_FIXTURE(LuteFixture, "lutepayload_multiple_files_roundtrip") // Verify bytecode matches CHECK(*decodedIt == *originalIt); } + + // Verify luaurc config was preserved + REQUIRE(decodeResult->payload.luauConfigFiles.size() == 1); + auto configIt = decodeResult->payload.luauConfigFiles.find(".luaurc"); + REQUIRE(configIt != nullptr); + CHECK(configIt->find("aliases") != std::string::npos); } TEST_CASE_FIXTURE(LuteFixture, "lutepayload_invalid_magic_flag") @@ -295,6 +317,11 @@ TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_single_file_roundtrip") LuteExePayload originalPayload{getReporter()}; originalPayload.add(testFilePath, testFilePath); + // Add luaurc config + Luau::DenseHashMap configs{""}; + configs[".luaurc"] = "{\"aliases\":{\"example\":\"./dep\"}}"; + originalPayload.setLuauConfig(configs); + // Create LuteExecutable and write it out std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_output_exe"); LuteExecutable executable{dummyExePath, getReporter()}; @@ -325,6 +352,12 @@ TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_single_file_roundtrip") REQUIRE(extractedBytecodeIt != nullptr); CHECK(*originalBytecodeIt == *extractedBytecodeIt); + // Verify luaurc config was preserved + REQUIRE(extractedPayload->luauConfigFiles.size() == 1); + auto configIt = extractedPayload->luauConfigFiles.find(".luaurc"); + REQUIRE(configIt != nullptr); + CHECK(configIt->find("aliases") != std::string::npos); + // Clean up temporary files std::remove(dummyExePath.c_str()); std::remove(outputExePath.c_str()); @@ -356,6 +389,11 @@ TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_multiple_files_roundtrip") originalPayload.add(file, file); } + // Add luaurc config + Luau::DenseHashMap configs{""}; + configs[".luaurc"] = "{\"aliases\":{\"example\":\"./dep\"}}"; + originalPayload.setLuauConfig(configs); + // First file should be the entry point CHECK(originalPayload.entryPointPath == testFiles[0]); @@ -389,6 +427,12 @@ TEST_CASE_FIXTURE(LuteFixture, "luteexecutable_multiple_files_roundtrip") CHECK(*extractedIt == *originalIt); } + // Verify luaurc config was preserved + REQUIRE(extractedPayload->luauConfigFiles.size() == 1); + auto configIt = extractedPayload->luauConfigFiles.find(".luaurc"); + REQUIRE(configIt != nullptr); + CHECK(configIt->find("aliases") != std::string::npos); + // Clean up std::remove(dummyExePath.c_str()); std::remove(outputExePath.c_str()); diff --git a/tests/src/staticrequires.test.cpp b/tests/src/staticrequires.test.cpp index 2dcde67f5..3bc204988 100644 --- a/tests/src/staticrequires.test.cpp +++ b/tests/src/staticrequires.test.cpp @@ -21,19 +21,29 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_simple_dependencies") auto pairs = tracer.getStaticRequirePairs(); - // Should find: main.luau, utils.luau, lib/helper.luau, shared.luau - REQUIRE(pairs.size() == 4); + // Should find: main.luau, utils.luau, lib/helper.luau, shared.luau, dep/option.luau, other/module.luau, other/lib/example.luau + REQUIRE(pairs.size() == 7); // Entry point should be first CHECK(pairs[0].first == "main.luau"); - std::vector expectedFiles = {"main.luau", "utils.luau", "lib/helper.luau", "shared.luau"}; + std::vector expectedFiles = { + "main.luau", "utils.luau", "lib/helper.luau", "shared.luau", "dep/option.luau", "other/lib/example.luau", "other/module.luau" + }; for (const auto& expected : expectedFiles) { std::string absolutePath = joinPaths(tracer.getLowestCommonRoot(), expected); CHECK(tracer.containsAbsolute(absolutePath)); } + + // Verify .luaurc file was discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 2); + + // Check that .luaurc exists in the map + CHECK(luaurcFiles.find(".luaurc") != nullptr); + CHECK(luaurcFiles.find("other/.luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_circular_dependencies") @@ -59,6 +69,11 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_circular_dependencies") CHECK(tracer.containsAbsolute(absoluteA)); CHECK(tracer.containsAbsolute(absoluteB)); + + // Verify .luaurc file was discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 1); + CHECK(luaurcFiles.find(".luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_no_dependencies") @@ -75,6 +90,11 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_no_dependencies") // utils.luau has no requires, should only return itself REQUIRE(pairs.size() == 1); CHECK(pairs[0].first == "utils.luau"); + + // Verify .luaurc file was discovered (should find it in the same directory) + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 1); + CHECK(luaurcFiles.find(".luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_relative_paths") @@ -95,6 +115,11 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_relative_paths") std::string absoluteShared = joinPaths(tracer.getLowestCommonRoot(), "shared.luau"); CHECK(tracer.containsAbsolute(absoluteShared)); + + // Verify .luaurc file was discovered (should find it in parent directory) + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 1); + CHECK(luaurcFiles.find(".luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_require_graph") @@ -111,12 +136,25 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_require_graph") std::string utilsPath = joinPaths(testDir, "utils.luau"); std::string helperPath = joinPaths(testDir, "lib/helper.luau"); std::string sharedPath = joinPaths(testDir, "shared.luau"); + std::string otherExample = joinPaths(testDir, "other/lib/example.luau"); + std::string otherModule = joinPaths(testDir, "other/module.luau"); + std::string dep = joinPaths(testDir, "dep/option.luau"); // Verify all expected files were discovered CHECK(tracer.containsAbsolute(mainPath)); CHECK(tracer.containsAbsolute(utilsPath)); CHECK(tracer.containsAbsolute(helperPath)); CHECK(tracer.containsAbsolute(sharedPath)); + CHECK(tracer.containsAbsolute(otherExample)); + CHECK(tracer.containsAbsolute(otherModule)); + CHECK(tracer.containsAbsolute(dep)); + + + // Verify .luaurc file was discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 2); + CHECK(luaurcFiles.find(".luaurc") != nullptr); + CHECK(luaurcFiles.find("other/.luaurc") != nullptr); // Verify the graph can be printed without errors (visual inspection of output) tracer.printRequireGraph(); @@ -187,10 +225,12 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_bundle_paths_and_contains") // Get discovered files as pairs auto pairs = tracer.getStaticRequirePairs(); - REQUIRE(pairs.size() == 4); + REQUIRE(pairs.size() == 7); // Verify bundle paths (without common prefix) - std::vector expectedBundlePaths = {"main.luau", "utils.luau", "lib/helper.luau", "shared.luau"}; + std::vector expectedBundlePaths = { + "main.luau", "utils.luau", "lib/helper.luau", "shared.luau", "dep/option.luau", "other/lib/example.luau", "other/module.luau" + }; for (const auto& expected : expectedBundlePaths) { @@ -216,4 +256,54 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_bundle_paths_and_contains") // Test non-existent module std::string nonExistentAbsolute = joinPaths(commonRoot, "nonexistent.luau"); CHECK(!tracer.containsAbsolute(nonExistentAbsolute)); + + // Verify .luaurc file was discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 2); + CHECK(luaurcFiles.find(".luaurc") != nullptr); + CHECK(luaurcFiles.find("other/.luaurc") != nullptr); +} + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_multiple_luaurc_files") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "main.luau"); + + StaticRequireTracer tracer{getReporter()}; + tracer.trace(entryPoint); + + // Get discovered files as pairs + auto pairs = tracer.getStaticRequirePairs(); + REQUIRE(pairs.size() == 7); // main, utils, lib/helper, shared, other/module, other/lib/example, dep/option + + // Verify all expected files are discovered including those in the other/ directory + std::vector expectedBundlePaths = { + "main.luau", "utils.luau", "lib/helper.luau", "shared.luau", "other/module.luau", "other/lib/example.luau" + }; + + for (const auto& expected : expectedBundlePaths) + { + bool found = false; + for (const auto& [bundlePath, absolutePath] : pairs) + { + if (bundlePath == expected) + { + found = true; + break; + } + } + CHECK(found); + } + + // Verify both .luaurc files were discovered + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 2); + + // Check that both .luaurc files exist in the map + const std::string* rootLuaurc = luaurcFiles.find(".luaurc"); + REQUIRE(rootLuaurc != nullptr); + + const std::string* otherLuaurc = luaurcFiles.find("other/.luaurc"); + REQUIRE(otherLuaurc != nullptr); } diff --git a/tests/src/staticrequires/.luaurc b/tests/src/staticrequires/.luaurc new file mode 100644 index 000000000..ceb0531c6 --- /dev/null +++ b/tests/src/staticrequires/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "example": "./dep" + } +} diff --git a/tests/src/staticrequires/dep/option.luau b/tests/src/staticrequires/dep/option.luau new file mode 100644 index 000000000..8df485807 --- /dev/null +++ b/tests/src/staticrequires/dep/option.luau @@ -0,0 +1,3 @@ +local v = { file = "option.luau" } + +return v diff --git a/tests/src/staticrequires/main.luau b/tests/src/staticrequires/main.luau index 6e9405843..3c9e2be1c 100644 --- a/tests/src/staticrequires/main.luau +++ b/tests/src/staticrequires/main.luau @@ -1,6 +1,11 @@ +-- test that you can require in an alias aware fashion +local example = require("@example/option") +print(`Required {example.file} from alias @example`) + -- Entry point that requires other modules local utils = require("./utils") local lib = require("./lib/helper") +local otherModule = require("./other/module") local process = require("@std/process") local path = require("@std/path") @@ -11,5 +16,6 @@ print("Main module running from: " .. cwd) return { utils = utils, lib = lib, + otherModule = otherModule, cwd = cwd, } diff --git a/tests/src/staticrequires/other/.luaurc b/tests/src/staticrequires/other/.luaurc new file mode 100644 index 000000000..82ecf5199 --- /dev/null +++ b/tests/src/staticrequires/other/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "otherlib": "./lib" + } +} diff --git a/tests/src/staticrequires/other/lib/example.luau b/tests/src/staticrequires/other/lib/example.luau new file mode 100644 index 000000000..b0d5a85f6 --- /dev/null +++ b/tests/src/staticrequires/other/lib/example.luau @@ -0,0 +1,3 @@ +local v = { file = "example.luau" } + +return v diff --git a/tests/src/staticrequires/other/module.luau b/tests/src/staticrequires/other/module.luau new file mode 100644 index 000000000..398e3dd2d --- /dev/null +++ b/tests/src/staticrequires/other/module.luau @@ -0,0 +1,7 @@ +-- test that we can require using an alias from other/.luaurc +local example = require("@otherlib/example") +print(`Required {example.file} from alias @otherlib`) + +return { + example = example, +} From 2e2a2812d2f8831f11e356790e6e5d9e501ee206 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 9 Dec 2025 13:38:26 -0800 Subject: [PATCH 226/642] Adds a global ignore directive to `lute lint` (#664) Adds a global ignore directive to lute lint, which is only parsed if included at the top of a file. --- lute/cli/commands/lint/init.luau | 26 ++++++++++++++++--- tests/cli/lint.test.luau | 44 ++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 175365ab1..9d535687e 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -139,9 +139,10 @@ end local function violationIsSuppressed( violation: types.LintViolation, ast: syntax.AstStatBlock, - directiveCache: { [syntax.AstNode]: { [string]: boolean } } + directiveCache: { [syntax.AstNode]: { [string]: boolean } }, + globalIgnores: { [string]: true } ): boolean - local violationSuppressed = false + local violationSuppressed = globalIgnores[violation.lintname] == true -- We want to find the lint directive with the tightest scope that applies to this violation local function nodeIsSuppressed(node: syntax.AstNode): boolean @@ -183,6 +184,20 @@ local function violationIsSuppressed( return violationSuppressed end +local function parseGlobalIgnores(trivia: { syntax.Trivia }): { [string]: true } + local globalIgnores: { [string]: true } = {} + + for _, triv in trivia do + if triv.tag == "blockcomment" or triv.tag == "comment" then + for rule in triv.text:gmatch("lute%-lint%-global%-ignore%((.+)%)") do + globalIgnores[rule] = true + end + end + end + + return globalIgnores +end + local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule }): { types.LintViolation } if VERBOSE then print(`Reading input file '{inputFilePath}'`) @@ -194,6 +209,11 @@ local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule end local ast = syntax.parseblock(fileContent) + if VERBOSE then + print(`Parsing global lint ignores in '{inputFilePath}'`) + end + local globalIgnores = parseGlobalIgnores(triviaUtils.leftmosttrivia(ast)) + if VERBOSE then print("Applying lint rules") end @@ -204,7 +224,7 @@ local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule if success then -- On success, err contains the returned violations for _, violation in err do - if not violationIsSuppressed(violation, ast, directiveCache) then + if not violationIsSuppressed(violation, ast, directiveCache, globalIgnores) then table.insert(violations, violation) end end diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 7e1bce0f9..7941d7581 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -1007,10 +1007,50 @@ violator.luau:1:11-16 ── expected = [[ violator.luau:6:12-18 ── │ - 6 │ local y = 10 / 0 - │ ^^^^^^ + 6 │ local y = 10 / 0 + │ ^^^^^^ │ ]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("lute lint global ignore directive", function(assert) + -- Create a file that violates the default almost_swapped and divide_by_zero rules + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +-- lute-lint-global-ignore(divide_by_zero) +local x = 1 / 0 +-- lute-lint-report(divide_by_zero) +if true then + local z = 3 / 0 + -- lute-lint-ignore(divide_by_zero) + local y = 10 / 0 +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + + local expected = [[ +violator.luau:5:12-17 ── + │ + 5 │ local z = 3 / 0 + │ ^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) -- Teardown fs.remove(violatorPath) From 6730315ed2200ec6091c6fa179c5b8190ed9c666 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 9 Dec 2025 13:40:29 -0800 Subject: [PATCH 227/642] Ensures that do blocks are seralized (#662) Now that we have Luau properly parsing do blocks into the CST, we can do the work of serializing them in lute. Addresses #581 --- definitions/luau.luau | 10 +++++++++ lute/cli/src/luauflags.cpp | 1 + lute/luau/src/luau.cpp | 32 +++++++++++++++++++++----- lute/std/libs/syntax/init.luau | 2 ++ lute/std/libs/syntax/types.luau | 2 ++ lute/std/libs/syntax/visitor.luau | 15 +++++++++++++ tests/std/syntax/parser.test.luau | 37 +++++++++++++++++++++---------- 7 files changed, 82 insertions(+), 17 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 38bb34a55..231ddac1b 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -274,6 +274,15 @@ export type AstStatBlock = { statements: { AstStat }, } +export type AstStatDo = { + location: span, + kind: "stat", + tag: "do", + dokeyword: Token<"do">, + body: { AstStat }, -- TODO: change to AstStatBlock when Luau adds AstStatDo on the C++ side + endkeyword: Token<"end">, +} + export type AstElseIfStat = { elseifkeyword: Token<"elseif">, condition: AstExpr, @@ -439,6 +448,7 @@ export type AstStatTypeFunction = { export type AstStat = { kind: "stat" } & ( | AstStatBlock + | AstStatDo | AstStatIf | AstStatWhile | AstStatRepeat diff --git a/lute/cli/src/luauflags.cpp b/lute/cli/src/luauflags.cpp index b461cf844..8de427da6 100644 --- a/lute/cli/src/luauflags.cpp +++ b/lute/cli/src/luauflags.cpp @@ -22,4 +22,5 @@ static void setLuauFlag(std::string_view name, bool state) void setLuauFlags() { setLuauFlag("LuauAutocompleteAttributes", true); + setLuauFlag("LuauCstStatDoWithStatsStart", true); } diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 3979dc3b5..2f14c294e 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1252,13 +1252,35 @@ struct AstSerialize : public Luau::AstVisitor void serializeStat(Luau::AstStatBlock* node) { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 1); + const auto cstNode = cstNodeMap.find(node); + const Luau::CstStatDo* cstDo = cstNode ? (*cstNode)->as() : nullptr; + + if (cstDo) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); - serializeNodePreamble(node, "block", "stat"); + serializeNodePreamble(node, "do", "stat"); - serializeStats(node->body); - lua_setfield(L, -2, "statements"); + serializeToken(node->location.begin, "do"); + lua_setfield(L, -2, "dokeyword"); + + serializeStats(node->body); + lua_setfield(L, -2, "body"); + + serializeToken(cstDo->endPosition, "end"); + lua_setfield(L, -2, "endkeyword"); + } + else + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node, "block", "stat"); + + serializeStats(node->body); + lua_setfield(L, -2, "statements"); + } } void serializeStat(Luau::AstStatIf* node) diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index 5090315d1..246f127d3 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -89,6 +89,8 @@ export type AstExpr = types.AstExpr export type AstStatBlock = types.AstStatBlock +export type AstStatDo = types.AstStatDo + export type AstElseIfStat = types.AstElseIfStat export type AstStatIf = types.AstStatIf diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index 29026454a..bb3be3adc 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -69,6 +69,8 @@ export type AstExpr = luau.AstExpr export type AstStatBlock = luau.AstStatBlock +export type AstStatDo = luau.AstStatDo + export type AstElseIfStat = luau.AstElseIfStat export type AstStatIf = luau.AstStatIf diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index e228a6903..a513ee860 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -9,6 +9,7 @@ local visitorlib = {} export type Visitor = { visitBlock: (types.AstStatBlock) -> boolean, visitBlockEnd: (types.AstStatBlock) -> (), + visitStatDo: (types.AstStatDo) -> boolean, visitIf: (types.AstStatIf) -> boolean, visitWhile: (types.AstStatWhile) -> boolean, visitRepeat: (types.AstStatRepeat) -> boolean, @@ -78,6 +79,7 @@ end local defaultVisitor: Visitor = { visitBlock = alwaysVisit :: any, visitBlockEnd = alwaysVisit :: any, + visitStatDo = alwaysVisit :: any, visitIf = alwaysVisit :: any, visitWhile = alwaysVisit :: any, visitRepeat = alwaysVisit :: any, @@ -179,6 +181,16 @@ local function visitBlock(block: types.AstStatBlock, visitor: Visitor) end end +local function visitStatDo(node: types.AstStatDo, visitor: Visitor) + if visitor.visitStatDo(node) then + visitToken(node.dokeyword, visitor) + for _, statement in node.body do + visitStatement(statement, visitor) + end + visitToken(node.endkeyword, visitor) + end +end + local function visitIf(node: types.AstStatIf, visitor: Visitor) if visitor.visitIf(node) then visitToken(node.ifkeyword, visitor) @@ -838,6 +850,8 @@ end function visitStatement(statement: types.AstStat, visitor: Visitor) if statement.tag == "block" then visitBlock(statement, visitor) + elseif statement.tag == "do" then + visitStatDo(statement, visitor) elseif statement.tag == "conditional" then visitIf(statement, visitor) elseif statement.tag == "expression" then @@ -920,6 +934,7 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor return { visitBlock = visit, visitBlockEnd = visit, + visitStatDo = visit, visitIf = visit, visitWhile = visit, visitRepeat = visit, diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 38d65ccf4..9e9dd452f 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -8,7 +8,7 @@ local syntax = require("@std/syntax") local T = require("@std/luau") local function assertEqualsLocation( - assert: asserts.Asserts, + assert: asserts.asserts, actual: T.span, startLine: number, startColumn: number, @@ -122,23 +122,24 @@ test.suite("Parser", function(suite) assert.eq(leadingToken.leadingtrivia[2].text, "\n") end) - suite:case("roundtrippableAst", function(assert) - local function visitDirectory(directory: string) - for _, entry in fs.listdirectory(directory) do - if entry.type ~= "file" then - continue - end + local function visitDirectory(directory: string) + for _, entry in fs.listdirectory(directory) do + if entry.type ~= "file" then + continue + end + + suite:case(`roundtrippable_Ast_{entry.name}`, function(assert) local p = path.join(directory, entry.name) local source = fs.readfiletostring(path.format(p)) local result = printer.printfile(parser.parse(source)) assert.eq(source, result) - end + end) end + end - visitDirectory("examples") - visitDirectory("tests/parserExamples") - end) + visitDirectory("examples") + visitDirectory("tests/parserExamples") suite:case("lineOffsetsField", function(assert) local singleLineCode = "local x = 1" @@ -500,7 +501,7 @@ test.suite("parseExpr", function(suite) end) end) -local function checkIsBlock(node: any, assert: asserts.Asserts) +local function checkIsBlock(node: any, assert: asserts.asserts) assert.eq(node.kind, "stat") assert.eq(node.tag, "block") end @@ -512,6 +513,18 @@ test.suite("parse", function(suite) assert.eq(#block.statements, 0) end) + suite:case("parseDoStatement", function(assert) + local block = parser.parseblock("do print('Hello, World!') end") + checkIsBlock(block, assert) + assert.eq(#block.statements, 1) + + local doStat = block.statements[1] :: syntax.AstStatDo + assert.eq(doStat.tag, "do") + assert.eq(doStat.dokeyword.text, "do") + assert.eq(#doStat.body, 1) + assert.eq(doStat.endkeyword.text, "end") + end) + suite:case("parseIfStatement", function(assert) local block = parser.parseblock( "if x > 0 then print('positive') elseif x < 0 then print('negative') else print('zero') end" From 0c0d643fffc5186d65239afd0cee2c7ebbbc0b79 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:42:01 -0800 Subject: [PATCH 228/642] adds timeout for `run-lutecli-luau-tests` workflow in CI (#665) This is because `fs_watch` test is hanging, see https://github.com/luau-lang/lute/issues/639 and https://github.com/luau-lang/lute/actions/runs/20073090896/job/57580297775 Setting a 15 minute timeout per discussion but still working on actual fix for fs.watch test (leaving unlabeled to not include in release notes) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6749dc566..7f5ae7ca1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ concurrency: jobs: run-lutecli-luau-tests: runs-on: ${{ matrix.os }} + timeout-minutes: 15 strategy: matrix: From 3589bf434f868476a3368ef6e99721dc99f578e4 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 9 Dec 2025 13:42:39 -0800 Subject: [PATCH 229/642] Implements flag arguments to specify test case and test suite to run for `lute test` (#558) This PR adds support for lute test to run specific tests: ``` lute test --suite SomeSuite lute test -s SomeSuite -- runs SomeSuite lute test --case some_case lute test -c some_case -- runs some_case lute test -s Suite -c case -- runs Suite.case lute test -s "ABC" -c "some test with spaces" -- runs `ABC.some test with spaces` ``` --- lute/cli/commands/test/filter.luau | 71 ++++++++++++++++++++++++++++++ lute/cli/commands/test/finder.luau | 39 +++++----------- lute/cli/commands/test/init.luau | 50 +++++++++++++++------ lute/std/libs/test/init.luau | 34 +++++++++++--- lute/std/libs/test/types.luau | 8 +++- tests/cli/test.test.luau | 32 +++++++++++++- 6 files changed, 185 insertions(+), 49 deletions(-) create mode 100644 lute/cli/commands/test/filter.luau diff --git a/lute/cli/commands/test/filter.luau b/lute/cli/commands/test/filter.luau new file mode 100644 index 000000000..7d045ba9c --- /dev/null +++ b/lute/cli/commands/test/filter.luau @@ -0,0 +1,71 @@ +local testtypes = require("@std/test/types") + +local function filtertests( + env: testtypes.testenvironment, + suiteName: string?, + caseName: string? +): testtypes.testenvironment + local filtered: testtypes.testenvironment = { + anonymous = {}, + suites = {}, + suiteindex = {}, + caseindex = {}, + } + + -- No filters: return all tests + if not suiteName and not caseName then + return env + end + + -- Filter by both suite and case name + if suiteName and caseName then + local caseEntry = env.caseindex[caseName] + if caseEntry and caseEntry.suites[suiteName] then + local suite = env.suiteindex[suiteName] + if suite then + local filteredSuite = { + name = suite.name, + cases = caseEntry.suites[suiteName], + _beforeeach = suite._beforeeach, + _beforeall = suite._beforeall, + _aftereach = suite._aftereach, + _afterall = suite._afterall, + } + table.insert(filtered.suites, filteredSuite) + end + end + + -- Filter by suite name only + elseif suiteName then + local suite = env.suiteindex[suiteName] + if suite then + table.insert(filtered.suites, suite) + end + elseif caseName then + local caseEntry = env.caseindex[caseName] + if caseEntry then + -- Add all anonymous tests with this case name + filtered.anonymous = caseEntry.anonymous + + -- Add all suites that have tests with this case name + for sName, cases in caseEntry.suites do + local suite = env.suiteindex[sName] + if suite then + local filteredSuite = { + name = suite.name, + cases = cases, + _beforeeach = suite._beforeeach, + _beforeall = suite._beforeall, + _aftereach = suite._aftereach, + _afterall = suite._afterall, + } + table.insert(filtered.suites, filteredSuite) + end + end + end + end + + return filtered +end + +return table.freeze({ filtertests = filtertests }) diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index c13b2b025..318b8ec11 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -8,37 +8,22 @@ local function istestfile(filename: string): boolean return stringext.hassuffix(filename, ".test.luau") or stringext.hassuffix(filename, ".spec.luau") end -local function findtestfilesrec(directory: path.path): { path.path } - local results = {} - - for _, entry in fs.listdirectory(directory) do - local fullPath = path.join(directory, entry.name) - - if entry.type == "dir" then - tableext.extend(results, findtestfilesrec(fullPath)) - elseif istestfile(entry.name) then - table.insert(results, path.normalize(fullPath)) - end - end - - return results -end - local function findtestfiles(paths: { string }): { path.path } local files = {} - local cwd = ps.cwd() - for _, filepath in paths do - local pathObj = path.parse(filepath) - - if not path.isabsolute(pathObj) then - pathObj = path.join(cwd, pathObj) + local function findfiles(p: path.pathlike) + local it = fs.walk(path.normalize(p), { recursive = true }) + local file = it() + while file do + if fs.type(file) ~= "dir" and istestfile(path.format(file)) then + table.insert(files, path.normalize(file)) + end + file = it() end + end - if fs.type(pathObj) == "dir" then - tableext.extend(files, findtestfilesrec(pathObj)) - elseif istestfile(filepath) then - table.insert(files, path.normalize(pathObj)) - end + local cwd = ps.cwd() + for _, p in paths do + findfiles(path.join(cwd, p)) end return files diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index 477813acb..9c73c9ded 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -1,4 +1,5 @@ local finder = require("@self/finder") +local filter = require("@self/filter") local luau = require("@std/luau") local path = require("@std/path") local ps = require("@std/process") @@ -16,16 +17,8 @@ Run tests discovered in .test.luau and .spec.luau files. OPTIONS: -h, --help Show this help message --list List all discovered test cases without running them - --filter=PATTERN Run only tests matching PATTERN - Examples: - --filter=SuiteName (run all tests in suite) - --filter=SuiteName.testname (run specific test) - --filter=testname (run anonymous test) - --reporter=REPORTER Choose test reporter (default: rich) - Options: - rich - Color output with visual diffs - simple - Plain text output - - Custom reporter from file path + -s, --suite=SUITE Run only tests in the specified suite + -c, --case=CASE Run only test cases matching the specified name PATHS: Directories or files to search for tests (default: ./tests) @@ -33,8 +26,9 @@ PATHS: EXAMPLES: lute test Run all tests in ./tests lute test --list List all test cases - lute test --filter=MyTestSuite Run all tests in MyTestSuite - lute test --reporter=simple src/ Run tests with simple reporter + lute test -s MyTestSuite Run all tests in MyTestSuite + lute test --suite MyTestSuite --case mytest Run specific test in suite + lute test --case "some case" Run all test cases named "some case" ]]) end @@ -91,6 +85,7 @@ end local function runtests(env: testtypes.testenvironment, runner: testtypes.testrunner, reporter: testtypes.testreporter) local result = runner(env) + reporter(result) if result.failed ~= 0 then ps.exit(1) @@ -102,17 +97,43 @@ local function main(...: string) local args = { ... } local testPaths = {} local isListMode = false + local suiteName: string? = nil + local caseName: string? = nil + + local i = 1 + while i <= #args do + local arg = args[i] - for _, arg in args do if arg == "-h" or arg == "--help" then printHelp() return elseif arg == "--list" then isListMode = true + i += 1 + elseif arg == "-s" or arg == "--suite" then + i += 1 + if i <= #args then + suiteName = args[i] + i += 1 + else + print(`Error: {arg} requires a value`) + ps.exit(1) + end + elseif arg == "-c" or arg == "--case" then + i += 1 + if i <= #args then + caseName = args[i] + i += 1 + else + print(`Error: {arg} requires a value`) + ps.exit(1) + end else table.insert(testPaths, arg) + i += 1 end end + local searchpath = if #testPaths > 0 then testPaths else { "./tests" } loadtests(finder.findtestfiles(searchpath)) @@ -120,7 +141,8 @@ local function main(...: string) listtests() else local env = test._registered() - runtests(env, runner.run, reporter.simple) + local filteredEnv = filter.filtertests(env, suiteName, caseName) + runtests(filteredEnv, runner.run, reporter.simple) end end diff --git a/lute/std/libs/test/init.luau b/lute/std/libs/test/init.luau index 058843372..eaf5ca4d4 100644 --- a/lute/std/libs/test/init.luau +++ b/lute/std/libs/test/init.luau @@ -49,19 +49,22 @@ function TestSuite:afterall(hook: () -> ()) self._afterall = hook end -local env: TestEnvironment = { anonymous = {}, suites = {}, reporter = reporter.simple } +local env: TestEnvironment = { anonymous = {}, suites = {}, suiteindex = {}, caseindex = {} } -- Test library export surface local test = {} -function test.setreporter(reporter: TestReporter) - test.reporter = reporter -end - -- register an anonymous test case function test.case(name: string, case: Test) local testcase = { name = name, case = case } table.insert(env.anonymous, testcase) + + -- Update caseindex + if not env.caseindex[name] then + env.caseindex[name] = { anonymous = { testcase }, suites = {} } + else + table.insert(env.caseindex[name].anonymous, testcase) + end end -- register a test suite @@ -69,11 +72,30 @@ function test.suite(name: string, registerFn: (TestSuite) -> ()) local suite = TestSuite.new(name) registerFn(suite) table.insert(env.suites, suite) + + -- Update suiteindex + env.suiteindex[name] = suite + + -- Update caseindex for all cases in this suite + for _, testcase in suite.cases do + if not env.caseindex[testcase.name] then + env.caseindex[testcase.name] = { anonymous = {}, suites = {} } + end + if not env.caseindex[testcase.name].suites[name] then + env.caseindex[testcase.name].suites[name] = {} + end + table.insert(env.caseindex[testcase.name].suites[name], testcase) + end end -- get all registered tests without running them (internal) function test._registered() - return table.freeze({ anonymous = env.anonymous, suites = env.suites }) + return table.freeze({ + anonymous = env.anonymous, + suites = env.suites, + suiteindex = env.suiteindex, + caseindex = env.caseindex, + }) end -- run all the tests diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index 243642dd0..edc5e596f 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -41,10 +41,16 @@ export type testrunresult = { export type testreporter = (testrunresult) -> () -- Testing Environment +export type caseindexentry = { + anonymous: { testcase }, + suites: { [string]: { testcase } }, +} + export type testenvironment = { anonymous: { testcase }, suites: { testsuite }, - reporter: testreporter, + suiteindex: { [string]: testsuite }, + caseindex: { [string]: caseindexentry }, } export type testrunner = (testenvironment) -> testrunresult diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 98d5dfefb..09ecc4dc8 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -32,7 +32,7 @@ test.suite("LuteTestCommand", function(suite) assert.eq(result.exitcode, 0) end) - suite:case("lute test discovers test files in custom directory", function(assert) + suite:case("lute_test_discovery_custom_directory", function(assert) -- Run lute test with custom directory path local result = process.run({ lutePath, "test", "--list", "tests/cli/discovery" }) @@ -40,6 +40,36 @@ test.suite("LuteTestCommand", function(suite) assert.eq(result.exitcode, 0) assert.eq(expected, result.stdout) end) + + suite:case("filter_by_suite_name", function(assert) + -- Run lute test with --suite flag + local result = process.run({ lutePath, "test", "-s", "SmokeSuite", "tests/cli/discovery" }) + + -- Check that it runs only tests in SmokeSuite + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Total: 2", 1, true), nil) + assert.neq(result.stdout:find("Passed: 2", 1, true), nil) + end) + + suite:case("lute test filters by case name", function(assert) + -- Run lute test with --case flag + local result = process.run({ lutePath, "test", "--case", "make_pass", "tests/cli/discovery" }) + + -- Check that it runs only tests named make_pass + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Total: 1", 1, true), nil) + assert.neq(result.stdout:find("Passed: 1", 1, true), nil) + end) + + suite:case("lute test filters by suite and case name", function(assert) + -- Run lute test with both --suite and --case flags + local result = process.run({ lutePath, "test", "-s", "SmokeSuite", "-c", "make_fail", "tests/cli/discovery" }) + + -- Check that it runs only make_fail in SmokeSuite + assert.eq(result.exitcode, 0) + assert.neq(result.stdout:find("Total: 1", 1, true), nil) + assert.neq(result.stdout:find("Passed: 1", 1, true), nil) + end) end) test.run() From eef10e92d39b523ef7032e4c77eea17bfa3e288d Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 9 Dec 2025 13:53:25 -0800 Subject: [PATCH 230/642] Surfaces some require errors in `lute run` (#660) I was running into a scenario where I was trying to `lute run` a file in a directory containing a folder with the same name as the file (ie `scratch.luau` and a folder called `scratch`). This was causing an ambiguous require, but lute would just report that `scratch.luau does not exist`, which was quite frustrating. I've added some piping to bubble the error up the stack, plus a test. --- lute/cli/src/climain.cpp | 54 ++++++++++++++++++++++++++-------------- tests/cli/run.test.luau | 42 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 tests/cli/run.test.luau diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index f3846e7d9..7141a912d 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -43,7 +43,6 @@ #include #endif -#include #include #include #include @@ -318,7 +317,8 @@ static int assertionHandler(const char* expr, const char* file, int line, const return 1; } -static std::optional getWithRequireByStringSemantics(std::string filePath) +// Returns whether the filePath could be resolved to a valid file path, and a string containing either the valid path or an error message +static std::pair getWithRequireByStringSemantics(std::string filePath) { std::string normalized = normalizePath(std::move(filePath)); @@ -332,26 +332,40 @@ static std::optional getWithRequireByStringSemantics(std::string fi std::optional mp = ModulePath::create(std::move(rootOfPath), std::move(restOfPath), isFile, isDirectory); if (!mp) - return std::nullopt; + return {false, "Could not initialize ModulePath instance."}; ResolvedRealPath resolved = mp->getRealPath(); - if (resolved.status != NavigationStatus::Success) - return std::nullopt; - if (resolved.type == ResolvedRealPath::PathType::File) - return resolved.realPath; - - return std::nullopt; + std::pair result; + switch (resolved.status) + { + case NavigationStatus::Success: + if (resolved.type == ResolvedRealPath::PathType::File) + result = {true, resolved.realPath}; + else + result = {false, "Path is a directory, not a file."}; + break; + case NavigationStatus::Ambiguous: + result = {false, "Unable to tell whether path is a file or directory. Is there a same-named file or directory?"}; + break; + case NavigationStatus::NotFound: + result = {false, "File or directory not found."}; + break; + } + + return result; }; -static std::optional getValidPath(std::string filePath) +// Returns whether the filePath could be resolved to a valid file path, and a string containing either the valid path or an error message +static std::pair getValidPath(std::string filePath) { - if (std::optional path = getWithRequireByStringSemantics(filePath)) - return *path; + auto [ok, res] = getWithRequireByStringSemantics(filePath); + if (ok) + return {true, res}; // Only fallback to checking .lute/* if the original path has no extension. if (filePath.find('.') != std::string::npos) - return std::nullopt; + return {false, res}; std::string fallbackPath = joinPaths(".lute", filePath); size_t fallbackSize = fallbackPath.size(); @@ -362,10 +376,11 @@ static std::optional getValidPath(std::string filePath) fallbackPath += ext; if (isFile(fallbackPath)) - return fallbackPath; + return {true, fallbackPath}; } - return std::nullopt; + + return {false, res}; } int handleRunCommand(int argc, char** argv, int argOffset, LuteReporter& reporter) @@ -408,14 +423,15 @@ int handleRunCommand(int argc, char** argv, int argOffset, LuteReporter& reporte Runtime runtime; lua_State* L = setupCliState(runtime); - std::optional validPath = getValidPath(filePath); - if (!validPath) + auto [ok, validPath] = getValidPath(filePath); + if (!ok) { - reporter.formatError("Error: File '%s' does not exist.", filePath.c_str()); + reporter.formatError("Error while resolving filepath '%s': %s", filePath.c_str(), validPath.c_str()); + return 1; } - bool success = runFile(runtime, validPath->c_str(), L, program_argc, program_argv, reporter); + bool success = runFile(runtime, validPath.c_str(), L, program_argc, program_argv, reporter); return success ? 0 : 1; } diff --git a/tests/cli/run.test.luau b/tests/cli/run.test.luau new file mode 100644 index 000000000..33234129f --- /dev/null +++ b/tests/cli/run.test.luau @@ -0,0 +1,42 @@ +local fs = require("@std/fs") +local pathlib = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local tmpdir = system.tmpdir() + +local rundir = pathlib.join(tmpdir, "lute-run") + +local lutePath = pathlib.format(process.execpath()) + +test.suite("lute-run", function(suite) + suite:beforeeach(function() + if fs.exists(rundir) then + fs.removedirectory(rundir, { recursive = true }) + end + end) + + suite:case("same-named file and directory", function(assert) + -- Setup + fs.createdirectory(rundir) + fs.createdirectory(pathlib.join(rundir, "hello")) + + local helloFilePath = pathlib.join(rundir, "hello.luau") + fs.writestringtofile(helloFilePath, "print('Hello world')") + + -- Execute + local result = process.run({ lutePath, pathlib.format(helloFilePath) }) + + assert.eq(result.exitcode, 1) + assert.neq( + result.stderr:find( + `Error while resolving filepath '.+{if system.win32 then "\\" else "/"}hello%.luau': Unable to tell whether path is a file or directory%. Is there a same%-named file or directory%?` + ), + nil + ) + assert.strnotcontains(result.stdout, "Hello world") + end) +end) + +test.run() From 4defe3c7552439f11248968c01db918ae83acca3 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 9 Dec 2025 15:10:29 -0800 Subject: [PATCH 231/642] Cleans up the std path library and adds `path.relative` (#658) I cleaned up the types in paths so that `path = setmetatable`, which I think is a little more digestible. I also added a `path.relative` function, which I need to get my gitignore fixes working on Windows --- lute/std/libs/path/init.luau | 8 + lute/std/libs/path/pathinterface.luau | 5 + lute/std/libs/path/posix/init.luau | 102 ++++++----- lute/std/libs/path/posix/types.luau | 10 +- lute/std/libs/path/win32/init.luau | 179 ++++++++++--------- lute/std/libs/path/win32/types.luau | 14 +- tests/std/path/path.posix.test.luau | 86 ++++++--- tests/std/path/path.win32.test.luau | 247 ++++++++++++++++---------- tests/std/process.test.luau | 15 +- 9 files changed, 402 insertions(+), 264 deletions(-) create mode 100644 lute/std/libs/path/pathinterface.luau diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau index 7e53b3462..78555415a 100644 --- a/lute/std/libs/path/init.luau +++ b/lute/std/libs/path/init.luau @@ -76,6 +76,14 @@ function pathlib.parse(path: pathlike): path end end +function pathlib.relative(from: pathlike, to: pathlike): path + if onWindows then + return win32.relative(from :: win32.pathlike, to :: win32.pathlike) + else + return posix.relative(from :: posix.pathlike, to :: posix.pathlike) + end +end + function pathlib.resolve(...: pathlike): path if onWindows then return win32.resolve(...) diff --git a/lute/std/libs/path/pathinterface.luau b/lute/std/libs/path/pathinterface.luau new file mode 100644 index 000000000..4bd143b01 --- /dev/null +++ b/lute/std/libs/path/pathinterface.luau @@ -0,0 +1,5 @@ +export type pathinterface = { + __tostring: () -> string, +} + +return table.freeze({}) diff --git a/lute/std/libs/path/posix/init.luau b/lute/std/libs/path/posix/init.luau index 4f939d4ec..0652b0b64 100644 --- a/lute/std/libs/path/posix/init.luau +++ b/lute/std/libs/path/posix/init.luau @@ -1,26 +1,19 @@ local process = require("@lute/process") local posixtypes = require("@self/types") -export type path = setmetatable +export type path = posixtypes.path export type pathlike = posixtypes.pathlike local posix = {} -local path_mt = {} +posix.pathmt = {} :: posixtypes.pathMT function posix.basename(path: pathlike): string? - if typeof(path) == "string" then - path = posix.parse(path) - end - path = path :: path + path = if typeof(path) == "string" then posix.parse(path) else path return if #path.parts > 0 then path.parts[#path.parts] else nil end function posix.dirname(path: pathlike): string - if typeof(path) == "string" then - path = posix.parse(path) - end - - path = path :: path + path = if typeof(path) == "string" then posix.parse(path) else path if #path.parts == 0 then return posix.format(path) @@ -33,11 +26,7 @@ function posix.dirname(path: pathlike): string end function posix.extname(path: pathlike): string - if typeof(path) == "string" then - path = posix.parse(path) - end - - path = path :: path + path = if typeof(path) == "string" then posix.parse(path) else path if #path.parts == 0 then return "" @@ -62,8 +51,6 @@ function posix.format(path: pathlike): string return path end - path = path :: path - if #path.parts == 0 then return if path.absolute then "/" else "." else @@ -72,31 +59,20 @@ function posix.format(path: pathlike): string end function posix.isabsolute(path: pathlike): boolean - if typeof(path) == "string" then - path = posix.parse(path) - end - path = path :: path + path = if typeof(path) == "string" then posix.parse(path) else path return path.absolute end -- In place extends path with addend local function joinHelper(path: path, addend: pathlike): () - if typeof(addend) == "string" then - addend = posix.parse(addend) - end - addend = addend :: path + addend = if typeof(addend) == "string" then posix.parse(addend) else addend if addend.absolute then local stringPath = posix.format(path) local stringAddend = posix.format(addend) - error(table.concat({ - "Could not join ", - (if path.absolute then "absolute" else "relative"), - " path ", - stringPath, - " and absolute path ", - stringAddend, - })) + error( + `Could not join {if path.absolute then "absolute" else "relative"} path {stringPath} and absolute path {stringAddend}` + ) end table.move(addend.parts, 1, #addend.parts, #path.parts + 1, path.parts) @@ -108,15 +84,15 @@ function posix.join(...: pathlike): path return setmetatable({ parts = {}, absolute = false, - }, path_mt) + }, posix.pathmt) end local path: path = if typeof(parts[1]) == "string" then posix.parse(parts[1]) else setmetatable({ - parts = table.clone((parts[1] :: path).parts), - absolute = (parts[1] :: path).absolute, - }, path_mt) + parts = table.clone((parts[1] :: path).parts), -- LUAUFIX: Shouldn't need cast + absolute = (parts[1] :: path).absolute, -- LUAUFIX: Shouldn't need cast + }, posix.pathmt) for i = 2, #parts do joinHelper(path, parts[i]) @@ -126,10 +102,7 @@ function posix.join(...: pathlike): path end function posix.normalize(path: pathlike): path - if typeof(path) == "string" then - path = posix.parse(path) - end - path = path :: path + path = if typeof(path) == "string" then posix.parse(path) else path local newParts = {} for _, part in path.parts do @@ -155,12 +128,12 @@ function posix.normalize(path: pathlike): path return setmetatable({ parts = newParts, absolute = path.absolute, - }, path_mt) + }, posix.pathmt) end function posix.parse(path: pathlike): path if typeof(path) == "table" then - return setmetatable(path, path_mt) + return setmetatable(path, posix.pathmt) end if typeof(path) ~= "string" then error("Expected string or path") @@ -192,7 +165,7 @@ function posix.parse(path: pathlike): path return setmetatable({ parts = parts, absolute = isAbs, - }, path_mt) + }, posix.pathmt) end function posix.resolve(...: pathlike): path @@ -227,7 +200,44 @@ function posix.resolve(...: pathlike): path return posix.normalize(path) end -function path_mt:__tostring(): string +function posix.relative(from: pathlike, to: pathlike): path + from = if typeof(from) == "string" then posix.parse(from) else from + to = if typeof(to) == "string" then posix.parse(to) else to + + if from.absolute ~= to.absolute then + error("Cannot compute relative path between absolute and relative paths") + end + + local commonPrefixLength = 0 + + for i = 1, math.min(#from.parts, #to.parts) do + if from.parts[i] ~= to.parts[i] then + break + end + commonPrefixLength += 1 + end + + local relativeParts = {} + + if #from.parts > commonPrefixLength then + for i = commonPrefixLength + 1, #from.parts do + table.insert(relativeParts, "..") + end + end + + if #to.parts > commonPrefixLength then + for i = commonPrefixLength + 1, #to.parts do + table.insert(relativeParts, to.parts[i]) + end + end + + return setmetatable({ + parts = relativeParts, + absolute = false, + }, posix.pathmt) +end + +function posix.pathmt:__tostring(): string return posix.format(self :: path) end diff --git a/lute/std/libs/path/posix/types.luau b/lute/std/libs/path/posix/types.luau index 25228496d..f158038ac 100644 --- a/lute/std/libs/path/posix/types.luau +++ b/lute/std/libs/path/posix/types.luau @@ -1,12 +1,12 @@ -export type pathinterface = { - __tostring: () -> string, -} +local pathinterface = require("../pathinterface") -export type path = { +export type pathdata = { parts: { string }, absolute: boolean, } -export type pathlike = string | path +export type path = setmetatable + +export type pathlike = string | path | pathdata return {} diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index 3f7c1a5e5..abf723d4f 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -2,26 +2,19 @@ local process = require("@lute/process") local win32types = require("@self/types") export type pathkind = win32types.pathkind -export type path = setmetatable +export type path = win32types.path export type pathlike = win32types.pathlike local win32 = {} -local path_mt = {} +win32.pathmt = {} :: win32types.pathMT function win32.basename(path: pathlike): string? - if typeof(path) == "string" then - path = win32.parse(path) - end - path = path :: path + path = if typeof(path) == "string" then win32.parse(path) else path return if #path.parts > 0 then path.parts[#path.parts] else nil end function win32.dirname(path: pathlike): string - if typeof(path) == "string" then - path = win32.parse(path) - end - - path = path :: path + path = if typeof(path) == "string" then win32.parse(path) else path if #path.parts == 0 then return win32.format(path) @@ -29,17 +22,13 @@ function win32.dirname(path: pathlike): string return win32.format({ parts = { table.unpack(path.parts, 1, #path.parts - 1) }, kind = path.kind, - driveLetter = path.driveLetter, + drive = path.drive, }) end end function win32.extname(path: pathlike): string - if typeof(path) == "string" then - path = win32.parse(path) - end - - path = path :: path + path = if typeof(path) == "string" then win32.parse(path) else path if #path.parts == 0 then return "" @@ -60,21 +49,20 @@ function win32.extname(path: pathlike): string end function win32.drive(path: pathlike): path - if typeof(path) == "string" then - path = win32.parse(path) - end - - path = path :: path + path = if typeof(path) == "string" then win32.parse(path) else path - if path.driveLetter == nil then + if path.drive == nil then error("Path does not have a drive letter: " .. win32.format(path)) end - return setmetatable({ - parts = {}, - kind = "absolute", - driveLetter = path.driveLetter, - }, path_mt) + return setmetatable( + { + parts = {}, + kind = "absolute", + drive = path.drive, + } :: win32types.pathdata, + win32.pathmt + ) end function win32.format(path: pathlike): string @@ -82,13 +70,11 @@ function win32.format(path: pathlike): string return path end - path = path :: path - if #path.parts == 0 then if path.kind == "unc" then return "\\\\" - elseif path.driveLetter ~= nil then - return table.concat({ path.driveLetter, ":", (if path.kind == "absolute" then "\\" else "") }) + elseif path.drive ~= nil then + return table.concat({ path.drive, ":", (if path.kind == "absolute" then "\\" else "") }) else return "." end @@ -96,8 +82,8 @@ function win32.format(path: pathlike): string local parts = table.concat(path.parts, "\\") if path.kind == "unc" then return "\\\\" .. parts - elseif path.driveLetter ~= nil then - return table.concat({ path.driveLetter, ":", (if path.kind == "absolute" then "\\" else ""), parts }) + elseif path.drive ~= nil then + return table.concat({ path.drive, ":", (if path.kind == "absolute" then "\\" else ""), parts }) else return parts end @@ -105,71 +91,53 @@ function win32.format(path: pathlike): string end function win32.isabsolute(path: pathlike): boolean - if typeof(path) == "string" then - path = win32.parse(path) - end - path = path :: path - return path.kind == "absolute" or path.kind == "unc" + path = if typeof(path) == "string" then win32.parse(path) else path + return path.kind ~= "relative" end -- In place extends path with addend local function joinHelper(path: path, addend: pathlike): () - if typeof(addend) == "string" then - addend = win32.parse(addend) - end - addend = addend :: path - - if addend.kind == "absolute" or addend.kind == "unc" then - local stringPath = win32.format(path) - local stringAddend = win32.format(addend) - error(table.concat({ - "Could not join ", - path.kind, - " path ", - stringPath, - " and ", - addend.kind, - " path ", - stringAddend, - })) + addend = if typeof(addend) == "string" then win32.parse(addend) else addend + + if addend.kind ~= "relative" then + error(`Could not join {path.kind} path {win32.format(path)} and {addend.kind} path {win32.format(addend)}`) end - local driveLettersIncompatible = path.driveLetter ~= nil - and addend.driveLetter ~= nil - and addend.driveLetter:lower() ~= path.driveLetter:lower() + local driveLettersIncompatible = path.drive ~= nil + and addend.drive ~= nil + and addend.drive:lower() ~= path.drive:lower() if driveLettersIncompatible then - local stringPath = win32.format(path) - local stringAddend = win32.format(addend) - error( - table.concat({ "Could not join paths with incompatible drive letters: ", stringPath, " and ", stringAddend }) - ) + error(`Could not join paths with incompatible drive letters: {win32.format(path)} and {win32.format(addend)}`) end table.move(addend.parts, 1, #addend.parts, #path.parts + 1, path.parts) - if path.driveLetter == nil and addend.driveLetter ~= nil then - path.driveLetter = addend.driveLetter + if path.drive == nil and addend.drive ~= nil then + path.drive = addend.drive end end function win32.join(...: pathlike): path local parts = { ... } if #parts == 0 then - return setmetatable({ - parts = {}, - kind = "relative", - driveLetter = nil, - }, path_mt) + return setmetatable( + { + parts = {}, + kind = "relative", + drive = nil, + } :: win32types.pathdata, -- Cast needed because of table invariance + win32.pathmt + ) end local path: path = if typeof(parts[1]) == "string" then win32.parse(parts[1]) else setmetatable({ - parts = table.clone((parts[1] :: path).parts), - kind = (parts[1] :: path).kind, - driveLetter = (parts[1] :: path).driveLetter, - }, path_mt) + parts = table.clone((parts[1] :: path).parts), -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] + kind = (parts[1] :: path).kind, -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] + drive = (parts[1] :: path).drive, -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] + }, win32.pathmt) for i = 2, #parts do joinHelper(path, parts[i]) @@ -179,10 +147,7 @@ function win32.join(...: pathlike): path end function win32.normalize(path: pathlike): path - if typeof(path) == "string" then - path = win32.parse(path) - end - path = path :: path + path = if typeof(path) == "string" then win32.parse(path) else path local newParts = {} for _, part in path.parts do @@ -208,13 +173,13 @@ function win32.normalize(path: pathlike): path return setmetatable({ parts = newParts, kind = path.kind, - driveLetter = path.driveLetter, - }, path_mt) + drive = path.drive, + }, win32.pathmt) end function win32.parse(path: pathlike): path if typeof(path) == "table" then - return setmetatable(path, path_mt) + return setmetatable(path, win32.pathmt) end if typeof(path) ~= "string" then error("Expected string or path") @@ -263,8 +228,50 @@ function win32.parse(path: pathlike): path return setmetatable({ parts = parts, kind = kind, - driveLetter = driveLetter, - }, path_mt) + drive = driveLetter, + }, win32.pathmt) +end + +function win32.relative(from: pathlike, to: pathlike): path + from = if typeof(from) == "string" then win32.parse(from) else from + to = if typeof(to) == "string" then win32.parse(to) else to + + if from.kind ~= to.kind then + error("Cannot compute relative path between different kinds of paths") + end + + if from.drive ~= to.drive then + error("Cannot compute relative path between different drives") + end + + local commonPrefixLength = 0 + + for i = 1, math.min(#from.parts, #to.parts) do + if from.parts[i] ~= to.parts[i] then + break + end + commonPrefixLength += 1 + end + + local relativeParts = {} + + if #from.parts > commonPrefixLength then + for i = commonPrefixLength + 1, #from.parts do + table.insert(relativeParts, "..") + end + end + + if #to.parts > commonPrefixLength then + for i = commonPrefixLength + 1, #to.parts do + table.insert(relativeParts, to.parts[i]) + end + end + + return setmetatable({ + parts = relativeParts, + kind = "relative", + drive = nil :: string?, + }, win32.pathmt) end function win32.resolve(...: pathlike): path @@ -299,7 +306,7 @@ function win32.resolve(...: pathlike): path return win32.normalize(path) end -function path_mt:__tostring(): string +function win32.pathmt:__tostring(): string return win32.format(self :: path) end diff --git a/lute/std/libs/path/win32/types.luau b/lute/std/libs/path/win32/types.luau index 470b3aa72..b07c6967b 100644 --- a/lute/std/libs/path/win32/types.luau +++ b/lute/std/libs/path/win32/types.luau @@ -1,15 +1,15 @@ -export type pathkind = "unc" | "absolute" | "relative" +local pathinterface = require("../pathinterface") -export type pathinterface = { - __tostring: () -> string, -} +export type pathkind = "unc" | "absolute" | "relative" -export type path = { +export type pathdata = { parts: { string }, kind: pathkind, - driveLetter: string?, + drive: string?, } -export type pathlike = string | path +export type path = setmetatable + +export type pathlike = string | pathdata | path return {} diff --git a/tests/std/path/path.posix.test.luau b/tests/std/path/path.posix.test.luau index c04db23bd..d9efafafb 100644 --- a/tests/std/path/path.posix.test.luau +++ b/tests/std/path/path.posix.test.luau @@ -43,10 +43,10 @@ test.suite("PathPosixParseSuite", function(suite) end) suite:case("parse_pathobj_returns_same", function(assert) - local originalPath: posix.path = { + local originalPath: posix.path = setmetatable({ parts = { "folder", "file.txt" }, absolute = false, - } + }, posix.pathmt) local result = path.posix.parse(originalPath) assert.eq(result, originalPath) end) @@ -84,10 +84,10 @@ test.suite("PathPosixBasenameSuite", function(suite) end) suite:case("basename_pathobj", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, absolute = false, - } + }, posix.pathmt) local result = path.posix.basename(pathobj) assert.eq(result, "file.txt") end) @@ -95,37 +95,37 @@ end) test.suite("PathPosixFormatSuite", function(suite) suite:case("format_posix_absolute_path", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = { "home", "user", "documents", "file.txt" }, absolute = true, - } + }, posix.pathmt) local result = path.posix.format(pathobj) assert.eq(result, "/home/user/documents/file.txt") end) suite:case("format_posix_relative_path", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = { "documents", "file.txt" }, absolute = false, - } + }, posix.pathmt) local result = path.posix.format(pathobj) assert.eq(result, "documents/file.txt") end) suite:case("format_empty_relative_path", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = {}, absolute = false, - } + }, posix.pathmt) local result = path.posix.format(pathobj) assert.eq(result, ".") end) suite:case("format_root_path", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = {}, absolute = true, - } + }, posix.pathmt) local result = path.posix.format(pathobj) assert.eq(result, "/") end) @@ -168,10 +168,10 @@ test.suite("PathPosixDirnameSuite", function(suite) end) suite:case("dirname_pathobj", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, absolute = false, - } + }, posix.pathmt) local result = path.posix.dirname(pathobj) assert.eq(result, "folder/subfolder") end) @@ -219,10 +219,10 @@ test.suite("PathPosixExtnameSuite", function(suite) end) suite:case("extname_pathobj", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = { "folder", "file.pdf" }, absolute = false, - } + }, posix.pathmt) local result = path.posix.extname(pathobj) assert.eq(result, ".pdf") end) @@ -265,19 +265,19 @@ test.suite("PathPosixIsAbsoluteSuite", function(suite) end) suite:case("absolute_pathobj_absolute", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = { "home", "user", "file.txt" }, absolute = true, - } + }, posix.pathmt) local result = path.posix.isabsolute(pathobj) assert.eq(result, true) end) suite:case("absolute_pathobj_relative", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = { "folder", "file.txt" }, absolute = false, - } + }, posix.pathmt) local result = path.posix.isabsolute(pathobj) assert.eq(result, false) end) @@ -346,10 +346,10 @@ test.suite("PathPosixNormalizeSuite", function(suite) end) suite:case("normalize_pathobj", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = { "home", ".", "user", "..", "documents" }, absolute = true, - } + }, posix.pathmt) local result = path.posix.normalize(pathobj) assert.eq(result.absolute, true) assert.eq(#result.parts, 2) @@ -443,10 +443,10 @@ test.suite("PathPosixJoinSuite", function(suite) end) suite:case("join_pathobj_and_string", function(assert) - local pathobj: posix.path = { + local pathobj: posix.path = setmetatable({ parts = { "home", "user" }, absolute = true, - } + }, posix.pathmt) local result = path.posix.join(pathobj, "documents/file.txt") assert.eq(result.absolute, true) assert.eq(#result.parts, 4) @@ -551,4 +551,42 @@ test.suite("PathPosixToStringSuite", function(suite) end) end) +test.suite("PathPosixRelativeSuite", function(suite) + suite:case("relative_same_path", function(assert) + local from = posix.parse("/home/user/documents") + local to = posix.parse("/home/user/documents") + local result = path.posix.relative(from, to) + assert.eq(result.absolute, false) + assert.eq(#result.parts, 0) + end) + + suite:case("relative_subdirectory", function(assert) + local from = posix.parse("/home/user") + local to = posix.parse("/home/user/documents/file.txt") + local result = path.posix.relative(from, to) + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "documents") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("relative_parent_directory", function(assert) + local from = posix.parse("/home/user/documents") + local to = posix.parse("/home/user/file.txt") + local result = path.posix.relative(from, to) + assert.eq(result.absolute, false) + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "file.txt") + end) + + suite:case("relative_different_absolute_values", function(assert) + local from = posix.parse("/home/user/documents") + local to = posix.parse("home/user/file.txt") + assert.erroreq(function() + path.posix.relative(from, to) + end, "Cannot compute relative path between absolute and relative paths") + end) +end) + test.run() diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index 0a0678936..da3c44869 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -9,7 +9,7 @@ test.suite("PathWin32ParseSuite", function(suite) suite:case("parse_windows_absolute_path", function(assert) local result: win32.path = path.win32.parse("C:\\Users\\username\\Documents\\file.txt") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 4) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "username") @@ -20,7 +20,7 @@ test.suite("PathWin32ParseSuite", function(suite) suite:case("parse_windows_unc_path", function(assert) local result: win32.path = path.win32.parse("\\\\server\\share\\folder\\file.txt") assert.eq(result.kind, "unc") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 4) assert.eq(result.parts[1], "server") assert.eq(result.parts[2], "share") @@ -31,24 +31,24 @@ test.suite("PathWin32ParseSuite", function(suite) suite:case("parse_empty_string", function(assert) local result: win32.path = path.win32.parse("") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 0) end) suite:case("parse_single_file", function(assert) local result: win32.path = path.win32.parse("file.txt") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 1) assert.eq(result.parts[1], "file.txt") end) suite:case("parse_pathobj_returns_same", function(assert) - local originalPath: win32.path = { + local originalPath: win32.path = setmetatable({ parts = { "folder", "file.txt" }, kind = "relative", - driveLetter = nil, - } + drive = nil :: string?, + }, win32.pathmt) local result = path.win32.parse(originalPath) assert.eq(result, originalPath) end) @@ -56,7 +56,7 @@ test.suite("PathWin32ParseSuite", function(suite) suite:case("parse_mixed_separators", function(assert) local result: win32.path = path.win32.parse("/home/user\\documents/file.txt") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 4) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -67,7 +67,7 @@ test.suite("PathWin32ParseSuite", function(suite) suite:case("parse_windows_relative_with_drive", function(assert) local result: win32.path = path.win32.parse("C:Documents\\file.txt") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 2) assert.eq(result.parts[1], "Documents") assert.eq(result.parts[2], "file.txt") @@ -106,11 +106,11 @@ test.suite("PathWin32BasenameSuite", function(suite) end) suite:case("basename_pathobj", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, kind = "relative", - driveLetter = nil, - } + drive = nil :: string?, + }, win32.pathmt) local result = path.win32.basename(pathobj) assert.eq(result, "file.txt") end) @@ -123,61 +123,61 @@ end) test.suite("PathWin32FormatSuite", function(suite) suite:case("format_windows_absolute_path", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "Users", "username", "Documents", "file.txt" }, kind = "absolute", - driveLetter = "C", - } + drive = "C" :: string?, + }, win32.pathmt) local result = path.win32.format(pathobj) assert.eq(result, "C:\\Users\\username\\Documents\\file.txt") end) suite:case("format_windows_relative_with_drive", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "Documents", "file.txt" }, kind = "relative", - driveLetter = "C", - } + drive = "C" :: string?, + }, win32.pathmt) local result = path.win32.format(pathobj) assert.eq(result, "C:Documents\\file.txt") end) suite:case("format_windows_unc_path", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "server", "share", "folder", "file.txt" }, kind = "unc", - driveLetter = nil, - } + drive = nil :: string?, + }, win32.pathmt) local result = path.win32.format(pathobj) assert.eq(result, "\\\\server\\share\\folder\\file.txt") end) suite:case("format_empty_relative_path", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = {}, kind = "relative", - driveLetter = nil, - } + drive = nil :: string?, + }, win32.pathmt) local result = path.win32.format(pathobj) assert.eq(result, ".") end) suite:case("format_empty_relative_path_with_drive", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = {}, kind = "relative", - driveLetter = "C", - } + drive = "C" :: string?, + }, win32.pathmt) local result = path.win32.format(pathobj) assert.eq(result, "C:") end) suite:case("format_root_path_with_drive", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = {}, kind = "absolute", - driveLetter = "C", - } + drive = "C" :: string?, + }, win32.pathmt) local result = path.win32.format(pathobj) assert.eq(result, "C:\\") end) @@ -215,11 +215,11 @@ test.suite("PathWin32DirnameSuite", function(suite) end) suite:case("dirname_pathobj", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, kind = "relative", - driveLetter = nil, - } + drive = nil :: string?, + }, win32.pathmt) local result = path.win32.dirname(pathobj) assert.eq(result, "folder\\subfolder") end) @@ -277,11 +277,11 @@ test.suite("PathWin32ExtnameSuite", function(suite) end) suite:case("extname_pathobj", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "folder", "file.pdf" }, kind = "relative", - driveLetter = nil, - } + drive = nil :: string?, + }, win32.pathmt) local result = path.win32.extname(pathobj) assert.eq(result, ".pdf") end) @@ -306,39 +306,39 @@ test.suite("PathWin32GetDriveSuite", function(suite) suite:case("getdrive_absolute_path_string", function(assert) local result = path.win32.drive("C:\\Users\\username\\Documents\\file.txt") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 0) end) suite:case("getdrive_relative_path_with_drive", function(assert) local result = path.win32.drive("D:Documents\\file.txt") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "D") + assert.eq(result.drive, "D") assert.eq(#result.parts, 0) end) suite:case("getdrive_path_object", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "Users", "username", "Documents" }, kind = "absolute", - driveLetter = "E", - } + drive = "E" :: string?, + }, win32.pathmt) local result = path.win32.drive(pathobj) assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "E") + assert.eq(result.drive, "E") assert.eq(#result.parts, 0) end) suite:case("getdrive_root_path", function(assert) local result = path.win32.drive("F:\\") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "F") + assert.eq(result.drive, "F") assert.eq(#result.parts, 0) end) suite:case("getdrive_error_on_empty_path", function(assert) assert.errors(function() - return path.win32.drive("") + path.win32.drive("") end) end) end) @@ -375,21 +375,21 @@ test.suite("PathWin32IsAbsoluteSuite", function(suite) end) suite:case("isAbsolute_pathobj_absolute", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "home", "user", "file.txt" }, kind = "absolute", - driveLetter = "C", - } + drive = "C" :: string?, + }, win32.pathmt) local result = path.win32.isabsolute(pathobj) assert.eq(result, true) end) suite:case("isAbsolute_pathobj_relative", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "folder", "file.txt" }, kind = "relative", - driveLetter = nil, - } + drive = nil :: string?, + }, win32.pathmt) local result = path.win32.isabsolute(pathobj) assert.eq(result, false) end) @@ -399,7 +399,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_removes_current_directory", function(assert) local result = path.win32.normalize("C:\\Users\\.\\username\\Documents") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 3) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "username") @@ -409,7 +409,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_resolves_parent_directory", function(assert) local result = path.win32.normalize("C:\\Users\\username\\..\\Documents") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 2) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "Documents") @@ -418,7 +418,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_multiple_dots", function(assert) local result = path.win32.normalize("C:\\Users\\username\\.\\Documents\\..\\files\\.\\") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 3) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "username") @@ -428,7 +428,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_relative_path", function(assert) local result = path.win32.normalize("Documents\\.\\subfolder\\..\\file.txt") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 2) assert.eq(result.parts[1], "Documents") assert.eq(result.parts[2], "file.txt") @@ -437,7 +437,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_removes_empty_segments", function(assert) local result = path.win32.normalize("C:\\Users\\\\username\\\\\\Documents") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 3) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "username") @@ -447,14 +447,14 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_parent_beyond_root", function(assert) local result = path.win32.normalize("C:\\Users\\..\\..\\") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 0) end) suite:case("normalize_unc_path", function(assert) local result = path.win32.normalize("\\\\server\\share\\.\\folder\\..\\files") assert.eq(result.kind, "unc") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "server") assert.eq(result.parts[2], "share") @@ -464,7 +464,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_relative_with_drive", function(assert) local result = path.win32.normalize("C:Documents\\.\\subfolder\\..\\file.txt") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 2) assert.eq(result.parts[1], "Documents") assert.eq(result.parts[2], "file.txt") @@ -473,7 +473,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_parent_directory_only", function(assert) local result = path.win32.normalize("..") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 1) assert.eq(result.parts[1], "..") end) @@ -481,7 +481,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_multiple_parent_directories", function(assert) local result = path.win32.normalize("..\\..\\..") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "..") assert.eq(result.parts[2], "..") @@ -491,7 +491,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_parent_then_folder", function(assert) local result = path.win32.normalize("..\\folder\\file.txt") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "..") assert.eq(result.parts[2], "folder") @@ -501,20 +501,20 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_folder_then_multiple_parents", function(assert) local result = path.win32.normalize("folder\\subfolder\\..\\..\\other") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 1) assert.eq(result.parts[1], "other") end) suite:case("normalize_pathobj", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "Users", ".", "username", "..", "Documents" }, kind = "absolute", - driveLetter = "C", - } + drive = "C" :: string?, + }, win32.pathmt) local result = path.win32.normalize(pathobj) assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 2) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "Documents") @@ -523,7 +523,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) suite:case("normalize_relative_with_drive_and_parent", function(assert) local result = path.win32.normalize("C:..\\folder\\file.txt") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 3) assert.eq(result.parts[1], "..") assert.eq(result.parts[2], "folder") @@ -535,7 +535,7 @@ test.suite("PathWin32JoinSuite", function(suite) suite:case("join_absolute_and_relative", function(assert) local result = path.win32.join("C:\\Users\\username", "Documents", "file.txt") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 4) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "username") @@ -546,7 +546,7 @@ test.suite("PathWin32JoinSuite", function(suite) suite:case("join_relative_paths", function(assert) local result = path.win32.join("Documents", "subfolder", "file.txt") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "Documents") assert.eq(result.parts[2], "subfolder") @@ -556,7 +556,7 @@ test.suite("PathWin32JoinSuite", function(suite) suite:case("join_drive_relative_paths", function(assert) local result = path.win32.join("C:Documents", "subfolder", "file.txt") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 3) assert.eq(result.parts[1], "Documents") assert.eq(result.parts[2], "subfolder") @@ -566,7 +566,7 @@ test.suite("PathWin32JoinSuite", function(suite) suite:case("join_unc_and_relative", function(assert) local result = path.win32.join("\\\\server\\share", "folder", "file.txt") assert.eq(result.kind, "unc") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 4) assert.eq(result.parts[1], "server") assert.eq(result.parts[2], "share") @@ -577,19 +577,19 @@ test.suite("PathWin32JoinSuite", function(suite) suite:case("join_no_arguments", function(assert) local result = path.win32.join() assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 0) end) suite:case("join_pathobj_and_string", function(assert) - local pathobj: win32.path = { + local pathobj: win32.path = setmetatable({ parts = { "Users", "username" }, kind = "absolute", - driveLetter = "C", - } + drive = "C" :: string?, + }, win32.pathmt) local result = path.win32.join(pathobj, "Documents\\file.txt") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 4) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "username") @@ -599,7 +599,7 @@ test.suite("PathWin32JoinSuite", function(suite) assert.eq(pathobj.parts[1], "Users") assert.eq(pathobj.parts[2], "username") assert.eq(pathobj.kind, "absolute") - assert.eq(pathobj.driveLetter, "C") + assert.eq(pathobj.drive, "C") end) suite:case("join_error_on_absolute_addend", function(assert) @@ -626,7 +626,7 @@ test.suite("PathWin32JoinSuite", function(suite) suite:case("join_compatible_drives", function(assert) local result = path.win32.join("C:Documents", "C:subfolder") assert.eq(result.kind, "relative") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 2) assert.eq(result.parts[1], "Documents") assert.eq(result.parts[2], "subfolder") @@ -637,7 +637,7 @@ test.suite("PathWin32ResolveSuite", function(suite) suite:case("resolve_absolute_path", function(assert) local result = path.win32.resolve("C:\\Users\\username\\Documents\\file.txt") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 4) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "username") @@ -661,7 +661,7 @@ test.suite("PathWin32ResolveSuite", function(suite) suite:case("resolve_multiple_paths", function(assert) local result = path.win32.resolve("C:\\Users", "username", "..\\admin", "Documents") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 3) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "admin") @@ -671,7 +671,7 @@ test.suite("PathWin32ResolveSuite", function(suite) suite:case("resolve_with_absolute_in_middle", function(assert) local result = path.win32.resolve("relative", "D:\\absolute\\path", "more") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "D") + assert.eq(result.drive, "D") assert.eq(#result.parts, 3) assert.eq(result.parts[1], "absolute") assert.eq(result.parts[2], "path") @@ -681,7 +681,7 @@ test.suite("PathWin32ResolveSuite", function(suite) suite:case("resolve_unc_path", function(assert) local result = path.win32.resolve("\\\\server\\share\\folder") assert.eq(result.kind, "unc") - assert.eq(result.driveLetter, nil) + assert.eq(result.drive, nil) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "server") assert.eq(result.parts[2], "share") @@ -698,15 +698,14 @@ test.suite("PathWin32ResolveSuite", function(suite) suite:case("resolve_drive_relative_path", function(assert) -- Get the drive letter from the current working directory - local cwd = process.cwd() - local cwdPath = path.win32.parse(cwd) - local driveLetter = if system.win32 then path.win32.drive(cwdPath).driveLetter else "C" + local cwdPath = path.win32.parse(process.cwd() :: win32.pathlike) + local drive = if system.win32 then path.win32.drive(cwdPath).drive :: string else "C" - local testPath = driveLetter .. ":Documents\\file.txt" + local testPath = drive .. ":Documents\\file.txt" local result = path.win32.resolve(testPath) -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters assert.eq(result.kind, if system.win32 then "absolute" else "relative") - assert.eq(result.driveLetter, driveLetter) + assert.eq(result.drive, drive) -- The exact parts depend on cwd, but should be absolute local endIndex = #result.parts assert.eq(result.parts[endIndex - 1], "Documents") @@ -716,7 +715,7 @@ test.suite("PathWin32ResolveSuite", function(suite) suite:case("resolve_with_dot_segments", function(assert) local result = path.win32.resolve("C:\\Users\\username", "..\\admin\\.\\Documents") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 3) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "admin") @@ -726,7 +725,7 @@ test.suite("PathWin32ResolveSuite", function(suite) suite:case("resolve_empty_strings", function(assert) local result = path.win32.resolve("", "", "C:\\Users\\username") assert.eq(result.kind, "absolute") - assert.eq(result.driveLetter, "C") + assert.eq(result.drive, "C") assert.eq(#result.parts, 2) assert.eq(result.parts[1], "Users") assert.eq(result.parts[2], "username") @@ -749,4 +748,74 @@ test.suite("PathWin32ToStringSuite", function(suite) end) end) +test.suite("PathWin32RelativeSuite", function(suite) + suite:case("relative_same_path", function(assert) + local from = win32.parse("C:\\Users\\username\\Documents") + local to = win32.parse("C:\\Users\\username\\Documents") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 0) + assert.eq(result.drive, nil) + end) + + suite:case("relative_subdirectory", function(assert) + local from = win32.parse("C:\\Users\\username") + local to = win32.parse("C:\\Users\\username\\Documents\\file.txt") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "Documents") + assert.eq(result.parts[2], "file.txt") + assert.eq(result.drive, nil) + end) + + suite:case("relative_parent_directory", function(assert) + local from = win32.parse("C:\\Users\\username\\Documents") + local to = win32.parse("C:\\Users\\username\\file.txt") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "file.txt") + assert.eq(result.drive, nil) + end) + + suite:case("relative_different_kinds", function(assert) + local from = win32.parse("C:\\Users\\username\\Documents") + local to = win32.parse("Documents\\file.txt") + assert.erroreq(function() + path.win32.relative(from, to) + end, "Cannot compute relative path between different kinds of paths") + end) + + suite:case("relative_different_drives", function(assert) + local from = win32.parse("C:\\Users\\username") + local to = win32.parse("D:\\Documents\\file.txt") + assert.erroreq(function() + path.win32.relative(from, to) + end, "Cannot compute relative path between different drives") + end) + + suite:case("relative_unc_paths", function(assert) + local from = win32.parse("\\\\server\\share\\folder1") + local to = win32.parse("\\\\server\\share\\folder2\\file.txt") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 3) + assert.eq(result.parts[1], "..") + assert.eq(result.parts[2], "folder2") + assert.eq(result.parts[3], "file.txt") + end) + + suite:case("relative_with_drive_letter_same_drive", function(assert) + local from = win32.parse("C:Documents") + local to = win32.parse("C:Documents\\subfolder\\file.txt") + local result = path.win32.relative(from, to) + assert.eq(result.kind, "relative") + assert.eq(#result.parts, 2) + assert.eq(result.parts[1], "subfolder") + assert.eq(result.parts[2], "file.txt") + end) +end) + test.run() diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 1a0a99e3f..74c7c700e 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -2,6 +2,7 @@ local pathlib = require("@std/path") local process = require("@std/process") local system = require("@std/system") local test = require("@std/test") +local windowsPath = require("@std/path/win32") test.suite("ProcessSuite", function(suite) suite:case("homedir_and_cwd_and_execpath", function(assert) @@ -29,20 +30,20 @@ test.suite("ProcessSuite", function(suite) local rootdir: string = "/" local root: pathlib.path? = nil if system.win32 then - root = pathlib.win32.drive(process.cwd()) + root = pathlib.win32.drive(process.cwd() :: windowsPath.path) rootdir = pathlib.format(root) end local r = process.run({ "pwd" }, { cwd = rootdir }) if system.win32 then - assert(root ~= nil and root.driveLetter ~= nil) - check.tableeq(r, { + assert(root ~= nil and (root :: windowsPath.path).drive ~= nil) + check.tableeq(r, { -- LUAUFIX: ProcessResult Date: Tue, 9 Dec 2025 15:14:20 -0800 Subject: [PATCH 232/642] Comment out print statements in `compile_command_e2e` unit test (#667) These prints are a little distracting, so we're commenting them out. They're left in place and can be uncommented locally since they're useful for debugging. --- tests/src/staticrequires/lib/helper.luau | 2 +- tests/src/staticrequires/main.luau | 4 ++-- tests/src/staticrequires/other/module.luau | 2 +- tests/src/staticrequires/shared.luau | 2 +- tests/src/staticrequires/utils.luau | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/src/staticrequires/lib/helper.luau b/tests/src/staticrequires/lib/helper.luau index 33591e43c..8134b7e13 100644 --- a/tests/src/staticrequires/lib/helper.luau +++ b/tests/src/staticrequires/lib/helper.luau @@ -1,7 +1,7 @@ -- Helper module that also requires another module local shared = require("../shared") -print("Helper module") +-- print("Helper module") return { help = function() return "helper" diff --git a/tests/src/staticrequires/main.luau b/tests/src/staticrequires/main.luau index 3c9e2be1c..0a2244f0a 100644 --- a/tests/src/staticrequires/main.luau +++ b/tests/src/staticrequires/main.luau @@ -1,6 +1,6 @@ -- test that you can require in an alias aware fashion local example = require("@example/option") -print(`Required {example.file} from alias @example`) +-- print(`Required {example.file} from alias @example`) -- Entry point that requires other modules local utils = require("./utils") @@ -11,7 +11,7 @@ local path = require("@std/path") -- Use standard library to verify it works in compiled bundles local cwd = path.format(process.cwd()) -print("Main module running from: " .. cwd) +-- print("Main module running from: " .. cwd) return { utils = utils, diff --git a/tests/src/staticrequires/other/module.luau b/tests/src/staticrequires/other/module.luau index 398e3dd2d..fdb90ea47 100644 --- a/tests/src/staticrequires/other/module.luau +++ b/tests/src/staticrequires/other/module.luau @@ -1,6 +1,6 @@ -- test that we can require using an alias from other/.luaurc local example = require("@otherlib/example") -print(`Required {example.file} from alias @otherlib`) +-- print(`Required {example.file} from alias @otherlib`) return { example = example, diff --git a/tests/src/staticrequires/shared.luau b/tests/src/staticrequires/shared.luau index 9ded675b6..fbe99b428 100644 --- a/tests/src/staticrequires/shared.luau +++ b/tests/src/staticrequires/shared.luau @@ -1,5 +1,5 @@ -- Shared module -print("Shared module") +-- print("Shared module") return { version = "1.0", } diff --git a/tests/src/staticrequires/utils.luau b/tests/src/staticrequires/utils.luau index b9e9b1666..a4af5a1cc 100644 --- a/tests/src/staticrequires/utils.luau +++ b/tests/src/staticrequires/utils.luau @@ -1,5 +1,5 @@ -- Utilities module -print("Utils module") +-- print("Utils module") return { add = function(a, b) return a + b From 47e16e6cf9d2a502e9a6aed03a6b645f85b1a4e2 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 9 Dec 2025 15:16:55 -0800 Subject: [PATCH 233/642] Cleans up method names in the syntax visitor library (#666) Cleans up methods in the visitor library so that they match the AST types they correspond to --- examples/transformer.luau | 2 +- lute/std/libs/syntax/printer.luau | 6 +- lute/std/libs/syntax/query.luau | 8 +- lute/std/libs/syntax/visitor.luau | 574 +++++++++++++++--------------- 4 files changed, 296 insertions(+), 294 deletions(-) diff --git a/examples/transformer.luau b/examples/transformer.luau index 5aa276f40..b70c33aab 100644 --- a/examples/transformer.luau +++ b/examples/transformer.luau @@ -5,7 +5,7 @@ local syntax = require("@std/syntax") local function transformation(ctx) local v = visitor.create() - v.visitBinary = function(node: syntax.AstExprBinary) + v.visitExprBinary = function(node: syntax.AstExprBinary) if node.operator.text ~= "~=" then return true end diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index aa185e02b..564878096 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -168,11 +168,11 @@ local function printVisitor(replacements: types.replacements?) printer.printString = printString printer.printInterpolatedString = printInterpolatedString printer.printReplacement = printReplacement - printer.visitInterpolatedString = function(node: types.AstExprInterpString) + printer.visitExprInterpString = function(node: types.AstExprInterpString) printer:printInterpolatedString(node) return false end - printer.visitTypeString = function(node: types.AstTypeSingletonString) + printer.visitTypeSingletonString = function(node: types.AstTypeSingletonString) printer:printString(node) return false end @@ -180,7 +180,7 @@ local function printVisitor(replacements: types.replacements?) printer:printToken(node) return false end - printer.visitString = function(node: types.AstExprConstantString) + printer.visitExprConstantString = function(node: types.AstExprConstantString) printer:printString(node) return false end diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index b8193e6ca..4be2b7ac4 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -74,12 +74,12 @@ local function newSelectVisitor(nodes: { T }, fn: (node) -> T?): visitor.Visi end local selectVisitor = visitor.create(visit) - selectVisitor.visitBlockEnd = function(n) end - selectVisitor.visitLocalDeclarationEnd = function(n) end - selectVisitor.visitExpression = function(n) + selectVisitor.visitStatBlockEnd = function(n) end + selectVisitor.visitStatLocalDeclarationEnd = function(n) end + selectVisitor.visitExpr = function(n) return true end - selectVisitor.visitExpressionEnd = function(n) end + selectVisitor.visitExprEnd = function(n) end selectVisitor.visitToken = function(n) if utils.isBaseToken(n) then -- only visit if it is a base token, so we don't double count diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index a513ee860..fd90b82d7 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -7,47 +7,52 @@ local types = require("./types") local visitorlib = {} export type Visitor = { - visitBlock: (types.AstStatBlock) -> boolean, - visitBlockEnd: (types.AstStatBlock) -> (), + visitStatBlock: (types.AstStatBlock) -> boolean, + visitStatBlockEnd: (types.AstStatBlock) -> (), visitStatDo: (types.AstStatDo) -> boolean, - visitIf: (types.AstStatIf) -> boolean, - visitWhile: (types.AstStatWhile) -> boolean, - visitRepeat: (types.AstStatRepeat) -> boolean, - visitBreak: (types.AstStatBreak) -> boolean, - visitContinue: (types.AstStatContinue) -> boolean, - visitReturn: (types.AstStatReturn) -> boolean, - visitLocalDeclaration: (types.AstStatLocal) -> boolean, - visitLocalDeclarationEnd: (types.AstStatLocal) -> (), - visitFor: (types.AstStatFor) -> boolean, - visitForIn: (types.AstStatForIn) -> boolean, - visitAssign: (types.AstStatAssign) -> boolean, - visitCompoundAssign: (types.AstStatCompoundAssign) -> boolean, - visitFunction: (types.AstStatFunction) -> boolean, - visitLocalFunction: (types.AstStatLocalFunction) -> boolean, - visitTypeAlias: (types.AstStatTypeAlias) -> boolean, + visitStatIf: (types.AstStatIf) -> boolean, + visitStatWhile: (types.AstStatWhile) -> boolean, + visitStatRepeat: (types.AstStatRepeat) -> boolean, + visitStatBreak: (types.AstStatBreak) -> boolean, + visitStatContinue: (types.AstStatContinue) -> boolean, + visitStatReturn: (types.AstStatReturn) -> boolean, + visitStatLocalDeclaration: (types.AstStatLocal) -> boolean, + visitStatLocalDeclarationEnd: (types.AstStatLocal) -> (), + visitStatFor: (types.AstStatFor) -> boolean, + visitStatForIn: (types.AstStatForIn) -> boolean, + visitStatAssign: (types.AstStatAssign) -> boolean, + visitStatCompoundAssign: (types.AstStatCompoundAssign) -> boolean, + visitStatFunction: (types.AstStatFunction) -> boolean, + visitStatLocalFunction: (types.AstStatLocalFunction) -> boolean, + visitStatTypeAlias: (types.AstStatTypeAlias) -> boolean, visitStatTypeFunction: (types.AstStatTypeFunction) -> boolean, visitStatExpr: (types.AstStatExpr) -> boolean, - visitExpression: (types.AstExpr) -> boolean, - visitExpressionEnd: (types.AstExpr) -> (), - visitLocalReference: (types.AstExprLocal) -> boolean, - visitGlobal: (types.AstExprGlobal) -> boolean, - visitCall: (types.AstExprCall) -> boolean, - visitUnary: (types.AstExprUnary) -> boolean, - visitBinary: (types.AstExprBinary) -> boolean, - visitAnonymousFunction: (types.AstExprAnonymousFunction) -> boolean, - visitTableItem: (types.AstExprTableItem) -> boolean, - visitTable: (types.AstExprTable) -> boolean, - visitIndexName: (types.AstExprIndexName) -> boolean, - visitIndexExpr: (types.AstExprIndexExpr) -> boolean, - visitGroup: (types.AstExprGroup) -> boolean, - visitInterpolatedString: (types.AstExprInterpString) -> boolean, - visitTypeAssertion: (types.AstExprTypeAssertion) -> boolean, - visitIfExpression: (types.AstExprIfElse) -> boolean, + visitExpr: (types.AstExpr) -> boolean, + visitExprEnd: (types.AstExpr) -> (), + visitExprConstantNil: (types.AstExprConstantNil) -> boolean, + visitExprConstantString: (types.AstExprConstantString) -> boolean, + visitExprConstantBool: (types.AstExprConstantBool) -> boolean, + visitExprConstantNumber: (types.AstExprConstantNumber) -> boolean, + visitExprLocal: (types.AstExprLocal) -> boolean, + visitExprGlobal: (types.AstExprGlobal) -> boolean, + visitExprCall: (types.AstExprCall) -> boolean, + visitExprUnary: (types.AstExprUnary) -> boolean, + visitExprBinary: (types.AstExprBinary) -> boolean, + visitExprAnonymousFunction: (types.AstExprAnonymousFunction) -> boolean, + visitExprTableItem: (types.AstExprTableItem) -> boolean, + visitExprTable: (types.AstExprTable) -> boolean, + visitExprIndexName: (types.AstExprIndexName) -> boolean, + visitExprIndexExpr: (types.AstExprIndexExpr) -> boolean, + visitExprGroup: (types.AstExprGroup) -> boolean, + visitExprInterpString: (types.AstExprInterpString) -> boolean, + visitExprTypeAssertion: (types.AstExprTypeAssertion) -> boolean, + visitExprIfElse: (types.AstExprIfElse) -> boolean, + visitExprVarargs: (types.AstExprVarargs) -> boolean, visitTypeReference: (types.AstTypeReference) -> boolean, - visitTypeBoolean: (types.AstTypeSingletonBool) -> boolean, - visitTypeString: (types.AstTypeSingletonString) -> boolean, + visitTypeSingletonBool: (types.AstTypeSingletonBool) -> boolean, + visitTypeSingletonString: (types.AstTypeSingletonString) -> boolean, visitTypeTypeof: (types.AstTypeTypeof) -> boolean, visitTypeGroup: (types.AstTypeGroup) -> boolean, visitTypeOptional: (types.AstTypeOptional) -> boolean, @@ -62,12 +67,8 @@ export type Visitor = { visitTypePackVariadic: (types.AstTypePackVariadic) -> boolean, visitToken: (types.Token) -> boolean, - visitNil: (types.AstExprConstantNil) -> boolean, - visitString: (types.AstExprConstantString) -> boolean, - visitBoolean: (types.AstExprConstantBool) -> boolean, - visitNumber: (types.AstExprConstantNumber) -> boolean, + visitLocal: (types.AstLocal) -> boolean, - visitVarargs: (types.AstExprVarargs) -> boolean, visitAttribute: (types.AstAttribute) -> boolean, } @@ -77,47 +78,52 @@ local function alwaysVisit(...: any) end local defaultVisitor: Visitor = { - visitBlock = alwaysVisit :: any, - visitBlockEnd = alwaysVisit :: any, + visitStatBlock = alwaysVisit :: any, + visitStatBlockEnd = alwaysVisit :: any, visitStatDo = alwaysVisit :: any, - visitIf = alwaysVisit :: any, - visitWhile = alwaysVisit :: any, - visitRepeat = alwaysVisit :: any, - visitBreak = alwaysVisit :: any, - visitContinue = alwaysVisit :: any, - visitReturn = alwaysVisit :: any, - visitLocalDeclaration = alwaysVisit :: any, - visitLocalDeclarationEnd = alwaysVisit :: any, - visitFor = alwaysVisit :: any, - visitForIn = alwaysVisit :: any, - visitAssign = alwaysVisit :: any, - visitCompoundAssign = alwaysVisit :: any, - visitFunction = alwaysVisit :: any, - visitLocalFunction = alwaysVisit :: any, - visitTypeAlias = alwaysVisit :: any, + visitStatIf = alwaysVisit :: any, + visitStatWhile = alwaysVisit :: any, + visitStatRepeat = alwaysVisit :: any, + visitStatBreak = alwaysVisit :: any, + visitStatContinue = alwaysVisit :: any, + visitStatReturn = alwaysVisit :: any, + visitStatLocalDeclaration = alwaysVisit :: any, + visitStatLocalDeclarationEnd = alwaysVisit :: any, + visitStatFor = alwaysVisit :: any, + visitStatForIn = alwaysVisit :: any, + visitStatAssign = alwaysVisit :: any, + visitStatCompoundAssign = alwaysVisit :: any, + visitStatFunction = alwaysVisit :: any, + visitStatLocalFunction = alwaysVisit :: any, + visitStatTypeAlias = alwaysVisit :: any, visitStatTypeFunction = alwaysVisit :: any, visitStatExpr = alwaysVisit :: any, - visitExpression = alwaysVisit :: any, - visitExpressionEnd = alwaysVisit :: any, - visitLocalReference = alwaysVisit :: any, - visitGlobal = alwaysVisit :: any, - visitCall = alwaysVisit :: any, - visitUnary = alwaysVisit :: any, - visitBinary = alwaysVisit :: any, - visitAnonymousFunction = alwaysVisit :: any, - visitTableItem = alwaysVisit :: any, - visitTable = alwaysVisit :: any, - visitIndexName = alwaysVisit :: any, - visitIndexExpr = alwaysVisit :: any, - visitGroup = alwaysVisit :: any, - visitInterpolatedString = alwaysVisit, - visitTypeAssertion = alwaysVisit, - visitIfExpression = alwaysVisit, + visitExpr = alwaysVisit :: any, + visitExprConstantNil = alwaysVisit :: any, + visitExprConstantString = alwaysVisit :: any, + visitExprConstantBool = alwaysVisit :: any, + visitExprConstantNumber = alwaysVisit :: any, + visitExprEnd = alwaysVisit :: any, + visitExprLocal = alwaysVisit :: any, + visitExprGlobal = alwaysVisit :: any, + visitExprCall = alwaysVisit :: any, + visitExprUnary = alwaysVisit :: any, + visitExprBinary = alwaysVisit :: any, + visitExprAnonymousFunction = alwaysVisit :: any, + visitExprTableItem = alwaysVisit :: any, + visitExprTable = alwaysVisit :: any, + visitExprIndexName = alwaysVisit :: any, + visitExprIndexExpr = alwaysVisit :: any, + visitExprGroup = alwaysVisit :: any, + visitExprInterpString = alwaysVisit, + visitExprTypeAssertion = alwaysVisit, + visitExprIfElse = alwaysVisit, + visitExprVarargs = alwaysVisit :: any, visitTypeReference = alwaysVisit :: any, - visitTypeBoolean = alwaysVisit :: any, - visitTypeString = alwaysVisit :: any, + visitTypeSingletonBool = alwaysVisit :: any, + visitTypeSingletonString = alwaysVisit :: any, visitTypeTypeof = alwaysVisit :: any, visitTypeGroup = alwaysVisit :: any, visitTypeOptional = alwaysVisit :: any, @@ -132,12 +138,8 @@ local defaultVisitor: Visitor = { visitTypePackVariadic = alwaysVisit, visitToken = alwaysVisit :: any, - visitNil = alwaysVisit :: any, - visitString = alwaysVisit :: any, - visitBoolean = alwaysVisit :: any, - visitNumber = alwaysVisit :: any, + visitLocal = alwaysVisit :: any, - visitVarargs = alwaysVisit :: any, visitAttribute = alwaysVisit :: any, } @@ -171,13 +173,13 @@ local function visitLocal(node: types.AstLocal, visitor: Visitor) end end -local function visitBlock(block: types.AstStatBlock, visitor: Visitor) - if visitor.visitBlock(block) then +local function visitStatBlock(block: types.AstStatBlock, visitor: Visitor) + if visitor.visitStatBlock(block) then for _, statement in block.statements do visitStatement(statement, visitor) end - visitor.visitBlockEnd(block) + visitor.visitStatBlockEnd(block) end end @@ -191,124 +193,124 @@ local function visitStatDo(node: types.AstStatDo, visitor: Visitor) end end -local function visitIf(node: types.AstStatIf, visitor: Visitor) - if visitor.visitIf(node) then +local function visitStatIf(node: types.AstStatIf, visitor: Visitor) + if visitor.visitStatIf(node) then visitToken(node.ifkeyword, visitor) - visitExpression(node.condition, visitor) + visitExpr(node.condition, visitor) visitToken(node.thenkeyword, visitor) - visitBlock(node.thenblock, visitor) + visitStatBlock(node.thenblock, visitor) for _, elseifNode in node.elseifs do visitToken(elseifNode.elseifkeyword, visitor) - visitExpression(elseifNode.condition, visitor) + visitExpr(elseifNode.condition, visitor) visitToken(elseifNode.thenkeyword, visitor) - visitBlock(elseifNode.thenblock, visitor) + visitStatBlock(elseifNode.thenblock, visitor) end if node.elsekeyword then visitToken(node.elsekeyword, visitor) end if node.elseblock then - visitBlock(node.elseblock, visitor) + visitStatBlock(node.elseblock, visitor) end visitToken(node.endkeyword, visitor) end end -local function visitWhile(node: types.AstStatWhile, visitor: Visitor) - if visitor.visitWhile(node) then +local function visitStatWhile(node: types.AstStatWhile, visitor: Visitor) + if visitor.visitStatWhile(node) then visitToken(node.whilekeyword, visitor) - visitExpression(node.condition, visitor) + visitExpr(node.condition, visitor) visitToken(node.dokeyword, visitor) - visitBlock(node.body, visitor) + visitStatBlock(node.body, visitor) visitToken(node.endkeyword, visitor) end end -local function visitRepeat(node: types.AstStatRepeat, visitor: Visitor) - if visitor.visitRepeat(node) then +local function visitStatRepeat(node: types.AstStatRepeat, visitor: Visitor) + if visitor.visitStatRepeat(node) then visitToken(node.repeatkeyword, visitor) - visitBlock(node.body, visitor) + visitStatBlock(node.body, visitor) visitToken(node.untilkeyword, visitor) - visitExpression(node.condition, visitor) + visitExpr(node.condition, visitor) end end -local function visitBreak(node: types.AstStatBreak, visitor: Visitor) - if visitor.visitBreak(node) then +local function visitStatBreak(node: types.AstStatBreak, visitor: Visitor) + if visitor.visitStatBreak(node) then visitToken(node, visitor) end end -local function visitContinue(node: types.AstStatContinue, visitor: Visitor) - if visitor.visitContinue(node) then +local function visitStatContinue(node: types.AstStatContinue, visitor: Visitor) + if visitor.visitStatContinue(node) then visitToken(node, visitor) end end -local function visitReturn(node: types.AstStatReturn, visitor: Visitor) - if visitor.visitReturn(node) then +local function visitStatReturn(node: types.AstStatReturn, visitor: Visitor) + if visitor.visitStatReturn(node) then visitToken(node.returnkeyword, visitor) - visitPunctuated(node.expressions, visitor, visitExpression) + visitPunctuated(node.expressions, visitor, visitExpr) end end local function visitLocalStatement(node: types.AstStatLocal, visitor: Visitor) - if visitor.visitLocalDeclaration(node) then + if visitor.visitStatLocalDeclaration(node) then visitToken(node.localkeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) if node.equals then visitToken(node.equals, visitor) end - visitPunctuated(node.values, visitor, visitExpression) + visitPunctuated(node.values, visitor, visitExpr) - visitor.visitLocalDeclarationEnd(node) + visitor.visitStatLocalDeclarationEnd(node) end end -local function visitFor(node: types.AstStatFor, visitor: Visitor) - if visitor.visitFor(node) then +local function visitStatFor(node: types.AstStatFor, visitor: Visitor) + if visitor.visitStatFor(node) then visitToken(node.forkeyword, visitor) visitLocal(node.variable, visitor) visitToken(node.equals, visitor) - visitExpression(node.from, visitor) + visitExpr(node.from, visitor) visitToken(node.tocomma, visitor) - visitExpression(node.to, visitor) + visitExpr(node.to, visitor) if node.stepcomma then visitToken(node.stepcomma, visitor) end if node.step then - visitExpression(node.step, visitor) + visitExpr(node.step, visitor) end visitToken(node.dokeyword, visitor) - visitBlock(node.body, visitor) + visitStatBlock(node.body, visitor) visitToken(node.endkeyword, visitor) end end -local function visitForIn(node: types.AstStatForIn, visitor: Visitor) - if visitor.visitForIn(node) then +local function visitStatForIn(node: types.AstStatForIn, visitor: Visitor) + if visitor.visitStatForIn(node) then visitToken(node.forkeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) visitToken(node.inkeyword, visitor) - visitPunctuated(node.values, visitor, visitExpression) + visitPunctuated(node.values, visitor, visitExpr) visitToken(node.dokeyword, visitor) - visitBlock(node.body, visitor) + visitStatBlock(node.body, visitor) visitToken(node.endkeyword, visitor) end end -local function visitAssign(node: types.AstStatAssign, visitor: Visitor) - if visitor.visitAssign(node) then - visitPunctuated(node.variables, visitor, visitExpression) +local function visitStatAssign(node: types.AstStatAssign, visitor: Visitor) + if visitor.visitStatAssign(node) then + visitPunctuated(node.variables, visitor, visitExpr) visitToken(node.equals, visitor) - visitPunctuated(node.values, visitor, visitExpression) + visitPunctuated(node.values, visitor, visitExpr) end end -local function visitCompoundAssign(node: types.AstStatCompoundAssign, visitor: Visitor) - if visitor.visitCompoundAssign(node) then - visitExpression(node.variable, visitor) +local function visitStatCompoundAssign(node: types.AstStatCompoundAssign, visitor: Visitor) + if visitor.visitStatCompoundAssign(node) then + visitExpr(node.variable, visitor) visitToken(node.operand, visitor) - visitExpression(node.value, visitor) + visitExpr(node.value, visitor) end end @@ -333,8 +335,8 @@ local function visitGenericPack(node: types.AstGenericTypePack, visitor: Visitor end end -local function visitTypeAlias(node: types.AstStatTypeAlias, visitor: Visitor) - if visitor.visitTypeAlias(node) then +local function visitStatTypeAlias(node: types.AstStatTypeAlias, visitor: Visitor) + if visitor.visitStatTypeAlias(node) then if node.export then visitToken(node.export, visitor) end @@ -359,81 +361,81 @@ end local function visitStatExpr(node: types.AstStatExpr, visitor: Visitor) if visitor.visitStatExpr(node) then - visitExpression(node.expression, visitor) + visitExpr(node.expression, visitor) end end -local function visitString(node: types.AstExprConstantString, visitor: Visitor) - if visitor.visitString(node) then +local function visitExprConstantString(node: types.AstExprConstantString, visitor: Visitor) + if visitor.visitExprConstantString(node) then visitor.visitToken(node) end end -local function visitNil(node: types.AstExprConstantNil, visitor: Visitor) - if visitor.visitNil(node) then +local function visitExprConstantNil(node: types.AstExprConstantNil, visitor: Visitor) + if visitor.visitExprConstantNil(node) then visitToken(node, visitor) end end -local function visitBoolean(node: types.AstExprConstantBool, visitor: Visitor) - if visitor.visitBoolean(node) then +local function visitExprConstantBool(node: types.AstExprConstantBool, visitor: Visitor) + if visitor.visitExprConstantBool(node) then visitToken(node, visitor) end end -local function visitNumber(node: types.AstExprConstantNumber, visitor: Visitor) - if visitor.visitNumber(node) then +local function visitExprConstantNumber(node: types.AstExprConstantNumber, visitor: Visitor) + if visitor.visitExprConstantNumber(node) then visitToken(node, visitor) end end -local function visitLocalReference(node: types.AstExprLocal, visitor: Visitor) - if visitor.visitLocalReference(node) then +local function visitExprLocal(node: types.AstExprLocal, visitor: Visitor) + if visitor.visitExprLocal(node) then visitor.visitToken(node.token) end end -local function visitGlobal(node: types.AstExprGlobal, visitor: Visitor) - if visitor.visitGlobal(node) then +local function visitExprGlobal(node: types.AstExprGlobal, visitor: Visitor) + if visitor.visitExprGlobal(node) then visitor.visitToken(node.name) end end -local function visitVarargs(node: types.AstExprVarargs, visitor: Visitor) - if visitor.visitVarargs(node) then +local function visitExprVarargs(node: types.AstExprVarargs, visitor: Visitor) + if visitor.visitExprVarargs(node) then visitToken(node, visitor) end end -local function visitCall(node: types.AstExprCall, visitor: Visitor) - if visitor.visitCall(node) then - visitExpression(node.func, visitor) +local function visitExprCall(node: types.AstExprCall, visitor: Visitor) + if visitor.visitExprCall(node) then + visitExpr(node.func, visitor) if node.openparens then visitToken(node.openparens, visitor) end - visitPunctuated(node.arguments, visitor, visitExpression) + visitPunctuated(node.arguments, visitor, visitExpr) if node.closeparens then visitToken(node.closeparens, visitor) end end end -local function visitUnary(node: types.AstExprUnary, visitor: Visitor) - if visitor.visitUnary(node) then +local function visitExprUnary(node: types.AstExprUnary, visitor: Visitor) + if visitor.visitExprUnary(node) then visitToken(node.operator, visitor) - visitExpression(node.operand, visitor) + visitExpr(node.operand, visitor) end end -local function visitBinary(node: types.AstExprBinary, visitor: Visitor) - if visitor.visitBinary(node) then - visitExpression(node.lhsoperand, visitor) +local function visitExprBinary(node: types.AstExprBinary, visitor: Visitor) + if visitor.visitExprBinary(node) then + visitExpr(node.lhsoperand, visitor) visitToken(node.operator, visitor) - visitExpression(node.rhsoperand, visitor) + visitExpr(node.rhsoperand, visitor) end end -local function visitFunctionBody(node: types.AstFunctionBody, visitor: Visitor) +local function visitStatFunctionBody(node: types.AstFunctionBody, visitor: Visitor) if node.opengenerics then visitToken(node.opengenerics, visitor) end @@ -464,7 +466,7 @@ local function visitFunctionBody(node: types.AstFunctionBody, visitor: Visitor) if node.returnannotation then visitTypePack(node.returnannotation, visitor) end - visitBlock(node.body, visitor) + visitStatBlock(node.body, visitor) visitToken(node.endkeyword, visitor) end @@ -474,36 +476,36 @@ local function visitAttribute(node: types.AstAttribute, visitor: Visitor) end end -local function visitAnonymousFunction(node: types.AstExprAnonymousFunction, visitor: Visitor) - if visitor.visitAnonymousFunction(node) then +local function visitExprAnonymousFunction(node: types.AstExprAnonymousFunction, visitor: Visitor) + if visitor.visitExprAnonymousFunction(node) then for _, attribute in node.attributes do visitAttribute(attribute, visitor) end visitToken(node.functionkeyword, visitor) - visitFunctionBody(node.body, visitor) + visitStatFunctionBody(node.body, visitor) end end -local function visitFunction(node: types.AstStatFunction, visitor: Visitor) - if visitor.visitFunction(node) then +local function visitStatFunction(node: types.AstStatFunction, visitor: Visitor) + if visitor.visitStatFunction(node) then for _, attribute in node.attributes do visitAttribute(attribute, visitor) end visitToken(node.functionkeyword, visitor) - visitExpression(node.name, visitor) - visitFunctionBody(node.body, visitor) + visitExpr(node.name, visitor) + visitStatFunctionBody(node.body, visitor) end end -local function visitLocalFunction(node: types.AstStatLocalFunction, visitor: Visitor) - if visitor.visitLocalFunction(node) then +local function visitStatLocalFunction(node: types.AstStatLocalFunction, visitor: Visitor) + if visitor.visitStatLocalFunction(node) then for _, attribute in node.attributes do visitAttribute(attribute, visitor) end visitToken(node.localkeyword, visitor) visitToken(node.functionkeyword, visitor) visitLocal(node.name, visitor) - visitFunctionBody(node.body, visitor) + visitStatFunctionBody(node.body, visitor) end end @@ -515,24 +517,24 @@ local function visitStatTypeFunction(node: types.AstStatTypeFunction, visitor: V visitToken(node.type, visitor) visitToken(node.functionkeyword, visitor) visitToken(node.name, visitor) - visitFunctionBody(node.body, visitor) + visitStatFunctionBody(node.body, visitor) end end -local function visitTableItem(node: types.AstExprTableItem, visitor: Visitor) - if visitor.visitTableItem(node) then +local function visitExprTableItem(node: types.AstExprTableItem, visitor: Visitor) + if visitor.visitExprTableItem(node) then if node.kind == "list" then - visitExpression(node.value, visitor) + visitExpr(node.value, visitor) elseif node.kind == "record" then visitToken(node.key, visitor) visitToken(node.equals, visitor) - visitExpression(node.value, visitor) + visitExpr(node.value, visitor) elseif node.kind == "general" then visitToken(node.indexeropen, visitor) - visitExpression(node.key, visitor) + visitExpr(node.key, visitor) visitToken(node.indexerclose, visitor) visitToken(node.equals, visitor) - visitExpression(node.value, visitor) + visitExpr(node.value, visitor) else exhaustiveMatch(node.kind) end @@ -543,74 +545,74 @@ local function visitTableItem(node: types.AstExprTableItem, visitor: Visitor) end end -local function visitTable(node: types.AstExprTable, visitor: Visitor) - if visitor.visitTable(node) then +local function visitExprTable(node: types.AstExprTable, visitor: Visitor) + if visitor.visitExprTable(node) then visitToken(node.openbrace, visitor) for _, item in node.entries do - visitTableItem(item, visitor) + visitExprTableItem(item, visitor) end visitToken(node.closebrace, visitor) end end -local function visitIndexName(node: types.AstExprIndexName, visitor: Visitor) - if visitor.visitIndexName(node) then - visitExpression(node.expression, visitor) +local function visitExprIndexName(node: types.AstExprIndexName, visitor: Visitor) + if visitor.visitExprIndexName(node) then + visitExpr(node.expression, visitor) visitToken(node.accessor, visitor) visitToken(node.index, visitor) end end -local function visitIndexExpr(node: types.AstExprIndexExpr, visitor: Visitor) - if visitor.visitIndexExpr(node) then - visitExpression(node.expression, visitor) +local function visitExprIndexExpr(node: types.AstExprIndexExpr, visitor: Visitor) + if visitor.visitExprIndexExpr(node) then + visitExpr(node.expression, visitor) visitToken(node.openbrackets, visitor) - visitExpression(node.index, visitor) + visitExpr(node.index, visitor) visitToken(node.closebrackets, visitor) end end -local function visitGroup(node: types.AstExprGroup, visitor: Visitor) - if visitor.visitGroup(node) then +local function visitExprGroup(node: types.AstExprGroup, visitor: Visitor) + if visitor.visitExprGroup(node) then visitToken(node.openparens, visitor) - visitExpression(node.expression, visitor) + visitExpr(node.expression, visitor) visitToken(node.closeparens, visitor) end end -local function visitInterpolatedString(node: types.AstExprInterpString, visitor: Visitor) - if visitor.visitInterpolatedString(node) then +local function visitExprInterpString(node: types.AstExprInterpString, visitor: Visitor) + if visitor.visitExprInterpString(node) then for i = 1, #node.strings do visitToken(node.strings[i], visitor) if i <= #node.expressions then - visitExpression(node.expressions[i], visitor) + visitExpr(node.expressions[i], visitor) end end end end -local function visitTypeAssertion(node: types.AstExprTypeAssertion, visitor: Visitor) - if visitor.visitTypeAssertion(node) then - visitExpression(node.operand, visitor) +local function visitExprTypeAssertion(node: types.AstExprTypeAssertion, visitor: Visitor) + if visitor.visitExprTypeAssertion(node) then + visitExpr(node.operand, visitor) visitToken(node.operator, visitor) visitType(node.annotation, visitor) end end -local function visitIfExpression(node: types.AstExprIfElse, visitor: Visitor) - if visitor.visitIfExpression(node) then +local function visitExprIfElse(node: types.AstExprIfElse, visitor: Visitor) + if visitor.visitExprIfElse(node) then visitToken(node.ifkeyword, visitor) - visitExpression(node.condition, visitor) + visitExpr(node.condition, visitor) visitToken(node.thenkeyword, visitor) - visitExpression(node.thenexpr, visitor) + visitExpr(node.thenexpr, visitor) for _, elseifs in node.elseifs do visitToken(elseifs.elseifkeyword, visitor) - visitExpression(elseifs.condition, visitor) + visitExpr(elseifs.condition, visitor) visitToken(elseifs.thenkeyword, visitor) - visitExpression(elseifs.thenexpr, visitor) + visitExpr(elseifs.thenexpr, visitor) end visitToken(node.elsekeyword, visitor) - visitExpression(node.elseexpr, visitor) + visitExpr(node.elseexpr, visitor) end end @@ -643,14 +645,14 @@ local function visitTypeReference(node: types.AstTypeReference, visitor: Visitor end end -local function visitTypeBoolean(node: types.AstTypeSingletonBool, visitor: Visitor) - if visitor.visitTypeBoolean(node) then +local function visitTypeSingletonBool(node: types.AstTypeSingletonBool, visitor: Visitor) + if visitor.visitTypeSingletonBool(node) then visitToken(node, visitor) end end -local function visitTypeString(node: types.AstTypeSingletonString, visitor: Visitor) - if visitor.visitTypeString(node) then +local function visitTypeSingletonString(node: types.AstTypeSingletonString, visitor: Visitor) + if visitor.visitTypeSingletonString(node) then visitToken(node, visitor) end end @@ -659,7 +661,7 @@ local function visitTypeTypeof(node: types.AstTypeTypeof, visitor: Visitor) if visitor.visitTypeTypeof(node) then visitToken(node.typeof, visitor) visitToken(node.openparens, visitor) - visitExpression(node.expression, visitor) + visitExpr(node.expression, visitor) visitToken(node.closeparens, visitor) end end @@ -720,7 +722,7 @@ local function visitTypeTable(node: types.AstTypeTable, visitor: Visitor) visitToken(entry.indexerclose, visitor) elseif entry.kind == "stringproperty" then visitToken(entry.indexeropen, visitor) - visitTypeString(entry.key, visitor) + visitTypeSingletonString(entry.key, visitor) visitToken(entry.indexerclose, visitor) else visitToken(entry.key, visitor) @@ -801,87 +803,87 @@ local function visitTypePackVariadic(node: types.AstTypePackVariadic, visitor: V end end -function visitExpression(expression: types.AstExpr, visitor: Visitor) - if visitor.visitExpression(expression) then +function visitExpr(expression: types.AstExpr, visitor: Visitor) + if visitor.visitExpr(expression) then if expression.tag == "nil" then - visitNil(expression, visitor) + visitExprConstantNil(expression, visitor) elseif expression.tag == "boolean" then - visitBoolean(expression, visitor) + visitExprConstantBool(expression, visitor) elseif expression.tag == "number" then - visitNumber(expression, visitor) + visitExprConstantNumber(expression, visitor) elseif expression.tag == "string" then - visitString(expression, visitor) + visitExprConstantString(expression, visitor) elseif expression.tag == "local" then - visitLocalReference(expression, visitor) + visitExprLocal(expression, visitor) elseif expression.tag == "global" then - visitGlobal(expression, visitor) + visitExprGlobal(expression, visitor) elseif expression.tag == "vararg" then - visitVarargs(expression, visitor) + visitExprVarargs(expression, visitor) elseif expression.tag == "call" then - visitCall(expression, visitor) + visitExprCall(expression, visitor) elseif expression.tag == "unary" then - visitUnary(expression, visitor) + visitExprUnary(expression, visitor) elseif expression.tag == "binary" then - visitBinary(expression, visitor) + visitExprBinary(expression, visitor) elseif expression.tag == "function" then - visitAnonymousFunction(expression, visitor) + visitExprAnonymousFunction(expression, visitor) elseif expression.tag == "table" then - visitTable(expression, visitor) + visitExprTable(expression, visitor) elseif expression.tag == "indexname" then - visitIndexName(expression, visitor) + visitExprIndexName(expression, visitor) elseif expression.tag == "index" then - visitIndexExpr(expression, visitor) + visitExprIndexExpr(expression, visitor) elseif expression.tag == "group" then - visitGroup(expression, visitor) + visitExprGroup(expression, visitor) elseif expression.tag == "interpolatedstring" then - visitInterpolatedString(expression, visitor) + visitExprInterpString(expression, visitor) elseif expression.tag == "cast" then - visitTypeAssertion(expression, visitor) + visitExprTypeAssertion(expression, visitor) elseif expression.tag == "conditional" then - visitIfExpression(expression, visitor) + visitExprIfElse(expression, visitor) else exhaustiveMatch(expression.tag) end - visitor.visitExpressionEnd(expression) + visitor.visitExprEnd(expression) end end function visitStatement(statement: types.AstStat, visitor: Visitor) if statement.tag == "block" then - visitBlock(statement, visitor) + visitStatBlock(statement, visitor) elseif statement.tag == "do" then visitStatDo(statement, visitor) elseif statement.tag == "conditional" then - visitIf(statement, visitor) + visitStatIf(statement, visitor) elseif statement.tag == "expression" then visitStatExpr(statement, visitor) elseif statement.tag == "local" then visitLocalStatement(statement, visitor) elseif statement.tag == "return" then - visitReturn(statement, visitor) + visitStatReturn(statement, visitor) elseif statement.tag == "while" then - visitWhile(statement, visitor) + visitStatWhile(statement, visitor) elseif statement.tag == "break" then - visitBreak(statement, visitor) + visitStatBreak(statement, visitor) elseif statement.tag == "continue" then - visitContinue(statement, visitor) + visitStatContinue(statement, visitor) elseif statement.tag == "repeat" then - visitRepeat(statement, visitor) + visitStatRepeat(statement, visitor) elseif statement.tag == "for" then - visitFor(statement, visitor) + visitStatFor(statement, visitor) elseif statement.tag == "forin" then - visitForIn(statement, visitor) + visitStatForIn(statement, visitor) elseif statement.tag == "assign" then - visitAssign(statement, visitor) + visitStatAssign(statement, visitor) elseif statement.tag == "compoundassign" then - visitCompoundAssign(statement, visitor) + visitStatCompoundAssign(statement, visitor) elseif statement.tag == "function" then - visitFunction(statement, visitor) + visitStatFunction(statement, visitor) elseif statement.tag == "localfunction" then - visitLocalFunction(statement, visitor) + visitStatLocalFunction(statement, visitor) elseif statement.tag == "typealias" then - visitTypeAlias(statement, visitor) + visitStatTypeAlias(statement, visitor) elseif statement.tag == "typefunction" then visitStatTypeFunction(statement, visitor) else @@ -893,9 +895,9 @@ function visitType(type: types.AstType, visitor: Visitor) if type.tag == "reference" then visitTypeReference(type, visitor) elseif type.tag == "boolean" then - visitTypeBoolean(type, visitor) + visitTypeSingletonBool(type, visitor) elseif type.tag == "string" then - visitTypeString(type, visitor) + visitTypeSingletonString(type, visitor) elseif type.tag == "typeof" then visitTypeTypeof(type, visitor) elseif type.tag == "group" then @@ -932,47 +934,47 @@ end local function create(visit: ((types.AstNode) -> boolean)?): Visitor if visit then return { - visitBlock = visit, - visitBlockEnd = visit, + visitStatBlock = visit, + visitStatBlockEnd = visit, visitStatDo = visit, - visitIf = visit, - visitWhile = visit, - visitRepeat = visit, - visitBreak = visit, - visitContinue = visit, - visitReturn = visit, - visitLocalDeclaration = visit, - visitLocalDeclarationEnd = visit, - visitFor = visit, - visitForIn = visit, - visitAssign = visit, - visitCompoundAssign = visit, - visitFunction = visit, - visitLocalFunction = visit, - visitTypeAlias = visit, + visitStatIf = visit, + visitStatWhile = visit, + visitStatRepeat = visit, + visitStatBreak = visit, + visitStatContinue = visit, + visitStatReturn = visit, + visitStatLocalDeclaration = visit, + visitStatLocalDeclarationEnd = visit, + visitStatFor = visit, + visitStatForIn = visit, + visitStatAssign = visit, + visitStatCompoundAssign = visit, + visitStatFunction = visit, + visitStatLocalFunction = visit, + visitStatTypeAlias = visit, visitStatTypeFunction = visit, visitStatExpr = visit, - visitExpression = visit, - visitExpressionEnd = visit, - visitLocalReference = visit, - visitGlobal = visit, - visitCall = visit, - visitUnary = visit, - visitBinary = visit, - visitAnonymousFunction = visit, - visitTableItem = visit, - visitTable = visit, - visitIndexName = visit, - visitIndexExpr = visit, - visitGroup = visit, - visitInterpolatedString = visit, - visitTypeAssertion = visit, - visitIfExpression = visit, + visitExpr = visit, + visitExprEnd = visit, + visitExprLocal = visit, + visitExprGlobal = visit, + visitExprCall = visit, + visitExprUnary = visit, + visitExprBinary = visit, + visitExprAnonymousFunction = visit, + visitExprTableItem = visit, + visitExprTable = visit, + visitExprIndexName = visit, + visitExprIndexExpr = visit, + visitExprGroup = visit, + visitExprInterpString = visit, + visitExprTypeAssertion = visit, + visitExprIfElse = visit, visitTypeReference = visit, - visitTypeBoolean = visit, - visitTypeString = visit, + visitTypeSingletonBool = visit, + visitTypeSingletonString = visit, visitTypeTypeof = visit, visitTypeGroup = visit, visitTypeOptional = visit, @@ -987,12 +989,12 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitTypePackVariadic = visit, visitToken = visit, - visitNil = visit, - visitString = visit, - visitBoolean = visit, - visitNumber = visit, + visitExprConstantNil = visit, + visitExprConstantString = visit, + visitExprConstantBool = visit, + visitExprConstantNumber = visit, visitLocal = visit, - visitVarargs = visit, + visitExprVarargs = visit, visitAttribute = visit, } @@ -1005,7 +1007,7 @@ local function visit(node: types.AstNode, visitor: Visitor) if node.kind == "stat" then visitStatement(node, visitor) elseif node.kind == "expr" then - visitExpression(node, visitor) + visitExpr(node, visitor) elseif node.kind == "type" then visitType(node, visitor) elseif node.kind == "typepack" then @@ -1017,16 +1019,16 @@ local function visit(node: types.AstNode, visitor: Visitor) elseif node.istoken then visitToken(node, visitor) elseif node.istableitem then - visitTableItem(node, visitor) + visitExprTableItem(node, visitor) else exhaustiveMatch(node.kind) end end visitorlib.create = create -visitorlib.visitblock = visitBlock +visitorlib.visitblock = visitStatBlock visitorlib.visitstatement = visitStatement -visitorlib.visitexpression = visitExpression +visitorlib.visitexpression = visitExpr visitorlib.visittype = visitType visitorlib.visittypepack = visitTypePack visitorlib.visittoken = visitToken From c9b66014a9126285e7fd3527a68eea37e250dbb0 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:31:31 -0800 Subject: [PATCH 234/642] CLI refactor: move out require setup code into separate source files (#668) Move all setup for the various CLI-facing virtual filesystems into their own files: `requiresetup.{h,cpp}`. Our `climain.cpp` file was getting a bit confusing to parse (and would have gotten worse after merging #657). Ran clang-format, and also made some trivial changes to clean up some other files in CLI. --- lute/cli/CMakeLists.txt | 2 + lute/cli/include/lute/climain.h | 1 - lute/cli/include/lute/requiresetup.h | 16 +++ lute/cli/include/lute/tc.h | 7 +- lute/cli/src/climain.cpp | 142 ++------------------------- lute/cli/src/fileutils.cpp | 3 +- lute/cli/src/main.cpp | 2 +- lute/cli/src/requiresetup.cpp | 138 ++++++++++++++++++++++++++ lute/cli/src/tc.cpp | 10 +- tests/src/cliruntimefixture.cpp | 7 +- tests/src/cliruntimefixture.h | 2 +- 11 files changed, 177 insertions(+), 153 deletions(-) create mode 100644 lute/cli/include/lute/requiresetup.h create mode 100644 lute/cli/src/requiresetup.cpp diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 6a61f6c2c..7c04711c2 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -28,6 +28,7 @@ target_sources(Lute.CLI.lib PRIVATE include/lute/fileutils.h include/lute/luauflags.h include/lute/reporter.h + include/lute/requiresetup.h include/lute/staticrequires.h include/lute/tc.h include/lute/uvstate.h @@ -36,6 +37,7 @@ target_sources(Lute.CLI.lib PRIVATE src/compile.cpp src/fileutils.cpp src/luauflags.cpp + src/requiresetup.cpp src/staticrequires.cpp src/tc.cpp src/uvstate.cpp diff --git a/lute/cli/include/lute/climain.h b/lute/cli/include/lute/climain.h index 3ebb588ec..30f219e14 100644 --- a/lute/cli/include/lute/climain.h +++ b/lute/cli/include/lute/climain.h @@ -6,7 +6,6 @@ struct lua_State; struct Runtime; class LuteReporter; -lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit = nullptr); int cliMain(int argc, char** argv, LuteReporter& reporter); bool runBytecode( Runtime& runtime, diff --git a/lute/cli/include/lute/requiresetup.h b/lute/cli/include/lute/requiresetup.h new file mode 100644 index 000000000..fcea867fd --- /dev/null +++ b/lute/cli/include/lute/requiresetup.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Luau/DenseHash.h" + +#include +#include + +struct lua_State; +struct Runtime; + +lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit = nullptr); +lua_State* setupBundleState( + Runtime& runtime, + Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap bundleMap +); diff --git a/lute/cli/include/lute/tc.h b/lute/cli/include/lute/tc.h index fc3f98272..99af61bdc 100644 --- a/lute/cli/include/lute/tc.h +++ b/lute/cli/include/lute/tc.h @@ -1,9 +1,8 @@ #pragma once -#include "Luau/FileResolver.h" -#include "Luau/FileUtils.h" -#include "Luau/Frontend.h" - #include "lute/reporter.h" +#include +#include + int typecheck(const std::vector& sourceFiles, LuteReporter& reporter); diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 7141a912d..1e208238c 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -1,52 +1,36 @@ #include "lute/climain.h" -#include "lute/bundlevfs.h" #include "lute/clicommands.h" -#include "lute/clivfs.h" #include "lute/compile.h" -#include "lute/crypto.h" -#include "lute/fs.h" -#include "lute/io.h" -#include "lute/luau.h" #include "lute/luauflags.h" -#include "lute/net.h" +#include "lute/modulepath.h" #include "lute/options.h" #include "lute/process.h" #include "lute/ref.h" #include "lute/reporter.h" -#include "lute/require.h" -#include "lute/requirevfs.h" +#include "lute/requiresetup.h" #include "lute/runtime.h" #include "lute/staticrequires.h" -#include "lute/system.h" -#include "lute/task.h" #include "lute/tc.h" -#include "lute/time.h" #include "lute/version.h" -#include "lute/vm.h" #include "Luau/CodeGen.h" #include "Luau/Common.h" #include "Luau/Compiler.h" #include "Luau/DenseHash.h" #include "Luau/FileUtils.h" -#include "Luau/Require.h" #include "lua.h" #include "lualib.h" -#include "uv.h" - -#include +#include +#include +#include #ifdef _WIN32 #include #endif -#include -#include -#include - static const char* HELP_STRING = R"(Usage: lute [options] [arguments...] Commands: @@ -116,120 +100,6 @@ Compile Options: -h, --help Display this usage message. )"; -void* createCliRequireContext(lua_State* L) -{ - void* ctx = lua_newuserdatadtor( - L, - sizeof(RequireCtx), - [](void* ptr) - { - std::destroy_at(static_cast(ptr)); - } - ); - - if (!ctx) - luaL_error(L, "unable to allocate RequireCtx"); - - ctx = new (ctx) RequireCtx{std::make_unique(CliVfs{})}; - - // Store RequireCtx in the registry to keep it alive for the lifetime of - // this lua_State. Memory address is used as a key to avoid collisions. - lua_pushlightuserdata(L, ctx); - lua_insert(L, -2); - lua_settable(L, LUA_REGISTRYINDEX); - - return ctx; -} - -static void luteopen_libs(lua_State* L) -{ - std::vector> libs = {{ - {"@lute/crypto", luteopen_crypto}, - {"@lute/fs", luteopen_fs}, - {"@lute/luau", luteopen_luau}, - {"@lute/net", luteopen_net}, - {"@lute/process", luteopen_process}, - {"@lute/task", luteopen_task}, - {"@lute/vm", luteopen_vm}, - {"@lute/system", luteopen_system}, - {"@lute/time", luteopen_time}, - {"@lute/io", luteopen_io}, - }}; - - for (const auto& [name, func] : libs) - { - lua_pushcfunction(L, luarequire_registermodule, nullptr); - lua_pushstring(L, name); - func(L); - lua_call(L, 2, 0); - } -} - -void* createBundleRequireContext( - lua_State* L, - Luau::DenseHashMap luaurcFiles, - Luau::DenseHashMap bundleMap -) -{ - void* ctx = lua_newuserdatadtor( - L, - sizeof(RequireCtx), - [](void* ptr) - { - std::destroy_at(static_cast(ptr)); - } - ); - - if (!ctx) - luaL_error(L, "unable to allocate RequireCtx"); - ctx = new (ctx) RequireCtx{std::make_unique(BundleVfs{std::move(luaurcFiles), std::move(bundleMap)})}; - - // Store RequireCtx in the registry to keep it alive for the lifetime of - // this lua_State. Memory address is used as a key to avoid collisions. - lua_pushlightuserdata(L, ctx); - lua_insert(L, -2); - lua_settable(L, LUA_REGISTRYINDEX); - - return ctx; -} - -lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit) -{ - return setupState( - runtime, - [preSandboxInit = std::move(preSandboxInit)](lua_State* L) - { - luteopen_libs(L); - - if (Luau::CodeGen::isSupported()) - Luau::CodeGen::create(L); - - luaopen_require(L, requireConfigInit, createCliRequireContext(L)); - if (preSandboxInit) - preSandboxInit(L); - } - ); -} - -lua_State* setupBundleState( - Runtime& runtime, - Luau::DenseHashMap luaurcFiles, - Luau::DenseHashMap bundleMap -) -{ - return setupState( - runtime, - [luaurcFiles = std::move(luaurcFiles), bundleMap = std::move(bundleMap)](lua_State* L) - { - luteopen_libs(L); - if (Luau::CodeGen::isSupported()) - Luau::CodeGen::create(L); - - luaopen_require(L, requireConfigInit, createBundleRequireContext(L, std::move(luaurcFiles), std::move(bundleMap))); - } - ); -} - static bool setupArguments(lua_State* L, int argc, char** argv) { if (!lua_checkstack(L, argc)) @@ -352,7 +222,7 @@ static std::pair getWithRequireByStringSemantics(std::string result = {false, "File or directory not found."}; break; } - + return result; }; diff --git a/lute/cli/src/fileutils.cpp b/lute/cli/src/fileutils.cpp index 215f10c7e..4be9a4585 100644 --- a/lute/cli/src/fileutils.cpp +++ b/lute/cli/src/fileutils.cpp @@ -16,9 +16,10 @@ #include #endif -#include #include "Luau/FileUtils.h" +#include + namespace Lute { diff --git a/lute/cli/src/main.cpp b/lute/cli/src/main.cpp index e120e6738..b9c0dfe71 100644 --- a/lute/cli/src/main.cpp +++ b/lute/cli/src/main.cpp @@ -1,6 +1,6 @@ #include "lute/climain.h" -#include "lute/uvstate.h" #include "lute/clireporter.h" +#include "lute/uvstate.h" int main(int argc, char** argv) { diff --git a/lute/cli/src/requiresetup.cpp b/lute/cli/src/requiresetup.cpp new file mode 100644 index 000000000..1f562dbcd --- /dev/null +++ b/lute/cli/src/requiresetup.cpp @@ -0,0 +1,138 @@ +#include "lute/requiresetup.h" + +#include "lute/bundlevfs.h" +#include "lute/clivfs.h" +#include "lute/crypto.h" +#include "lute/fs.h" +#include "lute/io.h" +#include "lute/luau.h" +#include "lute/net.h" +#include "lute/process.h" +#include "lute/require.h" +#include "lute/requirevfs.h" +#include "lute/runtime.h" +#include "lute/system.h" +#include "lute/task.h" +#include "lute/time.h" +#include "lute/vm.h" + +#include "Luau/CodeGen.h" +#include "Luau/Require.h" + +#include +#include +#include + +static void luteopen_libs(lua_State* L) +{ + std::vector> libs = {{ + {"@lute/crypto", luteopen_crypto}, + {"@lute/fs", luteopen_fs}, + {"@lute/luau", luteopen_luau}, + {"@lute/net", luteopen_net}, + {"@lute/process", luteopen_process}, + {"@lute/task", luteopen_task}, + {"@lute/vm", luteopen_vm}, + {"@lute/system", luteopen_system}, + {"@lute/time", luteopen_time}, + {"@lute/io", luteopen_io}, + }}; + + for (const auto& [name, func] : libs) + { + lua_pushcfunction(L, luarequire_registermodule, nullptr); + lua_pushstring(L, name); + func(L); + lua_call(L, 2, 0); + } +} + +static void* createCliRequireContext(lua_State* L) +{ + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + std::destroy_at(static_cast(ptr)); + } + ); + + if (!ctx) + luaL_error(L, "unable to allocate RequireCtx"); + + ctx = new (ctx) RequireCtx{std::make_unique(CliVfs{})}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + return ctx; +} + +static void* createBundleRequireContext( + lua_State* L, + Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap bundleMap +) +{ + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + std::destroy_at(static_cast(ptr)); + } + ); + + if (!ctx) + luaL_error(L, "unable to allocate RequireCtx"); + ctx = new (ctx) RequireCtx{std::make_unique(BundleVfs{std::move(luaurcFiles), std::move(bundleMap)})}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + return ctx; +} + +lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit) +{ + return setupState( + runtime, + [preSandboxInit = std::move(preSandboxInit)](lua_State* L) + { + luteopen_libs(L); + + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createCliRequireContext(L)); + if (preSandboxInit) + preSandboxInit(L); + } + ); +} + +lua_State* setupBundleState( + Runtime& runtime, + Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap bundleMap +) +{ + return setupState( + runtime, + [luaurcFiles = std::move(luaurcFiles), bundleMap = std::move(bundleMap)](lua_State* L) + { + luteopen_libs(L); + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createBundleRequireContext(L, std::move(luaurcFiles), std::move(bundleMap))); + } + ); +} diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index 025f7865d..60e6afe4e 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -1,13 +1,13 @@ #include "lute/tc.h" -#include "Luau/BuiltinDefinitions.h" -#include "Luau/Error.h" -#include "Luau/PrettyPrinter.h" -#include "Luau/TypeAttach.h" - #include "lute/configresolver.h" #include "lute/moduleresolver.h" +#include "Luau/BuiltinDefinitions.h" +#include "Luau/Error.h" +#include "Luau/FileUtils.h" +#include "Luau/Frontend.h" + static const std::string kLuteDefinitions = R"LUTE_TYPES( -- Net api declare net: { diff --git a/tests/src/cliruntimefixture.cpp b/tests/src/cliruntimefixture.cpp index 033696078..70b21caf2 100644 --- a/tests/src/cliruntimefixture.cpp +++ b/tests/src/cliruntimefixture.cpp @@ -1,14 +1,13 @@ #include "cliruntimefixture.h" +#include "lute/climain.h" +#include "lute/requiresetup.h" + #include "Luau/Compiler.h" #include "lua.h" #include "lualib.h" -#include "Luau/Compiler.h" - -#include "Luau/Compiler.h" - static int report(lua_State* L) { const char* str = luaL_tolstring(L, 1, nullptr); diff --git a/tests/src/cliruntimefixture.h b/tests/src/cliruntimefixture.h index e294d165c..868ca3479 100644 --- a/tests/src/cliruntimefixture.h +++ b/tests/src/cliruntimefixture.h @@ -1,5 +1,5 @@ #pragma once -#include "lute/climain.h" + #include "lute/runtime.h" #include "lutefixture.h" From 389c3f4022c838afb79a5e39626c779464e4fa0c Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:36:47 -0800 Subject: [PATCH 235/642] Sets `lute` to enable all `Luau*` flags by default (#669) This now matches the default behavior of Luau's CLI tools. We can still use `setLuauFlag` to disable any problematic flags. --- lute/cli/src/luauflags.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lute/cli/src/luauflags.cpp b/lute/cli/src/luauflags.cpp index 8de427da6..a054d3689 100644 --- a/lute/cli/src/luauflags.cpp +++ b/lute/cli/src/luauflags.cpp @@ -1,11 +1,21 @@ #include "lute/luauflags.h" #include "Luau/Common.h" +#include "Luau/ExperimentalFlags.h" #include #include -static void setLuauFlag(std::string_view name, bool state) +static void enableAllLuauFlags() +{ + for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) + { + if (strncmp(flag->name, "Luau", 4) == 0 && !Luau::isAnalysisFlagExperimental(flag->name)) + flag->value = true; + } +} + +[[maybe_unused]] static void setLuauFlag(std::string_view name, bool state) { for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) { @@ -21,6 +31,8 @@ static void setLuauFlag(std::string_view name, bool state) void setLuauFlags() { - setLuauFlag("LuauAutocompleteAttributes", true); - setLuauFlag("LuauCstStatDoWithStatsStart", true); + enableAllLuauFlags(); + + // Individual flags can be overridden here as needed, e.g.: + // setLuauFlag("LuauSomeFlagThatCausedARegression", false); } From d0192078e35d0dd0d766aa28df92fd292534f567 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:43:51 -0800 Subject: [PATCH 236/642] Fixes JSON number deserialization and adds a test suite (#670) Resolves #652 and #653 Added all of the tests for JSON numbers from https://github.com/nst/JSONTestSuite, and fixed all of the bugs related to numbers --- lute/std/libs/json.luau | 47 ++++++++++------- .../i_number_double_huge_neg_exp.json | 1 + .../i_number_huge_exp.json | 1 + .../i_number_neg_int_huge_exp.json | 1 + .../i_number_pos_double_huge_exp.json | 1 + .../i_number_real_neg_overflow.json | 1 + .../i_number_real_pos_overflow.json | 1 + .../i_number_real_underflow.json | 1 + .../i_number_too_big_neg_int.json | 1 + .../i_number_too_big_pos_int.json | 1 + .../i_number_very_big_negative_int.json | 1 + .../std/JSONParsingTestSuite/n_number_++.json | 1 + .../std/JSONParsingTestSuite/n_number_+1.json | 1 + .../JSONParsingTestSuite/n_number_+Inf.json | 1 + .../JSONParsingTestSuite/n_number_-01.json | 1 + .../JSONParsingTestSuite/n_number_-1.0..json | 1 + .../JSONParsingTestSuite/n_number_-2..json | 1 + .../JSONParsingTestSuite/n_number_-NaN.json | 1 + .../JSONParsingTestSuite/n_number_.-1.json | 1 + .../JSONParsingTestSuite/n_number_.2e-3.json | 1 + .../JSONParsingTestSuite/n_number_0.1.2.json | 1 + .../JSONParsingTestSuite/n_number_0.3e+.json | 1 + .../JSONParsingTestSuite/n_number_0.3e.json | 1 + .../JSONParsingTestSuite/n_number_0.e1.json | 1 + .../n_number_0_capital_E+.json | 1 + .../n_number_0_capital_E.json | 1 + .../JSONParsingTestSuite/n_number_0e+.json | 1 + .../std/JSONParsingTestSuite/n_number_0e.json | 1 + .../JSONParsingTestSuite/n_number_1.0e+.json | 1 + .../JSONParsingTestSuite/n_number_1.0e-.json | 1 + .../JSONParsingTestSuite/n_number_1.0e.json | 1 + .../JSONParsingTestSuite/n_number_1_000.json | 1 + .../JSONParsingTestSuite/n_number_1eE2.json | 1 + .../JSONParsingTestSuite/n_number_2.e+3.json | 1 + .../JSONParsingTestSuite/n_number_2.e-3.json | 1 + .../JSONParsingTestSuite/n_number_2.e3.json | 1 + .../JSONParsingTestSuite/n_number_9.e+.json | 1 + .../JSONParsingTestSuite/n_number_Inf.json | 1 + .../JSONParsingTestSuite/n_number_NaN.json | 1 + .../n_number_U+FF11_fullwidth_digit_one.json | 1 + .../n_number_expression.json | 1 + .../n_number_hex_1_digit.json | 1 + .../n_number_hex_2_digits.json | 1 + .../n_number_infinity.json | 1 + .../n_number_invalid+-.json | 1 + .../n_number_invalid-negative-real.json | 1 + .../n_number_invalid-utf-8-in-bigger-int.json | 1 + .../n_number_invalid-utf-8-in-exponent.json | 1 + .../n_number_invalid-utf-8-in-int.json | 1 + .../n_number_minus_infinity.json | 1 + ...mber_minus_sign_with_trailing_garbage.json | 1 + .../n_number_minus_space_1.json | 1 + .../n_number_neg_int_starting_with_zero.json | 1 + .../n_number_neg_real_without_int_part.json | 1 + .../n_number_neg_with_garbage_at_end.json | 1 + .../n_number_real_garbage_after_e.json | 1 + ...number_real_with_invalid_utf8_after_e.json | 1 + ...n_number_real_without_fractional_part.json | 1 + .../n_number_starting_with_dot.json | 1 + .../n_number_with_alpha.json | 1 + .../n_number_with_alpha_char.json | 1 + .../n_number_with_leading_zero.json | 1 + tests/std/JSONParsingTestSuite/y_number.json | 1 + .../JSONParsingTestSuite/y_number_0e+1.json | 1 + .../JSONParsingTestSuite/y_number_0e1.json | 1 + .../y_number_after_space.json | 1 + .../y_number_double_close_to_zero.json | 1 + .../y_number_int_with_exp.json | 1 + .../y_number_minus_zero.json | 1 + .../y_number_negative_int.json | 1 + .../y_number_negative_one.json | 1 + .../y_number_negative_zero.json | 1 + .../y_number_real_capital_e.json | 1 + .../y_number_real_capital_e_neg_exp.json | 1 + .../y_number_real_capital_e_pos_exp.json | 1 + .../y_number_real_exponent.json | 1 + .../y_number_real_fraction_exponent.json | 1 + .../y_number_real_neg_exp.json | 1 + .../y_number_real_pos_exponent.json | 1 + .../y_number_simple_int.json | 1 + .../y_number_simple_real.json | 1 + tests/std/json.test.luau | 51 +++++++++++++++++++ 82 files changed, 161 insertions(+), 17 deletions(-) create mode 100644 tests/std/JSONParsingTestSuite/i_number_double_huge_neg_exp.json create mode 100644 tests/std/JSONParsingTestSuite/i_number_huge_exp.json create mode 100755 tests/std/JSONParsingTestSuite/i_number_neg_int_huge_exp.json create mode 100755 tests/std/JSONParsingTestSuite/i_number_pos_double_huge_exp.json create mode 100644 tests/std/JSONParsingTestSuite/i_number_real_neg_overflow.json create mode 100644 tests/std/JSONParsingTestSuite/i_number_real_pos_overflow.json create mode 100644 tests/std/JSONParsingTestSuite/i_number_real_underflow.json create mode 100644 tests/std/JSONParsingTestSuite/i_number_too_big_neg_int.json create mode 100644 tests/std/JSONParsingTestSuite/i_number_too_big_pos_int.json create mode 100755 tests/std/JSONParsingTestSuite/i_number_very_big_negative_int.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_++.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_+1.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_+Inf.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_-01.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_-1.0..json create mode 100755 tests/std/JSONParsingTestSuite/n_number_-2..json create mode 100755 tests/std/JSONParsingTestSuite/n_number_-NaN.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_.-1.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_.2e-3.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_0.1.2.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_0.3e+.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_0.3e.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_0.e1.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_0_capital_E+.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_0_capital_E.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_0e+.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_0e.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_1.0e+.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_1.0e-.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_1.0e.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_1_000.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_1eE2.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_2.e+3.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_2.e-3.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_2.e3.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_9.e+.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_Inf.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_NaN.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_U+FF11_fullwidth_digit_one.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_expression.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_hex_1_digit.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_hex_2_digits.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_infinity.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_invalid+-.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_invalid-negative-real.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-bigger-int.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-exponent.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-int.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_minus_infinity.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_minus_sign_with_trailing_garbage.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_minus_space_1.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_neg_int_starting_with_zero.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_neg_real_without_int_part.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_neg_with_garbage_at_end.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_real_garbage_after_e.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_real_with_invalid_utf8_after_e.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_real_without_fractional_part.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_starting_with_dot.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_with_alpha.json create mode 100644 tests/std/JSONParsingTestSuite/n_number_with_alpha_char.json create mode 100755 tests/std/JSONParsingTestSuite/n_number_with_leading_zero.json create mode 100644 tests/std/JSONParsingTestSuite/y_number.json create mode 100755 tests/std/JSONParsingTestSuite/y_number_0e+1.json create mode 100755 tests/std/JSONParsingTestSuite/y_number_0e1.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_after_space.json create mode 100755 tests/std/JSONParsingTestSuite/y_number_double_close_to_zero.json create mode 100755 tests/std/JSONParsingTestSuite/y_number_int_with_exp.json create mode 100755 tests/std/JSONParsingTestSuite/y_number_minus_zero.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_negative_int.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_negative_one.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_negative_zero.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_real_capital_e.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_real_capital_e_neg_exp.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_real_capital_e_pos_exp.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_real_exponent.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_real_fraction_exponent.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_real_neg_exp.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_real_pos_exponent.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_simple_int.json create mode 100644 tests/std/JSONParsingTestSuite/y_number_simple_real.json create mode 100644 tests/std/json.test.luau diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 63a6795ff..bd211ebcd 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -228,7 +228,7 @@ local function deserializerError(state: DeserializerState, msg: string): never end local function skipWhitespace(state: DeserializerState): boolean - state.cursor = string.find(state.src, "%S", state.cursor) :: number + state.cursor = string.find(state.src, "[^ \n\r\t]", state.cursor) :: number if not state.cursor then return false @@ -242,32 +242,38 @@ local function currentByte(state: DeserializerState) end local function deserializeNumber(state: DeserializerState) - -- first "segment" - local nStart, nEnd = string.find(state.src, "^[%-%deE]*", state.cursor) - + -- Integer part + local nStart, nEnd = string.find(state.src, "^-?[1-9]%d*", state.cursor) if not nStart then - -- i dont think this is possible - deserializerError(state, "Could not match a number literal?") - end - - if string.byte(state.src, nEnd :: number + 1) == string.byte(".") then -- decimal! - local decStart, decEnd = string.find(state.src, "^[eE%-+%d]+", nEnd :: number + 2) + nStart, nEnd = string.find(state.src, "^-?0", state.cursor) + if not nStart then + deserializerError(state, "Malformed number (bad integer part)") + end - if not decStart then - deserializerError(state, "Trailing '.' in number value") + local nextChar = string.byte(state.src, nEnd :: number + 1) + if nextChar and nextChar >= string.byte("0") and nextChar <= string.byte("9") then + deserializerError(state, "Malformed number (leading zeros not allowed)") end + end + -- Decimal part + local decStart, decEnd = string.find(state.src, "^%.%d+", nEnd :: number + 1) + if decStart then nEnd = decEnd end - local num = tonumber(string.sub(state.src, nStart :: number, nEnd)) + -- Exponential part + local expStart, expEnd = string.find(state.src, "^[eE][+-]?%d+", nEnd :: number + 1) + if expStart then + nEnd = expEnd + end + local num = tonumber(string.sub(state.src, nStart :: number, nEnd)) if not num then deserializerError(state, "Malformed number value") end state.cursor = nEnd :: number + 1 - return num end @@ -453,8 +459,8 @@ deserialize = function(state: DeserializerState): value elseif string.sub(state.src, state.cursor, state.cursor + 4) == "false" then state.cursor += 5 return false - elseif string.match(state.src, "^[%-%d%.]", state.cursor) then - -- number + elseif string.match(state.src, "^[%-%d]", state.cursor) then + -- potential number return deserializeNumber(state) elseif string.byte(state.src, state.cursor) == string.byte('"') then return deserializeString(state) @@ -488,7 +494,14 @@ function json.deserialize(src: string) cursor = 0, } - return deserialize(state) + local result = deserialize(state) + + -- After parsing a value, there must be no non-whitespace trailing characters. + if skipWhitespace(state) then + deserializerError(state, "Trailing characters after JSON value") + end + + return result end function json.object(props: { [string]: value }): object diff --git a/tests/std/JSONParsingTestSuite/i_number_double_huge_neg_exp.json b/tests/std/JSONParsingTestSuite/i_number_double_huge_neg_exp.json new file mode 100644 index 000000000..ae4c7b71f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_double_huge_neg_exp.json @@ -0,0 +1 @@ +[123.456e-789] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_huge_exp.json b/tests/std/JSONParsingTestSuite/i_number_huge_exp.json new file mode 100644 index 000000000..9b5efa236 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_huge_exp.json @@ -0,0 +1 @@ +[0.4e00669999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999969999999006] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_neg_int_huge_exp.json b/tests/std/JSONParsingTestSuite/i_number_neg_int_huge_exp.json new file mode 100755 index 000000000..3abd58a5c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_neg_int_huge_exp.json @@ -0,0 +1 @@ +[-1e+9999] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_pos_double_huge_exp.json b/tests/std/JSONParsingTestSuite/i_number_pos_double_huge_exp.json new file mode 100755 index 000000000..e10a7eb62 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_pos_double_huge_exp.json @@ -0,0 +1 @@ +[1.5e+9999] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_real_neg_overflow.json b/tests/std/JSONParsingTestSuite/i_number_real_neg_overflow.json new file mode 100644 index 000000000..3d628a994 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_real_neg_overflow.json @@ -0,0 +1 @@ +[-123123e100000] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_real_pos_overflow.json b/tests/std/JSONParsingTestSuite/i_number_real_pos_overflow.json new file mode 100644 index 000000000..54d7d3dcd --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_real_pos_overflow.json @@ -0,0 +1 @@ +[123123e100000] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_real_underflow.json b/tests/std/JSONParsingTestSuite/i_number_real_underflow.json new file mode 100644 index 000000000..c5236eb26 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_real_underflow.json @@ -0,0 +1 @@ +[123e-10000000] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_too_big_neg_int.json b/tests/std/JSONParsingTestSuite/i_number_too_big_neg_int.json new file mode 100644 index 000000000..dfa384619 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_too_big_neg_int.json @@ -0,0 +1 @@ +[-123123123123123123123123123123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_too_big_pos_int.json b/tests/std/JSONParsingTestSuite/i_number_too_big_pos_int.json new file mode 100644 index 000000000..338a8c3c0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_too_big_pos_int.json @@ -0,0 +1 @@ +[100000000000000000000] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_number_very_big_negative_int.json b/tests/std/JSONParsingTestSuite/i_number_very_big_negative_int.json new file mode 100755 index 000000000..e2d9738c2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_number_very_big_negative_int.json @@ -0,0 +1 @@ +[-237462374673276894279832749832423479823246327846] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_++.json b/tests/std/JSONParsingTestSuite/n_number_++.json new file mode 100644 index 000000000..bdb62aaf4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_++.json @@ -0,0 +1 @@ +[++1234] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_+1.json b/tests/std/JSONParsingTestSuite/n_number_+1.json new file mode 100755 index 000000000..3cbe58c92 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_+1.json @@ -0,0 +1 @@ +[+1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_+Inf.json b/tests/std/JSONParsingTestSuite/n_number_+Inf.json new file mode 100755 index 000000000..871ae14d5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_+Inf.json @@ -0,0 +1 @@ +[+Inf] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_-01.json b/tests/std/JSONParsingTestSuite/n_number_-01.json new file mode 100755 index 000000000..0df32bac8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_-01.json @@ -0,0 +1 @@ +[-01] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_-1.0..json b/tests/std/JSONParsingTestSuite/n_number_-1.0..json new file mode 100755 index 000000000..7cf55a85a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_-1.0..json @@ -0,0 +1 @@ +[-1.0.] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_-2..json b/tests/std/JSONParsingTestSuite/n_number_-2..json new file mode 100755 index 000000000..9be84365d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_-2..json @@ -0,0 +1 @@ +[-2.] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_-NaN.json b/tests/std/JSONParsingTestSuite/n_number_-NaN.json new file mode 100755 index 000000000..f61615d40 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_-NaN.json @@ -0,0 +1 @@ +[-NaN] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_.-1.json b/tests/std/JSONParsingTestSuite/n_number_.-1.json new file mode 100644 index 000000000..1c9f2dd1b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_.-1.json @@ -0,0 +1 @@ +[.-1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_.2e-3.json b/tests/std/JSONParsingTestSuite/n_number_.2e-3.json new file mode 100755 index 000000000..c6c976f25 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_.2e-3.json @@ -0,0 +1 @@ +[.2e-3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0.1.2.json b/tests/std/JSONParsingTestSuite/n_number_0.1.2.json new file mode 100755 index 000000000..c83a25621 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0.1.2.json @@ -0,0 +1 @@ +[0.1.2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0.3e+.json b/tests/std/JSONParsingTestSuite/n_number_0.3e+.json new file mode 100644 index 000000000..a55a1bfef --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0.3e+.json @@ -0,0 +1 @@ +[0.3e+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0.3e.json b/tests/std/JSONParsingTestSuite/n_number_0.3e.json new file mode 100644 index 000000000..3dd5df4b3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0.3e.json @@ -0,0 +1 @@ +[0.3e] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0.e1.json b/tests/std/JSONParsingTestSuite/n_number_0.e1.json new file mode 100644 index 000000000..c92c71ccb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0.e1.json @@ -0,0 +1 @@ +[0.e1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0_capital_E+.json b/tests/std/JSONParsingTestSuite/n_number_0_capital_E+.json new file mode 100644 index 000000000..3ba2c7d6d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0_capital_E+.json @@ -0,0 +1 @@ +[0E+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0_capital_E.json b/tests/std/JSONParsingTestSuite/n_number_0_capital_E.json new file mode 100755 index 000000000..5301840d1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0_capital_E.json @@ -0,0 +1 @@ +[0E] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0e+.json b/tests/std/JSONParsingTestSuite/n_number_0e+.json new file mode 100644 index 000000000..8ab0bc4b8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0e+.json @@ -0,0 +1 @@ +[0e+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_0e.json b/tests/std/JSONParsingTestSuite/n_number_0e.json new file mode 100644 index 000000000..47ec421bb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_0e.json @@ -0,0 +1 @@ +[0e] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1.0e+.json b/tests/std/JSONParsingTestSuite/n_number_1.0e+.json new file mode 100755 index 000000000..cd84b9f69 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1.0e+.json @@ -0,0 +1 @@ +[1.0e+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1.0e-.json b/tests/std/JSONParsingTestSuite/n_number_1.0e-.json new file mode 100755 index 000000000..4eb7afa0f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1.0e-.json @@ -0,0 +1 @@ +[1.0e-] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1.0e.json b/tests/std/JSONParsingTestSuite/n_number_1.0e.json new file mode 100755 index 000000000..21753f4c7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1.0e.json @@ -0,0 +1 @@ +[1.0e] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1_000.json b/tests/std/JSONParsingTestSuite/n_number_1_000.json new file mode 100755 index 000000000..7b18b66b3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1_000.json @@ -0,0 +1 @@ +[1 000.0] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_1eE2.json b/tests/std/JSONParsingTestSuite/n_number_1eE2.json new file mode 100755 index 000000000..4318a341d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_1eE2.json @@ -0,0 +1 @@ +[1eE2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_2.e+3.json b/tests/std/JSONParsingTestSuite/n_number_2.e+3.json new file mode 100755 index 000000000..4442f394d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_2.e+3.json @@ -0,0 +1 @@ +[2.e+3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_2.e-3.json b/tests/std/JSONParsingTestSuite/n_number_2.e-3.json new file mode 100755 index 000000000..a65060edf --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_2.e-3.json @@ -0,0 +1 @@ +[2.e-3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_2.e3.json b/tests/std/JSONParsingTestSuite/n_number_2.e3.json new file mode 100755 index 000000000..66f7cf701 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_2.e3.json @@ -0,0 +1 @@ +[2.e3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_9.e+.json b/tests/std/JSONParsingTestSuite/n_number_9.e+.json new file mode 100644 index 000000000..732a7b11c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_9.e+.json @@ -0,0 +1 @@ +[9.e+] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_Inf.json b/tests/std/JSONParsingTestSuite/n_number_Inf.json new file mode 100755 index 000000000..c40c734c3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_Inf.json @@ -0,0 +1 @@ +[Inf] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_NaN.json b/tests/std/JSONParsingTestSuite/n_number_NaN.json new file mode 100755 index 000000000..499231790 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_NaN.json @@ -0,0 +1 @@ +[NaN] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_U+FF11_fullwidth_digit_one.json b/tests/std/JSONParsingTestSuite/n_number_U+FF11_fullwidth_digit_one.json new file mode 100644 index 000000000..b14587e5e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_U+FF11_fullwidth_digit_one.json @@ -0,0 +1 @@ +[1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_expression.json b/tests/std/JSONParsingTestSuite/n_number_expression.json new file mode 100644 index 000000000..76fdbc8a4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_expression.json @@ -0,0 +1 @@ +[1+2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_hex_1_digit.json b/tests/std/JSONParsingTestSuite/n_number_hex_1_digit.json new file mode 100644 index 000000000..3b214880c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_hex_1_digit.json @@ -0,0 +1 @@ +[0x1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_hex_2_digits.json b/tests/std/JSONParsingTestSuite/n_number_hex_2_digits.json new file mode 100644 index 000000000..83e516ab0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_hex_2_digits.json @@ -0,0 +1 @@ +[0x42] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_infinity.json b/tests/std/JSONParsingTestSuite/n_number_infinity.json new file mode 100755 index 000000000..8c2baf783 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_infinity.json @@ -0,0 +1 @@ +[Infinity] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid+-.json b/tests/std/JSONParsingTestSuite/n_number_invalid+-.json new file mode 100644 index 000000000..1cce602b5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid+-.json @@ -0,0 +1 @@ +[0e+-1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid-negative-real.json b/tests/std/JSONParsingTestSuite/n_number_invalid-negative-real.json new file mode 100644 index 000000000..5fc3c1efb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid-negative-real.json @@ -0,0 +1 @@ +[-123.123foo] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-bigger-int.json b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-bigger-int.json new file mode 100644 index 000000000..3b97e580e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-bigger-int.json @@ -0,0 +1 @@ +[123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-exponent.json b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-exponent.json new file mode 100644 index 000000000..ea35d723c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-exponent.json @@ -0,0 +1 @@ +[1e1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-int.json b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-int.json new file mode 100644 index 000000000..371226e4c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_invalid-utf-8-in-int.json @@ -0,0 +1 @@ +[0] diff --git a/tests/std/JSONParsingTestSuite/n_number_minus_infinity.json b/tests/std/JSONParsingTestSuite/n_number_minus_infinity.json new file mode 100755 index 000000000..cf4133d22 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_minus_infinity.json @@ -0,0 +1 @@ +[-Infinity] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_minus_sign_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_number_minus_sign_with_trailing_garbage.json new file mode 100644 index 000000000..a6d8e78e7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_minus_sign_with_trailing_garbage.json @@ -0,0 +1 @@ +[-foo] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_minus_space_1.json b/tests/std/JSONParsingTestSuite/n_number_minus_space_1.json new file mode 100644 index 000000000..9a5ebedf6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_minus_space_1.json @@ -0,0 +1 @@ +[- 1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_neg_int_starting_with_zero.json b/tests/std/JSONParsingTestSuite/n_number_neg_int_starting_with_zero.json new file mode 100644 index 000000000..67af0960a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_neg_int_starting_with_zero.json @@ -0,0 +1 @@ +[-012] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_neg_real_without_int_part.json b/tests/std/JSONParsingTestSuite/n_number_neg_real_without_int_part.json new file mode 100755 index 000000000..1f2a43496 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_neg_real_without_int_part.json @@ -0,0 +1 @@ +[-.123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_neg_with_garbage_at_end.json b/tests/std/JSONParsingTestSuite/n_number_neg_with_garbage_at_end.json new file mode 100644 index 000000000..2aa73119f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_neg_with_garbage_at_end.json @@ -0,0 +1 @@ +[-1x] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_real_garbage_after_e.json b/tests/std/JSONParsingTestSuite/n_number_real_garbage_after_e.json new file mode 100644 index 000000000..9213dfca8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_real_garbage_after_e.json @@ -0,0 +1 @@ +[1ea] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_real_with_invalid_utf8_after_e.json b/tests/std/JSONParsingTestSuite/n_number_real_with_invalid_utf8_after_e.json new file mode 100644 index 000000000..1e52ef964 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_real_with_invalid_utf8_after_e.json @@ -0,0 +1 @@ +[1e] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_real_without_fractional_part.json b/tests/std/JSONParsingTestSuite/n_number_real_without_fractional_part.json new file mode 100755 index 000000000..1de287cf8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_real_without_fractional_part.json @@ -0,0 +1 @@ +[1.] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_starting_with_dot.json b/tests/std/JSONParsingTestSuite/n_number_starting_with_dot.json new file mode 100755 index 000000000..f682dbdce --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_starting_with_dot.json @@ -0,0 +1 @@ +[.123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_with_alpha.json b/tests/std/JSONParsingTestSuite/n_number_with_alpha.json new file mode 100644 index 000000000..1e42d8182 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_with_alpha.json @@ -0,0 +1 @@ +[1.2a-3] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_with_alpha_char.json b/tests/std/JSONParsingTestSuite/n_number_with_alpha_char.json new file mode 100644 index 000000000..b79daccb8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_with_alpha_char.json @@ -0,0 +1 @@ +[1.8011670033376514H-308] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_number_with_leading_zero.json b/tests/std/JSONParsingTestSuite/n_number_with_leading_zero.json new file mode 100755 index 000000000..7106da1f3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_number_with_leading_zero.json @@ -0,0 +1 @@ +[012] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number.json b/tests/std/JSONParsingTestSuite/y_number.json new file mode 100644 index 000000000..e5f5cc334 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number.json @@ -0,0 +1 @@ +[123e65] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_0e+1.json b/tests/std/JSONParsingTestSuite/y_number_0e+1.json new file mode 100755 index 000000000..d1d396706 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_0e+1.json @@ -0,0 +1 @@ +[0e+1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_0e1.json b/tests/std/JSONParsingTestSuite/y_number_0e1.json new file mode 100755 index 000000000..3283a7936 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_0e1.json @@ -0,0 +1 @@ +[0e1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_after_space.json b/tests/std/JSONParsingTestSuite/y_number_after_space.json new file mode 100644 index 000000000..623570d96 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_after_space.json @@ -0,0 +1 @@ +[ 4] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_double_close_to_zero.json b/tests/std/JSONParsingTestSuite/y_number_double_close_to_zero.json new file mode 100755 index 000000000..96555ff78 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_double_close_to_zero.json @@ -0,0 +1 @@ +[-0.000000000000000000000000000000000000000000000000000000000000000000000000000001] diff --git a/tests/std/JSONParsingTestSuite/y_number_int_with_exp.json b/tests/std/JSONParsingTestSuite/y_number_int_with_exp.json new file mode 100755 index 000000000..a4ca9e754 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_int_with_exp.json @@ -0,0 +1 @@ +[20e1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_minus_zero.json b/tests/std/JSONParsingTestSuite/y_number_minus_zero.json new file mode 100755 index 000000000..37af1312a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_minus_zero.json @@ -0,0 +1 @@ +[-0] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_negative_int.json b/tests/std/JSONParsingTestSuite/y_number_negative_int.json new file mode 100644 index 000000000..8e30f8bd9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_negative_int.json @@ -0,0 +1 @@ +[-123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_negative_one.json b/tests/std/JSONParsingTestSuite/y_number_negative_one.json new file mode 100644 index 000000000..99d21a2a0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_negative_one.json @@ -0,0 +1 @@ +[-1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_negative_zero.json b/tests/std/JSONParsingTestSuite/y_number_negative_zero.json new file mode 100644 index 000000000..37af1312a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_negative_zero.json @@ -0,0 +1 @@ +[-0] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_capital_e.json b/tests/std/JSONParsingTestSuite/y_number_real_capital_e.json new file mode 100644 index 000000000..6edbdfcb1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_capital_e.json @@ -0,0 +1 @@ +[1E22] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_capital_e_neg_exp.json b/tests/std/JSONParsingTestSuite/y_number_real_capital_e_neg_exp.json new file mode 100644 index 000000000..0a01bd3ef --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_capital_e_neg_exp.json @@ -0,0 +1 @@ +[1E-2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_capital_e_pos_exp.json b/tests/std/JSONParsingTestSuite/y_number_real_capital_e_pos_exp.json new file mode 100644 index 000000000..5a8fc0972 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_capital_e_pos_exp.json @@ -0,0 +1 @@ +[1E+2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_exponent.json b/tests/std/JSONParsingTestSuite/y_number_real_exponent.json new file mode 100644 index 000000000..da2522d61 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_exponent.json @@ -0,0 +1 @@ +[123e45] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_fraction_exponent.json b/tests/std/JSONParsingTestSuite/y_number_real_fraction_exponent.json new file mode 100644 index 000000000..3944a7a45 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_fraction_exponent.json @@ -0,0 +1 @@ +[123.456e78] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_neg_exp.json b/tests/std/JSONParsingTestSuite/y_number_real_neg_exp.json new file mode 100644 index 000000000..ca40d3c25 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_neg_exp.json @@ -0,0 +1 @@ +[1e-2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_real_pos_exponent.json b/tests/std/JSONParsingTestSuite/y_number_real_pos_exponent.json new file mode 100644 index 000000000..343601d51 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_real_pos_exponent.json @@ -0,0 +1 @@ +[1e+2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_simple_int.json b/tests/std/JSONParsingTestSuite/y_number_simple_int.json new file mode 100644 index 000000000..e47f69afc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_simple_int.json @@ -0,0 +1 @@ +[123] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_number_simple_real.json b/tests/std/JSONParsingTestSuite/y_number_simple_real.json new file mode 100644 index 000000000..b02878e5f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_number_simple_real.json @@ -0,0 +1 @@ +[123.456789] \ No newline at end of file diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau new file mode 100644 index 000000000..1aa2916df --- /dev/null +++ b/tests/std/json.test.luau @@ -0,0 +1,51 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local test = require("@std/test") +local json = require("@std/json") +local stringext = require("@std/stringext") + +local suiteDir = "tests/std/JSONParsingTestSuite" + +test.suite("JSONParsingTestSuite", function(suite) + for _, entry in fs.listdirectory(suiteDir) do + if entry.type ~= "file" then + continue + end + + local name = entry.name + + suite:case(name, function(assert) + local p = path.join(suiteDir, name) + local src = fs.readfiletostring(path.format(p)) + + -- attempt to parse .json and print the error if it fails + local ok, parsed = pcall(function() + return json.deserialize(src) + end) + + if stringext.hasprefix(name, "y_") then + -- valid JSON: must parse + assert.eq(ok, true) + + -- serialize the parsed value and ensure it parses again + local _ok2, _ser = pcall(function() + return json.serialize(parsed) + end) + assert.eq(_ok2, true) + + local _ok3 = pcall(function() + json.deserialize(_ser) + end) + assert.eq(_ok3, true) + elseif stringext.hasprefix(name, "n_") then + -- invalid JSON: must fail to parse + assert.eq(ok, false) + else + -- indeterminate (i_*) or other: don't assert pass/fail, but ensure call completed + assert.eq(type(ok), "boolean") + end + end) + end +end) + +test.run() From 2787432037fea85793d5a6cb9231924f9d76a8f1 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:21:36 -0800 Subject: [PATCH 237/642] Fixes JSON array deserialization and adds tests (#671) Added all of the tests for JSON arrays from https://github.com/nst/JSONTestSuite, and fixed all of the bugs related to arrays --- lute/std/libs/json.luau | 71 ++++++++++++------- .../n_array_1_true_without_comma.json | 1 + .../n_array_a_invalid_utf8.json | 1 + .../n_array_colon_instead_of_comma.json | 1 + .../n_array_comma_after_close.json | 1 + .../n_array_comma_and_number.json | 1 + .../n_array_double_comma.json | 1 + .../n_array_double_extra_comma.json | 1 + .../n_array_extra_close.json | 1 + .../n_array_extra_comma.json | 1 + .../n_array_incomplete.json | 1 + .../n_array_incomplete_invalid_value.json | 1 + .../n_array_inner_array_no_comma.json | 1 + .../n_array_invalid_utf8.json | 1 + .../n_array_items_separated_by_semicolon.json | 1 + .../n_array_just_comma.json | 1 + .../n_array_just_minus.json | 1 + .../n_array_missing_value.json | 1 + .../n_array_newlines_unclosed.json | 3 + .../n_array_number_and_comma.json | 1 + .../n_array_number_and_several_commas.json | 1 + .../n_array_spaces_vertical_tab_formfeed.json | 1 + .../n_array_star_inside.json | 1 + .../n_array_unclosed.json | 1 + .../n_array_unclosed_trailing_comma.json | 1 + .../n_array_unclosed_with_new_lines.json | 3 + .../n_array_unclosed_with_object_inside.json | 1 + .../y_array_arraysWithSpaces.json | 1 + .../y_array_empty-string.json | 1 + .../JSONParsingTestSuite/y_array_empty.json | 1 + .../y_array_ending_with_newline.json | 1 + .../JSONParsingTestSuite/y_array_false.json | 1 + .../y_array_heterogeneous.json | 1 + .../JSONParsingTestSuite/y_array_null.json | 1 + .../y_array_with_1_and_newline.json | 2 + .../y_array_with_leading_space.json | 1 + .../y_array_with_several_null.json | 1 + .../y_array_with_trailing_space.json | 1 + 38 files changed, 88 insertions(+), 25 deletions(-) create mode 100644 tests/std/JSONParsingTestSuite/n_array_1_true_without_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_a_invalid_utf8.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_colon_instead_of_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_comma_after_close.json create mode 100755 tests/std/JSONParsingTestSuite/n_array_comma_and_number.json create mode 100755 tests/std/JSONParsingTestSuite/n_array_double_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_double_extra_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_extra_close.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_extra_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_incomplete.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_incomplete_invalid_value.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_inner_array_no_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_invalid_utf8.json create mode 100755 tests/std/JSONParsingTestSuite/n_array_items_separated_by_semicolon.json create mode 100755 tests/std/JSONParsingTestSuite/n_array_just_comma.json create mode 100755 tests/std/JSONParsingTestSuite/n_array_just_minus.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_missing_value.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_newlines_unclosed.json create mode 100755 tests/std/JSONParsingTestSuite/n_array_number_and_comma.json create mode 100755 tests/std/JSONParsingTestSuite/n_array_number_and_several_commas.json create mode 100755 tests/std/JSONParsingTestSuite/n_array_spaces_vertical_tab_formfeed.json create mode 100755 tests/std/JSONParsingTestSuite/n_array_star_inside.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_unclosed.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_unclosed_trailing_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_unclosed_with_new_lines.json create mode 100644 tests/std/JSONParsingTestSuite/n_array_unclosed_with_object_inside.json create mode 100755 tests/std/JSONParsingTestSuite/y_array_arraysWithSpaces.json create mode 100644 tests/std/JSONParsingTestSuite/y_array_empty-string.json create mode 100755 tests/std/JSONParsingTestSuite/y_array_empty.json create mode 100755 tests/std/JSONParsingTestSuite/y_array_ending_with_newline.json create mode 100644 tests/std/JSONParsingTestSuite/y_array_false.json create mode 100755 tests/std/JSONParsingTestSuite/y_array_heterogeneous.json create mode 100644 tests/std/JSONParsingTestSuite/y_array_null.json create mode 100644 tests/std/JSONParsingTestSuite/y_array_with_1_and_newline.json create mode 100755 tests/std/JSONParsingTestSuite/y_array_with_leading_space.json create mode 100755 tests/std/JSONParsingTestSuite/y_array_with_several_null.json create mode 100755 tests/std/JSONParsingTestSuite/y_array_with_trailing_space.json diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index bd211ebcd..5d7c93b2d 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -348,41 +348,62 @@ end local deserialize: (DeserializerState) -> (array | object | boolean | number | string)? local function deserializeArray(state: DeserializerState): array + if currentByte(state) ~= string.byte("[") then + return deserializerError(state, "Expected array opening '['") + end + state.cursor += 1 + skipWhitespace(state) local current: array = {} + local index = 1 - local expectingValue = false - while state.cursor < #state.src do - skipWhitespace(state) + -- empty array + if currentByte(state) == string.byte("]") then + state.cursor += 1 + return current + end - if currentByte(state) == string.byte(",") then - expectingValue = true - state.cursor += 1 - end + local expectingValue = true - skipWhitespace(state) + while state.cursor <= #state.src do + if not expectingValue then + -- Expect a comma or closing bracket + if currentByte(state) == string.byte(",") then + expectingValue = true + state.cursor += 1 + if not skipWhitespace(state) then + return deserializerError(state, "Unterminated array") + end + elseif currentByte(state) == string.byte("]") then + state.cursor += 1 + return current + else + return deserializerError(state, "Expected ',' or ']' after array value") + end + else + -- Expect a value next + if currentByte(state) == string.byte("]") then + if index == 1 then + -- empty array is ok + state.cursor += 1 + return current + else + return deserializerError(state, "Trailing comma in array") + end + end - if currentByte(state) == string.byte("]") then - break + table.insert(current, deserialize(state)) + index += 1 + expectingValue = false end - table.insert(current, deserialize(state)) - - expectingValue = false - end - - if expectingValue then - deserializerError(state, "Trailing comma") - end - - if not skipWhitespace(state) or currentByte(state) ~= string.byte("]") then - deserializerError(state, "Unterminated array") + if not skipWhitespace(state) then + return deserializerError(state, "Unterminated array") + end end - state.cursor += 1 - - return current + return deserializerError(state, "Unterminated array") end local function deserializeObject(state: DeserializerState): object @@ -464,7 +485,7 @@ deserialize = function(state: DeserializerState): value return deserializeNumber(state) elseif string.byte(state.src, state.cursor) == string.byte('"') then return deserializeString(state) - elseif string.byte(state.src, state.cursor) == string.byte("[") then + elseif string.match(state.src, "^%[", state.cursor) then return deserializeArray(state) elseif string.byte(state.src, state.cursor) == string.byte("{") then return deserializeObject(state) diff --git a/tests/std/JSONParsingTestSuite/n_array_1_true_without_comma.json b/tests/std/JSONParsingTestSuite/n_array_1_true_without_comma.json new file mode 100644 index 000000000..c14e3f6b1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_1_true_without_comma.json @@ -0,0 +1 @@ +[1 true] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_a_invalid_utf8.json b/tests/std/JSONParsingTestSuite/n_array_a_invalid_utf8.json new file mode 100644 index 000000000..38a86e2e6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_a_invalid_utf8.json @@ -0,0 +1 @@ +[a] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_colon_instead_of_comma.json b/tests/std/JSONParsingTestSuite/n_array_colon_instead_of_comma.json new file mode 100644 index 000000000..0d02ad448 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_colon_instead_of_comma.json @@ -0,0 +1 @@ +["": 1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_comma_after_close.json b/tests/std/JSONParsingTestSuite/n_array_comma_after_close.json new file mode 100644 index 000000000..2ccba8d95 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_comma_after_close.json @@ -0,0 +1 @@ +[""], \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_comma_and_number.json b/tests/std/JSONParsingTestSuite/n_array_comma_and_number.json new file mode 100755 index 000000000..d2c84e374 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_comma_and_number.json @@ -0,0 +1 @@ +[,1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_double_comma.json b/tests/std/JSONParsingTestSuite/n_array_double_comma.json new file mode 100755 index 000000000..0431712bc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_double_comma.json @@ -0,0 +1 @@ +[1,,2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_double_extra_comma.json b/tests/std/JSONParsingTestSuite/n_array_double_extra_comma.json new file mode 100644 index 000000000..3f01d3129 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_double_extra_comma.json @@ -0,0 +1 @@ +["x",,] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_extra_close.json b/tests/std/JSONParsingTestSuite/n_array_extra_close.json new file mode 100644 index 000000000..c12f9fae1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_extra_close.json @@ -0,0 +1 @@ +["x"]] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_extra_comma.json b/tests/std/JSONParsingTestSuite/n_array_extra_comma.json new file mode 100644 index 000000000..5f8ce18e4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_extra_comma.json @@ -0,0 +1 @@ +["",] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_incomplete.json b/tests/std/JSONParsingTestSuite/n_array_incomplete.json new file mode 100644 index 000000000..cc65b0b51 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_incomplete.json @@ -0,0 +1 @@ +["x" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_incomplete_invalid_value.json b/tests/std/JSONParsingTestSuite/n_array_incomplete_invalid_value.json new file mode 100644 index 000000000..c21a8f6cf --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_incomplete_invalid_value.json @@ -0,0 +1 @@ +[x \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_inner_array_no_comma.json b/tests/std/JSONParsingTestSuite/n_array_inner_array_no_comma.json new file mode 100644 index 000000000..c70b71647 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_inner_array_no_comma.json @@ -0,0 +1 @@ +[3[4]] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_invalid_utf8.json b/tests/std/JSONParsingTestSuite/n_array_invalid_utf8.json new file mode 100644 index 000000000..6099d3441 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_invalid_utf8.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_items_separated_by_semicolon.json b/tests/std/JSONParsingTestSuite/n_array_items_separated_by_semicolon.json new file mode 100755 index 000000000..d4bd7314c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_items_separated_by_semicolon.json @@ -0,0 +1 @@ +[1:2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_just_comma.json b/tests/std/JSONParsingTestSuite/n_array_just_comma.json new file mode 100755 index 000000000..9d7077c68 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_just_comma.json @@ -0,0 +1 @@ +[,] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_just_minus.json b/tests/std/JSONParsingTestSuite/n_array_just_minus.json new file mode 100755 index 000000000..29501c6ca --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_just_minus.json @@ -0,0 +1 @@ +[-] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_missing_value.json b/tests/std/JSONParsingTestSuite/n_array_missing_value.json new file mode 100644 index 000000000..3a6ba86f3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_missing_value.json @@ -0,0 +1 @@ +[ , ""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_newlines_unclosed.json b/tests/std/JSONParsingTestSuite/n_array_newlines_unclosed.json new file mode 100644 index 000000000..646680065 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_newlines_unclosed.json @@ -0,0 +1,3 @@ +["a", +4 +,1, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_number_and_comma.json b/tests/std/JSONParsingTestSuite/n_array_number_and_comma.json new file mode 100755 index 000000000..13f6f1d18 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_number_and_comma.json @@ -0,0 +1 @@ +[1,] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_number_and_several_commas.json b/tests/std/JSONParsingTestSuite/n_array_number_and_several_commas.json new file mode 100755 index 000000000..0ac408cb8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_number_and_several_commas.json @@ -0,0 +1 @@ +[1,,] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_spaces_vertical_tab_formfeed.json b/tests/std/JSONParsingTestSuite/n_array_spaces_vertical_tab_formfeed.json new file mode 100755 index 000000000..6cd7cf585 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_spaces_vertical_tab_formfeed.json @@ -0,0 +1 @@ +[" a"\f] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_star_inside.json b/tests/std/JSONParsingTestSuite/n_array_star_inside.json new file mode 100755 index 000000000..5a5194647 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_star_inside.json @@ -0,0 +1 @@ +[*] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_unclosed.json b/tests/std/JSONParsingTestSuite/n_array_unclosed.json new file mode 100644 index 000000000..060733059 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_unclosed.json @@ -0,0 +1 @@ +["" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_unclosed_trailing_comma.json b/tests/std/JSONParsingTestSuite/n_array_unclosed_trailing_comma.json new file mode 100644 index 000000000..6604698ff --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_unclosed_trailing_comma.json @@ -0,0 +1 @@ +[1, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_unclosed_with_new_lines.json b/tests/std/JSONParsingTestSuite/n_array_unclosed_with_new_lines.json new file mode 100644 index 000000000..4f61de3fb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_unclosed_with_new_lines.json @@ -0,0 +1,3 @@ +[1, +1 +,1 \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_array_unclosed_with_object_inside.json b/tests/std/JSONParsingTestSuite/n_array_unclosed_with_object_inside.json new file mode 100644 index 000000000..043a87e2d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_array_unclosed_with_object_inside.json @@ -0,0 +1 @@ +[{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_arraysWithSpaces.json b/tests/std/JSONParsingTestSuite/y_array_arraysWithSpaces.json new file mode 100755 index 000000000..582290798 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_arraysWithSpaces.json @@ -0,0 +1 @@ +[[] ] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_empty-string.json b/tests/std/JSONParsingTestSuite/y_array_empty-string.json new file mode 100644 index 000000000..93b6be2bc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_empty-string.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_empty.json b/tests/std/JSONParsingTestSuite/y_array_empty.json new file mode 100755 index 000000000..0637a088a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_ending_with_newline.json b/tests/std/JSONParsingTestSuite/y_array_ending_with_newline.json new file mode 100755 index 000000000..eac5f7b46 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_ending_with_newline.json @@ -0,0 +1 @@ +["a"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_false.json b/tests/std/JSONParsingTestSuite/y_array_false.json new file mode 100644 index 000000000..67b2f0760 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_false.json @@ -0,0 +1 @@ +[false] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_heterogeneous.json b/tests/std/JSONParsingTestSuite/y_array_heterogeneous.json new file mode 100755 index 000000000..d3c1e2648 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_heterogeneous.json @@ -0,0 +1 @@ +[null, 1, "1", {}] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_null.json b/tests/std/JSONParsingTestSuite/y_array_null.json new file mode 100644 index 000000000..500db4a86 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_null.json @@ -0,0 +1 @@ +[null] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_with_1_and_newline.json b/tests/std/JSONParsingTestSuite/y_array_with_1_and_newline.json new file mode 100644 index 000000000..994825500 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_with_1_and_newline.json @@ -0,0 +1,2 @@ +[1 +] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_with_leading_space.json b/tests/std/JSONParsingTestSuite/y_array_with_leading_space.json new file mode 100755 index 000000000..18bfe6422 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_with_leading_space.json @@ -0,0 +1 @@ + [1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_with_several_null.json b/tests/std/JSONParsingTestSuite/y_array_with_several_null.json new file mode 100755 index 000000000..99f6c5d1d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_with_several_null.json @@ -0,0 +1 @@ +[1,null,null,null,2] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_array_with_trailing_space.json b/tests/std/JSONParsingTestSuite/y_array_with_trailing_space.json new file mode 100755 index 000000000..de9e7a944 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_array_with_trailing_space.json @@ -0,0 +1 @@ +[2] \ No newline at end of file From 8124bed76673c168efa7ab07e702083024e35768 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:27:17 -0800 Subject: [PATCH 238/642] Add JSON object and structure tests and small fixes (#673) Added all of the remaining non-string tests for JSON from https://github.com/nst/JSONTestSuite, and 3 lines of style-ish changes --- lute/std/libs/json.luau | 7 +++---- .../i_object_key_lone_2nd_surrogate.json | 1 + .../i_structure_500_nested_arrays.json | 1 + .../i_structure_UTF-8_BOM_empty_object.json | 1 + .../JSONParsingTestSuite/n_incomplete_false.json | 1 + .../std/JSONParsingTestSuite/n_incomplete_null.json | 1 + .../std/JSONParsingTestSuite/n_incomplete_true.json | 1 + .../n_multidigit_number_then_00.json | Bin 0 -> 4 bytes .../JSONParsingTestSuite/n_object_bad_value.json | 1 + .../JSONParsingTestSuite/n_object_bracket_key.json | 1 + .../n_object_comma_instead_of_colon.json | 1 + .../JSONParsingTestSuite/n_object_double_colon.json | 1 + tests/std/JSONParsingTestSuite/n_object_emoji.json | 1 + .../n_object_garbage_at_end.json | 1 + .../n_object_key_with_single_quotes.json | 1 + ...continuation_byte_in_key_and_trailing_comma.json | 1 + .../n_object_missing_colon.json | 1 + .../JSONParsingTestSuite/n_object_missing_key.json | 1 + .../n_object_missing_semicolon.json | 1 + .../n_object_missing_value.json | 1 + .../std/JSONParsingTestSuite/n_object_no-colon.json | 1 + .../n_object_non_string_key.json | 1 + ...ject_non_string_key_but_huge_number_instead.json | 1 + .../n_object_repeated_null_null.json | 1 + .../n_object_several_trailing_commas.json | 1 + .../JSONParsingTestSuite/n_object_single_quote.json | 1 + .../n_object_trailing_comma.json | 1 + .../n_object_trailing_comment.json | 1 + .../n_object_trailing_comment_open.json | 1 + .../n_object_trailing_comment_slash_open.json | 1 + ...ject_trailing_comment_slash_open_incomplete.json | 1 + .../n_object_two_commas_in_a_row.json | 1 + .../JSONParsingTestSuite/n_object_unquoted_key.json | 1 + .../n_object_unterminated-value.json | 1 + .../n_object_with_single_string.json | 1 + .../n_object_with_trailing_garbage.json | 1 + tests/std/JSONParsingTestSuite/n_single_space.json | 1 + .../n_structure_100000_opening_arrays.json | 1 + .../n_structure_U+2060_word_joined.json | 1 + .../n_structure_UTF8_BOM_no_data.json | 1 + .../n_structure_angle_bracket_..json | 1 + .../n_structure_angle_bracket_null.json | 1 + .../n_structure_array_trailing_garbage.json | 1 + .../n_structure_array_with_extra_array_close.json | 1 + .../n_structure_array_with_unclosed_string.json | 1 + .../n_structure_ascii-unicode-identifier.json | 1 + .../n_structure_capitalized_True.json | 1 + .../n_structure_close_unopened_array.json | 1 + .../n_structure_comma_instead_of_closing_brace.json | 1 + .../n_structure_double_array.json | 1 + .../JSONParsingTestSuite/n_structure_end_array.json | 1 + .../n_structure_incomplete_UTF8_BOM.json | 1 + .../n_structure_lone-invalid-utf-8.json | 1 + .../n_structure_lone-open-bracket.json | 1 + .../JSONParsingTestSuite/n_structure_no_data.json | 0 .../n_structure_null-byte-outside-string.json | Bin 0 -> 3 bytes .../n_structure_number_with_trailing_garbage.json | 1 + ...structure_object_followed_by_closing_object.json | 1 + .../n_structure_object_unclosed_no_value.json | 1 + .../n_structure_object_with_comment.json | 1 + .../n_structure_object_with_trailing_garbage.json | 1 + .../n_structure_open_array_apostrophe.json | 1 + .../n_structure_open_array_comma.json | 1 + .../n_structure_open_array_object.json | 1 + .../n_structure_open_array_open_object.json | 1 + .../n_structure_open_array_open_string.json | 1 + .../n_structure_open_array_string.json | 1 + .../n_structure_open_object.json | 1 + .../n_structure_open_object_close_array.json | 1 + .../n_structure_open_object_comma.json | 1 + .../n_structure_open_object_open_array.json | 1 + .../n_structure_open_object_open_string.json | 1 + ...ructure_open_object_string_with_apostrophes.json | 1 + .../JSONParsingTestSuite/n_structure_open_open.json | 1 + .../n_structure_single_eacute.json | 1 + .../n_structure_single_star.json | 1 + .../n_structure_trailing_#.json | 1 + .../n_structure_uescaped_LF_before_string.json | 1 + .../n_structure_unclosed_array.json | 1 + .../n_structure_unclosed_array_partial_null.json | 1 + ...n_structure_unclosed_array_unfinished_false.json | 1 + .../n_structure_unclosed_array_unfinished_true.json | 1 + .../n_structure_unclosed_object.json | 1 + .../n_structure_unicode-identifier.json | 1 + .../n_structure_whitespace_U+2060_word_joiner.json | 1 + .../n_structure_whitespace_formfeed.json | 1 + tests/std/JSONParsingTestSuite/y_object.json | 1 + tests/std/JSONParsingTestSuite/y_object_basic.json | 1 + .../y_object_duplicated_key.json | 1 + .../y_object_duplicated_key_and_value.json | 1 + tests/std/JSONParsingTestSuite/y_object_empty.json | 1 + .../JSONParsingTestSuite/y_object_empty_key.json | 1 + .../y_object_escaped_null_in_key.json | 1 + .../y_object_extreme_numbers.json | 1 + .../JSONParsingTestSuite/y_object_long_strings.json | 1 + tests/std/JSONParsingTestSuite/y_object_simple.json | 1 + .../y_object_string_unicode.json | 1 + .../y_object_with_newlines.json | 3 +++ .../y_structure_lonely_false.json | 1 + .../y_structure_lonely_int.json | 1 + .../y_structure_lonely_negative_real.json | 1 + .../y_structure_lonely_null.json | 1 + .../y_structure_lonely_string.json | 1 + .../y_structure_lonely_true.json | 1 + .../y_structure_string_empty.json | 1 + .../y_structure_trailing_newline.json | 1 + .../y_structure_true_in_array.json | 1 + .../y_structure_whitespace_array.json | 1 + 108 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 tests/std/JSONParsingTestSuite/i_object_key_lone_2nd_surrogate.json create mode 100644 tests/std/JSONParsingTestSuite/i_structure_500_nested_arrays.json create mode 100755 tests/std/JSONParsingTestSuite/i_structure_UTF-8_BOM_empty_object.json create mode 100644 tests/std/JSONParsingTestSuite/n_incomplete_false.json create mode 100644 tests/std/JSONParsingTestSuite/n_incomplete_null.json create mode 100644 tests/std/JSONParsingTestSuite/n_incomplete_true.json create mode 100644 tests/std/JSONParsingTestSuite/n_multidigit_number_then_00.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_bad_value.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_bracket_key.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_comma_instead_of_colon.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_double_colon.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_emoji.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_garbage_at_end.json create mode 100755 tests/std/JSONParsingTestSuite/n_object_key_with_single_quotes.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_lone_continuation_byte_in_key_and_trailing_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_missing_colon.json create mode 100755 tests/std/JSONParsingTestSuite/n_object_missing_key.json create mode 100755 tests/std/JSONParsingTestSuite/n_object_missing_semicolon.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_missing_value.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_no-colon.json create mode 100755 tests/std/JSONParsingTestSuite/n_object_non_string_key.json create mode 100755 tests/std/JSONParsingTestSuite/n_object_non_string_key_but_huge_number_instead.json create mode 100755 tests/std/JSONParsingTestSuite/n_object_repeated_null_null.json create mode 100755 tests/std/JSONParsingTestSuite/n_object_several_trailing_commas.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_single_quote.json create mode 100755 tests/std/JSONParsingTestSuite/n_object_trailing_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_trailing_comment.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_trailing_comment_open.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open_incomplete.json create mode 100755 tests/std/JSONParsingTestSuite/n_object_two_commas_in_a_row.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_unquoted_key.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_unterminated-value.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_with_single_string.json create mode 100644 tests/std/JSONParsingTestSuite/n_object_with_trailing_garbage.json create mode 100755 tests/std/JSONParsingTestSuite/n_single_space.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_100000_opening_arrays.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_U+2060_word_joined.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_UTF8_BOM_no_data.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_angle_bracket_..json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_angle_bracket_null.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_array_trailing_garbage.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_array_with_extra_array_close.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_array_with_unclosed_string.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_ascii-unicode-identifier.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_capitalized_True.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_close_unopened_array.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_comma_instead_of_closing_brace.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_double_array.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_end_array.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_incomplete_UTF8_BOM.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_lone-invalid-utf-8.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_lone-open-bracket.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_no_data.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_null-byte-outside-string.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_number_with_trailing_garbage.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_object_followed_by_closing_object.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_object_unclosed_no_value.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_object_with_comment.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_object_with_trailing_garbage.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_array_apostrophe.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_array_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_array_object.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_array_open_object.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_array_open_string.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_array_string.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_object.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_open_object_close_array.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_object_comma.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_object_open_array.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_object_open_string.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_object_string_with_apostrophes.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_open_open.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_single_eacute.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_single_star.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_trailing_#.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_uescaped_LF_before_string.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_unclosed_array.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_unclosed_array_partial_null.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_false.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_true.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_unclosed_object.json create mode 100644 tests/std/JSONParsingTestSuite/n_structure_unicode-identifier.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_whitespace_U+2060_word_joiner.json create mode 100755 tests/std/JSONParsingTestSuite/n_structure_whitespace_formfeed.json create mode 100755 tests/std/JSONParsingTestSuite/y_object.json create mode 100755 tests/std/JSONParsingTestSuite/y_object_basic.json create mode 100755 tests/std/JSONParsingTestSuite/y_object_duplicated_key.json create mode 100755 tests/std/JSONParsingTestSuite/y_object_duplicated_key_and_value.json create mode 100644 tests/std/JSONParsingTestSuite/y_object_empty.json create mode 100755 tests/std/JSONParsingTestSuite/y_object_empty_key.json create mode 100644 tests/std/JSONParsingTestSuite/y_object_escaped_null_in_key.json create mode 100644 tests/std/JSONParsingTestSuite/y_object_extreme_numbers.json create mode 100644 tests/std/JSONParsingTestSuite/y_object_long_strings.json create mode 100644 tests/std/JSONParsingTestSuite/y_object_simple.json create mode 100644 tests/std/JSONParsingTestSuite/y_object_string_unicode.json create mode 100644 tests/std/JSONParsingTestSuite/y_object_with_newlines.json create mode 100644 tests/std/JSONParsingTestSuite/y_structure_lonely_false.json create mode 100755 tests/std/JSONParsingTestSuite/y_structure_lonely_int.json create mode 100755 tests/std/JSONParsingTestSuite/y_structure_lonely_negative_real.json create mode 100644 tests/std/JSONParsingTestSuite/y_structure_lonely_null.json create mode 100755 tests/std/JSONParsingTestSuite/y_structure_lonely_string.json create mode 100755 tests/std/JSONParsingTestSuite/y_structure_lonely_true.json create mode 100644 tests/std/JSONParsingTestSuite/y_structure_string_empty.json create mode 100644 tests/std/JSONParsingTestSuite/y_structure_trailing_newline.json create mode 100644 tests/std/JSONParsingTestSuite/y_structure_true_in_array.json create mode 100644 tests/std/JSONParsingTestSuite/y_structure_whitespace_array.json diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 5d7c93b2d..fa0412e97 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -346,7 +346,6 @@ local function deserializeString(state: DeserializerState): string end local deserialize: (DeserializerState) -> (array | object | boolean | number | string)? - local function deserializeArray(state: DeserializerState): array if currentByte(state) ~= string.byte("[") then return deserializerError(state, "Expected array opening '['") @@ -474,10 +473,10 @@ deserialize = function(state: DeserializerState): value if fourChars == "null" then state.cursor += 4 return json.null - elseif fourChars == "true" then + elseif string.match(fourChars, "^true") then state.cursor += 4 return true - elseif string.sub(state.src, state.cursor, state.cursor + 4) == "false" then + elseif string.match(string.sub(state.src, state.cursor, state.cursor + 4), "^false") then state.cursor += 5 return false elseif string.match(state.src, "^[%-%d]", state.cursor) then @@ -487,7 +486,7 @@ deserialize = function(state: DeserializerState): value return deserializeString(state) elseif string.match(state.src, "^%[", state.cursor) then return deserializeArray(state) - elseif string.byte(state.src, state.cursor) == string.byte("{") then + elseif string.match(state.src, "^{", state.cursor) then return deserializeObject(state) end diff --git a/tests/std/JSONParsingTestSuite/i_object_key_lone_2nd_surrogate.json b/tests/std/JSONParsingTestSuite/i_object_key_lone_2nd_surrogate.json new file mode 100644 index 000000000..5be7ebaf9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_object_key_lone_2nd_surrogate.json @@ -0,0 +1 @@ +{"\uDFAA":0} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_structure_500_nested_arrays.json b/tests/std/JSONParsingTestSuite/i_structure_500_nested_arrays.json new file mode 100644 index 000000000..711840589 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_structure_500_nested_arrays.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_structure_UTF-8_BOM_empty_object.json b/tests/std/JSONParsingTestSuite/i_structure_UTF-8_BOM_empty_object.json new file mode 100755 index 000000000..22fdca1b2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_structure_UTF-8_BOM_empty_object.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_incomplete_false.json b/tests/std/JSONParsingTestSuite/n_incomplete_false.json new file mode 100644 index 000000000..eb18c6a14 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_incomplete_false.json @@ -0,0 +1 @@ +[fals] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_incomplete_null.json b/tests/std/JSONParsingTestSuite/n_incomplete_null.json new file mode 100644 index 000000000..c18ef5385 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_incomplete_null.json @@ -0,0 +1 @@ +[nul] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_incomplete_true.json b/tests/std/JSONParsingTestSuite/n_incomplete_true.json new file mode 100644 index 000000000..f451ac6d2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_incomplete_true.json @@ -0,0 +1 @@ +[tru] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_multidigit_number_then_00.json b/tests/std/JSONParsingTestSuite/n_multidigit_number_then_00.json new file mode 100644 index 0000000000000000000000000000000000000000..c22507b864f7f089c91c1eb85b1b15cc63b943a6 GIT binary patch literal 4 LcmXpsGG+h(0mJ~8 literal 0 HcmV?d00001 diff --git a/tests/std/JSONParsingTestSuite/n_object_bad_value.json b/tests/std/JSONParsingTestSuite/n_object_bad_value.json new file mode 100644 index 000000000..a03a8c03b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_bad_value.json @@ -0,0 +1 @@ +["x", truth] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_bracket_key.json b/tests/std/JSONParsingTestSuite/n_object_bracket_key.json new file mode 100644 index 000000000..cc443b483 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_bracket_key.json @@ -0,0 +1 @@ +{[: "x"} diff --git a/tests/std/JSONParsingTestSuite/n_object_comma_instead_of_colon.json b/tests/std/JSONParsingTestSuite/n_object_comma_instead_of_colon.json new file mode 100644 index 000000000..8d5637708 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_comma_instead_of_colon.json @@ -0,0 +1 @@ +{"x", null} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_double_colon.json b/tests/std/JSONParsingTestSuite/n_object_double_colon.json new file mode 100644 index 000000000..80e8c7b89 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_double_colon.json @@ -0,0 +1 @@ +{"x"::"b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_emoji.json b/tests/std/JSONParsingTestSuite/n_object_emoji.json new file mode 100644 index 000000000..cb4078eaa --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_emoji.json @@ -0,0 +1 @@ +{🇨🇭} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_garbage_at_end.json b/tests/std/JSONParsingTestSuite/n_object_garbage_at_end.json new file mode 100644 index 000000000..80c42cbad --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_garbage_at_end.json @@ -0,0 +1 @@ +{"a":"a" 123} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_key_with_single_quotes.json b/tests/std/JSONParsingTestSuite/n_object_key_with_single_quotes.json new file mode 100755 index 000000000..77c327599 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_key_with_single_quotes.json @@ -0,0 +1 @@ +{key: 'value'} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_lone_continuation_byte_in_key_and_trailing_comma.json b/tests/std/JSONParsingTestSuite/n_object_lone_continuation_byte_in_key_and_trailing_comma.json new file mode 100644 index 000000000..aa2cb637c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_lone_continuation_byte_in_key_and_trailing_comma.json @@ -0,0 +1 @@ +{"":"0",} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_missing_colon.json b/tests/std/JSONParsingTestSuite/n_object_missing_colon.json new file mode 100644 index 000000000..b98eff62d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_missing_colon.json @@ -0,0 +1 @@ +{"a" b} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_missing_key.json b/tests/std/JSONParsingTestSuite/n_object_missing_key.json new file mode 100755 index 000000000..b4fb0f528 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_missing_key.json @@ -0,0 +1 @@ +{:"b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_missing_semicolon.json b/tests/std/JSONParsingTestSuite/n_object_missing_semicolon.json new file mode 100755 index 000000000..e3451384f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_missing_semicolon.json @@ -0,0 +1 @@ +{"a" "b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_missing_value.json b/tests/std/JSONParsingTestSuite/n_object_missing_value.json new file mode 100644 index 000000000..3ef538a60 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_missing_value.json @@ -0,0 +1 @@ +{"a": \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_no-colon.json b/tests/std/JSONParsingTestSuite/n_object_no-colon.json new file mode 100644 index 000000000..f3797b357 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_no-colon.json @@ -0,0 +1 @@ +{"a" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_non_string_key.json b/tests/std/JSONParsingTestSuite/n_object_non_string_key.json new file mode 100755 index 000000000..b9945b34b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_non_string_key.json @@ -0,0 +1 @@ +{1:1} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_non_string_key_but_huge_number_instead.json b/tests/std/JSONParsingTestSuite/n_object_non_string_key_but_huge_number_instead.json new file mode 100755 index 000000000..b37fa86c0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_non_string_key_but_huge_number_instead.json @@ -0,0 +1 @@ +{9999E9999:1} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_repeated_null_null.json b/tests/std/JSONParsingTestSuite/n_object_repeated_null_null.json new file mode 100755 index 000000000..f7d2959d0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_repeated_null_null.json @@ -0,0 +1 @@ +{null:null,null:null} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_several_trailing_commas.json b/tests/std/JSONParsingTestSuite/n_object_several_trailing_commas.json new file mode 100755 index 000000000..3c9afe8dc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_several_trailing_commas.json @@ -0,0 +1 @@ +{"id":0,,,,,} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_single_quote.json b/tests/std/JSONParsingTestSuite/n_object_single_quote.json new file mode 100644 index 000000000..e5cdf976a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_single_quote.json @@ -0,0 +1 @@ +{'a':0} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comma.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comma.json new file mode 100755 index 000000000..a4b025094 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comma.json @@ -0,0 +1 @@ +{"id":0,} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comment.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comment.json new file mode 100644 index 000000000..a372c6553 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comment.json @@ -0,0 +1 @@ +{"a":"b"}/**/ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comment_open.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_open.json new file mode 100644 index 000000000..d557f41ca --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_open.json @@ -0,0 +1 @@ +{"a":"b"}/**// \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open.json new file mode 100644 index 000000000..e335136c0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open.json @@ -0,0 +1 @@ +{"a":"b"}// \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open_incomplete.json b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open_incomplete.json new file mode 100644 index 000000000..d892e49f1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_trailing_comment_slash_open_incomplete.json @@ -0,0 +1 @@ +{"a":"b"}/ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_two_commas_in_a_row.json b/tests/std/JSONParsingTestSuite/n_object_two_commas_in_a_row.json new file mode 100755 index 000000000..7c639ae64 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_two_commas_in_a_row.json @@ -0,0 +1 @@ +{"a":"b",,"c":"d"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_unquoted_key.json b/tests/std/JSONParsingTestSuite/n_object_unquoted_key.json new file mode 100644 index 000000000..8ba137293 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_unquoted_key.json @@ -0,0 +1 @@ +{a: "b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_unterminated-value.json b/tests/std/JSONParsingTestSuite/n_object_unterminated-value.json new file mode 100644 index 000000000..7fe699a6a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_unterminated-value.json @@ -0,0 +1 @@ +{"a":"a \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_with_single_string.json b/tests/std/JSONParsingTestSuite/n_object_with_single_string.json new file mode 100644 index 000000000..d63f7fbb7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_with_single_string.json @@ -0,0 +1 @@ +{ "foo" : "bar", "a" } \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_object_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_object_with_trailing_garbage.json new file mode 100644 index 000000000..787c8f0a8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_object_with_trailing_garbage.json @@ -0,0 +1 @@ +{"a":"b"}# \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_single_space.json b/tests/std/JSONParsingTestSuite/n_single_space.json new file mode 100755 index 000000000..0519ecba6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_single_space.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_100000_opening_arrays.json b/tests/std/JSONParsingTestSuite/n_structure_100000_opening_arrays.json new file mode 100644 index 000000000..a4823eecc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_100000_opening_arrays.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_U+2060_word_joined.json b/tests/std/JSONParsingTestSuite/n_structure_U+2060_word_joined.json new file mode 100644 index 000000000..81156a699 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_U+2060_word_joined.json @@ -0,0 +1 @@ +[⁠] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_UTF8_BOM_no_data.json b/tests/std/JSONParsingTestSuite/n_structure_UTF8_BOM_no_data.json new file mode 100755 index 000000000..5f282702b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_UTF8_BOM_no_data.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_..json b/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_..json new file mode 100755 index 000000000..a56fef0b0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_..json @@ -0,0 +1 @@ +<.> \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_null.json b/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_null.json new file mode 100755 index 000000000..617f26254 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_null.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_array_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_structure_array_trailing_garbage.json new file mode 100644 index 000000000..5a745e6f3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_array_trailing_garbage.json @@ -0,0 +1 @@ +[1]x \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_array_with_extra_array_close.json b/tests/std/JSONParsingTestSuite/n_structure_array_with_extra_array_close.json new file mode 100755 index 000000000..6cfb1398d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_array_with_extra_array_close.json @@ -0,0 +1 @@ +[1]] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_array_with_unclosed_string.json b/tests/std/JSONParsingTestSuite/n_structure_array_with_unclosed_string.json new file mode 100755 index 000000000..ba6b1788b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_array_with_unclosed_string.json @@ -0,0 +1 @@ +["asd] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_ascii-unicode-identifier.json b/tests/std/JSONParsingTestSuite/n_structure_ascii-unicode-identifier.json new file mode 100644 index 000000000..ef2ab62fe --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_ascii-unicode-identifier.json @@ -0,0 +1 @@ +aå \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_capitalized_True.json b/tests/std/JSONParsingTestSuite/n_structure_capitalized_True.json new file mode 100755 index 000000000..7cd88469a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_capitalized_True.json @@ -0,0 +1 @@ +[True] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_close_unopened_array.json b/tests/std/JSONParsingTestSuite/n_structure_close_unopened_array.json new file mode 100755 index 000000000..d2af0c646 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_close_unopened_array.json @@ -0,0 +1 @@ +1] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_comma_instead_of_closing_brace.json b/tests/std/JSONParsingTestSuite/n_structure_comma_instead_of_closing_brace.json new file mode 100644 index 000000000..ac61b8200 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_comma_instead_of_closing_brace.json @@ -0,0 +1 @@ +{"x": true, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_double_array.json b/tests/std/JSONParsingTestSuite/n_structure_double_array.json new file mode 100755 index 000000000..058d1626e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_double_array.json @@ -0,0 +1 @@ +[][] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_end_array.json b/tests/std/JSONParsingTestSuite/n_structure_end_array.json new file mode 100644 index 000000000..54caf60b1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_end_array.json @@ -0,0 +1 @@ +] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_incomplete_UTF8_BOM.json b/tests/std/JSONParsingTestSuite/n_structure_incomplete_UTF8_BOM.json new file mode 100755 index 000000000..bfcdd514f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_incomplete_UTF8_BOM.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_lone-invalid-utf-8.json b/tests/std/JSONParsingTestSuite/n_structure_lone-invalid-utf-8.json new file mode 100644 index 000000000..8b1296cad --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_lone-invalid-utf-8.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_lone-open-bracket.json b/tests/std/JSONParsingTestSuite/n_structure_lone-open-bracket.json new file mode 100644 index 000000000..8e2f0bef1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_lone-open-bracket.json @@ -0,0 +1 @@ +[ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_no_data.json b/tests/std/JSONParsingTestSuite/n_structure_no_data.json new file mode 100644 index 000000000..e69de29bb diff --git a/tests/std/JSONParsingTestSuite/n_structure_null-byte-outside-string.json b/tests/std/JSONParsingTestSuite/n_structure_null-byte-outside-string.json new file mode 100644 index 0000000000000000000000000000000000000000..326db14422a756e9bd39221edf37b844cb8471c7 GIT binary patch literal 3 Kcma!Mhy?%vaR9jh literal 0 HcmV?d00001 diff --git a/tests/std/JSONParsingTestSuite/n_structure_number_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_structure_number_with_trailing_garbage.json new file mode 100644 index 000000000..0746539d2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_number_with_trailing_garbage.json @@ -0,0 +1 @@ +2@ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_object_followed_by_closing_object.json b/tests/std/JSONParsingTestSuite/n_structure_object_followed_by_closing_object.json new file mode 100644 index 000000000..aa9ebaec5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_object_followed_by_closing_object.json @@ -0,0 +1 @@ +{}} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_object_unclosed_no_value.json b/tests/std/JSONParsingTestSuite/n_structure_object_unclosed_no_value.json new file mode 100644 index 000000000..17d045147 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_object_unclosed_no_value.json @@ -0,0 +1 @@ +{"": \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_object_with_comment.json b/tests/std/JSONParsingTestSuite/n_structure_object_with_comment.json new file mode 100644 index 000000000..ed1b569b7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_object_with_comment.json @@ -0,0 +1 @@ +{"a":/*comment*/"b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_object_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_structure_object_with_trailing_garbage.json new file mode 100644 index 000000000..9ca2336d7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_object_with_trailing_garbage.json @@ -0,0 +1 @@ +{"a": true} "x" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_apostrophe.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_apostrophe.json new file mode 100644 index 000000000..8bebe3af0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_apostrophe.json @@ -0,0 +1 @@ +[' \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_comma.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_comma.json new file mode 100644 index 000000000..6295fdc36 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_comma.json @@ -0,0 +1 @@ +[, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_object.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_object.json new file mode 100644 index 000000000..e870445b2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_object.json @@ -0,0 +1 @@ +[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"":[{"": diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_open_object.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_open_object.json new file mode 100644 index 000000000..7a63c8c57 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_open_object.json @@ -0,0 +1 @@ +[{ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_open_string.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_open_string.json new file mode 100644 index 000000000..9822a6baf --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_open_string.json @@ -0,0 +1 @@ +["a \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_array_string.json b/tests/std/JSONParsingTestSuite/n_structure_open_array_string.json new file mode 100644 index 000000000..42a619362 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_array_string.json @@ -0,0 +1 @@ +["a" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object.json b/tests/std/JSONParsingTestSuite/n_structure_open_object.json new file mode 100644 index 000000000..81750b96f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object.json @@ -0,0 +1 @@ +{ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_close_array.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_close_array.json new file mode 100755 index 000000000..eebc700a1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_close_array.json @@ -0,0 +1 @@ +{] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_comma.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_comma.json new file mode 100644 index 000000000..47bc9106f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_comma.json @@ -0,0 +1 @@ +{, \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_open_array.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_open_array.json new file mode 100644 index 000000000..381ede5de --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_open_array.json @@ -0,0 +1 @@ +{[ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_open_string.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_open_string.json new file mode 100644 index 000000000..328c30cd7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_open_string.json @@ -0,0 +1 @@ +{"a \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_object_string_with_apostrophes.json b/tests/std/JSONParsingTestSuite/n_structure_open_object_string_with_apostrophes.json new file mode 100644 index 000000000..9dba17090 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_object_string_with_apostrophes.json @@ -0,0 +1 @@ +{'a' \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_open_open.json b/tests/std/JSONParsingTestSuite/n_structure_open_open.json new file mode 100644 index 000000000..841fd5f86 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_open_open.json @@ -0,0 +1 @@ +["\{["\{["\{["\{ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_single_eacute.json b/tests/std/JSONParsingTestSuite/n_structure_single_eacute.json new file mode 100644 index 000000000..92a39f398 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_single_eacute.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_single_star.json b/tests/std/JSONParsingTestSuite/n_structure_single_star.json new file mode 100755 index 000000000..f59ec20aa --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_single_star.json @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_trailing_#.json b/tests/std/JSONParsingTestSuite/n_structure_trailing_#.json new file mode 100644 index 000000000..898611087 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_trailing_#.json @@ -0,0 +1 @@ +{"a":"b"}#{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_uescaped_LF_before_string.json b/tests/std/JSONParsingTestSuite/n_structure_uescaped_LF_before_string.json new file mode 100755 index 000000000..df2f0f242 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_uescaped_LF_before_string.json @@ -0,0 +1 @@ +[\u000A""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_array.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array.json new file mode 100755 index 000000000..11209515c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array.json @@ -0,0 +1 @@ +[1 \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_partial_null.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_partial_null.json new file mode 100644 index 000000000..0d591762c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_partial_null.json @@ -0,0 +1 @@ +[ false, nul \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_false.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_false.json new file mode 100644 index 000000000..a2ff8504a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_false.json @@ -0,0 +1 @@ +[ true, fals \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_true.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_true.json new file mode 100644 index 000000000..3149e8f5a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_array_unfinished_true.json @@ -0,0 +1 @@ +[ false, tru \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unclosed_object.json b/tests/std/JSONParsingTestSuite/n_structure_unclosed_object.json new file mode 100755 index 000000000..694d69dbd --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unclosed_object.json @@ -0,0 +1 @@ +{"asd":"asd" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_unicode-identifier.json b/tests/std/JSONParsingTestSuite/n_structure_unicode-identifier.json new file mode 100644 index 000000000..7284aea33 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_unicode-identifier.json @@ -0,0 +1 @@ +å \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_whitespace_U+2060_word_joiner.json b/tests/std/JSONParsingTestSuite/n_structure_whitespace_U+2060_word_joiner.json new file mode 100755 index 000000000..81156a699 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_whitespace_U+2060_word_joiner.json @@ -0,0 +1 @@ +[⁠] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_structure_whitespace_formfeed.json b/tests/std/JSONParsingTestSuite/n_structure_whitespace_formfeed.json new file mode 100755 index 000000000..a9ea535d1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_structure_whitespace_formfeed.json @@ -0,0 +1 @@ +[ ] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object.json b/tests/std/JSONParsingTestSuite/y_object.json new file mode 100755 index 000000000..78262eda3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object.json @@ -0,0 +1 @@ +{"asd":"sdf", "dfg":"fgh"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_basic.json b/tests/std/JSONParsingTestSuite/y_object_basic.json new file mode 100755 index 000000000..646bbe7bb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_basic.json @@ -0,0 +1 @@ +{"asd":"sdf"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_duplicated_key.json b/tests/std/JSONParsingTestSuite/y_object_duplicated_key.json new file mode 100755 index 000000000..bbc2e1ce4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_duplicated_key.json @@ -0,0 +1 @@ +{"a":"b","a":"c"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_duplicated_key_and_value.json b/tests/std/JSONParsingTestSuite/y_object_duplicated_key_and_value.json new file mode 100755 index 000000000..211581c20 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_duplicated_key_and_value.json @@ -0,0 +1 @@ +{"a":"b","a":"b"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_empty.json b/tests/std/JSONParsingTestSuite/y_object_empty.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_empty.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_empty_key.json b/tests/std/JSONParsingTestSuite/y_object_empty_key.json new file mode 100755 index 000000000..c0013d3b8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_empty_key.json @@ -0,0 +1 @@ +{"":0} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_escaped_null_in_key.json b/tests/std/JSONParsingTestSuite/y_object_escaped_null_in_key.json new file mode 100644 index 000000000..593f0f67f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_escaped_null_in_key.json @@ -0,0 +1 @@ +{"foo\u0000bar": 42} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_extreme_numbers.json b/tests/std/JSONParsingTestSuite/y_object_extreme_numbers.json new file mode 100644 index 000000000..a0d3531c3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_extreme_numbers.json @@ -0,0 +1 @@ +{ "min": -1.0e+28, "max": 1.0e+28 } \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_long_strings.json b/tests/std/JSONParsingTestSuite/y_object_long_strings.json new file mode 100644 index 000000000..bdc4a0871 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_long_strings.json @@ -0,0 +1 @@ +{"x":[{"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}], "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_simple.json b/tests/std/JSONParsingTestSuite/y_object_simple.json new file mode 100644 index 000000000..dacac917f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_simple.json @@ -0,0 +1 @@ +{"a":[]} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_string_unicode.json b/tests/std/JSONParsingTestSuite/y_object_string_unicode.json new file mode 100644 index 000000000..8effdb297 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_string_unicode.json @@ -0,0 +1 @@ +{"title":"\u041f\u043e\u043b\u0442\u043e\u0440\u0430 \u0417\u0435\u043c\u043b\u0435\u043a\u043e\u043f\u0430" } \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_object_with_newlines.json b/tests/std/JSONParsingTestSuite/y_object_with_newlines.json new file mode 100644 index 000000000..246ec6b34 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_object_with_newlines.json @@ -0,0 +1,3 @@ +{ +"a": "b" +} \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_false.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_false.json new file mode 100644 index 000000000..02e4a84d6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_false.json @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_int.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_int.json new file mode 100755 index 000000000..f70d7bba4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_int.json @@ -0,0 +1 @@ +42 \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_negative_real.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_negative_real.json new file mode 100755 index 000000000..b5135a207 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_negative_real.json @@ -0,0 +1 @@ +-0.1 \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_null.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_null.json new file mode 100644 index 000000000..ec747fa47 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_null.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_string.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_string.json new file mode 100755 index 000000000..b6e982ca9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_string.json @@ -0,0 +1 @@ +"asd" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_lonely_true.json b/tests/std/JSONParsingTestSuite/y_structure_lonely_true.json new file mode 100755 index 000000000..f32a5804e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_lonely_true.json @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_string_empty.json b/tests/std/JSONParsingTestSuite/y_structure_string_empty.json new file mode 100644 index 000000000..3cc762b55 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_string_empty.json @@ -0,0 +1 @@ +"" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_trailing_newline.json b/tests/std/JSONParsingTestSuite/y_structure_trailing_newline.json new file mode 100644 index 000000000..0c3426d4c --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_trailing_newline.json @@ -0,0 +1 @@ +["a"] diff --git a/tests/std/JSONParsingTestSuite/y_structure_true_in_array.json b/tests/std/JSONParsingTestSuite/y_structure_true_in_array.json new file mode 100644 index 000000000..de601e305 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_true_in_array.json @@ -0,0 +1 @@ +[true] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_structure_whitespace_array.json b/tests/std/JSONParsingTestSuite/y_structure_whitespace_array.json new file mode 100644 index 000000000..2bedf7f2d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_structure_whitespace_array.json @@ -0,0 +1 @@ + [] \ No newline at end of file From 180d2b41d70f964f6534cce62d5ea986cfbf4c17 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:35:33 -0800 Subject: [PATCH 239/642] Implements `lute pkgrun `: a subcommand that runs Lute with package awareness (#657) Implement a simple layer that bridges the gap between Loom's generated `loom.lock` file and Lute's `Package::RequireVfs` (introduced in #339). Running a file with `lute pkgrun ` searches for a lockfile called `loom.lock` before executing ``. We use regex matching to parse out the set of dependencies from the lockfile, which are expected to be installed in a `Packages` folder in the same directory as `loom.lock`. Using this, we construct a `Package::RequireVfs`, which automatically injects aliases to dependencies when executing user code. Because of limitations with the current `loom.lock` format, there are still several improvements we need to make. I've left a few TODO comments related to this. Long term, regex matching is also not a great way to parse `loom.lock`, but I didn't want to embed a C++ TOML parser or deal with the complexities of embedding `@batteries/toml` and calling into it from C++. Instead, we can probably use Luau syntax for `loom.lock` since Lute can easily parse Luau tables. We could potentially base the format on [`.config.luau` files](https://rfcs.luau.org/config-luauconfig.html). --- lute/cli/CMakeLists.txt | 2 + lute/cli/include/lute/packagerun.h | 14 +++ lute/cli/include/lute/requiresetup.h | 9 ++ lute/cli/src/climain.cpp | 63 +++++++++-- lute/cli/src/packagerun.cpp | 107 ++++++++++++++++++ lute/cli/src/requiresetup.cpp | 52 +++++++++ lute/require/include/lute/userlandvfs.h | 4 + tests/src/packagerequire.test.cpp | 20 +++- .../dep/module.luau | 0 .../internaldep/init.luau | 0 .../packageentry/entry.luau | 0 .../Packages/dep/src/init.luau | 3 + .../Packages/internaldep/init.luau | 1 + .../packages/pkgrun_with_lockfile/loom.lock | 9 ++ .../packageentry/entry.luau | 7 ++ 15 files changed, 276 insertions(+), 15 deletions(-) create mode 100644 lute/cli/include/lute/packagerun.h create mode 100644 lute/cli/src/packagerun.cpp rename tests/src/packages/{ => package_aware_require}/dep/module.luau (100%) rename tests/src/packages/{ => package_aware_require}/internaldep/init.luau (100%) rename tests/src/packages/{ => package_aware_require}/packageentry/entry.luau (100%) create mode 100644 tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau create mode 100644 tests/src/packages/pkgrun_with_lockfile/Packages/internaldep/init.luau create mode 100644 tests/src/packages/pkgrun_with_lockfile/loom.lock create mode 100644 tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 7c04711c2..c9d870522 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -27,6 +27,7 @@ target_sources(Lute.CLI.lib PRIVATE include/lute/compile.h include/lute/fileutils.h include/lute/luauflags.h + include/lute/packagerun.h include/lute/reporter.h include/lute/requiresetup.h include/lute/staticrequires.h @@ -37,6 +38,7 @@ target_sources(Lute.CLI.lib PRIVATE src/compile.cpp src/fileutils.cpp src/luauflags.cpp + src/packagerun.cpp src/requiresetup.cpp src/staticrequires.cpp src/tc.cpp diff --git a/lute/cli/include/lute/packagerun.h b/lute/cli/include/lute/packagerun.h new file mode 100644 index 000000000..ee6052b84 --- /dev/null +++ b/lute/cli/include/lute/packagerun.h @@ -0,0 +1,14 @@ +#pragma once + +#include "lute/userlandvfs.h" + +#include +#include +#include +#include + +std::optional getAbsolutePathToNearestLockfile(std::string entryFile); + +std::pair, std::vector>> getDependenciesFromLockfile( + const std::string& lockfilePath +); diff --git a/lute/cli/include/lute/requiresetup.h b/lute/cli/include/lute/requiresetup.h index fcea867fd..735398515 100644 --- a/lute/cli/include/lute/requiresetup.h +++ b/lute/cli/include/lute/requiresetup.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/userlandvfs.h" + #include "Luau/DenseHash.h" #include @@ -9,6 +11,13 @@ struct lua_State; struct Runtime; lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit = nullptr); + +lua_State* setupPkgCliState( + Runtime& runtime, + std::vector directDependencies, + std::vector> allDependencies +); + lua_State* setupBundleState( Runtime& runtime, Luau::DenseHashMap luaurcFiles, diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 1e208238c..b8020fd17 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -5,6 +5,7 @@ #include "lute/luauflags.h" #include "lute/modulepath.h" #include "lute/options.h" +#include "lute/packagerun.h" #include "lute/process.h" #include "lute/ref.h" #include "lute/reporter.h" @@ -84,6 +85,12 @@ Run Options: -h, --help Display this usage message. )"; +static const char* PKGRUN_HELP_STRING = R"(Usage: lute pkgrun [args...] + +Run Options: + -h, --help Display this usage message. +)"; + static const char* CHECK_HELP_STRING = R"(Usage: lute check [file2.luau...] Check Options: @@ -253,8 +260,9 @@ static std::pair getValidPath(std::string filePath) return {false, res}; } -int handleRunCommand(int argc, char** argv, int argOffset, LuteReporter& reporter) +int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness, LuteReporter& reporter) { + std::string command = packageAwareness ? "pkgrun" : "run"; std::string filePath; int program_argc = 0; char** program_argv = nullptr; @@ -265,13 +273,13 @@ int handleRunCommand(int argc, char** argv, int argOffset, LuteReporter& reporte if (strcmp(currentArg, "-h") == 0 || strcmp(currentArg, "--help") == 0) { - reporter.reportOutput(RUN_HELP_STRING); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); return 0; } else if (currentArg[0] == '-') { - reporter.formatError("Error: Unrecognized option '%s' for 'run' command.", currentArg); - reporter.reportOutput(RUN_HELP_STRING); + reporter.formatError("Error: Unrecognized option '%s' for '%s' command.", currentArg, command.c_str()); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); return 1; } else @@ -285,22 +293,49 @@ int handleRunCommand(int argc, char** argv, int argOffset, LuteReporter& reporte if (filePath.empty()) { - reporter.reportError("Error: No file specified for 'run' command."); - reporter.reportOutput(RUN_HELP_STRING); + reporter.formatError("Error: No file specified for '%s' command.", command.c_str()); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); return 1; } - Runtime runtime; - lua_State* L = setupCliState(runtime); - auto [ok, validPath] = getValidPath(filePath); if (!ok) { reporter.formatError("Error while resolving filepath '%s': %s", filePath.c_str(), validPath.c_str()); - return 1; } + Runtime runtime; + lua_State* L; + + if (packageAwareness) + { + if (!isAbsolutePath(validPath)) + { + std::optional cwd = getCurrentWorkingDirectory(); + if (!cwd) + { + reporter.reportError("Error: Failed to get current working directory.\n"); + return 1; + } + validPath = normalizePath(joinPaths(*cwd, filePath)); + } + + std::optional lockfile = getAbsolutePathToNearestLockfile(validPath); + if (!lockfile) + { + reporter.formatError("Error: No loom.lock file found for '%s'", validPath.c_str()); + return 1; + } + + auto [directDependencies, allDependencies] = getDependenciesFromLockfile(*lockfile); + L = setupPkgCliState(runtime, std::move(directDependencies), std::move(allDependencies)); + } + else + { + L = setupCliState(runtime); + } + bool success = runFile(runtime, validPath.c_str(), L, program_argc, program_argv, reporter); return success ? 0 : 1; } @@ -551,7 +586,11 @@ int cliMain(int argc, char** argv, LuteReporter& reporter) if (strcmp(command, "run") == 0) { - return handleRunCommand(argc, argv, argOffset, reporter); + return handleRunCommand(argc, argv, argOffset, /* packageAwareness = */ false, reporter); + } + else if (strcmp(command, "pkgrun") == 0) + { + return handleRunCommand(argc, argv, argOffset, /* packageAwareness = */ true, reporter); } else if (strcmp(command, "check") == 0) { @@ -579,6 +618,6 @@ int cliMain(int argc, char** argv, LuteReporter& reporter) { // Default to 'run' command argOffset = 1; - return handleRunCommand(argc, argv, argOffset, reporter); + return handleRunCommand(argc, argv, argOffset, /* packageAwareness = */ false, reporter); } } diff --git a/lute/cli/src/packagerun.cpp b/lute/cli/src/packagerun.cpp new file mode 100644 index 000000000..1400d970d --- /dev/null +++ b/lute/cli/src/packagerun.cpp @@ -0,0 +1,107 @@ +#include "lute/packagerun.h" + +#include "Luau/FileUtils.h" + +#include +#include +#include +#include + +std::optional getAbsolutePathToNearestLockfile(std::string entryFile) +{ + if (!isAbsolutePath(entryFile)) + { + std::optional cwd = getCurrentWorkingDirectory(); + if (!cwd) + return std::nullopt; + + entryFile = joinPaths(*cwd, entryFile); + } + entryFile = normalizePath(entryFile); + + std::optional currentPath = getParentPath(entryFile); + while (currentPath) + { + std::string lockfilePath = joinPaths(*currentPath, "loom.lock"); + if (isFile(lockfilePath)) + return lockfilePath; + + currentPath = getParentPath(*currentPath); + } + + return std::nullopt; +} + +static std::vector extractIdentifiers(std::string lockfileContents) +{ + std::vector packages; + + // TODO: regex matching is a temporary solution; the lockfile format is + // likely to change, and we will use a stable parsing solution then. + auto it = lockfileContents.cbegin(); + std::smatch match; + std::regex re{R"LUTE(name = "([^"]+)"[\r\n]+version = "?([^"\r\n]+)"?)LUTE"}; + while (std::regex_search(it, lockfileContents.cend(), match, re)) + { + packages.push_back(Package::Identifier{match[1].str(), match[2].str()}); + it = match.suffix().first; + } + + return packages; +} + +// TODO: lockfile must specify entry file location; for now, we try out a few +// likely candidates. +static std::string getEntryPoint(const std::string& packageRoot) +{ + const std::vector candidateEntryPoints = { + "src/init.luau", + "src/init.lua", + "init.luau", + "init.lua", + }; + + for (const std::string& candidate : candidateEntryPoints) + { + std::string candidatePath = joinPaths(packageRoot, candidate); + if (isFile(candidatePath)) + return candidatePath; + } + + // Fallback to a default even if it doesn't exist + return joinPaths(packageRoot, "src/init.luau"); +} + +std::pair, std::vector>> getDependenciesFromLockfile( + const std::string& lockfilePath +) +{ + LUAU_ASSERT(isFile(lockfilePath)); + + std::optional contents = readFile(lockfilePath); + if (!contents) + return {}; + + std::optional lockfileParentDir = getParentPath(lockfilePath); + if (!lockfileParentDir) + return {}; + + std::string packagesPath = joinPaths(std::move(*lockfileParentDir), "Packages"); + + std::vector directDependencies = extractIdentifiers(*contents); + std::vector> allDependencies; + for (const Package::Identifier& identifier : directDependencies) + { + Package::Info info; + info.rootDirectory = joinPaths(packagesPath, identifier.name); + info.entryFile = getEntryPoint(info.rootDirectory); + + // TODO: lockfile must specify transitive dependency structure; we + // currently make all dependencies available to each other. + info.dependencies = directDependencies; + + allDependencies.emplace_back(identifier, std::move(info)); + } + + return {std::move(directDependencies), std::move(allDependencies)}; +} diff --git a/lute/cli/src/requiresetup.cpp b/lute/cli/src/requiresetup.cpp index 1f562dbcd..3440f13b9 100644 --- a/lute/cli/src/requiresetup.cpp +++ b/lute/cli/src/requiresetup.cpp @@ -7,6 +7,7 @@ #include "lute/io.h" #include "lute/luau.h" #include "lute/net.h" +#include "lute/packagerequirevfs.h" #include "lute/process.h" #include "lute/require.h" #include "lute/requirevfs.h" @@ -14,6 +15,7 @@ #include "lute/system.h" #include "lute/task.h" #include "lute/time.h" +#include "lute/userlandvfs.h" #include "lute/vm.h" #include "Luau/CodeGen.h" @@ -72,6 +74,36 @@ static void* createCliRequireContext(lua_State* L) return ctx; } +static void* createPkgRequireContext( + lua_State* L, + std::vector directDependencies, + std::vector> allDependencies +) +{ + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + std::destroy_at(static_cast(ptr)); + } + ); + + if (!ctx) + luaL_error(L, "unable to allocate RequireCtx"); + + Package::UserlandVfs userlandVfs = Package::UserlandVfs::create(std::move(directDependencies), std::move(allDependencies)); + ctx = new (ctx) RequireCtx{std::make_unique(std::move(userlandVfs))}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + return ctx; +} + static void* createBundleRequireContext( lua_State* L, Luau::DenseHashMap luaurcFiles, @@ -118,6 +150,26 @@ lua_State* setupCliState(Runtime& runtime, std::function preSa ); } +lua_State* setupPkgCliState( + Runtime& runtime, + std::vector directDependencies, + std::vector> allDependencies +) +{ + return setupState( + runtime, + [directDependencies = std::move(directDependencies), allDependencies = std::move(allDependencies)](lua_State* L) + { + luteopen_libs(L); + + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createPkgRequireContext(L, std::move(directDependencies), std::move(allDependencies))); + } + ); +} + lua_State* setupBundleState( Runtime& runtime, Luau::DenseHashMap luaurcFiles, diff --git a/lute/require/include/lute/userlandvfs.h b/lute/require/include/lute/userlandvfs.h index 264ddbe1a..71967948c 100644 --- a/lute/require/include/lute/userlandvfs.h +++ b/lute/require/include/lute/userlandvfs.h @@ -23,6 +23,10 @@ struct Identifier { return std::tie(name, version) == std::tie(other.name, other.version); } + bool operator!=(const Identifier& other) const + { + return !(*this == other); + } }; struct IdentifierHashDefault diff --git a/tests/src/packagerequire.test.cpp b/tests/src/packagerequire.test.cpp index 421630aa8..22e6f7d3b 100644 --- a/tests/src/packagerequire.test.cpp +++ b/tests/src/packagerequire.test.cpp @@ -13,6 +13,8 @@ #include "lualib.h" #include +#include +#include #include "doctest.h" #include "lutefixture.h" @@ -27,13 +29,13 @@ TEST_CASE_FIXTURE(LuteFixture, "package_aware_require") [](lua_State* L) { std::string luteProjectRoot = getLuteProjectRootAbsolute(); - std::string entryRoot = luteProjectRoot + '/' + "tests/src/packages/packageentry"; + std::string entryRoot = luteProjectRoot + '/' + "tests/src/packages/package_aware_require/packageentry"; std::vector directDependencies = { {"dep", "2.0.0"}, }; - std::string packagesRoot = luteProjectRoot + '/' + "tests/src/packages"; + std::string packagesRoot = luteProjectRoot + '/' + "tests/src/packages/package_aware_require"; std::vector> allDependencies; allDependencies.push_back( {{"dep", "1.0.0"}, @@ -78,7 +80,7 @@ TEST_CASE_FIXTURE(LuteFixture, "package_aware_require") } ); - std::string path = getLuteProjectRootAbsolute() + "/tests/src/packages/packageentry/entry.luau"; + std::string path = getLuteProjectRootAbsolute() + "/tests/src/packages/package_aware_require/packageentry/entry.luau"; std::optional contents = readFile(path); REQUIRE(contents); @@ -86,3 +88,15 @@ TEST_CASE_FIXTURE(LuteFixture, "package_aware_require") bool success = runBytecode(runtime, bytecode, "@" + path, L, 0, nullptr, getReporter()); CHECK(success); } + +TEST_CASE_FIXTURE(LuteFixture, "pkgrun_with_lockfile") +{ + std::string entry = getLuteProjectRootAbsolute() + "/tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau"; + + char executablePlaceholder[] = "lute"; + char subcommand[] = "pkgrun"; + std::vector argv = {executablePlaceholder, subcommand, entry.data()}; + + CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); + auto rep = getReporter(); +} diff --git a/tests/src/packages/dep/module.luau b/tests/src/packages/package_aware_require/dep/module.luau similarity index 100% rename from tests/src/packages/dep/module.luau rename to tests/src/packages/package_aware_require/dep/module.luau diff --git a/tests/src/packages/internaldep/init.luau b/tests/src/packages/package_aware_require/internaldep/init.luau similarity index 100% rename from tests/src/packages/internaldep/init.luau rename to tests/src/packages/package_aware_require/internaldep/init.luau diff --git a/tests/src/packages/packageentry/entry.luau b/tests/src/packages/package_aware_require/packageentry/entry.luau similarity index 100% rename from tests/src/packages/packageentry/entry.luau rename to tests/src/packages/package_aware_require/packageentry/entry.luau diff --git a/tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau b/tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau new file mode 100644 index 000000000..22761ff19 --- /dev/null +++ b/tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau @@ -0,0 +1,3 @@ +local result = require("@internaldep") +result[#result + 1] = "external" +return result diff --git a/tests/src/packages/pkgrun_with_lockfile/Packages/internaldep/init.luau b/tests/src/packages/pkgrun_with_lockfile/Packages/internaldep/init.luau new file mode 100644 index 000000000..c351ed5a0 --- /dev/null +++ b/tests/src/packages/pkgrun_with_lockfile/Packages/internaldep/init.luau @@ -0,0 +1 @@ +return { "internal" } diff --git a/tests/src/packages/pkgrun_with_lockfile/loom.lock b/tests/src/packages/pkgrun_with_lockfile/loom.lock new file mode 100644 index 000000000..44c9f992d --- /dev/null +++ b/tests/src/packages/pkgrun_with_lockfile/loom.lock @@ -0,0 +1,9 @@ +version = 1 + +[[package]] +name = "dep" +version = "1.2.3" + +[[package]] +name = "internaldep" +version = "4.5.6" \ No newline at end of file diff --git a/tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau b/tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau new file mode 100644 index 000000000..d95ed26a3 --- /dev/null +++ b/tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau @@ -0,0 +1,7 @@ +--!strict +local result = require("@dep") :: { string } +assert(type(result) == "table") +assert(#result == 2) + +assert(result[1] == "internal") +assert(result[2] == "external") From be632d945ca5bffcc33b7a9953f9aa88394696ff Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:52:12 -0800 Subject: [PATCH 240/642] Creates a new `@std/net` library with the `request` API exposed (#672) This is needed for the package manager. Very barebones for now, and we will still need to expose the other parts of `@lute/net` under `@std/net`. --- definitions/net.luau | 4 ++-- lute/std/libs/luau.luau | 2 +- lute/std/libs/net.luau | 16 ++++++++++++++++ tests/std/net.test.luau | 16 ++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 lute/std/libs/net.luau create mode 100644 tests/std/net.test.luau diff --git a/definitions/net.luau b/definitions/net.luau index eb80bab32..572dbf4ac 100644 --- a/definitions/net.luau +++ b/definitions/net.luau @@ -6,14 +6,14 @@ export type Metadata = { headers: { [string]: string }?, } -export type Request = { +export type Response = { body: string, headers: { [string]: string }, status: number, ok: boolean, } -function net.request(url: string, metadata: Metadata?): Request +function net.request(url: string, metadata: Metadata?): Response error("not implemented") end diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 36374fb01..932801aaf 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -43,4 +43,4 @@ function luau.resolverequire(modulepath: string, frompath: path.pathlike): strin return luteLuau.resolverequire(modulepath, `@{frompathString}`) end -return luau +return table.freeze(luau) diff --git a/lute/std/libs/net.luau b/lute/std/libs/net.luau new file mode 100644 index 000000000..9b0a6c1dc --- /dev/null +++ b/lute/std/libs/net.luau @@ -0,0 +1,16 @@ +--!strict +-- @std/net +-- stdlib for `@lute/net` + +local luteNet = require("@lute/net") + +local net = {} + +export type metadata = luteNet.Metadata +export type response = luteNet.Response + +function net.request(url: string, metadata: metadata?): response + return luteNet.request(url, metadata) +end + +return table.freeze(net) diff --git a/tests/std/net.test.luau b/tests/std/net.test.luau new file mode 100644 index 000000000..d7317453c --- /dev/null +++ b/tests/std/net.test.luau @@ -0,0 +1,16 @@ +local net = require("@std/net") +local test = require("@std/test") + +test.suite("NetTestSuite", function(suite) + suite:case("request", function(assert) + -- Assumption: if this test is running in CI, then the repository's + -- GitHub page is reachable. + local response = net.request("https://github.com/luau-lang/lute", { + method = "GET", + }) + assert.eq(true, response.ok) + assert.eq(200, response.status) + end) +end) + +test.run() From 5efe8279b454a0f45b31070f96679b1fabc4775a Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 10 Dec 2025 14:09:11 -0800 Subject: [PATCH 241/642] Fixes issues with gitignore parsing in commands lib (#654) I encountered some `.gitignore` parsing bugs when I was trying to run `lute lint` in the lint directory. I've: - Ported over a fix that went into `luau-lang/codemod` but for some reason didn't make it into `lute transform` when I initially ported it - Added some additional fixups needed in `.gitignore` parsing - Removed some redundant rules from our actual `.gitignore` - Added tests for `cli/lib/files` which is built on the ignore parsing. the ignore stuff doesn't have a very nice api surface, so i opted to test it using the `files` stuff instead - Use system-specific path separators so it works on Windows (I would love to eventually port this stuff to just run on path objects so the OS stuff is abstracted away) --- .gitignore | 2 - lute/cli/commands/lib/files.luau | 2 +- lute/cli/commands/lib/ignore.luau | 45 ++--- tests/cli/lib/files.test.luau | 269 ++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+), 22 deletions(-) create mode 100644 tests/cli/lib/files.test.luau diff --git a/.gitignore b/.gitignore index 8a03e7908..3193d5aeb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,6 @@ __pycache__ .clangd compile_commands.json *~ -extern/libuv/libuv-static.pc -extern/libuv/DartConfiguration.tcl extern/*/ generated **/generated-types.luau diff --git a/lute/cli/commands/lib/files.luau b/lute/cli/commands/lib/files.luau index 8ed07dd10..7000e6bf0 100644 --- a/lute/cli/commands/lib/files.luau +++ b/lute/cli/commands/lib/files.luau @@ -53,7 +53,7 @@ local function getSourceFiles(paths: { string }, verbose: boolean?): { path.path local pathObj = path.parse(filepath) if not path.isabsolute(pathObj) then - pathObj = path.join(process.cwd(), pathObj) + pathObj = path.resolve(process.cwd(), pathObj) end filepath = path.format(pathObj) diff --git a/lute/cli/commands/lib/ignore.luau b/lute/cli/commands/lib/ignore.luau index 7c8a5d266..9a4f7eadf 100644 --- a/lute/cli/commands/lib/ignore.luau +++ b/lute/cli/commands/lib/ignore.luau @@ -1,13 +1,15 @@ -local fs = require("@lute/fs") - +local fs = require("@std/fs") local path = require("@std/path") local stringext = require("@std/stringext") +local system = require("@std/system") + +local separator = if system.win32 then "\\" else "/" --- Performs ignore resolution on a set of local IgnoreResolver = {} IgnoreResolver.__index = IgnoreResolver -type Glob = { pattern: string, onlyMatchDirectories: boolean } +type Glob = { pattern: string, onlyMatchDirectories: boolean, matchAnywhere: boolean } type GitignoreData = { location: string, @@ -50,7 +52,9 @@ local function parseGlob(glob: string): Glob -- i.e., anchor the pattern to the start if glob:find("/") then table.insert(lua_parts, "^") - if glob:sub(1, 1) == "." then + if glob:sub(1, 2) == "./" then + idx = 3 + elseif glob:sub(1, 1) == "/" or glob:sub(1, 1) == "." then idx = 2 end else @@ -59,7 +63,6 @@ local function parseGlob(glob: string): Glob -- We can't match for two distinct patterns bc Lua lacks an OR pattern operator -- Instead add unique marker we check for in matchesGlob, indicating we match -- against the two distinct patterns: anchored at beginning, or after a slash - table.insert(lua_parts, "MATCH_ANYWHERE:") matchAnywhere = true end @@ -86,17 +89,20 @@ local function parseGlob(glob: string): Glob idx = idx + 2 end else - -- Single asterisk `*` (matches anything except /) - table.insert(lua_parts, "[^/]*") + -- Single asterisk `*` (matches anything except directory separator) + table.insert(lua_parts, `[^{separator}]*`) idx = idx + 1 end elseif char == "?" then - -- Match any single character, except for / - table.insert(lua_parts, "[^/]") + -- Match any single character, except for directory separator + table.insert(lua_parts, `[^{separator}]`) idx = idx + 1 elseif char == "-" then table.insert(lua_parts, "%-") idx = idx + 1 + elseif char == "/" or char == "\\" then + table.insert(lua_parts, separator) + idx = idx + 1 else table.insert(lua_parts, char) idx = idx + 1 @@ -108,7 +114,11 @@ local function parseGlob(glob: string): Glob table.insert(lua_parts, "$") end - return { pattern = table.concat(lua_parts), onlyMatchDirectories = onlyMatchDirectories } + return { + pattern = table.concat(lua_parts), + onlyMatchDirectories = onlyMatchDirectories, + matchAnywhere = matchAnywhere, + } end local function parseGitignoreContents(location: string, contents: string): GitignoreData @@ -151,13 +161,11 @@ local function matchesGlob(glob: Glob, filepath: string, isDirectory: boolean): if glob.onlyMatchDirectories and not isDirectory then return false end - -- If the pattern starts with our matchAnywhere marker, we need to test + -- If the pattern is marked with matchAnywhere, we need to test -- both: at start of path OR after any directory separator - if glob.pattern:match("^MATCH_ANYWHERE:") then - local actualPattern = glob.pattern:gsub("^MATCH_ANYWHERE:(.*)$", "%1") - - local matchAtStart = filepath:match("^" .. actualPattern) ~= nil - local matchAfterDir = filepath:match("/" .. actualPattern) ~= nil + if glob.matchAnywhere then + local matchAtStart = filepath:match("^" .. glob.pattern) ~= nil + local matchAfterDir = filepath:match(separator .. glob.pattern) ~= nil return matchAtStart or matchAfterDir else @@ -168,8 +176,7 @@ end local function isIgnored(ignoreData: GitignoreData, filepath: string, isDirectory: boolean) local matched, ignored = false, false - -- TODO: compute relative path correctly - local relativePath = stringext.removeprefix(filepath, ignoreData.location) + local relativePath = path.format(path.relative(ignoreData.location, filepath)) for _, ignore in ignoreData.ignores do if matchesGlob(ignore, relativePath, isDirectory) then @@ -242,7 +249,7 @@ function IgnoreResolver._readIgnoreRecursive(self: IgnoreResolver, directory: st end if not baseIgnores then - baseIgnores = { location = "", ignores = {}, whitelists = {}, next = nil } :: GitignoreData + baseIgnores = { location = directory, ignores = {}, whitelists = {}, next = nil } :: GitignoreData end assert(baseIgnores, "Luau") diff --git a/tests/cli/lib/files.test.luau b/tests/cli/lib/files.test.luau new file mode 100644 index 000000000..fb657ed8d --- /dev/null +++ b/tests/cli/lib/files.test.luau @@ -0,0 +1,269 @@ +local files = require("@commands/lib/files") +local fs = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") + +local tmpdir = system.tmpdir() +local testPath = path.join(tmpdir, "gitignore_test") + +test.suite("cli/lib/files", function(suite) + suite:beforeeach(function() + if fs.exists(testPath) then + fs.removedirectory(testPath, { recursive = true }) + end + end) + + suite:case("parse simple file pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "*.lua\n") + + local shouldIgnorePath = path.join(testPath, "debug.lua") + fs.writestringtofile(shouldIgnorePath, "") + + local shouldNotIgnorePath = path.join(testPath, "readme.luau") + fs.writestringtofile(shouldNotIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse directory pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "node_modules/\n") + + local dirPath = path.join(testPath, "node_modules") + fs.createdirectory(dirPath) + local shouldIgnoreFile = path.join(dirPath, "package.luau") + fs.writestringtofile(shouldIgnoreFile, "") + + local nestedDirPath = path.join(testPath, "a", "node_modules") + fs.createdirectory(nestedDirPath, { makeparents = true }) + local shouldIgnoreNestedFile = path.join(nestedDirPath, "nested_package.luau") + fs.writestringtofile(shouldIgnoreNestedFile, "") + + local shouldNotIgnoreFile = path.join(testPath, "node_modules.luau") + fs.writestringtofile(shouldNotIgnoreFile, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnoreFile)], nil) + assert.eq(resultPaths[path.format(shouldIgnoreNestedFile)], nil) + assert.eq(resultPaths[path.format(shouldNotIgnoreFile)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse negation pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "*.lua\n!important.lua\n") + + local shouldIgnorePath = path.join(testPath, "debug.lua") + fs.writestringtofile(shouldIgnorePath, "") + + local shouldNotIgnorePath = path.join(testPath, "important.lua") + fs.writestringtofile(shouldNotIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse leading double asterisk pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "**/temp/foo\n") + + local subdir = path.join(testPath, "src", "temp", "foo") + fs.createdirectory(subdir, { makeparents = true }) + + local shouldIgnorePath = path.join(subdir, "template.luau") + fs.writestringtofile(shouldIgnorePath, "") + + local shouldntIgnorePath = path.join(testPath, "src", "temp", "hello.luau") + fs.writestringtofile(shouldntIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldntIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse trailing double asterisk pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "temp/foo/**\n") + + local subdir = path.join(testPath, "temp", "foo", "src") + fs.createdirectory(subdir, { makeparents = true }) + + local shouldIgnorePath = path.join(subdir, "template.luau") + fs.writestringtofile(shouldIgnorePath, "") + + local shouldIgnorePath2 = path.join(testPath, "temp", "foo", "template.luau") + fs.writestringtofile(shouldIgnorePath2, "") + + local shouldntIgnorePath = path.join(testPath, "temp", "hello.luau") + fs.writestringtofile(shouldntIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldIgnorePath2)], nil) + assert.eq(resultPaths[path.format(shouldntIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse middle double asterisk pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "temp/**/foo\n") + + local subdir = path.join(testPath, "temp", "src", "foo") + fs.createdirectory(subdir, { makeparents = true }) + + local shouldIgnorePath = path.join(subdir, "template.luau") + fs.writestringtofile(shouldIgnorePath, "") + + local subdir2 = path.join(testPath, "temp", "src", "test", "foo") + fs.createdirectory(subdir2, { makeparents = true }) + + local shouldIgnorePath2 = path.join(subdir2, "template.luau") + fs.writestringtofile(shouldIgnorePath2, "") + + local shouldntIgnorePath = path.join(testPath, "temp", "hello.luau") + fs.writestringtofile(shouldntIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) + assert.eq(resultPaths[path.format(shouldIgnorePath2)], nil) + assert.eq(resultPaths[path.format(shouldntIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse rooted pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "/build\n") + + fs.createdirectory(path.join(testPath, "build"), { makeparents = true }) + + local shouldIgnorePath = path.join(testPath, "build", "foo.luau") + fs.writestringtofile(shouldIgnorePath, "") + + fs.createdirectory(path.join(testPath, "src", "build"), { makeparents = true }) + + local shouldNotIgnorePath = path.join(testPath, "src", "build", "bar.luau") + fs.writestringtofile(shouldNotIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[(path.format(shouldIgnorePath))], nil) + assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) + + suite:case("parse question mark pattern", function(assert) + -- Setup + fs.createdirectory(testPath) + local gitignorePath = path.join(testPath, ".gitignore") + fs.writestringtofile(gitignorePath, "file?.luau\n") + + local shouldIgnorePath1 = path.join(testPath, "file1.luau") + fs.writestringtofile(shouldIgnorePath1, "") + + local shouldIgnorePath2 = path.join(testPath, "fileA.luau") + fs.writestringtofile(shouldIgnorePath2, "") + + local shouldNotIgnorePath = path.join(testPath, "file10.luau") + fs.writestringtofile(shouldNotIgnorePath, "") + + -- Do + local results = files.getSourceFiles({ path.format(testPath) }) + -- Check + local resultPaths = {} + for _, p in results do + resultPaths[path.format(p)] = true + end + assert.eq(resultPaths[path.format(shouldIgnorePath1)], nil) + assert.eq(resultPaths[path.format(shouldIgnorePath2)], nil) + assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) + + -- Teardown + fs.removedirectory(testPath, { recursive = true }) + end) +end) + +test.run() From b8a337b191031bc43ed66502842a0aeec69725ad Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:12:56 -0800 Subject: [PATCH 242/642] Fixes JSON string serialization and deserialization and adds tests (#674) Added all of the tests for JSON strings from https://github.com/nst/JSONTestSuite, and fixes all of the bugs related to strings --- lute/std/libs/json.luau | 141 +++++++++++------- ..._string_1st_surrogate_but_2nd_missing.json | 1 + ...tring_1st_valid_surrogate_2nd_invalid.json | 1 + .../i_string_UTF-16LE_with_BOM.json | Bin 0 -> 12 bytes .../i_string_UTF-8_invalid_sequence.json | 1 + .../i_string_UTF8_surrogate_U+D800.json | 1 + ...incomplete_surrogate_and_escape_valid.json | 1 + .../i_string_incomplete_surrogate_pair.json | 1 + ...ng_incomplete_surrogates_escape_valid.json | 1 + .../i_string_invalid_lonely_surrogate.json | 1 + .../i_string_invalid_surrogate.json | 1 + .../i_string_invalid_utf-8.json | 1 + .../i_string_inverted_surrogates_U+1D11E.json | 1 + .../i_string_iso_latin_1.json | 1 + .../i_string_lone_second_surrogate.json | 1 + .../i_string_lone_utf8_continuation_byte.json | 1 + .../i_string_not_in_unicode_range.json | 1 + .../i_string_overlong_sequence_2_bytes.json | 1 + .../i_string_overlong_sequence_6_bytes.json | 1 + ...string_overlong_sequence_6_bytes_null.json | 1 + .../i_string_truncated-utf-8.json | 1 + .../i_string_utf16BE_no_BOM.json | Bin 0 -> 10 bytes .../i_string_utf16LE_no_BOM.json | Bin 0 -> 10 bytes .../n_string_1_surrogate_then_escape.json | 1 + .../n_string_1_surrogate_then_escape_u.json | 1 + .../n_string_1_surrogate_then_escape_u1.json | 1 + .../n_string_1_surrogate_then_escape_u1x.json | 1 + .../n_string_accentuated_char_no_quotes.json | 1 + .../n_string_backslash_00.json | Bin 0 -> 6 bytes .../n_string_escape_x.json | 1 + .../n_string_escaped_backslash_bad.json | 1 + .../n_string_escaped_ctrl_char_tab.json | 1 + .../n_string_escaped_emoji.json | 1 + .../n_string_incomplete_escape.json | 1 + ...n_string_incomplete_escaped_character.json | 1 + .../n_string_incomplete_surrogate.json | 1 + ...g_incomplete_surrogate_escape_invalid.json | 1 + .../n_string_invalid-utf-8-in-escape.json | 1 + .../n_string_invalid_backslash_esc.json | 1 + .../n_string_invalid_unicode_escape.json | 1 + .../n_string_invalid_utf8_after_escape.json | 1 + .../n_string_leading_uescaped_thinspace.json | 1 + .../n_string_no_quotes_with_bad_escape.json | 1 + .../n_string_single_doublequote.json | 1 + .../n_string_single_quote.json | 1 + ...string_single_string_no_double_quotes.json | 1 + .../n_string_start_escape_unclosed.json | 1 + .../n_string_unescaped_ctrl_char.json | Bin 0 -> 7 bytes .../n_string_unescaped_newline.json | 2 + .../n_string_unescaped_tab.json | 1 + .../n_string_unicode_CapitalU.json | 1 + .../n_string_with_trailing_garbage.json | 1 + .../y_string_1_2_3_bytes_UTF-8_sequences.json | 1 + .../y_string_accepted_surrogate_pair.json | 1 + .../y_string_accepted_surrogate_pairs.json | 1 + .../y_string_allowed_escapes.json | 1 + ...y_string_backslash_and_u_escaped_zero.json | 1 + .../y_string_backslash_doublequotes.json | 1 + .../y_string_comments.json | 1 + .../y_string_double_escape_a.json | 1 + .../y_string_double_escape_n.json | 1 + .../y_string_escaped_control_character.json | 1 + .../y_string_escaped_noncharacter.json | 1 + .../y_string_in_array.json | 1 + .../y_string_in_array_with_leading_space.json | 1 + .../y_string_last_surrogates_1_and_2.json | 1 + .../y_string_nbsp_uescaped.json | 1 + ...y_string_nonCharacterInUTF-8_U+10FFFF.json | 1 + .../y_string_nonCharacterInUTF-8_U+FFFF.json | 1 + .../y_string_null_escape.json | 1 + .../y_string_one-byte-utf-8.json | 1 + .../std/JSONParsingTestSuite/y_string_pi.json | 1 + ...ring_reservedCharacterInUTF-8_U+1BFFF.json | 1 + .../y_string_simple_ascii.json | 1 + .../JSONParsingTestSuite/y_string_space.json | 1 + ...rogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json | 1 + .../y_string_three-byte-utf-8.json | 1 + .../y_string_two-byte-utf-8.json | 1 + .../y_string_u+2028_line_sep.json | 1 + .../y_string_u+2029_par_sep.json | 1 + .../y_string_uEscape.json | 1 + .../y_string_uescaped_newline.json | 1 + .../y_string_unescaped_char_delete.json | 1 + .../y_string_unicode.json | 1 + .../y_string_unicodeEscapedBackslash.json | 1 + .../y_string_unicode_2.json | 1 + .../y_string_unicode_U+10FFFE_nonchar.json | 1 + .../y_string_unicode_U+1FFFE_nonchar.json | 1 + ...tring_unicode_U+200B_ZERO_WIDTH_SPACE.json | 1 + ..._string_unicode_U+2064_invisible_plus.json | 1 + .../y_string_unicode_U+FDD0_nonchar.json | 1 + .../y_string_unicode_U+FFFE_nonchar.json | 1 + ...y_string_unicode_escaped_double_quote.json | 1 + .../JSONParsingTestSuite/y_string_utf8.json | 1 + .../y_string_with_del_character.json | 1 + 95 files changed, 176 insertions(+), 55 deletions(-) create mode 100644 tests/std/JSONParsingTestSuite/i_string_1st_surrogate_but_2nd_missing.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_1st_valid_surrogate_2nd_invalid.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_UTF-16LE_with_BOM.json create mode 100755 tests/std/JSONParsingTestSuite/i_string_UTF-8_invalid_sequence.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_UTF8_surrogate_U+D800.json create mode 100755 tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_and_escape_valid.json create mode 100755 tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_pair.json create mode 100755 tests/std/JSONParsingTestSuite/i_string_incomplete_surrogates_escape_valid.json create mode 100755 tests/std/JSONParsingTestSuite/i_string_invalid_lonely_surrogate.json create mode 100755 tests/std/JSONParsingTestSuite/i_string_invalid_surrogate.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_invalid_utf-8.json create mode 100755 tests/std/JSONParsingTestSuite/i_string_inverted_surrogates_U+1D11E.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_iso_latin_1.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_lone_second_surrogate.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_lone_utf8_continuation_byte.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_not_in_unicode_range.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_overlong_sequence_2_bytes.json create mode 100755 tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes.json create mode 100755 tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes_null.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_truncated-utf-8.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_utf16BE_no_BOM.json create mode 100644 tests/std/JSONParsingTestSuite/i_string_utf16LE_no_BOM.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1x.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_accentuated_char_no_quotes.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_backslash_00.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_escape_x.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_escaped_backslash_bad.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_escaped_ctrl_char_tab.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_escaped_emoji.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_incomplete_escape.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_incomplete_escaped_character.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate_escape_invalid.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_invalid-utf-8-in-escape.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_invalid_backslash_esc.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_invalid_unicode_escape.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_invalid_utf8_after_escape.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_leading_uescaped_thinspace.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_no_quotes_with_bad_escape.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_single_doublequote.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_single_quote.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_single_string_no_double_quotes.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_start_escape_unclosed.json create mode 100755 tests/std/JSONParsingTestSuite/n_string_unescaped_ctrl_char.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_unescaped_newline.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_unescaped_tab.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_unicode_CapitalU.json create mode 100644 tests/std/JSONParsingTestSuite/n_string_with_trailing_garbage.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_1_2_3_bytes_UTF-8_sequences.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pair.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pairs.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_allowed_escapes.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_backslash_and_u_escaped_zero.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_backslash_doublequotes.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_comments.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_double_escape_a.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_double_escape_n.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_escaped_control_character.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_escaped_noncharacter.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_in_array.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_in_array_with_leading_space.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_last_surrogates_1_and_2.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_nbsp_uescaped.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+10FFFF.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+FFFF.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_null_escape.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_one-byte-utf-8.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_pi.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_reservedCharacterInUTF-8_U+1BFFF.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_simple_ascii.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_space.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_three-byte-utf-8.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_two-byte-utf-8.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_u+2028_line_sep.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_u+2029_par_sep.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_uEscape.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_uescaped_newline.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_unescaped_char_delete.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_unicode.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_unicodeEscapedBackslash.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_unicode_2.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_unicode_U+10FFFE_nonchar.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_unicode_U+1FFFE_nonchar.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_unicode_U+2064_invisible_plus.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_unicode_U+FDD0_nonchar.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_unicode_U+FFFE_nonchar.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_unicode_escaped_double_quote.json create mode 100644 tests/std/JSONParsingTestSuite/y_string_utf8.json create mode 100755 tests/std/JSONParsingTestSuite/y_string_with_del_character.json diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index fa0412e97..6b476f8ef 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -16,6 +16,8 @@ local object_key = newproxy() -- serialization +local bufferSize = 1024 + type SerializerState = { buf: buffer, cursor: number, @@ -176,9 +178,8 @@ local function serializeTable(state: SerializerState, object: object) writeSpaces(state) - writeByte(state, string.byte('"')) - writeString(state, key) - writeString(state, '": ') + serializeString(state, key) + writeString(state, ": ") serializeAny(state, value) end @@ -277,75 +278,105 @@ local function deserializeNumber(state: DeserializerState) return num end -local function decodeSurrogatePair(high, low): string? - local highVal = tonumber(high, 16) - local lowVal = tonumber(low, 16) - - if not highVal or not lowVal then - return nil -- Invalid +local function deserializeString(state: DeserializerState): string + -- Must start with " + if currentByte(state) ~= string.byte('"') then + return deserializerError(state, "Expected string opening quote") end - -- Calculate the actual Unicode codepoint - local codepoint = 0x10000 + ((highVal - 0xD800) * 0x400) + (lowVal - 0xDC00) - return utf8.char(codepoint) -end - -local function deserializeString(state: DeserializerState): string + -- Skip opening quote " state.cursor += 1 local startPos = state.cursor - if currentByte(state) == string.byte('"') then - state.cursor += 1 + local buf = buffer.create(bufferSize) + local bufferPos = 0 - return "" + local function writeByteToBuffer(byte: number, dbuf: number, dstart: number) + buffer.writeu8(buf, bufferPos, byte) + bufferPos += dbuf + startPos += dstart end - while state.cursor <= #state.src do - if currentByte(state) == string.byte('"') then - state.cursor += 1 + while startPos <= #state.src do + local c = string.sub(state.src, startPos, startPos) - local source = string.sub(state.src, startPos, state.cursor - 2) - - source = string.gsub( - source, - "\\u([dD]83[dD])\\u(d[cC]%w%w)", - function(high, low) - return decodeSurrogatePair(high, low) or deserializerError(state, "Invalid unicode surrogate pair") - end :: any - ) - -- Handle regular Unicode escapes - source = string.gsub(source, "\\u(%x%x%x%x)", function(code) - return utf8.char(tonumber(code, 16) :: number) - end) - - source = string.gsub(source, "\\\\", "\0") - source = string.gsub(source, "\\b", "\b") - source = string.gsub(source, "\\f", "\f") - source = string.gsub(source, "\\n", "\n") - source = string.gsub(source, "\\r", "\r") - source = string.gsub(source, "\\t", "\t") - source = string.gsub(source, '\\"', '"') - source = string.gsub(source, "\0", "\\") - - return source + -- End of string + if c == '"' then + state.cursor = startPos + 1 + return buffer.tostring(buf, bufferPos) end - if currentByte(state) == string.byte("\\") then - state.cursor += 1 - end + -- Normal character + if c ~= "\\" then + local byte = string.byte(c) - state.cursor += 1 - end + -- No raw control chars allowed + if byte < string.byte(" ") then + return deserializerError(state, "Unescaped control character in string") + end + + writeByteToBuffer(byte, 1, 1) + else + -- Escape sequence + local source = string.sub(state.src, startPos + 1, startPos + 1) + + if source == "b" then + writeByteToBuffer(string.byte("\b"), 1, 2) + elseif source == "f" then + writeByteToBuffer(string.byte("\f"), 1, 2) + elseif source == "n" then + writeByteToBuffer(string.byte("\n"), 1, 2) + elseif source == "r" then + writeByteToBuffer(string.byte("\r"), 1, 2) + elseif source == "t" then + writeByteToBuffer(string.byte("\t"), 1, 2) + elseif source == '"' then + writeByteToBuffer(string.byte('"'), 1, 2) + elseif source == "\\" then + writeByteToBuffer(string.byte("\\"), 1, 2) + elseif source == "/" then + writeByteToBuffer(string.byte("/"), 1, 2) + elseif source == "u" then + -- Unicode escape: \uXXXX + local hex = string.sub(state.src, startPos + 2, startPos + 5) + if not hex:match("^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$") then + return deserializerError(state, "Invalid \\u escape") + end - -- error + local codepoint = tonumber(hex, 16) + if not codepoint then + return deserializerError(state, "Invalid \\u escape") + end + startPos += 6 -- skip \uXXXX + + -- Surrogate pair + if codepoint >= 0xD800 and codepoint <= 0xDBFF then + -- Look for \uDCxx following + local lo = state.src:match("^\\u([dD][cdefCDEF][0-9a-fA-F][0-9a-fA-F])", startPos) + if lo then + local lo_val = tonumber(lo, 16) + -- Combine surrogate halves + codepoint = 0x10000 + (codepoint :: number - 0xD800) * 0x400 + (lo_val :: number - 0xDC00) + startPos += 6 + end + end - state.cursor = startPos + local utf8_bytes = utf8.char(codepoint) + buffer.writestring(buf, bufferPos, utf8_bytes) + bufferPos += #utf8_bytes + else + -- Invalid escape: \" already covered + return deserializerError(state, "Invalid escape sequence '\\" .. source .. "'") + end + end + end return deserializerError(state, "Unterminated string") end local deserialize: (DeserializerState) -> (array | object | boolean | number | string)? + local function deserializeArray(state: DeserializerState): array if currentByte(state) ~= string.byte("[") then return deserializerError(state, "Expected array opening '['") @@ -470,7 +501,7 @@ deserialize = function(state: DeserializerState): value local fourChars = string.sub(state.src, state.cursor, state.cursor + 3) - if fourChars == "null" then + if string.match(fourChars, "^null") then state.cursor += 4 return json.null elseif string.match(fourChars, "^true") then @@ -482,7 +513,7 @@ deserialize = function(state: DeserializerState): value elseif string.match(state.src, "^[%-%d]", state.cursor) then -- potential number return deserializeNumber(state) - elseif string.byte(state.src, state.cursor) == string.byte('"') then + elseif string.match(state.src, '^"', state.cursor) then return deserializeString(state) elseif string.match(state.src, "^%[", state.cursor) then return deserializeArray(state) @@ -497,7 +528,7 @@ end function json.serialize(value: value, prettyPrint: boolean?) local state: SerializerState = { - buf = buffer.create(1024), + buf = buffer.create(bufferSize), cursor = 0, prettyPrint = prettyPrint or false, depth = 0, diff --git a/tests/std/JSONParsingTestSuite/i_string_1st_surrogate_but_2nd_missing.json b/tests/std/JSONParsingTestSuite/i_string_1st_surrogate_but_2nd_missing.json new file mode 100644 index 000000000..3b9e37c67 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_1st_surrogate_but_2nd_missing.json @@ -0,0 +1 @@ +["\uDADA"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_1st_valid_surrogate_2nd_invalid.json b/tests/std/JSONParsingTestSuite/i_string_1st_valid_surrogate_2nd_invalid.json new file mode 100644 index 000000000..487592832 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_1st_valid_surrogate_2nd_invalid.json @@ -0,0 +1 @@ +["\uD888\u1234"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_UTF-16LE_with_BOM.json b/tests/std/JSONParsingTestSuite/i_string_UTF-16LE_with_BOM.json new file mode 100644 index 0000000000000000000000000000000000000000..2a79c0629a49133d8f715bddbef19cfb3ef025bd GIT binary patch literal 12 ScmezWFPcG#;Uy5qG5`P~Km+3d literal 0 HcmV?d00001 diff --git a/tests/std/JSONParsingTestSuite/i_string_UTF-8_invalid_sequence.json b/tests/std/JSONParsingTestSuite/i_string_UTF-8_invalid_sequence.json new file mode 100755 index 000000000..e2a968a15 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_UTF-8_invalid_sequence.json @@ -0,0 +1 @@ +["日ш"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_UTF8_surrogate_U+D800.json b/tests/std/JSONParsingTestSuite/i_string_UTF8_surrogate_U+D800.json new file mode 100644 index 000000000..916bff920 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_UTF8_surrogate_U+D800.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_and_escape_valid.json b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_and_escape_valid.json new file mode 100755 index 000000000..3cb11d229 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_and_escape_valid.json @@ -0,0 +1 @@ +["\uD800\n"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_pair.json b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_pair.json new file mode 100755 index 000000000..38ec23bb0 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogate_pair.json @@ -0,0 +1 @@ +["\uDd1ea"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogates_escape_valid.json b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogates_escape_valid.json new file mode 100755 index 000000000..c9cd6f6c3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_incomplete_surrogates_escape_valid.json @@ -0,0 +1 @@ +["\uD800\uD800\n"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_invalid_lonely_surrogate.json b/tests/std/JSONParsingTestSuite/i_string_invalid_lonely_surrogate.json new file mode 100755 index 000000000..3abbd8d8d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_invalid_lonely_surrogate.json @@ -0,0 +1 @@ +["\ud800"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_invalid_surrogate.json b/tests/std/JSONParsingTestSuite/i_string_invalid_surrogate.json new file mode 100755 index 000000000..ffddc04f5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_invalid_surrogate.json @@ -0,0 +1 @@ +["\ud800abc"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_invalid_utf-8.json b/tests/std/JSONParsingTestSuite/i_string_invalid_utf-8.json new file mode 100644 index 000000000..8e45a7eca --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_invalid_utf-8.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_inverted_surrogates_U+1D11E.json b/tests/std/JSONParsingTestSuite/i_string_inverted_surrogates_U+1D11E.json new file mode 100755 index 000000000..0d5456cc3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_inverted_surrogates_U+1D11E.json @@ -0,0 +1 @@ +["\uDd1e\uD834"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_iso_latin_1.json b/tests/std/JSONParsingTestSuite/i_string_iso_latin_1.json new file mode 100644 index 000000000..9389c9823 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_iso_latin_1.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_lone_second_surrogate.json b/tests/std/JSONParsingTestSuite/i_string_lone_second_surrogate.json new file mode 100644 index 000000000..1dbd397f3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_lone_second_surrogate.json @@ -0,0 +1 @@ +["\uDFAA"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_lone_utf8_continuation_byte.json b/tests/std/JSONParsingTestSuite/i_string_lone_utf8_continuation_byte.json new file mode 100644 index 000000000..729337c0a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_lone_utf8_continuation_byte.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_not_in_unicode_range.json b/tests/std/JSONParsingTestSuite/i_string_not_in_unicode_range.json new file mode 100644 index 000000000..df90a2916 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_not_in_unicode_range.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_2_bytes.json b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_2_bytes.json new file mode 100644 index 000000000..c8cee5e0a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_2_bytes.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes.json b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes.json new file mode 100755 index 000000000..9a91da791 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes_null.json b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes_null.json new file mode 100755 index 000000000..d24fffdd9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_overlong_sequence_6_bytes_null.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_truncated-utf-8.json b/tests/std/JSONParsingTestSuite/i_string_truncated-utf-8.json new file mode 100644 index 000000000..63c7777fb --- /dev/null +++ b/tests/std/JSONParsingTestSuite/i_string_truncated-utf-8.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/i_string_utf16BE_no_BOM.json b/tests/std/JSONParsingTestSuite/i_string_utf16BE_no_BOM.json new file mode 100644 index 0000000000000000000000000000000000000000..57e5392ff6309134268c7e3ec2a9289b99ff7148 GIT binary patch literal 10 PcmZRGW>8{y3B<7g33~zN literal 0 HcmV?d00001 diff --git a/tests/std/JSONParsingTestSuite/i_string_utf16LE_no_BOM.json b/tests/std/JSONParsingTestSuite/i_string_utf16LE_no_BOM.json new file mode 100644 index 0000000000000000000000000000000000000000..c49c1b25d8e5819591993d4d45e4649a46c1b3e0 GIT binary patch literal 10 Pcma!MP-1uq#IXzj3t$1} literal 0 HcmV?d00001 diff --git a/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape.json b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape.json new file mode 100644 index 000000000..acec66d8f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape.json @@ -0,0 +1 @@ +["\uD800\"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u.json b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u.json new file mode 100644 index 000000000..e834b05e9 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u.json @@ -0,0 +1 @@ +["\uD800\u"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1.json b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1.json new file mode 100644 index 000000000..a04cd3489 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1.json @@ -0,0 +1 @@ +["\uD800\u1"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1x.json b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1x.json new file mode 100644 index 000000000..bfbd23409 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_1_surrogate_then_escape_u1x.json @@ -0,0 +1 @@ +["\uD800\u1x"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_accentuated_char_no_quotes.json b/tests/std/JSONParsingTestSuite/n_string_accentuated_char_no_quotes.json new file mode 100644 index 000000000..fd6895693 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_accentuated_char_no_quotes.json @@ -0,0 +1 @@ +[é] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_backslash_00.json b/tests/std/JSONParsingTestSuite/n_string_backslash_00.json new file mode 100644 index 0000000000000000000000000000000000000000..b5bf267b5d4ee922d20cec1543b66d1530ff76cf GIT binary patch literal 6 Ncma!6ieXTS1pox&0a*Y5 literal 0 HcmV?d00001 diff --git a/tests/std/JSONParsingTestSuite/n_string_escape_x.json b/tests/std/JSONParsingTestSuite/n_string_escape_x.json new file mode 100644 index 000000000..fae291938 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_escape_x.json @@ -0,0 +1 @@ +["\x00"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_escaped_backslash_bad.json b/tests/std/JSONParsingTestSuite/n_string_escaped_backslash_bad.json new file mode 100755 index 000000000..016fcb47e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_escaped_backslash_bad.json @@ -0,0 +1 @@ +["\\\"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_escaped_ctrl_char_tab.json b/tests/std/JSONParsingTestSuite/n_string_escaped_ctrl_char_tab.json new file mode 100644 index 000000000..f35ea382b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_escaped_ctrl_char_tab.json @@ -0,0 +1 @@ +["\ "] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_escaped_emoji.json b/tests/std/JSONParsingTestSuite/n_string_escaped_emoji.json new file mode 100644 index 000000000..a27775421 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_escaped_emoji.json @@ -0,0 +1 @@ +["\🌀"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_incomplete_escape.json b/tests/std/JSONParsingTestSuite/n_string_incomplete_escape.json new file mode 100755 index 000000000..3415c33ca --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_incomplete_escape.json @@ -0,0 +1 @@ +["\"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_incomplete_escaped_character.json b/tests/std/JSONParsingTestSuite/n_string_incomplete_escaped_character.json new file mode 100755 index 000000000..0f2197ea2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_incomplete_escaped_character.json @@ -0,0 +1 @@ +["\u00A"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate.json b/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate.json new file mode 100755 index 000000000..75504a656 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate.json @@ -0,0 +1 @@ +["\uD834\uDd"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate_escape_invalid.json b/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate_escape_invalid.json new file mode 100755 index 000000000..bd9656060 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_incomplete_surrogate_escape_invalid.json @@ -0,0 +1 @@ +["\uD800\uD800\x"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_invalid-utf-8-in-escape.json b/tests/std/JSONParsingTestSuite/n_string_invalid-utf-8-in-escape.json new file mode 100644 index 000000000..0c4300643 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_invalid-utf-8-in-escape.json @@ -0,0 +1 @@ +["\u"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_invalid_backslash_esc.json b/tests/std/JSONParsingTestSuite/n_string_invalid_backslash_esc.json new file mode 100755 index 000000000..d1eb60921 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_invalid_backslash_esc.json @@ -0,0 +1 @@ +["\a"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_invalid_unicode_escape.json b/tests/std/JSONParsingTestSuite/n_string_invalid_unicode_escape.json new file mode 100644 index 000000000..7608cb6ba --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_invalid_unicode_escape.json @@ -0,0 +1 @@ +["\uqqqq"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_invalid_utf8_after_escape.json b/tests/std/JSONParsingTestSuite/n_string_invalid_utf8_after_escape.json new file mode 100644 index 000000000..2f757a25b --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_invalid_utf8_after_escape.json @@ -0,0 +1 @@ +["\"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_leading_uescaped_thinspace.json b/tests/std/JSONParsingTestSuite/n_string_leading_uescaped_thinspace.json new file mode 100755 index 000000000..7b297c636 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_leading_uescaped_thinspace.json @@ -0,0 +1 @@ +[\u0020"asd"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_no_quotes_with_bad_escape.json b/tests/std/JSONParsingTestSuite/n_string_no_quotes_with_bad_escape.json new file mode 100644 index 000000000..01bc70aba --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_no_quotes_with_bad_escape.json @@ -0,0 +1 @@ +[\n] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_single_doublequote.json b/tests/std/JSONParsingTestSuite/n_string_single_doublequote.json new file mode 100755 index 000000000..9d68933c4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_single_doublequote.json @@ -0,0 +1 @@ +" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_single_quote.json b/tests/std/JSONParsingTestSuite/n_string_single_quote.json new file mode 100644 index 000000000..caff239bf --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_single_quote.json @@ -0,0 +1 @@ +['single quote'] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_single_string_no_double_quotes.json b/tests/std/JSONParsingTestSuite/n_string_single_string_no_double_quotes.json new file mode 100755 index 000000000..f2ba8f84a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_single_string_no_double_quotes.json @@ -0,0 +1 @@ +abc \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_start_escape_unclosed.json b/tests/std/JSONParsingTestSuite/n_string_start_escape_unclosed.json new file mode 100644 index 000000000..db62a46fc --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_start_escape_unclosed.json @@ -0,0 +1 @@ +["\ \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_unescaped_ctrl_char.json b/tests/std/JSONParsingTestSuite/n_string_unescaped_ctrl_char.json new file mode 100755 index 0000000000000000000000000000000000000000..9f21348071d3d736bdd469f159cea674c09237af GIT binary patch literal 7 Ocma!6N@Pe>iUj}$`2oKG literal 0 HcmV?d00001 diff --git a/tests/std/JSONParsingTestSuite/n_string_unescaped_newline.json b/tests/std/JSONParsingTestSuite/n_string_unescaped_newline.json new file mode 100644 index 000000000..700d36086 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_unescaped_newline.json @@ -0,0 +1,2 @@ +["new +line"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_unescaped_tab.json b/tests/std/JSONParsingTestSuite/n_string_unescaped_tab.json new file mode 100644 index 000000000..160264a2d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_unescaped_tab.json @@ -0,0 +1 @@ +[" "] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_unicode_CapitalU.json b/tests/std/JSONParsingTestSuite/n_string_unicode_CapitalU.json new file mode 100644 index 000000000..17332bb17 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_unicode_CapitalU.json @@ -0,0 +1 @@ +"\UA66D" \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/n_string_with_trailing_garbage.json b/tests/std/JSONParsingTestSuite/n_string_with_trailing_garbage.json new file mode 100644 index 000000000..efe3bd272 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/n_string_with_trailing_garbage.json @@ -0,0 +1 @@ +""x \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_1_2_3_bytes_UTF-8_sequences.json b/tests/std/JSONParsingTestSuite/y_string_1_2_3_bytes_UTF-8_sequences.json new file mode 100755 index 000000000..9967ddeb8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_1_2_3_bytes_UTF-8_sequences.json @@ -0,0 +1 @@ +["\u0060\u012a\u12AB"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pair.json b/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pair.json new file mode 100755 index 000000000..996875cc8 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pair.json @@ -0,0 +1 @@ +["\uD801\udc37"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pairs.json b/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pairs.json new file mode 100755 index 000000000..3401021ec --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_accepted_surrogate_pairs.json @@ -0,0 +1 @@ +["\ud83d\ude39\ud83d\udc8d"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_allowed_escapes.json b/tests/std/JSONParsingTestSuite/y_string_allowed_escapes.json new file mode 100644 index 000000000..7f495532f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_allowed_escapes.json @@ -0,0 +1 @@ +["\"\\\/\b\f\n\r\t"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_backslash_and_u_escaped_zero.json b/tests/std/JSONParsingTestSuite/y_string_backslash_and_u_escaped_zero.json new file mode 100755 index 000000000..d4439eda7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_backslash_and_u_escaped_zero.json @@ -0,0 +1 @@ +["\\u0000"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_backslash_doublequotes.json b/tests/std/JSONParsingTestSuite/y_string_backslash_doublequotes.json new file mode 100644 index 000000000..ae03243b6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_backslash_doublequotes.json @@ -0,0 +1 @@ +["\""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_comments.json b/tests/std/JSONParsingTestSuite/y_string_comments.json new file mode 100644 index 000000000..2260c20c2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_comments.json @@ -0,0 +1 @@ +["a/*b*/c/*d//e"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_double_escape_a.json b/tests/std/JSONParsingTestSuite/y_string_double_escape_a.json new file mode 100644 index 000000000..6715d6f40 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_double_escape_a.json @@ -0,0 +1 @@ +["\\a"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_double_escape_n.json b/tests/std/JSONParsingTestSuite/y_string_double_escape_n.json new file mode 100644 index 000000000..44ca56c4d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_double_escape_n.json @@ -0,0 +1 @@ +["\\n"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_escaped_control_character.json b/tests/std/JSONParsingTestSuite/y_string_escaped_control_character.json new file mode 100644 index 000000000..5b014a9c2 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_escaped_control_character.json @@ -0,0 +1 @@ +["\u0012"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_escaped_noncharacter.json b/tests/std/JSONParsingTestSuite/y_string_escaped_noncharacter.json new file mode 100755 index 000000000..2ff52e2c5 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_escaped_noncharacter.json @@ -0,0 +1 @@ +["\uFFFF"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_in_array.json b/tests/std/JSONParsingTestSuite/y_string_in_array.json new file mode 100755 index 000000000..21d7ae4cd --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_in_array.json @@ -0,0 +1 @@ +["asd"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_in_array_with_leading_space.json b/tests/std/JSONParsingTestSuite/y_string_in_array_with_leading_space.json new file mode 100755 index 000000000..9e1887c1e --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_in_array_with_leading_space.json @@ -0,0 +1 @@ +[ "asd"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_last_surrogates_1_and_2.json b/tests/std/JSONParsingTestSuite/y_string_last_surrogates_1_and_2.json new file mode 100644 index 000000000..3919cef76 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_last_surrogates_1_and_2.json @@ -0,0 +1 @@ +["\uDBFF\uDFFF"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_nbsp_uescaped.json b/tests/std/JSONParsingTestSuite/y_string_nbsp_uescaped.json new file mode 100644 index 000000000..2085ab1a1 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_nbsp_uescaped.json @@ -0,0 +1 @@ +["new\u00A0line"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+10FFFF.json b/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+10FFFF.json new file mode 100755 index 000000000..059e4d9dd --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+10FFFF.json @@ -0,0 +1 @@ +["􏿿"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+FFFF.json b/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+FFFF.json new file mode 100755 index 000000000..4c913bd41 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_nonCharacterInUTF-8_U+FFFF.json @@ -0,0 +1 @@ +["￿"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_null_escape.json b/tests/std/JSONParsingTestSuite/y_string_null_escape.json new file mode 100644 index 000000000..c1ad84404 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_null_escape.json @@ -0,0 +1 @@ +["\u0000"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_one-byte-utf-8.json b/tests/std/JSONParsingTestSuite/y_string_one-byte-utf-8.json new file mode 100644 index 000000000..157185923 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_one-byte-utf-8.json @@ -0,0 +1 @@ +["\u002c"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_pi.json b/tests/std/JSONParsingTestSuite/y_string_pi.json new file mode 100644 index 000000000..9df11ae88 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_pi.json @@ -0,0 +1 @@ +["π"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_reservedCharacterInUTF-8_U+1BFFF.json b/tests/std/JSONParsingTestSuite/y_string_reservedCharacterInUTF-8_U+1BFFF.json new file mode 100755 index 000000000..10a33a171 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_reservedCharacterInUTF-8_U+1BFFF.json @@ -0,0 +1 @@ +["𛿿"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_simple_ascii.json b/tests/std/JSONParsingTestSuite/y_string_simple_ascii.json new file mode 100644 index 000000000..8cadf7d05 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_simple_ascii.json @@ -0,0 +1 @@ +["asd "] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_space.json b/tests/std/JSONParsingTestSuite/y_string_space.json new file mode 100644 index 000000000..efd782cc3 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_space.json @@ -0,0 +1 @@ +" " \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json b/tests/std/JSONParsingTestSuite/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json new file mode 100755 index 000000000..7620b6655 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json @@ -0,0 +1 @@ +["\uD834\uDd1e"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_three-byte-utf-8.json b/tests/std/JSONParsingTestSuite/y_string_three-byte-utf-8.json new file mode 100644 index 000000000..108f1d67d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_three-byte-utf-8.json @@ -0,0 +1 @@ +["\u0821"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_two-byte-utf-8.json b/tests/std/JSONParsingTestSuite/y_string_two-byte-utf-8.json new file mode 100644 index 000000000..461503c31 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_two-byte-utf-8.json @@ -0,0 +1 @@ +["\u0123"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_u+2028_line_sep.json b/tests/std/JSONParsingTestSuite/y_string_u+2028_line_sep.json new file mode 100755 index 000000000..897b6021a --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_u+2028_line_sep.json @@ -0,0 +1 @@ +["
"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_u+2029_par_sep.json b/tests/std/JSONParsingTestSuite/y_string_u+2029_par_sep.json new file mode 100755 index 000000000..8cd998c89 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_u+2029_par_sep.json @@ -0,0 +1 @@ +["
"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_uEscape.json b/tests/std/JSONParsingTestSuite/y_string_uEscape.json new file mode 100755 index 000000000..f7b41a02f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_uEscape.json @@ -0,0 +1 @@ +["\u0061\u30af\u30EA\u30b9"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_uescaped_newline.json b/tests/std/JSONParsingTestSuite/y_string_uescaped_newline.json new file mode 100644 index 000000000..3a5a220b6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_uescaped_newline.json @@ -0,0 +1 @@ +["new\u000Aline"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unescaped_char_delete.json b/tests/std/JSONParsingTestSuite/y_string_unescaped_char_delete.json new file mode 100755 index 000000000..7d064f498 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unescaped_char_delete.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode.json b/tests/std/JSONParsingTestSuite/y_string_unicode.json new file mode 100644 index 000000000..3598095b7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode.json @@ -0,0 +1 @@ +["\uA66D"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicodeEscapedBackslash.json b/tests/std/JSONParsingTestSuite/y_string_unicodeEscapedBackslash.json new file mode 100755 index 000000000..0bb3b51e7 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicodeEscapedBackslash.json @@ -0,0 +1 @@ +["\u005C"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_2.json b/tests/std/JSONParsingTestSuite/y_string_unicode_2.json new file mode 100644 index 000000000..a7dcb9768 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_2.json @@ -0,0 +1 @@ +["⍂㈴⍂"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+10FFFE_nonchar.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+10FFFE_nonchar.json new file mode 100644 index 000000000..9a8370b96 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+10FFFE_nonchar.json @@ -0,0 +1 @@ +["\uDBFF\uDFFE"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+1FFFE_nonchar.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+1FFFE_nonchar.json new file mode 100644 index 000000000..c51f8ae45 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+1FFFE_nonchar.json @@ -0,0 +1 @@ +["\uD83F\uDFFE"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json new file mode 100644 index 000000000..626d5f815 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json @@ -0,0 +1 @@ +["\u200B"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+2064_invisible_plus.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+2064_invisible_plus.json new file mode 100644 index 000000000..1e23972c6 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+2064_invisible_plus.json @@ -0,0 +1 @@ +["\u2064"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+FDD0_nonchar.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+FDD0_nonchar.json new file mode 100644 index 000000000..18ef151b4 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+FDD0_nonchar.json @@ -0,0 +1 @@ +["\uFDD0"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_U+FFFE_nonchar.json b/tests/std/JSONParsingTestSuite/y_string_unicode_U+FFFE_nonchar.json new file mode 100644 index 000000000..13d261fda --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_U+FFFE_nonchar.json @@ -0,0 +1 @@ +["\uFFFE"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_unicode_escaped_double_quote.json b/tests/std/JSONParsingTestSuite/y_string_unicode_escaped_double_quote.json new file mode 100755 index 000000000..4e6257856 --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_unicode_escaped_double_quote.json @@ -0,0 +1 @@ +["\u0022"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_utf8.json b/tests/std/JSONParsingTestSuite/y_string_utf8.json new file mode 100644 index 000000000..40878435f --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_utf8.json @@ -0,0 +1 @@ +["€𝄞"] \ No newline at end of file diff --git a/tests/std/JSONParsingTestSuite/y_string_with_del_character.json b/tests/std/JSONParsingTestSuite/y_string_with_del_character.json new file mode 100755 index 000000000..8bd24907d --- /dev/null +++ b/tests/std/JSONParsingTestSuite/y_string_with_del_character.json @@ -0,0 +1 @@ +["aa"] \ No newline at end of file From 5e354a471832805f2a31d3163ba6b56406e1328c Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 10 Dec 2025 16:17:48 -0800 Subject: [PATCH 243/642] Ensures that Lute will not block other runtime threads during basic file system operations (open, close, read, write) (#661) This PR ensures that `fs.(open, close, read, write)` are non-blocking - that is, other `Luau` threads that could continue to run, will not blocked by long i/o. This fixes https://github.com/luau-lang/lute/issues/642, whereby a very long read, e.g. to /dev/random could cause the main Lute Runtime thread to block synchronously (which would prevent stepping the uv event loop as well). I've tested this locally and it works as expected, but I'm not sure how to write a test for this in a cross platform way. This PR also: - Adds a base `UVRequest` type - responsible for interfacing with the lute runtime to resume the original luau thread. During destruction, will invoke the uv_*_cleanup function. - Makes File Handles a lightuserdata. - Rewrites `stdfs.writefiletostring` and `stdfs.readstringfromfile` in terms of the low level `lute` primitives that don't block anymore. - Removes `readasync`, `writefiletostring` and `readstringfromfile` from the `lute` api surface. --- definitions/fs.luau | 12 - lute/fs/CMakeLists.txt | 2 + lute/fs/include/lute/fs.h | 3 - lute/fs/src/fs.cpp | 406 ++------------------------ lute/fs/src/fs_impl.cpp | 240 +++++++++++++++ lute/fs/src/fs_impl.h | 20 ++ lute/runtime/CMakeLists.txt | 2 + lute/runtime/include/lute/UVRequest.h | 124 ++++++++ lute/runtime/src/UVRequest.cpp | 15 + lute/std/libs/fs.luau | 9 +- tests/CMakeLists.txt | 1 + tests/src/lutefs.test.cpp | 57 ++++ 12 files changed, 498 insertions(+), 393 deletions(-) create mode 100644 lute/fs/src/fs_impl.cpp create mode 100644 lute/fs/src/fs_impl.h create mode 100644 lute/runtime/include/lute/UVRequest.h create mode 100644 lute/runtime/src/UVRequest.cpp create mode 100644 tests/src/lutefs.test.cpp diff --git a/definitions/fs.luau b/definitions/fs.luau index 0e00b1a46..8ebe3b6fd 100644 --- a/definitions/fs.luau +++ b/definitions/fs.luau @@ -91,16 +91,4 @@ function fs.rmdir(path: string): () error("not implemented") end -function fs.readfiletostring(filepath: string): string - error("not implemented") -end - -function fs.writestringtofile(filepath: string, contents: string): () - error("not implemented") -end - -function fs.readasync(filepath: string): string - error("not implemented") -end - return fs diff --git a/lute/fs/CMakeLists.txt b/lute/fs/CMakeLists.txt index a41db2abd..7d97b78fc 100644 --- a/lute/fs/CMakeLists.txt +++ b/lute/fs/CMakeLists.txt @@ -4,6 +4,8 @@ target_sources(Lute.Fs PRIVATE include/lute/fs.h src/fs.cpp + src/fs_impl.h + src/fs_impl.cpp ) target_compile_features(Lute.Fs PUBLIC cxx_std_17) diff --git a/lute/fs/include/lute/fs.h b/lute/fs/include/lute/fs.h index 33b840c88..c20698301 100644 --- a/lute/fs/include/lute/fs.h +++ b/lute/fs/include/lute/fs.h @@ -89,9 +89,6 @@ static const luaL_Reg lib[] = { {"listdir", listdir}, {"rmdir", fs_rmdir}, - {"readfiletostring", readfiletostring}, - {"writestringtofile", writestringtofile}, - {"readasync", readasync}, {NULL, NULL}, }; diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index a20cabc5c..e3d31b11c 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -11,6 +11,8 @@ #include #include + +#include "fs_impl.h" #ifdef _WIN32 #include #else @@ -70,6 +72,22 @@ const char* UV_DIRENT_TYPES[] = { UV_TYPENAME_BLOCK, }; +static UVFile* getFileHandle(lua_State* L, int index) +{ + if (!lua_islightuserdata(L, index)) + { + luaL_errorL(L, "Error: expected file handle"); + } + + auto* handle = static_cast(lua_tolightuserdata(L, index)); + if (!handle) + { + luaL_errorL(L, "Error: invalid file handle"); + } + + return handle; +} + std::optional setFlags(const char* c, int* openFlags) { int modeFlags = 0x0000; @@ -160,165 +178,26 @@ static const char* fileModeToType(uint64_t mode) } } -struct FileHandle -{ - ssize_t fileDescriptor = -1; - int errcode = -1; -}; - -l_noret luaL_errorHandle(lua_State* L, FileHandle& handle) -{ -#ifdef _MSC_VER - luaL_errorL(L, "Error writing to file with descriptor %Iu\n", handle.fileDescriptor); -#else - luaL_errorL(L, "Error writing to file with descriptor %zu\n", handle.fileDescriptor); -#endif -} - -void setfield(lua_State* L, const char* index, int value) -{ - lua_pushstring(L, index); - lua_pushinteger(L, value); - lua_settable(L, -3); -} - -void createFileHandle(lua_State* L, const FileHandle& toCreate) -{ - lua_newtable(L); - setfield(L, "fd", toCreate.fileDescriptor); - setfield(L, "err", toCreate.errcode); -} - -FileHandle unpackFileHandle(lua_State* L) -{ - FileHandle result; - - luaL_checktype(L, 1, LUA_TTABLE); - lua_getfield(L, 1, "fd"); - lua_getfield(L, 1, "err"); - - ssize_t fd = luaL_checkinteger(L, -2); - int err = luaL_checknumber(L, -1); - result.fileDescriptor = fd; - result.errcode = err; - - lua_pop(L, 2); // we got the args by value, so we can clean up the stack here - - return result; -} - int close(lua_State* L) { - lua_settop(L, 1); - FileHandle file = unpackFileHandle(L); - - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, file.fileDescriptor, nullptr); - uv_fs_req_cleanup(&closeReq); - return 0; + auto* handle = getFileHandle(L, 1); + return close_impl(L, handle); } -static char readBuffer[1024]; int read(lua_State* L) { - memset(readBuffer, 0, sizeof(readBuffer)); - // discard any extra arguments passed in - lua_settop(L, 1); - FileHandle file = unpackFileHandle(L); - - int numBytesRead = 0; - uv_fs_t readReq; - uv_buf_t iov = uv_buf_init(readBuffer, sizeof(readBuffer)); - // Output data - std::vector resultData; - do - { - uv_fs_read(uv_default_loop(), &readReq, file.fileDescriptor, &iov, 1, -1, nullptr); - - numBytesRead = readReq.result; - uv_fs_req_cleanup(&readReq); - - if (numBytesRead < 0) - { - memset(readBuffer, 0, sizeof(readBuffer)); - luaL_errorL(L, "Error reading: %s. Closing file.\n", uv_err_name(numBytesRead)); - return 0; - } - - for (int i = 0; i < numBytesRead; i++) - resultData.push_back(readBuffer[i]); - - } while (numBytesRead > 0); - - lua_pushlstring(L, resultData.data(), resultData.size()); - - // Clean up the scratch space - memset(readBuffer, 0, sizeof(readBuffer)); - return 1; + auto* handle = getFileHandle(L, 1); + return read_impl(L, handle); } int write(lua_State* L) { - char writeBuffer[4096]; + auto* handle = getFileHandle(L, 1); - // Reset the write buffer - int wbSize = sizeof(writeBuffer); - memset(writeBuffer, 0, sizeof(writeBuffer)); - FileHandle file = unpackFileHandle(L); size_t len; - const char* stringToWrite = luaL_checklstring(L, 2, &len); - - // Set up the buffer to write - int numBytesLeftToWrite = len; - int offset = 0; - do - { - // copy stringToWrite[0], numBytesLeftToWrite into write buffer - - int sizeToWrite = std::min(wbSize, numBytesLeftToWrite); - memcpy(writeBuffer, stringToWrite + offset, sizeToWrite); - uv_buf_t iov = uv_buf_init(writeBuffer, sizeToWrite); - - uv_fs_t writeReq; - int bytesWritten = 0; - uv_fs_write(uv_default_loop(), &writeReq, file.fileDescriptor, &iov, 1, -1, nullptr); - bytesWritten = writeReq.result; - uv_fs_req_cleanup(&writeReq); - - if (bytesWritten < 0) - { - // Error case. - memset(writeBuffer, 0, sizeof(writeBuffer)); - luaL_errorHandle(L, file); - return 0; - } + const char* data = luaL_checklstring(L, 2, &len); - - offset += bytesWritten; - numBytesLeftToWrite -= bytesWritten; - } while (numBytesLeftToWrite > 0); - - return 0; -} -// Returns 0 on error, 1 otherwise -std::optional openHelper(lua_State* L, const char* path, const char* mode, int* openFlags) -{ - std::optional modeFlags = setFlags(mode, openFlags); - if (!modeFlags) - return std::nullopt; - - uv_fs_t openReq; - int errcode = uv_fs_open(uv_default_loop(), &openReq, path, *openFlags, *modeFlags, nullptr); - auto result = openReq.result; - uv_fs_req_cleanup(&openReq); - - if (result < 0) - { - luaL_errorL(L, "Error opening file %s\n", path); - return std::nullopt; - } - - return FileHandle{result, errcode}; + return write_impl(L, handle, data, len); } int open(lua_State* L) @@ -327,7 +206,6 @@ int open(lua_State* L) if (nArgs < 1) { luaL_errorL(L, "Error: no file supplied\n"); - return 0; } const char* path = luaL_checkstring(L, 1); @@ -343,21 +221,13 @@ int open(lua_State* L) mode = luaL_checkstring(L, 2); } - if (std::optional result = openHelper(L, path, mode, &openFlags)) + std::optional modeFlags = setFlags(mode, &openFlags); + if (!modeFlags) { - createFileHandle(L, *result); - return 1; + luaL_errorL(L, "Error decoding mode: %s\n", mode); } - return 0; -} - -void cleanup(char* buffer, int size, const FileHandle& handle) -{ - memset(buffer, 0, size); - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, handle.fileDescriptor, nullptr); - uv_fs_req_cleanup(&closeReq); + return open_impl(L, path, openFlags, *modeFlags); } int fs_remove(lua_State* L) @@ -561,7 +431,7 @@ struct WatchHandle luaL_errorL(L, "Error stopping fs event: %s", uv_strerror(err)); } - uv_close((uv_handle_t*) &handle, nullptr); + uv_close((uv_handle_t*)&handle, nullptr); isClosed = true; @@ -786,222 +656,6 @@ int listdir(lua_State* L) return lua_yield(L, 0); } -int readfiletostring(lua_State* L) -{ - const char* path = luaL_checkstring(L, 1); - const char openMode[] = "r"; - int openFlags = 0x0000; - std::optional handle = openHelper(L, path, openMode, &openFlags); - if (!handle) - { - luaL_errorL(L, "Error opening file for reading at %s\n", path); - return 0; - } - - memset(readBuffer, 0, sizeof(readBuffer)); - // discard any extra arguments passed in - lua_settop(L, 1); - - int numBytesRead = 0; - uv_fs_t readReq; - uv_buf_t iov = uv_buf_init(readBuffer, sizeof(readBuffer)); - // Output data - std::vector resultData; - do - { - uv_fs_read(uv_default_loop(), &readReq, handle->fileDescriptor, &iov, 1, -1, nullptr); - - numBytesRead = readReq.result; - uv_fs_req_cleanup(&readReq); - - if (numBytesRead < 0) - { - cleanup(readBuffer, sizeof(readBuffer), *handle); - luaL_errorL(L, "Error reading: %s. Closing file.\n", uv_err_name(numBytesRead)); - return 0; - } - - for (int i = 0; i < numBytesRead; i++) - resultData.push_back(readBuffer[i]); - - } while (numBytesRead > 0); - - lua_pushlstring(L, resultData.data(), resultData.size()); - - // Clean up the scratch space - cleanup(readBuffer, sizeof(readBuffer), *handle); - return 1; -} - -int writestringtofile(lua_State* L) -{ - char writeBuffer[4096]; - - const char* path = luaL_checkstring(L, 1); - const char openMode[] = "w+"; - int openFlags = 0x0000; - std::optional handle = openHelper(L, path, openMode, &openFlags); - if (!handle) - { - luaL_errorL(L, "Error opening file for reading at %s\n", path); - return 0; - } - - int wbSize = sizeof(writeBuffer); - memset(writeBuffer, 0, sizeof(writeBuffer)); - size_t len; - const char* stringToWrite = luaL_checklstring(L, 2, &len); - - // Set up the buffer to write - int numBytesLeftToWrite = len; - int offset = 0; - uv_buf_t iov; - do - { - // copy stringToWrite[0], numBytesLeftToWrite into write buffer - - int sizeToWrite = std::min(wbSize, numBytesLeftToWrite); - memcpy(writeBuffer, stringToWrite + offset, sizeToWrite); - iov = uv_buf_init(writeBuffer, sizeToWrite); - - uv_fs_t writeReq; - int bytesWritten = 0; - uv_fs_write(uv_default_loop(), &writeReq, handle->fileDescriptor, &iov, 1, -1, nullptr); - bytesWritten = writeReq.result; - uv_fs_req_cleanup(&writeReq); - - if (bytesWritten < 0) - { - // Error case. - cleanup(writeBuffer, sizeof(writeBuffer), *handle); - luaL_errorHandle(L, *handle); - return 0; - } - - - offset += bytesWritten; - numBytesLeftToWrite -= bytesWritten; - } while (numBytesLeftToWrite > 0); - - cleanup(writeBuffer, sizeof(writeBuffer), *handle); - return 0; -} - -struct ResumeCaptureInformation -{ - explicit ResumeCaptureInformation(lua_State* L) - : token(getResumeToken(L)) - { - } - - ResumeToken token = nullptr; -}; - -uv_fs_t* createRequest(lua_State* L) -{ - uv_fs_t* req = new uv_fs_t(); - req->data = new ResumeCaptureInformation(L); - return req; -} - -ResumeCaptureInformation* getResumeInformation(uv_fs_t* req) -{ - return static_cast(req->data); -} - -int readasync(lua_State* L) -{ - const char* path = luaL_checkstring(L, 1); - - uv_fs_t* openReq = createRequest(L); - int err = uv_fs_open( - uv_default_loop(), - openReq, - path, - O_RDONLY, - 0, - [](uv_fs_t* req) - { - ResumeCaptureInformation* info = getResumeInformation(req); - int fd = req->result; - - if (fd < 0) - { - info->token->fail("Error opening file"); - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, fd, nullptr); - uv_fs_req_cleanup(&closeReq); - uv_fs_req_cleanup(req); - delete (ResumeCaptureInformation*)req->data; - delete req; - return; - } - - // Allocate the destination buffer for reading - char readBuffer[1024]; - memset(readBuffer, 0, sizeof(readBuffer)); - uv_buf_t iov = uv_buf_init(readBuffer, sizeof(readBuffer)); - // Read data - int numBytesRead = 0; - uv_fs_t readReq; - // Output data - std::vector resultData; - - do - { - uv_fs_read(uv_default_loop(), &readReq, fd, &iov, 1, -1, nullptr); - numBytesRead = readReq.result; - uv_fs_req_cleanup(&readReq); - - if (numBytesRead < 0) - { - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, fd, nullptr); - uv_fs_req_cleanup(&closeReq); - // Schedule error; - // Also, we should free the original request. We don't have to do this for the read req since it's sycnrhonous - info->token->fail("Error reading file"); - uv_fs_req_cleanup(req); - delete (ResumeCaptureInformation*)req->data; - delete req; - return; - } - - for (int i = 0; i < numBytesRead; i++) - resultData.push_back(readBuffer[i]); - - } while (numBytesRead > 0); - - // Push the result buffer onto the stack - info->token->complete( - [data = std::move(resultData)](lua_State* L) - { - lua_pushlstring(L, data.data(), data.size()); - return 1; - } - ); - - uv_fs_t closeReq; - uv_fs_close(uv_default_loop(), &closeReq, fd, nullptr); - uv_fs_req_cleanup(&closeReq); - uv_fs_req_cleanup(req); - // free the read buffer as well as the resume information and the request - delete (ResumeCaptureInformation*)req->data; - delete req; - return; - } - ); - - if (err) - { - delete static_cast(openReq->data); - delete openReq; - luaL_errorL(L, "%s", uv_strerror(err)); - } - - return lua_yield(L, 0); -} - } // namespace fs static void initalizeFS(lua_State* L) diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp new file mode 100644 index 000000000..0d84722bf --- /dev/null +++ b/lute/fs/src/fs_impl.cpp @@ -0,0 +1,240 @@ +#include "fs_impl.h" + +#include "lute/UVRequest.h" + +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + +constexpr size_t kChunkIOSize = 4096; + +namespace fs +{ + +using FSRequest = uvutils::UVRequest; + +struct FSRead : FSRequest +{ + FSRead(lua_State* L, UVFile* file) + : FSRequest(L) + , file(file) + { + chunk.resize(kChunkIOSize); + iov = uv_buf_init(chunk.data(), chunk.size()); + buffer.reserve(kChunkIOSize); + } + + static void readCallback(uv_fs_t* req); + + UVFile* file = nullptr; + std::vector buffer; + std::vector chunk; + uv_buf_t iov; +}; + +struct FSWrite : FSRequest +{ + FSWrite(lua_State* L, UVFile* file, const char* buf, size_t len) + : FSRequest(L) + , file(file) + , toWrite(buf, buf + len) + , offset(0) + { + chunk.resize(kChunkIOSize); + } + + static void writeCallback(uv_fs_t* req); + + UVFile* file = nullptr; + std::vector chunk; + uv_buf_t iov; + std::vector toWrite; + size_t offset = 0; +}; + +struct FSClose : FSRequest +{ + FSClose(lua_State* L, UVFile* file) + : FSRequest(L) + , file(file) + { + } + + ~FSClose() + { + delete file; + } + + UVFile* file = nullptr; +}; + +int open_impl(lua_State* L, const char* path, int flags, int mode) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_open( + uv_default_loop(), + &req->req, + path, + flags, + mode, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("Error opening file %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [result](lua_State* L) + { + auto* file = new UVFile(); + file->fd = result; + lua_pushlightuserdata(L, file); + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + +void FSRead::readCallback(uv_fs_t* req) +{ + auto r = uvutils::retake(req); + auto bytesRead = req->result; + + if (bytesRead < 0) + { + r->fail("Error reading file: %s", uv_strerror(bytesRead)); + return; + } + + if (bytesRead == 0) + { + r->succeed( + [buffer = std::move(r->buffer)](lua_State* L) + { + lua_pushlstring(L, buffer.data(), buffer.size()); + return 1; + } + ); + return; + } + + // Append the read data to our buffer + r->buffer.insert(r->buffer.end(), r->chunk.begin(), r->chunk.begin() + bytesRead); + + // It's possible that the next read call will read fewer than chunk.size() bytes + // In this case, the chunk buffer might still retain some data from this read. Just to be safe, zero it out + std::fill(r->chunk.begin(), r->chunk.end(), 0); + + uvutils::ScopedUVRequest scopedReq{std::move(r)}; + uv_fs_read(uv_default_loop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSRead::readCallback); +} + +void FSWrite::writeCallback(uv_fs_t* req) +{ + auto w = uvutils::retake(req); + auto bytesWritten = req->result; + if (bytesWritten < 0) + { + w->fail("Error writing file: %s", uv_strerror(bytesWritten)); + return; + } + + w->offset += bytesWritten; + if (w->offset == w->toWrite.size()) + { + w->succeed( + [](lua_State* L) + { + return 0; + } + ); + + return; + } + + // Copy next chunk to write + size_t remaining = w->toWrite.size() - w->offset; + size_t chunkSize = std::min(remaining, w->chunk.size()); + std::copy(w->toWrite.begin() + w->offset, w->toWrite.begin() + w->offset + chunkSize, w->chunk.begin()); + w->iov = uv_buf_init(w->chunk.data(), chunkSize); + + uvutils::ScopedUVRequest scopedReq{std::move(w)}; + uv_fs_write(uv_default_loop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSWrite::writeCallback); +} + +int read_impl(lua_State* L, UVFile* handle) +{ + if (!handle->fd.has_value()) + { + luaL_errorL(L, "File handle is closed"); + } + + uvutils::ScopedUVRequest req{L, handle}; + uv_fs_read(uv_default_loop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSRead::readCallback); + // Automatically releases when req goes out of scope + return lua_yield(L, 0); +} + +int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numBytes) +{ + if (!handle->fd.has_value()) + { + luaL_errorL(L, "File handle is closed"); + } + + uvutils::ScopedUVRequest req{L, handle, toWrite, numBytes}; + + // Copy first chunk to write + size_t chunkSize = std::min(numBytes, req->chunk.size()); + std::copy(req->toWrite.begin(), req->toWrite.begin() + chunkSize, req->chunk.begin()); + req->iov = uv_buf_init(req->chunk.data(), chunkSize); + + uv_fs_write(uv_default_loop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSWrite::writeCallback); + + return lua_yield(L, 0); +} + +int close_impl(lua_State* L, UVFile* handle) +{ + if (!handle->fd.has_value()) + { + luaL_errorL(L, "File handle is already closed"); + } + + uvutils::ScopedUVRequest req{L, handle}; + uv_fs_close( + uv_default_loop(), + &req->req, + handle->fd.value(), + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("Error closing file: %s", uv_strerror(result)); + return; + } + + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); +} + +} // namespace fs diff --git a/lute/fs/src/fs_impl.h b/lute/fs/src/fs_impl.h new file mode 100644 index 000000000..2d07a657f --- /dev/null +++ b/lute/fs/src/fs_impl.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +struct lua_State; + +namespace fs +{ + +struct UVFile +{ + std::optional fd = std::nullopt; +}; + +// New fs operations using UVRequest abstraction +int open_impl(lua_State* L, const char* path, int flags, int mode); +int read_impl(lua_State* L, UVFile* handle); +int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numBytes); +int close_impl(lua_State* L, UVFile* handle); +} // namespace fs diff --git a/lute/runtime/CMakeLists.txt b/lute/runtime/CMakeLists.txt index 839d88377..4d6cf895a 100644 --- a/lute/runtime/CMakeLists.txt +++ b/lute/runtime/CMakeLists.txt @@ -4,10 +4,12 @@ target_sources(Lute.Runtime PRIVATE include/lute/ref.h include/lute/runtime.h include/lute/userdatas.h + include/lute/UVRequest.h include/lute/uvutils.h src/ref.cpp src/runtime.cpp + src/UVRequest.cpp src/uvutils.cpp ) diff --git a/lute/runtime/include/lute/UVRequest.h b/lute/runtime/include/lute/UVRequest.h new file mode 100644 index 000000000..a8006b6c1 --- /dev/null +++ b/lute/runtime/include/lute/UVRequest.h @@ -0,0 +1,124 @@ +#pragma once + +#include "lute/runtime.h" + +#include "uv.h" + +#include +#include + +namespace uvutils +{ + +template +void cleanup_uv_req(ReqT* req) +{ +} + +template<> +void cleanup_uv_req(uv_fs_t* req); + +// Free template function to recover type +template +std::unique_ptr retake(ReqT* req) +{ + return std::unique_ptr(static_cast(req->data)); +} + +template +struct UVRequest +{ + UVRequest(lua_State* L) + : token(getResumeToken(L)) + { + req.data = this; + } + + UVRequest(const UVRequest&) = delete; + UVRequest& operator=(const UVRequest&) = delete; + UVRequest(UVRequest&&) = delete; + UVRequest& operator=(UVRequest&&) = delete; + + // Proxy to token->fail with format string support + template + void fail(const char* fmt, Args&&... args) + { + // First, determine the required size + int size = snprintf(nullptr, 0, fmt, std::forward(args)...); + if (size < 0) + { + token->fail("Format error in fail"); + return; + } + + // Allocate buffer with exact size needed (+1 for null terminator) + std::vector buffer(size + 1); + snprintf(buffer.data(), buffer.size(), fmt, std::forward(args)...); + token->fail(std::string(buffer.data())); + } + + template + void succeed(F&& cont) + { + token->complete(std::forward(cont)); + } + + ~UVRequest() + { + cleanup_uv_req(&req); + } + + ResumeToken token; + ReqT req; +}; + + +template +struct ScopedUVRequest +{ + + ScopedUVRequest(std::unique_ptr req) + : ptr(std::move(req)) + { + } + + // Constructor that creates the unique_ptr from arguments + template + explicit ScopedUVRequest(Args&&... args) + : ptr(std::make_unique(std::forward(args)...)) + { + } + + ~ScopedUVRequest() + { + // The actual libuv C request struct retains a pointer to the request in the data field. + // If we don't call release, this unique_pointer will be freed at the end of the scope that + // contains the request, when we actually want this to be freed inside the asynchronously called callback. + // The user should then take ownership of the request from the data via `retake` and the destructr + // will automatically be invoked after we've scheduled the Luau code to resume + if (ptr) + { + ptr.release(); + } + } + + // Non-copyable and non-movable to prevent accidental double-release + ScopedUVRequest(const ScopedUVRequest&) = delete; + ScopedUVRequest& operator=(const ScopedUVRequest&) = delete; + ScopedUVRequest(ScopedUVRequest&&) = delete; + ScopedUVRequest& operator=(ScopedUVRequest&&) = delete; + + T* get() const + { + return ptr.get(); + } + + T* operator->() const + { + return ptr.get(); + } + + std::unique_ptr ptr; +}; + +} // namespace uvutils diff --git a/lute/runtime/src/UVRequest.cpp b/lute/runtime/src/UVRequest.cpp new file mode 100644 index 000000000..149e11bd1 --- /dev/null +++ b/lute/runtime/src/UVRequest.cpp @@ -0,0 +1,15 @@ +#include "lute/UVRequest.h" + +#include "uv.h" + +namespace uvutils +{ + +// UV request cleanup specializations +template<> +void cleanup_uv_req(uv_fs_t* req) +{ + uv_fs_req_cleanup(req); +} + +} // namespace uvutils diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index def4b9386..c9e791ce8 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -111,11 +111,16 @@ function fslib.listdirectory(path: pathlike): { directoryentry } end function fslib.readfiletostring(filepath: pathlike): string - return fs.readfiletostring(pathlib.format(filepath)) + local f = fs.open(pathlib.format(filepath), "r") + local contents = fs.read(f) + fs.close(f) + return contents end function fslib.writestringtofile(filepath: pathlike, contents: string): () - return fs.writestringtofile(pathlib.format(filepath), contents) + local f = fs.open(pathlib.format(filepath), "w+") + fs.write(f, contents) + fs.close(f) end function fslib.createdirectory(path: pathlike, options: createdirectoryoptions?): () diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4b7cf783e..b353f8384 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ target_sources(Lute.Test PRIVATE # Test files src/compile.test.cpp src/configresolver.test.cpp + src/lutefs.test.cpp src/modulepath.test.cpp src/moduleresolver.test.cpp src/packagerequire.test.cpp diff --git a/tests/src/lutefs.test.cpp b/tests/src/lutefs.test.cpp new file mode 100644 index 000000000..46bf3f499 --- /dev/null +++ b/tests/src/lutefs.test.cpp @@ -0,0 +1,57 @@ +#include "lute/fileutils.h" + +#include "Luau/FileUtils.h" + +#include + +#include "cliruntimefixture.h" +#include "doctest.h" +#include "luteprojectroot.h" + +TEST_CASE_FIXTURE(CliRuntimeFixture, "fs_open_write_read_close") +{ + std::string testFile = "./tmp/lute_test_file.txt"; + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string localTmpDir = joinPaths(luteProjectRoot, "./tmp"); + + // Clean up any existing file + Lute::removeFile(testFile); + Lute::createDirectories(localTmpDir); + runCode( + R"( + local fs = require("@lute/fs") + local path = ")" + + testFile + R"(" + + -- Open file for writing + local h = fs.open(path, "w+") + + -- Write some data + fs.write(h, "Hello, World!") + + -- Close the file + fs.close(h) + + -- Open file for reading + local hr = fs.open(path, "r") + assert(hr ~= nil, "File handle for reading should not be nil") + + -- Read the data + local content = fs.read(hr) + local correctness = content == "Hello, World!" + + -- Close the read handle + fs.close(hr) + + report(content) + report(correctness) + )" + ); + + CHECK(getReporter().getOutputs()[0] == "Hello, World!"); + CHECK(getReporter().getOutputs()[1] == "true"); + + // Clean up + Lute::removeFile(testFile); + Lute::removeDirectory(localTmpDir); +} From 7bb6cdd55b524620b0cec515c84c518e7b415991 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 11 Dec 2025 13:13:24 -0800 Subject: [PATCH 244/642] @std/test Tech Debt: Refactor how we provide debug information via assertions + make runtime error detection less brittle (#651) This PR attempts to refactor the test library to be slightly more maintainable. While trying to bring https://github.com/luau-lang/lute/pull/519 up to date, I experienced a lot of brittleness with our test libraries ability to detect when a test failed due to runtime errors. The test runner also produces debug information in an adhoc manner multiple times in different places, which really sucks. This PR ports over the [frktest](https://github.com/itsfrank/frktest/blob/main/src/assert_impl.luau) implementation of assertions. Rather than having `assertions` immediately yield control flow via `error`, all assertions internally return a typed `failure?`. When this failure is emitted, we extract all of the debug information needed for tests, exactly once at the point of the assertion 'failure'. This means that the test runner no longer has to know how to produce debug information (woo separation of concerns!), and the actual assertion implementation is responsible for this. Another freebie is that we only have to produce stacktraces in exactly one place - I've gone ahead and added that, along with updating the runtime error tests. ## Future Work - The stacktraces currently produces are not very good - this is because `lute run` compiles the script with all optimizations on, and the inling negatively impacts the quality of stacktraces. I discussed an option with @vrn-sn to lower the optimization level for .test.luau files. - The current implementation of `asserts` behaves like `REQUIRE` - control flow is immediately stopped at this point. Since all assertion implementations are wrapped by a function that determines how to handle the control flow, we can now also provide an implementation of `CHECK`, where you can fail a test and resume control flow. --- lute/std/libs/test/assert.luau | 117 +++++++++++++++++++++---------- lute/std/libs/test/failure.luau | 30 ++++++++ lute/std/libs/test/reporter.luau | 14 ++-- lute/std/libs/test/runner.luau | 58 ++++----------- lute/std/libs/test/types.luau | 41 +++++++++-- tests/std/test.test.luau | 8 ++- 6 files changed, 173 insertions(+), 95 deletions(-) create mode 100644 lute/std/libs/test/failure.luau diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index 813aae4eb..cf3f1939c 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -1,28 +1,17 @@ --!strict -- @std/test/assert -- Standard assertion library for Luau + +local failure = require("./failure") local stringext = require("@std/stringext") +local types = require("./types") -local assertions = {} +-- types +type failure = types.failure +type asserts = types.asserts -function assertions.eq(lhs: T, rhs: T, msg: string?) - if lhs ~= rhs then - error({ msg = msg or `{lhs} ~= {rhs}` }) - end -end - -function assertions.neq(lhs: T, rhs: T, msg: string?) - if lhs == rhs then - error({ msg = msg or `{lhs} == {rhs}` }) - end -end - -function assertions.errors(callback: () -> (), msg: string?) - local success = pcall(callback) - if success then - error({ msg = msg or `{callback} did not throw error.` }) - end -end +-- more utils +local assertion = failure.assertion -- Helper to format values for error messages local function formatValue(val: any): string @@ -74,25 +63,52 @@ local function tablesEqual(t1: unknown, t2: unknown, path: string): (boolean, st return true, nil end +local function eq(lhs: T, rhs: T, msg: string?): failure? + if lhs == rhs then + return nil + end + + return assertion(msg or `{lhs} ~= {rhs}`) +end + +local function neq(lhs: T, rhs: T, msg: string?): failure? + if lhs ~= rhs then + return nil + end + + return assertion(msg or `{lhs} == {rhs}`) +end + +local function errors(callback: () -> (), msg: string?): failure? + local success = pcall(callback) + -- if the callback failed, then this assert passes + if not success then + return nil + end + + return assertion(msg or `{callback} did not throw error.`) +end + -- Table equality assertionsion with deep comparison -function assertions.tableeq(lhs: { [unknown]: unknown }, rhs: { [unknown]: unknown }) +local function tableeq(lhs: { [unknown]: unknown }, rhs: { [unknown]: unknown }): failure? local equal, msg = tablesEqual(lhs, rhs, "") - if not equal then - error({ msg = msg or "tables are not equal" }) + if equal then + return nil end + return assertion(msg or "tables are not equal") end -function assertions.buffereq(lhs: buffer, rhs: buffer) +local function buffereq(lhs: buffer, rhs: buffer): failure? if typeof(lhs) ~= "buffer" then - error({ msg = `lhs is of type {typeof(lhs)}, not buffer` }) + return assertion(`lhs is of type {typeof(lhs)}, not buffer`) end if typeof(rhs) ~= "buffer" then - error({ msg = `rhs is of type {typeof(rhs)}, not buffer` }) + return assertion(`rhs is of type {typeof(rhs)}, not buffer`) end if buffer.len(lhs) ~= buffer.len(rhs) then - error({ msg = "buffers are of unequal length" }) + return assertion("buffers are of unequal length") end local len = buffer.len(lhs) @@ -101,48 +117,73 @@ function assertions.buffereq(lhs: buffer, rhs: buffer) local right = buffer.readu8(rhs, offset) if left ~= right then - error({ msg = string.format("lhs[%d] = %02x, but rhs[%d] = %02x", offset, left, offset, right) }) + return assertion(string.format("lhs[%d] = %02x, but rhs[%d] = %02x", offset, left, offset, right)) end end + return nil end -function assertions.erroreq(func: (A...) -> ...unknown, expectedErrorMessage: string, ...: A...) +local function erroreq(func: (A...) -> ...unknown, expectedErrorMessage: string, ...: A...): failure? local success, err = pcall(func, ...) if success then - error({ msg = "Function did not error as expected." }) + return assertion("Function did not error as expected.") end local actualErrorMessage = tostring(err) -- pcall appends filename and line number of the error, so we check suffix only if not stringext.hassuffix(actualErrorMessage, expectedErrorMessage) then - error({ msg = `Expected suffix of error message "{expectedErrorMessage}", but got "{actualErrorMessage}"` }) + return assertion(`Expected suffix of error message "{expectedErrorMessage}", but got "{actualErrorMessage}"`) end + + return nil end -function assertions.strcontains(haystack: string, needle: string, msg: string?, instances: number?) +local function strcontains(haystack: string, needle: string, msg: string?, instances: number?): failure? instances = instances or 1 if instances <= 0 then - error({ msg = "instances must be greater than 0" }) + return assertion("instances must be greater than 0") end if instances == 1 then if haystack:find(needle, 1, true) == nil then - error({ msg = if msg then msg else `Expected "{haystack}" to contain "{needle}".` }) + return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) end else if #haystack:split(needle) ~= instances + 1 then - error({ msg = if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.` }) + return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.`) end end + return nil end -function assertions.strnotcontains(haystack: string, needle: string, msg: string?) +local function strnotcontains(haystack: string, needle: string, msg: string?): failure? if haystack:find(needle, 1, true) ~= nil then - error({ msg = if msg then msg else `Expected "{haystack}" to not contain "{needle}".` }) + return assertion(if msg then msg else `Expected "{haystack}" to not contain "{needle}".`) end + + return nil end -export type asserts = typeof(assertions) +-- utility for taking an assertion and wrapping it as a check that must complete successfully or will return control flow to the error handler +local function reqassert(req: (T...) -> failure?): (T...) -> failure? + return function(...): failure? + local result: failure? = req(...) + if result ~= nil then + error(result) + end + + return nil + end +end -return table.freeze(assertions) +return table.freeze({ + eq = reqassert(eq), + neq = reqassert(neq), + errors = reqassert(errors), + tableeq = reqassert(tableeq), + buffereq = reqassert(buffereq), + erroreq = reqassert(erroreq), + strcontains = reqassert(strcontains), + strnotcontains = reqassert(strnotcontains), +}) :: asserts diff --git a/lute/std/libs/test/failure.luau b/lute/std/libs/test/failure.luau new file mode 100644 index 000000000..636dc77bc --- /dev/null +++ b/lute/std/libs/test/failure.luau @@ -0,0 +1,30 @@ +--!strict +-- @std/test/failure +-- utilities for working with test failures +local types = require("./types") + +type failure = types.failure + +local failures = {} +-- Generates the debug information needed to describe assertion failure locations +function failures.assertion(msg: string): failure + -- we need to go 5 function calls up to get to the actual assertion that invoked this + local filename, linenumber = debug.info(4, "sl") + local name = debug.info(2, "n") + return { assertion = name, msg = msg, filename = filename, linenumber = linenumber, __tag = "assertion" } +end + +-- Generates the debug information needed to describe assertion failure locations +function failures.lifecycle(hook: hook, err): failure + -- we need to go 5 function calls up to get to the actual assertion that invoked this + local filename, linenumber = debug.info(3, "sl") + return { hook = hook, msg = tostring(err), filename = filename, linenumber = linenumber, __tag = "lifecycle" } +end + +-- Generates the debug information needed to describe runtime error failure +function failures.runtimeerror(err): failure + -- we need to go 5 function calls up to get to the actual assertion that invoked this + return { msg = tostring(err), stacktrace = debug.traceback(tostring(err)), __tag = "runtime_error" } +end + +return table.freeze(failures) diff --git a/lute/std/libs/test/reporter.luau b/lute/std/libs/test/reporter.luau index eb36aa5ed..8f0a48a10 100644 --- a/lute/std/libs/test/reporter.luau +++ b/lute/std/libs/test/reporter.luau @@ -13,12 +13,16 @@ local reporter = {} local function printFailedTest(failed: FailedTest) print(`❌ {failed.test}`) - if failed.assertion ~= nil then - print(` {failed.filename}:{failed.linenumber}`) - print(` {failed.assertion}: {failed.error}`) + if failed.failure.__tag == "assertion" then + print(`\t{failed.failure.filename}:{failed.failure.linenumber}`) + print(`\t\t{failed.failure.assertion}: {failed.failure.msg}`) + elseif failed.failure.__tag == "lifecycle" then + print(`\t{failed.failure.filename}:{failed.failure.linenumber}`) + print(`\t\t{failed.failure.hook}: {failed.failure.msg}`) else - print(` Failed with: Runtime error in {failed.test} in:`) - print(` {failed.filename}:{failed.linenumber}`) + print(`\tRuntime error: {failed.failure.msg}`) + print(`Stacktrace:`) + print(failed.failure.stacktrace) end print() end diff --git a/lute/std/libs/test/runner.luau b/lute/std/libs/test/runner.luau index 2c1be5934..2d6688934 100644 --- a/lute/std/libs/test/runner.luau +++ b/lute/std/libs/test/runner.luau @@ -2,9 +2,9 @@ -- @std/test/runner -- Standard test runner library for Luau -local types = require("./types") local asserts = require("./assert") -local path = require("@std/path") +local failure = require("./failure") +local types = require("./types") type Test = types.test type TestCase = types.testcase @@ -31,14 +31,11 @@ function runner.run(env: TestEnvironment): TestRunResult for _, tc in suite.cases do local failedtest: FailedTest = { test = `{suite.name}.{tc.name}`, - assertion = "beforeall", - error = `beforeall hook failed: {err}`, - filename = "", - linenumber = 0, + failure = failure.lifecycle("beforeall", err), } table.insert(failures, failedtest) end - return tostring(err) + return err end end @@ -48,13 +45,10 @@ function runner.run(env: TestEnvironment): TestRunResult failed += 1 local failedtest: FailedTest = { test = testFullName, - assertion = "beforeeach", - error = `beforeeach hook failed: {err}`, - filename = "", - linenumber = 0, + failure = failure.lifecycle("beforeeach", err), } table.insert(failures, failedtest) - return tostring(err) + return err end end @@ -63,13 +57,10 @@ function runner.run(env: TestEnvironment): TestRunResult return function(err) local failedtest: FailedTest = { test = testFullName, - assertion = "aftereach", - error = `aftereach hook failed: {err}`, - filename = "", - linenumber = 0, + failure = failure.lifecycle("aftereach", err), } table.insert(failures, failedtest) - return tostring(err) + return err end end @@ -79,13 +70,10 @@ function runner.run(env: TestEnvironment): TestRunResult failed += 1 local failedtest: FailedTest = { test = suiteName, - assertion = "afterall", - error = `afterall hook failed: {err}`, - filename = "", - linenumber = 0, + failure = failure.lifecycle("afterall", err), } table.insert(failures, failedtest) - return tostring(err) + return err end end @@ -93,19 +81,10 @@ function runner.run(env: TestEnvironment): TestRunResult for _, tc in env.anonymous do total += 1 local function handlefailure(err) - local fileName, lineNumber = debug.info(4, "sl") - local assertName: string = debug.info(3, "n") - - if assertName == "xpcall" then - fileName, lineNumber = debug.info(2, "sl") - end - + local wasruntimefailure = not (typeof(err) == "table" and err.__tag and err.__tag == "assertion") local failedtest: FailedTest = { test = tc.name, - assertion = if assertName == "xpcall" then nil else assertName, - error = if assertName == "xpcall" then nil else err.msg, - filename = path.format(fileName), - linenumber = lineNumber, + failure = if wasruntimefailure then failure.runtimeerror(err) else err, } table.insert(failures, failedtest) failed += 1 @@ -132,19 +111,10 @@ function runner.run(env: TestEnvironment): TestRunResult total += 1 local testFullName = `{suite.name}.{tc.name}` local function handlefailure(err) - local fileName, lineNumber = debug.info(4, "sl") - local assertName = debug.info(3, "n") - - if assertName == "xpcall" then - fileName, lineNumber = debug.info(2, "sl") - end - + local wasruntimefailure = not (typeof(err) == "table" and err.__tag and err.__tag == "assertion") local failedtest: FailedTest = { test = testFullName, - assertion = if assertName == "xpcall" then nil else assertName, - error = if assertName == "xpcall" then nil else err.msg, - filename = path.format(fileName), - linenumber = lineNumber, + failure = if wasruntimefailure then failure.runtimeerror(err) else err, } table.insert(failures, failedtest) end diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index edc5e596f..2a00284ab 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -3,9 +3,41 @@ -- Standard test types library for Luau -- Test Reporting -local assert = require("./assert") -export type test = (assert.asserts) -> () +export type hook = "beforeeach" | "beforeall" | "aftereach" | "afterall" +export type failure = + { + assertion: string, -- name of the assertion + msg: string, -- failure message + filename: string, -- filename + linenumber: number, -- linenumber + __tag: "assertion", + } + | { + hook: hook, -- the name of the hook that broke + msg: string, -- failure message + filename: string, -- filename + linenumber: number, -- linenumber + __tag: "lifecycle", + } + | { + msg: string, -- failure message + stacktrace: string, -- stacktrace + __tag: "runtime_error", + } + +export type asserts = { + eq: (T, T, string?) -> failure?, + neq: (T, T, string?) -> failure?, + errors: (() -> (), string?) -> failure?, + tableeq: ({ [unknown]: unknown }, { [unknown]: unknown }) -> failure?, + buffereq: (buffer, buffer) -> failure?, + erroreq: ((A...) -> ...unknown, string, A...) -> failure?, + strcontains: (string, string, string?, number?) -> failure?, + strnotcontains: (string, string, string?) -> failure?, +} + +export type test = (asserts) -> () export type testcase = { name: string, case: test } @@ -25,10 +57,7 @@ export type testsuite = { export type failedtest = { test: string, -- name of the test case that failed - assertion: string?, -- if an assertion failed, name of the assertion that failed (e.g., "assert.eq") - error: string?, -- the error message to report with a failed assertion - filename: string, -- source file where the failure occurred - linenumber: number, -- line number of the failure + failure: failure, } export type testrunresult = { diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index 6c00a5179..df8538a83 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -51,11 +51,13 @@ test.run() assert.eq(result.exitcode, 1) assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number - assert.strcontains(result.stdout, `Failed with: Runtime error in nil_field_suite.access_field_of_nil in:`) + assert.strcontains(result.stdout, `Runtime error`) assert.strcontains( result.stdout, `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:6` ) + assert.strcontains(result.stdout, `nil_field_access_test.test.luau:6: attempt to index nil with 'field'`) + assert.strcontains(result.stdout, `nil_field_access_test.test.luau:10`) fs.remove(testFilePath) end) @@ -84,11 +86,13 @@ test.run() assert.eq(result.exitcode, 1) assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number - assert.strcontains(result.stdout, `Failed with: Runtime error in access_field_of_nil in:`) + assert.strcontains(result.stdout, `Runtime error`) assert.strcontains( result.stdout, `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:5` ) + assert.strcontains(result.stdout, `nil_field_access_test.test.luau:5: attempt to index nil with 'field'`) + assert.strcontains(result.stdout, `nil_field_access_test.test.luau:8`) fs.remove(testFilePath) end) From bdf062d8d8d0bc689b55ea12ed83ae90859a1adc Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:23:26 -0800 Subject: [PATCH 245/642] Adds test for std/json object() and asobject() (#675) --- tests/std/json.test.luau | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau index 1aa2916df..c3a34bc37 100644 --- a/tests/std/json.test.luau +++ b/tests/std/json.test.luau @@ -46,6 +46,21 @@ test.suite("JSONParsingTestSuite", function(suite) end end) end + + suite:case("json_object_and_asobject", function(assert) + local obj = json.object({ + name = "Alice", + age = 30, + }) + assert.eq(type(obj), "table") + assert.eq(obj.name, "Alice") + assert.eq(obj.age, 30) + + local asobj = json.asobject(obj) + assert.eq(type(asobj), "table") + assert.eq(asobj.name, "Alice") + assert.eq(asobj.age, 30) + end) end) test.run() From f005d8841f38c8bcc17945622a5c97fe93c96d5b Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:31:23 -0800 Subject: [PATCH 246/642] Exposes `net.serve` through `@std/net` and adds its missing return type (#676) 1. Expose `net.serve` in `@std/net` as a wrapper for `@lute/net`'s version. 2. Add a `Server` return type to this function that seemed to have been missed when it was first added. 3. Update the `net.test.luau` unit test to create a local server and request from it instead of hitting an arbitrary URL (this was causing some flakiness). 4. Add types to our example file `examples/serve_html.luau` so that it passes a type check. --- definitions/net.luau | 8 +++++++- examples/serve_html.luau | 4 ++-- lute/net/src/net.cpp | 2 +- lute/std/libs/net.luau | 24 +++++++++++++++++------- tests/std/net.test.luau | 18 ++++++++++++++---- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/definitions/net.luau b/definitions/net.luau index 572dbf4ac..3135efb52 100644 --- a/definitions/net.luau +++ b/definitions/net.luau @@ -41,7 +41,13 @@ export type Configuration = { handler: Handler, } -function net.serve(config: Handler | Configuration) +export type Server = { + hostname: string, + port: number, + close: () -> (), +} + +function net.serve(config: Handler | Configuration): Server error("not implemented") end diff --git a/examples/serve_html.luau b/examples/serve_html.luau index cecccc9da..127a26d80 100644 --- a/examples/serve_html.luau +++ b/examples/serve_html.luau @@ -1,8 +1,8 @@ -local net = require("@lute/net") +local net = require("@std/net") local server = net.serve({ port = 8080, - handler = function(req) + handler = function(req: net.receivedrequest): net.serverresponse local headers = "" for key, value in req.headers do headers ..= `\n {key}: {value}` diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index fba5a1aa0..6314674b6 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -532,7 +532,7 @@ int lua_serve(lua_State* L) { uWS::Loop::get(uv_default_loop()); - std::string hostname = "0.0.0.0"; + std::string hostname = "127.0.0.1"; int port = 3000; bool reusePort = false; std::optional tlsOptions; diff --git a/lute/std/libs/net.luau b/lute/std/libs/net.luau index 9b0a6c1dc..7c5ddbc78 100644 --- a/lute/std/libs/net.luau +++ b/lute/std/libs/net.luau @@ -2,15 +2,25 @@ -- @std/net -- stdlib for `@lute/net` -local luteNet = require("@lute/net") +local net = require("@lute/net") -local net = {} +local netlib = {} -export type metadata = luteNet.Metadata -export type response = luteNet.Response +export type metadata = net.Metadata +export type response = net.Response -function net.request(url: string, metadata: metadata?): response - return luteNet.request(url, metadata) +function netlib.request(url: string, metadata: metadata?): response + return net.request(url, metadata) end -return table.freeze(net) +export type receivedrequest = net.ReceivedRequest +export type serverresponse = net.ServerResponse +export type handler = net.Handler +export type configuration = net.Configuration +export type server = net.Server + +function netlib.serve(config: handler | configuration): server + return net.serve(config) +end + +return table.freeze(netlib) diff --git a/tests/std/net.test.luau b/tests/std/net.test.luau index d7317453c..d62b9932e 100644 --- a/tests/std/net.test.luau +++ b/tests/std/net.test.luau @@ -2,14 +2,24 @@ local net = require("@std/net") local test = require("@std/test") test.suite("NetTestSuite", function(suite) - suite:case("request", function(assert) - -- Assumption: if this test is running in CI, then the repository's - -- GitHub page is reachable. - local response = net.request("https://github.com/luau-lang/lute", { + suite:case("serve_locally_and_request", function(assert) + local server = net.serve({ + handler = function(req: net.receivedrequest): net.serverresponse + return { + status = 200, + body = "Hello, World!", + } + end, + }) + + local response = net.request(`{server.hostname}:{server.port}`, { method = "GET", }) assert.eq(true, response.ok) assert.eq(200, response.status) + assert.eq("Hello, World!", response.body) + + server.close() end) end) From 79cd6536916c7a23f11ae01d8c7884e38bda1e60 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 11 Dec 2025 15:38:46 -0800 Subject: [PATCH 247/642] Extends `lute setup` to generate `@lint` alias (#679) This allows users to write lint rules outside the lute repo while still getting lint-specific type definitions by using `require(@lint/types)`. There were also some bugs in `luthier` because it was calling functions which were deleted from `@lute/fs`. This was uncaught in CI because our CI jobs currently call `luthier` using a pinned, previous version of `lute`. We should add CI jobs which use `bootstrap.sh` to build `lute0` to call `luthier`. --- lute/cli/commands/setup/init.luau | 1 + tools/luthier.luau | 40 +++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 492feeb35..a0bd745ae 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -9,6 +9,7 @@ local BASE_PATH = path.join(process.homedir(), TYPEDEFS_PATH) local BASE_PATH_PRETTY = `~/{TYPEDEFS_PATH}` local TEMPLATE_RC_FILE = { aliases = { + lint = `{BASE_PATH_PRETTY}/lint`, lute = `{BASE_PATH_PRETTY}/lute`, std = `{BASE_PATH_PRETTY}/std`, }, diff --git a/tools/luthier.luau b/tools/luthier.luau index 43f4e57ca..21c6df921 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -35,6 +35,19 @@ local function gitVersion(): { major: number, minor: number, path: number } } end +local function readFileToString(path: string): string + local file = fs.open(path, "r") + local contents = fs.read(file) + fs.close(file) + return contents +end + +local function writeStringToFile(path: string, contents: string): () + local file = fs.open(path, "w+") + fs.write(file, contents) + fs.close(file) +end + local gitVersionInfo = gitVersion() local os = string.lower(system.os) @@ -234,7 +247,7 @@ local function getStdLibHash(): string traverseFileTree(libsPath, function(path, relativePath) if safeFsType(path) == "file" then toHash ..= "./" .. relativePath - toHash ..= fs.readfiletostring(path) + toHash ..= readFileToString(path) end end) @@ -249,7 +262,7 @@ local function isGeneratedStdLibUpToDate(): boolean return false end - local hash = fs.readfiletostring(hashFile) + local hash = readFileToString(hashFile) return hash == getStdLibHash() end @@ -291,7 +304,7 @@ local function generateStdLibFilesIfNeeded() local aliasedPath = "@std/" .. relativePath if safeFsType(path) == "file" then - local libSrc = fs.readfiletostring(path) + local libSrc = readFileToString(path) if #libSrc > 16_000 then -- https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026 -- MSVC can't handle a single string literal being over 16380 single-byte characters, @@ -348,7 +361,7 @@ local function getTypeDefinitionsHash(): string traverseFileTree(path, function(path, relativePath) if safeFsType(path) == "file" then toHash ..= `./{namespace}/{relativePath}` - toHash ..= fs.readfiletostring(path) + toHash ..= readFileToString(path) end end) end @@ -367,7 +380,7 @@ local function isGeneratedTypeDefinitionsUpToDate(): boolean return false end - local hash = fs.readfiletostring(hashFile) + local hash = readFileToString(hashFile) return hash == getTypeDefinitionsHash() end @@ -389,7 +402,7 @@ local function generateTypeDefinitionFiles() local function traverseDefs(path: string, namespace: string) traverseFileTree(path, function(path, relativePath) if safeFsType(path) == "file" then - local content = fs.readasync(path) + local content = readFileToString(path) dictionaryEntries[`{namespace}/{relativePath}`] = content end end) @@ -398,6 +411,9 @@ local function generateTypeDefinitionFiles() traverseDefs(libsPath, "lute") traverseDefs(stdPath, "std") + dictionaryEntries["lint/types.luau"] = + readFileToString(projectRelative("lute", "cli", "commands", "lint", "types.luau")) + local stringDictionary = "" for key, value in dictionaryEntries do @@ -408,7 +424,7 @@ local function generateTypeDefinitionFiles() fs.write(hash, getTypeDefinitionsHash()) fs.close(hash) - fs.writestringtofile( + writeStringToFile( projectRelative("lute", "cli", "commands", "setup", "generated", "definitions.luau"), string.format(TYPEDEF_PATTERN, stringDictionary) ) @@ -421,7 +437,7 @@ local function getCliCommandsHash() traverseFileTree(libsPath, function(path, relativePath) if safeFsType(path) == "file" then toHash ..= "./" .. relativePath - toHash ..= fs.readfiletostring(path) + toHash ..= readFileToString(path) end end) @@ -436,7 +452,7 @@ local function isGeneratedCliCommandsUpToDate() return false end - local hash = fs.readfiletostring(hashFile) + local hash = readFileToString(hashFile) return hash == getCliCommandsHash() end @@ -473,7 +489,7 @@ local function generateCliCommandsFilesIfNeeded() local aliasedPath = "@cli/" .. relativePath if safeFsType(path) == "file" then - local libSrc = fs.readfiletostring(path) + local libSrc = readFileToString(path) if #libSrc > 16_000 then -- https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026 -- MSVC can't handle a single string literal being over 16380 single-byte characters, @@ -660,7 +676,7 @@ local function getTuneFilesHash(): string end toHash ..= file.name - toHash ..= fs.readfiletostring(joinPath(externPath, file.name)) + toHash ..= readFileToString(joinPath(externPath, file.name)) end local digest = crypto.digest(crypto.hash.blake2b256, toHash) @@ -674,7 +690,7 @@ local function areTuneFilesUpToDate(): boolean return false end - local hash = fs.readfiletostring(hashFile) + local hash = readFileToString(hashFile) return hash == getTuneFilesHash() end From 3e7b585d4fc562152c0e8f69fe0e15728bb87b7e Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 11 Dec 2025 16:06:31 -0800 Subject: [PATCH 248/642] Adds CI jobs which call luthier with a bootstrapped lute (#680) --- .github/actions/setup-and-build/action.yml | 12 ++++++++++++ .github/workflows/ci.yml | 10 ++++++++++ tools/luthier.luau | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index def71560d..ecfc89091 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -16,6 +16,10 @@ inputs: token: description: "Token for foreman" required: true + use-bootstrap: + description: "Whether to use the bootstrap script to build the Lute used to run luthier" + required: false + default: "false" outputs: exe_path: @@ -60,6 +64,14 @@ runs: path: extern key: extern-${{ hashFiles('extern/*.tune') }} + - name: Bootstrap Lute + if: ${{ inputs.use-bootstrap == 'true' }} + shell: bash + run: | + echo "Bootstrapping Lute..." + ./tools/bootstrap.sh + alias lute="./build/lute0" + - name: Fetch ${{ inputs.target }} if: steps.cache-extern.outputs.cache-hit != 'true' shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f5ae7ca1..1b7e5be3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,15 @@ jobs: options: --c-compiler clang --cxx-compiler clang++ --enable-asan - os: macos-latest options: --enable-asan + - os: ubuntu-latest + options: --c-compiler gcc --cxx-compiler g++ --enable-asan + use-bootstrap: true + - os: ubuntu-latest + options: --c-compiler clang --cxx-compiler clang++ --enable-asan + use-bootstrap: true + - os: macos-latest + options: --enable-asan + use-bootstrap: true fail-fast: false steps: @@ -41,6 +50,7 @@ jobs: config: debug options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} + use-bootstrap: ${{ matrix.use-bootstrap || 'false' }} - name: Use CI .luaurc file run: rm .luaurc && mv .luaurc.ci .luaurc diff --git a/tools/luthier.luau b/tools/luthier.luau index 21c6df921..fa0ece8f6 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -607,7 +607,7 @@ local function getConfigureArguments() end local function readTuneFile(path: string): Tune - return toml.deserialize(fs.readasync(path)) + return toml.deserialize(readFileToString(path)) end local function check(exitCode: number) From 139e48374bae3ee3e8065840e787e57303905c72 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:53:28 -0800 Subject: [PATCH 249/642] Supports parsing Luau-syntax lock files in `lute pkgrun ` (#678) We make use of the existing Luau-syntax configuration file extraction API to parse our Luau-syntax lock file (`loom.lock.luau`). The temporary regex hack has been deleted. --- lute/cli/src/packagerun.cpp | 71 ++++++++++++++++--- .../packages/pkgrun_with_lockfile/loom.lock | 9 --- .../pkgrun_with_lockfile/loom.lock.luau | 13 ++++ 3 files changed, 74 insertions(+), 19 deletions(-) delete mode 100644 tests/src/packages/pkgrun_with_lockfile/loom.lock create mode 100644 tests/src/packages/pkgrun_with_lockfile/loom.lock.luau diff --git a/lute/cli/src/packagerun.cpp b/lute/cli/src/packagerun.cpp index 1400d970d..f64614eee 100644 --- a/lute/cli/src/packagerun.cpp +++ b/lute/cli/src/packagerun.cpp @@ -1,9 +1,13 @@ #include "lute/packagerun.h" +#include "lute/userlandvfs.h" + #include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" +#include +#include #include -#include #include #include @@ -22,7 +26,7 @@ std::optional getAbsolutePathToNearestLockfile(std::string entryFil std::optional currentPath = getParentPath(entryFile); while (currentPath) { - std::string lockfilePath = joinPaths(*currentPath, "loom.lock"); + std::string lockfilePath = joinPaths(*currentPath, "loom.lock.luau"); if (isFile(lockfilePath)) return lockfilePath; @@ -32,19 +36,66 @@ std::optional getAbsolutePathToNearestLockfile(std::string entryFil return std::nullopt; } +static std::string toLower(std::string_view str) +{ + std::string result(str); + std::transform( + result.begin(), + result.end(), + result.begin(), + [](unsigned char c) + { + return std::tolower(c); + } + ); + return result; +} + static std::vector extractIdentifiers(std::string lockfileContents) { + // TODO: support timing out Luau-syntax configurations + std::optional configOpt = Luau::extractConfig(lockfileContents, {}); + if (!configOpt) + return {}; + + Luau::ConfigTable config = std::move(*configOpt); + if (!config.contains("package")) + return {}; + + Luau::ConfigTable* packageTable = config["package"].get_if(); + if (!packageTable) + return {}; + std::vector packages; + packages.resize(packageTable->size()); - // TODO: regex matching is a temporary solution; the lockfile format is - // likely to change, and we will use a stable parsing solution then. - auto it = lockfileContents.cbegin(); - std::smatch match; - std::regex re{R"LUTE(name = "([^"]+)"[\r\n]+version = "?([^"\r\n]+)"?)LUTE"}; - while (std::regex_search(it, lockfileContents.cend(), match, re)) + for (const auto& [k, v] : *packageTable) { - packages.push_back(Package::Identifier{match[1].str(), match[2].str()}); - it = match.suffix().first; + const double* key = k.get_if(); + if (!key) + return {}; + + const size_t index = static_cast(*key); + if (index < 1 || packageTable->size() < index) + return {}; + + const Luau::ConfigTable* package = v.get_if(); + if (!package) + return {}; + + if (!package->contains("name") || !package->contains("rev")) + return {}; + + const std::string* name = (*package).find("name")->get_if(); + const std::string* rev = (*package).find("rev")->get_if(); + + if (!name || !rev) + return {}; + + Package::Identifier entry{}; + entry.name = toLower(*name); + entry.version = *rev; + packages[index - 1] = std::move(entry); } return packages; diff --git a/tests/src/packages/pkgrun_with_lockfile/loom.lock b/tests/src/packages/pkgrun_with_lockfile/loom.lock deleted file mode 100644 index 44c9f992d..000000000 --- a/tests/src/packages/pkgrun_with_lockfile/loom.lock +++ /dev/null @@ -1,9 +0,0 @@ -version = 1 - -[[package]] -name = "dep" -version = "1.2.3" - -[[package]] -name = "internaldep" -version = "4.5.6" \ No newline at end of file diff --git a/tests/src/packages/pkgrun_with_lockfile/loom.lock.luau b/tests/src/packages/pkgrun_with_lockfile/loom.lock.luau new file mode 100644 index 000000000..e2fe02dab --- /dev/null +++ b/tests/src/packages/pkgrun_with_lockfile/loom.lock.luau @@ -0,0 +1,13 @@ +return { + version = 1, + package = { + { + name = "dep", + rev = "v1.2.3", + }, + { + name = "internaldep", + rev = "v4.5.6", + }, + }, +} From 13501bb08834dc0aba651ae8bf2f038bfbf6f39f Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 11 Dec 2025 16:58:26 -0800 Subject: [PATCH 250/642] Disables bootstrapping in CI on macOS (#683) @vrn-sn pointed out that the times are quite bad for bootstrapping in CI, but we also want to retain the benefits of confirming that it is still working on a per-PR basis. The compromise position here is that the marginal difference between macOS without bootstrap on GH Actions runners and Linux with bootstrap on GH Actions runners is quite small (8 minutes vs 9 minutes). We'll remove the macOS bootstrap since that one is prone to timing out, but the Linux bootstrap is only marginally slower than our existing CI and gets us most of the benefit of knowing if `luthier` broke somehow. --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b7e5be3c..55227d542 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,9 +34,6 @@ jobs: - os: ubuntu-latest options: --c-compiler clang --cxx-compiler clang++ --enable-asan use-bootstrap: true - - os: macos-latest - options: --enable-asan - use-bootstrap: true fail-fast: false steps: From 3c3a61f302b8775da1d29d725c99cd2246cb3c35 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 12 Dec 2025 11:03:22 -0800 Subject: [PATCH 251/642] Updates lute lint to return an error code if any lint violations are found (#682) However, if we're outputting a json, doesn't report any error code. Also adds lute lint to GC --- .github/workflows/ci.yml | 24 +++++++++++++-- batteries/toml.luau | 1 + examples/badisnan.luau | 1 + lute/cli/commands/lint/init.luau | 6 +++- tests/cli/lint.test.luau | 53 ++++++++++++++++++++------------ 5 files changed, 62 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55227d542..743e677e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,15 +145,35 @@ jobs: with: path: docs/.vitepress/dist/ + lute-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup and Build Lute + id: build_lute + uses: ./.github/actions/setup-and-build + with: + target: Lute.CLI + config: debug + options: ${{ matrix.options }} + token: ${{ secrets.GITHUB_TOKEN }} + use-bootstrap: true + + - name: Run Lute Lint + run: ${{ steps.build_lute.outputs.exe_path }} lint -v . + aggregator: name: Gated Commits runs-on: ubuntu-latest - needs: [run-lutecli-luau-tests, run-lute-cxx-unittests, check-format, build-docs-site] + needs: [run-lutecli-luau-tests, run-lute-cxx-unittests, check-format, build-docs-site, lute-lint] if: ${{ always() }} steps: - name: Aggregator run: | - if [ "${{ needs.run-lutecli-luau-tests.result }}" == "success" ] && [ "${{ needs.run-lute-cxx-unittests.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ] && [ "${{ needs.build-docs-site.result }}" == "success" ]; then + if [ "${{ needs.run-lutecli-luau-tests.result }}" == "success" ] && [ "${{ needs.run-lute-cxx-unittests.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ] && [ "${{ needs.build-docs-site.result }}" == "success" ] && [ "${{ needs.lute-lint.result }}" == "success" ]; then echo "All jobs succeeded" else echo "One or more jobs failed" diff --git a/batteries/toml.luau b/batteries/toml.luau index b6a191adc..4971c763b 100644 --- a/batteries/toml.luau +++ b/batteries/toml.luau @@ -159,6 +159,7 @@ local function deserialize(input: string) elseif value == "-inf" then value = -math.huge elseif value == "nan" then + --lute-lint-ignore(divide_by_zero) value = 0 / 0 end diff --git a/examples/badisnan.luau b/examples/badisnan.luau index 6bdeba554..afa20dd27 100644 --- a/examples/badisnan.luau +++ b/examples/badisnan.luau @@ -1,3 +1,4 @@ return function(n) + --lute-lint-ignore(divide_by_zero) return n == 0 / 0 end diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 9d535687e..90f38c0de 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -6,6 +6,7 @@ local lsp = require("@self/lsp") local luau = require("@std/luau") local pathLib = require("@std/path") local printer = require("@self/printer") +local process = require("@std/process") local syntax = require("@std/syntax") local tableext = require("@std/tableext") local triviaUtils = require("@std/syntax/utils/trivia") @@ -264,7 +265,7 @@ local function main(...: string) local inputFiles = args:forwarded() if inputFiles == nil or #inputFiles == 0 then print("Error: No input files specified.\n\n" .. USAGE .. "\nUse --help for more information.") - return + process.exit(1) end if VERBOSE then @@ -313,11 +314,14 @@ local function main(...: string) if VERBOSE then print(`Printing violations from {#allViolations} files\n`) end + local violationsFound = false for sourcePath, violations in allViolations do if #violations > 0 then + violationsFound = true printer.printLints(violations, sourcePath) end end + process.exit(if violationsFound then 1 else 0) end end diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 7941d7581..c6e6e0643 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -45,10 +45,23 @@ test.suite("lute lint", function(suite) suite:case("lute lint no input file", function(assert) local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "Error: No input files specified.") end) + suite:case("lute lint error code 0 with no violations", function(assert) + local modulePath = path.join(tmpDir, "module") + fs.createdirectory(modulePath) + + local result = process.run({ lutePath, "lint", path.format(modulePath) }) + + assert.eq(result.exitcode, 0) + + assert.eq(result.stdout, "") + + fs.removedirectory(modulePath) + end) + suite:case("lute lint non verbose", function(assert) -- Setup -- Create a file that violates the rule @@ -56,7 +69,7 @@ test.suite("lute lint", function(suite) fs.writestringtofile(violatorPath, "") -- Do - -- Run the transformer on the transformee + -- Run the linter local result = process.run({ lutePath, "lint", violatorPath }) -- Check @@ -107,7 +120,7 @@ local x = 1 / 0 local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") local expected = [[ @@ -154,7 +167,7 @@ end local result = process.run({ lutePath, "lint", "-r", almostSwappedExample, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) -- We expect 4 warnings, so stdout should be split into 5 parts assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.", nil, 4) local expected = [[ @@ -235,7 +248,7 @@ b = a local result = process.run({ lutePath, "lint", "-r", rulesDir, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") local expected = [[ @@ -296,7 +309,7 @@ b = a local result = process.run({ lutePath, "lint", "-r", rulesDir, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") local expected = [[ @@ -358,7 +371,7 @@ y = x process.run({ lutePath, "lint", "-r", "examples/lints", "-v", violatorPath1, path.format(modulePath) }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.eq(#result.stdout:split("warning[divide_by_zero]: Division by zero detected."), 3) local expected = [[ @@ -429,7 +442,7 @@ b = a local result = process.run({ lutePath, "lint", "-r", "examples/lints", lintee, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) local expected = [[ lintee.luau': @std/syntax/parser.luau:12: parsing failed: @@ -463,7 +476,7 @@ b = a local result = process.run({ lutePath, "lint", violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") local expected = [[ @@ -514,7 +527,7 @@ b = a local result = process.run({ lutePath, "lint", violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strnotcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") @@ -556,7 +569,7 @@ local z = 1 / 0 local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) local expected = [[ @@ -604,7 +617,7 @@ local z = 1 / 0 local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) local expected = [[ @@ -650,7 +663,7 @@ end local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) local expected = [[ @@ -687,7 +700,7 @@ end local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) local expected = [[ @@ -724,7 +737,7 @@ end local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) local expected = [[ @@ -762,7 +775,7 @@ end local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) local expected = [[ @@ -822,7 +835,7 @@ a = b -- Run the transformer on the transformee local result = process.run({ lutePath, "lint", violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.", nil, 1) @@ -945,7 +958,7 @@ local y = 10 / 0 local result = process.run({ lutePath, "lint", path.format(modulePath) }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) local expected = [[ @@ -991,7 +1004,7 @@ end local result = process.run({ lutePath, "lint", violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) @@ -1039,7 +1052,7 @@ end local result = process.run({ lutePath, "lint", violatorPath }) -- Check - assert.eq(result.exitcode, 0) + assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) From be77b2e76c226e33f147bb3ef8edd4fd9d95c6fe Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:22:39 -0800 Subject: [PATCH 252/642] Fixes JSON string deserialization (#686) There were some bug reports from @Vighnesh-V and @wmccrthy about deserialized objects returning `nil` when correctly accessed as a table, and that led to us discovering there were issues with the `deserializationString` logic I wrote in https://github.com/luau-lang/lute/pull/674 even though the test cases were passing. I essentially rewrote the logic to be more similar to the original implementation of it and just added invalid case checks to make it RFC compliant, so this new version now works with actual use cases (see the test case) --- lute/std/libs/json.luau | 140 +++++++++++++++++++-------------------- tests/std/json.test.luau | 31 +++++++-- 2 files changed, 95 insertions(+), 76 deletions(-) diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 6b476f8ef..14502f5e9 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -278,6 +278,17 @@ local function deserializeNumber(state: DeserializerState) return num end +local function decodeSurrogatePair(high, low): string? + local highVal = tonumber(high, 16) + local lowVal = tonumber(low, 16) + if not highVal or not lowVal then + return nil -- Invalid + end + -- Calculate the actual Unicode codepoint + local codepoint = 0x10000 + ((highVal - 0xD800) * 0x400) + (lowVal - 0xDC00) + return utf8.char(codepoint) +end + local function deserializeString(state: DeserializerState): string -- Must start with " if currentByte(state) ~= string.byte('"') then @@ -289,89 +300,72 @@ local function deserializeString(state: DeserializerState): string local startPos = state.cursor - local buf = buffer.create(bufferSize) - local bufferPos = 0 + while state.cursor <= #state.src do + local byte = currentByte(state) - local function writeByteToBuffer(byte: number, dbuf: number, dstart: number) - buffer.writeu8(buf, bufferPos, byte) - bufferPos += dbuf - startPos += dstart - end + -- Check for unescaped control characters (0x00-0x1F) + if byte < 0x20 then + return deserializerError(state, "Unescaped control character in string") + end - while startPos <= #state.src do - local c = string.sub(state.src, startPos, startPos) + if byte == string.byte('"') then + state.cursor += 1 + local source = string.sub(state.src, startPos, state.cursor - 2) - -- End of string - if c == '"' then - state.cursor = startPos + 1 - return buffer.tostring(buf, bufferPos) - end + -- Validate escape sequences + -- Remove valid \\ first + local temp = string.gsub(source, "\\\\", "") - -- Normal character - if c ~= "\\" then - local byte = string.byte(c) + -- Check for invalid single-char escapes + if temp:match('\\[^bfnrt"\\/u]') then + return deserializerError(state, "Invalid escape sequence") + end - -- No raw control chars allowed - if byte < string.byte(" ") then - return deserializerError(state, "Unescaped control character in string") + -- Check that all \u escapes have exactly 4 hex digits + if temp:match("\\u[^0-9a-fA-F]") or temp:match("\\u%x?%x?%x?$") then + return deserializerError(state, "Incomplete or invalid \\u escape") end - writeByteToBuffer(byte, 1, 1) - else - -- Escape sequence - local source = string.sub(state.src, startPos + 1, startPos + 1) - - if source == "b" then - writeByteToBuffer(string.byte("\b"), 1, 2) - elseif source == "f" then - writeByteToBuffer(string.byte("\f"), 1, 2) - elseif source == "n" then - writeByteToBuffer(string.byte("\n"), 1, 2) - elseif source == "r" then - writeByteToBuffer(string.byte("\r"), 1, 2) - elseif source == "t" then - writeByteToBuffer(string.byte("\t"), 1, 2) - elseif source == '"' then - writeByteToBuffer(string.byte('"'), 1, 2) - elseif source == "\\" then - writeByteToBuffer(string.byte("\\"), 1, 2) - elseif source == "/" then - writeByteToBuffer(string.byte("/"), 1, 2) - elseif source == "u" then - -- Unicode escape: \uXXXX - local hex = string.sub(state.src, startPos + 2, startPos + 5) - if not hex:match("^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$") then - return deserializerError(state, "Invalid \\u escape") - end + -- Handle all surrogate pairs + source = string.gsub( + source, + "\\u([dD][89aAbB][0-9a-fA-F][0-9a-fA-F])\\u([dD][c-fC-F][0-9a-fA-F][0-9a-fA-F])", + function(high, low) + return decodeSurrogatePair(high, low) or deserializerError(state, "Invalid unicode surrogate pair") + end :: any + ) + + -- Detect incomplete/invalid surrogates + if source:match("\\u[dD][89aAbB][0-9a-fA-F][0-9a-fA-F]") then + return deserializerError(state, "Incomplete surrogate pair") + end - local codepoint = tonumber(hex, 16) - if not codepoint then - return deserializerError(state, "Invalid \\u escape") - end - startPos += 6 -- skip \uXXXX - - -- Surrogate pair - if codepoint >= 0xD800 and codepoint <= 0xDBFF then - -- Look for \uDCxx following - local lo = state.src:match("^\\u([dD][cdefCDEF][0-9a-fA-F][0-9a-fA-F])", startPos) - if lo then - local lo_val = tonumber(lo, 16) - -- Combine surrogate halves - codepoint = 0x10000 + (codepoint :: number - 0xD800) * 0x400 + (lo_val :: number - 0xDC00) - startPos += 6 - end - end + -- Handle regular Unicode escapes + source = string.gsub(source, "\\u(%x%x%x%x)", function(code) + return utf8.char(tonumber(code, 16) :: number) + end) + + source = string.gsub(source, "\\\\", "\0") + source = string.gsub(source, "\\b", "\b") + source = string.gsub(source, "\\f", "\f") + source = string.gsub(source, "\\n", "\n") + source = string.gsub(source, "\\r", "\r") + source = string.gsub(source, "\\t", "\t") + source = string.gsub(source, '\\"', '"') + source = string.gsub(source, "\\/", "/") + source = string.gsub(source, "\0", "\\") + + return source + end - local utf8_bytes = utf8.char(codepoint) - buffer.writestring(buf, bufferPos, utf8_bytes) - bufferPos += #utf8_bytes - else - -- Invalid escape: \" already covered - return deserializerError(state, "Invalid escape sequence '\\" .. source .. "'") - end + if byte == string.byte("\\") then + state.cursor += 1 end + + state.cursor += 1 end + state.cursor = startPos return deserializerError(state, "Unterminated string") end @@ -518,7 +512,9 @@ deserialize = function(state: DeserializerState): value elseif string.match(state.src, "^%[", state.cursor) then return deserializeArray(state) elseif string.match(state.src, "^{", state.cursor) then - return deserializeObject(state) + local obj = deserializeObject(state) + obj[object_key] = true + return obj end return deserializerError(state, `Unexpected token '{string.sub(state.src, state.cursor, state.cursor)}'`) diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau index c3a34bc37..4c950dad0 100644 --- a/tests/std/json.test.luau +++ b/tests/std/json.test.luau @@ -55,11 +55,34 @@ test.suite("JSONParsingTestSuite", function(suite) assert.eq(type(obj), "table") assert.eq(obj.name, "Alice") assert.eq(obj.age, 30) + end) + + suite:case("json_asobject_test", function(assert) + local myjsonstr = [[ + { + "a": true, + "b": { + "c": true, + "d": [1, 2, 3] + } + } + ]] + + local maybeJson = json.deserialize(myjsonstr) + local obj = json.asobject(maybeJson) - local asobj = json.asobject(obj) - assert.eq(type(asobj), "table") - assert.eq(asobj.name, "Alice") - assert.eq(asobj.age, 30) + if obj then + assert.eq(type(obj), "table") + assert.eq(obj.a, true) + assert.eq(type(obj.b), "table") + if json.asobject(obj.b) then + assert.eq(obj.b.c, true) + assert.eq(type(obj.b.d), "table") + for i = 1, #obj.b.d do + assert.eq(type(obj.b.d[i]), "number") + end + end + end end) end) From 29551114d05c46d067669b24bb57fec47d8bcb64 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:52:49 -0800 Subject: [PATCH 253/642] Enable UBSan in CI (#441) --- .github/workflows/ci.yml | 16 ++++++++-------- CMakeLists.txt | 14 ++++++++++---- tools/luthier.luau | 6 +++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 743e677e7..fb82941df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,16 +23,16 @@ jobs: - os: windows-latest options: --c-compiler cl.exe --cxx-compiler cl.exe - os: ubuntu-latest - options: --c-compiler gcc --cxx-compiler g++ --enable-asan + options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers - os: ubuntu-latest - options: --c-compiler clang --cxx-compiler clang++ --enable-asan + options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers - os: macos-latest - options: --enable-asan + options: --enable-sanitizers - os: ubuntu-latest - options: --c-compiler gcc --cxx-compiler g++ --enable-asan + options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers use-bootstrap: true - os: ubuntu-latest - options: --c-compiler clang --cxx-compiler clang++ --enable-asan + options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers use-bootstrap: true fail-fast: false @@ -75,11 +75,11 @@ jobs: - os: windows-latest options: --c-compiler cl.exe --cxx-compiler cl.exe - os: ubuntu-latest - options: --c-compiler gcc --cxx-compiler g++ --enable-asan + options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers - os: ubuntu-latest - options: --c-compiler clang --cxx-compiler clang++ --enable-asan + options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers - os: macos-latest - options: --enable-asan + options: --enable-sanitizers fail-fast: false steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index 1dd383a07..b4bf119be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,11 +16,12 @@ project( LANGUAGES CXX C ) -option(LUTE_ENABLE_ASAN "Enable AddressSanitizer" OFF) -if (LUTE_ENABLE_ASAN) +# Enable sanitizers globally before any subdirectory is added +option(LUTE_ENABLE_SANITIZERS "Enable sanitizers" OFF) +if (LUTE_ENABLE_SANITIZERS) if(NOT MSVC) - add_compile_options(-fsanitize=address -fno-omit-frame-pointer) - add_link_options(-fsanitize=address) + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) endif() endif() @@ -67,6 +68,11 @@ add_subdirectory(extern/boringssl) set(BORINGSSL_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/extern/boringssl") set(BORINGSSL_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/extern/boringssl/include) +# Suppress frame-size warnings in boringssl when sanitizers are enabled +if (LUTE_ENABLE_SANITIZERS AND NOT MSVC) + target_compile_options(crypto PRIVATE -Wno-frame-larger-than) +endif() + # curl setup set(USE_LIBIDN2 OFF) diff --git a/tools/luthier.luau b/tools/luthier.luau index fa0ece8f6..ba51fccea 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -79,7 +79,7 @@ args:add("clean", "flag", { help = "perform a clean build" }) args:add("which", "flag", { help = "print out the path to the compiled binary and exit", aliases = { "w" } }) args:add("cxx-compiler", "option", { help = "C++ compiler to use" }) args:add("c-compiler", "option", { help = "C compiler to use" }) -args:add("enable-asan", "flag", { help = "enable AddressSanitizer" }) +args:add("enable-sanitizers", "flag", { help = "enable sanitizers during configuration" }) if not isWindows and not isMac and not isLinux then error("Unknown platform " .. os) @@ -599,8 +599,8 @@ local function getConfigureArguments() table.insert(configArgs, "-DCMAKE_C_COMPILER=" .. args:get("c-compiler")) end - if args:has("enable-asan") then - table.insert(configArgs, "-DLUTE_ENABLE_ASAN=ON") + if args:has("enable-sanitizers") then + table.insert(configArgs, "-DLUTE_ENABLE_SANITIZERS=ON") end return configArgs From dc79710dfaf5033e8867817cafcf2fc2f1c8da7e Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:35:37 -0500 Subject: [PATCH 254/642] Option for print line numbers in prettydiff (#659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Printing line numbers in `prettydiff` provides a lot of utility/clarity in properly visualizing differences b/w files. It also helps distinguish the diff output from other arbitrary text in the command line. Here is an example on a 100 line diff, to showcase the alignment of printed side-headers: Screenshot 2025-12-08 at 3 22 31 PM --- batteries/difftext/myersdiff.luau | 12 ++- batteries/difftext/printdiff.luau | 160 ++++++++++++++++++++++++----- batteries/difftext/types.luau | 4 +- examples/difftext.luau | 11 ++ tests/batteries/difftext.test.luau | 101 ++++++++++++++---- 5 files changed, 239 insertions(+), 49 deletions(-) diff --git a/batteries/difftext/myersdiff.luau b/batteries/difftext/myersdiff.luau index 4ca0411bb..791a6bce6 100644 --- a/batteries/difftext/myersdiff.luau +++ b/batteries/difftext/myersdiff.luau @@ -121,8 +121,16 @@ local function myersdiff(a: string | { string }, b: string | { string }): diff return diff end -local function myersdiffbyline(a: string, b: string): diff - return myersdiff(a:split("\n"), b:split("\n")) +local function myersdiffbyline(a: string | { string }, b: string | { string }): diff + local src = a + local dest = b + if typeof(a) == "string" then + src = a:split("\n") + end + if typeof(b) == "string" then + dest = b:split("\n") + end + return myersdiff(src, dest) end local function myersdiffbychar(a: string, b: string): diff diff --git a/batteries/difftext/printdiff.luau b/batteries/difftext/printdiff.luau index b8a833593..88d7b109a 100644 --- a/batteries/difftext/printdiff.luau +++ b/batteries/difftext/printdiff.luau @@ -3,6 +3,10 @@ local myersdiff = require("./myersdiff") local myersdiffbychar = myersdiff.myersdiffbychar local myersdiffbyline = myersdiff.myersdiffbyline local types = require("./types") +local brightGreen = richterm.brightGreen +local brightRed = richterm.brightRed +local bgGreen = richterm.bgGreen +local bgRed = richterm.bgRed type diff = types.diff @@ -33,22 +37,61 @@ local function visualizeCharDiff(a: string, b: string): (string, string) for _, op in charOps do if op.key == "EQUAL" then - table.insert(srcVisual, richterm.brightRed(op.text)) - table.insert(destVisual, richterm.brightGreen(op.text)) + table.insert(srcVisual, brightRed(op.text)) + table.insert(destVisual, brightGreen(op.text)) elseif op.key == "DELETE" then - table.insert(srcVisual, richterm.bgRed(op.text)) + table.insert(srcVisual, bgRed(op.text)) elseif op.key == "ADD" then - table.insert(destVisual, richterm.bgGreen(op.text)) + table.insert(destVisual, bgGreen(op.text)) end end return table.concat(srcVisual, ""), table.concat(destVisual, "") end -local function printDiffByLineDetailed(a: string, b: string): string - local diff = myersdiffbyline(a, b) +local function formatLineSideHeader( + operation: types.diffoperationkey, + maxNumWidth: number, + lineNumbers: { + oldLine: number?, + newLine: number?, + } +): string + -- side header structure: + -- if line numbers included: + -- ALWAYS has length maxNumWidth*2 + 4 + -- the padded structure is essentialy: + -- {operation} {oldLine# padded to len of maxNumWidth} {newLine# padded to len = maxNumWidth}| + + local oldStr = tostring(if lineNumbers then lineNumbers.oldLine else "") + local newStr = tostring(if lineNumbers then lineNumbers.newLine else "") + maxNumWidth = maxNumWidth or 1 + if operation == "EQUAL" then + return string.format(` %{maxNumWidth}s %{maxNumWidth}s| `, oldStr, newStr) + elseif operation == "ADD" then + return string.format(`+ %{maxNumWidth}s %{maxNumWidth}s| `, "", newStr) + elseif operation == "DELETE" then + return string.format(`- %{maxNumWidth}s %{maxNumWidth}s| `, oldStr, "") + end + + error(`formatLineSideHeader called with invalid diff operation key: {operation}`) +end + +local function printDiffByLineDetailed(a: string, b: string, includeLineNumbers: boolean?): string + local src, dest = a:split("\n"), b:split("\n") + local maxNumLines = math.max(#src, #dest) + local maxLineNumberLen = math.floor(math.log10(maxNumLines)) + 1 + local diff = myersdiffbyline(src, dest) + local i = 1 local result: { string } = {} + local oldLine, newLine = 1, 1 + + -- Helper to format line numbers + local function lineNums(old: number?, new: number?) + return { oldLine = old, newLine = new } + end + while i <= #diff do local op = diff[i] @@ -58,18 +101,54 @@ local function printDiffByLineDetailed(a: string, b: string): string local pair = #deletes == 1 and #adds == 1 if pair then local srcLine, destLine = visualizeCharDiff(deletes[1], adds[1]) - table.insert(result, richterm.brightRed("- ") .. srcLine) - table.insert(result, richterm.brightGreen("+ ") .. destLine) + if includeLineNumbers then + table.insert( + result, + brightRed(formatLineSideHeader("DELETE", maxLineNumberLen, lineNums(oldLine, nil))) .. srcLine + ) + table.insert( + result, + brightGreen(formatLineSideHeader("ADD", maxLineNumberLen, lineNums(nil, newLine))) .. destLine + ) + oldLine, newLine = oldLine + 1, newLine + 1 + else + table.insert(result, brightRed("- ") .. srcLine) + table.insert(result, brightGreen("+ ") .. destLine) + end else for j, deleted in deletes do - table.insert(result, richterm.brightRed(`- {deleted}`)) + if includeLineNumbers then + table.insert( + result, + brightRed( + formatLineSideHeader("DELETE", maxLineNumberLen, lineNums(oldLine, nil)) .. deleted + ) + ) + oldLine += 1 + else + table.insert(result, brightRed("- " .. deleted)) + end end for j, added in adds do - table.insert(result, richterm.brightGreen(`+ {added}`)) + if includeLineNumbers then + table.insert( + result, + brightGreen(formatLineSideHeader("ADD", maxLineNumberLen, lineNums(nil, newLine)) .. added) + ) + newLine += 1 + else + table.insert(result, brightGreen("+ " .. added)) + end end end else -- EQUAL - table.insert(result, op.text) + table.insert( + result, + if includeLineNumbers + then formatLineSideHeader("EQUAL", maxLineNumberLen, lineNums(oldLine, newLine)) .. op.text + else " " .. op.text + ) + oldLine, newLine = oldLine + 1, newLine + 1 i += 1 end end @@ -81,24 +160,57 @@ local function prettydiff( b: string, options: { detailed: boolean?, + includeLineNumbers: boolean?, }? ): string - if options then - if options.detailed then - return printDiffByLineDetailed(a, b) - end + if options and options.detailed then + return printDiffByLineDetailed(a, b, options.includeLineNumbers) end local result = {} :: { string } - local diff = myersdiffbyline(a, b) - for i, op in diff do - table.insert( - result, - if op.key == "ADD" - then richterm.brightGreen(`+ {op.text}`) - elseif op.key == "DELETE" then richterm.brightRed(`- {op.text}`) - else `{op.text}` - ) + local src, dest = a:split("\n"), b:split("\n") + local diff = myersdiffbyline(src, dest) + if options and options.includeLineNumbers then + -- Helper to format line number tables so we don't get too verbose + local function lineNums(old: number?, new: number?) + return { oldLine = old, newLine = new } + end + + local maxNumLines = math.max(#src, #dest) + local maxLineNumberLen = math.floor(math.log10(maxNumLines)) + 1 + local oldLine, newLine = 1, 1 + + for i, op in diff do + if op.key == "ADD" then + table.insert( + result, + brightGreen(formatLineSideHeader(op.key, maxLineNumberLen, lineNums(nil, newLine)) .. op.text) + ) + newLine += 1 + elseif op.key == "DELETE" then + table.insert( + result, + brightRed(formatLineSideHeader(op.key, maxLineNumberLen, lineNums(oldLine, nil)) .. op.text) + ) + oldLine += 1 + else -- EQUAL + table.insert( + result, + formatLineSideHeader(op.key, maxLineNumberLen, lineNums(oldLine, newLine)) .. op.text + ) + oldLine, newLine = oldLine + 1, newLine + 1 + end + end + else + for i, op in diff do + if op.key == "ADD" then + table.insert(result, brightGreen("+ " .. op.text)) + elseif op.key == "DELETE" then + table.insert(result, brightRed("- " .. op.text)) + else -- EQUAL + table.insert(result, " " .. op.text) + end + end end return table.concat(result, "\n") end diff --git a/batteries/difftext/types.luau b/batteries/difftext/types.luau index ce2e57abb..19be452e2 100644 --- a/batteries/difftext/types.luau +++ b/batteries/difftext/types.luau @@ -1,5 +1,7 @@ +export type diffoperationkey = "EQUAL" | "ADD" | "DELETE" + export type diffoperation = { - key: "EQUAL" | "ADD" | "DELETE", + key: diffoperationkey, text: string, } diff --git a/examples/difftext.luau b/examples/difftext.luau index 2aee1dc30..ac51b6a4f 100644 --- a/examples/difftext.luau +++ b/examples/difftext.luau @@ -26,7 +26,18 @@ doSomething() print(richterm.bold("Diff")) print(difftext.prettydiff(src, destination)) +print(richterm.bold("Diff (w/ line numbers)")) +print(difftext.prettydiff(src, destination, { + includeLineNumbers = true, +})) +print() print(richterm.bold("Diff (Detailed)")) print(difftext.prettydiff(src, destination, { detailed = true, })) +print() +print(richterm.bold("Diff (Detailed w/ Line Numbers)")) +print(difftext.prettydiff(src, destination, { + detailed = true, + includeLineNumbers = true, +})) diff --git a/tests/batteries/difftext.test.luau b/tests/batteries/difftext.test.luau index b4025e65d..ca13bdc46 100644 --- a/tests/batteries/difftext.test.luau +++ b/tests/batteries/difftext.test.luau @@ -302,9 +302,9 @@ test.suite("myersdiff - byline", function(suite) end) end) -test.suite("printdiff", function(suite: any) - local function composeDetailedRemoval(str: string, changedCharPos: { [number]: true }) - local composed = richterm.brightRed("- ") +test.suite("printdiff", function(suite) + local function composeDetailedRemoval(str: string, changedCharPos: { [number]: true }, sideHeader: string?) + local composed = richterm.brightRed(`- {sideHeader or ""}`) for i = 1, #str do if changedCharPos[i] then composed ..= richterm.bgRed(str:sub(i, i)) @@ -315,8 +315,8 @@ test.suite("printdiff", function(suite: any) return composed end - local function composeDetailedAddition(str: string, changedCharPos: { [number]: true }) - local composed = richterm.brightGreen("+ ") + local function composeDetailedAddition(str: string, changedCharPos: { [number]: true }, sideHeader: string?) + local composed = richterm.brightGreen(`+ {sideHeader or ""}`) for i = 1, #str do if changedCharPos[i] then composed ..= richterm.bgGreen(str:sub(i, i)) @@ -334,8 +334,8 @@ test.suite("printdiff", function(suite: any) local lines = result:split("\n") assert.eq(#lines, 3) - assert.eq(lines[1], "line1") - assert.eq(lines[2], "line2") + assert.eq(lines[1], " line1") + assert.eq(lines[2], " line2") assert.eq(lines[3], richterm.brightGreen("+ line3")) end) @@ -346,9 +346,9 @@ test.suite("printdiff", function(suite: any) local lines = result:split("\n") assert.eq(#lines, 3) - assert.eq(lines[1], "line1") + assert.eq(lines[1], " line1") assert.eq(lines[2], richterm.brightRed("- line2")) - assert.eq(lines[3], "line3") + assert.eq(lines[3], " line3") end) suite:case("printdiff default - modification", function(assert) @@ -358,10 +358,10 @@ test.suite("printdiff", function(suite: any) local lines = result:split("\n") assert.eq(#lines, 4) - assert.eq(lines[1], "line1") + assert.eq(lines[1], " line1") assert.eq(lines[2], richterm.brightRed("- line2")) assert.eq(lines[3], richterm.brightGreen("+ modified")) - assert.eq(lines[4], "line3") + assert.eq(lines[4], " line3") end) suite:case("printdiff default - unchanged lines", function(assert) @@ -371,9 +371,9 @@ test.suite("printdiff", function(suite: any) local lines = result:split("\n") assert.eq(#lines, 3) - assert.eq(lines[1], "line1") - assert.eq(lines[2], "line2") - assert.eq(lines[3], "line3") + assert.eq(lines[1], " line1") + assert.eq(lines[2], " line2") + assert.eq(lines[3], " line3") end) suite:case("printdiff default - multiple changes", function(assert) @@ -383,12 +383,30 @@ test.suite("printdiff", function(suite: any) local lines = result:split("\n") assert.eq(#lines, 6) - assert.eq(lines[1], "A") + assert.eq(lines[1], " A") assert.eq(lines[2], richterm.brightRed("- B")) assert.eq(lines[3], richterm.brightGreen("+ X")) - assert.eq(lines[4], "C") + assert.eq(lines[4], " C") assert.eq(lines[5], richterm.brightGreen("+ Y")) - assert.eq(lines[6], "D") + assert.eq(lines[6], " D") + end) + + suite:case("printdiff default - with line numbers", function(assert) + local a = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" + local b = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" + local result = difftext.prettydiff(a, b, { + includeLineNumbers = true, + }) + local lines = result:split("\n") + + assert.eq(#lines, 10) + for i, line in lines do + if i < 10 then + assert.eq(line, ` {i} {i}| line{i}`) + else + assert.eq(line, ` {i} {i}| line{i}`) + end + end end) suite:case("printdiff detailed - single line change", function(assert) @@ -399,7 +417,11 @@ test.suite("printdiff", function(suite: any) -- Detailed mode shows both old and new lines with character-level highlighting assert.eq(#lines, 2) - assert.eq(lines[1], composeDetailedRemoval("hello world", { [8] = true })) + assert.eq( + lines[1], + composeDetailedRemoval("hello world", { [8] = true }), + `Not equal:\n{difftext.prettydiff(lines[1], composeDetailedRemoval("hello world", { [8] = true }))}` + ) assert.eq(lines[2], composeDetailedAddition("hello wirld", { [8] = true })) end) @@ -417,10 +439,10 @@ test.suite("printdiff", function(suite: any) -- Should have: line1, - old line, + new line, line3 assert.eq(#lines, 4) - assert.eq(lines[1], "line1") + assert.eq(lines[1], " line1") assert.eq(lines[2], composeDetailedRemoval("old line", changedChars)) assert.eq(lines[3], composeDetailedAddition("new line", changedChars)) - assert.eq(lines[4], "line3") + assert.eq(lines[4], " line3") end) suite:case("printdiff detailed - unchanged lines", function(assert) @@ -430,8 +452,43 @@ test.suite("printdiff", function(suite: any) local lines = result:split("\n") assert.eq(#lines, 2) - assert.eq(lines[1], "line1") - assert.eq(lines[2], "line2") + assert.eq(lines[1], " line1") + assert.eq(lines[2], " line2") + end) + + suite:case("printdiff detailed - with line numbers", function(assert) + local a = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" + local b = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nlineten" + local result = difftext.prettydiff(a, b, { + detailed = true, + includeLineNumbers = true, + }) + local lines = result:split("\n") + + assert.eq(#lines, 11) + for i = 1, 9 do + local line = lines[i] + assert.eq(line, ` {i} {i}| line{i}`) + end + assert.eq(lines[10], composeDetailedRemoval("line10", { [5] = true, [6] = true }, "10 | ")) + assert.eq(lines[11], composeDetailedAddition("lineten", { [5] = true, [6] = true, [7] = true }, " 10| ")) + end) + + suite:case("printdiff detailed - sanity check w/ 3 digits of line numbers", function(assert) + local a = "unmodified\n" .. string.rep("line\n", 99) .. "unmodified" + local b = "modified\n" .. string.rep("line\n", 99) .. "modified" + local result = difftext.prettydiff(a, b, { + detailed = true, + includeLineNumbers = true, + }) + local lines = result:split("\n") + + assert.eq(#lines, 103) + assert.eq(lines[1], composeDetailedRemoval("unmodified", { [1] = true, [2] = true }, " 1 | ")) + assert.eq(lines[2], composeDetailedAddition("modified", {}, " 1| ")) + assert.eq(lines[101], " 100 100| line") + assert.eq(lines[102], composeDetailedRemoval("unmodified", { [1] = true, [2] = true }, "101 | ")) + assert.eq(lines[103], composeDetailedAddition("modified", {}, " 101| ")) end) end) From cde4c7cce7d152ca603dd8b6049a724ba2c592d3 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:03:47 -0800 Subject: [PATCH 255/642] Remove `net.serve` API from `@std` (#693) We're only going to expose this API under `@lute/net`. --- examples/serve_html.luau | 4 ++-- lute/std/libs/net.luau | 10 ---------- tests/std/net.test.luau | 4 ++-- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/examples/serve_html.luau b/examples/serve_html.luau index 127a26d80..94d67837c 100644 --- a/examples/serve_html.luau +++ b/examples/serve_html.luau @@ -1,8 +1,8 @@ -local net = require("@std/net") +local net = require("@lute/net") local server = net.serve({ port = 8080, - handler = function(req: net.receivedrequest): net.serverresponse + handler = function(req: net.ReceivedRequest): net.ServerResponse local headers = "" for key, value in req.headers do headers ..= `\n {key}: {value}` diff --git a/lute/std/libs/net.luau b/lute/std/libs/net.luau index 7c5ddbc78..0f1ea8b34 100644 --- a/lute/std/libs/net.luau +++ b/lute/std/libs/net.luau @@ -13,14 +13,4 @@ function netlib.request(url: string, metadata: metadata?): response return net.request(url, metadata) end -export type receivedrequest = net.ReceivedRequest -export type serverresponse = net.ServerResponse -export type handler = net.Handler -export type configuration = net.Configuration -export type server = net.Server - -function netlib.serve(config: handler | configuration): server - return net.serve(config) -end - return table.freeze(netlib) diff --git a/tests/std/net.test.luau b/tests/std/net.test.luau index d62b9932e..dc8caec95 100644 --- a/tests/std/net.test.luau +++ b/tests/std/net.test.luau @@ -1,10 +1,10 @@ -local net = require("@std/net") +local net = require("@lute/net") local test = require("@std/test") test.suite("NetTestSuite", function(suite) suite:case("serve_locally_and_request", function(assert) local server = net.serve({ - handler = function(req: net.receivedrequest): net.serverresponse + handler = function(req: net.ReceivedRequest): net.ServerResponse return { status = 200, body = "Hello, World!", From 68dd5bd81eed4894abd9c0f0a81c8a1c38e1e16b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:52:47 -0800 Subject: [PATCH 256/642] Update Luau to 0.703 (#687) **Luau**: Updated from `0.702` to `0.703` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.703 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- extern/luau.tune | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 05fe1c586..959501695 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.702" -revision = "c836feb2450a074581010f84da2eadeea38a3d55" +branch = "0.703" +revision = "c33adf13ed76302c293c442ab13fc84874c53ea5" From 6a8e5f0de40439b94c9bbff7dbcf994bc15b6770 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:07:42 -0800 Subject: [PATCH 257/642] Make `@std` and `@lute` aliases nonoverridable (#694) We use the new `to_alias_override` Luau.Require callback to ensure that the `@std` and `@lute` aliases always point at the embedded libraries, not ones on disk that the user may have set up using `.luaurc` or `.config.luau` files. @Vighnesh-V, this means that `@std/test` will always point at the embedded module now. --- lute/require/include/lute/packagerequirevfs.h | 1 + lute/require/include/lute/require.h | 1 + lute/require/include/lute/requirevfs.h | 1 + lute/require/src/packagerequirevfs.cpp | 15 ++++++++++++--- lute/require/src/require.cpp | 7 +++++++ lute/require/src/requirevfs.cpp | 7 ++++++- 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lute/require/include/lute/packagerequirevfs.h b/lute/require/include/lute/packagerequirevfs.h index 959ce8e24..84f29f396 100644 --- a/lute/require/include/lute/packagerequirevfs.h +++ b/lute/require/include/lute/packagerequirevfs.h @@ -16,6 +16,7 @@ class RequireVfs : public IRequireVfs NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) override; NavigationStatus jumpToAlias(lua_State* L, std::string_view path) override; + NavigationStatus toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) override; NavigationStatus toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) override; NavigationStatus toParent(lua_State* L) override; diff --git a/lute/require/include/lute/require.h b/lute/require/include/lute/require.h index 755041969..911da889f 100644 --- a/lute/require/include/lute/require.h +++ b/lute/require/include/lute/require.h @@ -18,6 +18,7 @@ class IRequireVfs virtual NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) = 0; virtual NavigationStatus jumpToAlias(lua_State* L, std::string_view path) = 0; + virtual NavigationStatus toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) = 0; virtual NavigationStatus toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) = 0; virtual NavigationStatus toParent(lua_State* L) = 0; diff --git a/lute/require/include/lute/requirevfs.h b/lute/require/include/lute/requirevfs.h index 88afd763e..f3cf77f9b 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -23,6 +23,7 @@ class RequireVfs : public IRequireVfs NavigationStatus reset(lua_State* L, std::string_view requirerChunkname) override; NavigationStatus jumpToAlias(lua_State* L, std::string_view path) override; + NavigationStatus toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) override; NavigationStatus toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) override; NavigationStatus toParent(lua_State* L) override; diff --git a/lute/require/src/packagerequirevfs.cpp b/lute/require/src/packagerequirevfs.cpp index 49dbebd78..6430cd27a 100644 --- a/lute/require/src/packagerequirevfs.cpp +++ b/lute/require/src/packagerequirevfs.cpp @@ -1,5 +1,7 @@ #include "lute/packagerequirevfs.h" +#include "lute/modulepath.h" + #include "Luau/FileUtils.h" #include "lua.h" @@ -62,7 +64,7 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) return status; } -NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) +NavigationStatus RequireVfs::toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) { if (aliasUnprefixed == "std") { @@ -76,8 +78,15 @@ NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view alia return NavigationStatus::Success; } - vfsType = VFSType::Userland; - return userlandVfs.toAliasFallback(aliasUnprefixed); + return NavigationStatus::NotFound; +} + +NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) +{ + NavigationStatus status = userlandVfs.toAliasFallback(aliasUnprefixed); + if (status == NavigationStatus::Success) + vfsType = VFSType::Userland; + return status; } NavigationStatus RequireVfs::toParent(lua_State* L) diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index 40267c14a..617107c9d 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -87,6 +87,12 @@ static luarequire_NavigateResult jump_to_alias(lua_State* L, void* ctx, const ch return convert(reqCtx->vfs->jumpToAlias(L, path)); } +static luarequire_NavigateResult to_alias_override(lua_State* L, void* ctx, const char* alias_unprefixed) +{ + RequireCtx* reqCtx = static_cast(ctx); + return convert(reqCtx->vfs->toAliasOverride(L, alias_unprefixed)); +} + static luarequire_NavigateResult to_alias_fallback(lua_State* L, void* ctx, const char* alias_unprefixed) { RequireCtx* reqCtx = static_cast(ctx); @@ -211,6 +217,7 @@ void requireConfigInit(luarequire_Configuration* config) config->is_require_allowed = is_require_allowed; config->reset = reset; config->jump_to_alias = jump_to_alias; + config->to_alias_override = to_alias_override; config->to_alias_fallback = to_alias_fallback; config->to_parent = to_parent; config->to_child = to_child; diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 16ffdccfd..e3d4e3bac 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -85,7 +85,7 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) return status; } -NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) +NavigationStatus RequireVfs::toAliasOverride(lua_State* L, std::string_view aliasUnprefixed) { if (aliasUnprefixed == "std") { @@ -102,6 +102,11 @@ NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view alia return NavigationStatus::NotFound; } +NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) +{ + return NavigationStatus::NotFound; +} + NavigationStatus RequireVfs::toParent(lua_State* L) { NavigationStatus status = NavigationStatus::NotFound; From fc19ad0b29cd6cae692607995dd6743f229c61b7 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:25:19 -0800 Subject: [PATCH 258/642] Enables the `@lute` libraries in spawned VMs by implementing a new `LuteVfs` (#696) Resolves #691. As @Nicell points out in #692, we have some libuv memory safety issues that have been revealed with this change, so they'll need to be fixed outside of this PR. --- lute/cli/CMakeLists.txt | 2 +- lute/cli/src/requiresetup.cpp | 41 +------ lute/require/CMakeLists.txt | 4 +- lute/require/include/lute/lutevfs.h | 30 +++++ lute/require/include/lute/packagerequirevfs.h | 3 +- lute/require/include/lute/requirevfs.h | 3 +- lute/require/src/lutevfs.cpp | 106 ++++++++++++++++++ lute/require/src/packagerequirevfs.cpp | 17 ++- lute/require/src/require.cpp | 11 ++ lute/require/src/requirevfs.cpp | 55 ++++----- tests/lute/vm.test.luau | 70 ++++++++++++ 11 files changed, 269 insertions(+), 73 deletions(-) create mode 100644 lute/require/include/lute/lutevfs.h create mode 100644 lute/require/src/lutevfs.cpp create mode 100644 tests/lute/vm.test.luau diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index c9d870522..503dcb671 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -47,7 +47,7 @@ target_sources(Lute.CLI.lib PRIVATE target_compile_features(Lute.CLI.lib PUBLIC cxx_std_17) target_include_directories(Lute.CLI.lib PUBLIC include ${CLI_GENERATED_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR}) -target_link_libraries(Lute.CLI.lib PRIVATE Luau.Common Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Require Lute.Runtime Luau.CLI.lib zlibstatic) +target_link_libraries(Lute.CLI.lib PRIVATE Luau.Common Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Luau Lute.Process Lute.Require Lute.Runtime Luau.CLI.lib zlibstatic) target_compile_options(Lute.CLI.lib PRIVATE ${LUTE_OPTIONS}) add_executable(Lute.CLI) diff --git a/lute/cli/src/requiresetup.cpp b/lute/cli/src/requiresetup.cpp index 3440f13b9..54e59283f 100644 --- a/lute/cli/src/requiresetup.cpp +++ b/lute/cli/src/requiresetup.cpp @@ -2,53 +2,21 @@ #include "lute/bundlevfs.h" #include "lute/clivfs.h" -#include "lute/crypto.h" -#include "lute/fs.h" -#include "lute/io.h" -#include "lute/luau.h" -#include "lute/net.h" #include "lute/packagerequirevfs.h" -#include "lute/process.h" #include "lute/require.h" #include "lute/requirevfs.h" #include "lute/runtime.h" -#include "lute/system.h" -#include "lute/task.h" -#include "lute/time.h" #include "lute/userlandvfs.h" -#include "lute/vm.h" #include "Luau/CodeGen.h" #include "Luau/Require.h" +#include "lualib.h" + #include #include #include -static void luteopen_libs(lua_State* L) -{ - std::vector> libs = {{ - {"@lute/crypto", luteopen_crypto}, - {"@lute/fs", luteopen_fs}, - {"@lute/luau", luteopen_luau}, - {"@lute/net", luteopen_net}, - {"@lute/process", luteopen_process}, - {"@lute/task", luteopen_task}, - {"@lute/vm", luteopen_vm}, - {"@lute/system", luteopen_system}, - {"@lute/time", luteopen_time}, - {"@lute/io", luteopen_io}, - }}; - - for (const auto& [name, func] : libs) - { - lua_pushcfunction(L, luarequire_registermodule, nullptr); - lua_pushstring(L, name); - func(L); - lua_call(L, 2, 0); - } -} - static void* createCliRequireContext(lua_State* L) { void* ctx = lua_newuserdatadtor( @@ -138,8 +106,6 @@ lua_State* setupCliState(Runtime& runtime, std::function preSa runtime, [preSandboxInit = std::move(preSandboxInit)](lua_State* L) { - luteopen_libs(L); - if (Luau::CodeGen::isSupported()) Luau::CodeGen::create(L); @@ -160,8 +126,6 @@ lua_State* setupPkgCliState( runtime, [directDependencies = std::move(directDependencies), allDependencies = std::move(allDependencies)](lua_State* L) { - luteopen_libs(L); - if (Luau::CodeGen::isSupported()) Luau::CodeGen::create(L); @@ -180,7 +144,6 @@ lua_State* setupBundleState( runtime, [luaurcFiles = std::move(luaurcFiles), bundleMap = std::move(bundleMap)](lua_State* L) { - luteopen_libs(L); if (Luau::CodeGen::isSupported()) Luau::CodeGen::create(L); diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index 3c090c850..d5bd07276 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(Lute.Require PRIVATE include/lute/bundlevfs.h include/lute/clivfs.h include/lute/filevfs.h + include/lute/lutevfs.h include/lute/modulepath.h include/lute/options.h include/lute/packagerequirevfs.h @@ -15,6 +16,7 @@ target_sources(Lute.Require PRIVATE src/bundlevfs.cpp src/clivfs.cpp src/filevfs.cpp + src/lutevfs.cpp src/modulepath.cpp src/options.cpp src/packagerequirevfs.cpp @@ -26,5 +28,5 @@ target_sources(Lute.Require PRIVATE target_compile_features(Lute.Require PUBLIC cxx_std_17) target_include_directories(Lute.Require PUBLIC include) -target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Std Lute.CLI.Commands Lute.Runtime) +target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Std Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Runtime) target_compile_options(Lute.Require PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/require/include/lute/lutevfs.h b/lute/require/include/lute/lutevfs.h new file mode 100644 index 000000000..bc586021a --- /dev/null +++ b/lute/require/include/lute/lutevfs.h @@ -0,0 +1,30 @@ +#pragma once + +#include "lute/modulepath.h" + +#include "Luau/DenseHash.h" + +#include "lua.h" + +#include + +extern const Luau::DenseHashMap kLuteModules; + +class LuteVfs +{ +public: + NavigationStatus resetToPath(const std::string& path); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& name); + + bool isModulePresent() const; + std::string getIdentifier() const; + std::optional getContents(const std::string& path) const; + + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; + +private: + std::optional modulePath; +}; diff --git a/lute/require/include/lute/packagerequirevfs.h b/lute/require/include/lute/packagerequirevfs.h index 84f29f396..023aee368 100644 --- a/lute/require/include/lute/packagerequirevfs.h +++ b/lute/require/include/lute/packagerequirevfs.h @@ -1,5 +1,6 @@ #pragma once +#include "lute/lutevfs.h" #include "lute/require.h" #include "lute/stdlibvfs.h" #include "lute/userlandvfs.h" @@ -49,7 +50,7 @@ class RequireVfs : public IRequireVfs Package::UserlandVfs userlandVfs; StdLibVfs stdLibVfs; - std::string lutePath; + LuteVfs luteVfs; }; } // namespace Package diff --git a/lute/require/include/lute/requirevfs.h b/lute/require/include/lute/requirevfs.h index f3cf77f9b..06292d9ee 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -3,6 +3,7 @@ #include "lute/bundlevfs.h" #include "lute/clivfs.h" #include "lute/filevfs.h" +#include "lute/lutevfs.h" #include "lute/modulepath.h" #include "lute/require.h" #include "lute/stdlibvfs.h" @@ -58,7 +59,7 @@ class RequireVfs : public IRequireVfs FileVfs fileVfs; StdLibVfs stdLibVfs; + LuteVfs luteVfs; std::optional cliVfs = std::nullopt; std::optional bundleVfs = std::nullopt; - std::string lutePath; }; diff --git a/lute/require/src/lutevfs.cpp b/lute/require/src/lutevfs.cpp new file mode 100644 index 000000000..315d10538 --- /dev/null +++ b/lute/require/src/lutevfs.cpp @@ -0,0 +1,106 @@ +#include "lute/lutevfs.h" + +#include "lute/crypto.h" +#include "lute/fs.h" +#include "lute/io.h" +#include "lute/luau.h" +#include "lute/modulepath.h" +#include "lute/net.h" +#include "lute/process.h" +#include "lute/system.h" +#include "lute/task.h" +#include "lute/time.h" +#include "lute/vm.h" + +#include "Luau/DenseHash.h" + +#include "lua.h" + +const Luau::DenseHashMap kLuteModules = []() +{ + Luau::DenseHashMap map{""}; + map["@lute/crypto.luau"] = luteopen_crypto; + map["@lute/fs.luau"] = luteopen_fs; + map["@lute/luau.luau"] = luteopen_luau; + map["@lute/net.luau"] = luteopen_net; + map["@lute/process.luau"] = luteopen_process; + map["@lute/task.luau"] = luteopen_task; + map["@lute/vm.luau"] = luteopen_vm; + map["@lute/system.luau"] = luteopen_system; + map["@lute/time.luau"] = luteopen_time; + map["@lute/io.luau"] = luteopen_io; + return map; +}(); + +static bool isLuteModule(const std::string& path) +{ + return kLuteModules.contains(path); +} + +static bool isLuteDirectory(const std::string& path) +{ + return path == "@lute"; +} + +NavigationStatus LuteVfs::resetToPath(const std::string& path) +{ + if (path == "@lute") + { + modulePath = ModulePath::create("@lute", "", isLuteModule, isLuteDirectory); + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; + } + + std::string lutePrefix = "@lute/"; + + if (path.rfind(lutePrefix, 0) != 0) + return NavigationStatus::NotFound; + + modulePath = ModulePath::create("@lute", path.substr(lutePrefix.size()), isLuteModule, isLuteDirectory); + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; +} + +NavigationStatus LuteVfs::toParent() +{ + LUAU_ASSERT(modulePath); + return modulePath->toParent(); +} + +NavigationStatus LuteVfs::toChild(const std::string& name) +{ + LUAU_ASSERT(modulePath); + return modulePath->toChild(name); +} + +bool LuteVfs::isModulePresent() const +{ + LUAU_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUAU_ASSERT(result.status == NavigationStatus::Success); + return result.type == ResolvedRealPath::PathType::File; +} + +std::string LuteVfs::getIdentifier() const +{ + LUAU_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUAU_ASSERT(result.status == NavigationStatus::Success); + return result.realPath; +} + +std::optional LuteVfs::getContents(const std::string& path) const +{ + // Lute modules have no source code. + return ""; +} + +ConfigStatus LuteVfs::getConfigStatus() const +{ + // Currently, we do not support .luaurc files in Lute commands. + return ConfigStatus::Absent; +} + +std::optional LuteVfs::getConfig() const +{ + // Currently, we do not support .luaurc files in Lute commands. + return std::nullopt; +} diff --git a/lute/require/src/packagerequirevfs.cpp b/lute/require/src/packagerequirevfs.cpp index 6430cd27a..f4c0b11aa 100644 --- a/lute/require/src/packagerequirevfs.cpp +++ b/lute/require/src/packagerequirevfs.cpp @@ -59,6 +59,7 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) status = stdLibVfs.resetToPath(std::string(path)); break; case VFSType::Lute: + status = luteVfs.resetToPath(std::string(path)); break; } return status; @@ -74,8 +75,7 @@ NavigationStatus RequireVfs::toAliasOverride(lua_State* L, std::string_view alia else if (aliasUnprefixed == "lute") { vfsType = VFSType::Lute; - lutePath = "@lute"; - return NavigationStatus::Success; + return luteVfs.resetToPath("@lute"); } return NavigationStatus::NotFound; @@ -101,7 +101,8 @@ NavigationStatus RequireVfs::toParent(lua_State* L) status = stdLibVfs.toParent(); break; case VFSType::Lute: - luaL_error(L, "cannot get the parent of @lute"); + status = luteVfs.toParent(); + break; } return status; @@ -116,7 +117,7 @@ NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) case VFSType::Std: return stdLibVfs.toChild(std::string(name)); case VFSType::Lute: - luaL_error(L, "'%s' is not a lute library", std::string(name).c_str()); + return luteVfs.toChild(std::string(name)); } return NavigationStatus::NotFound; @@ -131,7 +132,7 @@ bool RequireVfs::isModulePresent(lua_State* L) const case VFSType::Std: return stdLibVfs.isModulePresent(); case VFSType::Lute: - luaL_error(L, "@lute is not requirable"); + return luteVfs.isModulePresent(); } return false; @@ -149,6 +150,7 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c contents = stdLibVfs.getContents(loadname); break; case VFSType::Lute: + contents = luteVfs.getContents(loadname); break; } return contents ? *contents : ""; @@ -166,6 +168,7 @@ std::string RequireVfs::getChunkname(lua_State* L) const chunkname = "@" + stdLibVfs.getIdentifier(); break; case VFSType::Lute: + chunkname = "@" + luteVfs.getIdentifier(); break; } return chunkname; @@ -183,6 +186,7 @@ std::string RequireVfs::getLoadname(lua_State* L) const loadname = stdLibVfs.getIdentifier(); break; case VFSType::Lute: + loadname = luteVfs.getIdentifier(); break; } return loadname; @@ -200,6 +204,7 @@ std::string RequireVfs::getCacheKey(lua_State* L) const cacheKey = stdLibVfs.getIdentifier(); break; case VFSType::Lute: + cacheKey = luteVfs.getIdentifier(); break; } return cacheKey; @@ -217,6 +222,7 @@ ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const status = stdLibVfs.getConfigStatus(); break; case VFSType::Lute: + status = luteVfs.getConfigStatus(); break; } return status; @@ -234,6 +240,7 @@ std::string RequireVfs::getConfig(lua_State* L) const configContents = stdLibVfs.getConfig(); break; case VFSType::Lute: + configContents = luteVfs.getConfig(); break; } return configContents ? *configContents : ""; diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index 617107c9d..a0c0414cb 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -1,5 +1,6 @@ #include "lute/require.h" +#include "lute/lutevfs.h" #include "lute/modulepath.h" #include "lute/options.h" @@ -149,6 +150,16 @@ static luarequire_WriteResult get_config(lua_State* L, void* ctx, char* buffer, static int load(lua_State* L, void* ctx, const char* path, const char* chunkname, const char* loadname) { + // Lute modules are built-in and don't need to be compiled or executed. + if (strncmp(loadname, "@lute/", 6) == 0) + { + const lua_CFunction* func = kLuteModules.find(loadname); + LUAU_ASSERT(func); + lua_pushcfunction(L, *func, nullptr); + lua_call(L, 0, 1); + return 1; + } + // module needs to run in a new thread, isolated from the rest // note: we create ML on main thread so that it doesn't inherit environment of L lua_State* GL = lua_mainthread(L); diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index e3d4e3bac..278e21081 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -71,6 +71,9 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) case VFSType::Std: status = stdLibVfs.resetToPath(std::string(path)); break; + case VFSType::Lute: + status = luteVfs.resetToPath(std::string(path)); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); status = cliVfs->resetToPath(std::string(path)); @@ -79,8 +82,6 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) LUAU_ASSERT(bundleVfs); status = bundleVfs->resetToPath(std::string(path)); break; - case VFSType::Lute: - break; } return status; } @@ -95,8 +96,7 @@ NavigationStatus RequireVfs::toAliasOverride(lua_State* L, std::string_view alia else if (aliasUnprefixed == "lute") { vfsType = VFSType::Lute; - lutePath = "@lute"; - return NavigationStatus::Success; + return luteVfs.resetToPath("@lute"); } return NavigationStatus::NotFound; @@ -118,6 +118,9 @@ NavigationStatus RequireVfs::toParent(lua_State* L) case VFSType::Std: status = stdLibVfs.toParent(); break; + case VFSType::Lute: + status = luteVfs.toParent(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); status = cliVfs->toParent(); @@ -126,9 +129,6 @@ NavigationStatus RequireVfs::toParent(lua_State* L) LUAU_ASSERT(bundleVfs); status = bundleVfs->toParent(); break; - case VFSType::Lute: - luaL_error(L, "cannot get the parent of @lute"); - break; } return status; } @@ -141,15 +141,14 @@ NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) return fileVfs.toChild(std::string(name)); case VFSType::Std: return stdLibVfs.toChild(std::string(name)); + case VFSType::Lute: + return luteVfs.toChild(std::string(name)); case VFSType::Cli: LUAU_ASSERT(cliVfs); return cliVfs->toChild(std::string(name)); case VFSType::Bundle: LUAU_ASSERT(bundleVfs); return bundleVfs->toChild(std::string(name)); - case VFSType::Lute: - luaL_error(L, "'%s' is not a lute library", std::string(name).c_str()); - break; } return NavigationStatus::NotFound; } @@ -162,15 +161,15 @@ bool RequireVfs::isModulePresent(lua_State* L) const return fileVfs.isModulePresent(); case VFSType::Std: return stdLibVfs.isModulePresent(); + case VFSType::Lute: + return luteVfs.isModulePresent(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); return cliVfs->isModulePresent(); case VFSType::Bundle: LUAU_ASSERT(bundleVfs); return bundleVfs->isModulePresent(); - case VFSType::Lute: - luaL_error(L, "@lute is not requirable"); - break; } return false; @@ -188,6 +187,9 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c case VFSType::Std: contents = stdLibVfs.getContents(loadname); break; + case VFSType::Lute: + contents = luteVfs.getContents(loadname); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); contents = cliVfs->getContents(loadname); @@ -196,8 +198,6 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c LUAU_ASSERT(bundleVfs); contents = bundleVfs->getContents(loadname); break; - case VFSType::Lute: - break; } return contents ? *contents : ""; @@ -214,6 +214,9 @@ std::string RequireVfs::getChunkname(lua_State* L) const case VFSType::Std: chunkname = "@" + stdLibVfs.getIdentifier(); break; + case VFSType::Lute: + chunkname = "@" + luteVfs.getIdentifier(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); chunkname = "@" + cliVfs->getIdentifier(); @@ -222,8 +225,6 @@ std::string RequireVfs::getChunkname(lua_State* L) const LUAU_ASSERT(bundleVfs); chunkname = "@" + bundleVfs->getIdentifier(); break; - case VFSType::Lute: - break; } return chunkname; } @@ -239,6 +240,9 @@ std::string RequireVfs::getLoadname(lua_State* L) const case VFSType::Std: loadname = stdLibVfs.getIdentifier(); break; + case VFSType::Lute: + loadname = luteVfs.getIdentifier(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); loadname = cliVfs->getIdentifier(); @@ -247,8 +251,6 @@ std::string RequireVfs::getLoadname(lua_State* L) const LUAU_ASSERT(bundleVfs); loadname = bundleVfs->getIdentifier(); break; - case VFSType::Lute: - break; } return loadname; } @@ -264,6 +266,9 @@ std::string RequireVfs::getCacheKey(lua_State* L) const case VFSType::Std: cacheKey = stdLibVfs.getIdentifier(); break; + case VFSType::Lute: + cacheKey = luteVfs.getIdentifier(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); cacheKey = cliVfs->getIdentifier(); @@ -272,8 +277,6 @@ std::string RequireVfs::getCacheKey(lua_State* L) const LUAU_ASSERT(bundleVfs); cacheKey = bundleVfs->getIdentifier(); break; - case VFSType::Lute: - break; } return cacheKey; } @@ -289,6 +292,9 @@ ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const case VFSType::Std: status = stdLibVfs.getConfigStatus(); break; + case VFSType::Lute: + status = luteVfs.getConfigStatus(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); status = cliVfs->getConfigStatus(); @@ -297,8 +303,6 @@ ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const LUAU_ASSERT(bundleVfs); status = bundleVfs->getConfigStatus(); break; - case VFSType::Lute: - break; } return status; } @@ -314,6 +318,9 @@ std::string RequireVfs::getConfig(lua_State* L) const case VFSType::Std: configContents = stdLibVfs.getConfig(); break; + case VFSType::Lute: + configContents = luteVfs.getConfig(); + break; case VFSType::Cli: LUAU_ASSERT(cliVfs); configContents = cliVfs->getConfig(); @@ -322,8 +329,6 @@ std::string RequireVfs::getConfig(lua_State* L) const LUAU_ASSERT(bundleVfs); configContents = bundleVfs->getConfig(); break; - case VFSType::Lute: - break; } return configContents ? *configContents : ""; } diff --git a/tests/lute/vm.test.luau b/tests/lute/vm.test.luau new file mode 100644 index 000000000..fc32f1761 --- /dev/null +++ b/tests/lute/vm.test.luau @@ -0,0 +1,70 @@ +local test = require("@std/test") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") + +test.suite("LuteVmSuite", function(suite) + suite:case("child_vm_has_access_to_builtins", function(assert) + local tmpdir = system.tmpdir() + local child_vm = path.join(tmpdir, "child_vm.luau") + local parent_vm = path.join(tmpdir, "parent_vm.luau") + + do + local handle = fs.open(child_vm, "w+") + assert.eq(fs.exists(child_vm), true) + fs.write( + handle, + [[ + local std_test_exists = require("@std/test") ~= nil + local std_test_consistent = require("@std/test") == require("@std/test") + local lute_vm_exists = require("@lute/vm") ~= nil + local lute_vm_consistent = require("@lute/vm") == require("@lute/vm") + + local exported = {} + + function exported.std_test_exists() + return std_test_exists + end + function exported.std_test_consistent() + return std_test_consistent + end + function exported.lute_vm_exists() + return lute_vm_exists + end + function exported.lute_vm_consistent() + return lute_vm_consistent + end + + return exported + ]] + ) + fs.close(handle) + end + + do + local handle = fs.open(parent_vm, "w+") + assert.eq(fs.exists(parent_vm), true) + fs.write( + handle, + [[ + local vm = require("@lute/vm") + local result = vm.create("./child_vm") + assert(result.std_test_exists()) + assert(result.std_test_consistent()) + assert(result.lute_vm_exists()) + assert(result.lute_vm_consistent()) + ]] + ) + fs.close(handle) + end + + local result = process.run({ path.format(process.execpath()), path.format(parent_vm) }) + assert.eq(result.exitcode, 0) + + fs.remove(child_vm) + fs.remove(parent_vm) + end) +end) + +test.run() From 2326c7f29e53808910e0d566106c9ffeb4cca81e Mon Sep 17 00:00:00 2001 From: jkelaty-rbx <78873527+jkelaty-rbx@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:37:10 -0800 Subject: [PATCH 259/642] Hierarchical run command discovery (#695) When invoking lute, it will attempt to discover a script named with the provided argument, or within a `.lute` directory. This change supports the same discovery mechanism, but additionally supports discovering scripts in hierarchical `.lute` directories. As a practical example, I provided a luthier shorthand to demonstrate how this mechanism can be used. Instead of `lute tools/luthier.luau ...args` we can now invoke `lute luthier ...args`, and this can now be invoked from within any nested directory of lute. --- .gitignore | 1 + .lute/luthier.luau | 10 +++++++++ lute/cli/src/climain.cpp | 46 ++++++++++++++++++++++------------------ 3 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 .lute/luthier.luau diff --git a/.gitignore b/.gitignore index 3193d5aeb..6e0df9ce7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ __pycache__ .cache .clangd +**/.DS_Store compile_commands.json *~ extern/*/ diff --git a/.lute/luthier.luau b/.lute/luthier.luau new file mode 100644 index 000000000..4c0c1ddbc --- /dev/null +++ b/.lute/luthier.luau @@ -0,0 +1,10 @@ +--!strict + +local process = require("@lute/process") + +local args = table.pack(...) + +local cmd = { process.execpath(), "tools/luthier.luau", table.unpack(args, 2, args.n) } + +local result = process.run(cmd, { stdio = "inherit" }) +process.exit(result.exitcode) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index b8020fd17..073b5528c 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -39,8 +39,8 @@ static const char* HELP_STRING = R"(Usage: lute [options] [arguments.. check Type check Luau files. compile Compile a Luau script into a standalone executable. setup Generate type definition files for the language server. - transform Run a specified code transformation on specified Luau files. - lint Run linting rules on specified Luau files. + transform Run a specified code transformation on specified Luau files. + lint Run linting rules on specified Luau files. Run Options (when using 'run' or no command): lute [run] [args...] @@ -57,20 +57,20 @@ Compile Options: Setup Options: lute setup Generates type definition files for the language server. - --with-luaurc Defines aliases to the type definition files in the working directory's luaurc file. + --with-luaurc Defines aliases to the type definition files in the working directory's luaurc file. Transform Options: - lute transform [options...] - Runs the specified code transformation on the provided Luau files. - --dry-run Runs the transformation without actually overwriting or deleting any files. - --output Specifies an output file for a transformed file. Only valid when - transforming a single file. If not specified, files are overwritten in place. + lute transform [options...] + Runs the specified code transformation on the provided Luau files. + --dry-run Runs the transformation without actually overwriting or deleting any files. + --output Specifies an output file for a transformed file. Only valid when + transforming a single file. If not specified, files are overwritten in place. Lint Options: - lute lint [options...] - Runs linting rules on the specified Luau files. - --rules Path to a single lint rule or a directory containing multiple lint rules. - If not specified, default lint rules are used. + lute lint [options...] + Runs linting rules on the specified Luau files. + --rules Path to a single lint rule or a directory containing multiple lint rules. + If not specified, default lint rules are used. General Options: -h, --help Display this usage message. @@ -244,19 +244,23 @@ static std::pair getValidPath(std::string filePath) if (filePath.find('.') != std::string::npos) return {false, res}; - std::string fallbackPath = joinPaths(".lute", filePath); - size_t fallbackSize = fallbackPath.size(); + const std::array fallbackPaths = { + joinPaths(".lute", filePath + ".lua"), + joinPaths(".lute", filePath + ".luau"), + joinPaths(joinPaths(".lute", filePath), "init.lua"), + joinPaths(joinPaths(".lute", filePath), "init.luau"), + }; - for (const auto& ext : {".luau", ".lua"}) + for (std::optional currentPath = getCurrentWorkingDirectory(); currentPath; currentPath = getParentPath(*currentPath)) { - fallbackPath.resize(fallbackSize); - fallbackPath += ext; - - if (isFile(fallbackPath)) - return {true, fallbackPath}; + for (const std::string& fallbackPath : fallbackPaths) + { + const std::string commandPath = joinPaths(*currentPath, fallbackPath); + if (isFile(commandPath)) + return {true, commandPath}; + } } - return {false, res}; } From 49760c9c6e2b719ad6a9765d006df0abe26ef650 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 05:52:36 +0000 Subject: [PATCH 260/642] Update Lute to 0.1.0-nightly.20251220 (#688) **Lute**: Updated from `0.1.0-nightly.20251205` to `0.1.0-nightly.20251220` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20251220 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index f6e686a53..bbaad2d16 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251205" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251220" } diff --git a/rokit.toml b/rokit.toml index 3a1f07b74..26666a3c0 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20251205" +lute = "luau-lang/lute@0.1.0-nightly.20251220" From 79e3110224de0a172abf73d120fb3f2b67ee62bd Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 5 Jan 2026 14:21:23 -0800 Subject: [PATCH 261/642] Runs `lute test` as part of CI (#705) Thanks to recent changes to how require works, namely that `@std` requires will always resolve to the single builtin instance in the runtime (ignoring aliases), `lute test` can now handle transitive and batteries requires correctly without needing to intercept requires at all. This was the last blocker remaining before `lute test` could be used in CI. This PR: a) updates CI to call `lute test` b) removes the require interception that `lute test` did b) removes the explicit call to test.run() within the test files in our repo, since test discovery handles this I've left `test.run()` in for now, in case there is some desire to use the testing library without `lute test` (and the standard library tests in `std.test.luau` exercise this behaviour). I'm open to opinions about what to do with it, and can address those in a followup. Resolves issue #634 --- .github/workflows/ci.yml | 13 +------------ lute/cli/commands/test/init.luau | 20 +------------------- tests/batteries/base64.test.luau | 2 -- tests/batteries/collections/deque.test.luau | 2 -- tests/batteries/difftext.test.luau | 2 -- tests/cli/compile.test.luau | 2 -- tests/cli/discovery/example.spec.luau | 2 -- tests/cli/discovery/smoke.test.luau | 2 -- tests/cli/lib/files.test.luau | 2 -- tests/cli/lint.test.luau | 2 -- tests/cli/loadbypath.test.luau | 2 -- tests/cli/run.test.luau | 2 -- tests/cli/test.test.luau | 2 -- tests/cli/transform.test.luau | 2 -- tests/lute/task.test.luau | 2 -- tests/lute/vm.test.luau | 2 -- tests/runtime/crypto.test.luau | 2 -- tests/std/fs.test.luau | 2 -- tests/std/json.test.luau | 2 -- tests/std/luau.test.luau | 2 -- tests/std/net.test.luau | 2 -- tests/std/path/path.posix.test.luau | 2 -- tests/std/path/path.win32.test.luau | 2 -- tests/std/process.test.luau | 2 -- tests/std/stringext.test.luau | 2 -- tests/std/syntax/parser.test.luau | 2 -- tests/std/syntax/printer.test.luau | 2 -- tests/std/syntax/query.test.luau | 2 -- tests/std/tableext.test.luau | 2 -- tests/std/test.test.luau | 2 -- tests/std/time.test.luau | 2 -- 31 files changed, 2 insertions(+), 89 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb82941df..3bb8d022a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,18 +53,7 @@ jobs: run: rm .luaurc && mv .luaurc.ci .luaurc - name: Run Luau Tests (POSIX) - if: runner.os != 'Windows' - shell: bash - run: find tests -name "*.test.luau" | xargs -I {} "${{ steps.build_lute.outputs.exe_path }}" run {} - - - name: Run Luau Tests (Windows) - if: runner.os == 'Windows' - shell: cmd - run: | - set _errorFlag=0 - for /r tests %%f in (*.test.luau) do "${{ steps.build_lute.outputs.exe_path }}" run "%%f" || (set _errorFlag=1) - if %_errorFlag% neq 0 exit /b 1 - + run: ${{ steps.build_lute.outputs.exe_path }} test run-lute-cxx-unittests: runs-on: ${{ matrix.os }} diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index 9c73c9ded..ea3c01d9d 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -32,28 +32,10 @@ EXAMPLES: ]]) end ---[[ - Any of the tests we load will use the instance of std/test initialized in this module (cli/commands/test/init.luau) - Tests that run via lute test don't need to explicitly call test.run, so we'll pass an empty run function so that we don't - execute any tests, while giving us time to remove unecessary run() calls (doing this because this doesn't support running tests just yet) - The reason we have to inject a custom require here is because each `loadbypath` on a test file will return the "@std/test" instance provided by - the EmbeddedStdVFS. However, running `lute test` from the root of the lute repo will use a distinct @std/test instance, which is provided by the .luaurc when running - this file, which means the test environment won't get populated. The consequence of this is that `lute test` will work perfectly everywhere except inside of - the lute repo, which sucks. Intercepting @std/test requires works correctly and ensures that the correct @std/test instance gets populated -]] --- -local function lutetestrequire(req: string) - if req == "@std/test" then - return table.freeze({ case = test.case, suite = test.suite, run = function() end }) - end - return require(req) :: any -end - local function loadtests(testfiles: { path.path }) - local fenv: { [unknown]: unknown } = setmetatable({ ["require"] = lutetestrequire }, { __index = _G }) -- Load all test files first for _, p in testfiles do - local success, err = pcall(luau.loadbypath, p, fenv) + local success, err = pcall(luau.loadbypath, p, nil) if not success then print(`Error loading {path.format(p)}: {err}`) diff --git a/tests/batteries/base64.test.luau b/tests/batteries/base64.test.luau index 6f616369b..5e5f4be68 100644 --- a/tests/batteries/base64.test.luau +++ b/tests/batteries/base64.test.luau @@ -72,5 +72,3 @@ test.suite("Base64", function(suite) end) end end) - -test.run() diff --git a/tests/batteries/collections/deque.test.luau b/tests/batteries/collections/deque.test.luau index f3e013f6d..590fe709a 100644 --- a/tests/batteries/collections/deque.test.luau +++ b/tests/batteries/collections/deque.test.luau @@ -93,5 +93,3 @@ test.suite("deque", function(suite) assert.eq(deq:peekback(), 2) end) end) - -test.run() diff --git a/tests/batteries/difftext.test.luau b/tests/batteries/difftext.test.luau index ca13bdc46..37908dab4 100644 --- a/tests/batteries/difftext.test.luau +++ b/tests/batteries/difftext.test.luau @@ -491,5 +491,3 @@ test.suite("printdiff", function(suite) assert.eq(lines[103], composeDetailedAddition("modified", {}, " 101| ")) end) end) - -test.run() diff --git a/tests/cli/compile.test.luau b/tests/cli/compile.test.luau index 89f4ffd90..d50631a86 100644 --- a/tests/cli/compile.test.luau +++ b/tests/cli/compile.test.luau @@ -48,5 +48,3 @@ test.suite("Lute CLI Compile", function(suite) fs.remove(compileePath) end) end) - -test.run() diff --git a/tests/cli/discovery/example.spec.luau b/tests/cli/discovery/example.spec.luau index de73312ba..e1280ef91 100644 --- a/tests/cli/discovery/example.spec.luau +++ b/tests/cli/discovery/example.spec.luau @@ -5,5 +5,3 @@ test.suite("ExampleSuite", function(suite) assert.eq(2 + 2, 4) end) end) - -test.run() diff --git a/tests/cli/discovery/smoke.test.luau b/tests/cli/discovery/smoke.test.luau index 84f6d32b5..eb7a52fdf 100644 --- a/tests/cli/discovery/smoke.test.luau +++ b/tests/cli/discovery/smoke.test.luau @@ -11,5 +11,3 @@ test.suite("SmokeSuite", function(suite) end) test.case("Passing", function(assert) end) - -test.run() diff --git a/tests/cli/lib/files.test.luau b/tests/cli/lib/files.test.luau index fb657ed8d..3752b527b 100644 --- a/tests/cli/lib/files.test.luau +++ b/tests/cli/lib/files.test.luau @@ -265,5 +265,3 @@ test.suite("cli/lib/files", function(suite) fs.removedirectory(testPath, { recursive = true }) end) end) - -test.run() diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index c6e6e0643..caf23a12b 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -1069,5 +1069,3 @@ violator.luau:5:12-17 ── fs.remove(violatorPath) end) end) - -test.run() diff --git a/tests/cli/loadbypath.test.luau b/tests/cli/loadbypath.test.luau index fcb029ad7..cde7065d4 100644 --- a/tests/cli/loadbypath.test.luau +++ b/tests/cli/loadbypath.test.luau @@ -56,5 +56,3 @@ test.suite("Lute CLI Run", function(suite) end end) end) - -test.run() diff --git a/tests/cli/run.test.luau b/tests/cli/run.test.luau index 33234129f..b1ae8babe 100644 --- a/tests/cli/run.test.luau +++ b/tests/cli/run.test.luau @@ -38,5 +38,3 @@ test.suite("lute-run", function(suite) assert.strnotcontains(result.stdout, "Hello world") end) end) - -test.run() diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 09ecc4dc8..1e088ef4a 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -71,5 +71,3 @@ test.suite("LuteTestCommand", function(suite) assert.neq(result.stdout:find("Passed: 1", 1, true), nil) end) end) - -test.run() diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index 405baa92a..f4d37b601 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -147,5 +147,3 @@ test.suite("lute transform", function(suite) fs.remove(outputPath) end) end) - -test.run() diff --git a/tests/lute/task.test.luau b/tests/lute/task.test.luau index 12a5da513..6dc7c1c33 100644 --- a/tests/lute/task.test.luau +++ b/tests/lute/task.test.luau @@ -23,5 +23,3 @@ test.suite("LuteTaskSuite", function(suite) assert.eq(endTime - startTime >= 0.5, true) end) end) - -test.run() diff --git a/tests/lute/vm.test.luau b/tests/lute/vm.test.luau index fc32f1761..d74be1ec9 100644 --- a/tests/lute/vm.test.luau +++ b/tests/lute/vm.test.luau @@ -66,5 +66,3 @@ test.suite("LuteVmSuite", function(suite) fs.remove(parent_vm) end) end) - -test.run() diff --git a/tests/runtime/crypto.test.luau b/tests/runtime/crypto.test.luau index 5062f7aaf..2ce2d8ba7 100644 --- a/tests/runtime/crypto.test.luau +++ b/tests/runtime/crypto.test.luau @@ -121,5 +121,3 @@ test.suite("CryptoRuntimeTests", function(suite) end) end) end) - -test.run() diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index ab1fb309b..d9d29d8de 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -261,5 +261,3 @@ test.suite("FsSuite", function(suite) fs.removedirectory(baseDir, { recursive = true }) end) end) - -test.run() diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau index 4c950dad0..cf3dd7260 100644 --- a/tests/std/json.test.luau +++ b/tests/std/json.test.luau @@ -85,5 +85,3 @@ test.suite("JSONParsingTestSuite", function(suite) end end) end) - -test.run() diff --git a/tests/std/luau.test.luau b/tests/std/luau.test.luau index 6c9182e54..0d542cdcc 100644 --- a/tests/std/luau.test.luau +++ b/tests/std/luau.test.luau @@ -45,5 +45,3 @@ test.suite("LuauTypeOfModuleSuite", function(suite) assert.eq(resolvedPath, expected) end) end) - -test.run() diff --git a/tests/std/net.test.luau b/tests/std/net.test.luau index dc8caec95..e1c5eacaf 100644 --- a/tests/std/net.test.luau +++ b/tests/std/net.test.luau @@ -22,5 +22,3 @@ test.suite("NetTestSuite", function(suite) server.close() end) end) - -test.run() diff --git a/tests/std/path/path.posix.test.luau b/tests/std/path/path.posix.test.luau index d9efafafb..f5fcd5f61 100644 --- a/tests/std/path/path.posix.test.luau +++ b/tests/std/path/path.posix.test.luau @@ -588,5 +588,3 @@ test.suite("PathPosixRelativeSuite", function(suite) end, "Cannot compute relative path between absolute and relative paths") end) end) - -test.run() diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index da3c44869..543fcdedf 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -817,5 +817,3 @@ test.suite("PathWin32RelativeSuite", function(suite) assert.eq(result.parts[2], "file.txt") end) end) - -test.run() diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 74c7c700e..62f1d73c8 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -135,5 +135,3 @@ test.suite("ProcessSuite", function(suite) end, "process options must be a table") end) end) - -test.run() diff --git a/tests/std/stringext.test.luau b/tests/std/stringext.test.luau index 7fdb5b620..1b78bbb9c 100644 --- a/tests/std/stringext.test.luau +++ b/tests/std/stringext.test.luau @@ -36,5 +36,3 @@ test.suite("StringExt", function(suite) assert.eq(stringext.count("hello world", "[hw]"), 2) -- count h and w end) end) - -test.run() diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 9e9dd452f..4446535a0 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -1193,5 +1193,3 @@ test.suite("parse types", function(suite) assert.eq(variadicType.type.tag, "reference") end) end) - -test.run() diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index ef9cc08ca..1a86b7b24 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -894,5 +894,3 @@ function foo() return end end, assert) end) end) - -test.run() diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index e6f78fbf4..21d6ab62e 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -244,5 +244,3 @@ test.suite("AST Query", function(suite) assert.eq(evenOnly[3], 4) end) end) - -test.run() diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index 266d6e346..6395b43a4 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -63,5 +63,3 @@ test.suite("TableExt", function(suite) assert.tableeq(c, { 6, 5, 4 }) end) end) - -test.run() diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index df8538a83..3558eb36e 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -338,5 +338,3 @@ test.run() ) end) end) - -test.run() diff --git a/tests/std/time.test.luau b/tests/std/time.test.luau index c671527d5..48f05443b 100644 --- a/tests/std/time.test.luau +++ b/tests/std/time.test.luau @@ -155,5 +155,3 @@ test.suite("TimeSuite", function(suite) assert.eq(tostring(d), "0.123000456") end) end) - -test.run() From 157a116f7f8dc71d984ef0a835c697df5a695915 Mon Sep 17 00:00:00 2001 From: Alexander Robert <171880829+Alexander-L-Robert@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:32:12 -0800 Subject: [PATCH 262/642] Add `tableext.toset` (#699) Felt like tableext should have a `toset` util. --- lute/std/libs/tableext.luau | 8 ++++++++ tests/std/tableext.test.luau | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau index a2a626909..acd16ff99 100644 --- a/lute/std/libs/tableext.luau +++ b/lute/std/libs/tableext.luau @@ -56,6 +56,14 @@ function tableext.keys(tbl: { [K]: V }): { K } return keys end +function tableext.toset(tbl: { T }): { [T]: true } + local set: { [T]: true } = {} + for _, v in tbl do + set[v] = true + end + return set +end + function tableext.reverse(tbl: { T }, inplace: boolean?) local new = if inplace then tbl else {} local i, j = 1, #tbl diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index 6395b43a4..8e39bb363 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -62,4 +62,39 @@ test.suite("TableExt", function(suite) assert.tableeq(b, { 4, 5, 6 }) assert.tableeq(c, { 6, 5, 4 }) end) + + suite:case("toset", function(assert) + local a = { 1, 2, 4, 2, 1 } + local setA = tableext.toset(a) + assert.eq(setA[1], true) + assert.eq(setA[2], true) + assert.eq(setA[3], nil) + assert.eq(setA[4], true) + assert.eq(setA[5], nil) + + local setSize = #tableext.keys(setA) + assert.eq(setSize, 3) + assert.neq(setSize, #a) + + -- Sanity check: sets created using an array of numbers don't break from having "gaps" between the number keys. + + local foundKeys: { number } = {} + for key, value in setA do + assert.eq(value, true) + table.insert(foundKeys, key) + end + assert.eq(#foundKeys, setSize) + assert.eq(setA[foundKeys[1]], true) + assert.eq(setA[foundKeys[2]], true) + assert.eq(setA[foundKeys[3]], true) + + local b = { "a", "b", "c", "a" } + local setB = tableext.toset(b) + assert.eq(setB["a"], true) + assert.eq(setB["b"], true) + assert.eq(setB["c"], true) + assert.eq(setB["d"], nil) + assert.eq(#tableext.keys(setB), 3) + assert.neq(#tableext.keys(setB), #b) + end) end) From a1794874ee2fa49b3dc1314750061beb5353f57e Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 6 Jan 2026 15:03:07 -0800 Subject: [PATCH 263/642] Improves lute runtime performance when scheduling many threads (#706) The lute runtime currently schedules coroutines in a std::vector, and we always pop off the first thread in the vector to run. For small numbers of coroutines, this is okay, but when you get to 10k+ this becomes very inefficient as we have to move all the other elements one over `O(n)`. This PR just replaces the `std::vector` with a `Luau::VecDeque`, making the pop operation `O(1)`. I've also included a profiling script that I used to profile lute since I keep forgetting the `xctrace` incantation - it can be invoked by: ``` ./tools/profile.sh /path/to/.luau(defaults to profile.luau) ``` --- lute/runtime/include/lute/runtime.h | 3 ++- lute/runtime/src/runtime.cpp | 2 +- tools/profile.luau | 32 +++++++++++++++++++++++++++++ tools/profile.sh | 4 ++++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 tools/profile.luau create mode 100755 tools/profile.sh diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index 1defc5ec8..b3635d3bc 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -3,6 +3,7 @@ #include "lute/ref.h" #include "Luau/Variant.h" +#include "Luau/VecDeque.h" #include #include @@ -80,7 +81,7 @@ struct Runtime std::mutex dataCopyMutex; std::unique_ptr dataCopy; - std::vector runningThreads; + Luau::VecDeque runningThreads; private: std::mutex continuationMutex; diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index d36a7606c..e617ea420 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -67,7 +67,7 @@ RuntimeStep Runtime::runOnce() return StepEmpty{}; auto next = std::move(runningThreads.front()); - runningThreads.erase(runningThreads.begin()); + runningThreads.pop_front(); next.ref->push(GL); lua_State* L = lua_tothread(GL, -1); diff --git a/tools/profile.luau b/tools/profile.luau new file mode 100644 index 000000000..3e7259428 --- /dev/null +++ b/tools/profile.luau @@ -0,0 +1,32 @@ +local task = require("@lute/task") + +local started = os.clock() + +local amount = 100000 +local batches = 5 +local per_batch = amount / batches + +for current = 1, batches do + local thread = coroutine.running() + + print(`Batch {current} / {batches}`) + + for i = 1, per_batch do + --print("Spawning thread #" .. i) + task.spawn(function() + task.wait(0.1) + --_TEST_ASYNC_WORK(0.1) + if i == per_batch then + print("Last thread in batch #" .. current) + assert(coroutine.status(thread) == "suspended", `Thread {i} has status {coroutine.status(thread)}`) + task.spawn(thread) + end + end) + end + + coroutine.yield() +end +local took = os.clock() - started +print(`Spawned {amount} sleeping threads in {took}s`) + +return -1 diff --git a/tools/profile.sh b/tools/profile.sh new file mode 100755 index 000000000..c1eab24fc --- /dev/null +++ b/tools/profile.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +DEFAULT_SCRIPTNAME=${1:-profile.luau} +echo "profiling $DEFAULT_SCRIPTNAME" +xctrace record --template 'Time Profiler' --launch -- ./build/xcode/debug/lute/cli/lute $DEFAULT_SCRIPTNAME From 102c38850a33e329a07a3afaa3c4436c9de6d5fc Mon Sep 17 00:00:00 2001 From: Raymond Ng Date: Thu, 8 Jan 2026 16:10:29 -0800 Subject: [PATCH 264/642] Lute check uses the new solver by default (#711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses #710 Updates `lute check` to use the new solver by default. Fails the test as expected without the change ``` Failed Tests (1): ❌ lute check.uses new solver /Users/rng/dev/lute/tests/cli/check.test.luau:32 eq: 1 ~= 0 ``` --- lute/cli/src/tc.cpp | 29 +------------------------ tests/CMakeLists.txt | 3 ++- tests/cli/check.test.luau | 16 ++++++++++++++ tests/src/staticrequires/newsolver.luau | 14 ++++++++++++ tests/src/typecheck.test.cpp | 16 ++++++++++++++ 5 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 tests/cli/check.test.luau create mode 100644 tests/src/staticrequires/newsolver.luau create mode 100644 tests/src/typecheck.test.cpp diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index 60e6afe4e..002282fba 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -8,31 +8,6 @@ #include "Luau/FileUtils.h" #include "Luau/Frontend.h" -static const std::string kLuteDefinitions = R"LUTE_TYPES( --- Net api -declare net: { - get: (string) -> string, - getAsync: (string) -> string, -} --- fs api -declare class file end -declare fs: { - -- probably not the correct sig - open: (string, "r" | "w" | "a" | "r+" | "w+") -> file, - close: (file) -> (), - read: (file) -> string, - write: (file, string) -> (), - readfiletostring : (string) -> string, - writestringtofile : (string, string) -> (), - -- is this right? I feel like we want a promise type here - readasync : (string) -> string, -} - --- globals -declare function spawn(path: string): any - -)LUTE_TYPES"; - struct LuteFileResolver : Luau::LuteModuleResolver { std::optional readSource(const Luau::ModuleName& name) override @@ -186,11 +161,9 @@ int typecheck(const std::vector& sourceFilesInput, LuteReporter& re LuteFileResolver fileResolver; Luau::LuteConfigResolver configResolver(mode); Luau::Frontend frontend(&fileResolver, &configResolver, frontendOptions); + frontend.setLuauSolverMode(Luau::SolverMode::New); Luau::registerBuiltinGlobals(frontend, frontend.globals); - Luau::LoadDefinitionFileResult loadResult = - frontend.loadDefinitionFile(frontend.globals, frontend.globals.globalScope, kLuteDefinitions, "@luau", false, false); - LUAU_ASSERT(loadResult.success); Luau::freeze(frontend.globals.globalTypes); for (const std::string& path : sourceFiles) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b353f8384..f243eb161 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -24,7 +24,8 @@ target_sources(Lute.Test PRIVATE src/packagerequire.test.cpp src/require.test.cpp src/staticrequires.test.cpp - src/stdsystem.test.cpp) + src/stdsystem.test.cpp + src/typecheck.test.cpp) set_target_properties(Lute.Test PROPERTIES OUTPUT_NAME lute-tests) target_compile_features(Lute.Test PUBLIC cxx_std_17) diff --git a/tests/cli/check.test.luau b/tests/cli/check.test.luau new file mode 100644 index 000000000..ed33c3756 --- /dev/null +++ b/tests/cli/check.test.luau @@ -0,0 +1,16 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local lutePath = path.format(process.execpath()) + +test.suite("LuteTypeCheck", function(suite) + suite:case("UsesNewSolver", function(assert) + local testFilePath = path.format(path.join("tests", "src", "staticrequires", "newsolver.luau")) + + local result = process.run({ lutePath, "check", testFilePath }) + assert.eq(result.exitcode, 0) + end) +end) diff --git a/tests/src/staticrequires/newsolver.luau b/tests/src/staticrequires/newsolver.luau new file mode 100644 index 000000000..533dfcc7e --- /dev/null +++ b/tests/src/staticrequires/newsolver.luau @@ -0,0 +1,14 @@ +-- This file should fail the old solver +function add(a, b) + return a + b +end +local vec2 = {} +function vec2.new(x, y) + return setmetatable({ x = x or 0, y = y or 0 }, { + __add = function(v1, v2) + return { x = v1.x + v2.x, y = v1.y + v2.y } + end, + }) +end +add(1, 1) +add(vec2.new(0, 0), vec2.new(1, 1)) diff --git a/tests/src/typecheck.test.cpp b/tests/src/typecheck.test.cpp new file mode 100644 index 000000000..b9e0b3f47 --- /dev/null +++ b/tests/src/typecheck.test.cpp @@ -0,0 +1,16 @@ +#include "lute/tc.h" + +#include "Luau/FileUtils.h" + +#include "doctest.h" +#include "lutefixture.h" +#include "luteprojectroot.h" + +TEST_CASE_FIXTURE(LuteFixture, "typecheck_uses_new_solver") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/newsolver.luau"); + + auto result = typecheck({testFilePath}, getReporter()); + CHECK(result == 0); +} From a9ed8df5254d2233f6bb0e7d3ea15bd7691b2b67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:09:20 -0800 Subject: [PATCH 265/642] Update Luau to 0.704 (#713) **Luau**: Updated from `0.703` to `0.704` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.704 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Nick Winans --- extern/luau.tune | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 959501695..66f11596f 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.703" -revision = "c33adf13ed76302c293c442ab13fc84874c53ea5" +branch = "0.704" +revision = "dc955d68e70d7bff0735cfc3927f30be2277a2fc" From 972b0836e236cca8fd534672f08cc3b9b400e35f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:20:31 -0800 Subject: [PATCH 266/642] Update Lute to 0.1.0-nightly.20260109 (#714) **Lute**: Updated from `0.1.0-nightly.20251220` to `0.1.0-nightly.20260109` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260109 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Nick Winans --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index bbaad2d16..92e6e9cfa 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20251220" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260109" } diff --git a/rokit.toml b/rokit.toml index 26666a3c0..fb2317eb8 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20251220" +lute = "luau-lang/lute@0.1.0-nightly.20260109" From 8bcd1f7dea687cfc04326ed00d5e8a71aa8b8c42 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:47:44 -0500 Subject: [PATCH 267/642] Fix `parseGlob` handling of `.` character (#716) --- lute/cli/commands/lib/ignore.luau | 4 ++-- tests/cli/lib/files.test.luau | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lute/cli/commands/lib/ignore.luau b/lute/cli/commands/lib/ignore.luau index 9a4f7eadf..6bba90665 100644 --- a/lute/cli/commands/lib/ignore.luau +++ b/lute/cli/commands/lib/ignore.luau @@ -97,8 +97,8 @@ local function parseGlob(glob: string): Glob -- Match any single character, except for directory separator table.insert(lua_parts, `[^{separator}]`) idx = idx + 1 - elseif char == "-" then - table.insert(lua_parts, "%-") + elseif char == "-" or char == "." then + table.insert(lua_parts, `%{char}`) idx = idx + 1 elseif char == "/" or char == "\\" then table.insert(lua_parts, separator) diff --git a/tests/cli/lib/files.test.luau b/tests/cli/lib/files.test.luau index 3752b527b..15843aa2d 100644 --- a/tests/cli/lib/files.test.luau +++ b/tests/cli/lib/files.test.luau @@ -26,6 +26,11 @@ test.suite("cli/lib/files", function(suite) local shouldNotIgnorePath = path.join(testPath, "readme.luau") fs.writestringtofile(shouldNotIgnorePath, "") + local shouldNotIgnoreDirectory = path.join(testPath, "lua") + fs.createdirectory(shouldNotIgnoreDirectory) + local shouldNotIgnoreNestedPath = path.join(shouldNotIgnoreDirectory, "readme.luau") + fs.writestringtofile(shouldNotIgnoreNestedPath, "") + -- Do local results = files.getSourceFiles({ path.format(testPath) }) @@ -36,6 +41,7 @@ test.suite("cli/lib/files", function(suite) end assert.eq(resultPaths[path.format(shouldIgnorePath)], nil) assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) + assert.eq(resultPaths[path.format(shouldNotIgnoreNestedPath)], true) -- Teardown fs.removedirectory(testPath, { recursive = true }) From abebecc8155c1441a8cebbe918248c231e07b0d5 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:07:42 -0500 Subject: [PATCH 268/642] Ensure `ExprTableItem` nodes are serialized with `location` (#718) `AstExprTableItem` nodes are not serialized with location right now. I encountered a bug due to this behavior while using `lute lint` CLI: ``` Error linting file '/Users/wmccarthy/git/roblox/luau-ui-ecosystem-lints/repro_nil_location.luau': @std/syntax/init.luau:14: attempt to index nil with 'beginline' ``` If you follow the trace from std/syntax/init.luau:14, you'll see the error stems from calls to syntax.span.subsumes in lint's init file ([see here](https://github.com/luau-lang/lute/blob/primary/lute/cli/commands/lint/init.luau?rgh-link-date=2026-01-07T21%3A07%3A33Z#L150-L160)). Here, `nodeIsSuppressed` is used as the default visitor function, in other words, it is called at every visited node as the visitor walks the tree. It relies on the assumption that every node has location field, which is not true. `ExprTableItem` nodes, specifically, do not have a `location` field. Adding `location` to table item nodes will fix this issue and help avoid any similar future issues. --- definitions/luau.luau | 10 +++++++++- lute/luau/src/luau.cpp | 13 ++++++++++--- tests/cli/lint.test.luau | 22 ++++++++++++++++++++++ tests/std/syntax/parser.test.luau | 27 +++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 231ddac1b..3b605073b 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -158,9 +158,16 @@ export type AstExprAnonymousFunction = { } -- helper types for items contained in an AstExprTable, not actually AstExprs themselves -export type AstExprTableItemList = { kind: "list", value: AstExpr, separator: Token<"," | ";">?, istableitem: true } +export type AstExprTableItemList = { + location: span, + kind: "list", + value: AstExpr, + separator: Token<"," | ";">?, + istableitem: true, +} export type AstExprTableItemRecord = { + location: span, kind: "record", key: Token, equals: Token<"=">, @@ -170,6 +177,7 @@ export type AstExprTableItemRecord = { } export type AstExprTableItemGeneral = { + location: span, kind: "general", indexeropen: Token<"[">, key: AstExpr, diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 2f14c294e..2d86f5068 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -462,27 +462,32 @@ struct AstSerialize : public Luau::AstVisitor lua_rawcheckstack(L, 2); + if (item.kind == Luau::AstExprTable::Item::List) { - lua_createtable(L, 0, 4); + lua_createtable(L, 0, 5); lua_pushstring(L, "list"); lua_setfield(L, -2, "kind"); lua_pushboolean(L, 1); lua_setfield(L, -2, "istableitem"); + withLocation(Luau::Location{item.value->location.begin, item.value->location.end}); + visit(item.value); lua_setfield(L, -2, "value"); } else if (item.kind == Luau::AstExprTable::Item::Record) { - lua_createtable(L, 0, 6); + lua_createtable(L, 0, 7); lua_pushstring(L, "record"); lua_setfield(L, -2, "kind"); lua_pushboolean(L, 1); lua_setfield(L, -2, "istableitem"); + withLocation(Luau::Location{item.key->location.begin, item.value->location.end}); + const auto& value = item.key->as()->value; serializeToken(item.key->location.begin, std::string(value.data, value.size).data()); lua_setfield(L, -2, "key"); @@ -496,7 +501,7 @@ struct AstSerialize : public Luau::AstVisitor } else if (item.kind == Luau::AstExprTable::Item::General) { - lua_createtable(L, 0, 8); + lua_createtable(L, 0, 9); lua_pushstring(L, "general"); lua_setfield(L, -2, "kind"); @@ -504,6 +509,8 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "istableitem"); LUAU_ASSERT(cstNode->indexerOpenPosition); + withLocation(Luau::Location{*cstNode->indexerOpenPosition, item.value->location.end}); + serializeToken(*cstNode->indexerOpenPosition, "["); lua_setfield(L, -2, "indexeropen"); diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index caf23a12b..f03f427c7 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -1068,4 +1068,26 @@ violator.luau:5:12-17 ── -- Teardown fs.remove(violatorPath) end) + + suite:case("lute lint ignore (table item)", function(assert) + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local y = { + -- lute-lint-ignore(divide_by_zero) + x = 3 / 0 +} + +local z = 5 / 0 +]] + ) + + local result = process.run({ lutePath, "lint", violatorPath }) + assert.eq(result.exitcode, 1) + assert.strnotcontains(result.stdout, `Error linting file '{violatorPath}'`) + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + + fs.remove(violatorPath) + end) end) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 4446535a0..6768d0fed 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -499,6 +499,33 @@ test.suite("parseExpr", function(suite) assert.eq(expr.operator.text, "::") assert.eq(expr.annotation.tag, "reference") end) + + -- table items serialized with location field + suite:case("ExprTableItem serialized with location field", function(assert) + local tableExpr = parser.parseexpr([[ +{ +1, +foo = bar, +[2] = baz +} + ]]) + assert.eq(tableExpr.tag, "table") + assert.eq(tableExpr.kind, "expr") + tableExpr = tableExpr :: syntax.AstExprTable + assert.eq(#tableExpr.entries, 3) + assert.eq(tableExpr.entries[1].location.beginline, 2) + assert.eq(tableExpr.entries[1].location.begincolumn, 1) + assert.eq(tableExpr.entries[1].location.endline, 2) + assert.eq(tableExpr.entries[1].location.endcolumn, 2) + assert.eq(tableExpr.entries[2].location.beginline, 3) + assert.eq(tableExpr.entries[2].location.begincolumn, 1) + assert.eq(tableExpr.entries[2].location.endline, 3) + assert.eq(tableExpr.entries[2].location.endcolumn, 10) + assert.eq(tableExpr.entries[3].location.beginline, 4) + assert.eq(tableExpr.entries[3].location.begincolumn, 1) + assert.eq(tableExpr.entries[3].location.endline, 4) + assert.eq(tableExpr.entries[3].location.endcolumn, 10) + end) end) local function checkIsBlock(node: any, assert: asserts.asserts) From 2736876f0e95efd429e0dec67f2915d293c381c0 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 12 Jan 2026 14:37:43 -0800 Subject: [PATCH 269/642] Extends `lute lint` to support linting an input string (#719) The bulk of the changes are refactoring some of the linting logic to extract the common logic between linting a file and a string. Also, paths in reported lint violations are now optional since they're not relevant to linting string input. The broader context for this PR is that it allows linting to be consumed by VSCode (and maybe other) extensions, since the source of truth for file contents is whatever the editor/LSP server has in memory, rather than what's on disk in the file. --- lute/cli/commands/lint/init.luau | 135 ++++++++++++------ lute/cli/commands/lint/lsp/init.luau | 6 +- lute/cli/commands/lint/printer.luau | 8 +- .../commands/lint/rules/almost_swapped.luau | 2 +- .../commands/lint/rules/divide_by_zero.luau | 2 +- lute/cli/commands/lint/types.luau | 4 +- tests/cli/lint.test.luau | 89 +++++++++++- 7 files changed, 191 insertions(+), 55 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 90f38c0de..d1c31cfbb 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -30,6 +30,7 @@ OPTIONS: modules exporting lint rules, while all other .luau files will be treated as individual lint rules. If unspecified, the default lint rules are used. -j, --json Output lint violations in JSON format matching the LSP diagnostic spec. + -s, --string-input Lint the provided string input instead of reading from files. PATHS: Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. @@ -199,19 +200,18 @@ local function parseGlobalIgnores(trivia: { syntax.Trivia }): { [string]: true } return globalIgnores end -local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule }): { types.LintViolation } +local function lintString( + source: string, + lintRules: { types.LintRule }, + inputFilePath: pathLib.path? +): { types.LintViolation } if VERBOSE then - print(`Reading input file '{inputFilePath}'`) + print(`Parsing input {if inputFilePath then `file '{inputFilePath}'` else "string"}`) end - local fileContent = fs.readfiletostring(inputFilePath) + local ast = syntax.parseblock(source) if VERBOSE then - print(`Parsing input file '{inputFilePath}'`) - end - local ast = syntax.parseblock(fileContent) - - if VERBOSE then - print(`Parsing global lint ignores in '{inputFilePath}'`) + print(`Parsing global lint ignores in input {if inputFilePath then `file '{inputFilePath}'` else "string"}`) end local globalIgnores = parseGlobalIgnores(triviaUtils.leftmosttrivia(ast)) @@ -244,6 +244,48 @@ local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule return violations end +local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule }): { types.LintViolation } + if VERBOSE then + print(`Reading input file '{inputFilePath}'`) + end + local fileContent = fs.readfiletostring(inputFilePath) + + return lintString(fileContent, lintRules, inputFilePath) +end + +local function lintPaths( + inputFilePaths: { string }, + lintRules: { types.LintRule } +): { [pathLib.path]: { types.LintViolation } } + if VERBOSE then + print("Filtering input files by gitignore") + end + local inputFiles = files.getSourceFiles(inputFilePaths, VERBOSE) + + local allViolations: { [pathLib.path]: { types.LintViolation } } = {} + + for _, inputPath in inputFiles do + local walker = fs.walk(inputPath, { recursive = true }) + + local curr = walker() + while curr ~= nil do + local inputFilePath = pathLib.parse(curr) + + local success, err = pcall(lintFile, inputFilePath, lintRules) + if success then + -- Violations are returned as the second return value from pcall + allViolations[inputFilePath] = err + else + print(`Error linting file '{inputFilePath}': {err}`) + end + + curr = walker() + end + end + + return allViolations +end + local function main(...: string) local args = cli.parser() @@ -251,7 +293,11 @@ local function main(...: string) args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) args:add("verbose", "flag", { help = "Enable verbose output", aliases = { "v" } }) args:add("json", "flag", { help = "Output lint violations in JSON format", aliases = { "j" } }) - -- TODO: ignore blobs + args:add( + "string-input", + "option", + { help = "Lint the provided string input instead of reading from files", aliases = { "s" } } + ) args:parse({ ... }) @@ -263,16 +309,12 @@ local function main(...: string) VERBOSE = args:has("verbose") local inputFiles = args:forwarded() - if inputFiles == nil or #inputFiles == 0 then - print("Error: No input files specified.\n\n" .. USAGE .. "\nUse --help for more information.") + local stringInput = args:get("string-input") + if (inputFiles == nil or #inputFiles == 0) and not stringInput then + print("Error: No input files or string input specified.\n\n" .. USAGE .. "\nUse --help for more information.") process.exit(1) end - if VERBOSE then - print("Filtering input files by gitignore") - end - local inputFilePaths = files.getSourceFiles(inputFiles, VERBOSE) - local lintRules: { types.LintRule } local rulePath = args:get("rules") if rulePath == nil then @@ -287,41 +329,48 @@ local function main(...: string) lintRules = loadLintRules(rulePath) end - local allViolations: { [pathLib.path]: { types.LintViolation } } = {} + if stringInput ~= nil then + local violations = lintString(stringInput, lintRules) - for _, inputPath in inputFilePaths do - local walker = fs.walk(inputPath, { recursive = true }) + if args:has("json") then + local diagnostics = tableext.map(violations, lsp.diagnostic) + print(json.serialize(diagnostics :: json.array, true)) + elseif #violations > 0 then + if VERBOSE then + print(`Printing violations from string input\n`) + end - local curr = walker() - while curr ~= nil do - local inputFilePath = pathLib.parse(curr) + printer.printLintsWithSource(violations, stringInput) - local success, err = pcall(lintFile, inputFilePath, lintRules) - if success then - -- Violations are returned as the second return value from pcall - allViolations[inputFilePath] = err - else - print(`Error linting file '{inputFilePath}': {err}`) + process.exit(1) + else + if VERBOSE then + print("No lint violations found.") end - curr = walker() + process.exit(0) end - end - - if args:has("json") then - print(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.object, true)) else - if VERBOSE then - print(`Printing violations from {#allViolations} files\n`) - end - local violationsFound = false - for sourcePath, violations in allViolations do - if #violations > 0 then - violationsFound = true - printer.printLints(violations, sourcePath) + local allViolations = lintPaths(inputFiles :: { string }, lintRules) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed + + if args:has("json") then + print(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.object, true)) + else + if VERBOSE then + print(`Printing violations from {#allViolations} files\n`) + end + + local violationsFound = false + + for sourcePath, violations in allViolations do + if #violations > 0 then + violationsFound = true + printer.printLintsWithPath(violations, sourcePath) + end end + + process.exit(if violationsFound then 1 else 0) end - process.exit(if violationsFound then 1 else 0) end end diff --git a/lute/cli/commands/lint/lsp/init.luau b/lute/cli/commands/lint/lsp/init.luau index fc3a16249..5af844410 100644 --- a/lute/cli/commands/lint/lsp/init.luau +++ b/lute/cli/commands/lint/lsp/init.luau @@ -25,7 +25,7 @@ local function tag(t: lintTypes.tag): number end end -local function diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic +function lsp.diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic return table.freeze({ range = table.freeze({ start = table.freeze({ @@ -39,7 +39,7 @@ local function diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic }), severity = severity(lint.severity), code = lint.lintname, - source = "lute lint", -- LUAUFIX: Errors because this isn't inferred as a singleton + source = "lute lint" :: "lute lint", -- LUAUFIX: Annotation needed, otherwise this errors because string isn't inferred as a singleton message = lint.message, tags = if lint.tags then table.freeze(tableext.map(lint.tags, tag)) else nil, }) @@ -51,7 +51,7 @@ local function workspaceDocumentDiagnosticReport( ): lspTypes.WorkspaceDocumentDiagnosticReport local report = { items = {}, uri = pathLib.format(path) } for _, violation in violations do - table.insert(report.items, diagnostic(violation)) + table.insert(report.items, lsp.diagnostic(violation)) end return table.freeze(report) end diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau index 922033102..10ad98804 100644 --- a/lute/cli/commands/lint/printer.luau +++ b/lute/cli/commands/lint/printer.luau @@ -11,7 +11,7 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () print(`{lint.severity}[{lint.lintname}]: {lint.message}\n`) local location = lint.location - local filepath = path.format(lint.sourcepath) + local filepath = if lint.sourcepath then path.format(lint.sourcepath) else "input" if location.beginline == location.endline then local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number @@ -77,7 +77,7 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () end end -local function printLints(lints: { types.LintViolation }, sourceContent: string): () +function printerLib.printLintsWithSource(lints: { types.LintViolation }, sourceContent: string): () local sourceLines = sourceContent:split("\n") for _, lint in lints do @@ -85,8 +85,8 @@ local function printLints(lints: { types.LintViolation }, sourceContent: string) end end -function printerLib.printLints(lints: { types.LintViolation }, sourcePath: path.path): () - printLints(lints, fs.readfiletostring(sourcePath)) +function printerLib.printLintsWithPath(lints: { types.LintViolation }, sourcePath: path.path): () + printerLib.printLintsWithSource(lints, fs.readfiletostring(sourcePath)) end return table.freeze(printerLib) diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index c9342de98..d45b22630 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -57,7 +57,7 @@ end -- Report instances of attempted swaps like: -- a = b; b = a -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } local violations = {} local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau index dbf67e5d1..f4c7bbdcf 100644 --- a/lute/cli/commands/lint/rules/divide_by_zero.luau +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -7,7 +7,7 @@ local utils = require("@std/syntax/utils") local name = "divide_by_zero" local message = "Division by zero detected." -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } return query .findallfromroot(ast, utils.isExprBinary) :filter(function(bin) diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index c3601ab65..b177c4aff 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -9,12 +9,12 @@ export type LintViolation = { read location: syntax.span, read message: string, read severity: severity, - read sourcepath: path.path, + read sourcepath: path.path?, read tags: { tag }?, } -- TODO: add suggested fix export type LintRule = { - read lint: (ast: syntax.AstStatBlock, sourcepath: path.path) -> { LintViolation }, + read lint: (ast: syntax.AstStatBlock, sourcepath: path.path?) -> { LintViolation }, read name: string, } diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index f03f427c7..b49039fad 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -46,7 +46,7 @@ test.suite("lute lint", function(suite) local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "Error: No input files specified.") + assert.strcontains(result.stdout, "Error: No input files or string input specified.") end) suite:case("lute lint error code 0 with no violations", function(assert) @@ -1069,6 +1069,93 @@ violator.luau:5:12-17 ── fs.remove(violatorPath) end) + suite:case("lute lint string input", function(assert) + local inputString = [[ +a = b +b = a +]] + + -- Do + -- Run the linter on string input + local result = process.run({ lutePath, "lint", "-s", inputString }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + local expected = [[ + ┌── input:1:1-2:6 ── + │ + 1 │ ╭ a = b + 2 │ │ b = a + │ ╰─────^ + │ +]] + assert.strcontains(result.stdout, expected) + end) + + suite:case("lute lint string input verbose", function(assert) + local inputString = "local x = 1 / 0" + + -- Do + -- Run the linter on string input with verbose flag + local result = process.run({ lutePath, "lint", "-v", "-s", inputString }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains(result.stdout, "Using default lint rules.") + assert.strcontains(result.stdout, "Parsing input string") + assert.strcontains(result.stdout, "Parsing global lint ignores in input string") + assert.strcontains(result.stdout, "Applying lint rules") + assert.strcontains(result.stdout, "Printing violations from string input") + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + end) + + suite:case("lute lint string input with no violations", function(assert) + local inputString = "local x = 1 + 2" + + -- Do + -- Run the linter on string input + local result = process.run({ lutePath, "lint", "-v", "-s", inputString }) + + -- Check + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, "No lint violations found.") + end) + + suite:case("lute lint string input json output", function(assert) + local inputString = "local x = 1 / 0" + + -- Do + -- Run the linter on string input with json flag + local result = process.run({ lutePath, "lint", "-j", "-s", inputString }) + + -- Check + assert.eq(result.exitcode, 0) + + local expected = [=[ +[ + { + "message": "Division by zero detected.", + "source": "lute lint", + "code": "divide_by_zero", + "severity": 2, + "range": { + "start": { + "character": 10, + "line": 0 + }, + "end": { + "character": 15, + "line": 0 + } + } + } +]]=] + assert.strcontains(result.stdout, expected) + end) + suite:case("lute lint ignore (table item)", function(assert) local violatorPath = path.format(path.join(tmpDir, "violator.luau")) fs.writestringtofile( From f08bc5d6c518fa720a6f0607229449dc6cb628f7 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 13 Jan 2026 11:47:16 -0800 Subject: [PATCH 270/642] Adds any and all (for array-like tables) to tableext (#720) I had a need for `any` when working on something else, and added `all` along the way --- lute/std/libs/tableext.luau | 18 +++++++++++++++++ tests/std/tableext.test.luau | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau index acd16ff99..6f191c6b6 100644 --- a/lute/std/libs/tableext.luau +++ b/lute/std/libs/tableext.luau @@ -27,6 +27,24 @@ function tableext.filter(table: { [K]: V }, predicate: (V) -> boolean): { return new end +function tableext.any(arr: { T }, predicate: (T) -> boolean): boolean + for _, v in arr do + if predicate(v) then + return true + end + end + return false +end + +function tableext.all(arr: { T }, predicate: (T) -> boolean): boolean + for _, v in arr do + if not predicate(v) then + return false + end + end + return true +end + function tableext.extend(tbl: { T }, ...: { T }) local extensions = table.pack(...) extensions.n = nil :: any diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index 8e39bb363..8cf38a31c 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -97,4 +97,42 @@ test.suite("TableExt", function(suite) assert.eq(#tableext.keys(setB), 3) assert.neq(#tableext.keys(setB), #b) end) + + suite:case("any", function(assert) + local a = { 1, 2, 3, 4, 5 } + local hasEven = tableext.any(a, function(v: number) + return v % 2 == 0 + end) + assert.eq(hasEven, true) + + local hasGreaterThanFive = tableext.any(a, function(v: number) + return v > 5 + end) + assert.eq(hasGreaterThanFive, false) + + a = {} + local anyInEmpty = tableext.any(a, function(v: number) + return true + end) + assert.eq(anyInEmpty, false) + end) + + suite:case("all", function(assert) + local a = { 2, 4, 6, 8 } + local allEven = tableext.all(a, function(v: number) + return v % 2 == 0 + end) + assert.eq(allEven, true) + + local allLessThanEight = tableext.all(a, function(v: number) + return v < 8 + end) + assert.eq(allLessThanEight, false) + + a = {} + local allInEmpty = tableext.all(a, function(v: number) + return false + end) + assert.eq(allInEmpty, true) + end) end) From a15af345458a53f8fbd46582a7048fdf5a609021 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 13 Jan 2026 13:42:08 -0800 Subject: [PATCH 271/642] Adds suggested fix as a lint violation field (#721) This lays the groundwork for the autofix work, and means that normal `lute lint` invocations can function as dry runs. --- lute/cli/commands/lint/printer.luau | 4 ++++ .../commands/lint/rules/almost_swapped.luau | 13 +++++++++++-- lute/cli/commands/lint/types.luau | 6 +++++- tests/cli/lint.test.luau | 18 +++++++++++++++++- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau index 10ad98804..831cdf423 100644 --- a/lute/cli/commands/lint/printer.luau +++ b/lute/cli/commands/lint/printer.luau @@ -75,6 +75,10 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () print(`{blankGutter}│`) print() end + + if lint.suggestedfix then + print(`Suggested fix: {lint.suggestedfix.fix}\n`) + end end function printerLib.printLintsWithSource(lints: { types.LintViolation }, sourceContent: string): () diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index d45b22630..5e2ffc60b 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -1,7 +1,9 @@ local lintTypes = require("../types") local path = require("@std/path") local query = require("@std/syntax/query") +local stringext = require("@std/stringext") local syntax = require("@std/syntax") +local printer = require("@std/syntax/printer") local utils = require("@std/syntax/utils") local name = "almost_swapped" @@ -78,7 +80,11 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp local nextVar, nextVal = nextStat.variables[1].node, nextStat.values[1].node if compFuncs.refExprsSame(currVar, nextVal) and compFuncs.refExprsSame(nextVar, currVal) then - table.insert(violations, { -- LUAUFIX: severity isn't inferred as a singleton, so table.insert is mad + local currVarStr = stringext.trim(printer.printnode(currVar)) + local currValStr = stringext.trim(printer.printnode(currVal)) + local suggestedFix = `{currVarStr}, {currValStr} = {currValStr}, {currVarStr}` + + table.insert(violations, { lintname = name, location = syntax.span.create({ beginline = currStat.location.beginline, @@ -87,8 +93,11 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp endcolumn = nextStat.location.endcolumn, }), message = message, - severity = "warning", + severity = "warning" :: "warning", -- LUAUFIX: cast needed because severity isn't inferred as a singleton sourcepath = sourcepath, + suggestedfix = { + fix = suggestedFix, + }, }) end end diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index b177c4aff..0fb1872c9 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -10,8 +10,12 @@ export type LintViolation = { read message: string, read severity: severity, read sourcepath: path.path?, + read suggestedfix: { + read location: syntax.span?, -- if nil, applies to whole violation location + read fix: string, + }?, read tags: { tag }?, -} -- TODO: add suggested fix +} export type LintRule = { read lint: (ast: syntax.AstStatBlock, sourcepath: path.path?) -> { LintViolation }, diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index b49039fad..f9011f018 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -180,6 +180,10 @@ violator.luau:1:1-2:6 ── ]] assert.strcontains(result.stdout, expected) + expected = "Suggested fix: a, b = b, a" + + assert.strcontains(result.stdout, expected) + expected = [[ violator.luau:6:1-7:6 ── │ @@ -190,6 +194,10 @@ violator.luau:6:1-7:6 ── ]] assert.strcontains(result.stdout, expected) + expected = "Suggested fix: x, y = y, x" + + assert.strcontains(result.stdout, expected) + expected = [[ violator.luau:10:1-11:10 ── │ @@ -200,6 +208,10 @@ violator.luau:10:1-11:10 ── ]] assert.strcontains(result.stdout, expected) + expected = "Suggested fix: t.a, t.b = t.b, t.a" + + assert.strcontains(result.stdout, expected) + expected = [[ violator.luau:14:2-15:17 ── │ @@ -207,7 +219,11 @@ violator.luau:14:2-15:17 ── 15 │ │ t["b"] = t["a"] │ ╰───────────────────^ │ -]] -- todo: address tab shenanigans +]] + assert.strcontains(result.stdout, expected) + + expected = 'Suggested fix: t["a"], t["b"] = t["b"], t["a"]' + assert.strcontains(result.stdout, expected) -- Teardown From 27ddf0a20efd9bcab25507451225b401211238f4 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 13 Jan 2026 15:01:39 -0800 Subject: [PATCH 272/642] Adds a default lint rule for parenthesized conditions (#722) New lint rule! It's also useful for a test case I want to add for the autofix stuff --- lute/cli/commands/lint/init.luau | 2 +- .../commands/lint/rules/divide_by_zero.luau | 2 +- .../lint/rules/parenthesized_conditions.luau | 71 ++++++++++ tests/cli/lint.test.luau | 126 ++++++++++++++++-- 4 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 lute/cli/commands/lint/rules/parenthesized_conditions.luau diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index d1c31cfbb..2c2191bd8 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -13,7 +13,7 @@ local triviaUtils = require("@std/syntax/utils/trivia") local types = require("@self/types") local visitor = require("@std/syntax/visitor") -local DEFAULT_RULES = { "almost_swapped", "divide_by_zero" } +local DEFAULT_RULES = { "almost_swapped", "divide_by_zero", "parenthesized_conditions" } local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" local VERBOSE = false diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau index f4c7bbdcf..a3a34f853 100644 --- a/lute/cli/commands/lint/rules/divide_by_zero.luau +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -19,7 +19,7 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp :maptoarray( function( n: syntax.AstExprBinary - ): lintTypes.LintViolation -- LUAUFIX: Bidiretional inference of generics should let us not need this annotation + ): lintTypes.LintViolation -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation return { lintname = name, location = n.location, diff --git a/lute/cli/commands/lint/rules/parenthesized_conditions.luau b/lute/cli/commands/lint/rules/parenthesized_conditions.luau new file mode 100644 index 000000000..3453f6f38 --- /dev/null +++ b/lute/cli/commands/lint/rules/parenthesized_conditions.luau @@ -0,0 +1,71 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") + +local name = "parenthesized_conditions" +local message = "Luau doesn't require parentheses around conditions. You can remove them for readability." + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } + local violations: { lintTypes.LintViolation } = {} + + query + .findallfromroot(ast, function(node: syntax.AstNode): (syntax.AstExprIfElse | syntax.AstStatIf)? + if (node.kind == "expr" or node.kind == "stat") and node.tag == "conditional" then + return node + end + return nil + end) + :foreach(function(ifStat) + if ifStat.condition.tag == "group" then + table.insert(violations, { + lintname = name, + location = ifStat.condition.location, + message = message, + severity = "info", + sourcepath = sourcepath, + }) + end + + for _, elseIfStat in ifStat.elseifs do + if elseIfStat.condition.tag == "group" then + table.insert(violations, { + lintname = name, + location = elseIfStat.condition.location, + message = message, + severity = "info", + sourcepath = sourcepath, + }) + end + end + end) + + query + .findallfromroot(ast, function(node: syntax.AstNode): (syntax.AstStatWhile | syntax.AstStatRepeat)? + if node.kind == "stat" and (node.tag == "while" or node.tag == "repeat") then + return node + end + return nil + end) + :filter(function(n) + return n.condition.tag == "group" + end) + :foreach(function(n) + table.insert(violations, { + lintname = name, + location = n.condition.location, + message = message, + severity = "info", + sourcepath = sourcepath, + }) + end) + + return violations +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index f9011f018..1caf00bb9 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -7,8 +7,10 @@ local test = require("@std/test") local lutePath = path.format(process.execpath()) local tmpDir = system.tmpdir() local rulesDir = path.format(path.join(".", "lints")) -local almostSwappedExample = path.format(path.join("lute", "cli", "commands", "lint", "rules", "almost_swapped.luau")) -local divideByZeroExample = path.format(path.join("lute", "cli", "commands", "lint", "rules", "divide_by_zero.luau")) +local almostSwappedRule = path.format(path.join("lute", "cli", "commands", "lint", "rules", "almost_swapped.luau")) +local divideByZeroRule = path.format(path.join("lute", "cli", "commands", "lint", "rules", "divide_by_zero.luau")) +local parenthesizedConditionsRule = + path.format(path.join("lute", "cli", "commands", "lint", "rules", "parenthesized_conditions.luau")) local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" @@ -117,7 +119,7 @@ local x = 1 / 0 -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -164,7 +166,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", almostSwappedExample, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", almostSwappedRule, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -582,7 +584,7 @@ local z = 1 / 0 -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -630,7 +632,7 @@ local z = 1 / 0 -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -676,7 +678,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -713,7 +715,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -750,7 +752,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -788,7 +790,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroExample, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -1193,4 +1195,108 @@ local z = 5 / 0 fs.remove(violatorPath) end) + + suite:case("parenthesized_conditions", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +if (3 > 0) then + print("Hello, world!") +elseif (false) then + print("This won't print.") +end + +local x = if (2 < 1) then 10 elseif (true) then 3 else 20 + +while ("hello" == "world") do + print("Nope.") +end + +repeat + print("Still nope.") +until (5 == 5) + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", parenthesizedConditionsRule, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "info[parenthesized_conditions]: Luau doesn't require parentheses around conditions. You can remove them for readability.", + nil, + 6 + ) + + local expected = [[ +violator.luau:1:4-11 ── + │ + 1 │ if (3 > 0) then + │ ^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:3:8-15 ── + │ + 3 │ elseif (false) then + │ ^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Have to do this here because the path is a lot shorter on Linux, so there's more dashes to match the length of the line + expected = [[ +violator.luau:7:14-21 ──]] + assert.strcontains(result.stdout, expected) + + expected = [[ + │ + 7 │ local x = if (2 < 1) then 10 elseif (true) then 3 else 20 + │ ^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:7:37-43 ──]] + assert.strcontains(result.stdout, expected) + + expected = [[ + │ + 7 │ local x = if (2 < 1) then 10 elseif (true) then 3 else 20 + │ ^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:9:7-27 ── + │ + 9 │ while ("hello" == "world") do + │ ^^^^^^^^^^^^^^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:15:7-15 ── + │ + 15 │ until (5 == 5) + │ ^^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) end) From 923f0e89a82372778dec8fd68a8ba758ae345d22 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 14 Jan 2026 11:31:26 -0800 Subject: [PATCH 273/642] Adds autofix support to `lute lint` (#723) Adds support for autofixing reported lint violations (if the lint rules provide suggested fixes). The core logic for applying the fixes is pretty simple: split the content by line and do the replacements to the relevant lines. For now, we don't handle the cases where suggested fixes overlap (although my approach would probably be to just ignore fixes that overlap with previously applied fixes). We rerun the lints after applying fixes and report any violations that are still left. It's possible that those reported violations have suggested fixes, but we ignore them to prevent the chance of looping between suggested fixes infinitely. --- lute/cli/commands/lint/init.luau | 106 ++++++++++++- .../lint/rules/parenthesized_conditions.luau | 10 ++ tests/cli/lint.test.luau | 148 ++++++++++++++++++ 3 files changed, 259 insertions(+), 5 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 2c2191bd8..5a6c109c8 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -31,6 +31,8 @@ OPTIONS: as individual lint rules. If unspecified, the default lint rules are used. -j, --json Output lint violations in JSON format matching the LSP diagnostic spec. -s, --string-input Lint the provided string input instead of reading from files. + --auto-fix Automatically apply fixes for lint violations that provide a suggested fix. + Assumes that suggested fixes do not overlap. PATHS: Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. @@ -244,18 +246,98 @@ local function lintString( return violations end -local function lintFile(inputFilePath: pathLib.path, lintRules: { types.LintRule }): { types.LintViolation } +type fix = { + location: syntax.span, + replacement: string, +} + +local function applySuggestedFixes(source: string, fixes: { fix }): string + local lines = source:split("\n") + + -- Sort fixes by starting location descending to avoid messing up locations of earlier fixes + table.sort(fixes, function(a: fix, b: fix) + return not (a.location < b.location) + end) + + for _, fix in fixes do + local beginLine = fix.location.beginline + local endLine = fix.location.endline + local beginColumn = fix.location.begincolumn + local endColumn = fix.location.endcolumn + + if beginLine == endLine then + -- Single-line fix + local line = lines[beginLine] + lines[beginLine] = line:sub(1, beginColumn - 1) .. fix.replacement .. line:sub(endColumn) + else + -- Multi-line fix + local firstLine = lines[beginLine] + local lastLine = lines[endLine] + + lines[beginLine] = firstLine:sub(1, beginColumn - 1) .. fix.replacement .. lastLine:sub(endColumn) + + -- Remove the lines that were replaced + for i = beginLine + 1, endLine do + lines[i] = nil -- this is cheaper than doing table.remove, but we need to handle the resulting holes later + end + end + end + + local newLines: { string } = {} + for _, line in lines do -- we rely on the assumption that we'll see the entries in the same order they were created in the array + table.insert(newLines, line) + end + + return table.concat(newLines, "\n") +end + +local function lintFile( + inputFilePath: pathLib.path, + lintRules: { types.LintRule }, + autofixEnabled: boolean +): { types.LintViolation } if VERBOSE then print(`Reading input file '{inputFilePath}'`) end local fileContent = fs.readfiletostring(inputFilePath) - return lintString(fileContent, lintRules, inputFilePath) + local violations = lintString(fileContent, lintRules, inputFilePath) + + if not autofixEnabled then + return violations + end + + local hasSuggestedFixes = tableext.any(violations, function(violation) + return violation.suggestedfix ~= nil + end) + + if not hasSuggestedFixes then + return violations + end + + if VERBOSE then + print(`Applying suggested fixes to file '{inputFilePath}'`) + end + + local fixes: { fix } = {} + for _, violation in violations do + if violation.suggestedfix ~= nil then + local fixLocation = violation.suggestedfix.location or violation.location + table.insert(fixes, { location = fixLocation, replacement = violation.suggestedfix.fix }) + end + end + + local newContent = applySuggestedFixes(fileContent, fixes) + + fs.writestringtofile(inputFilePath, newContent) + + return lintString(newContent, lintRules, inputFilePath) end local function lintPaths( inputFilePaths: { string }, - lintRules: { types.LintRule } + lintRules: { types.LintRule }, + autofixEnabled: boolean ): { [pathLib.path]: { types.LintViolation } } if VERBOSE then print("Filtering input files by gitignore") @@ -271,7 +353,7 @@ local function lintPaths( while curr ~= nil do local inputFilePath = pathLib.parse(curr) - local success, err = pcall(lintFile, inputFilePath, lintRules) + local success, err = pcall(lintFile, inputFilePath, lintRules, autofixEnabled) if success then -- Violations are returned as the second return value from pcall allViolations[inputFilePath] = err @@ -298,6 +380,11 @@ local function main(...: string) "option", { help = "Lint the provided string input instead of reading from files", aliases = { "s" } } ) + args:add( + "auto-fix", + "flag", + { help = "Automatically apply fixes for lint violations that provide a suggested fix" } + ) args:parse({ ... }) @@ -330,6 +417,10 @@ local function main(...: string) end if stringInput ~= nil then + if args:has("auto-fix") then + print("Auto-fix has no effect on string input.") + end + local violations = lintString(stringInput, lintRules) if args:has("json") then @@ -351,7 +442,12 @@ local function main(...: string) process.exit(0) end else - local allViolations = lintPaths(inputFiles :: { string }, lintRules) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed + local autofixEnabled = args:has("auto-fix") + if autofixEnabled and VERBOSE then + print("Auto-fix is enabled.") + end + + local allViolations = lintPaths(inputFiles :: { string }, lintRules, autofixEnabled) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed if args:has("json") then print(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.object, true)) diff --git a/lute/cli/commands/lint/rules/parenthesized_conditions.luau b/lute/cli/commands/lint/rules/parenthesized_conditions.luau index 3453f6f38..22b916b69 100644 --- a/lute/cli/commands/lint/rules/parenthesized_conditions.luau +++ b/lute/cli/commands/lint/rules/parenthesized_conditions.luau @@ -2,6 +2,7 @@ local lintTypes = require("../types") local path = require("@std/path") local query = require("@std/syntax/query") local syntax = require("@std/syntax") +local syntaxPrinter = require("@std/syntax/printer") local name = "parenthesized_conditions" local message = "Luau doesn't require parentheses around conditions. You can remove them for readability." @@ -24,6 +25,9 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp message = message, severity = "info", sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printnode(ifStat.condition.expression), + }, }) end @@ -35,6 +39,9 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp message = message, severity = "info", sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printnode(elseIfStat.condition.expression), + }, }) end end @@ -57,6 +64,9 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp message = message, severity = "info", sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printnode((n.condition :: syntax.AstExprGroup).expression), + }, }) end) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 1caf00bb9..b39aa7191 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -232,6 +232,60 @@ violator.luau:14:2-15:17 ── fs.remove(violatorPath) end) + suite:case("almost swapped auto-fix", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +a = b +b = a + +local x = 0 +local y = "hello" +x = y +y = x + +local t = { a = 1, b = 2 } +t.a = t.b +t.b = t.a + +if true then + t["a"] = t["b"] + t["b"] = t["a"] +end +]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "--auto-fix", violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) -- After applying the auto-fix, there are no more violations + + local fixedContents = fs.readfiletostring(violatorPath) + local expectedFixedContents = [[ +a, b = b, a + +local x = 0 +local y = "hello" +x, y = y, x + +local t = { a = 1, b = 2 } +t.a, t.b = t.b, t.a + +if true then + t["a"], t["b"] = t["b"], t["a"] +end +]] + assert.eq(fixedContents, expectedFixedContents) + + -- Teardown + fs.remove(violatorPath) + end) + suite:case("lute lint multiple rules", function(assert) -- Setup fs.createdirectory(rulesDir) @@ -1299,4 +1353,98 @@ violator.luau:15:7-15 ── -- Teardown fs.remove(violatorPath) end) + + suite:case("parenthesized_conditions auto-fix", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +if (3 > 0) then + print("Hello, world!") +elseif (false) then + print("This won't print.") +end + +local x = if (2 < 1) then 10 elseif (true) then 3 else 20 + +while ("hello" == "world") do + print("Nope.") +end + +repeat + print("Still nope.") +until (5 == 5) +]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "--auto-fix", "-r", parenthesizedConditionsRule, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + local fixedContents = fs.readfiletostring(violatorPath) + local expectedFixedContents = [[ +if 3 > 0 then + print("Hello, world!") +elseif false then + print("This won't print.") +end + +local x = if 2 < 1 then 10 elseif true then 3 else 20 + +while "hello" == "world" do + print("Nope.") +end + +repeat + print("Still nope.") +until 5 == 5 +]] + + assert.eq(fixedContents, expectedFixedContents) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("report violations remaining after auto-fix", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +a = b +b = a + +local x = 3 / 0 +]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "--auto-fix", violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) -- After applying the auto-fix, there is still a violation + + local fixedContents = fs.readfiletostring(violatorPath) + local expectedFixedContents = [[ +a, b = b, a + +local x = 3 / 0 +]] + assert.eq(fixedContents, expectedFixedContents) + + assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + + assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + + -- Teardown + fs.remove(violatorPath) + end) end) From 3ad0e7191d4dbe82e6d1e9f2ac5431198e37d7f2 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:39:15 -0800 Subject: [PATCH 274/642] Ensures `fs.remove` is asynchronous (#729) Continuation of https://github.com/luau-lang/lute/pull/661 and one part of making file system APIs asynchronous (https://github.com/luau-lang/lute/issues/709) --- lute/fs/include/lute/fs.h | 4 ++-- lute/fs/src/fs.cpp | 19 ++++++++++++------- lute/fs/src/fs_impl.cpp | 30 ++++++++++++++++++++++++++++++ lute/fs/src/fs_impl.h | 1 + 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/lute/fs/include/lute/fs.h b/lute/fs/include/lute/fs.h index c20698301..8e57c1de1 100644 --- a/lute/fs/include/lute/fs.h +++ b/lute/fs/include/lute/fs.h @@ -35,7 +35,7 @@ int writestringtofile(lua_State* L); int readasync(lua_State* L); /* Removes a file */ -int fs_remove(lua_State* L); +int remove(lua_State* L); /* Creates a folder */ int fs_mkdir(lua_State* L); @@ -74,7 +74,7 @@ static const luaL_Reg lib[] = { {"write", write}, {"close", close}, - {"remove", fs_remove}, + {"remove", remove}, {"stat", fs_stat}, {"exists", fs_exists}, diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index e3d31b11c..867a4a838 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -230,16 +230,21 @@ int open(lua_State* L) return open_impl(L, path, openFlags, *modeFlags); } -int fs_remove(lua_State* L) +int remove(lua_State* L) { - uv_fs_t unlink_req; - int err = uv_fs_unlink(uv_default_loop(), &unlink_req, luaL_checkstring(L, 1), nullptr); - uv_fs_req_cleanup(&unlink_req); + int nArgs = lua_gettop(L); + if (nArgs < 1) + { + luaL_errorL(L, "Error: no file supplied\n"); + } - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); + if (nArgs > 1) + { + luaL_errorL(L, "Error: too many arguments supplied\n"); + } + const char* path = luaL_checkstring(L, 1); - return 0; + return remove_impl(L, path); } int fs_mkdir(lua_State* L) diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp index 0d84722bf..b63dc5315 100644 --- a/lute/fs/src/fs_impl.cpp +++ b/lute/fs/src/fs_impl.cpp @@ -237,4 +237,34 @@ int close_impl(lua_State* L, UVFile* handle) return lua_yield(L, 0); } +int remove_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_unlink( + uv_default_loop(), + &req->req, + path, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("Error removing file %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); + +} + } // namespace fs diff --git a/lute/fs/src/fs_impl.h b/lute/fs/src/fs_impl.h index 2d07a657f..f19c5d9b6 100644 --- a/lute/fs/src/fs_impl.h +++ b/lute/fs/src/fs_impl.h @@ -17,4 +17,5 @@ int open_impl(lua_State* L, const char* path, int flags, int mode); int read_impl(lua_State* L, UVFile* handle); int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numBytes); int close_impl(lua_State* L, UVFile* handle); +int remove_impl(lua_State* L, const char* path); } // namespace fs From 012552626d2730f8cd1424ed949ec11f4dfacf2d Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:21:29 -0800 Subject: [PATCH 275/642] Ensures `fs.mkdir` is asynchronous (#731) Addresses #709's journey of making our file system APIs asynchronous --- lute/fs/include/lute/fs.h | 4 ++-- lute/fs/src/fs.cpp | 21 ++++++++++++--------- lute/fs/src/fs_impl.cpp | 29 +++++++++++++++++++++++++++++ lute/fs/src/fs_impl.h | 1 + 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/lute/fs/include/lute/fs.h b/lute/fs/include/lute/fs.h index 8e57c1de1..ae269fb65 100644 --- a/lute/fs/include/lute/fs.h +++ b/lute/fs/include/lute/fs.h @@ -38,7 +38,7 @@ int readasync(lua_State* L); int remove(lua_State* L); /* Creates a folder */ -int fs_mkdir(lua_State* L); +int mkdir(lua_State* L); /* Removes a directory */ int fs_rmdir(lua_State* L); @@ -85,7 +85,7 @@ static const luaL_Reg lib[] = { {"symlink", fs_symlink}, {"copy", fs_copy}, - {"mkdir", fs_mkdir}, + {"mkdir", mkdir}, {"listdir", listdir}, {"rmdir", fs_rmdir}, diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 867a4a838..cdf1f72c7 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -247,19 +247,22 @@ int remove(lua_State* L) return remove_impl(L, path); } -int fs_mkdir(lua_State* L) +int mkdir(lua_State* L) { + int nArgs = lua_gettop(L); + if (nArgs < 1) + { + luaL_errorL(L, "Error: no path supplied\n"); + } + + if (nArgs > 2) + { + luaL_errorL(L, "Error: too many arguments supplied\n"); + } const char* path = luaL_checkstring(L, 1); int mode = luaL_optinteger(L, 2, 0777); - uv_fs_t req; - int err = uv_fs_mkdir(uv_default_loop(), &req, path, mode, nullptr); - uv_fs_req_cleanup(&req); - - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); - - return 0; + return mkdir_impl(L, path, mode); } int fs_rmdir(lua_State* L) diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp index b63dc5315..3d64b105e 100644 --- a/lute/fs/src/fs_impl.cpp +++ b/lute/fs/src/fs_impl.cpp @@ -264,7 +264,36 @@ int remove_impl(lua_State* L, const char* path) ); return lua_yield(L, 0); +} + +int mkdir_impl(lua_State* L, const char* path, int mode) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_mkdir( + uv_default_loop(), + &req->req, + path, + mode, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("Error creating directory %s: %s", req->path, uv_strerror(result)); + return; + } + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); } } // namespace fs diff --git a/lute/fs/src/fs_impl.h b/lute/fs/src/fs_impl.h index f19c5d9b6..c0360907a 100644 --- a/lute/fs/src/fs_impl.h +++ b/lute/fs/src/fs_impl.h @@ -18,4 +18,5 @@ int read_impl(lua_State* L, UVFile* handle); int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numBytes); int close_impl(lua_State* L, UVFile* handle); int remove_impl(lua_State* L, const char* path); +int mkdir_impl(lua_State* L, const char* path, int mode); } // namespace fs From 77ebef5708b879c9798a1c37a2b3e86c4db16c12 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 15 Jan 2026 17:02:02 -0800 Subject: [PATCH 276/642] Adds a readme to lute lint folder (#733) Co-authored-by: ariel --- lute/cli/commands/lint/README.MD | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 lute/cli/commands/lint/README.MD diff --git a/lute/cli/commands/lint/README.MD b/lute/cli/commands/lint/README.MD new file mode 100644 index 000000000..c661396b9 --- /dev/null +++ b/lute/cli/commands/lint/README.MD @@ -0,0 +1,17 @@ +# Lute Lint + +`lute lint` is a programmable linter for Luau code, shipped as part of Lute. +As a linter, it works to statically analyze the user's code to warn them about common pitfalls they may be falling into, or to nudge them away from discouraged coding practices. +It is _programmable_ meaning that you can write a new lint rule for your Luau code _in Luau_. +It's also built on top of the official Luau language stack, allowing it to leverage the same parser used by Luau and Roblox, unlike third-party linters that rely on separate, custom parser implementations. + +## Usage + +`lute lint` can be invoked by calling `lute lint ` or `lute lint ` (use `lute lint --help` for more uses). +`lute lint` comes with a set of default lint rules which will run automatically when it is called. +This collection of builtin default lint rules is under active development. +We're also working on [Mandolin](https://github.com/luau-lang/mandolin), a VSCode extension to surface lint violations in the editor, rather than just on the command line. + +## Contact + +Both `lute lint` and Mandolin are under active development. If you run into any problems or have any questions, feel free to reach out to skanosue@roblox.com or the rest of the Luau team! From 43eba877787c4a71bf866fa204d6e31e5aa60bc9 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 16 Jan 2026 11:25:55 -0800 Subject: [PATCH 277/642] Adds a lint rule for constant table comparison (#732) Port of [this Selene rule](https://kampfkarren.github.io/selene/lints/constant_table_comparison.html) to lute lint --- lute/cli/commands/lint/init.luau | 2 +- .../lint/rules/constant_table_comparison.luau | 75 ++++++++ tests/cli/lint.test.luau | 165 ++++++++++++++++-- 3 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 lute/cli/commands/lint/rules/constant_table_comparison.luau diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 5a6c109c8..d0be255b4 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -13,7 +13,7 @@ local triviaUtils = require("@std/syntax/utils/trivia") local types = require("@self/types") local visitor = require("@std/syntax/visitor") -local DEFAULT_RULES = { "almost_swapped", "divide_by_zero", "parenthesized_conditions" } +local DEFAULT_RULES = { "almost_swapped", "constant_table_comparison", "divide_by_zero", "parenthesized_conditions" } local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" local VERBOSE = false diff --git a/lute/cli/commands/lint/rules/constant_table_comparison.luau b/lute/cli/commands/lint/rules/constant_table_comparison.luau new file mode 100644 index 000000000..c7bf74b5b --- /dev/null +++ b/lute/cli/commands/lint/rules/constant_table_comparison.luau @@ -0,0 +1,75 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local stringext = require("@std/stringext") +local syntax = require("@std/syntax") +local syntaxPrinter = require("@std/syntax/printer") +local utils = require("@std/syntax/utils") + +local name = "constant_table_comparison" +local message = + "Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false." + +local function isTableLiteral(expr: syntax.AstExpr): boolean + return expr.tag == "table" + or (expr.tag == "group" and isTableLiteral(expr.expression)) + or (expr.tag == "cast" and isTableLiteral(expr.operand)) +end + +local function isEmptyTableLiteral(expr: syntax.AstExpr): boolean + if expr.tag == "table" then + return #expr.entries == 0 + elseif expr.tag == "group" then + return isEmptyTableLiteral(expr.expression) + elseif expr.tag == "cast" then + return isEmptyTableLiteral(expr.operand) + else + return false + end +end + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } + return query + .findallfromroot(ast, utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "==" or bin.operator.text == "~=" + end) + :filter(function(bin) + return isTableLiteral(bin.rhsoperand) or isTableLiteral(bin.lhsoperand) + end) + :maptoarray( + function( + bin: syntax.AstExprBinary + ): lintTypes.LintViolation -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation + local suggestedFix: string? = nil + + if isEmptyTableLiteral(bin.lhsoperand) then + suggestedFix = + `next({stringext.trim(syntaxPrinter.printnode(bin.rhsoperand))}) {bin.operator.text} nil` + elseif isEmptyTableLiteral(bin.rhsoperand) then + suggestedFix = + `next({stringext.trim(syntaxPrinter.printnode(bin.lhsoperand))}) {bin.operator.text} nil` + end + + return { + lintname = name, + location = bin.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + suggestedfix = if suggestedFix ~= nil + then { + fix = suggestedFix, + } + else nil, + } + end + ) +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index b39aa7191..447eade37 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -7,10 +7,13 @@ local test = require("@std/test") local lutePath = path.format(process.execpath()) local tmpDir = system.tmpdir() local rulesDir = path.format(path.join(".", "lints")) -local almostSwappedRule = path.format(path.join("lute", "cli", "commands", "lint", "rules", "almost_swapped.luau")) -local divideByZeroRule = path.format(path.join("lute", "cli", "commands", "lint", "rules", "divide_by_zero.luau")) -local parenthesizedConditionsRule = - path.format(path.join("lute", "cli", "commands", "lint", "rules", "parenthesized_conditions.luau")) +local defaultRulesFolder = path.format(path.join("lute", "cli", "commands", "lint", "rules")) +local defaultRulesPaths = { + almost_swapped = path.format(path.join(defaultRulesFolder, "almost_swapped.luau")), + constant_table_comparison = path.format(path.join(defaultRulesFolder, "constant_table_comparison.luau")), + divide_by_zero = path.format(path.join(defaultRulesFolder, "divide_by_zero.luau")), + parenthesized_conditions = path.format(path.join(defaultRulesFolder, "parenthesized_conditions.luau")), +} local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" @@ -119,7 +122,7 @@ local x = 1 / 0 -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -166,7 +169,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", almostSwappedRule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.almost_swapped, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -638,7 +641,7 @@ local z = 1 / 0 -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -686,7 +689,7 @@ local z = 1 / 0 -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -732,7 +735,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -769,7 +772,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -806,7 +809,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -844,7 +847,7 @@ end -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", divideByZeroRule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -1277,7 +1280,7 @@ until (5 == 5) -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", parenthesizedConditionsRule, violatorPath }) + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.parenthesized_conditions, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -1381,7 +1384,14 @@ until (5 == 5) -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "--auto-fix", "-r", parenthesizedConditionsRule, violatorPath }) + local result = process.run({ + lutePath, + "lint", + "--auto-fix", + "-r", + defaultRulesPaths.parenthesized_conditions, + violatorPath, + }) -- Check assert.eq(result.exitcode, 0) @@ -1447,4 +1457,131 @@ local x = 3 / 0 -- Teardown fs.remove(violatorPath) end) + + suite:case("constant_table_comparison", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local a = { 1, 2, 3 } == t +local b = t == { 4, 5, 6 } +local c = { 1 } == t +local d = t == ({ 4 } :: { string }) +local e = t ~= { 1, 2 } + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = + process.run({ lutePath, "lint", "-r", defaultRulesPaths.constant_table_comparison, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[constant_table_comparison]: Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false.", + nil, + 5 + ) + + local expected = [[ +violator.luau:1:11-27 ── + │ + 1 │ local a = { 1, 2, 3 } == t + │ ^^^^^^^^^^^^^^^^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:2:11-27 ── + │ + 2 │ local b = t == { 4, 5, 6 } + │ ^^^^^^^^^^^^^^^^ + │ +]] + + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:3:11-21 ── + │ + 3 │ local c = { 1 } == t + │ ^^^^^^^^^^ + │ +]] + + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:4:11-37 ── + │ + 4 │ local d = t == ({ 4 } :: { string }) + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + │ +]] + + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:5:11-24 ── + │ + 5 │ local e = t ~= { 1, 2 } + │ ^^^^^^^^^^^^^ + │ +]] + + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("constant_table_comparison auto-fix", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local a = {} == t +local b = t == {} +local c = t == ({}) +local d = t == ({} :: { string }) +local e = t ~= {} +]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ + lutePath, + "lint", + "--auto-fix", + "-r", + defaultRulesPaths.constant_table_comparison, + violatorPath, + }) + + -- Check + assert.eq(result.exitcode, 0) + + local fixedContents = fs.readfiletostring(violatorPath) + local expectedFixedContents = [[ +local a = next(t) == nil +local b = next(t) == nil +local c = next(t) == nil +local d = next(t) == nil +local e = next(t) ~= nil +]] + + assert.eq(fixedContents, expectedFixedContents) + + -- Teardown + fs.remove(violatorPath) + end) end) From 398ef254766a3a521d3833f3851c4409899eef43 Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Fri, 16 Jan 2026 11:42:16 -0800 Subject: [PATCH 278/642] Add CLI docs (#725) --- docs/.vitepress/config.mts | 7 +++-- docs/cli/check.md | 15 +++++++++ docs/cli/compile.md | 27 ++++++++++++++++ docs/cli/index.md | 34 ++++++++++++++++++++ docs/cli/lint.md | 45 +++++++++++++++++++++++++++ docs/cli/run.md | 15 +++++++++ docs/cli/setup.md | 15 +++++++++ docs/cli/test.md | 63 ++++++++++++++++++++++++++++++++++++++ docs/cli/transform.md | 19 ++++++++++++ docs/guide/index.md | 7 +++++ 10 files changed, 245 insertions(+), 2 deletions(-) create mode 100755 docs/cli/check.md create mode 100755 docs/cli/compile.md create mode 100755 docs/cli/index.md create mode 100755 docs/cli/lint.md create mode 100755 docs/cli/run.md create mode 100755 docs/cli/setup.md create mode 100755 docs/cli/test.md create mode 100755 docs/cli/transform.md create mode 100644 docs/guide/index.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index d6e99a05d..06563b600 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -9,6 +9,7 @@ export default withSidebar( themeConfig: { nav: [ { text: 'Guide', link: '/guide/installation' }, + { text: 'CLI', link: '/cli/' }, { text: 'Reference', link: '/reference/lute/crypto' }, ], search: { provider: 'local' }, @@ -19,12 +20,14 @@ export default withSidebar( }), { // ============ [ SIDEBAR OPTIONS ] ============ - useFolderLinkFromSameNameSubFile: true, + useFolderLinkFromIndexFile: true, + useFolderTitleFromIndexFile: true, useTitleFromFileHeading: true, useTitleFromFrontmatter: true, hyphenToSpace: true, underscoreToSpace: true, - sortMenusByName: true, + sortMenusByFrontmatterOrder: true, + frontmatterOrderDefaultValue: 100, excludeByGlobPattern: ['**/test/**'], } ) diff --git a/docs/cli/check.md b/docs/cli/check.md new file mode 100755 index 000000000..09684901d --- /dev/null +++ b/docs/cli/check.md @@ -0,0 +1,15 @@ +# check + +Type check Luau files. + +## Usage + +```bash +lute check [file2.luau...] +``` + +## Options + +### `-h, --help` + +Display this usage message. diff --git a/docs/cli/compile.md b/docs/cli/compile.md new file mode 100755 index 000000000..2cdabb63a --- /dev/null +++ b/docs/cli/compile.md @@ -0,0 +1,27 @@ +# compile + +Compile a Luau script into a standalone executable. + +## Usage + +```bash +lute compile [options] +``` + +## Options + +### `--output ` + +Name for the compiled executable. Defaults to entry file's base name (with .exe on Windows). + +### `--bundle-stats` + +Display bundle size and compression statistics. + +### `--show-require-graph` + +Print the require dependency graph. + +### `-h, --help` + +Display this usage message. diff --git a/docs/cli/index.md b/docs/cli/index.md new file mode 100755 index 000000000..f7fdd60c9 --- /dev/null +++ b/docs/cli/index.md @@ -0,0 +1,34 @@ +--- +order: 2 +--- + +# CLI Reference + +Lute provides a command-line interface for running, type checking, compiling, testing, and linting Luau code. + +## Usage + +```bash +lute [options] [arguments...] +``` + +If no command is specified, `run` is used by default. + +## Commands + +| Command | Description | +| ------- | ----------- | +| [check](./check) | Type check Luau files. | +| [compile](./compile) | Compile a Luau script into a standalone executable. | +| [lint](./lint) | Lint the specified Luau file using the specified lint rule(s) or using the default rules. | +| [run](./run) | Run a Luau script. | +| [setup](./setup) | Generate type definition files for the language server. | +| [test](./test) | Run tests discovered in .test.luau and .spec.luau files. | +| [transform](./transform) | Run a specified code transformation on specified Luau files. | + +## Global Options + +| Option | Description | +| ------ | ----------- | +| `-h`, `--help` | Display help message for the command. | +| `--version` | Show the lute version. | diff --git a/docs/cli/lint.md b/docs/cli/lint.md new file mode 100755 index 000000000..05b0fbeec --- /dev/null +++ b/docs/cli/lint.md @@ -0,0 +1,45 @@ +# lint + +Lint the specified Luau file using the specified lint rule(s) or using the default rules. + +## Usage + +```bash +lute lint [OPTIONS] [...PATHS] +``` + +## Options + +### `-h, --help` + +Show this help message + +### `-v, --verbose` + +Enable verbose output + +### `-r, --rules [RULE]` + +Path to a single lint rule or a folder containing lint rules. If a folder is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated as individual lint rules. If unspecified, the default lint rules are used. + +### `-j, --json` + +Output lint violations in JSON format matching the LSP diagnostic spec. + +### `-s, --string-input` + +Lint the provided string input instead of reading from files. + +## Arguments + +Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. + +## Examples + +```bash +lute lint -r examples/lints/almost_swapped.luau bad_swap.luau +``` + +```bash +lute lint -r examples/lints/ lintee.luau src_code/ +``` diff --git a/docs/cli/run.md b/docs/cli/run.md new file mode 100755 index 000000000..3eb1550f0 --- /dev/null +++ b/docs/cli/run.md @@ -0,0 +1,15 @@ +# run + +Run a Luau script. + +## Usage + +```bash +lute run [args...] +``` + +## Options + +### `-h, --help` + +Display this usage message. diff --git a/docs/cli/setup.md b/docs/cli/setup.md new file mode 100755 index 000000000..b30602e5c --- /dev/null +++ b/docs/cli/setup.md @@ -0,0 +1,15 @@ +# setup + +Generate type definition files for the language server. + +## Usage + +```bash +lute setup +``` + +## Options + +### `--with-luaurc` + +Defines aliases to the type definition files in the working directory's luaurc file. diff --git a/docs/cli/test.md b/docs/cli/test.md new file mode 100755 index 000000000..3620acb3b --- /dev/null +++ b/docs/cli/test.md @@ -0,0 +1,63 @@ +# test + +Run tests discovered in .test.luau and .spec.luau files. + +## Usage + +```bash +lute test [OPTIONS] [PATHS...] +``` + +## Options + +### `-h, --help` + +Show this help message + +### `--list` + +List all discovered test cases without running them + +### `-s, --suite=SUITE` + +Run only tests in the specified suite + +### `-c, --case=CASE` + +Run only test cases matching the specified name + +## Arguments + +Directories or files to search for tests (default: ./tests) + +## Examples + +Run all tests in ./tests: + +```bash +lute test +``` + +List all test cases: + +```bash +lute test --list +``` + +Run all tests in MyTestSuite: + +```bash +lute test -s MyTestSuite +``` + +Run specific test in suite: + +```bash +lute test --suite MyTestSuite --case mytest +``` + +Run all test cases named "some case": + +```bash +lute test --case "some case" +``` diff --git a/docs/cli/transform.md b/docs/cli/transform.md new file mode 100755 index 000000000..435692c1c --- /dev/null +++ b/docs/cli/transform.md @@ -0,0 +1,19 @@ +# transform + +Run a specified code transformation on specified Luau files. + +## Usage + +```bash +lute transform [options...] +``` + +## Options + +### `--dry-run` + +Runs the transformation without actually overwriting or deleting any files. + +### `--output ` + +Specifies an output file for a transformed file. Only valid when transforming a single file. If not specified, files are overwritten in place. diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 000000000..3ebfb3ce4 --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,7 @@ +--- +order: 1 +--- + +# Guide + +- [Installation](./installation) From 9c3d25a196d4f221758a9b79840d31756d0c7ea7 Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Fri, 16 Jan 2026 12:06:34 -0800 Subject: [PATCH 279/642] Improve reference docs (#726) --- docs/.vitepress/config.mts | 2 +- docs/scripts/reference.luau | 77 +++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 06563b600..920b54cf0 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -10,7 +10,7 @@ export default withSidebar( nav: [ { text: 'Guide', link: '/guide/installation' }, { text: 'CLI', link: '/cli/' }, - { text: 'Reference', link: '/reference/lute/crypto' }, + { text: 'Reference', link: '/reference' }, ], search: { provider: 'local' }, socialLinks: [ diff --git a/docs/scripts/reference.luau b/docs/scripts/reference.luau index 06267992c..0a97407e4 100644 --- a/docs/scripts/reference.luau +++ b/docs/scripts/reference.luau @@ -246,11 +246,14 @@ local function processDirectory( processDirectory(entryPath, docPath, libraryPath, requireLibraryAlias) elseif entry.type == "file" and string.find(entry.name, "%.luau$") then local moduleName = string.gsub(entry.name, "%.luau$", "") - if moduleName == "init" then + local isInit = moduleName == "init" + if isInit then moduleName = string.match(tostring(modulePath), "([^/\\]+)$") or moduleName end - local documentationFilePath = path.join(documentationPath, moduleName .. ".md") + -- Use index.md for init.luau files, otherwise use moduleName.md + local docFileName = if isInit then "index.md" else moduleName .. ".md" + local documentationFilePath = path.join(documentationPath, docFileName) print(`Processing {moduleName}...`) @@ -268,14 +271,80 @@ local function processDirectory( end end +local referenceBasePath = path.join(process.cwd(), "reference") +fs.createdirectory(referenceBasePath, { makeparents = true }) + +-- Generate reference index with frontmatter for sidebar ordering +local referenceIndex = [[--- +order: 3 +--- + +# Reference + +API reference documentation for Lute's built-in libraries. + +- [Lute Libraries](./lute/index.md) - Core runtime libraries (`@lute/*`) +- [Standard Libraries](./std/index.md) - Standard library modules (`@std/*`) +]] +fs.writestringtofile(path.join(referenceBasePath, "index.md"), referenceIndex) + +-- Helper to generate index with table of children +local function generateLibraryIndex(title: string, alias: string, modulePath: path.pathlike): string + local entries = fs.listdirectory(modulePath) + local modules = {} + + for _, entry in entries do + local name = entry.name + if entry.type == "dir" then + -- Check if it has an init.luau (is a module) + local initPath = path.join(modulePath, name, "init.luau") + if fs.exists(initPath) then + table.insert(modules, { name = name, isDir = true }) + end + elseif entry.type == "file" and string.match(name, "%.luau$") then + local modName = string.gsub(name, "%.luau$", "") + if modName ~= "init" then + table.insert(modules, { name = modName, isDir = false }) + end + end + end + + table.sort(modules, function(a, b) + return a.name < b.name + end) + + local lines = { + `# {title}`, + "", + "| Module | Require |", + "| ------ | ------- |", + } + + for _, mod in modules do + local link = if mod.isDir then `./{mod.name}/` else `./{mod.name}` + table.insert(lines, `| [{mod.name}]({link}) | \`require("@{alias}/{mod.name}")\` |`) + end + + table.insert(lines, "") + return table.concat(lines, "\n") +end + local definitionsPath = path.join(process.cwd(), "..", "definitions") -local referencePath = path.join(process.cwd(), "reference", "lute") +local referencePath = path.join(referenceBasePath, "lute") processDirectory(definitionsPath, referencePath, definitionsPath, "lute") +-- Generate lute subfolder index with table of modules +local luteIndex = generateLibraryIndex("lute", "lute", definitionsPath) +fs.writestringtofile(path.join(referencePath, "index.md"), luteIndex) + local stdlibPath = path.join(process.cwd(), "..", "lute", "std", "libs") -local stdlibReferencePath = path.join(process.cwd(), "reference", "std") +local stdlibReferencePath = path.join(referenceBasePath, "std") processDirectory(stdlibPath, stdlibReferencePath, stdlibPath, "std") +-- Generate std subfolder index with table of modules +local stdIndex = generateLibraryIndex("std", "std", stdlibPath) +fs.writestringtofile(path.join(stdlibReferencePath, "index.md"), stdIndex) + -- Mock module in test/ for testing purposes -- local testPath = path.join(process.cwd(), "test") From b525e23a3155281fc081523b87ec900d1f0825cb Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 16 Jan 2026 12:42:09 -0800 Subject: [PATCH 280/642] Updates lute documentation for the run, test, and compile cli commands (#734) This PR just updates the `run`, `test` and `compile` cli command reference. --- docs/cli/compile.md | 22 ++++++++++++++++++---- docs/cli/run.md | 8 +++++++- docs/cli/test.md | 12 +++++++++--- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/cli/compile.md b/docs/cli/compile.md index 2cdabb63a..c4517d79c 100755 --- a/docs/cli/compile.md +++ b/docs/cli/compile.md @@ -1,6 +1,6 @@ # compile -Compile a Luau script into a standalone executable. +Compile a Luau script, along with its dependencies, into a standalone executable. ## Usage @@ -12,16 +12,30 @@ lute compile [options] ### `--output ` -Name for the compiled executable. Defaults to entry file's base name (with .exe on Windows). +Name for the compiled executable. If omitted, defaults to entry file's base name (with .exe on Windows). ### `--bundle-stats` -Display bundle size and compression statistics. +Display compiled bytecode bundle size and compression statistics. ### `--show-require-graph` -Print the require dependency graph. +Print the dependency graph of files that have been included in the bundle. ### `-h, --help` Display this usage message. + +## Examples + +Outputs a standalone executable called foo(.exe on windows): + +```bash +lute compile foo.luau +``` + +Outputs a standalone executable called main(.exe on windows): + +```bash +lute compile foo.luau --output main +``` \ No newline at end of file diff --git a/docs/cli/run.md b/docs/cli/run.md index 3eb1550f0..b7afcf4e9 100755 --- a/docs/cli/run.md +++ b/docs/cli/run.md @@ -1,12 +1,18 @@ # run -Run a Luau script. +Run a Luau script. Can be omitted if you just pass the name of the script +to Lute. ## Usage ```bash lute run [args...] ``` +is equivalent to: + +```bash +lute [args...] +``` ## Options diff --git a/docs/cli/test.md b/docs/cli/test.md index 3620acb3b..0efe2c4d1 100755 --- a/docs/cli/test.md +++ b/docs/cli/test.md @@ -1,6 +1,6 @@ # test -Run tests discovered in .test.luau and .spec.luau files. +Run tests discovered in .test.luau and .spec.luau files (defaults to looking for files in a tests/ directory). ## Usage @@ -18,11 +18,11 @@ Show this help message List all discovered test cases without running them -### `-s, --suite=SUITE` +### `-s, --suite SUITE` Run only tests in the specified suite -### `-c, --case=CASE` +### `-c, --case CASE` Run only test cases matching the specified name @@ -61,3 +61,9 @@ Run all test cases named "some case": ```bash lute test --case "some case" ``` + +List tests that were discovered in my/other/testdir: + +```bash +lute test --list my/other/testdir +``` \ No newline at end of file From 40508262cedc4bf71a087eacd961d85f1bb48ae4 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 16 Jan 2026 13:28:39 -0800 Subject: [PATCH 281/642] Brings in profiler and codecoverage from Luau (#730) This PR just pulls in the Luau CLI's Profiler and Code Coverage modules into `lute`. We're doing this instead of exposing them from Luau because these features are not really products, so much as best effort attempts, so we don't necessarily want to expose them from Luau yet. This is just copied from Luau/CLI - no changes have been made. --- lute/cli/CMakeLists.txt | 4 + lute/cli/include/lute/coverage.h | 9 ++ lute/cli/include/lute/profiler.h | 7 ++ lute/cli/src/coverage.cpp | 87 +++++++++++++++++ lute/cli/src/profiler.cpp | 163 +++++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+) create mode 100644 lute/cli/include/lute/coverage.h create mode 100644 lute/cli/include/lute/profiler.h create mode 100644 lute/cli/src/coverage.cpp create mode 100644 lute/cli/src/profiler.cpp diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 503dcb671..30fc7a1e5 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -25,9 +25,11 @@ add_library(Lute.CLI.lib STATIC) target_sources(Lute.CLI.lib PRIVATE include/lute/climain.h include/lute/compile.h + include/lute/coverage.h include/lute/fileutils.h include/lute/luauflags.h include/lute/packagerun.h + include/lute/profiler.h include/lute/reporter.h include/lute/requiresetup.h include/lute/staticrequires.h @@ -36,9 +38,11 @@ target_sources(Lute.CLI.lib PRIVATE src/climain.cpp src/compile.cpp + src/coverage.cpp src/fileutils.cpp src/luauflags.cpp src/packagerun.cpp + src/profiler.cpp src/requiresetup.cpp src/staticrequires.cpp src/tc.cpp diff --git a/lute/cli/include/lute/coverage.h b/lute/cli/include/lute/coverage.h new file mode 100644 index 000000000..2327570fd --- /dev/null +++ b/lute/cli/include/lute/coverage.h @@ -0,0 +1,9 @@ +#pragma once + +struct lua_State; + +void coverageInit(lua_State* L); +bool coverageActive(); + +void coverageTrack(lua_State* L, int funcindex); +void coverageDump(const char* path); diff --git a/lute/cli/include/lute/profiler.h b/lute/cli/include/lute/profiler.h new file mode 100644 index 000000000..dadc95b96 --- /dev/null +++ b/lute/cli/include/lute/profiler.h @@ -0,0 +1,7 @@ +#pragma once + +struct lua_State; + +void profilerStart(lua_State* L, int frequency); +void profilerStop(); +void profilerDump(const char* path); diff --git a/lute/cli/src/coverage.cpp b/lute/cli/src/coverage.cpp new file mode 100644 index 000000000..af6d3b062 --- /dev/null +++ b/lute/cli/src/coverage.cpp @@ -0,0 +1,87 @@ +#include "lute/coverage.h" + +#include "lua.h" + +#include +#include + +struct Coverage +{ + lua_State* L = nullptr; + std::vector functions; +} gCoverage; + +void coverageInit(lua_State* L) +{ + gCoverage.L = lua_mainthread(L); +} + +bool coverageActive() +{ + return gCoverage.L != nullptr; +} + +void coverageTrack(lua_State* L, int funcindex) +{ + int ref = lua_ref(L, funcindex); + gCoverage.functions.push_back(ref); +} + +static void coverageCallback(void* context, const char* function, int linedefined, int depth, const int* hits, size_t size) +{ + FILE* f = static_cast(context); + + std::string name; + + if (depth == 0) + name = "
"; + else if (function) + name = std::string(function) + ":" + std::to_string(linedefined); + else + name = ":" + std::to_string(linedefined); + + fprintf(f, "FN:%d,%s\n", linedefined, name.c_str()); + + for (size_t i = 0; i < size; ++i) + if (hits[i] != -1) + { + fprintf(f, "FNDA:%d,%s\n", hits[i], name.c_str()); + break; + } + + for (size_t i = 0; i < size; ++i) + if (hits[i] != -1) + fprintf(f, "DA:%d,%d\n", int(i), hits[i]); +} + +void coverageDump(const char* path) +{ + lua_State* L = gCoverage.L; + + FILE* f = fopen(path, "w"); + if (!f) + { + fprintf(stderr, "Error opening coverage %s\n", path); + return; + } + + fprintf(f, "TN:\n"); + + for (int fref : gCoverage.functions) + { + lua_getref(L, fref); + + lua_Debug ar = {}; + lua_getinfo(L, -1, "s", &ar); + + fprintf(f, "SF:%s\n", ar.short_src); + lua_getcoverage(L, -1, f, coverageCallback); + fprintf(f, "end_of_record\n"); + + lua_pop(L, 1); + } + + fclose(f); + + printf("Coverage dump written to %s (%d functions)\n", path, int(gCoverage.functions.size())); +} diff --git a/lute/cli/src/profiler.cpp b/lute/cli/src/profiler.cpp new file mode 100644 index 000000000..028d9eb81 --- /dev/null +++ b/lute/cli/src/profiler.cpp @@ -0,0 +1,163 @@ +#include "lute/profiler.h" + +#include "lua.h" + +#include "Luau/DenseHash.h" + +#include +#include +#include + +struct Profiler +{ + // static state + lua_Callbacks* callbacks = nullptr; + int frequency = 1000; + std::thread thread; + + // variables for communication between loop and trigger + std::atomic exit = false; + std::atomic ticks = 0; + std::atomic samples = 0; + + // private state for trigger + uint64_t currentTicks = 0; + std::string stackScratch; + + // statistics, updated by trigger + Luau::DenseHashMap data{""}; + uint64_t gc[16] = {}; +} gProfiler; + +static void profilerTrigger(lua_State* L, int gc) +{ + uint64_t currentTicks = gProfiler.ticks.load(); + uint64_t elapsedTicks = currentTicks - gProfiler.currentTicks; + + if (elapsedTicks) + { + std::string& stack = gProfiler.stackScratch; + + stack.clear(); + + if (gc > 0) + stack += "GC,GC,"; + + lua_Debug ar; + for (int level = 0; lua_getinfo(L, level, "sn", &ar); ++level) + { + if (!stack.empty()) + stack += ';'; + + stack += ar.short_src; + stack += ','; + if (ar.name) + stack += ar.name; + stack += ','; + if (ar.linedefined > 0) + stack += std::to_string(ar.linedefined); + } + + if (!stack.empty()) + { + gProfiler.data[stack] += elapsedTicks; + } + + if (gc > 0) + { + gProfiler.gc[gc] += elapsedTicks; + } + } + + gProfiler.currentTicks = currentTicks; + gProfiler.callbacks->interrupt = nullptr; +} + +static void profilerLoop() +{ + double last = lua_clock(); + + while (!gProfiler.exit) + { + double now = lua_clock(); + + if (now - last >= 1.0 / double(gProfiler.frequency)) + { + int64_t ticks = int64_t((now - last) * 1e6); + + gProfiler.ticks += ticks; + gProfiler.samples++; + gProfiler.callbacks->interrupt = profilerTrigger; + + last += ticks * 1e-6; + } + else + { + std::this_thread::yield(); + } + } +} + +void profilerStart(lua_State* L, int frequency) +{ + gProfiler.frequency = frequency; + gProfiler.callbacks = lua_callbacks(L); + + gProfiler.exit = false; + gProfiler.thread = std::thread(profilerLoop); +} + +void profilerStop() +{ + gProfiler.exit = true; + gProfiler.thread.join(); +} + +void profilerDump(const char* path) +{ + FILE* f = fopen(path, "wb"); + if (!f) + { + fprintf(stderr, "Error opening profile %s\n", path); + return; + } + + uint64_t total = 0; + + for (auto& p : gProfiler.data) + { + fprintf(f, "%lld %s\n", static_cast(p.second), p.first.c_str()); + total += p.second; + } + + fclose(f); + + printf( + "Profiler dump written to %s (total runtime %.3f seconds, %lld samples, %lld stacks)\n", + path, + double(total) / 1e6, + static_cast(gProfiler.samples.load()), + static_cast(gProfiler.data.size()) + ); + + uint64_t totalgc = 0; + for (uint64_t p : gProfiler.gc) + totalgc += p; + + if (totalgc) + { + printf("GC: %.3f seconds (%.2f%%)", double(totalgc) / 1e6, double(totalgc) / double(total) * 100); + + for (size_t i = 0; i < std::size(gProfiler.gc); ++i) + { + extern const char* luaC_statename(int state); + + uint64_t p = gProfiler.gc[i]; + + if (p) + printf(", %s %.2f%%", luaC_statename(int(i)), double(p) / double(totalgc) * 100); + } + + printf("\n"); + } +} From f22a0e8beab128e0c52518749334ca06b1ac7a1a Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 16 Jan 2026 14:06:15 -0800 Subject: [PATCH 282/642] Edits lint and transform docs (#735) Adds some more description to the docs for `lute lint` and `lute transform` --- docs/cli/lint.md | 6 +++++- docs/cli/transform.md | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/cli/lint.md b/docs/cli/lint.md index 05b0fbeec..154ef8e15 100755 --- a/docs/cli/lint.md +++ b/docs/cli/lint.md @@ -1,6 +1,10 @@ # lint -Lint the specified Luau file using the specified lint rule(s) or using the default rules. +`lute lint` is a programmable linter for Luau code, shipped as part of Lute. +As a linter, it works to statically analyze the user's code to warn them about common pitfalls they may be falling into, or to nudge them away from discouraged coding practices. +It is _programmable_ meaning that you can write a new lint rule for your Luau code _in Luau_. +It's also built on top of the official Luau language stack, allowing it to leverage the same parser used by Luau and Roblox, unlike third-party linters that rely on separate, custom parser implementations. +The examples folder contains two instances of sample lint rules, and you can find the full suite of `lute lint`'s default rules [here](https://github.com/luau-lang/lute/tree/primary/lute/cli/commands/lint/rules). ## Usage diff --git a/docs/cli/transform.md b/docs/cli/transform.md index 435692c1c..816d892b4 100755 --- a/docs/cli/transform.md +++ b/docs/cli/transform.md @@ -1,6 +1,7 @@ # transform Run a specified code transformation on specified Luau files. +Individual code transformers can specify custom migration options, which are parsed as additional arguments on the command line (i.e. `lute transform transformer.luau --custom-arg=value transformee.luau`). ## Usage From 4f79486cc7b8c091aa587a407e5630d14190ed3c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:30:15 +0000 Subject: [PATCH 283/642] Update Luau to 0.705 (#738) **Luau**: Updated from `0.704` to `0.705` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.705 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: ariel --- extern/luau.tune | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 66f11596f..e70cb35e0 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.704" -revision = "dc955d68e70d7bff0735cfc3927f30be2277a2fc" +branch = "0.705" +revision = "93c83a46d7c6c1b040e58e82ef167340a7db1aef" From 52e0377272929eda115187287f1191ea0a421938 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:30:58 +0000 Subject: [PATCH 284/642] Update Lute to 0.1.0-nightly.20260116 (#739) **Lute**: Updated from `0.1.0-nightly.20260109` to `0.1.0-nightly.20260116` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260116 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Annie Tang <98965493+annieetang@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 92e6e9cfa..0e92d6802 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260109" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260116" } diff --git a/rokit.toml b/rokit.toml index fb2317eb8..d8cb0de6a 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20260109" +lute = "luau-lang/lute@0.1.0-nightly.20260116" From 0f59bf9211cfb96153a89e5f56846942f41b5d91 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:36:22 -0800 Subject: [PATCH 285/642] Ensures `fs.stat` and `fs.type` are asynchronous (#741) part of #709 ! --- lute/fs/include/lute/fs.h | 4 +- lute/fs/src/fs.cpp | 156 +---------------------------------- lute/fs/src/fs_impl.cpp | 168 ++++++++++++++++++++++++++++++++++++++ lute/fs/src/fs_impl.h | 24 ++++++ 4 files changed, 198 insertions(+), 154 deletions(-) diff --git a/lute/fs/include/lute/fs.h b/lute/fs/include/lute/fs.h index ae269fb65..92ebaeadd 100644 --- a/lute/fs/include/lute/fs.h +++ b/lute/fs/include/lute/fs.h @@ -44,7 +44,7 @@ int mkdir(lua_State* L); int fs_rmdir(lua_State* L); /* Gets the metadata of a file */ -int fs_stat(lua_State* L); +int stat(lua_State* L); /* Checks if a file exists */ int fs_exists(lua_State* L); @@ -76,7 +76,7 @@ static const luaL_Reg lib[] = { {"remove", remove}, - {"stat", fs_stat}, + {"stat", stat}, {"exists", fs_exists}, {"type", type}, diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index cdf1f72c7..726370636 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -1,7 +1,6 @@ #include "lute/fs.h" #include "lute/runtime.h" -#include "lute/time.h" #include "lute/userdatas.h" #include "lua.h" @@ -26,52 +25,9 @@ #include #include -#include - - -#if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG) -#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) -#endif - -#if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR) -#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) -#endif - -#if !defined(S_ISCHR) && defined(S_IFMT) && defined(S_IFCHR) -#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) -#endif - -#if !defined(S_ISLNK) && defined(S_IFMT) && defined(S_IFLNK) -#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) -#endif - -#if !defined(S_ISFIFO) && defined(S_IFMT) && defined(S_IFIFO) -#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) -#endif - namespace fs { -const char* UV_TYPENAME_UNKNOWN = "unknown"; // UV_DIRENT_UNKNOWN -const char* UV_TYPENAME_FILE = "file"; // UV_DIRENT_FILE -const char* UV_TYPENAME_DIR = "dir"; // UV_DIRENT_DIR -const char* UV_TYPENAME_LINK = "link"; // UV_DIRENT_LINK -const char* UV_TYPENAME_FIFO = "fifo"; // UV_DIRENT_FIFO -const char* UV_TYPENAME_SOCKET = "socket"; // UV_DIRENT_SOCKET -const char* UV_TYPENAME_CHAR = "char"; // UV_DIRENT_CHAR -const char* UV_TYPENAME_BLOCK = "block"; // UV_DIRENT_BLOCK - -const char* UV_DIRENT_TYPES[] = { - UV_TYPENAME_UNKNOWN, - UV_TYPENAME_FILE, - UV_TYPENAME_DIR, - UV_TYPENAME_LINK, - UV_TYPENAME_FIFO, - UV_TYPENAME_SOCKET, - UV_TYPENAME_CHAR, - UV_TYPENAME_BLOCK, -}; - static UVFile* getFileHandle(lua_State* L, int index) { if (!lua_islightuserdata(L, index)) @@ -130,54 +86,6 @@ std::optional setFlags(const char* c, int* openFlags) return modeFlags; } -static int createDurationFromTimespec32(lua_State* L, uv_timespec_t timespec) -{ - uv_timespec64_t extended{static_cast(timespec.tv_sec), static_cast(timespec.tv_nsec)}; - return createDurationFromTimespec(L, extended); -} - -static const char* fileModeToType(uint64_t mode) -{ - if (S_ISDIR(mode)) - { - return UV_TYPENAME_DIR; - } - else if (S_ISREG(mode)) - { - return UV_TYPENAME_FILE; - } - else if (S_ISCHR(mode)) - { - return UV_TYPENAME_CHAR; - } - else if (S_ISLNK(mode)) - { - return UV_TYPENAME_LINK; - } -#ifdef S_ISBLK - else if (S_ISBLK(mode)) - { - return UV_TYPENAME_BLOCK; - } -#endif -#ifdef S_ISFIFO - else if (S_ISFIFO(mode)) - { - return UV_TYPENAME_FIFO; - } -#endif -#ifdef S_ISSOCK - else if (S_ISSOCK(mode)) - { - return UV_TYPENAME_SOCKET; - } -#endif - else - { - return UV_TYPENAME_UNKNOWN; - } -} - int close(lua_State* L) { auto* handle = getFileHandle(L, 1); @@ -279,53 +187,11 @@ int fs_rmdir(lua_State* L) return 0; } -int fs_stat(lua_State* L) +int stat(lua_State* L) { const char* path = luaL_checkstring(L, 1); - uv_fs_t stat_req; - int err = uv_fs_stat(uv_default_loop(), &stat_req, path, nullptr); - - if (err) - { - uv_fs_req_cleanup(&stat_req); - luaL_errorL(L, "%s", uv_strerror(err)); - } - - lua_createtable(L, 0, 6); - - auto stat = stat_req.statbuf; - - auto type = fileModeToType(stat.st_mode); - lua_pushstring(L, type); - lua_setfield(L, -2, "type"); - - // this is fine unless the file is 9 petabytes - lua_pushnumber(L, static_cast(stat.st_size)); - lua_setfield(L, -2, "size"); - - createDurationFromTimespec32(L, stat.st_birthtim); - lua_setfield(L, -2, "created"); - - createDurationFromTimespec32(L, stat.st_atim); - lua_setfield(L, -2, "accessed"); - - createDurationFromTimespec32(L, stat.st_mtim); - lua_setfield(L, -2, "modified"); - - // permissions - lua_createtable(L, 0, 2); - - // libuv writes this correctly cross-platform - bool canAnyWrite = stat.st_mode & 0222; - lua_pushboolean(L, !canAnyWrite); - lua_setfield(L, -2, "readonly"); - - lua_setfield(L, -2, "permissions"); - - uv_fs_req_cleanup(&stat_req); - - return 1; + return stat_impl(L, path); } static void defaultCallback(uv_fs_t* req) @@ -584,21 +450,7 @@ int type(lua_State* L) { const char* path = luaL_checkstring(L, 1); - uv_fs_t req; - - int err = uv_fs_stat(uv_default_loop(), &req, path, nullptr); - - if (err) - { - uv_fs_req_cleanup(&req); - luaL_errorL(L, "%s", uv_strerror(err)); - } - - auto type = fileModeToType(req.statbuf.st_mode); - lua_pushstring(L, type); - uv_fs_req_cleanup(&req); - - return 1; + return type_impl(L, path); } int listdir(lua_State* L) @@ -634,7 +486,7 @@ int listdir(lua_State* L) lua_pushstring(L, dir.name); lua_setfield(L, -2, "name"); - lua_pushstring(L, UV_DIRENT_TYPES[dir.type]); + lua_pushstring(L, fs::UV_DIRENT_TYPES[dir.type]); lua_setfield(L, -2, "type"); lua_settable(L, -3); diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp index 3d64b105e..3f6071a0f 100644 --- a/lute/fs/src/fs_impl.cpp +++ b/lute/fs/src/fs_impl.cpp @@ -1,5 +1,6 @@ #include "fs_impl.h" +#include "lute/time.h" #include "lute/UVRequest.h" #include "lua.h" @@ -7,6 +8,34 @@ #include "uv.h" +#ifdef _WIN32 +#include +#else +#include +#endif + +#include + +#if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG) +#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) +#endif + +#if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR) +#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#endif + +#if !defined(S_ISCHR) && defined(S_IFMT) && defined(S_IFCHR) +#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) +#endif + +#if !defined(S_ISLNK) && defined(S_IFMT) && defined(S_IFLNK) +#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) +#endif + +#if !defined(S_ISFIFO) && defined(S_IFMT) && defined(S_IFIFO) +#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) +#endif + constexpr size_t kChunkIOSize = 4096; namespace fs @@ -266,6 +295,145 @@ int remove_impl(lua_State* L, const char* path) return lua_yield(L, 0); } +static const char* fileModeToType(uint64_t mode) +{ + if (S_ISDIR(mode)) + { + return UV_TYPENAME_DIR; + } + else if (S_ISREG(mode)) + { + return UV_TYPENAME_FILE; + } + else if (S_ISCHR(mode)) + { + return UV_TYPENAME_CHAR; + } + else if (S_ISLNK(mode)) + { + return UV_TYPENAME_LINK; + } +#ifdef S_ISBLK + else if (S_ISBLK(mode)) + { + return UV_TYPENAME_BLOCK; + } +#endif +#ifdef S_ISFIFO + else if (S_ISFIFO(mode)) + { + return UV_TYPENAME_FIFO; + } +#endif +#ifdef S_ISSOCK + else if (S_ISSOCK(mode)) + { + return UV_TYPENAME_SOCKET; + } +#endif + else + { + return UV_TYPENAME_UNKNOWN; + } +} + +static int createDurationFromTimespec32(lua_State* L, uv_timespec_t timespec) +{ + uv_timespec64_t extended{static_cast(timespec.tv_sec), static_cast(timespec.tv_nsec)}; + return createDurationFromTimespec(L, extended); +} + +int stat_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_stat( + uv_default_loop(), + &req->req, + path, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("Error getting metadata of file %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [stat = req->statbuf](lua_State* L) + { + lua_createtable(L, 0, 6); + + auto type = fileModeToType(stat.st_mode); + lua_pushstring(L, type); + lua_setfield(L, -2, "type"); + + // this is fine unless the file is 9 petabytes + lua_pushnumber(L, static_cast(stat.st_size)); + lua_setfield(L, -2, "size"); + + createDurationFromTimespec32(L, stat.st_birthtim); + lua_setfield(L, -2, "created"); + + createDurationFromTimespec32(L, stat.st_atim); + lua_setfield(L, -2, "accessed"); + + createDurationFromTimespec32(L, stat.st_mtim); + lua_setfield(L, -2, "modified"); + + // permissions + lua_createtable(L, 0, 2); + + // libuv writes this correctly cross-platform + bool canAnyWrite = stat.st_mode & 0222; + lua_pushboolean(L, !canAnyWrite); + lua_setfield(L, -2, "readonly"); + + lua_setfield(L, -2, "permissions"); + + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + +int type_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_stat( + uv_default_loop(), + &req->req, + path, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("Error getting type of file %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [stat = req->statbuf](lua_State* L) + { + auto type = fileModeToType(stat.st_mode); + lua_pushstring(L, type); + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + int mkdir_impl(lua_State* L, const char* path, int mode) { uvutils::ScopedUVRequest req(L); diff --git a/lute/fs/src/fs_impl.h b/lute/fs/src/fs_impl.h index c0360907a..adbeffb67 100644 --- a/lute/fs/src/fs_impl.h +++ b/lute/fs/src/fs_impl.h @@ -7,6 +7,26 @@ struct lua_State; namespace fs { +constexpr char UV_TYPENAME_UNKNOWN[] = "unknown"; // UV_DIRENT_UNKNOWN +constexpr char UV_TYPENAME_FILE[] = "file"; // UV_DIRENT_FILE +constexpr char UV_TYPENAME_DIR[] = "dir"; // UV_DIRENT_DIR +constexpr char UV_TYPENAME_LINK[] = "link"; // UV_DIRENT_LINK +constexpr char UV_TYPENAME_FIFO[] = "fifo"; // UV_DIRENT_FIFO +constexpr char UV_TYPENAME_SOCKET[] = "socket"; // UV_DIRENT_SOCKET +constexpr char UV_TYPENAME_CHAR[] = "char"; // UV_DIRENT_CHAR +constexpr char UV_TYPENAME_BLOCK[] = "block"; // UV_DIRENT_BLOCK + +constexpr const char* UV_DIRENT_TYPES[] = { + UV_TYPENAME_UNKNOWN, + UV_TYPENAME_FILE, + UV_TYPENAME_DIR, + UV_TYPENAME_LINK, + UV_TYPENAME_FIFO, + UV_TYPENAME_SOCKET, + UV_TYPENAME_CHAR, + UV_TYPENAME_BLOCK, +}; + struct UVFile { std::optional fd = std::nullopt; @@ -17,6 +37,10 @@ int open_impl(lua_State* L, const char* path, int flags, int mode); int read_impl(lua_State* L, UVFile* handle); int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numBytes); int close_impl(lua_State* L, UVFile* handle); + int remove_impl(lua_State* L, const char* path); +int stat_impl(lua_State* L, const char* path); +int type_impl(lua_State* L, const char* path); + int mkdir_impl(lua_State* L, const char* path, int mode); } // namespace fs From 3cfa63fd1ce26cea87c2e45f11e18495482a63cf Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:47:11 -0800 Subject: [PATCH 286/642] Ensures `fs.rmdir` is asynchronous (#743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit part of https://github.com/luau-lang/lute/issues/709 😃👍 --- lute/fs/include/lute/fs.h | 4 ++-- lute/fs/src/fs.cpp | 20 ++++++++++++-------- lute/fs/src/fs_impl.cpp | 29 +++++++++++++++++++++++++++++ lute/fs/src/fs_impl.h | 1 + 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/lute/fs/include/lute/fs.h b/lute/fs/include/lute/fs.h index 92ebaeadd..71c48bdd8 100644 --- a/lute/fs/include/lute/fs.h +++ b/lute/fs/include/lute/fs.h @@ -41,7 +41,7 @@ int remove(lua_State* L); int mkdir(lua_State* L); /* Removes a directory */ -int fs_rmdir(lua_State* L); +int rmdir(lua_State* L); /* Gets the metadata of a file */ int stat(lua_State* L); @@ -87,7 +87,7 @@ static const luaL_Reg lib[] = { {"mkdir", mkdir}, {"listdir", listdir}, - {"rmdir", fs_rmdir}, + {"rmdir", rmdir}, {NULL, NULL}, }; diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 726370636..afdf6cb0e 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -173,18 +173,22 @@ int mkdir(lua_State* L) return mkdir_impl(L, path, mode); } -int fs_rmdir(lua_State* L) +int rmdir(lua_State* L) { - const char* path = luaL_checkstring(L, 1); + int nArgs = lua_gettop(L); + if (nArgs < 1) + { + luaL_errorL(L, "rmdir: no path supplied\n"); + } - uv_fs_t rmdir_req; - int err = uv_fs_rmdir(uv_default_loop(), &rmdir_req, path, nullptr); - uv_fs_req_cleanup(&rmdir_req); + if (nArgs > 1) + { + luaL_errorL(L, "rmdir: too many arguments supplied\n"); + } - if (err) - luaL_errorL(L, "%s", uv_strerror(err)); + const char* path = luaL_checkstring(L, 1); - return 0; + return rmdir_impl(L, path); } int stat(lua_State* L) diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp index 3f6071a0f..93d0a9a04 100644 --- a/lute/fs/src/fs_impl.cpp +++ b/lute/fs/src/fs_impl.cpp @@ -464,4 +464,33 @@ int mkdir_impl(lua_State* L, const char* path, int mode) return lua_yield(L, 0); } +int rmdir_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_rmdir( + uv_default_loop(), + &req->req, + path, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("Error removing directory %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); +} + } // namespace fs diff --git a/lute/fs/src/fs_impl.h b/lute/fs/src/fs_impl.h index adbeffb67..a5bcbd262 100644 --- a/lute/fs/src/fs_impl.h +++ b/lute/fs/src/fs_impl.h @@ -43,4 +43,5 @@ int stat_impl(lua_State* L, const char* path); int type_impl(lua_State* L, const char* path); int mkdir_impl(lua_State* L, const char* path, int mode); +int rmdir_impl(lua_State* L, const char* path); } // namespace fs From 0a54699e299cd9308e9b4c142f63ebb54eef18c9 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 20 Jan 2026 14:28:40 -0800 Subject: [PATCH 287/642] Fixes a crash in listdir (#744) This PR just pulls in @checkraisefold 's fix here: https://github.com/luau-lang/lute/pull/740. The main change is to check the return error code from `uv_fs_scandir`. Using the UVRequest abstractions a) helps make this function suitable for use cooperatively (async), and handles the additional memory management improvments that @checkraisefold added. Helps address one of the functions listed in https://github.com/luau-lang/lute/issues/709 --- lute/fs/src/fs.cpp | 62 ++++------------------------------------- lute/fs/src/fs_impl.cpp | 61 ++++++++++++++++++++++++++++++++++++++++ lute/fs/src/fs_impl.h | 2 ++ 3 files changed, 69 insertions(+), 56 deletions(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index afdf6cb0e..f419d223b 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -459,65 +459,15 @@ int type(lua_State* L) int listdir(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); - - int err = uv_fs_scandir( - uv_default_loop(), - req, - path, - 0, - [](uv_fs_t* req) - { - auto* request_state = static_cast(req->data); - - request_state->get()->complete( - [req](lua_State* L) - { - lua_createtable(L, 1, 0); - - uv_dirent_t dir; - int i = 0; - int err = 0; - while ((err = uv_fs_scandir_next(req, &dir)) >= 0) - { - lua_pushinteger(L, ++i); - - lua_createtable(L, 0, 2); - - lua_pushstring(L, dir.name); - lua_setfield(L, -2, "name"); - - lua_pushstring(L, fs::UV_DIRENT_TYPES[dir.type]); - lua_setfield(L, -2, "type"); - - lua_settable(L, -3); - } - - uv_fs_req_cleanup(req); - - delete static_cast(req->data); - delete req; - - if (err != UV_EOF) - luaL_errorL(L, "%s", uv_strerror(err)); - - return 1; - } - ); - } - ); - - if (err) + int nArgs = lua_gettop(L); + if (nArgs > 1) { - delete static_cast(req->data); - delete req; - luaL_errorL(L, "%s", uv_strerror(err)); + luaL_errorL(L, "listdir: too many arguments supplied\n"); } - return lua_yield(L, 0); + const char* path = luaL_checkstring(L, 1); + + return listdir_impl(L, path); } } // namespace fs diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp index 93d0a9a04..6e311f7d1 100644 --- a/lute/fs/src/fs_impl.cpp +++ b/lute/fs/src/fs_impl.cpp @@ -464,6 +464,7 @@ int mkdir_impl(lua_State* L, const char* path, int mode) return lua_yield(L, 0); } + int rmdir_impl(lua_State* L, const char* path) { uvutils::ScopedUVRequest req(L); @@ -493,4 +494,64 @@ int rmdir_impl(lua_State* L, const char* path) return lua_yield(L, 0); } + +int listdir_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_scandir( + uv_default_loop(), + &req->req, + path, + 0, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + if (result < 0) + { + r->fail("listdir: Error listing directory %s (%s)", req->path, uv_strerror(result)); + return; + } + + std::vector> entries; + uv_dirent_t dirent; + int err = 0; + while ((err = uv_fs_scandir_next(req, &dirent)) >= 0) + { + entries.emplace_back(dirent.name, dirent.type); + } + + if (err != UV_EOF) + { + r->fail("listdir: Error reading directory entry (%s)", uv_strerror(err)); + return; + } + + r->succeed( + [entries = std::move(entries)](lua_State* L) + { + lua_createtable(L, entries.size(), 0); + for (size_t i = 0; i < entries.size(); ++i) + { + lua_pushinteger(L, i + 1); + + lua_createtable(L, 0, 2); + + lua_pushstring(L, entries[i].first.c_str()); + lua_setfield(L, -2, "name"); + + lua_pushstring(L, UV_DIRENT_TYPES[entries[i].second]); + lua_setfield(L, -2, "type"); + + lua_settable(L, -3); + } + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + } // namespace fs diff --git a/lute/fs/src/fs_impl.h b/lute/fs/src/fs_impl.h index a5bcbd262..227008bfa 100644 --- a/lute/fs/src/fs_impl.h +++ b/lute/fs/src/fs_impl.h @@ -44,4 +44,6 @@ int type_impl(lua_State* L, const char* path); int mkdir_impl(lua_State* L, const char* path, int mode); int rmdir_impl(lua_State* L, const char* path); +int listdir_impl(lua_State* L, const char* path); + } // namespace fs From 1145bea8900e180dea5b32e4d455dbe72b7fc1e4 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:55:40 -0500 Subject: [PATCH 288/642] Lute Lint: Ensure `suggestedfix` is included in lsp diagnostic (#742) To support registering code actions in Mandolin, it is essential that the `suggestedfix` data is serialized as part of `lute lint`'s JSON diagnostic output. This PR adds support for this and a sanity check test case --- lute/cli/commands/lint/lsp/init.luau | 33 ++++++++++++------ lute/cli/commands/lint/lsp/types.luau | 14 +++++--- tests/cli/lint.test.luau | 48 +++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 15 deletions(-) diff --git a/lute/cli/commands/lint/lsp/init.luau b/lute/cli/commands/lint/lsp/init.luau index 5af844410..436134061 100644 --- a/lute/cli/commands/lint/lsp/init.luau +++ b/lute/cli/commands/lint/lsp/init.luau @@ -1,6 +1,7 @@ -local pathLib = require("@std/path") local lintTypes = require("./types") local lspTypes = require("@self/types") +local pathLib = require("@std/path") +local syntax = require("@std/syntax") local tableext = require("@std/tableext") local lsp = {} @@ -25,23 +26,33 @@ local function tag(t: lintTypes.tag): number end end -function lsp.diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic +local function getRange(location: syntax.span): lspTypes.Range return table.freeze({ - range = table.freeze({ - start = table.freeze({ - line = lint.location.beginline - 1, - character = lint.location.begincolumn - 1, - }), - ["end"] = table.freeze({ - line = lint.location.endline - 1, - character = lint.location.endcolumn - 1, - }), + start = table.freeze({ + line = location.beginline - 1, + character = location.begincolumn - 1, }), + ["end"] = table.freeze({ + line = location.endline - 1, + character = location.endcolumn - 1, + }), + }) +end + +function lsp.diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic + return table.freeze({ + range = getRange(lint.location), severity = severity(lint.severity), code = lint.lintname, source = "lute lint" :: "lute lint", -- LUAUFIX: Annotation needed, otherwise this errors because string isn't inferred as a singleton message = lint.message, tags = if lint.tags then table.freeze(tableext.map(lint.tags, tag)) else nil, + suggestedfix = if lint.suggestedfix + then table.freeze({ + fix = lint.suggestedfix.fix, + range = getRange(lint.suggestedfix.location or lint.location), + }) + else nil, }) end diff --git a/lute/cli/commands/lint/lsp/types.luau b/lute/cli/commands/lint/lsp/types.luau index c9fbe176c..6d6d08168 100644 --- a/lute/cli/commands/lint/lsp/types.luau +++ b/lute/cli/commands/lint/lsp/types.luau @@ -1,13 +1,19 @@ export type Diagnostic = { - read range: { - read start: { read line: number, read character: number }, - read ["end"]: { read line: number, read character: number }, - }, -- zero-based + read range: Range, -- zero-based read severity: number, -- 1: Error, 2: Warning, 3: Information, 4: Hint read code: string, -- lint name read source: "lute lint", read message: string, read tags: { number }?, -- 1: Unnecessary, 2: Deprecated + read suggestedfix: { + read fix: string, + read range: Range, + }?, +} + +export type Range = { + read start: { read line: number, read character: number }, + read ["end"]: { read line: number, read character: number }, } export type WorkspaceDocumentDiagnosticReport = { diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 447eade37..732f0f93d 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -1584,4 +1584,52 @@ local e = next(t) ~= nil -- Teardown fs.remove(violatorPath) end) + + suite:case("lute lint json output includes suggestedfix", function(assert) + local inputString = [[ + a = b + b = a + ]] + + -- Do + -- Run the linter on string input with json flag + local result = process.run({ lutePath, "lint", "-j", "-s", inputString }) + + -- Check + assert.eq(result.exitcode, 0) + + local expected = [=[ +[ + { + "message": "This looks like a failed attempt to swap.", + "source": "lute lint", + "code": "almost_swapped", + "severity": 2, + "range": { + "start": { + "character": 3, + "line": 0 + }, + "end": { + "character": 8, + "line": 1 + } + }, + "suggestedfix": { + "range": { + "start": { + "character": 3, + "line": 0 + }, + "end": { + "character": 8, + "line": 1 + } + }, + "fix": "a, b = b, a" + } + } +]]=] + assert.strcontains(result.stdout, expected) + end) end) From 5155a5b466d87dbb08c7539339b0dc181ecd9e78 Mon Sep 17 00:00:00 2001 From: Luka Date: Wed, 21 Jan 2026 20:17:49 +0100 Subject: [PATCH 289/642] Add executable bit to release binaries (#747) GitHub does not preserve permissions when uploading files as an artifact, causing the executable bit of the binaries within the release artifacts to be lost. This makes Lute not executable when installing it using [mise-en-place's GitHub backend](https://mise.jdx.dev/dev-tools/backends/github.html). This PR re-adds the executable bit to the release artifacts. --------- Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c198d413d..f31837d7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -131,6 +131,7 @@ jobs: mkdir release for dir in ./artifacts/*; do base_name=$(basename "$dir") + chmod 755 "$dir"/* # Artifacts don't preserve permissions, so re-add them. zip -rj release/"${base_name}.zip" "$dir"/* done From 1660a6177e783492527ca8985afac5dbee1b9223 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:31:43 -0800 Subject: [PATCH 290/642] Updates `fs.mkdir` to no longer support setting permissions (#737) See #736 --- definitions/fs.luau | 2 ++ lute/fs/src/fs.cpp | 10 +++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/definitions/fs.luau b/definitions/fs.luau index 8ebe3b6fd..088762b7a 100644 --- a/definitions/fs.luau +++ b/definitions/fs.luau @@ -59,6 +59,8 @@ function fs.type(path: string): FileType error("not implemented") end +--- Creates a directory at the specified path. +--- To set the permissions mode for a directory (Unix only), see @std/process for `run` or `system` to shell out to `chmod` or the equivalent. function fs.mkdir(path: string): () error("not implemented") end diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index f419d223b..1923756e7 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -158,17 +158,13 @@ int remove(lua_State* L) int mkdir(lua_State* L) { int nArgs = lua_gettop(L); - if (nArgs < 1) + if (nArgs != 1) { - luaL_errorL(L, "Error: no path supplied\n"); + luaL_errorL(L, "Error: expected 1 argument\n"); } - if (nArgs > 2) - { - luaL_errorL(L, "Error: too many arguments supplied\n"); - } const char* path = luaL_checkstring(L, 1); - int mode = luaL_optinteger(L, 2, 0777); + int mode = 0777; // default permission for Unix, not used on Windows return mkdir_impl(L, path, mode); } From 16caf0569291cc3ca6bbce312ed21717761e1b2d Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:03:17 -0800 Subject: [PATCH 291/642] Refactors `fileutils` into Lute.Common (#750) So we can use `fileutils` without including `Lute.CLI` --- CMakeLists.txt | 1 + lute/cli/CMakeLists.txt | 4 +--- lute/common/CMakeLists.txt | 12 ++++++++++++ lute/{cli => common}/include/lute/fileutils.h | 0 lute/{cli => common}/src/fileutils.cpp | 0 tests/CMakeLists.txt | 2 +- 6 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 lute/common/CMakeLists.txt rename lute/{cli => common}/include/lute/fileutils.h (100%) rename lute/{cli => common}/src/fileutils.cpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b4bf119be..6ce241956 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,6 +130,7 @@ else() endif() # libraries +add_subdirectory(lute/common) add_subdirectory(lute/require) add_subdirectory(lute/runtime) add_subdirectory(lute/crypto) diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 30fc7a1e5..408e61256 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -26,7 +26,6 @@ target_sources(Lute.CLI.lib PRIVATE include/lute/climain.h include/lute/compile.h include/lute/coverage.h - include/lute/fileutils.h include/lute/luauflags.h include/lute/packagerun.h include/lute/profiler.h @@ -39,7 +38,6 @@ target_sources(Lute.CLI.lib PRIVATE src/climain.cpp src/compile.cpp src/coverage.cpp - src/fileutils.cpp src/luauflags.cpp src/packagerun.cpp src/profiler.cpp @@ -51,7 +49,7 @@ target_sources(Lute.CLI.lib PRIVATE target_compile_features(Lute.CLI.lib PUBLIC cxx_std_17) target_include_directories(Lute.CLI.lib PUBLIC include ${CLI_GENERATED_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR}) -target_link_libraries(Lute.CLI.lib PRIVATE Luau.Common Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Luau Lute.Process Lute.Require Lute.Runtime Luau.CLI.lib zlibstatic) +target_link_libraries(Lute.CLI.lib PRIVATE Luau.Common Luau.Compiler Luau.Config Luau.CodeGen Luau.Analysis Luau.VM Lute.CLI.Commands Lute.Luau Lute.Process Lute.Require Lute.Runtime Luau.CLI.lib Lute.Common zlibstatic) target_compile_options(Lute.CLI.lib PRIVATE ${LUTE_OPTIONS}) add_executable(Lute.CLI) diff --git a/lute/common/CMakeLists.txt b/lute/common/CMakeLists.txt new file mode 100644 index 000000000..0d9cc7897 --- /dev/null +++ b/lute/common/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(Lute.Common STATIC) + +target_sources(Lute.Common PRIVATE + include/lute/fileutils.h + + src/fileutils.cpp +) + +target_compile_features(Lute.Common PUBLIC cxx_std_17) +target_include_directories(Lute.Common PUBLIC include) +target_link_libraries(Lute.Common PRIVATE Luau.CLI.lib) +target_compile_options(Lute.Common PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/cli/include/lute/fileutils.h b/lute/common/include/lute/fileutils.h similarity index 100% rename from lute/cli/include/lute/fileutils.h rename to lute/common/include/lute/fileutils.h diff --git a/lute/cli/src/fileutils.cpp b/lute/common/src/fileutils.cpp similarity index 100% rename from lute/cli/src/fileutils.cpp rename to lute/common/src/fileutils.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f243eb161..cdc6d2dce 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,5 +29,5 @@ target_sources(Lute.Test PRIVATE set_target_properties(Lute.Test PROPERTIES OUTPUT_NAME lute-tests) target_compile_features(Lute.Test PUBLIC cxx_std_17) -target_link_libraries(Lute.Test PRIVATE Lute.CLI.lib Lute.Luau Lute.Require Lute.Runtime Luau.CLI.lib Luau.Analysis Luau.Compiler Luau.VM) +target_link_libraries(Lute.Test PRIVATE Lute.CLI.lib Lute.Common Lute.Luau Lute.Require Lute.Runtime Luau.CLI.lib Luau.Analysis Luau.Compiler Luau.VM) target_compile_options(Lute.Test PRIVATE ${LUTE_OPTIONS}) From 6f627987c232d5e44813242c0225d9b0681ff078 Mon Sep 17 00:00:00 2001 From: Hunter Goldstein Date: Wed, 21 Jan 2026 17:54:50 -0800 Subject: [PATCH 292/642] Change `std.path.types.pathlike` to be the union of POSIX and Win32 path-like types (#749) I noticed some errors in `std/libs/fs.luau` that seemed like false positives, it looks like we were requiring that "pathlike" data either be a string or `path`, the latter of which must be a table with the correct metatable. Based on the definitions of `posixtypes.pathlike` and `win32types.pathlike`, it seems like this type was _intended_ to take the underlying data as well, so one can write something like: ```luau local pathlib = require("@std/path") print(pathlib.format({ absolute = true, parts = {"foo", "bar", "baz"}})) ``` --- lute/std/libs/path/types.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lute/std/libs/path/types.luau b/lute/std/libs/path/types.luau index 08cf91461..2c2bd392c 100644 --- a/lute/std/libs/path/types.luau +++ b/lute/std/libs/path/types.luau @@ -2,6 +2,6 @@ local posixtypes = require("@std/path/posix/types") local win32types = require("@std/path/win32/types") export type path = posixtypes.path | win32types.path -export type pathlike = string | path +export type pathlike = posixtypes.pathlike | win32types.pathlike return {} From f414099305a2493a11b21f97f68889413165197f Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:07:19 -0800 Subject: [PATCH 293/642] Ensures `fs.exists`, `fs.link`, `fs.symlink`, `fs.copy` are all asynchronous (#745) Another four `fs` functions for https://github.com/luau-lang/lute/issues/709! --- lute/common/include/lute/fileutils.h | 3 + lute/common/src/fileutils.cpp | 20 ++++ lute/fs/CMakeLists.txt | 2 +- lute/fs/include/lute/fs.h | 16 +-- lute/fs/src/fs.cpp | 143 +++++--------------------- lute/fs/src/fs_impl.cpp | 148 ++++++++++++++++++++++++++- lute/fs/src/fs_impl.h | 6 ++ 7 files changed, 211 insertions(+), 127 deletions(-) diff --git a/lute/common/include/lute/fileutils.h b/lute/common/include/lute/fileutils.h index 15a150a07..45c411fa5 100644 --- a/lute/common/include/lute/fileutils.h +++ b/lute/common/include/lute/fileutils.h @@ -25,4 +25,7 @@ bool removeFile(const std::string& path); // Remove a directory (must be empty) bool removeDirectory(const std::string& path); +// Check if a path is a directory +bool isDirectory(const std::string& path); + } // namespace Lute diff --git a/lute/common/src/fileutils.cpp b/lute/common/src/fileutils.cpp index 4be9a4585..25bc3536b 100644 --- a/lute/common/src/fileutils.cpp +++ b/lute/common/src/fileutils.cpp @@ -144,4 +144,24 @@ bool removeDirectory(const std::string& path) #endif } +bool isDirectory(const std::string& path) +{ +#ifdef _WIN32 + std::wstring wpath = fromUtf8(path); + if (wpath.empty()) + return false; + + DWORD attrs = GetFileAttributesW(wpath.c_str()); + if (attrs == INVALID_FILE_ATTRIBUTES) + return false; + + return (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0; +#else + struct stat pathStat; + if (stat(path.c_str(), &pathStat) != 0) + return false; + return S_ISDIR(pathStat.st_mode); +#endif +} + } // namespace Lute diff --git a/lute/fs/CMakeLists.txt b/lute/fs/CMakeLists.txt index 7d97b78fc..57f0b5a10 100644 --- a/lute/fs/CMakeLists.txt +++ b/lute/fs/CMakeLists.txt @@ -10,5 +10,5 @@ target_sources(Lute.Fs PRIVATE target_compile_features(Lute.Fs PUBLIC cxx_std_17) target_include_directories(Lute.Fs PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Fs PRIVATE Lute.Runtime Lute.Time Luau.VM uv_a) +target_link_libraries(Lute.Fs PRIVATE Lute.Common Lute.Runtime Lute.Time Luau.VM uv_a) target_compile_options(Lute.Fs PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/fs/include/lute/fs.h b/lute/fs/include/lute/fs.h index 71c48bdd8..b66559a10 100644 --- a/lute/fs/include/lute/fs.h +++ b/lute/fs/include/lute/fs.h @@ -47,16 +47,16 @@ int rmdir(lua_State* L); int stat(lua_State* L); /* Checks if a file exists */ -int fs_exists(lua_State* L); +int exists(lua_State* L); /* Copies a file to another path */ -int fs_copy(lua_State* L); +int copy(lua_State* L); /* Creates a link to a file */ -int fs_link(lua_State* L); +int link(lua_State* L); /* Creates a symlink to a file */ -int fs_symlink(lua_State* L); +int symlink(lua_State* L); /* Gets the type of a file entry */ int type(lua_State* L); @@ -77,13 +77,13 @@ static const luaL_Reg lib[] = { {"remove", remove}, {"stat", stat}, - {"exists", fs_exists}, + {"exists", exists}, {"type", type}, {"watch", fs_watch}, - {"link", fs_link}, - {"symlink", fs_symlink}, - {"copy", fs_copy}, + {"link", link}, + {"symlink", symlink}, + {"copy", copy}, {"mkdir", mkdir}, {"listdir", listdir}, diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 1923756e7..6dd63cae2 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -8,21 +8,12 @@ #include "uv.h" -#include #include #include "fs_impl.h" -#ifdef _WIN32 -#include -#else -#include -#endif -#include -#include -#include + #include #include -#include #include namespace fs @@ -194,98 +185,46 @@ int stat(lua_State* L) return stat_impl(L, path); } -static void defaultCallback(uv_fs_t* req) +int copy(lua_State* L) { - auto* request_state = static_cast(req->data); - auto token = std::move(*request_state); - delete request_state; - - auto err = req->result; - - uv_fs_req_cleanup(req); - delete req; - - if (err) + int nArgs = lua_gettop(L); + if (nArgs > 2) { - token->fail(uv_strerror(err)); - return; + luaL_errorL(L, "copy: too many arguments supplied\n"); } - token->complete( - [](lua_State* L) - { - return 0; - } - ); -} - -int fs_copy(lua_State* L) -{ const char* path = luaL_checkstring(L, 1); const char* dest = luaL_checkstring(L, 2); - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); - - int err = uv_fs_copyfile(uv_default_loop(), req, path, dest, 0, defaultCallback); - - if (err) - { - delete static_cast(req->data); - delete req; - luaL_errorL(L, "%s", uv_strerror(err)); - } - - return lua_yield(L, 0); + return copy_impl(L, path, dest); } -int fs_link(lua_State* L) +int link(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - const char* dest = luaL_checkstring(L, 2); - - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); - - int err = uv_fs_link(uv_default_loop(), req, path, dest, defaultCallback); - - if (err) + int nArgs = lua_gettop(L); + if (nArgs > 2) { - delete static_cast(req->data); - delete req; - luaL_errorL(L, "%s", uv_strerror(err)); + luaL_errorL(L, "link: too many arguments supplied\n"); } - return lua_yield(L, 0); -} - -int fs_symlink(lua_State* L) -{ const char* path = luaL_checkstring(L, 1); const char* dest = luaL_checkstring(L, 2); - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); + return link_impl(L, path, dest); +} - if (std::filesystem::is_directory(path)) - { - req->flags = UV_FS_SYMLINK_DIR; // windows - } - else +int symlink(lua_State* L) +{ + int nArgs = lua_gettop(L); + if (nArgs > 2) { - req->flags = 0; + luaL_errorL(L, "symlink: too many arguments supplied\n"); } - int err = uv_fs_symlink(uv_default_loop(), req, path, dest, req->flags, defaultCallback); - - if (err) - { - delete static_cast(req->data); - delete req; - luaL_errorL(L, "%s", uv_strerror(err)); - } + const char* path = luaL_checkstring(L, 1); + const char* dest = luaL_checkstring(L, 2); - return lua_yield(L, 0); + return symlink_impl(L, path, dest); } struct WatchHandle @@ -405,45 +344,17 @@ int fs_watch(lua_State* L) return 1; // return the watch handle } -int fs_exists(lua_State* L) +int exists(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - - auto* req = new uv_fs_t(); - req->data = new ResumeToken(getResumeToken(L)); - - int err = uv_fs_access( - uv_default_loop(), - req, - path, - F_OK, - [](uv_fs_t* req) - { - auto* request_state = static_cast(req->data); - - request_state->get()->complete( - [req](lua_State* L) - { - lua_pushboolean(L, req->result == 0); - uv_fs_req_cleanup(req); - - delete req; - - return 1; - } - ); - delete request_state; - } - ); - - if (err) + int nArgs = lua_gettop(L); + if (nArgs > 1) { - delete static_cast(req->data); - delete req; - luaL_errorL(L, "%s", uv_strerror(err)); + luaL_errorL(L, "exists: too many arguments supplied\n"); } - return lua_yield(L, 0); + const char* path = luaL_checkstring(L, 1); + + return exists_impl(L, path); } int type(lua_State* L) diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp index 6e311f7d1..91f5f7d67 100644 --- a/lute/fs/src/fs_impl.cpp +++ b/lute/fs/src/fs_impl.cpp @@ -1,5 +1,6 @@ #include "fs_impl.h" +#include "lute/fileutils.h" #include "lute/time.h" #include "lute/UVRequest.h" @@ -14,8 +15,6 @@ #include #endif -#include - #if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG) #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) #endif @@ -98,6 +97,19 @@ struct FSClose : FSRequest UVFile* file = nullptr; }; +struct FSPathPairRequest : FSRequest +{ + FSPathPairRequest(lua_State* L, const char* src, const char* dest) + : FSRequest(L) + , src(src) + , dest(dest) + { + } + + const std::string src; + const std::string dest; +}; + int open_impl(lua_State* L, const char* path, int flags, int mode) { uvutils::ScopedUVRequest req(L); @@ -402,6 +414,38 @@ int stat_impl(lua_State* L, const char* path) return lua_yield(L, 0); } +int exists_impl(lua_State* L, const char* path) +{ + uvutils::ScopedUVRequest req(L); + uv_fs_access( + uv_default_loop(), + &req->req, + path, + F_OK, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0 && result != UV_ENOENT) + { + r->fail("exists: Error checking existence of %s: %s", req->path, uv_strerror(result)); + return; + } + + r->succeed( + [exists = (result == 0)](lua_State* L) + { + lua_pushboolean(L, exists); + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + int type_impl(lua_State* L, const char* path) { uvutils::ScopedUVRequest req(L); @@ -434,6 +478,106 @@ int type_impl(lua_State* L, const char* path) return lua_yield(L, 0); } +int link_impl(lua_State* L, const char* path, const char* dest) +{ + uvutils::ScopedUVRequest req{L, path, dest}; + uv_fs_link( + uv_default_loop(), + &req->req, + path, + dest, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("link: Error creating link from %s to %s: %s", r->src.c_str(), r->dest.c_str(), uv_strerror(result)); + return; + } + + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); +} + +int symlink_impl(lua_State* L, const char* path, const char* dest) +{ + uvutils::ScopedUVRequest req{L, path, dest}; + int flags = 0; +#if _WIN32 + flags = Lute::isDirectory(path) ? UV_FS_SYMLINK_DIR : 0; +#endif + + uv_fs_symlink( + uv_default_loop(), + &req->req, + path, + dest, + flags, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("symlink: Error creating symlink from %s to %s: %s", r->src.c_str(), r->dest.c_str(), uv_strerror(result)); + return; + } + + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); +} + +int copy_impl(lua_State* L, const char* path, const char* dest) +{ + uvutils::ScopedUVRequest req{L, path, dest}; + uv_fs_copyfile( + uv_default_loop(), + &req->req, + path, + dest, + 0, + [](uv_fs_t* req) + { + auto r = uvutils::retake(req); + auto result = req->result; + + if (result < 0) + { + r->fail("copy: Error copying file from %s to %s: %s", r->src.c_str(), r->dest.c_str(), uv_strerror(result)); + return; + } + + r->succeed( + [](lua_State* L) + { + return 0; + } + ); + } + ); + + return lua_yield(L, 0); +} + int mkdir_impl(lua_State* L, const char* path, int mode) { uvutils::ScopedUVRequest req(L); diff --git a/lute/fs/src/fs_impl.h b/lute/fs/src/fs_impl.h index 227008bfa..17e908e1f 100644 --- a/lute/fs/src/fs_impl.h +++ b/lute/fs/src/fs_impl.h @@ -39,9 +39,15 @@ int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numByte int close_impl(lua_State* L, UVFile* handle); int remove_impl(lua_State* L, const char* path); + int stat_impl(lua_State* L, const char* path); +int exists_impl(lua_State* L, const char* path); int type_impl(lua_State* L, const char* path); +int link_impl(lua_State* L, const char* path, const char* dest); +int symlink_impl(lua_State* L, const char* path, const char* dest); +int copy_impl(lua_State* L, const char* path, const char* dest); + int mkdir_impl(lua_State* L, const char* path, int mode); int rmdir_impl(lua_State* L, const char* path); int listdir_impl(lua_State* L, const char* path); From da6fa502df99fc081250dc2768f5a77e2cb74130 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:03:56 -0800 Subject: [PATCH 294/642] Adds initial `lute doc` CLI (#748) Initial version of a new `lute doc` Command Line Interface! tldr; this is basically a refactor of the `scripts/reference.luau` file via CLI to make reviewing easier Details: This currently follows the exact logic of `scripts/reference.luau` that string matches generating the `@lute` and `@std` parts of lute.luau.org/reference as well as the default pages, so it's very much hard coded and not intended on being used by any other repo yet, but I wanted to get the CLI set up and iterate incrementally on this afterwards to avoid massive PRs. Current workflow: ```bash $ lute doc ``` is the equivalent of ```bash $ lute run scripts/reference.luau ``` There's an option to specify the output directory, which github actions needs because it's run in a different cwd I think? So you can also run with ```bash $ lute doc -o ./specified-docs-folder ``` That generates this lute repo's `definitions/` and `std/` docs the exact same way the website shows it as. #### Immediate next steps (PRs): 1. Refactor and add an `--module` option that specifies which module to generate, so the current `generateAllDocs` won't be hard coding the `definitions` and `std` paths. 1. Note: for this I'd then want `--module` to be a required option and not have the CLI generate docs for everything since the script isn't reliable yet. 3. Update the `processDirectory` to use the `typeofmodule` API in `@lute/lute` that gets the actual type info of the module instead of this string matching pattern. 1. `luau.typeofmodule` is currently returning a string representation of the module type so this code will probably look a bit ugly too until we're unblocked on having that API be represented in some data structure form. 5. Have `lute doc` generate docs for everything in the current working directory by default, so `generateAllDocs` actually generates all docs. then there's more after that but just listing these for now! --- .github/workflows/ci.yml | 20 ++-- .github/workflows/docs.yml | 20 ++-- docs/package.json | 2 +- {docs/test => examples/docs}/test_module.luau | 0 {docs/test => examples/docs}/test_module.md | 0 .../cli/commands/doc/init.luau | 112 ++++++++++++------ tests/cli/doc.test.luau | 56 +++++++++ 7 files changed, 160 insertions(+), 50 deletions(-) rename {docs/test => examples/docs}/test_module.luau (100%) rename {docs/test => examples/docs}/test_module.md (100%) rename docs/scripts/reference.luau => lute/cli/commands/doc/init.luau (78%) create mode 100644 tests/cli/doc.test.luau diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bb8d022a..fe5d82a2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,21 +111,27 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - - - name: Install tools - uses: Roblox/setup-foreman@v3 + - name: Setup and Build Lute + id: build_lute + uses: ./.github/actions/setup-and-build with: + target: Lute.CLI + config: debug + options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} - allow-external-github-orgs: true + use-bootstrap: true + + - name: Setup Node.js + uses: actions/setup-node@v4 - name: Install dependencies run: npm install working-directory: docs - name: Build docs site - run: npm run build + run: | + export PATH="$(dirname '${{ steps.build_lute.outputs.exe_path }}'):$PATH" + npm run build working-directory: docs - name: Upload docs artifact diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ffda5489c..b1b43a878 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,21 +13,27 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - - - name: Install tools - uses: Roblox/setup-foreman@v3 + - name: Setup and Build Lute + id: build_lute + uses: ./.github/actions/setup-and-build with: + target: Lute.CLI + config: debug + options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} - allow-external-github-orgs: true + use-bootstrap: true + + - name: Setup Node.js + uses: actions/setup-node@v4 - name: Install dependencies run: npm install working-directory: docs - name: Build docs site - run: npm run build + run: | + export PATH="$(dirname '${{ steps.build_lute.outputs.exe_path }}'):$PATH" + npm run build working-directory: docs - name: Upload docs artifact diff --git a/docs/package.json b/docs/package.json index 4764fb0e4..96d0c7aa9 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "scripts": { - "gen": "lute run scripts/reference.luau", + "gen": "lute doc -o ./", "dev": "npm run gen && vitepress dev", "build": "npm run gen && vitepress build", "preview": "vitepress preview" diff --git a/docs/test/test_module.luau b/examples/docs/test_module.luau similarity index 100% rename from docs/test/test_module.luau rename to examples/docs/test_module.luau diff --git a/docs/test/test_module.md b/examples/docs/test_module.md similarity index 100% rename from docs/test/test_module.md rename to examples/docs/test_module.md diff --git a/docs/scripts/reference.luau b/lute/cli/commands/doc/init.luau similarity index 78% rename from docs/scripts/reference.luau rename to lute/cli/commands/doc/init.luau index 0a97407e4..1133362e8 100644 --- a/docs/scripts/reference.luau +++ b/lute/cli/commands/doc/init.luau @@ -1,8 +1,11 @@ +local cli = require("./lib/cli") local fs = require("@std/fs") -local process = require("@std/process") local path = require("@std/path") +local process = require("@std/process") local stringext = require("@std/stringext") +local DEFAULT_OUTPUT_DIR = path.resolve(process.cwd(), "docs") + local STDLIB_MODULE_ALIASES = { assert = "assertions", fs = "fslib", @@ -30,6 +33,22 @@ type PropertySignature = { blockEq: string?, } +local function printHelp() + print([[ +Usage: lute doc [OPTIONS] + +Generate Markdown documentation for Luau module(s). + +OPTIONS: + -h, --help Show this help message + -o, --output=DIR Output directory for generated docs (default: ./docs in current working directory) + +EXAMPLES: + lute doc Generate docs to default ./docs directory + lute doc --output ./my-docs-folder Generate docs to custom directory ./my-docs-folder +]]) +end + -- extractPropertySignature: (this function runs on one line at a time so we need to keep some states for block comments) -- module (module name) -- line (current line) @@ -271,23 +290,6 @@ local function processDirectory( end end -local referenceBasePath = path.join(process.cwd(), "reference") -fs.createdirectory(referenceBasePath, { makeparents = true }) - --- Generate reference index with frontmatter for sidebar ordering -local referenceIndex = [[--- -order: 3 ---- - -# Reference - -API reference documentation for Lute's built-in libraries. - -- [Lute Libraries](./lute/index.md) - Core runtime libraries (`@lute/*`) -- [Standard Libraries](./std/index.md) - Standard library modules (`@std/*`) -]] -fs.writestringtofile(path.join(referenceBasePath, "index.md"), referenceIndex) - -- Helper to generate index with table of children local function generateLibraryIndex(title: string, alias: string, modulePath: path.pathlike): string local entries = fs.listdirectory(modulePath) @@ -329,26 +331,66 @@ local function generateLibraryIndex(title: string, alias: string, modulePath: pa return table.concat(lines, "\n") end -local definitionsPath = path.join(process.cwd(), "..", "definitions") -local referencePath = path.join(referenceBasePath, "lute") -processDirectory(definitionsPath, referencePath, definitionsPath, "lute") +local function generateAllDocs(outputDir: path.path): () + local referenceBasePath = path.join(outputDir, "reference") + fs.createdirectory(referenceBasePath, { makeparents = true }) --- Generate lute subfolder index with table of modules -local luteIndex = generateLibraryIndex("lute", "lute", definitionsPath) -fs.writestringtofile(path.join(referencePath, "index.md"), luteIndex) + -- Generate reference index with frontmatter for sidebar ordering + local referenceIndex = [[--- +order: 3 +--- + +# Reference + +API reference documentation for Lute's built-in libraries. -local stdlibPath = path.join(process.cwd(), "..", "lute", "std", "libs") -local stdlibReferencePath = path.join(referenceBasePath, "std") -processDirectory(stdlibPath, stdlibReferencePath, stdlibPath, "std") +- [Lute Libraries](./lute/index.md) - Core runtime libraries (`@lute/*`) +- [Standard Libraries](./std/index.md) - Standard library modules (`@std/*`) + ]] + fs.writestringtofile(path.join(referenceBasePath, "index.md"), referenceIndex) --- Generate std subfolder index with table of modules -local stdIndex = generateLibraryIndex("std", "std", stdlibPath) -fs.writestringtofile(path.join(stdlibReferencePath, "index.md"), stdIndex) + local definitionsPath = path.join(process.cwd(), "..", "definitions") + local referencePath = path.join(referenceBasePath, "lute") + processDirectory(definitionsPath, referencePath, definitionsPath, "lute") --- Mock module in test/ for testing purposes + -- Generate lute subfolder index with table of modules + local luteIndex = generateLibraryIndex("lute", "lute", definitionsPath) + fs.writestringtofile(path.join(referencePath, "index.md"), luteIndex) --- local testPath = path.join(process.cwd(), "test") --- local testReferencePath = path.join(process.cwd(), "test") --- processDirectory(testPath, testReferencePath, testPath, "test") + local stdlibPath = path.join(process.cwd(), "..", "lute", "std", "libs") + local stdlibReferencePath = path.join(referenceBasePath, "std") + processDirectory(stdlibPath, stdlibReferencePath, stdlibPath, "std") + + -- Generate std subfolder index with table of modules + local stdIndex = generateLibraryIndex("std", "std", stdlibPath) + fs.writestringtofile(path.join(stdlibReferencePath, "index.md"), stdIndex) +end + +local function main(...: string) + local args = cli.parser() + + args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) + args:add( + "output", + "option", + { help = "Output directory for generated docs", default = tostring(DEFAULT_OUTPUT_DIR), aliases = { "o" } } + ) + + args:parse({ ... }) + + if args:has("help") then + printHelp() + return + end + + local outputDir = path.parse(args:get("output") or tostring(DEFAULT_OUTPUT_DIR)) + if not path.isabsolute(outputDir) then + outputDir = path.resolve(process.cwd(), outputDir) + end + + print("Generating documentation...") + generateAllDocs(outputDir) + print("Documentation generation complete! Files written to: ", tostring(outputDir)) +end -print("Reference documentation generation complete!") +main(...) diff --git a/tests/cli/doc.test.luau b/tests/cli/doc.test.luau new file mode 100644 index 000000000..a271b11a4 --- /dev/null +++ b/tests/cli/doc.test.luau @@ -0,0 +1,56 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local tmpDir = system.tmpdir() +local tmpOutputDir = path.join(tmpDir, "lute-doc-output") +local lutePath = path.format(process.execpath()) + +local USAGE = "Usage: lute doc [OPTIONS]" + +-- Will add specific module test case (see examples/docs) once --module option is added +test.suite("lute-doc", function(suite) + suite:case("lute doc help message", function(assert) + local result = process.run({ lutePath, "doc", "-h" }) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, USAGE) + + result = process.run({ lutePath, "doc", "--h" }) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, USAGE) + + result = process.run({ lutePath, "doc", "--help" }) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, USAGE) + end) + + suite:case("generate test docs default directory", function(assert) + local result = process.system(`cd docs && {lutePath} doc`) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, "Documentation generation complete!") + end) + + suite:case("generate test docs specified directory", function(assert) + if fs.exists(tmpOutputDir) then + fs.removedirectory(tmpOutputDir, { recursive = true }) + end + + fs.createdirectory(tmpOutputDir) + + local result = process.system(`cd docs && {lutePath} doc -o {path.format(tmpOutputDir)}`) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, "Documentation generation complete!") + assert.strcontains(result.stdout, tostring(tmpOutputDir)) -- "Files written to: " + + assert.eq(fs.exists(path.join(tmpOutputDir, "reference", "lute", "fs.md")), true) + + fs.removedirectory(tmpOutputDir, { recursive = true }) + end) +end) From 26ebb02b508e40bc2682769b24cdd733e409c9b3 Mon Sep 17 00:00:00 2001 From: Alexander Robert <171880829+Alexander-L-Robert@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:52:26 -0800 Subject: [PATCH 295/642] Group tableext methods pertaining to arrays and dicts (#700) I reordered tableext methods to group dictionary and array methods more distinctly (I ensured `keys` and `toset` are at the boundary as they convert between the two table formats) This PR is intended to be merged after: - https://github.com/luau-lang/lute/pull/699 --- lute/std/libs/tableext.luau | 60 +++++++++++++++++++----------------- tests/std/tableext.test.luau | 4 +++ 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau index 6f191c6b6..41c044410 100644 --- a/lute/std/libs/tableext.luau +++ b/lute/std/libs/tableext.luau @@ -5,6 +5,8 @@ local tableext = {} +-- Functions on general tables + function tableext.map(table: { [K]: A }, f: (A) -> B): { [K]: B } local new = {} @@ -27,34 +29,6 @@ function tableext.filter(table: { [K]: V }, predicate: (V) -> boolean): { return new end -function tableext.any(arr: { T }, predicate: (T) -> boolean): boolean - for _, v in arr do - if predicate(v) then - return true - end - end - return false -end - -function tableext.all(arr: { T }, predicate: (T) -> boolean): boolean - for _, v in arr do - if not predicate(v) then - return false - end - end - return true -end - -function tableext.extend(tbl: { T }, ...: { T }) - local extensions = table.pack(...) - extensions.n = nil :: any - for _, extension in extensions do - for _, entry in extension do - table.insert(tbl, entry) - end - end -end - function tableext.combine(tbl: { [K]: V }, ...: { [K]: V }): { [K]: V } local tables = table.pack(...) tables.n = nil :: any @@ -74,6 +48,8 @@ function tableext.keys(tbl: { [K]: V }): { K } return keys end +-- Functions on array-like tables + function tableext.toset(tbl: { T }): { [T]: true } local set: { [T]: true } = {} for _, v in tbl do @@ -82,6 +58,16 @@ function tableext.toset(tbl: { T }): { [T]: true } return set end +function tableext.extend(tbl: { T }, ...: { T }) + local extensions = table.pack(...) + extensions.n = nil :: any + for _, extension in extensions do + for _, entry in extension do + table.insert(tbl, entry) + end + end +end + function tableext.reverse(tbl: { T }, inplace: boolean?) local new = if inplace then tbl else {} local i, j = 1, #tbl @@ -93,4 +79,22 @@ function tableext.reverse(tbl: { T }, inplace: boolean?) return new end +function tableext.any(arr: { T }, predicate: (T) -> boolean): boolean + for _, v in arr do + if predicate(v) then + return true + end + end + return false +end + +function tableext.all(arr: { T }, predicate: (T) -> boolean): boolean + for _, v in arr do + if not predicate(v) then + return false + end + end + return true +end + return table.freeze(tableext) diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index 8cf38a31c..c4388153f 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -96,6 +96,10 @@ test.suite("TableExt", function(suite) assert.eq(setB["d"], nil) assert.eq(#tableext.keys(setB), 3) assert.neq(#tableext.keys(setB), #b) + + local empty = {} + local emptySet = tableext.toset(empty) + assert.eq(#emptySet, 0) end) suite:case("any", function(assert) From d9c20cbb691335e980ea2d38df8039d0efc5e887 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:56:51 -0800 Subject: [PATCH 296/642] Adds module path specification to `lute doc` CLI (#754) Next step towards lute doc gen! 1st follow up from https://github.com/luau-lang/lute/pull/748 `lute doc` now requires module paths to be specified: ```bash Usage: lute doc [OPTIONS] [...MODULE_PATHS] ``` Example: ```bash $ lute doc -o mydocs module1 module2 ``` For the Lute repo, this would be: ```bash $ lute doc definitions lute/std/libs ``` (since the default directory is `docs/`) Will find the `definitions` and `lute/std/libs` folder from current working directory and generate docs for them. The module paths can be relative or absolute as well! This example is basically what the github actions bot runs to generate the lute site! I also don't really know how to show the output other than linking the docs artifact that github ran that shows the folder layout? Artifact download URL: https://github.com/luau-lang/lute/actions/runs/21268929608/artifacts/5227316204 --- docs/package.json | 2 +- lute/cli/commands/doc/init.luau | 72 +++++++++++++++++++++++---------- tests/cli/doc.test.luau | 18 ++++++--- 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/docs/package.json b/docs/package.json index 96d0c7aa9..f6660518f 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "scripts": { - "gen": "lute doc -o ./", + "gen": "lute doc -o ./ ../definitions ../lute/std/libs", "dev": "npm run gen && vitepress dev", "build": "npm run gen && vitepress build", "preview": "vitepress preview" diff --git a/lute/cli/commands/doc/init.luau b/lute/cli/commands/doc/init.luau index 1133362e8..4575efab7 100644 --- a/lute/cli/commands/doc/init.luau +++ b/lute/cli/commands/doc/init.luau @@ -4,6 +4,7 @@ local path = require("@std/path") local process = require("@std/process") local stringext = require("@std/stringext") +local USAGE = "Usage: lute doc [OPTIONS] [...MODULE_PATHS]" local DEFAULT_OUTPUT_DIR = path.resolve(process.cwd(), "docs") local STDLIB_MODULE_ALIASES = { @@ -34,18 +35,23 @@ type PropertySignature = { } local function printHelp() + print(USAGE) print([[ -Usage: lute doc [OPTIONS] - Generate Markdown documentation for Luau module(s). OPTIONS: -h, --help Show this help message -o, --output=DIR Output directory for generated docs (default: ./docs in current working directory) +MODULE_PATHS: + Path(s) to Luau module(s) to generate documentation for. Only files with .luau or .lua extensions will be processed. + Paths can be absolute or relative to the current working directory. + EXAMPLES: - lute doc Generate docs to default ./docs directory - lute doc --output ./my-docs-folder Generate docs to custom directory ./my-docs-folder + lute doc definitions Generate docs for definitions module to the default directory ./docs + lute doc --output ./my-docs-folder definitions Generate docs for definitions module to custom directory ./my-docs-folder + lute doc definitions lute/std/libs Generate docs for specific modules located at ./definitions and ./lute/std/libs + lute doc ./absolute/module/path Generate docs for module at the specified absolute path ]]) end @@ -331,7 +337,8 @@ local function generateLibraryIndex(title: string, alias: string, modulePath: pa return table.concat(lines, "\n") end -local function generateAllDocs(outputDir: path.path): () +local function generateAllDocs(outputDir: path.path, modulePaths: { string }): () + -- Create top-level reference index page local referenceBasePath = path.join(outputDir, "reference") fs.createdirectory(referenceBasePath, { makeparents = true }) @@ -343,27 +350,42 @@ order: 3 # Reference API reference documentation for Lute's built-in libraries. +]] + + -- Generate docs for each specified module path + for _, modulePath in modulePaths do + local modulePathObj = path.parse(modulePath) + local absModulePath = if path.isabsolute(modulePathObj) + then modulePathObj + else path.resolve(process.cwd(), modulePathObj) + + -- Explicitly match the aliases for lute and std, otherwise use the last part of the path as the alias + local libraryAlias = absModulePath.parts[#absModulePath.parts - 1] + if string.match(tostring(absModulePath), "/lute/std/libs") then + libraryAlias = "std" + elseif string.match(tostring(absModulePath), "/definitions") then + libraryAlias = "lute" + end -- [Lute Libraries](./lute/index.md) - Core runtime libraries (`@lute/*`) -- [Standard Libraries](./std/index.md) - Standard library modules (`@std/*`) - ]] - fs.writestringtofile(path.join(referenceBasePath, "index.md"), referenceIndex) + local documentationPath = path.join(referenceBasePath, libraryAlias) + processDirectory(absModulePath, documentationPath, absModulePath, libraryAlias) - local definitionsPath = path.join(process.cwd(), "..", "definitions") - local referencePath = path.join(referenceBasePath, "lute") - processDirectory(definitionsPath, referencePath, definitionsPath, "lute") + -- Generate module index with table of children + local moduleIndex = generateLibraryIndex(libraryAlias, libraryAlias, absModulePath) + fs.writestringtofile(path.join(documentationPath, "index.md"), moduleIndex) - -- Generate lute subfolder index with table of modules - local luteIndex = generateLibraryIndex("lute", "lute", definitionsPath) - fs.writestringtofile(path.join(referencePath, "index.md"), luteIndex) + -- Add to the reference index with the correct alias + referenceIndex ..= "\n\n- [" .. libraryAlias .. "](./" .. libraryAlias .. "/index.md)" - local stdlibPath = path.join(process.cwd(), "..", "lute", "std", "libs") - local stdlibReferencePath = path.join(referenceBasePath, "std") - processDirectory(stdlibPath, stdlibReferencePath, stdlibPath, "std") + -- Add the existing hard-coded info for lute and std + if libraryAlias == "lute" then + referenceIndex ..= " - Core runtime library (`@lute/*`)" + elseif libraryAlias == "std" then + referenceIndex ..= " - Standard library modules (`@std/*`)" + end + end - -- Generate std subfolder index with table of modules - local stdIndex = generateLibraryIndex("std", "std", stdlibPath) - fs.writestringtofile(path.join(stdlibReferencePath, "index.md"), stdIndex) + fs.writestringtofile(path.join(referenceBasePath, "index.md"), referenceIndex) end local function main(...: string) @@ -388,8 +410,14 @@ local function main(...: string) outputDir = path.resolve(process.cwd(), outputDir) end + local modulePaths = args:forwarded() + assert( + modulePaths ~= nil and #modulePaths > 0, + "Error: No module paths specified.\n\n" .. USAGE .. "\n\nUse --help for more information." + ) + print("Generating documentation...") - generateAllDocs(outputDir) + generateAllDocs(outputDir, modulePaths) print("Documentation generation complete! Files written to: ", tostring(outputDir)) end diff --git a/tests/cli/doc.test.luau b/tests/cli/doc.test.luau index a271b11a4..8143522ec 100644 --- a/tests/cli/doc.test.luau +++ b/tests/cli/doc.test.luau @@ -12,7 +12,7 @@ local USAGE = "Usage: lute doc [OPTIONS]" -- Will add specific module test case (see examples/docs) once --module option is added test.suite("lute-doc", function(suite) - suite:case("lute doc help message", function(assert) + suite:case("lute-doc-help-message", function(assert) local result = process.run({ lutePath, "doc", "-h" }) assert.eq(result.exitcode, 0) @@ -29,27 +29,35 @@ test.suite("lute-doc", function(suite) assert.strcontains(result.stdout, USAGE) end) - suite:case("generate test docs default directory", function(assert) - local result = process.system(`cd docs && {lutePath} doc`) + suite:case("lute-doc-no-modules-provided", function(assert) + local result = process.system(`{lutePath} doc`) + + assert.eq(result.exitcode, 1) + assert.strcontains(result.stderr, "Error: No module paths specified.") + end) + + suite:case("lute-doc-default-directory", function(assert) + local result = process.system(`{lutePath} doc definitions lute/std/libs`) assert.eq(result.exitcode, 0) assert.strcontains(result.stdout, "Documentation generation complete!") end) - suite:case("generate test docs specified directory", function(assert) + suite:case("lute-doc-specified-directory", function(assert) if fs.exists(tmpOutputDir) then fs.removedirectory(tmpOutputDir, { recursive = true }) end fs.createdirectory(tmpOutputDir) - local result = process.system(`cd docs && {lutePath} doc -o {path.format(tmpOutputDir)}`) + local result = process.system(`{lutePath} doc -o {path.format(tmpOutputDir)} definitions lute/std/libs`) assert.eq(result.exitcode, 0) assert.strcontains(result.stdout, "Documentation generation complete!") assert.strcontains(result.stdout, tostring(tmpOutputDir)) -- "Files written to: " assert.eq(fs.exists(path.join(tmpOutputDir, "reference", "lute", "fs.md")), true) + assert.eq(fs.exists(path.join(tmpOutputDir, "reference", "std", "fs.md")), true) fs.removedirectory(tmpOutputDir, { recursive = true }) end) From 799bad3d562bee8bf9deddc1afbfc6efe4e48f8c Mon Sep 17 00:00:00 2001 From: Raymond Ng Date: Thu, 22 Jan 2026 17:02:49 -0800 Subject: [PATCH 297/642] Fix `lute compile` fails to detect a luaurc when the entry point is not in the same directory as the luaurc (#753) Addresses https://github.com/luau-lang/lute/issues/708 In the linked issue, removing the entry point limit to the `.luaurc` discovery doesn't fix the issue. When doing so, `src` doesn't exist as a path in the bundle VFS and it crashes at runtime when trying to `require("@ext/bat")` The LCR calculation calculates `LCR("repo/src/entry.luau", "repo/src/batteries/bat.luau") = src`, and the final bundle VFS gets laid out as: ``` @bundle entry.luau batteries bat.luau ``` The `.luaurc` has this, which doesn't actually match with the bundle VFS. ``` "aliases": { "ext": "./src/batteries/" } ``` So when trying to `require("@ext/bat")`, lute tries to navigate to `@bundle/src/batteries`. `"@bundle/src"` doesn't exist and crashes [here](https://github.com/luau-lang/lute/blob/e11f8b0c0d933e7bbd0463c9213a364fb966f920/lute/require/src/modulepath.cpp#L119). This fix adds any discovered `.luaurc` to the LCR calculation, which makes the paths between the bundle VFS and the `.luaurc` at the repo root symmetric. Alternatively, could have used the LCR to strip prefixes in the bundled `.luaurc`. --- lute/cli/src/staticrequires.cpp | 29 +++++--------- tests/src/compile.test.cpp | 38 +++++++++++++++++++ tests/src/staticrequires.test.cpp | 28 ++++++++++++++ tests/src/staticrequires/.luaurc | 3 +- .../staticrequires/dep/nested/example.luau | 3 ++ .../src/staticrequires/dep/requirealias.luau | 10 +++++ 6 files changed, 91 insertions(+), 20 deletions(-) create mode 100644 tests/src/staticrequires/dep/nested/example.luau create mode 100644 tests/src/staticrequires/dep/requirealias.luau diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index 0a43f4941..cd7dd4031 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -53,18 +53,6 @@ void StaticRequireTracer::trace(const std::string& entryPoint) return; } - // Calculate the entry point directory - we should not look for .luaurc files beyond this directory - std::string entryPointDir = entryPoint; - size_t lastSlash = entryPointDir.find_last_of("/\\"); - if (lastSlash != std::string::npos) - { - entryPointDir = entryPointDir.substr(0, lastSlash); - } - else - { - entryPointDir = ""; - } - // Temporary set to collect absolute paths to .luaurc files Luau::DenseHashSet luaurcAbsolutePaths{""}; @@ -98,7 +86,7 @@ void StaticRequireTracer::trace(const std::string& entryPoint) { dir = dir.substr(0, lastSlash); - // Walk up the directory tree looking for .luaurc files, but stop at the entry point directory + // Walk up the directory tree looking for .luaurc files while (!dir.empty()) { std::string luaurcPath = dir + "/" + Luau::kConfigName; @@ -108,10 +96,6 @@ void StaticRequireTracer::trace(const std::string& entryPoint) break; } - // Stop if we've reached the entry point directory - if (dir == entryPointDir) - break; - // Move to parent directory size_t parentSlash = dir.find_last_of("/\\"); if (parentSlash == std::string::npos || parentSlash == 0) @@ -153,8 +137,15 @@ void StaticRequireTracer::trace(const std::string& entryPoint) // Store the resolved dependencies in the graph requireGraph[filePath] = std::move(resolvedDeps); } - - lowestCommonRoot = findLowestCommonRoot(discovered); + + // Include .luaurc files in the lowest common root calculation + std::vector allPaths = discovered; + for (const auto& luaurcPath : luaurcAbsolutePaths) + { + allPaths.push_back(luaurcPath); + } + + lowestCommonRoot = findLowestCommonRoot(allPaths); // Convert absolute .luaurc paths to LCR-relative .luaurc paths and read their content size_t commonRootLen = lowestCommonRoot.empty() ? 0 : lowestCommonRoot.length() + 1; // +1 for the trailing slash diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp index e26f15682..1f2011f28 100644 --- a/tests/src/compile.test.cpp +++ b/tests/src/compile.test.cpp @@ -538,3 +538,41 @@ TEST_CASE_FIXTURE(LuteFixture, "compile_command_e2e") // Clean up std::remove(outputExePath.c_str()); } + +TEST_CASE_FIXTURE(LuteFixture, "compile_command_e2e_requirealias") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + + // Use a test file that has the entry point and dependencies that are below the .luaurc + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires/dep/requirealias.luau"); + + // Create a temporary output path for the compiled executable + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_compiled_e2e_requirealias"); +#ifdef _WIN32 + outputExePath += ".exe"; +#endif + + // Build argv for cliMain: ["lute", "compile", , "--output", ] + char executablePlaceholder[] = "lute"; + char compileCommand[] = "compile"; + char outputFlag[] = "--output"; + + std::vector argv = {executablePlaceholder, compileCommand, testFilePath.data(), outputFlag, outputExePath.data()}; + + // Run the compile command + int compileResult = cliMain(argv.size(), argv.data(), getReporter()); + REQUIRE(compileResult == 0); + + // Verify the output file was created + std::ifstream checkFile(outputExePath, std::ios::binary); + REQUIRE(checkFile.is_open()); + checkFile.close(); + + // Now run the compiled executable to verify it works + std::vector runArgv = {outputExePath.data()}; + int runResult = cliMain(runArgv.size(), runArgv.data(), getReporter()); + CHECK(runResult == 0); + + // Clean up + std::remove(outputExePath.c_str()); +} diff --git a/tests/src/staticrequires.test.cpp b/tests/src/staticrequires.test.cpp index 3bc204988..0d2fe1707 100644 --- a/tests/src/staticrequires.test.cpp +++ b/tests/src/staticrequires.test.cpp @@ -122,6 +122,34 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_relative_paths") CHECK(luaurcFiles.find(".luaurc") != nullptr); } +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_requirealias") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires"); + std::string entryPoint = joinPaths(testDir, "dep/requirealias.luau"); + + StaticRequireTracer tracer{getReporter()}; + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); + + // requirealias.luau requires @nested/example, should resolve correctly + REQUIRE(pairs.size() == 2); + + CHECK(pairs[0].first == "dep/requirealias.luau"); + + std::string absoluteExample = joinPaths(tracer.getLowestCommonRoot(), "dep/nested/example.luau"); + CHECK(tracer.containsAbsolute(absoluteExample)); + + // Verify that the lowestCommonRoot is staticrequires, NOT dep even though entry point and dependency are in dep + CHECK(tracer.getLowestCommonRoot() == testDir); + + // Verify .luaurc file was discovered (should find it in parent directory) + auto luaurcFiles = tracer.getLuaurcFiles(); + REQUIRE(luaurcFiles.size() == 1); + CHECK(luaurcFiles.find(".luaurc") != nullptr); +} + TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_require_graph") { std::string luteProjectRoot = getLuteProjectRootAbsolute(); diff --git a/tests/src/staticrequires/.luaurc b/tests/src/staticrequires/.luaurc index ceb0531c6..2a20af3b1 100644 --- a/tests/src/staticrequires/.luaurc +++ b/tests/src/staticrequires/.luaurc @@ -1,5 +1,6 @@ { "aliases": { - "example": "./dep" + "example": "./dep", + "nested": "./dep/nested" } } diff --git a/tests/src/staticrequires/dep/nested/example.luau b/tests/src/staticrequires/dep/nested/example.luau new file mode 100644 index 000000000..b0d5a85f6 --- /dev/null +++ b/tests/src/staticrequires/dep/nested/example.luau @@ -0,0 +1,3 @@ +local v = { file = "example.luau" } + +return v diff --git a/tests/src/staticrequires/dep/requirealias.luau b/tests/src/staticrequires/dep/requirealias.luau new file mode 100644 index 000000000..9364dcfc6 --- /dev/null +++ b/tests/src/staticrequires/dep/requirealias.luau @@ -0,0 +1,10 @@ +-- Helper module that also requires using an alias +local example = require("@nested/example") + +-- print("Helper module") +return { + help = function() + return "helper" + end, + example = example, +} From 5d57b69a355802b38db14009fc5dda22cc438bc1 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 23 Jan 2026 10:13:45 -0800 Subject: [PATCH 298/642] Fixes excessive spinning of the runtime when there is no work to do (#755) This PR fixes https://github.com/luau-lang/lute/issues/703, which reports high CPU consumption when the runtime should have no work to do. A fix was contributed here https://github.com/luau-lang/lute/pull/704/changes, but ended up running into issues and blocking the runtime thread even though there were continuations to process. The modification needed here is to check if there are threads that need to be scheduled, or any continuations, and if there are, run in NO_WAIT mode. If there aren't any of these, then block until there is work on the event loop to do. This is only a patch - ideally, we would always run in UV_RUN_ONCE mode. Eventually, we should get rid of the continuations vector, and schedule continuations on the libuv event loop. At that point, we can delete these checks, and just pass UV_RUN_ONCE to the uv_run method. --- lute/runtime/src/runtime.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index e617ea420..5d5de2136 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -49,7 +49,8 @@ bool Runtime::hasWork() RuntimeStep Runtime::runOnce() { - uv_run(uv_default_loop(), UV_RUN_NOWAIT); + uv_run_mode mode = (hasContinuations() || hasThreads()) ? UV_RUN_NOWAIT : UV_RUN_ONCE; + uv_run(uv_default_loop(), mode); // Complete all C++ continuations std::vector> copy; From 19805ad73ccc107208b7c3744bb85d4d7a277c01 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:41:47 -0500 Subject: [PATCH 299/642] Support `target` in lint diagnostics (#752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can use additional diagnostic metadata like a `target` URI to surface additional information about a lint violation in Mandolin [(vscode API)](https://code.visualstudio.com/api/references/vscode-api#Diagnostic). This PR supports this by: - extending `LintViolation` type to include `target` - extending `Diagnostic` type to align with the vscode API - serializing `diagnostic.code` as an object with `value` and `target` fields, if the passed violation includes a `target` This provides a baseline that can be easily consumed by Mandolin to surface such metadata. One can imagine the convenience of being able to follow a lint violation to relevant docs, or something similar: Screenshot 2026-01-22 at 9 58 52 AM --- lute/cli/commands/lint/lsp/init.luau | 1 + lute/cli/commands/lint/lsp/types.luau | 1 + lute/cli/commands/lint/printer.luau | 4 ++++ lute/cli/commands/lint/types.luau | 1 + 4 files changed, 7 insertions(+) diff --git a/lute/cli/commands/lint/lsp/init.luau b/lute/cli/commands/lint/lsp/init.luau index 436134061..f6b4ece9c 100644 --- a/lute/cli/commands/lint/lsp/init.luau +++ b/lute/cli/commands/lint/lsp/init.luau @@ -44,6 +44,7 @@ function lsp.diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic range = getRange(lint.location), severity = severity(lint.severity), code = lint.lintname, + codeDescription = lint.target, source = "lute lint" :: "lute lint", -- LUAUFIX: Annotation needed, otherwise this errors because string isn't inferred as a singleton message = lint.message, tags = if lint.tags then table.freeze(tableext.map(lint.tags, tag)) else nil, diff --git a/lute/cli/commands/lint/lsp/types.luau b/lute/cli/commands/lint/lsp/types.luau index 6d6d08168..623605dee 100644 --- a/lute/cli/commands/lint/lsp/types.luau +++ b/lute/cli/commands/lint/lsp/types.luau @@ -2,6 +2,7 @@ export type Diagnostic = { read range: Range, -- zero-based read severity: number, -- 1: Error, 2: Warning, 3: Information, 4: Hint read code: string, -- lint name + read codeDescription: string?, -- optional property to describe the violation read source: "lute lint", read message: string, read tags: { number }?, -- 1: Unnecessary, 2: Deprecated diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau index 831cdf423..c6f492faf 100644 --- a/lute/cli/commands/lint/printer.luau +++ b/lute/cli/commands/lint/printer.luau @@ -79,6 +79,10 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () if lint.suggestedfix then print(`Suggested fix: {lint.suggestedfix.fix}\n`) end + + if lint.target then + print(`For additional info: {lint.target}\n`) + end end function printerLib.printLintsWithSource(lints: { types.LintViolation }, sourceContent: string): () diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index 0fb1872c9..e7ed05f2b 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -14,6 +14,7 @@ export type LintViolation = { read location: syntax.span?, -- if nil, applies to whole violation location read fix: string, }?, + read target: string?, -- Additional information on the violation (e.g link to docs) read tags: { tag }?, } From 4cd8fca4566553d088742b029d28c2e571156386 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:23:02 -0800 Subject: [PATCH 300/642] Embed and expose the package manager prototype under `lute pkg install` and `lute pkg run` (#715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Very early prototype that will likely change substantially in the next few weeks as we start getting feedback and iterate on the design. If the current directory has a `loom.config.luau` in it like the following example: ```luau -- loom.config.luau return { package = { name = "MyProject", version = "0.1.0", dependencies = { ["pretty-print"] = { rev = "main", source_kind = "github", source = "https://github.com/afujiwara-roblox/pretty-print" }, base64 = { rev = "main", source_kind = "github", source = "https://github.com/afujiwara-roblox/base64" }, toml = { rev = "main", source_kind = "github", source = "https://github.com/afujiwara-roblox/toml" }, }, }, } ``` Running `lute pkg install` will install the declared dependencies into a `Packages` folder in the current directory. (No global package cache support yet, but there are no technical reasons we can't do that—this is just simpler as a starting point.) When the dependencies are installed, a `loom.lock.luau` file will be generated. When this file is present in a project, we can opt-in to package awareness by using `lute pkg run `. With package awareness enabled, case-insensitive aliases to the installed packages are automatically made available to the Lute runtime, allowing us to run a script like this: ```luau -- These three dependencies will be required successfully if: -- 1. They were declared in a manifest file (`loom.config.luau`). -- 2. The `lute pkg install` subcommand successfully installed them into the `Packages` folder. -- 3. This script was run with package awareness enabled (`lute pkg run `). local toml = require("@toml") local prettyprint = require("@pretty-print") local base64 = require("@base64") local toml_content = [[ [config] key = "TG9vbSBpcyBzbyBjb29sISB3b3dvd293" key2 = "VGhlIGJlc3QgdGhpbmcgc2luY2Ugc2xpY2VkIGJyZWFkIQ==" ]] -- Deserialize the TOML content using the @toml library local config = toml.deserialize(toml_content) :: any print("------------------------------") print("This is the original config") print(config) print("------------------------------") print("Pretty printed original config:") print(prettyprint(config)) print("------------------------------") -- Decode the base64 encoded keys using the @base64 library config.config.key = buffer.tostring(base64.decode(buffer.fromstring(config.config.key))) config.config.key2 = buffer.tostring(base64.decode(buffer.fromstring(config.config.key2))) print("Pretty printed decoded config:") print(prettyprint(config)) print("------------------------------") ``` This prototype still has rough edges that we (primarily @afujiwara-roblox and @vrn-sn) hope to iron out as we receive feedback. --- lute/cli/commands/pkg/init.luau | 9 + lute/cli/commands/pkg/src/extern/pp.luau | 186 +++ .../commands/pkg/src/extern/unzip/LICENSE.md | 7 + .../commands/pkg/src/extern/unzip/crc.luau | 31 + .../pkg/src/extern/unzip/inflate.luau | 388 ++++++ .../commands/pkg/src/extern/unzip/init.luau | 1066 +++++++++++++++++ .../pkg/src/extern/unzip/utils/path.luau | 124 ++ .../src/extern/unzip/utils/validate_crc.luau | 22 + .../pkg/src/loom/commands/install.luau | 35 + .../pkg/src/loom/lockfile/lockfile.luau | 22 + lute/cli/commands/pkg/src/loom/loom.luau | 14 + lute/cli/commands/pkg/src/loom/loomtypes.luau | 10 + .../src/loom/manifest/projectmanifest.luau | 53 + .../pkg/src/loom/packagesource/git.luau | 79 ++ .../pkg/src/loom/resolver/package.luau | 36 + .../pkg/src/loom/resolver/resolution.luau | 46 + .../commands/pkg/src/loom/utils/fsutils.luau | 33 + lute/cli/src/climain.cpp | 9 +- tests/src/packagerequire.test.cpp | 5 +- 19 files changed, 2169 insertions(+), 6 deletions(-) create mode 100644 lute/cli/commands/pkg/init.luau create mode 100644 lute/cli/commands/pkg/src/extern/pp.luau create mode 100644 lute/cli/commands/pkg/src/extern/unzip/LICENSE.md create mode 100644 lute/cli/commands/pkg/src/extern/unzip/crc.luau create mode 100644 lute/cli/commands/pkg/src/extern/unzip/inflate.luau create mode 100644 lute/cli/commands/pkg/src/extern/unzip/init.luau create mode 100644 lute/cli/commands/pkg/src/extern/unzip/utils/path.luau create mode 100644 lute/cli/commands/pkg/src/extern/unzip/utils/validate_crc.luau create mode 100644 lute/cli/commands/pkg/src/loom/commands/install.luau create mode 100644 lute/cli/commands/pkg/src/loom/lockfile/lockfile.luau create mode 100644 lute/cli/commands/pkg/src/loom/loom.luau create mode 100644 lute/cli/commands/pkg/src/loom/loomtypes.luau create mode 100644 lute/cli/commands/pkg/src/loom/manifest/projectmanifest.luau create mode 100644 lute/cli/commands/pkg/src/loom/packagesource/git.luau create mode 100644 lute/cli/commands/pkg/src/loom/resolver/package.luau create mode 100644 lute/cli/commands/pkg/src/loom/resolver/resolution.luau create mode 100644 lute/cli/commands/pkg/src/loom/utils/fsutils.luau diff --git a/lute/cli/commands/pkg/init.luau b/lute/cli/commands/pkg/init.luau new file mode 100644 index 000000000..8177e82da --- /dev/null +++ b/lute/cli/commands/pkg/init.luau @@ -0,0 +1,9 @@ +local args = { ... } + +local loom = require("@self/src/loom/loom") + +if args[1] == "install" then + loom:install() +else + error("Usage: `lute pkg install`") +end diff --git a/lute/cli/commands/pkg/src/extern/pp.luau b/lute/cli/commands/pkg/src/extern/pp.luau new file mode 100644 index 000000000..413668cdf --- /dev/null +++ b/lute/cli/commands/pkg/src/extern/pp.luau @@ -0,0 +1,186 @@ +type ExistingOptions = { + rawStrings: boolean?, +} + +local isPrimitiveType = { string = true, number = true, boolean = true } + +local typeSortOrder: { [string]: number } = { + ["boolean"] = 1, + ["number"] = 2, + ["string"] = 3, + ["function"] = 4, + ["vector"] = 5, + ["buffer"] = 6, + ["thread"] = 7, + ["table"] = 8, + ["userdata"] = 9, + ["nil"] = 10, +} + +local function isPrimitiveArray(array: { [unknown]: unknown }): boolean + local max, len = 0, #array + + for key, value in array do + if type(key) ~= "number" then + return false + elseif key <= 0 then + return false + -- userdatas arent primitives + elseif not isPrimitiveType[type(value)] then + return false + end + + max = math.max(key, max) + end + + return len == max +end + +local function getFormattedAdress(t: {}): string + return `table<({t})>` +end + +local function formatValue(value: unknown, options: ExistingOptions?): string + if type(value) == "table" then + return getFormattedAdress(value) -- simple representation for table values + elseif type(value) ~= "string" then + return tostring(value) + end + + if options and options.rawStrings then + return `"{value}"` + end + return string.format("%q", value) +end + +local function formatKey(key: unknown, seq: boolean): string + if seq then + return "" + end + + if type(key) == "table" then + return `[{getFormattedAdress(key)}] = ` -- TODO: handling for table keys + end + if type(key) ~= "string" then + return `[{tostring(key)}] =` + end + + -- key is a simple identifier + if string.match(key, "^[%a_][%w_]-$") == key then + return `{key} = ` + end + + return `[{string.format("%q", key)}] = ` +end + +local function isEmpty(t: { [unknown]: unknown }): boolean + for _ in t do + return false + end + return true +end + +-- FIXME(luau): mark `dataTable` indexer as read-only +local function traverseTable( + dataTable: { [unknown]: unknown }, + seen: { [unknown]: boolean }, + indent: number, + options: ExistingOptions? +): string + local output = "" + local indentStr = string.rep(" ", indent) + + local keys = {} + + -- Collect all keys, not just primitives + for key in dataTable do + table.insert(keys, key) + end + + table.sort(keys, function(a: string, b: string): boolean + local typeofTableA, typeofTableB = typeof(dataTable[a]), typeof(dataTable[b]) + + if typeofTableA ~= typeofTableB then + return typeSortOrder[typeofTableA] < typeSortOrder[typeofTableB] + end + + if type(a) == "number" and type(b) == "number" then + return a < b + end + + return tostring(a) < tostring(b) + end) + + local inSequence = false + local previousKey = 0 + + for idx, key in keys do + if type(key) == "number" and key > 0 and key - 1 == previousKey then + previousKey = key + inSequence = true + else + inSequence = false + end + + local value = dataTable[key] + + if type(value) ~= "table" then + output = `{output}{indentStr}{formatKey(key, inSequence)}{formatValue(value, options)},\n` + continue + end + + -- prevents self-referential tables from looping infinitely + if seen[value] then + output = `{output}{indentStr}{formatKey(key, inSequence)}[Circular Reference <({value})>],\n` + continue + else + seen[value] = true + end + + if isEmpty(value :: { [unknown]: unknown }) then + output = string.format("%s%s%s{},\n", output, indentStr, formatKey(key, inSequence)) + continue + end + + if isPrimitiveArray(value :: { [unknown]: unknown }) then -- collapse primitive arrays + local outputConcatTbl = table.create(#value) :: { string } + + for valueIndex, valueInArray in value :: { unknown } do + outputConcatTbl[valueIndex] = formatValue(valueInArray) + end + + output = string.format( + "%s%s%s{%*},\n", + output, + indentStr, + formatKey(key, inSequence), + table.concat(outputConcatTbl, ", ") + ) + continue + end + + output = string.format( + "%s%s%s{\n%s%s},\n", + output, + indentStr, + formatKey(key, inSequence), + traverseTable(value :: any, seen, indent + 1, options), + indentStr + ) + + seen[value] = nil + end + + return output +end + +return function(data: unknown, options: ExistingOptions?): string + options = options or {} :: ExistingOptions + + -- if it's not a primitive, we'll pretty print it as a value + if type(data) ~= "table" then + return formatValue(data, options) + end + + return `\{\n{traverseTable(data :: { [unknown]: unknown }, { [data] = true }, 1, options)}\}` +end diff --git a/lute/cli/commands/pkg/src/extern/unzip/LICENSE.md b/lute/cli/commands/pkg/src/extern/unzip/LICENSE.md new file mode 100644 index 000000000..21d615a34 --- /dev/null +++ b/lute/cli/commands/pkg/src/extern/unzip/LICENSE.md @@ -0,0 +1,7 @@ +Copyright © 2024 0x5eal + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/lute/cli/commands/pkg/src/extern/unzip/crc.luau b/lute/cli/commands/pkg/src/extern/unzip/crc.luau new file mode 100644 index 000000000..38aac2d4e --- /dev/null +++ b/lute/cli/commands/pkg/src/extern/unzip/crc.luau @@ -0,0 +1,31 @@ +local CRC32_TABLE = table.create(256) + +-- TODO: Maybe compute this AoT as an optimization +-- Initialize the lookup table and lock it in place +for i = 0, 255 do + local crc = i + for _ = 1, 8 do + if bit32.band(crc, 1) == 1 then + crc = bit32.bxor(bit32.rshift(crc, 1), 0xEDB88320) + else + crc = bit32.rshift(crc, 1) + end + end + CRC32_TABLE[i] = crc +end + +table.freeze(CRC32_TABLE) + +local function crc32(buf: buffer): number + local crc = 0xFFFFFFFF + + for i = 0, buffer.len(buf) - 1 do + local byte = buffer.readu8(buf, i) + local index = bit32.band(bit32.bxor(crc, byte), 0xFF) + crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[index]) + end + + return bit32.bxor(crc, 0xFFFFFFFF) +end + +return crc32 diff --git a/lute/cli/commands/pkg/src/extern/unzip/inflate.luau b/lute/cli/commands/pkg/src/extern/unzip/inflate.luau new file mode 100644 index 000000000..36d6235cf --- /dev/null +++ b/lute/cli/commands/pkg/src/extern/unzip/inflate.luau @@ -0,0 +1,388 @@ +-- Tree class for storing Huffman trees used in DEFLATE decompression +local Tree = {} + +export type Tree = typeof(setmetatable({} :: TreeInner, { __index = Tree })) +type TreeInner = { + table: { number }, -- Length of 16, stores code length counts + trans: { number }, -- Length of 288, stores code to symbol translations +} + +--- Creates a new Tree instance with initialized tables +function Tree.new(): Tree + return setmetatable( + { + table = table.create(16, 0), + trans = table.create(288, 0), + } :: TreeInner, + { __index = Tree } + ) +end + +-- Data class for managing compression state and buffers +local Data = {} +export type Data = typeof(setmetatable({} :: DataInner, { __index = Data })) +-- stylua: ignore +export type DataInner = { + source: buffer, -- Input buffer containing compressed data + sourceIndex: number, -- Current position in source buffer + tag: number, -- Bit buffer for reading compressed data + bitcount: number, -- Number of valid bits in tag + + dest: buffer, -- Output buffer for decompressed data + destLen: number, -- Current length of decompressed data + + ltree: Tree, -- Length/literal tree for current block + dtree: Tree, -- Distance tree for current block +} + +--- Creates a new Data instance with initialized compression state +function Data.new(source: buffer, dest: buffer): Data + return setmetatable( + { + source = source, + sourceIndex = 0, + tag = 0, + bitcount = 0, + dest = dest, + destLen = 0, + ltree = Tree.new(), + dtree = Tree.new(), + } :: DataInner, + { __index = Data } + ) +end + +-- Static Huffman trees used for fixed block types +local staticLengthTree = Tree.new() +local staticDistTree = Tree.new() + +-- Tables for storing extra bits and base values for length/distance codes +local lengthBits = table.create(30, 0) +local lengthBase = table.create(30, 0) +local distBits = table.create(30, 0) +local distBase = table.create(30, 0) + +-- Special ordering of code length codes used in dynamic Huffman trees +-- stylua: ignore +local clcIndex = { + 16, 17, 18, 0, 8, 7, 9, 6, + 10, 5, 11, 4, 12, 3, 13, 2, + 14, 1, 15 +} + +-- Tree used for decoding code lengths in dynamic blocks +local codeTree = Tree.new() +local lengths = table.create(288 + 32, 0) + +--- Builds the extra bits and base tables for length and distance codes +local function buildBitsBase(bits: { number }, base: { number }, delta: number, first: number) + local sum = first + + -- Initialize the bits table with appropriate bit lengths + for i = 0, delta - 1 do + bits[i] = 0 + end + for i = 0, 29 - delta do + bits[i + delta] = math.floor(i / delta) + end + + -- Build the base value table using bit lengths + for i = 0, 29 do + base[i] = sum + sum += bit32.lshift(1, bits[i]) + end +end + +--- Constructs the fixed Huffman trees used in DEFLATE format +local function buildFixedTrees(lengthTree: Tree, distTree: Tree) + -- Build the fixed length tree according to DEFLATE specification + for i = 0, 6 do + lengthTree.table[i] = 0 + end + lengthTree.table[7] = 24 + lengthTree.table[8] = 152 + lengthTree.table[9] = 112 + + -- Populate the translation table for length codes + for i = 0, 23 do + lengthTree.trans[i] = 256 + i + end + for i = 0, 143 do + lengthTree.trans[24 + i] = i + end + for i = 0, 7 do + lengthTree.trans[24 + 144 + i] = 280 + i + end + for i = 0, 111 do + lengthTree.trans[24 + 144 + 8 + i] = 144 + i + end + + -- Build the fixed distance tree (simpler than length tree) + for i = 0, 4 do + distTree.table[i] = 0 + end + distTree.table[5] = 32 + + for i = 0, 31 do + distTree.trans[i] = i + end +end + +--- Temporary array for building trees +local offs = table.create(16, 0) + +--- Builds a Huffman tree from a list of code lengths +local function buildTree(t: Tree, lengths: { number }, off: number, num: number) + -- Initialize the code length count table + for i = 0, 15 do + t.table[i] = 0 + end + + -- Count the frequency of each code length + for i = 0, num - 1 do + t.table[lengths[off + i]] += 1 + end + + t.table[0] = 0 + + -- Calculate offsets for distribution sort + local sum = 0 + for i = 0, 15 do + offs[i] = sum + sum += t.table[i] + end + + -- Create the translation table mapping codes to symbols + for i = 0, num - 1 do + local len = lengths[off + i] + if len > 0 then + t.trans[offs[len]] = i + offs[len] += 1 + end + end +end + +--- Reads a single bit from the input stream +local function getBit(d: Data): number + if d.bitcount <= 0 then + d.tag = buffer.readu8(d.source, d.sourceIndex) + d.sourceIndex += 1 + d.bitcount = 8 + end + + local bit = bit32.band(d.tag, 1) + d.tag = bit32.rshift(d.tag, 1) + d.bitcount -= 1 + + return bit +end + +--- Reads multiple bits from the input stream with a base value +local function readBits(d: Data, num: number?, base: number): number + if not num then + return base + end + + -- Ensure we have enough bits in the buffer + while d.bitcount < 24 and d.sourceIndex < buffer.len(d.source) do + d.tag = bit32.bor(d.tag, bit32.lshift(buffer.readu8(d.source, d.sourceIndex), d.bitcount)) + d.sourceIndex += 1 + d.bitcount += 8 + end + + local val = bit32.band(d.tag, bit32.rshift(0xffff, 16 - num)) + d.tag = bit32.rshift(d.tag, num) + d.bitcount -= num + + return val + base +end + +--- Decodes a symbol using a Huffman tree +local function decodeSymbol(d: Data, t: Tree): number + while d.bitcount < 24 and d.sourceIndex < buffer.len(d.source) do + d.tag = bit32.bor(d.tag, bit32.lshift(buffer.readu8(d.source, d.sourceIndex), d.bitcount)) + d.sourceIndex += 1 + d.bitcount += 8 + end + + local sum, cur, len = 0, 0, 0 + local tag = d.tag + + -- Traverse the Huffman tree to find the symbol + repeat + cur = 2 * cur + bit32.band(tag, 1) + tag = bit32.rshift(tag, 1) + len += 1 + sum += t.table[len] + cur -= t.table[len] + until cur < 0 + + d.tag = tag + d.bitcount -= len + + return t.trans[sum + cur] +end + +--- Decodes the dynamic Huffman trees for a block +local function decodeTrees(d: Data, lengthTree: Tree, distTree: Tree) + local hlit = readBits(d, 5, 257) -- Number of literal/length codes + local hdist = readBits(d, 5, 1) -- Number of distance codes + local hclen = readBits(d, 4, 4) -- Number of code length codes + + -- Initialize code lengths array + for i = 0, 18 do + lengths[i] = 0 + end + + -- Read code lengths for the code length alphabet + for i = 0, hclen - 1 do + lengths[clcIndex[i + 1]] = readBits(d, 3, 0) + end + + -- Build the code lengths tree + buildTree(codeTree, lengths, 0, 19) + + -- Decode length/distance tree code lengths + local num = 0 + while num < hlit + hdist do + local sym = decodeSymbol(d, codeTree) + + if sym == 16 then + -- Copy previous code length 3-6 times + local prev = lengths[num - 1] + for _ = 1, readBits(d, 2, 3) do + lengths[num] = prev + num += 1 + end + elseif sym == 17 then + -- Repeat zero 3-10 times + for _ = 1, readBits(d, 3, 3) do + lengths[num] = 0 + num += 1 + end + elseif sym == 18 then + -- Repeat zero 11-138 times + for _ = 1, readBits(d, 7, 11) do + lengths[num] = 0 + num += 1 + end + else + -- Regular code length 0-15 + lengths[num] = sym + num += 1 + end + end + + -- Build the literal/length and distance trees + buildTree(lengthTree, lengths, 0, hlit) + buildTree(distTree, lengths, hlit, hdist) +end + +--- Inflates a block of data using Huffman trees +local function inflateBlockData(d: Data, lengthTree: Tree, distTree: Tree) + while true do + local sym = decodeSymbol(d, lengthTree) + + if sym == 256 then + -- End of block + return + end + + if sym < 256 then + -- Literal byte + buffer.writeu8(d.dest, d.destLen, sym) + d.destLen += 1 + else + -- Length/distance pair for copying + sym -= 257 + + local length = readBits(d, lengthBits[sym], lengthBase[sym]) + local dist = decodeSymbol(d, distTree) + + local offs = d.destLen - readBits(d, distBits[dist], distBase[dist]) + + -- Copy bytes from back reference + for i = offs, offs + length - 1 do + buffer.writeu8(d.dest, d.destLen, buffer.readu8(d.dest, i)) + d.destLen += 1 + end + end + end +end + +--- Processes an uncompressed block +local function inflateUncompressedBlock(d: Data) + -- Align to byte boundary + local bytesToMove = d.bitcount // 8 + d.sourceIndex -= bytesToMove + d.bitcount = 0 + d.tag = 0 + + -- Read block length and its complement + local length = buffer.readu8(d.source, d.sourceIndex + 1) + length = 256 * length + buffer.readu8(d.source, d.sourceIndex) + + local invlength = buffer.readu8(d.source, d.sourceIndex + 3) + invlength = 256 * invlength + buffer.readu8(d.source, d.sourceIndex + 2) + + -- Verify block length using ones complement + if length ~= bit32.bxor(invlength, 0xffff) then + error("Invalid block length") + end + + d.sourceIndex += 4 + + -- Copy uncompressed data to output + for _ = 1, length do + buffer.writeu8(d.dest, d.destLen, buffer.readu8(d.source, d.sourceIndex)) + d.destLen += 1 + d.sourceIndex += 1 + end + + d.bitcount = 0 +end + +--- Main decompression function that processes DEFLATE compressed data +local function uncompress(source: buffer, uncompressedSize: number?): buffer + local dest = buffer.create( + -- If the uncompressed size is known, we use that, otherwise we use a default + -- size that is a 7 times more than the compressed size; this factor works + -- well for most cases other than those with a very high compression ratio + uncompressedSize or buffer.len(source) * 7 + ) + local d = Data.new(source, dest) + + repeat + local bfinal = getBit(d) -- Last block flag + local btype = readBits(d, 2, 0) -- Block type (0=uncompressed, 1=fixed, 2=dynamic) + + if btype == 0 then + inflateUncompressedBlock(d) + elseif btype == 1 then + inflateBlockData(d, staticLengthTree, staticDistTree) + elseif btype == 2 then + decodeTrees(d, d.ltree, d.dtree) + inflateBlockData(d, d.ltree, d.dtree) + else + error("Invalid block type") + end + until bfinal == 1 + + -- Trim output buffer to actual size if needed + if d.destLen < buffer.len(dest) then + local result = buffer.create(d.destLen) + buffer.copy(result, 0, dest, 0, d.destLen) + return result + end + + return dest +end + +-- Initialize static trees and lookup tables for DEFLATE format +buildFixedTrees(staticLengthTree, staticDistTree) +buildBitsBase(lengthBits, lengthBase, 4, 3) +buildBitsBase(distBits, distBase, 2, 1) +lengthBits[28] = 0 +lengthBase[28] = 258 + +return uncompress diff --git a/lute/cli/commands/pkg/src/extern/unzip/init.luau b/lute/cli/commands/pkg/src/extern/unzip/init.luau new file mode 100644 index 000000000..d458d92e0 --- /dev/null +++ b/lute/cli/commands/pkg/src/extern/unzip/init.luau @@ -0,0 +1,1066 @@ +local inflate = require("@self/inflate") +local validateCrc = require("@self/utils/validate_crc") +local path = require("@self/utils/path") + +-- The maximum supported PKZIP format specification version that we support +local MAX_SUPPORTED_PKZIP_VERSION = 63 + +-- Little endian constant signatures used in the ZIP file format +local SIGNATURES = table.freeze({ + -- Marks the beginning of each file in the ZIP + LOCAL_FILE = 0x04034b50, + -- Marks the start of a data descriptor + DATA_DESCRIPTOR = 0x08074b50, + -- Marks entries in the central directory + CENTRAL_DIR = 0x02014b50, + -- Marks the end of the central directory + END_OF_CENTRAL_DIR = 0x06054b50, +}) + +-- Decompression routines for each supported compression method +local DECOMPRESSION_ROUTINES: { [number]: { name: CompressionMethod, decompress: (buffer, number, validateCrc.CrcValidationOptions) -> buffer } } = + { + -- `STORE` decompression method - No compression + [0x00] = { + name = "STORE" :: CompressionMethod, + decompress = function(buf, _, validation): buffer + validateCrc(buf, validation) + return buf + end, + }, + + -- `DEFLATE` decompression method - Compressed raw deflate chunks + [0x08] = { + name = "DEFLATE" :: CompressionMethod, + decompress = function(buf, uncompressedSize, validation): buffer + local decompressed = inflate(buf, uncompressedSize) + validateCrc(decompressed, validation) + return decompressed + end, + }, + } + +-- Set of placeholder entry properties for incompatible entries +local EMPTY_PROPERTIES: ZipEntryProperties = table.freeze({ + versionMadeBy = 0, + compressedSize = 0, + size = 0, + attributes = 0, + timestamp = 0, + crc = 0, +}) + +-- Lookup table for the OS that created the ZIP +local MADE_BY_OS_LOOKUP: { [number]: MadeByOS } = { + [0x0] = "FAT", + [0x1] = "AMIGA", + [0x2] = "VMS", + [0x3] = "UNIX", + [0x4] = "VM/CMS", + [0x5] = "Atari ST", + [0x6] = "OS/2", + [0x7] = "MAC", + [0x8] = "Z-System", + [0x9] = "CP/M", + [0xa] = "NTFS", + [0xb] = "MVS", + [0xc] = "VSE", + [0xd] = "Acorn RISCOS", + [0xe] = "VFAT", + [0xf] = "Alternate MVS", + [0x10] = "BeOS", + [0x11] = "TANDEM", + [0x12] = "OS/400", + [0x13] = "OS/X", +} + +--[=[ + @class ZipEntry + + A single entry (a file or a directory) in a ZIP file, and its properties. +]=] +local ZipEntry = {} + +--[=[ + @interface ZipEntry + @within ZipEntry + + @field name string -- File path within ZIP, '/' suffix indicates directory + @field versionMadeBy { software: string, os: MadeByOS } -- Version of software and OS that created the ZIP + @field compressedSize number -- Compressed size in bytes + @field size number -- Uncompressed size in bytes + @field offset number -- Absolute position of local header in ZIP + @field timestamp number -- MS-DOS format timestamp + @field method CompressionMethod -- Method used to compress the file + @field crc number -- CRC32 checksum of the uncompressed data + @field isDirectory boolean -- Whether the entry is a directory or not + @field isText boolean -- Whether the entry is plain ASCII text or binary + @field attributes number -- File attributes + @field parent ZipEntry? -- Parent directory entry, `nil` if entry is root + @field children { ZipEntry } -- Children of the entry, if it was a directory, empty array for files +]=] +export type ZipEntry = typeof(setmetatable({} :: ZipEntryInner, { __index = ZipEntry })) +type ZipEntryInner = { + name: string, + + versionMadeBy: { + software: string, + os: MadeByOS, + }, + + compressedSize: number, + size: number, + offset: number, + timestamp: number, + method: CompressionMethod, + crc: number, + isDirectory: boolean, + isText: boolean, + attributes: number, + parent: ZipEntry?, + children: { ZipEntry }, +} + +-- stylua: ignore +--[=[ + @within ZipEntry + @type MadeByOS "FAT" | "AMIGA" | "VMS" | "UNIX" | "VM/CMS" | "Atari ST" | "OS/2" | "MAC" | "Z-System" | "CP/M" | "NTFS" | "MVS" | "VSE" | "Acorn RISCOS" | "VFAT" | "Alternate MVS" | "BeOS" | "TANDEM" | "OS/400" | "OS/X" | "Unknown" + + The OS that created the ZIP. +]=] +export type MadeByOS = + | "FAT" -- 0x0; MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) + | "AMIGA" -- 0x1; Amiga + | "VMS" -- 0x2; OpenVMS + | "UNIX" -- 0x3; Unix + | "VM/CMS" -- 0x4; VM/CMS + | "Atari ST" -- 0x5; Atari ST + | "OS/2" -- 0x6; OS/2 HPFS + | "MAC" -- 0x7; Macintosh + | "Z-System" -- 0x8; Z-System + | "CP/M" -- 0x9; Original CP/M + | "NTFS" -- 0xa; Windows NTFS + | "MVS" -- 0xb; OS/390 & VM/ESA + | "VSE" -- 0xc; VSE + | "Acorn RISCOS" -- 0xd; Acorn RISCOS + | "VFAT" -- 0xe; VFAT + | "Alternate MVS" -- 0xf; Alternate MVS + | "BeOS" -- 0x10; BeOS + | "TANDEM" -- 0x11; Tandem + | "OS/400" -- 0x12; OS/400 + | "OS/X" -- 0x13; Darwin + | "Unknown" -- 0x14 - 0xff; Unused + +--[=[ + @within ZipEntry + @type CompressionMethod "STORE" | "DEFLATE" + + The method used to compress the file: + - `STORE` - No compression + - `DEFLATE` - Compressed raw deflate chunks +]=] +export type CompressionMethod = "STORE" | "DEFLATE" + +--[=[ + @interface ZipEntryProperties + @within ZipEntry + @private + + A set of properties that describe a ZIP entry. Used internally for construction of + [ZipEntry] objects. + + @field versionMadeBy number -- Version of software and OS that created the ZIP + @field compressedSize number -- Compressed size in bytes + @field size number -- Uncompressed size in bytes + @field attributes number -- File attributes + @field timestamp number -- MS-DOS format timestamp + @field method CompressionMethod? -- Method used + @field crc number -- CRC32 checksum of the uncompressed data +]=] +type ZipEntryProperties = { + versionMadeBy: number, + compressedSize: number, + size: number, + attributes: number, + timestamp: number, + method: CompressionMethod?, + crc: number, +} + +--[=[ + @within ZipEntry + @function new + @private + + @param offset number -- Offset of the entry in the ZIP file + @param name string -- File path within ZIP, '/' suffix indicates directory + @param properties ZipEntryProperties -- Properties of the entry + @return ZipEntry -- The constructed entry +]=] +function ZipEntry.new(offset: number, name: string, properties: ZipEntryProperties): ZipEntry + local versionMadeByOS = bit32.rshift(properties.versionMadeBy, 8) + local versionMadeByVersion = bit32.band(properties.versionMadeBy, 0x00ff) + + return setmetatable( + { + name = name, + versionMadeBy = { + software = string.format("%d.%d", versionMadeByVersion / 10, versionMadeByVersion % 10), + os = MADE_BY_OS_LOOKUP[versionMadeByOS] :: MadeByOS, + }, + compressedSize = properties.compressedSize, + size = properties.size, + offset = offset, + timestamp = properties.timestamp, + method = properties.method, + crc = properties.crc, + isDirectory = string.sub(name, -1) == "/", + attributes = properties.attributes, + parent = nil, + children = {}, + } :: ZipEntryInner, + { __index = ZipEntry } + ) +end + +--[=[ + @within ZipEntry + @method isSymlink + + Returns whether the entry is a symlink. + + @return boolean +]=] +function ZipEntry.isSymlink(self: ZipEntry): boolean + return bit32.band(self.attributes, 0xA0000000) == 0xA0000000 +end + +--[=[ + @within ZipEntry + @method getPath + + Resolves the path of the entry based on its relationship with other entries. It is recommended to use this + method instead of accessing the `name` property directly, although they should be equivalent. + + > [!WARNING] + > Never use this method when extracting files from the ZIP, since it can contain absolute paths + > (say `/etc/passwd`) referencing directories outside the current directory (say `/tmp/extracted`), + > causing unintended overwrites of files. + + @return string -- The path of the entry +]=] +function ZipEntry.getPath(self: ZipEntry): string + if self.name == "/" then + return "/" + end + + -- Get just the entry name without the path + local name = string.match(self.name, "([^/]+)/?$") or self.name + + if not self.parent or self.parent.name == "/" then + return self.name + end + + -- Combine parent path with entry name + local path = string.gsub(self.parent:getPath() .. name, "//+", "/") + return path +end + +--[=[ + @within ZipEntry + @method getSafePath + + Resolves the path of the entry based on its relationship with other entries and returns it + only if it is safe to use for extraction, otherwise returns `nil`. + + @return string? -- Optional path of the entry if it was safe +]=] +function ZipEntry.getSafePath(self: ZipEntry): string? + local pathStr = self:getPath() + + if path.isSafe(pathStr) then + return pathStr + end + + return nil +end + +--[=[ + @within ZipEntry + @method sanitizePath + + Sanitizes the path of the entry, potentially losing information, but ensuring the path is + safe to use for extraction. + + @return string -- The sanitized path of the entry +]=] +function ZipEntry.sanitizePath(self: ZipEntry): string + local pathStr = self:getPath() + return path.sanitize(pathStr) +end + +--[=[ + @within ZipEntry + @method compressionEfficiency + + Calculates the compression efficiency of the entry, or `nil` if the entry is a directory. + + Uses the formula: `round((1 - compressedSize / size) * 100)` and outputs a percentage. + + @return number? -- Optional compression efficiency of the entry +]=] +function ZipEntry.compressionEfficiency(self: ZipEntry): number? + if self.size == 0 or self.compressedSize == 0 then + return nil + end + + local ratio = 1 - self.compressedSize / self.size + return math.round(ratio * 100) +end + +--[=[ + @within ZipEntry + @method isFile + + Returns whether the entry is a file, i.e., not a directory or symlink. + + @return boolean -- Whether the entry is a file +]=] +function ZipEntry.isFile(self: ZipEntry): boolean + return not (self.isDirectory and self:isSymlink()) +end + +--[=[ + @within ZipEntry + @interface UnixMode + + A object representation of the UNIX mode. + + @field perms string -- The permission octal + @field typeFlags string -- The type flags octal +]=] +export type UnixMode = { perms: string, typeFlags: string } + +--[=[ + @within ZipEntry + @method unixMode + + Parses the entry's attributes to extract a UNIX mode, represented as a [UnixMode]. + + @return UnixMode? -- The UNIX mode of the entry, or `nil` if the entry is not a UNIX file +]=] +function ZipEntry.unixMode(self: ZipEntry): UnixMode? + if self.versionMadeBy.os ~= "UNIX" then + return nil + end + + local mode = bit32.rshift(self.attributes, 16) + local typeFlags = bit32.band(self.attributes, 0x1FF) + local perms = bit32.band(mode, 0x01FF) + + return { + perms = string.format("0o%o", perms), + typeFlags = string.format("0o%o", typeFlags), + } +end + +--[=[ + @class ZipReader + + The main class which represents a decoded state of a ZIP file, holding references + to its entries. This is the primary point of interaction with the ZIP file's contents. +]=] +local ZipReader = {} + +--[=[ + @interface ZipReader + @within ZipReader + + @field data buffer -- The buffer containing the raw bytes of the ZIP + @field comment string -- Comment associated with the ZIP + @field entries { ZipEntry } -- The decoded entries present + @field directories { [string]: ZipEntry } -- The directories and their respective entries + @field root ZipEntry -- The entry of the root directory +]=] +export type ZipReader = typeof(setmetatable({} :: ZipReaderInner, { __index = ZipReader })) +type ZipReaderInner = { + data: buffer, + comment: string, + entries: { ZipEntry }, + directories: { [string]: ZipEntry }, + root: ZipEntry, +} + +--[=[ + @within ZipReader + @function new + + Creates a new ZipReader instance from the raw bytes of a ZIP file. + + **Errors if the ZIP file is invalid.** + + @param data buffer -- The buffer containing the raw bytes of the ZIP + @return ZipReader -- The new ZipReader instance +]=] +function ZipReader.new(data): ZipReader + local root = ZipEntry.new(0, "/", EMPTY_PROPERTIES) + root.isDirectory = true + + local this = setmetatable( + { + data = data, + entries = {}, + directories = {}, + root = root, + } :: ZipReaderInner, + { __index = ZipReader } + ) + + this:parseCentralDirectory() + this:buildDirectoryTree() + return this +end + +--[=[ + @within ZipReader + @method findEocdPosition + @private + + Finds the position of the End of Central Directory (EoCD) signature in the ZIP file. This + implementation is inspired by that of [async_zip], a Rust library for parsing ZIP files + asynchronously. + + This method involves buffered reading in reverse and reverse linear searching along those buffers + for the EoCD signature. As a result of the buffered approach, we reduce individual reads when compared + to reading every single byte sequentially, by a factor of the buffer size (4 KB by default). The buffer + size of 4 KB was arrived at because it aligns with many systems' page sizes, and also provides a + good balance between read efficiency (not too small), memory usage (not too large) and CPU cache + performance. + + From my primitive benchmarks, this method is ~1.5x faster than the sequential approach. + + **Errors if the ZIP file is invalid.** + + [async_zip]: https://github.com/Majored/rs-async-zip/blob/527bda9/src/base/read/io/locator.rs#L37-L45 + + @error "Could not find End of Central Directory signature" + + @return number -- The offset to the End of Central Directory (including the signature) +]=] +function ZipReader.findEocdPosition(self: ZipReader): number + local BUFFER_SIZE = 4096 + local SIGNATURE_LENGTH = 4 + local bufSize = buffer.len(self.data) + + -- Start from the minimum possible position of EoCD (22 bytes from end) + local position = math.max(0, bufSize - (22 + 65536) --[[ max comment size: 64 KB ]]) + local searchBuf = buffer.create(BUFFER_SIZE) + + while position < bufSize do + local readSize = math.min(BUFFER_SIZE, bufSize - position) + buffer.copy(searchBuf, 0, self.data, position, readSize) + + -- Search backwards through buffer for signature + for i = readSize - 1, SIGNATURE_LENGTH - 1, -1 do + if buffer.readu32(searchBuf, i - SIGNATURE_LENGTH + 1) == SIGNATURES.END_OF_CENTRAL_DIR then + return position + i - SIGNATURE_LENGTH + 1 + end + end + + -- Move position backward with overlap for cross-boundary signatures + position += BUFFER_SIZE - SIGNATURE_LENGTH + end + + error("Could not find End of Central Directory signature") +end + +--[=[ + @within ZipReader + @interface EocdRecord + @private + + A parsed End of Central Directory record. + + @field diskNumber number -- The disk number + @field diskWithCD number -- The disk number of the disk with the Central Directory + @field cdEntries number -- The number of entries in the Central Directory + @field totalCDEntries number -- The total number of entries in the Central Directory + @field cdSize number -- The size of the Central Directory + @field cdOffset number -- The offset of the Central Directory + @field comment string -- The comment associated with the ZIP +]=] +export type EocdRecord = { + diskNumber: number, + diskWithCD: number, + cdEntries: number, + totalCDEntries: number, + cdSize: number, + cdOffset: number, + comment: string, +} + +--[=[ + @within ZipReader + @method parseEocdRecord + @private + + Parses the End of Central Directory record at the given position, usually located + using the [ZipReader:findEocdPosition]. + + **Errors if the ZIP file is invalid.** + + @error "Invalid Central Directory offset or size" + + @param pos number -- The offset to the End of Central Directory record + @return EocdRecord -- Structural representation of the parsed record +]=] +function ZipReader.parseEocdRecord(self: ZipReader, pos: number): EocdRecord + -- End of Central Directory format: + -- Offset Bytes Description + -- 0 4 End of central directory signature + -- 4 2 Number of this disk + -- 6 2 Disk where central directory starts + -- 8 2 Number of central directory records on this disk + -- 10 2 Total number of central directory records + -- 12 4 Size of central directory (bytes) + -- 16 4 Offset of start of central directory + -- 20 2 Comment length (n) + -- 22 n Comment + + local cdEntries = buffer.readu16(self.data, pos + 10) + local cdSize = buffer.readu32(self.data, pos + 12) + local cdOffset = buffer.readu32(self.data, pos + 16) + + -- Validate CD boundaries and entry count + local bufSize = buffer.len(self.data) + if cdOffset >= bufSize or cdOffset + cdSize > bufSize then + error("Invalid Central Directory offset or size") + end + + -- Validate CD size range; min = 46 bytes per entry, max = 0xFFFF * 3 + 46 bytes per entry + if cdSize < cdEntries * 46 or cdEntries * (0xFFFF * 3 + 46) < cdSize then + error("Invalid Central Directory size for claimed number of entries") + end + + local commentLength = buffer.readu16(self.data, pos + 20) + return { + diskNumber = buffer.readu16(self.data, pos + 4), + diskWithCD = buffer.readu16(self.data, pos + 6), + cdEntries = cdEntries, + totalCDEntries = buffer.readu16(self.data, pos + 8), + cdSize = cdSize, + cdOffset = cdOffset, + comment = buffer.readstring(self.data, pos + 22, commentLength), + } +end + +--[=[ + @within ZipReader + @method parseCentralDirectory + @private + + Parses the central directory of the ZIP file and populates the `entries` and `directories` + fields. Used internally during initialization of the [ZipReader]. + + **Errors if the ZIP file is invalid.** + + @error "Invalid Central Directory entry signature" + @error "Found different entries than specified in Central Directory" +]=] +function ZipReader.parseCentralDirectory(self: ZipReader): () + local eocdPos = self:findEocdPosition() + local record = self:parseEocdRecord(eocdPos) + + -- Track actual entries found + local entriesFound = 0 + local pos = record.cdOffset + while pos < record.cdOffset + record.cdSize do + if buffer.readu32(self.data, pos) ~= SIGNATURES.CENTRAL_DIR then + error("Invalid Central Directory entry signature") + end + + -- Central Directory Entry format: + -- Offset Bytes Description + -- 0 4 Central directory entry signature + -- 4 2 Version made by + -- 8 2 General purpose bitflags + -- 10 2 Compression method (8 = DEFLATE) + -- 12 4 Last mod time/date + -- 16 4 CRC-32 + -- 20 4 Compressed size + -- 24 4 Uncompressed size + -- 28 2 File name length (n) + -- 30 2 Extra field length (m) + -- 32 2 Comment length (k) + -- 36 2 Internal file attributes + -- 38 4 External file attributes + -- 42 4 Local header offset + -- 46 n File name + -- 46+n m Extra field + -- 46+n+m k Comment + + local versionMadeBy = buffer.readu16(self.data, pos + 4) + local _bitflags = buffer.readu16(self.data, pos + 8) + local timestamp = buffer.readu32(self.data, pos + 12) + local compressionMethod = buffer.readu16(self.data, pos + 10) + local crc = buffer.readu32(self.data, pos + 16) + local compressedSize = buffer.readu32(self.data, pos + 20) + local size = buffer.readu32(self.data, pos + 24) + local nameLength = buffer.readu16(self.data, pos + 28) + local extraLength = buffer.readu16(self.data, pos + 30) + local commentLength = buffer.readu16(self.data, pos + 32) + local internalAttrs = buffer.readu16(self.data, pos + 36) + local externalAttrs = buffer.readu32(self.data, pos + 38) + local offset = buffer.readu32(self.data, pos + 42) + local name = buffer.readstring(self.data, pos + 46, nameLength) + + local entrySize = 46 + nameLength + extraLength + commentLength + if pos + entrySize > record.cdOffset + record.cdSize then + error("Invalid Central Directory entry size") + end + + table.insert( + self.entries, + ZipEntry.new(offset, name, { + versionMadeBy = versionMadeBy, + compressedSize = compressedSize, + size = size, + crc = crc, + method = DECOMPRESSION_ROUTINES[compressionMethod].name :: CompressionMethod, + timestamp = timestamp, + attributes = externalAttrs, + isText = bit32.btest(internalAttrs, 0x0001), + }) + ) + + pos = pos + 46 + nameLength + extraLength + commentLength + entriesFound += 1 + end + + if entriesFound ~= record.cdEntries then + error("Found different entries than specified in Central Directory") + end + + self.comment = record.comment +end + +--[=[ + @within ZipReader + @method buildDirectoryTree + @private + + Builds the directory tree from the entries. Used internally during initialization of the + [ZipReader]. +]=] +function ZipReader.buildDirectoryTree(self: ZipReader): () + -- Sort entries to process directories first; I could either handle + -- directories and files in separate passes over the entries, or sort + -- the entries so I handled the directories first -- I decided to do + -- the latter + table.sort(self.entries, function(a, b) + if a.isDirectory ~= b.isDirectory then + return a.isDirectory + end + return a.name < b.name + end) + + for _, entry in self.entries do + local parts = {} + -- Split entry path into individual components + -- e.g. "folder/subfolder/file.txt" -> {"folder", "subfolder", "file.txt"} + for part in string.gmatch(entry.name, "([^/]+)/?") do + table.insert(parts, part) + end + + -- Start from root directory + local current = self.root + local path = "" + + -- Process each path component + for i, part in parts do + path ..= part + + if i < #parts or entry.isDirectory then + -- Create missing directory entries for intermediate paths + if not self.directories[path] then + if entry.isDirectory and i == #parts then + -- Existing directory entry, reuse it + self.directories[path] = entry + else + -- Create new directory entry for intermediate paths or undefined + -- parent directories in the ZIP + local dir = ZipEntry.new(0, path .. "/", { + versionMadeBy = 0, + compressedSize = 0, + size = 0, + crc = 0, + compressionMethod = "STORED", + timestamp = entry.timestamp, + attributes = entry.attributes, + }) + + dir.versionMadeBy = entry.versionMadeBy + dir.isDirectory = true + dir.parent = current + self.directories[path] = dir + end + + -- Track directory in both lookup table and parent's children + table.insert(current.children, self.directories[path]) + end + + -- Move deeper into the tree + current = self.directories[path] + continue + end + + -- Link file entry to its parent directory + entry.parent = current + table.insert(current.children, entry) + end + end +end + +--[=[ + @within ZipReader + @method findEntry + + Finds a [ZipEntry] by its path in the ZIP archive. + + @param path string -- Path to the entry to find + @return ZipEntry? -- The found entry, or `nil` if not found +]=] +function ZipReader.findEntry(self: ZipReader, path: string): ZipEntry? + if path == "/" then + -- If the root directory's entry was requested we do not + -- need to do any additional work + return self.root + end + + -- Normalize path by removing leading and trailing slashes + -- This ensures consistent lookup regardless of input format + -- e.g., "/folder/file.txt/" -> "folder/file.txt" + path = string.gsub(path, "^/", ""):gsub("/$", "") + + -- First check regular files and explicit directories + for _, entry in self.entries do + -- Compare normalized paths + if string.gsub(entry.name, "/$", "") == path then + return entry + end + end + + -- If not found, check virtual directory entries + -- These are directories that were created implicitly + return self.directories[path] +end + +--[=[ + @interface ExtractionOptions + @within ZipReader + @ignore + + Options accepted by the [ZipReader:extract] method. + + @field followSymlinks boolean? -- Whether to follow symlinks + @field decompress boolean -- Whether to decompress the entry or only return the raw data + @field type ("binary" | "text")? -- The type of data to return, automatically inferred based on the type of contents if not specified + @field skipCrcValidation boolean? -- Whether to skip CRC validation + @field skipSizeValidation boolean? -- Whether to skip size validation +]=] +type ExtractionOptions = { + followSymlinks: boolean?, + decompress: boolean?, + type: ("binary" | "text")?, + skipCrcValidation: boolean?, + skipSizeValidation: boolean?, +} + +--[=[ + @within ZipReader + @method extract + + Extracts the specified [ZipEntry] from the ZIP archive. See [ZipReader:extractDirectory] for + extracting directories. + + @error "Cannot extract directory" -- If the entry is a directory, use [ZipReader:extractDirectory] instead + @error "Invalid local file header" -- Invalid ZIP file, local header signature did not match + @error "Unsupported PKZip spec version: {versionNeeded}" -- The ZIP file was created with an unsupported version of the ZIP specification + @error "Symlink path not found" -- If `followSymlinks` of options is `true` and the symlink path was not found + @error "Unsupported compression, ID: {compressionMethod}" -- The entry was compressed using an unsupported compression method + + @param entry ZipEntry -- The entry to extract + @param options ExtractionOptions? -- Options for the extraction + @return buffer | string -- The extracted data +]=] +function ZipReader.extract(self: ZipReader, entry: ZipEntry, options: ExtractionOptions?): buffer | string + -- Local File Header format: + -- Offset Bytes Description + -- 0 4 Local file header signature + -- 4 2 Version needed to extract + -- 6 2 General purpose bitflags + -- 8 2 Compression method (8 = DEFLATE) + -- 14 4 CRC32 checksum + -- 18 4 Compressed size + -- 22 4 Uncompressed size + -- 26 2 File name length (n) + -- 28 2 Extra field length (m) + -- 30 n File name + -- 30+n m Extra field + -- 30+n+m - File data + + if entry.isDirectory then + error("Cannot extract directory") + end + + local defaultOptions: ExtractionOptions = { + followSymlinks = false, + decompress = true, + type = if entry.isText then "text" else "binary", + skipValidation = false, + } + + -- TODO: Use a `Partial` type function for this in the future! + local optionsOrDefault: { + followSymlinks: boolean, + decompress: boolean, + type: "binary" | "text", + skipCrcValidation: boolean, + skipSizeValidation: boolean, + } = if options + then setmetatable(options, { __index = defaultOptions }) :: any + else defaultOptions + + local pos = entry.offset + if buffer.readu32(self.data, pos) ~= SIGNATURES.LOCAL_FILE then + error("Invalid local file header") + end + + -- Validate that the version needed to extract is supported + local versionNeeded = buffer.readu16(self.data, pos + 4) + assert(MAX_SUPPORTED_PKZIP_VERSION >= versionNeeded, `Unsupported PKZip spec version: {versionNeeded}`) + + local bitflags = buffer.readu16(self.data, pos + 6) + local crcChecksum = buffer.readu32(self.data, pos + 14) + local compressedSize = buffer.readu32(self.data, pos + 18) + local uncompressedSize = buffer.readu32(self.data, pos + 22) + local nameLength = buffer.readu16(self.data, pos + 26) + local extraLength = buffer.readu16(self.data, pos + 28) + + pos = pos + 30 + nameLength + extraLength + + if bit32.btest(bitflags, 0x08) then + -- The bit at offset 3 was set, meaning we did not have the file sizes + -- and CRC checksum at the time of the creation of the ZIP. Instead, they + -- were appended after the compressed data chunks in a data descriptor + + -- Data Descriptor format: + -- Offset Bytes Description + -- 0 0 or 4 0x08074b50 (optional signature) + -- 0 or 4 4 CRC32 checksum + -- 4 or 8 4 Compressed size + -- 8 or 12 4 Uncompressed size + + -- Start at the compressed data + local descriptorPos = pos + while true do + -- Try reading a u32 starting from current offset + local leading = buffer.readu32(self.data, descriptorPos) + + if leading == SIGNATURES.DATA_DESCRIPTOR then + -- If we find a data descriptor signature, that must mean + -- the current offset points to the start of the descriptor + break + end + + if leading == entry.crc then + -- If we find our file's CRC checksum, that means the data + -- descriptor signature was omitted, so our chunk starts 4 + -- bytes before + descriptorPos -= 4 + break + end + + -- Skip to the next byte + descriptorPos += 1 + end + + crcChecksum = buffer.readu32(self.data, descriptorPos + 4) + compressedSize = buffer.readu32(self.data, descriptorPos + 8) + uncompressedSize = buffer.readu32(self.data, descriptorPos + 12) + end + + local content = buffer.create(compressedSize) + buffer.copy(content, 0, self.data, pos, compressedSize) + + if optionsOrDefault.decompress then + local compressionMethod = buffer.readu16(self.data, entry.offset + 8) + local algo = DECOMPRESSION_ROUTINES[compressionMethod] + if algo == nil then + error(`Unsupported compression, ID: {compressionMethod}`) + end + + if optionsOrDefault.followSymlinks then + local linkPath = buffer.tostring(algo.decompress(content, 0, { + expected = 0x00000000, + skip = true, + })) + + -- Check if the path was a relative path + if path.isRelative(linkPath) then + if string.sub(linkPath, -1) ~= "/" then + linkPath ..= "/" + end + + linkPath = path.canonicalize(`{(entry.parent or self.root).name}{linkPath}`) + end + + optionsOrDefault.followSymlinks = false + optionsOrDefault.type = "binary" + optionsOrDefault.skipCrcValidation = true + optionsOrDefault.skipSizeValidation = true + content = + self:extract(self:findEntry(linkPath) or error("Symlink path not found"), optionsOrDefault) :: buffer + end + + content = algo.decompress(content, uncompressedSize, { + expected = crcChecksum, + skip = optionsOrDefault.skipCrcValidation, + }) + + -- Unless skipping validation is requested, we make sure the uncompressed size matches + assert( + optionsOrDefault.skipSizeValidation or uncompressedSize == buffer.len(content), + "Validation failed; uncompressed size does not match" + ) + end + + return if optionsOrDefault.type == "text" then buffer.tostring(content) else content +end + +--[=[ + @within ZipReader + @method extractDirectory + + Extracts all the files in a specified directory, skipping any directory entries. + + **Errors if [ZipReader:extract] errors on an entry in the directory.** + + @param path string -- The path to the directory to extract + @param options ExtractionOptions? -- Options for the extraction + @return { [string]: buffer } | { [string]: string } -- A map of extracted file paths and their contents +]=] +function ZipReader.extractDirectory( + self: ZipReader, + path: string, + options: ExtractionOptions? +): { [string]: buffer } | { [string]: string } + local files: { [string]: buffer } | { [string]: string } = {} + -- Normalize path by removing leading slash for consistent prefix matching + path = string.gsub(path, "^/", "") + + -- Iterate through all entries to find files within target directory + for _, entry in self.entries do + -- Check if entry is a file (not directory) and its path starts with target directory + if not entry.isDirectory and string.sub(entry.name, 1, #path) == path then + -- Store extracted content mapped to full path + files[entry.name] = self:extract(entry, options) + end + end + + -- Return a map of file to contents + return files +end + +--[=[ + @within ZipReader + @method listDirectory + + Lists the entries within a specified directory path. + + @error "Not a directory" -- If the path does not exist or is not a directory + + @param path string -- The path to the directory to list + @return { ZipEntry } -- The list of entries in the directory +]=] +function ZipReader.listDirectory(self: ZipReader, path: string): { ZipEntry } + -- Locate the entry with the path + local entry = self:findEntry(path) + if not entry or not entry.isDirectory then + -- If an entry was not found, we error + error("Not a directory") + end + + -- Return the children of our discovered entry + return entry.children +end + +--[=[ + @within ZipReader + @method walk + + Recursively walks through the ZIP file, calling the provided callback for each entry + with the current entry and its depth. + + @param callback (entry: ZipEntry, depth: number) -> () -- The function to call for each entry +]=] +function ZipReader.walk(self: ZipReader, callback: (entry: ZipEntry, depth: number) -> ()): () + -- Wrapper function which recursively calls callback for every child + -- in an entry + local function walkEntry(entry: ZipEntry, depth: number) + callback(entry, depth) + + for _, child in entry.children do + -- ooo spooky recursion... blame this if shit go wrong + walkEntry(child, depth + 1) + end + end + + walkEntry(self.root, 0) +end + +--[=[ + @interface ZipStatistics + @within ZipReader + + @field fileCount number -- The number of files in the ZIP + @field dirCount number -- The number of directories in the ZIP + @field totalSize number -- The total size of all files in the ZIP +]=] +export type ZipStatistics = { fileCount: number, dirCount: number, totalSize: number } + +--[=[ + @within ZipReader + @method getStats + + Retrieves statistics about the ZIP file. + + @return ZipStatistics -- The statistics about the ZIP file +]=] +function ZipReader.getStats(self: ZipReader): ZipStatistics + local stats: ZipStatistics = { + fileCount = 0, + dirCount = 0, + totalSize = 0, + } + + -- Iterate through the entries, updating stats + for _, entry in self.entries do + if entry.isDirectory then + stats.dirCount += 1 + continue + end + + stats.fileCount += 1 + stats.totalSize += entry.size + end + + return stats +end + +return { + -- Creates a `ZipReader` from a `buffer` of ZIP data. + load = function(data: buffer) + return ZipReader.new(data) + end, +} diff --git a/lute/cli/commands/pkg/src/extern/unzip/utils/path.luau b/lute/cli/commands/pkg/src/extern/unzip/utils/path.luau new file mode 100644 index 000000000..030aec3d4 --- /dev/null +++ b/lute/cli/commands/pkg/src/extern/unzip/utils/path.luau @@ -0,0 +1,124 @@ +--- Canonicalize a path by removing redundant components +local function canonicalize(path: string): string + -- We convert any `\\` separators to `/` separators to ensure consistency + local components = string.split(path:gsub("\\", "/"), "/") + local result = {} + for _, component in components do + if component == "." then + -- Skip current directory + continue + end + + if component == ".." then + -- Traverse one upwards + table.remove(result, #result) + continue + end + + -- Otherwise, add the component to the result + table.insert(result, component) + end + + return table.concat(result, "/") +end + +--- Check if a path is absolute +local function isAbsolute(path: string): boolean + return ( + string.match(path, "^/") + or string.match(path, "^[a-zA-Z]:[\\/]") + or string.match(path, "^//") + or string.match(path, "^\\\\") + ) ~= nil +end + +--- Check if a path is relative +local function isRelative(path: string): boolean + return not isAbsolute(path) +end + +local function replaceBackslashes(input: string, replacement: "/"): string + -- Check if the string starts with double backslashes + if input:sub(1, 2) == "\\\\" then + -- Replace all single backslashes except the first two + return "\\\\" .. input:sub(3):gsub("\\", replacement) + else + -- Replace all single backslashes + return input:gsub("\\", replacement) + end +end + +--- Check if a path is safe to use, i.e., it does not: +--- - Contain null bytes +--- - Resolve to a directory outside of the current directory +--- - Have absolute components +local function isSafe(path: string): boolean + if string.find(path, "\0") or isAbsolute(path) then + -- Null bytes or absolute path, path is unsafe + return false + end + + local components = string.split(replaceBackslashes(path, "/"), "/") + local depth = 0 + for _, component in components do + if string.match(component, "^[a-zA-Z]:$") or string.find(component, "^\\\\") or component == "" then + -- Was a prefix component, or the root directory, path is unsafe + return false + end + + if component == ".." then + -- Traverse one upwards + depth -= 1 + if depth < 0 then + -- Traversed too far, path is unsafe + return false + end + + continue + end + + if component == "." then + -- Skip current directory + continue + end + + -- Otherwise, increment depth + depth += 1 + end + + return depth >= 0 +end + +--- Sanitize a path by ignoring special components +--- - Absolute paths become relative +--- - Special components (like upwards traversing) are removed +--- - Truncates path to the first null byte +local function sanitize(path: string): string + local truncatedPath = if string.find(path, "\0") then string.split(path, "\0")[1] else path + local components = string.split(replaceBackslashes(truncatedPath, "/"), "/") + local result = {} + for _, component in components do + if + not ( + component == ".." + or component == "." + or component == string.match(component, "^[a-zA-Z]:$") + or string.find(component, "^\\\\") + or component == "" + ) + then + -- If the path is not a special component, add it to the result + table.insert(result, component) + end + end + + return table.concat(result, "/") +end + +return { + canonicalize = canonicalize, + isAbsolute = isAbsolute, + isRelative = isRelative, + isSafe = isSafe, + sanitize = sanitize, +} diff --git a/lute/cli/commands/pkg/src/extern/unzip/utils/validate_crc.luau b/lute/cli/commands/pkg/src/extern/unzip/utils/validate_crc.luau new file mode 100644 index 000000000..6c88051ae --- /dev/null +++ b/lute/cli/commands/pkg/src/extern/unzip/utils/validate_crc.luau @@ -0,0 +1,22 @@ +local crc32 = require("../crc") + +export type CrcValidationOptions = { + skip: boolean, + expected: number, +} + +local function validateCrc(decompressed: buffer, validation: CrcValidationOptions) + -- Unless skipping validation is requested, we verify the checksum + if not validation.skip then + local computed = crc32(decompressed) + assert( + validation.expected == computed, + `Validation failed; CRC checksum does not match: {string.format("%x", computed)} ~= {string.format( + "%x", + computed + )} (expected ~= got)` + ) + end +end + +return validateCrc diff --git a/lute/cli/commands/pkg/src/loom/commands/install.luau b/lute/cli/commands/pkg/src/loom/commands/install.luau new file mode 100644 index 000000000..1421b16e5 --- /dev/null +++ b/lute/cli/commands/pkg/src/loom/commands/install.luau @@ -0,0 +1,35 @@ +local resolution = require("../resolver/resolution") +local loomTypes = require("../loomtypes") +local projectManifest = require("../manifest/projectmanifest") +local pp = require("../../extern/pp") + +local fsUtils = require("../utils/fsutils") +local luau = require("@std/luau") + +local function executeLuauSource(source: string): any + local bytecode = luau.compile(source) + local func = luau.load(bytecode, "loom.config.luau", {}) + return func() +end + +function install(packageSource: loomTypes.PackageSource) + local manifestPath = "loom.config.luau" + local manifestContent = fsUtils.readfiletostring(manifestPath) + local manifestTable = executeLuauSource(manifestContent) + local manifest = projectManifest.fromTable(manifestTable) + + local projectResolution = resolution.fromManifest(manifest) + + local lockfile = resolution.toLockFile(projectResolution) + fsUtils.writestringtofile("loom.lock.luau", "return " .. pp(lockfile)) + + for _, pkg in projectResolution.packages do + print(`package: {pkg.name}, Rev: {pkg.rev}, Source: {pkg.sourceKind} - {pkg.source}`) + if pkg.sourceKind == "github" then + local zipArchive = packageSource.git:downloadFromGitHubArchive(pkg.source) + packageSource.git:installZipArchive(pkg.name, pkg.rev, zipArchive) + end + end +end + +return install diff --git a/lute/cli/commands/pkg/src/loom/lockfile/lockfile.luau b/lute/cli/commands/pkg/src/loom/lockfile/lockfile.luau new file mode 100644 index 000000000..0e0158d90 --- /dev/null +++ b/lute/cli/commands/pkg/src/loom/lockfile/lockfile.luau @@ -0,0 +1,22 @@ +-- export type LockFileDependency = { +-- name: string, +-- version: string?, +-- sourceKind: string?, +-- source: string?, +-- } + +-- TODO: Support path/registry dependencies +export type LockFilePackage = { + name: string, + rev: string, + sourceKind: string, + source: string, + -- dependencies: { LockFileDependency }, +} + +export type LockFile = { + version: number, + package: { LockFilePackage }, +} + +return {} diff --git a/lute/cli/commands/pkg/src/loom/loom.luau b/lute/cli/commands/pkg/src/loom/loom.luau new file mode 100644 index 000000000..9432f55e8 --- /dev/null +++ b/lute/cli/commands/pkg/src/loom/loom.luau @@ -0,0 +1,14 @@ +local git = require("./packagesource/git") +local types = require("./loomtypes") +local install = require("./commands/install") + +local loom = {} :: types.Loom + +function loom:install() + local packageSource = {} :: types.PackageSource + packageSource.git = git + + install(packageSource) +end + +return loom diff --git a/lute/cli/commands/pkg/src/loom/loomtypes.luau b/lute/cli/commands/pkg/src/loom/loomtypes.luau new file mode 100644 index 000000000..0e82276f3 --- /dev/null +++ b/lute/cli/commands/pkg/src/loom/loomtypes.luau @@ -0,0 +1,10 @@ +local git = require("./packagesource/git") +export type PackageSource = { + git: git.Git, +} + +export type Loom = { + install: (self: Loom) -> (), +} + +return nil diff --git a/lute/cli/commands/pkg/src/loom/manifest/projectmanifest.luau b/lute/cli/commands/pkg/src/loom/manifest/projectmanifest.luau new file mode 100644 index 000000000..429ca67f1 --- /dev/null +++ b/lute/cli/commands/pkg/src/loom/manifest/projectmanifest.luau @@ -0,0 +1,53 @@ +-- TODO: Support path/registry dependencies +export type DependencySpec = { + rev: string, + sourceKind: string, + source: string, +} + +export type PackageManifest = { + name: string, + version: string, + dependencies: { [string]: DependencySpec }, +} +--[[ +{ + "name": "MyPackage", + "version": "1.0.0", + "dependencies": { + "OtherPackage": "1.0.0", + } +} +]] + +export type ProjectManifest = { + package: PackageManifest, +} + +-- FIXME(Luau): running into issues with getting this to type check properly. +-- We also should also do proper error handling because this is user input. +function fromTable(table: any): ProjectManifest + assert(table.package ~= nil and type(table.package) == "table", "Expected 'package' field in manifest") + + local package = table.package + + assert(package.name ~= nil and type(package.name) == "string", "Expected 'name' field in package manifest") + assert(package.version ~= nil and type(package.version) == "string", "Expected 'rev' field in package manifest") + if package.dependencies ~= nil then + local dependencies = package.dependencies + for name, spec in dependencies do + assert(spec.rev ~= nil and type(spec.rev) == "string", "Expected 'rev' field in dependency spec") + assert( + spec.sourceKind ~= nil and type(spec.sourceKind) == "string", + "Expected 'sourceKind' field in dependency spec" + ) + assert(spec.source ~= nil and type(spec.source) == "string", "Expected 'source' field in dependency spec") + end + end + + return table +end + +return { + fromTable = fromTable, +} diff --git a/lute/cli/commands/pkg/src/loom/packagesource/git.luau b/lute/cli/commands/pkg/src/loom/packagesource/git.luau new file mode 100644 index 000000000..9835f3d1f --- /dev/null +++ b/lute/cli/commands/pkg/src/loom/packagesource/git.luau @@ -0,0 +1,79 @@ +local fsUtils = require("../utils/fsutils") +local net = require("@std/net") +local unzip = require("../../extern/unzip") + +export type Git = { + installZipArchive: (self: Git, name: string, version: string, content: string) -> (), + downloadFromGitHubArchive: (self: Git, url: string, token: string?) -> string, +} + +local Git = {} :: Git + +local function sanitizePath(path: string): string + -- Input validation + if not path or type(path) ~= "string" then + error("Path must be a string") + end + + if #path == 0 then + error("Path cannot be empty") + end + + local maxPathLength = 255 + if #path > maxPathLength then + error("Path too long (max " .. maxPathLength .. " characters)") + end + + -- Security checks - prevent directory traversal + if path:match("%.%.") then + error("Path contains '..' (directory traversal attempt)") + end + + -- Remove/reject dangerous characters + if path:match("[\0\r\n]") then + error("Path contains invalid control characters") + end + + -- Normalize path separators and remove redundant slashes + local normalized = path:gsub("\\", "/"):gsub("//+", "/") + + -- Remove leading slashes but preserve trailing slash for directories + normalized = normalized:gsub("^/+", "") + + return normalized +end + +function Git:installZipArchive(name: string, version: string, content: string) + local reader = unzip.load(buffer.fromstring(content)) + + reader:walk(function(entry, depth) + if entry.isDirectory then + return + end + + local content = reader:extract(entry, { type = "text" }) :: string + + local sanitized = sanitizePath(entry.name) + local slash = string.find(sanitized, "/") + assert(slash, `Expected entry name to contain a slash: {sanitized}`) + + local outputPath = `Packages/{name}/{string.sub(sanitized, slash)}` + fsUtils.writestringtofile(outputPath, content) + end) +end + +function Git:downloadFromGitHubArchive(url: string): string + local headers = {} + + -- TODO: support token authentication. + -- local token = nil + -- if token ~= nil and token ~= "" then + -- headers["Authorization"] = `Bearer {token}` + -- end + + local response = net.request(url, { method = "GET", headers = headers }) + local body = response.body + return body +end + +return Git diff --git a/lute/cli/commands/pkg/src/loom/resolver/package.luau b/lute/cli/commands/pkg/src/loom/resolver/package.luau new file mode 100644 index 000000000..0a38d67fd --- /dev/null +++ b/lute/cli/commands/pkg/src/loom/resolver/package.luau @@ -0,0 +1,36 @@ +local lockfile = require("../lockfile/lockfile") +local manifest = require("../manifest/projectmanifest") +export type Package = { + name: string, + rev: string, + sourceKind: string, + source: string, +} + +local function fromLockPackage(lockPackage: lockfile.LockFilePackage): Package + return { + name = lockPackage.name, + rev = lockPackage.rev, + sourceKind = lockPackage.sourceKind, + source = lockPackage.source, + } +end + +local function fromDependencySpec(name: string, spec: manifest.DependencySpec): Package + if spec.sourceKind == "github" then + local source = spec.source .. "/archive/" .. spec.rev .. ".zip" + return { + name = name, + rev = spec.rev, + sourceKind = spec.sourceKind, + source = source, + } + else + error(`Unsupported source kind: {spec.sourceKind} for package {name}`) + end +end + +return { + fromLockPackage = fromLockPackage, + fromDependencySpec = fromDependencySpec, +} diff --git a/lute/cli/commands/pkg/src/loom/resolver/resolution.luau b/lute/cli/commands/pkg/src/loom/resolver/resolution.luau new file mode 100644 index 000000000..e5fd5a1c5 --- /dev/null +++ b/lute/cli/commands/pkg/src/loom/resolver/resolution.luau @@ -0,0 +1,46 @@ +local package = require("./package") +local lockfile = require("../lockfile/lockfile") +local projectManifest = require("../manifest/projectmanifest") + +type Resolution = { + packages: { package.Package }, +} + +function fromLockfile(lockfile: lockfile.LockFile): Resolution + local packages = {} + for _, lockfilePackage in lockfile.package do + table.insert(packages, package.fromLockPackage(lockfilePackage)) + end + return { packages = packages } +end + +function fromManifest(manifest: projectManifest.ProjectManifest): Resolution + local dependencies = manifest.package.dependencies + local packages = {} + for name, spec in dependencies do + table.insert(packages, package.fromDependencySpec(name, spec)) + end + return { packages = packages } +end + +function toLockFile(resolution: Resolution): lockfile.LockFile + local lockfilePackages = {} + for _, pkg in resolution.packages do + table.insert(lockfilePackages, { + name = pkg.name, + rev = pkg.rev, + sourceKind = pkg.sourceKind, + source = pkg.source, + }) + end + return { + version = 1, + package = lockfilePackages, + } +end + +return { + fromLockfile = fromLockfile, + fromManifest = fromManifest, + toLockFile = toLockFile, +} diff --git a/lute/cli/commands/pkg/src/loom/utils/fsutils.luau b/lute/cli/commands/pkg/src/loom/utils/fsutils.luau new file mode 100644 index 000000000..9a64160ba --- /dev/null +++ b/lute/cli/commands/pkg/src/loom/utils/fsutils.luau @@ -0,0 +1,33 @@ +local fs = require("@std/fs") + +export type FileSystemInterface = { + readfiletostring: (path: string) -> string, + writestringtofile: (path: string, content: string) -> (), +} + +local fsUtils = {} :: FileSystemInterface + +local function getParentPath(path: string): string + -- Remove any trailing slashes + path = path:gsub("[/\\]+$", "") + + -- Match everything up to the last slash or backslash + local parent = path:match("^(.*)[/\\][^/\\]+$") + return parent or "" +end + +local function writestringtofile(path: string, content: string): () + local parentPath = getParentPath(path) + fs.createdirectory(parentPath, { makeparents = true }) + return fs.writestringtofile(path, content) +end + +function fsUtils.readfiletostring(path: string): string + return fs.readfiletostring(path) +end + +function fsUtils.writestringtofile(path: string, content: string): () + return writestringtofile(path, content) +end + +return fsUtils diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 073b5528c..3f540d7fb 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -85,7 +85,7 @@ Run Options: -h, --help Display this usage message. )"; -static const char* PKGRUN_HELP_STRING = R"(Usage: lute pkgrun [args...] +static const char* PKGRUN_HELP_STRING = R"(Usage: lute pkg run [args...] Run Options: -h, --help Display this usage message. @@ -266,7 +266,7 @@ static std::pair getValidPath(std::string filePath) int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness, LuteReporter& reporter) { - std::string command = packageAwareness ? "pkgrun" : "run"; + std::string command = packageAwareness ? "pkg run" : "run"; std::string filePath; int program_argc = 0; char** program_argv = nullptr; @@ -586,15 +586,16 @@ int cliMain(int argc, char** argv, LuteReporter& reporter) } const char* command = argv[1]; + const char* subcommand = argc >= 3 ? argv[2] : nullptr; int argOffset = 2; if (strcmp(command, "run") == 0) { return handleRunCommand(argc, argv, argOffset, /* packageAwareness = */ false, reporter); } - else if (strcmp(command, "pkgrun") == 0) + else if (strcmp(command, "pkg") == 0 && subcommand && strcmp(subcommand, "run") == 0) { - return handleRunCommand(argc, argv, argOffset, /* packageAwareness = */ true, reporter); + return handleRunCommand(argc, argv, argOffset + 1, /* packageAwareness = */ true, reporter); } else if (strcmp(command, "check") == 0) { diff --git a/tests/src/packagerequire.test.cpp b/tests/src/packagerequire.test.cpp index 22e6f7d3b..b70654f7f 100644 --- a/tests/src/packagerequire.test.cpp +++ b/tests/src/packagerequire.test.cpp @@ -94,8 +94,9 @@ TEST_CASE_FIXTURE(LuteFixture, "pkgrun_with_lockfile") std::string entry = getLuteProjectRootAbsolute() + "/tests/src/packages/pkgrun_with_lockfile/packageentry/entry.luau"; char executablePlaceholder[] = "lute"; - char subcommand[] = "pkgrun"; - std::vector argv = {executablePlaceholder, subcommand, entry.data()}; + char command[] = "pkg"; + char subcommand[] = "run"; + std::vector argv = {executablePlaceholder, command, subcommand, entry.data()}; CHECK_EQ(cliMain(argv.size(), argv.data(), getReporter()), 0); auto rep = getReporter(); From 3c11568746a95faf19a1290dcf5f0aee8ef81e10 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 23 Jan 2026 11:40:16 -0800 Subject: [PATCH 301/642] Adds a CI job that prevents us from adding new type errors to the lute repo. (#751) This PR adds a script called `check.luau` that gathers up every .luau file in `std/libs/` and `tests/` and passes them to the `lute check` command. `check.luau` takes an optional `--update` argument that will update a list of all the type errors. I'm using the diffext battery to show the diff, but I think we need to add some more pretty printing options for that to be useful atm. I've also added a CI job that will prevent PR's from merging if they introduce a new type error. --- .github/workflows/ci.yml | 23 +- tools/check-faillist.txt | 527 +++++++++++++++++++++++++++++++++++++++ tools/check.luau | 178 +++++++++++++ 3 files changed, 726 insertions(+), 2 deletions(-) create mode 100755 tools/check-faillist.txt create mode 100644 tools/check.luau diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe5d82a2c..e19e5e0bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,15 +160,34 @@ jobs: - name: Run Lute Lint run: ${{ steps.build_lute.outputs.exe_path }} lint -v . + lute-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup and Build Lute + id: build_lute + uses: ./.github/actions/setup-and-build + with: + target: Lute.CLI + config: debug + token: ${{ secrets.GITHUB_TOKEN }} + use-bootstrap: true + + - name: Run Lute Check + run: ${{ steps.build_lute.outputs.exe_path }} tools/check.luau ${{ steps.build_lute.outputs.exe_path }} + aggregator: name: Gated Commits runs-on: ubuntu-latest - needs: [run-lutecli-luau-tests, run-lute-cxx-unittests, check-format, build-docs-site, lute-lint] + needs: [run-lutecli-luau-tests, run-lute-cxx-unittests, check-format, build-docs-site, lute-lint, lute-check] if: ${{ always() }} steps: - name: Aggregator run: | - if [ "${{ needs.run-lutecli-luau-tests.result }}" == "success" ] && [ "${{ needs.run-lute-cxx-unittests.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ] && [ "${{ needs.build-docs-site.result }}" == "success" ] && [ "${{ needs.lute-lint.result }}" == "success" ]; then + if [ "${{ needs.run-lutecli-luau-tests.result }}" == "success" ] && [ "${{ needs.run-lute-cxx-unittests.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ] && [ "${{ needs.build-docs-site.result }}" == "success" ] && [ "${{ needs.lute-lint.result }}" == "success" ] && [ "${{ needs.lute-check.result }}" == "success" ]; then echo "All jobs succeeded" else echo "One or more jobs failed" diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt new file mode 100755 index 000000000..2e5436537 --- /dev/null +++ b/tools/check-faillist.txt @@ -0,0 +1,527 @@ +batteries/cli.luau:50:2-13 +batteries/cli.luau:52:3-14 +batteries/cli.luau:144:36-75 +batteries/cli.luau:148:6-45 +batteries/difftext/myersdiff.luau:76:8-22 +batteries/difftext/printdiff.luau:70:10-77 +batteries/difftext/printdiff.luau:72:10-73 +batteries/difftext/printdiff.luau:74:10-73 +batteries/pp.luau:100:2-11 +batteries/toml.luau:27:14-16 +batteries/toml.luau:40:14-16 +batteries/toml.luau:45:36-38 +batteries/toml.luau:75:26-28 +batteries/toml.luau:133:33-41 +batteries/toml.luau:143:23-25 +batteries/toml.luau:144:25-29 +batteries/toml.luau:146:20-24 +batteries/toml.luau:146:55-59 +batteries/toml.luau:147:24-28 +batteries/toml.luau:166:24-28 +examples/a.luau:1:7-7 +examples/colorful.luau:1:18-47 +examples/directories.luau:3:16-25 +examples/docs/test_module.luau:30:7-12 +examples/json.luau:13:7-24 +examples/json.luau:13:7-24 +examples/lints/almost_swapped.luau:83:5-16 +examples/lints/almost_swapped.luau:104:9-12 +examples/lints/divide_by_zero.luau:38:9-12 +examples/net_example.luau:8:2-48 +examples/net_example.luau:8:2-48 +examples/net_example.luau:13:33-43 +examples/net_example.luau:15:24-34 +examples/net_example.luau:16:24-34 +examples/net_example.luau:17:24-34 +examples/parallel_sort.luau:6:15-31 +examples/parallel_sort.luau:6:25-30 +examples/parallel_sort.luau:9:2-54 +examples/parallel_sort.luau:9:2-54 +examples/parallel_sort.luau:9:16-33 +examples/parallel_sort.luau:9:20-33 +examples/parallel_sort.luau:9:21-25 +examples/parallel_sort.luau:9:36-43 +examples/parallel_sort.luau:48:2-13 +examples/parallel_sort.luau:59:14-16 +examples/parallel_sort.luau:64:29-38 +examples/parallel_sort.luau:64:41-54 +examples/parallel_sort.luau:85:9-13 +examples/parallel_sort.luau:90:24-25 +examples/parallel_sort.luau:91:9-13 +examples/parallel_sort_helper.luau:4:11-15 +examples/process.luau:4:38-39 +examples/process.luau:11:24-34 +examples/process.luau:12:24-34 +examples/process.luau:13:24-34 +examples/process.luau:17:7-15 +examples/process.luau:18:7-15 +examples/process.luau:19:7-15 +examples/query.luau:23:7-18 +examples/task-delay.luau:3:15-37 +examples/task-resume.luau:12:7-7 +examples/transformee.luau:3:7-7 +examples/transformer.luau:33:38-100 +lute/cli/commands/doc/init.luau:203:2-11 +lute/cli/commands/doc/init.luau:204:10-24 +lute/cli/commands/doc/init.luau:310:5-16 +lute/cli/commands/doc/init.luau:315:5-16 +lute/cli/commands/doc/init.luau:321:10-24 +lute/cli/commands/lib/cli.luau:50:2-13 +lute/cli/commands/lib/cli.luau:50:2-13 +lute/cli/commands/lib/cli.luau:52:3-14 +lute/cli/commands/lib/cli.luau:52:3-14 +lute/cli/commands/lib/cli.luau:144:36-75 +lute/cli/commands/lib/cli.luau:144:36-75 +lute/cli/commands/lib/cli.luau:148:6-45 +lute/cli/commands/lib/cli.luau:148:6-45 +lute/cli/commands/lint/init.luau:51:10-18 +lute/cli/commands/lint/printer.luau:51:7-62 +lute/cli/commands/lint/printer.luau:51:7-62 +lute/cli/commands/lint/printer.luau:58:8-52 +lute/cli/commands/lint/printer.luau:58:8-52 +lute/cli/commands/lint/rules/parenthesized_conditions.luau:22:5-16 +lute/cli/commands/lint/rules/parenthesized_conditions.luau:36:6-17 +lute/cli/commands/lint/rules/parenthesized_conditions.luau:61:4-15 +lute/cli/commands/test/filter.luau:34:5-16 +lute/cli/commands/test/filter.luau:34:5-16 +lute/cli/commands/test/filter.luau:62:6-17 +lute/cli/commands/test/filter.luau:62:6-17 +lute/cli/commands/test/finder.luau:5:7-14 +lute/cli/commands/test/finder.luau:5:7-14 +lute/cli/commands/test/init.luau:126:42-44 +lute/cli/commands/transform/init.luau:16:26-30 +lute/cli/commands/transform/lib/arguments.luau:47:26-37 +lute/cli/commands/transform/lib/arguments.luau:47:26-37 +lute/cli/commands/transform/lib/arguments.luau:75:9-14 +lute/cli/commands/transform/lib/arguments.luau:75:9-14 +lute/std/libs/fs.luau:82:3-14 +lute/std/libs/fs.luau:82:3-14 +lute/std/libs/fs.luau:90:17-28 +lute/std/libs/fs.luau:90:17-28 +lute/std/libs/fs.luau:91:11-20 +lute/std/libs/fs.luau:91:11-20 +lute/std/libs/fs.luau:91:11-20 +lute/std/libs/fs.luau:91:11-20 +lute/std/libs/fs.luau:135:34-39 +lute/std/libs/fs.luau:135:34-39 +lute/std/libs/fs.luau:184:52-68 +lute/std/libs/fs.luau:184:52-68 +lute/std/libs/fs.luau:192:11-17 +lute/std/libs/fs.luau:192:11-17 +lute/std/libs/path/posix/init.luau:8:22-38 +lute/std/libs/path/posix/init.luau:8:22-38 +lute/std/libs/path/win32/init.luau:9:22-38 +lute/std/libs/path/win32/init.luau:9:22-38 +lute/std/libs/syntax/printer.luau:41:23-27 +lute/std/libs/syntax/printer.luau:41:23-27 +lute/std/libs/syntax/printer.luau:42:25-29 +lute/std/libs/syntax/printer.luau:42:25-29 +lute/std/libs/syntax/printer.luau:42:50-54 +lute/std/libs/syntax/printer.luau:42:50-54 +lute/std/libs/syntax/printer.luau:203:2-19 +lute/std/libs/syntax/printer.luau:203:2-19 +lute/std/libs/syntax/query.luau:83:29-100 +lute/std/libs/syntax/query.luau:83:29-100 +lute/std/libs/syntax/query.luau:136:24-31 +lute/std/libs/syntax/query.luau:136:24-31 +lute/std/libs/syntax/query.luau:136:45-52 +lute/std/libs/syntax/query.luau:136:45-52 +lute/std/libs/syntax/query.luau:143:9-20 +lute/std/libs/syntax/query.luau:143:9-20 +lute/std/libs/syntax/query.luau:145:13-28 +lute/std/libs/syntax/query.luau:145:13-28 +lute/std/libs/syntax/query.luau:147:16-34 +lute/std/libs/syntax/query.luau:147:16-34 +lute/std/libs/syntax/utils/init.luau:62:12-24 +lute/std/libs/syntax/utils/init.luau:62:12-24 +lute/std/libs/syntax/utils/init.luau:62:31-31 +lute/std/libs/syntax/utils/init.luau:62:31-31 +lute/std/libs/syntax/utils/init.luau:234:12-20 +lute/std/libs/syntax/utils/init.luau:234:12-20 +lute/std/libs/syntax/utils/init.luau:234:27-27 +lute/std/libs/syntax/utils/init.luau:234:27-27 +lute/std/libs/syntax/utils/init.luau:238:12-20 +lute/std/libs/syntax/utils/init.luau:238:12-20 +lute/std/libs/syntax/utils/trivia.luau:7:38-100 +lute/std/libs/syntax/utils/trivia.luau:7:38-100 +lute/std/libs/syntax/utils/trivia.luau:8:13-21 +lute/std/libs/syntax/utils/trivia.luau:8:13-21 +lute/std/libs/syntax/utils/trivia.luau:30:38-100 +lute/std/libs/syntax/utils/trivia.luau:30:38-100 +lute/std/libs/syntax/utils/trivia.luau:31:13-21 +lute/std/libs/syntax/utils/trivia.luau:31:13-21 +lute/std/libs/syntax/visitor.luau:239:3-12 +lute/std/libs/syntax/visitor.luau:239:3-12 +lute/std/libs/syntax/visitor.luau:245:3-12 +lute/std/libs/syntax/visitor.luau:245:3-12 +lute/std/libs/syntax/visitor.luau:376:3-12 +lute/std/libs/syntax/visitor.luau:376:3-12 +lute/std/libs/syntax/visitor.luau:382:3-12 +lute/std/libs/syntax/visitor.luau:382:3-12 +lute/std/libs/syntax/visitor.luau:388:3-12 +lute/std/libs/syntax/visitor.luau:388:3-12 +lute/std/libs/syntax/visitor.luau:406:3-12 +lute/std/libs/syntax/visitor.luau:406:3-12 +lute/std/libs/syntax/visitor.luau:475:3-12 +lute/std/libs/syntax/visitor.luau:475:3-12 +lute/std/libs/syntax/visitor.luau:650:3-12 +lute/std/libs/syntax/visitor.luau:650:3-12 +lute/std/libs/syntax/visitor.luau:656:3-12 +lute/std/libs/syntax/visitor.luau:656:3-12 +lute/std/libs/syntax/visitor.luau:679:3-12 +lute/std/libs/syntax/visitor.luau:679:3-12 +lute/std/libs/syntax/visitor.luau:938:24-28 +lute/std/libs/syntax/visitor.luau:938:24-28 +lute/std/libs/syntax/visitor.luau:947:35-39 +lute/std/libs/syntax/visitor.luau:947:35-39 +lute/std/libs/syntax/visitor.luau:959:19-23 +lute/std/libs/syntax/visitor.luau:959:19-23 +lute/std/libs/syntax/visitor.luau:966:25-29 +lute/std/libs/syntax/visitor.luau:966:25-29 +lute/std/libs/syntax/visitor.luau:970:21-25 +lute/std/libs/syntax/visitor.luau:970:21-25 +lute/std/libs/syntax/visitor.luau:991:17-21 +lute/std/libs/syntax/visitor.luau:991:17-21 +lute/std/libs/syntax/visitor.luau:1019:9-20 +lute/std/libs/syntax/visitor.luau:1019:9-20 +lute/std/libs/syntax/visitor.luau:1020:3-12 +lute/std/libs/syntax/visitor.luau:1020:3-12 +lute/std/libs/syntax/visitor.luau:1021:9-24 +lute/std/libs/syntax/visitor.luau:1021:9-24 +lute/std/libs/syntax/visitor.luau:1022:22-25 +lute/std/libs/syntax/visitor.luau:1022:22-25 +lute/std/libs/test/failure.luau:18:35-38 +lute/std/libs/test/failure.luau:18:35-38 +lute/std/libs/test/reporter.luau:10:19-32 +lute/std/libs/test/reporter.luau:10:19-32 +tests/batteries/collections/deque.test.luau:9:13-16 +tests/batteries/collections/deque.test.luau:16:13-16 +tests/batteries/collections/deque.test.luau:24:13-16 +tests/batteries/collections/deque.test.luau:32:13-16 +tests/batteries/collections/deque.test.luau:39:13-16 +tests/batteries/collections/deque.test.luau:48:13-16 +tests/batteries/collections/deque.test.luau:55:13-16 +tests/batteries/collections/deque.test.luau:61:13-16 +tests/batteries/collections/deque.test.luau:81:13-16 +tests/batteries/collections/deque.test.luau:86:13-16 +tests/batteries/collections/deque.test.luau:91:13-16 +tests/cli/check.test.luau:1:7-8 +tests/cli/check.test.luau:4:7-12 +tests/cli/test.test.luau:3:7-12 +tests/parserExamples/assignment-1.luau:1:1-1 +tests/parserExamples/assignment-1.luau:3:1-1 +tests/parserExamples/assignment-1.luau:3:4-4 +tests/parserExamples/assignment-1.luau:5:7-7 +tests/parserExamples/assignment-1.luau:5:13-13 +tests/parserExamples/assignment-1.luau:5:16-16 +tests/parserExamples/assignment-1.luau:5:23-23 +tests/parserExamples/assignment-1.luau:5:31-31 +tests/parserExamples/assignment-1.luau:5:38-38 +tests/parserExamples/assignment-1.luau:5:43-46 +tests/parserExamples/attributes-1.luau:2:16-18 +tests/parserExamples/attributes-1.luau:5:10-12 +tests/parserExamples/attributes-1.luau:7:7-7 +tests/parserExamples/function-declaration-1.luau:2:2-5 +tests/parserExamples/function-declaration-1.luau:5:10-12 +tests/parserExamples/function-declaration-2.luau:2:10-10 +tests/parserExamples/function-declaration-3.luau:1:10-10 +tests/parserExamples/function-declaration-3.luau:3:10-10 +tests/parserExamples/function-declaration-3.luau:5:10-10 +tests/parserExamples/function-declaration-3.luau:7:10-10 +tests/parserExamples/function-declaration-3.luau:9:10-10 +tests/parserExamples/function-declaration-4.luau:1:10-10 +tests/parserExamples/function-declaration-4.luau:3:10-10 +tests/parserExamples/function-declaration-4.luau:5:10-10 +tests/parserExamples/function-declaration-4.luau:7:1-24 +tests/parserExamples/function-declaration-4.luau:7:10-10 +tests/parserExamples/generic-for-loop-1.luau:1:27-30 +tests/parserExamples/generic-for-loop-1.luau:2:2-5 +tests/parserExamples/generic-for-loop-1.luau:5:27-30 +tests/parserExamples/generic-for-loop-1.luau:6:2-5 +tests/parserExamples/generic-for-loop-1.luau:9:21-24 +tests/parserExamples/generic-for-loop-1.luau:10:2-5 +tests/parserExamples/if-expression-1.luau:1:7-7 +tests/parserExamples/if-expression-1.luau:2:7-7 +tests/parserExamples/if-expression-2.luau:2:7-7 +tests/parserExamples/if-expression-2.luau:3:7-10 +tests/parserExamples/if-expression-2.luau:5:8-11 +tests/parserExamples/if-expression-2.luau:6:8-11 +tests/parserExamples/interpolated-string-1.luau:1:7-7 +tests/parserExamples/interpolated-string-1.luau:2:7-7 +tests/parserExamples/interpolated-string-1.luau:3:7-7 +tests/parserExamples/interpolated-string-1.luau:4:7-7 +tests/parserExamples/interpolated-string-2.luau:2:7-7 +tests/parserExamples/interpolated-string-2.luau:7:9-15 +tests/parserExamples/interpolated-string-2.luau:7:24-30 +tests/parserExamples/local-assignment-1.luau:1:7-7 +tests/parserExamples/local-assignment-1.luau:2:7-7 +tests/parserExamples/local-assignment-1.luau:2:10-10 +tests/parserExamples/local-assignment-1.luau:3:7-7 +tests/parserExamples/local-function-declaration-1.luau:1:16-16 +tests/parserExamples/local-function-declaration-1.luau:2:2-5 +tests/parserExamples/numeric-for-loop-1.luau:2:2-5 +tests/parserExamples/numeric-for-loop-1.luau:4:9-13 +tests/parserExamples/numeric-for-loop-1.luau:4:16-20 +tests/parserExamples/repeat-until-1.luau:2:2-5 +tests/parserExamples/repeat-until-1.luau:3:7-15 +tests/parserExamples/table-1.luau:1:7-7 +tests/parserExamples/table-2.luau:1:7-7 +tests/parserExamples/type-assertion-1.luau:1:7-7 +tests/parserExamples/while-1.luau:1:7-15 +tests/parserExamples/while-1.luau:2:2-5 +tests/runtime/crypto.test.luau:66:26-30 +tests/runtime/crypto.test.luau:108:56-68 +tests/runtime/crypto.test.luau:112:72-84 +tests/src/packages/package_aware_require/dep/module.luau:1:16-30 +tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau:1:16-38 +tests/src/require/config_tests/config_ambiguity/requirer.luau:1:8-22 +tests/src/require/config_tests/config_cannot_be_required/requirer.luau:1:8-27 +tests/src/require/config_tests/tilde_config/main.luau:1:7-7 +tests/src/require/config_tests/tilde_config/main.luau:1:11-31 +tests/src/require/without_config/ambiguous_directory_requirer.luau:1:16-58 +tests/src/require/without_config/ambiguous_file_requirer.luau:1:16-53 +tests/src/staticrequires/circular_a.luau:2:11-33 +tests/src/staticrequires/circular_b.luau:2:11-33 +tests/src/staticrequires/main.luau:2:7-13 +tests/std/fs.test.luau:114:13-24 +tests/std/fs.test.luau:133:18-20 +tests/std/fs.test.luau:142:18-20 +tests/std/fs.test.luau:210:18-20 +tests/std/json.test.luau:79:15-21 +tests/std/json.test.luau:79:15-21 +tests/std/json.test.luau:80:20-26 +tests/std/json.test.luau:80:20-26 +tests/std/json.test.luau:81:16-23 +tests/std/json.test.luau:81:16-23 +tests/std/json.test.luau:81:17-23 +tests/std/json.test.luau:81:17-23 +tests/std/json.test.luau:82:21-27 +tests/std/json.test.luau:82:21-27 +tests/std/json.test.luau:82:21-30 +tests/std/process.test.luau:39:18-18 +tests/std/process.test.luau:46:18-18 +tests/std/process.test.luau:55:17-18 +tests/std/process.test.luau:65:18-18 +tests/std/process.test.luau:79:45-46 +tests/std/process.test.luau:83:47-48 +tests/std/process.test.luau:87:45-57 +tests/std/process.test.luau:91:37-52 +tests/std/process.test.luau:95:37-49 +tests/std/process.test.luau:111:4-17 +tests/std/process.test.luau:115:19-20 +tests/std/process.test.luau:119:44-45 +tests/std/process.test.luau:123:41-42 +tests/std/process.test.luau:126:43-44 +tests/std/process.test.luau:130:41-53 +tests/std/process.test.luau:134:33-48 +tests/std/syntax/parser.test.luau:11:10-24 +tests/std/syntax/parser.test.luau:12:10-15 +tests/std/syntax/parser.test.luau:36:24-29 +tests/std/syntax/parser.test.luau:50:24-29 +tests/std/syntax/parser.test.luau:64:24-29 +tests/std/syntax/parser.test.luau:67:24-29 +tests/std/syntax/parser.test.luau:81:24-29 +tests/std/syntax/parser.test.luau:84:24-29 +tests/std/syntax/parser.test.luau:531:48-62 +tests/std/syntax/printer.test.luau:9:16-29 +tests/std/syntax/printer.test.luau:23:10-26 +tests/std/syntax/printer.test.luau:45:8-13 +tests/std/syntax/printer.test.luau:59:8-13 +tests/std/syntax/printer.test.luau:73:8-13 +tests/std/syntax/printer.test.luau:92:8-13 +tests/std/syntax/printer.test.luau:105:10-100 +tests/std/syntax/printer.test.luau:107:7-32 +tests/std/syntax/printer.test.luau:113:8-13 +tests/std/syntax/printer.test.luau:129:8-13 +tests/std/syntax/printer.test.luau:141:8-13 +tests/std/syntax/printer.test.luau:153:8-13 +tests/std/syntax/printer.test.luau:165:8-13 +tests/std/syntax/printer.test.luau:177:8-13 +tests/std/syntax/printer.test.luau:190:8-13 +tests/std/syntax/printer.test.luau:202:8-13 +tests/std/syntax/printer.test.luau:214:8-13 +tests/std/syntax/printer.test.luau:226:8-13 +tests/std/syntax/printer.test.luau:238:8-13 +tests/std/syntax/printer.test.luau:250:8-13 +tests/std/syntax/printer.test.luau:262:8-13 +tests/std/syntax/printer.test.luau:274:8-13 +tests/std/syntax/printer.test.luau:286:8-13 +tests/std/syntax/printer.test.luau:298:8-13 +tests/std/syntax/printer.test.luau:310:8-13 +tests/std/syntax/printer.test.luau:322:8-13 +tests/std/syntax/printer.test.luau:334:8-13 +tests/std/syntax/printer.test.luau:346:8-13 +tests/std/syntax/printer.test.luau:359:8-13 +tests/std/syntax/printer.test.luau:376:8-13 +tests/std/syntax/printer.test.luau:391:8-13 +tests/std/syntax/printer.test.luau:406:8-13 +tests/std/syntax/printer.test.luau:421:8-13 +tests/std/syntax/printer.test.luau:436:8-13 +tests/std/syntax/printer.test.luau:449:8-13 +tests/std/syntax/printer.test.luau:462:8-13 +tests/std/syntax/printer.test.luau:475:8-13 +tests/std/syntax/printer.test.luau:490:8-13 +tests/std/syntax/printer.test.luau:505:8-13 +tests/std/syntax/printer.test.luau:519:8-13 +tests/std/syntax/printer.test.luau:533:8-13 +tests/std/syntax/printer.test.luau:548:8-13 +tests/std/syntax/printer.test.luau:563:8-13 +tests/std/syntax/printer.test.luau:576:8-13 +tests/std/syntax/printer.test.luau:589:8-13 +tests/std/syntax/printer.test.luau:603:8-13 +tests/std/syntax/printer.test.luau:616:8-13 +tests/std/syntax/printer.test.luau:629:8-13 +tests/std/syntax/printer.test.luau:642:8-13 +tests/std/syntax/printer.test.luau:655:8-13 +tests/std/syntax/printer.test.luau:668:8-13 +tests/std/syntax/printer.test.luau:681:8-13 +tests/std/syntax/printer.test.luau:694:8-13 +tests/std/syntax/printer.test.luau:707:8-13 +tests/std/syntax/printer.test.luau:720:8-13 +tests/std/syntax/printer.test.luau:733:8-13 +tests/std/syntax/printer.test.luau:746:8-13 +tests/std/syntax/printer.test.luau:759:8-13 +tests/std/syntax/printer.test.luau:772:8-13 +tests/std/syntax/printer.test.luau:785:8-13 +tests/std/syntax/printer.test.luau:798:8-13 +tests/std/syntax/printer.test.luau:811:8-13 +tests/std/syntax/printer.test.luau:824:8-13 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:835:38-60 +tests/std/syntax/printer.test.luau:838:8-13 +tests/std/syntax/printer.test.luau:859:8-13 +tests/std/syntax/printer.test.luau:887:23-31 +tests/std/syntax/printer.test.luau:894:8-13 +tests/std/tableext.test.luau:57:18-18 +tests/std/tableext.test.luau:62:18-18 +tests/std/tableext.test.luau:63:18-18 +tests/std/test.test.luau:15:10-27 +tests/std/test.test.luau:113:4-9 +tests/std/test.test.luau:132:4-9 +tests/std/test.test.luau:149:4-9 +tests/std/test.test.luau:168:4-9 +tests/std/test.test.luau:185:4-9 +tests/std/test.test.luau:204:4-9 +tests/std/test.test.luau:223:4-9 +tests/std/test.test.luau:246:4-9 +tests/std/test.test.luau:263:4-9 +tests/std/test.test.luau:282:4-9 +tests/std/test.test.luau:299:4-9 +tests/std/test.test.luau:318:4-9 +tests/std/test.test.luau:337:4-9 +tools/luthier.luau:139:10-34 +tools/luthier.luau:162:2-26 +tools/luthier.luau:201:3-12 +tools/luthier.luau:202:11-25 +tools/luthier.luau:243:8-11 +tools/luthier.luau:296:8-11 +tools/luthier.luau:420:24-68 +tools/luthier.luau:429:3-50 +tools/luthier.luau:547:29-34 +tools/luthier.luau:565:18-27 +tools/luthier.luau:595:3-79 +tools/luthier.luau:595:28-78 +tools/luthier.luau:599:3-75 +tools/luthier.luau:599:28-74 +tools/luthier.luau:610:2-48 +tools/luthier.luau:663:2-11 +tools/luthier.luau:664:10-24 +tools/luthier.luau:794:8-42 diff --git a/tools/check.luau b/tools/check.luau new file mode 100644 index 000000000..4c2121259 --- /dev/null +++ b/tools/check.luau @@ -0,0 +1,178 @@ +local cli = require("@batteries/cli") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local tbl = require("@std/tableext") +local difftext = require("@batteries/difftext") + +local FAILLIST_PATH = "tools/check-faillist.txt" + +local TYPECHECK_PATHS = { + "examples", + "lute/cli", + "lute/std/libs", + "tests", + "tools", +} + +local function collectFiles(dir: string): { string } + local files: { string } = {} + local it = fs.walk(dir, { recursive = true }) + local p = it() + while p do + local pathStr = path.format(p) + if fs.type(p) == "file" and string.match(pathStr, "%.luau$") then + table.insert(files, pathStr) + end + p = it() + end + table.sort(files) + return files +end + +local function getAllFiles(): { string } + local allFiles: { string } = {} + for _, dir in TYPECHECK_PATHS do + local files = collectFiles(dir) + tbl.extend(allFiles, files) + end + return allFiles +end + +local function readFaillist(): string + local ok, file = pcall(fs.open, FAILLIST_PATH, "r") + if not ok or not file then + return "" + end + + local contents = fs.read(file) + fs.close(file) + return contents +end + +local function writeFaillist(contents: string): () + local file = fs.open(FAILLIST_PATH, "w+") + fs.write(file, contents) + fs.close(file) +end + +local function runTypecheck(lutePath: string, files: { string }): string + local args: { string } = { lutePath, "check" } + for _, file in files do + table.insert(args, file) + end + local result = process.run(args) + return result.stdout .. result.stderr +end + +type ErrorEntry = { + file: string, + line: number, + col: number, + colEnd: number, +} + +-- We need this so that the faillist is machine agnostic +-- e.g. homedirs shouldnt show up in the path +local function relativePath(p: string, cwdStr: string): string + local cwdWithSep = cwdStr + if string.sub(cwdWithSep, -1) ~= "/" then + cwdWithSep = cwdWithSep .. "/" + end + if string.sub(p, 1, #cwdWithSep) == cwdWithSep then + p = string.sub(p, #cwdWithSep + 1) + end + + if string.sub(p, 1, 2) == "./" then + p = string.sub(p, 3) + end + + return p +end + +local function parseErrors(output: string, cwdStr: string): { ErrorEntry } + local errors: { ErrorEntry } = {} + + for line in string.gmatch(output, "[^\n]+") do + -- Matches the output of the `report` function in tc.cpp: path:line:col-colEnd: ... + local file, lineNum, col, colEnd = string.match(line, "^(.+):(%d+):(%d+)-(%d+):") + + if file and lineNum then + table.insert(errors, { + file = relativePath(file, cwdStr), + line = tonumber(lineNum) :: number, + col = tonumber(col) :: number, + colEnd = tonumber(colEnd) :: number, + }) + end + end + + return errors +end + +local function sortErrors(errors: { ErrorEntry }): { ErrorEntry } + table.sort(errors, function(a: ErrorEntry, b: ErrorEntry) + if a.file ~= b.file then + return a.file < b.file + end + if a.line ~= b.line then + return a.line < b.line + end + return a.col < b.col + end) + return errors +end + +local function formatFaillist(errors: { ErrorEntry }): string + local lines: { string } = {} + + for _, err in errors do + table.insert(lines, `{err.file}:{err.line}:{err.col}-{err.colEnd}`) + end + + return table.concat(lines, "\n") .. "\n" +end + +local function main(args: { string }) + local parser = cli.parser() + parser:add("update", "flag", { help = "Update the faillist file", aliases = { "u" } }) + parser:add("check", "positional", { help = "This script" }) + parser:add("lute", "positional", { help = "Path to lute executable", required = true }) + parser:parse(args) + + local shouldUpdate = parser:has("update") + local lutePath = parser:get("lute") + assert(lutePath, "Missing required lute path argument") + + print("Collecting files to typecheck...") + local allFiles = getAllFiles() + print(`Found {#allFiles} files`) + + print("Running typecheck...") + local output = runTypecheck(lutePath, allFiles) + + local cwdStr = path.format(process.cwd()) + local errors = parseErrors(output, cwdStr) + local sortedErrors = sortErrors(errors) + local formattedOutput = formatFaillist(sortedErrors) + + local oldFaillist = readFaillist() + + if shouldUpdate then + writeFaillist(formattedOutput) + print(`Updated {FAILLIST_PATH} with {#sortedErrors} errors`) + return + end + + if oldFaillist == formattedOutput then + print("\nTypecheck passed! (output matches faillist)") + return + end + + print("\nTypecheck output differs from faillist:") + print(difftext.prettydiff(oldFaillist, formattedOutput, { detailed = true, includeLineNumbers = true })) + print("\nRun with --update to update the faillist, or fix the errors.") + process.exit(1) +end + +main({ ... }) From e1b8fcb8b838d8c3d05c3713a5859261bca17c05 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 23 Jan 2026 11:54:07 -0800 Subject: [PATCH 302/642] Adds a profiler to `lute run` (#746) You can invoke the profiler with: ``` lute --profile <--frequency 1000> <--profile-output filename> myfile.luau ``` to get a trace file that can be inspected by ui.perfetto.dev. This profiler works by sampling callstacks at a given frequency, assigning the delta time between samples to _each_ frame in the currently observed callstack(which is a simplifying assumption, but if your sample frequency is high enough, end up being okay). By tracking callstacks between calls from the VM to `interrupt`, we can emit begin and end events by diffing the stacks, nesting as appropriate. For example, a callstack like: ``` main -> foo -> bar -> baz ``` should produce: ``` BEGIN_main, BEGIN_foo, BEGIN_bar, BEGIN_baz, END_baz, END_bar, END_foo, END_main ``` --- docs/cli/run.md | 26 +- examples/profile_test.json | 2788 ++++++++++++++++++++++++++ examples/profile_test.luau | 92 + lute/cli/include/lute/climain.h | 10 +- lute/cli/include/lute/profiler.h | 3 +- lute/cli/src/climain.cpp | 93 +- lute/cli/src/profiler.cpp | 195 +- lute/common/include/lute/fileutils.h | 6 +- lute/common/src/fileutils.cpp | 14 + tests/std/syntax/parser.test.luau | 4 + tools/check-faillist.txt | 36 +- 11 files changed, 3164 insertions(+), 103 deletions(-) create mode 100644 examples/profile_test.json create mode 100644 examples/profile_test.luau diff --git a/docs/cli/run.md b/docs/cli/run.md index b7afcf4e9..4be89933b 100755 --- a/docs/cli/run.md +++ b/docs/cli/run.md @@ -1,7 +1,8 @@ # run Run a Luau script. Can be omitted if you just pass the name of the script -to Lute. +to Lute. `lute run` also comes with a sampling profiler, that outputs json traces that can be +viewed at ui.perfetto.dev. The profiler currently only works with single threaded code. ## Usage @@ -14,8 +15,31 @@ is equivalent to: lute [args...] ``` +```bash +lute --profile [--profile-output somefile] [--frequency ] [args...] +``` + +will profile the execution of `script.luau` at the provided frequency, outputting the trace to a file called +somefile. + + ## Options ### `-h, --help` Display this usage message. + +### `--profile` + +Enable profiling for the script being passed to `lute run`. Default sampling frequency is +10000Hz, and default output filename is `_.json`. + +### `--profile-output ` + +Sets the desired filename for the profile trace dump; + +### `--frequency ` + +Sets the frequency at which the profiler will sample the callstack. + + diff --git a/examples/profile_test.json b/examples/profile_test.json new file mode 100644 index 000000000..25fad11ff --- /dev/null +++ b/examples/profile_test.json @@ -0,0 +1,2788 @@ +{"traceEvents":[ +{"ph":"B","name":"(anonymous)","cat":"function","pid":1,"tid":1,"ts":20,"args":{"file":"./examples/profile_test.luau","line":1}}, +{"ph":"B","name":"runTests","cat":"function","pid":1,"tid":1,"ts":90,"args":{"file":"./examples/profile_test.luau","line":71}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":100,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":114,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"format","cat":"function","pid":1,"tid":1,"ts":114,"args":{"file":"[C]","line":-1}}, +{"ph":"E","name":"format","cat":"function","pid":1,"tid":1,"ts":131,"args":{"file":"[C]","line":-1}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":131,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":131,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":131,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":143,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":153,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":163,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":173,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"createTree","cat":"function","pid":1,"tid":1,"ts":183,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"E","name":"createTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":52}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":198,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":208,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":208,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":208,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"E","name":"sumTree","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":63}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":258,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":258,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":278,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":278,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":278,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":298,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":308,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":318,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":328,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":358,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":358,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":368,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":368,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":378,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":378,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":398,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":418,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":428,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":428,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":428,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":474,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":474,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":484,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":484,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":494,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":494,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":514,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":514,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":524,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":524,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":554,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":574,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":574,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":574,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":574,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":584,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":594,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":594,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":594,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":604,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":624,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":624,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":624,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":624,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":634,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":634,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":634,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":634,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":644,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":644,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":644,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":654,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":654,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":654,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":664,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":704,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":734,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":734,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":734,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":734,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":754,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":754,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":754,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":764,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":764,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":764,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":774,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":784,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":784,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":784,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":814,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":844,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":844,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":854,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":854,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":864,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":884,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":884,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":904,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":934,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":987,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":997,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":997,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1007,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1027,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1047,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1077,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1097,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1117,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1117,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1117,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1117,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1137,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1147,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1177,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1187,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1197,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1207,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1207,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1217,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1227,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1237,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1237,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1247,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1267,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1277,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1277,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1287,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1287,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1297,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1297,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1297,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1297,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1307,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1307,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1307,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1347,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1347,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1357,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1357,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1357,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1387,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1477,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1497,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1507,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1517,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1517,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1517,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1537,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1557,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1567,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1577,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1587,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1587,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1597,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1597,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1597,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1597,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1617,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1627,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1627,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1627,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1681,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1681,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1681,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1691,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1721,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1721,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1731,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1731,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1741,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1761,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1761,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1761,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1771,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1781,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1781,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1781,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1791,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1801,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1821,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1821,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1821,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1831,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1831,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1831,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1841,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1851,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1861,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":1861,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1861,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1891,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1905,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1915,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1915,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1927,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1927,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1937,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":1997,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2007,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2007,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2027,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2047,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2047,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2057,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2067,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2077,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2077,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2087,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2097,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2097,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2097,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2097,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2127,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2170,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2170,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2190,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2190,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2230,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2240,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2240,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2250,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2260,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2260,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2270,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2270,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2280,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2310,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2340,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2350,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2350,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":2350,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2350,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2402,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2412,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2422,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2442,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2442,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2452,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2452,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2482,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2492,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2502,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2512,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2522,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2522,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2552,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2562,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2562,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2582,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2612,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2632,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2642,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2652,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2652,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2732,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2802,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2802,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2802,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2812,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":2842,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2892,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2902,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2902,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2902,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2952,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2952,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2982,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":2992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3022,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3022,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3022,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3042,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3052,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3052,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3052,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3104,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3114,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3114,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3124,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3124,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3124,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3124,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3144,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3184,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3194,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3204,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3204,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3214,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3224,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3224,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3234,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3234,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3234,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3244,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3244,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3254,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3274,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3284,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3284,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3284,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3314,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3328,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3378,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3388,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3398,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3398,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3418,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3428,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3428,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3428,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3428,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3458,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3468,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3468,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3478,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3488,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3488,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3488,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3498,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3518,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3548,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3558,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3568,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3568,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3593,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3603,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3603,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3613,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3613,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3613,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3623,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3623,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3633,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3633,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3643,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3663,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3673,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3673,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3703,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3703,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3733,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3743,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3793,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3793,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3813,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3813,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3823,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3823,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3823,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3833,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3833,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3853,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3863,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3903,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3923,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3963,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3973,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3983,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3983,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":3983,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":3983,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4048,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4078,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4078,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4088,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4088,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4088,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4098,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4138,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4138,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4148,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4188,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4208,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4218,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4218,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4218,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4261,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4261,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4271,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4271,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4281,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4281,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4331,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4341,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4351,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4351,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4361,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4361,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4361,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4371,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4371,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4381,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4381,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4381,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4391,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4391,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4391,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4391,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4401,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4401,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4411,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4411,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4431,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4441,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4451,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4451,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4461,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4471,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4471,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4471,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4481,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4521,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4531,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4541,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4551,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4551,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4561,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4571,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4571,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4571,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4591,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4601,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4611,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4611,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4611,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4611,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4621,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4621,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4621,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4631,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4631,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4641,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4641,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4641,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4651,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4651,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4661,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4671,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4701,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4711,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4721,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4721,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":4721,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4721,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4751,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4784,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4794,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4794,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4794,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4804,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4824,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4834,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4844,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4854,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4854,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4864,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4864,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4874,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4874,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4884,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4894,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4904,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4914,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4924,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4924,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4944,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4954,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4984,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":4998,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5008,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5018,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5048,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5098,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5098,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5118,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5148,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5148,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5168,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5178,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5188,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5188,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5188,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5218,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5232,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5232,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5242,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5252,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5252,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5313,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5313,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5313,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5363,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5373,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5383,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5403,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5403,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5413,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5453,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5465,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5465,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5475,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5475,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5485,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5485,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5485,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5485,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5495,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5495,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5495,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5505,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5515,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5525,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5535,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5545,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5545,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5545,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5545,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5555,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5555,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5575,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5595,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5595,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5605,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5605,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5605,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5605,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5635,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5635,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5635,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5645,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5655,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":5655,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5655,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5699,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5699,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5709,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5709,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5709,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5719,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5719,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5739,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5739,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5749,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5749,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5759,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5779,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5779,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5789,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5789,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5809,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5829,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5839,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5839,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5839,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5849,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5859,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5859,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5869,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5879,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5919,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5933,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5933,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5973,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5993,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5993,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":5993,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6063,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6083,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6083,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6103,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6113,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6123,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6123,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6123,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6153,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6163,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6173,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6173,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6173,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6183,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6193,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6193,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6193,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6203,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6213,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6223,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6223,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6223,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6233,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6243,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6243,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6253,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6253,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6253,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6263,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6273,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6283,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6293,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6303,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6313,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6323,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6333,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6343,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6353,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6363,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":6363,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6363,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6393,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6426,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6426,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6426,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6436,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6436,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6446,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6446,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6456,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6456,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6456,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6466,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6476,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6486,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6496,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6496,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6506,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6506,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6506,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6506,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6516,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6516,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6516,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6516,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6526,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6526,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6536,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6536,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6546,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6546,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6566,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6566,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6576,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6586,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6586,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6586,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6596,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6626,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6636,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6646,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6646,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6646,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6662,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6802,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6822,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":6852,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6892,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6892,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6902,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6912,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6952,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6962,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6962,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6962,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6962,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6982,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":6992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7052,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7062,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7062,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7062,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7089,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7104,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7104,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7134,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7144,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7154,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7164,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7174,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7184,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7204,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7214,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7224,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7244,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7254,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7264,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7274,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7284,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7314,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7324,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7334,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7334,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7334,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7344,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7344,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7364,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7384,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7394,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7404,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7424,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7434,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7444,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7454,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7454,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7464,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7464,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7464,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7474,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7474,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7484,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7494,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7504,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7514,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7514,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7514,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7544,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7555,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7565,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7575,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7575,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7585,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7585,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7595,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7615,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7625,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7645,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7655,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7665,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7675,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7685,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7695,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7695,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7695,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7705,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7705,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7715,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7725,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7755,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7765,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7775,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7795,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7795,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7815,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7815,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7825,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7825,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7835,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7835,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7835,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7835,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7845,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7855,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7855,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7875,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7895,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7905,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7925,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7925,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7935,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":7945,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7945,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":7985,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8005,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8015,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8025,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8025,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8025,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8055,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8065,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8065,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8075,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8075,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8095,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8105,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8135,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8135,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8135,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8145,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8145,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8155,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8165,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8210,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8220,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8220,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8220,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8220,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8230,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8230,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8240,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8240,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8250,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8250,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8250,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8270,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8270,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8280,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8300,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8320,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8330,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8340,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8350,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8350,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8360,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8370,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8370,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8380,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8410,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8420,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8430,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8460,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8470,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8470,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8470,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8550,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8550,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8560,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8560,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8560,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8580,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8580,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8650,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8650,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8650,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8650,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":8660,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":8670,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":8670,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8670,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8700,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8732,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8772,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8782,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8812,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8822,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8832,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8832,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8842,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8842,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8852,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8862,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8872,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8882,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":8912,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8922,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8932,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8942,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8972,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":8992,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9002,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9012,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9022,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9032,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9042,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9052,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9062,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9072,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9082,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9092,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9102,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9102,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9102,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9142,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9152,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9152,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9152,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9162,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9162,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9162,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9172,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9192,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9202,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9212,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9222,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9222,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9222,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9222,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9232,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9242,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9262,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9272,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9272,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9282,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9312,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9312,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9312,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9312,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9322,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9352,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9362,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9372,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9402,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9402,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9402,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9412,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9412,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9422,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9432,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9432,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9442,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9452,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9472,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9492,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9502,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9512,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9512,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9512,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9522,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9522,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9532,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9542,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9542,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9542,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9572,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9582,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9592,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9592,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9592,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9602,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9602,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9612,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9612,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9612,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9622,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9632,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9632,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9632,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9652,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9672,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9682,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9692,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9702,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9712,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9722,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9732,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9732,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9742,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9752,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9762,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9792,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9807,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9817,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9827,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9827,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9837,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9847,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9847,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9847,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9857,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9867,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9867,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9887,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9897,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9897,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9907,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9907,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9907,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9927,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9947,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9957,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9967,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9967,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9977,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9987,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":9987,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":9987,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10017,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10028,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10038,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10048,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10058,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10068,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10078,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10098,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10108,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10128,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10138,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10158,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10168,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10168,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10168,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10178,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10188,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10198,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10228,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10238,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10248,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10258,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10258,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10268,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10278,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10288,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10298,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10308,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10308,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10308,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10318,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10328,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10338,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10348,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10358,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10368,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10378,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":10408,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":10418,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10418,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10448,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10459,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10469,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10480,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10490,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10500,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10510,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10510,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10540,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10550,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10570,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10580,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10590,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10600,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10610,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10610,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10620,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10620,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10630,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10660,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10673,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10673,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10683,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10693,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10703,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10703,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10713,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10723,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10733,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10743,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10753,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10763,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10763,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10773,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10783,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10793,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10803,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10803,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10803,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10813,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10823,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10833,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10843,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":10873,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10883,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10893,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10893,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10903,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10903,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10913,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10923,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10923,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10943,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10953,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10963,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10963,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10963,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10983,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":10993,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11003,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11013,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11023,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11033,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"processArray","cat":"function","pid":1,"tid":1,"ts":11053,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"E","name":"processArray","cat":"function","pid":1,"tid":1,"ts":11063,"args":{"file":"./examples/profile_test.luau","line":12}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11063,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11093,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11103,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11103,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11113,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11125,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11125,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11135,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11145,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11145,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11175,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11175,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11185,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11195,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11205,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11215,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11215,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11225,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11235,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11235,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11255,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11255,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11255,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11255,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11265,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11275,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11305,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11317,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11327,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11337,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11337,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11337,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11337,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11347,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11367,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11377,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11387,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11397,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11407,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11407,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11417,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11417,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11427,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11437,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11447,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11457,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11467,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11477,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11477,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11487,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11517,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11527,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11547,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11567,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11577,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11587,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11587,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11607,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11607,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11607,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11617,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11627,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11637,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11647,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11657,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11667,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11667,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11667,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11677,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11677,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11677,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11687,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11687,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11697,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"E","name":"fibonacci","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":4}}, +{"ph":"B","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11707,"args":{"file":"./examples/profile_test.luau","line":37}}, +{"ph":"E","name":"(anonymous)","cat":"function","pid":1,"tid":1,"ts":11737,"args":{"file":"./examples/profile_test.luau","line":1}}, +{"ph":"E","name":"runTests","cat":"function","pid":1,"tid":1,"ts":11737,"args":{"file":"./examples/profile_test.luau","line":71}}, +{"ph":"E","name":"middleWork","cat":"function","pid":1,"tid":1,"ts":11737,"args":{"file":"./examples/profile_test.luau","line":37}} +]} diff --git a/examples/profile_test.luau b/examples/profile_test.luau new file mode 100644 index 000000000..46616e853 --- /dev/null +++ b/examples/profile_test.luau @@ -0,0 +1,92 @@ +-- Test program for profiler with deep call stacks and recursion + +-- Fibonacci with recursion (creates deep stacks) +local function fibonacci(n: number): number + if n <= 1 then + return n + end + return fibonacci(n - 1) + fibonacci(n - 2) +end + +-- Array processing with nested calls +local function processArray(arr: { number }, depth: number): number + if depth == 0 then + local sum = 0 + for _, v in arr do + sum += v * v + end + return sum + else + local result = 0 + for i = 1, #arr do + result += processArray({ arr[i], arr[i] * 2 }, depth - 1) + end + return result + end +end + +-- Nested function calls +local function innerWork(x: number): number + local result = 0 + for i = 1, x do + result += math.sin(i) * math.cos(i) + end + return result +end + +local function middleWork(x: number): number + local sum = 0 + for i = 1, 10 do + sum += innerWork(x) + end + return sum +end + +-- Tree traversal (creates interesting call patterns) +type TreeNode = { + value: number, + left: TreeNode?, + right: TreeNode?, +} + +local function createTree(depth: number, value: number): TreeNode + if depth == 0 then + return { value = value, left = nil, right = nil } + end + return { + value = value, + left = createTree(depth - 1, value * 2), + right = createTree(depth - 1, value * 2 + 1), + } +end + +local function sumTree(node: TreeNode?): number + if node == nil then + return 0 + end + return node.value + sumTree(node.left) + sumTree(node.right) +end + +-- Main test function +local function runTests() + print("Starting profiler test...") + + print("Tree traversal") + for i = 1, 5 do + local tree = createTree(i, 1) + local sum = sumTree(tree) + print(` Tree sum = {sum}`) + end + + print("Mixed workload") + for round = 1, 50 do + local fib = fibonacci(15) + local arr_result = processArray({ 1, 2, 3 }, 2) + local work = middleWork(50) + print(` Round {round}: fib={fib}, arr={arr_result}, work={work}`) + end + + print("\nProfiler test complete!") +end + +runTests() diff --git a/lute/cli/include/lute/climain.h b/lute/cli/include/lute/climain.h index 30f219e14..9c304962b 100644 --- a/lute/cli/include/lute/climain.h +++ b/lute/cli/include/lute/climain.h @@ -1,11 +1,18 @@ #pragma once #include +#include #include struct lua_State; struct Runtime; class LuteReporter; +struct ProfileOptions +{ + std::string filename; + int frequency = 10000; +}; + int cliMain(int argc, char** argv, LuteReporter& reporter); bool runBytecode( Runtime& runtime, @@ -14,5 +21,6 @@ bool runBytecode( lua_State* GL, int program_argc, char** program_argv, - LuteReporter& reporter + LuteReporter& reporter, + std::optional profileOptions = std::nullopt ); diff --git a/lute/cli/include/lute/profiler.h b/lute/cli/include/lute/profiler.h index dadc95b96..2ff881fa7 100644 --- a/lute/cli/include/lute/profiler.h +++ b/lute/cli/include/lute/profiler.h @@ -1,7 +1,8 @@ #pragma once +#include "lute/reporter.h" struct lua_State; void profilerStart(lua_State* L, int frequency); void profilerStop(); -void profilerDump(const char* path); +void profilerDump(const char* path, LuteReporter& reporter); diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 3f540d7fb..953622953 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -2,11 +2,13 @@ #include "lute/clicommands.h" #include "lute/compile.h" +#include "lute/fileutils.h" #include "lute/luauflags.h" #include "lute/modulepath.h" #include "lute/options.h" #include "lute/packagerun.h" #include "lute/process.h" +#include "lute/profiler.h" #include "lute/ref.h" #include "lute/reporter.h" #include "lute/requiresetup.h" @@ -24,6 +26,9 @@ #include "lua.h" #include "lualib.h" +#include +#include +#include #include #include #include @@ -82,7 +87,10 @@ static const char* VERSION_STRING = LUTE_VERSION_FULL; static const char* RUN_HELP_STRING = R"(Usage: lute run [args...] Run Options: - -h, --help Display this usage message. + --profile Enable profiling for the script. + --profile-output Output file for the profile (default: _.json). + --frequency Profiler sampling frequency in Hz (default: 10000). + -h, --help Display this usage message. )"; static const char* PKGRUN_HELP_STRING = R"(Usage: lute pkg run [args...] @@ -125,12 +133,16 @@ bool runBytecode( lua_State* GL, int program_argc, char** program_argv, - LuteReporter& reporter + LuteReporter& reporter, + std::optional profileOptions ) { // module needs to run in a new thread, isolated from the rest lua_State* L = lua_newthread(GL); + if (profileOptions) + profilerStart(L, profileOptions->frequency); + // new thread needs to have the globals sandboxed luaL_sandboxthread(L); @@ -163,10 +175,25 @@ bool runBytecode( lua_pop(GL, 1); - return runtime.runToCompletion(); + bool b = runtime.runToCompletion(); + if (profileOptions) + { + profilerStop(); + profilerDump(profileOptions->filename.c_str(), reporter); + } + + return b; } -static bool runFile(Runtime& runtime, const char* name, lua_State* GL, int program_argc, char** program_argv, LuteReporter& reporter) +static bool runFile( + Runtime& runtime, + const char* name, + lua_State* GL, + int program_argc, + char** program_argv, + LuteReporter& reporter, + std::optional profileOptions = std::nullopt +) { if (isDirectory(name)) { @@ -185,7 +212,7 @@ static bool runFile(Runtime& runtime, const char* name, lua_State* GL, int progr std::string bytecode = Luau::compile(*source, copts()); - return runBytecode(runtime, bytecode, chunkname, GL, program_argc, program_argv, reporter); + return runBytecode(runtime, bytecode, chunkname, GL, program_argc, program_argv, reporter, profileOptions); } static int assertionHandler(const char* expr, const char* file, int line, const char* function) @@ -269,6 +296,10 @@ int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness std::string command = packageAwareness ? "pkg run" : "run"; std::string filePath; int program_argc = 0; + bool enableProfiling = false; + ProfileOptions profileOpts; + bool hasProfileOutputArg = false; + bool hasProfileFreqArg = false; char** program_argv = nullptr; for (int i = argOffset; i < argc; ++i) @@ -280,6 +311,32 @@ int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); return 0; } + else if (strcmp(currentArg, "--profile") == 0) + { + enableProfiling = true; + } + else if (strcmp(currentArg, "--profile-output") == 0) + { + if (i + 1 >= argc) + { + reporter.reportError("Error: --profile-output requires a path argument."); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); + return 1; + } + profileOpts.filename = argv[++i]; + hasProfileOutputArg = true; + } + else if (strcmp(currentArg, "--frequency") == 0) + { + if (i + 1 >= argc) + { + reporter.reportError("Error: --frequency requires a value argument."); + reporter.reportOutput(packageAwareness ? PKGRUN_HELP_STRING : RUN_HELP_STRING); + return 1; + } + profileOpts.frequency = std::stoi(argv[++i]); + hasProfileFreqArg = true; + } else if (currentArg[0] == '-') { reporter.formatError("Error: Unrecognized option '%s' for '%s' command.", currentArg, command.c_str()); @@ -309,6 +366,30 @@ int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness return 1; } + std::optional profileOptions; + if (enableProfiling) + { + if (profileOpts.filename.empty()) + { + auto now = std::chrono::system_clock::now(); + std::time_t nowTime = std::chrono::system_clock::to_time_t(now); + std::tm* localTime = std::localtime(&nowTime); + + char dateTimeStr[20]; + std::strftime(dateTimeStr, sizeof(dateTimeStr), "%Y-%m-%d_%H-%M-%S", localTime); + + std::string baseName = Lute::getFilenameWithoutExtension(filePath); + profileOpts.filename = std::string(dateTimeStr) + "_" + baseName + ".json"; + } + + profileOptions.emplace(profileOpts); + } + else if (hasProfileOutputArg || hasProfileFreqArg) + { + reporter.reportError("You passed --profile-output or --frequency without passing --profile."); + return 1; + } + Runtime runtime; lua_State* L; @@ -340,7 +421,7 @@ int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness L = setupCliState(runtime); } - bool success = runFile(runtime, validPath.c_str(), L, program_argc, program_argv, reporter); + bool success = runFile(runtime, validPath.c_str(), L, program_argc, program_argv, reporter, profileOptions); return success ? 0 : 1; } diff --git a/lute/cli/src/profiler.cpp b/lute/cli/src/profiler.cpp index 028d9eb81..44d797cf9 100644 --- a/lute/cli/src/profiler.cpp +++ b/lute/cli/src/profiler.cpp @@ -1,75 +1,112 @@ #include "lute/profiler.h" -#include "lua.h" +#include "lute/reporter.h" -#include "Luau/DenseHash.h" +#include "lua.h" -#include #include #include +#include +#include + + +struct FrameInfo +{ + std::string name; + std::string file; + int line; + + FrameInfo(const lua_Debug& dbg) + : name(dbg.name ? dbg.name : "(anonymous)") + , file(dbg.source ? &dbg.source[1] : "(unknown)") // TODO(Varun)replace with chunkname utils API + , line(dbg.linedefined) + { + } + + bool operator==(const FrameInfo& other) const + { + return name == other.name && file == other.file && line == other.line; + } +}; + +struct TraceEvent +{ + TraceEvent(char phase, const FrameInfo& fi, uint64_t timestamp) + : phase(phase) + , name(fi.name) + , file(fi.file) + , line(fi.line) + , timestamp(timestamp) + { + } + char phase; // 'B' for begin, 'E' for end + std::string name; + std::string file; + int line; + uint64_t timestamp; // Timestamp in microseconds +}; struct Profiler { // static state lua_Callbacks* callbacks = nullptr; - int frequency = 1000; + int frequency = 10000; std::thread thread; // variables for communication between loop and trigger std::atomic exit = false; - std::atomic ticks = 0; - std::atomic samples = 0; + std::atomic pendingTicks = 0; - // private state for trigger - uint64_t currentTicks = 0; - std::string stackScratch; + // state for tracking stack changes (only accessed in trigger) + std::vector previousStack; + std::vector events; + uint64_t currentTimestamp = 0; - // statistics, updated by trigger - Luau::DenseHashMap data{""}; - uint64_t gc[16] = {}; } gProfiler; static void profilerTrigger(lua_State* L, int gc) { - uint64_t currentTicks = gProfiler.ticks.load(); - uint64_t elapsedTicks = currentTicks - gProfiler.currentTicks; - - if (elapsedTicks) + uint64_t ticks = gProfiler.pendingTicks.exchange(0); + if (ticks == 0) { - std::string& stack = gProfiler.stackScratch; + gProfiler.callbacks->interrupt = nullptr; + return; + } - stack.clear(); + std::vector currentStack; + lua_Debug dbg; + for (int level = 0; lua_getinfo(L, level, "sln", &dbg); ++level) + { + currentStack.emplace_back(dbg); + } - if (gc > 0) - stack += "GC,GC,"; + // Stacks are in reverse order, i.e + // leaf ..... main, so we should walk backwards + size_t commonDepth = 0; + size_t iterPrev = gProfiler.previousStack.size(); + size_t iterCurr = currentStack.size(); - lua_Debug ar; - for (int level = 0; lua_getinfo(L, level, "sn", &ar); ++level) - { - if (!stack.empty()) - stack += ';'; - - stack += ar.short_src; - stack += ','; - if (ar.name) - stack += ar.name; - stack += ','; - if (ar.linedefined > 0) - stack += std::to_string(ar.linedefined); - } + while (iterPrev > 0 && iterCurr > 0 && gProfiler.previousStack[iterPrev - 1] == currentStack[iterCurr - 1]) + { + commonDepth++; + iterPrev--; + iterCurr--; + } - if (!stack.empty()) - { - gProfiler.data[stack] += elapsedTicks; - } + for (size_t i = 0; i < gProfiler.previousStack.size() - commonDepth; i++) + { + const FrameInfo& frame = gProfiler.previousStack.at(i); + gProfiler.events.emplace_back('E', frame, gProfiler.currentTimestamp); + } - if (gc > 0) - { - gProfiler.gc[gc] += elapsedTicks; - } + for (size_t i = currentStack.size() - commonDepth; i > 0; i--) + { + const FrameInfo& frame = currentStack.at(i - 1); + gProfiler.events.emplace_back('B', frame, gProfiler.currentTimestamp); } - gProfiler.currentTicks = currentTicks; + gProfiler.currentTimestamp += ticks; + gProfiler.previousStack = std::move(currentStack); gProfiler.callbacks->interrupt = nullptr; } @@ -83,10 +120,9 @@ static void profilerLoop() if (now - last >= 1.0 / double(gProfiler.frequency)) { - int64_t ticks = int64_t((now - last) * 1e6); + uint64_t ticks = uint64_t((now - last) * 1e6); - gProfiler.ticks += ticks; - gProfiler.samples++; + gProfiler.pendingTicks += ticks; gProfiler.callbacks->interrupt = profilerTrigger; last += ticks * 1e-6; @@ -102,6 +138,10 @@ void profilerStart(lua_State* L, int frequency) { gProfiler.frequency = frequency; gProfiler.callbacks = lua_callbacks(L); + gProfiler.previousStack.clear(); + gProfiler.events.clear(); + gProfiler.currentTimestamp = 0; + gProfiler.pendingTicks = 0; gProfiler.exit = false; gProfiler.thread = std::thread(profilerLoop); @@ -111,53 +151,44 @@ void profilerStop() { gProfiler.exit = true; gProfiler.thread.join(); -} -void profilerDump(const char* path) -{ - FILE* f = fopen(path, "wb"); - if (!f) + // For any of the remaining frames on the stack, insert an artificial end(E) event. + for (size_t i = 0; i < gProfiler.previousStack.size(); i++) { - fprintf(stderr, "Error opening profile %s\n", path); - return; + const FrameInfo& frame = gProfiler.previousStack.at(i); + gProfiler.events.emplace_back('E', frame, gProfiler.currentTimestamp); } + gProfiler.previousStack.clear(); +} - uint64_t total = 0; - - for (auto& p : gProfiler.data) +void profilerDump(const char* path, LuteReporter& reporter) +{ + FILE* out = fopen(path, "w"); + if (!out) { - fprintf(f, "%lld %s\n", static_cast(p.second), p.first.c_str()); - total += p.second; + reporter.formatError("Failed to open profile output file: %s", path); + return; } - fclose(f); - - printf( - "Profiler dump written to %s (total runtime %.3f seconds, %lld samples, %lld stacks)\n", - path, - double(total) / 1e6, - static_cast(gProfiler.samples.load()), - static_cast(gProfiler.data.size()) - ); + fprintf(out, "{\"traceEvents\":[\n"); - uint64_t totalgc = 0; - for (uint64_t p : gProfiler.gc) - totalgc += p; - - if (totalgc) + for (size_t i = 0; i < gProfiler.events.size(); i++) { - printf("GC: %.3f seconds (%.2f%%)", double(totalgc) / 1e6, double(totalgc) / double(total) * 100); + const TraceEvent& evt = gProfiler.events[i]; - for (size_t i = 0; i < std::size(gProfiler.gc); ++i) - { - extern const char* luaC_statename(int state); + if (i > 0) + fprintf(out, ",\n"); - uint64_t p = gProfiler.gc[i]; + fprintf(out, "{\"ph\":\"%c\",", evt.phase); + fprintf(out, "\"name\":\"%s\",", evt.name.c_str()); + fprintf(out, "\"cat\":\"function\","); + fprintf(out, "\"pid\":1,\"tid\":1,"); + fprintf(out, "\"ts\":%llu", static_cast(evt.timestamp)); + fprintf(out, ",\"args\":{\"file\":\"%s\",\"line\":%d}}", evt.file.c_str(), evt.line); + } - if (p) - printf(", %s %.2f%%", luaC_statename(int(i)), double(p) / double(totalgc) * 100); - } + fprintf(out, "\n]}\n"); + fclose(out); - printf("\n"); - } + reporter.reportOutput("Profile written to " + std::string(path) + " (" + std::to_string(gProfiler.events.size()) + " events)"); } diff --git a/lute/common/include/lute/fileutils.h b/lute/common/include/lute/fileutils.h index 45c411fa5..261a1661f 100644 --- a/lute/common/include/lute/fileutils.h +++ b/lute/common/include/lute/fileutils.h @@ -1,7 +1,6 @@ // This file is part of the Lute programming language and is licensed under MIT License #pragma once -#include #include namespace Lute @@ -25,7 +24,10 @@ bool removeFile(const std::string& path); // Remove a directory (must be empty) bool removeDirectory(const std::string& path); -// Check if a path is a directory + bool isDirectory(const std::string& path); +// Get the name of a file without its extension +std::string getFilenameWithoutExtension(const std::string& path); + } // namespace Lute diff --git a/lute/common/src/fileutils.cpp b/lute/common/src/fileutils.cpp index 25bc3536b..1ca5b4422 100644 --- a/lute/common/src/fileutils.cpp +++ b/lute/common/src/fileutils.cpp @@ -164,4 +164,18 @@ bool isDirectory(const std::string& path) #endif } +std::string getFilenameWithoutExtension(const std::string& path) +{ + std::string base = path; + size_t lastSlash = base.find_last_of("/\\"); + if (lastSlash != std::string::npos) + base = base.substr(lastSlash + 1); + // Once we've stripped off the last '/' in the path, we need to trim the last '.' to get the filename. + // If a filename ends in .foo.bar, only the .bar will be stripped. + size_t lastDot = base.rfind('.'); + if (lastDot != std::string::npos) + base = base.substr(0, lastDot); + return base; +} + } // namespace Lute diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 6768d0fed..37514e425 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -1,5 +1,6 @@ local fs = require("@std/fs") local path = require("@std/path") +local strext = require("@std/stringext") local test = require("@std/test") local asserts = require("@std/test/assert") local parser = require("@std/syntax/parser") @@ -127,6 +128,9 @@ test.suite("Parser", function(suite) if entry.type ~= "file" then continue end + if strext.hassuffix(entry.name, ".json") then + continue + end suite:case(`roundtrippable_Ast_{entry.name}`, function(assert) local p = path.join(directory, entry.name) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 2e5436537..89c75f12a 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -82,6 +82,22 @@ lute/cli/commands/lint/printer.luau:58:8-52 lute/cli/commands/lint/rules/parenthesized_conditions.luau:22:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:36:6-17 lute/cli/commands/lint/rules/parenthesized_conditions.luau:61:4-15 +lute/cli/commands/pkg/src/extern/pp.luau:100:2-11 +lute/cli/commands/pkg/src/extern/pp.luau:100:2-11 +lute/cli/commands/pkg/src/extern/unzip/crc.luau:25:42-59 +lute/cli/commands/pkg/src/extern/unzip/crc.luau:25:42-59 +lute/cli/commands/pkg/src/extern/unzip/init.luau:44:46-100 +lute/cli/commands/pkg/src/extern/unzip/init.luau:44:46-100 +lute/cli/commands/pkg/src/extern/unzip/init.luau:660:2-11 +lute/cli/commands/pkg/src/extern/unzip/init.luau:660:2-11 +lute/cli/commands/pkg/src/extern/unzip/init.luau:664:10-24 +lute/cli/commands/pkg/src/extern/unzip/init.luau:664:10-24 +lute/cli/commands/pkg/src/extern/unzip/init.luau:832:8-21 +lute/cli/commands/pkg/src/extern/unzip/init.luau:832:8-21 +lute/cli/commands/pkg/src/extern/unzip/init.luau:923:79-94 +lute/cli/commands/pkg/src/extern/unzip/init.luau:923:79-94 +lute/cli/commands/pkg/src/extern/unzip/utils/path.luau:47:3-38 +lute/cli/commands/pkg/src/extern/unzip/utils/path.luau:47:3-38 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:62:6-17 @@ -295,9 +311,9 @@ tests/std/json.test.luau:81:16-23 tests/std/json.test.luau:81:16-23 tests/std/json.test.luau:81:17-23 tests/std/json.test.luau:81:17-23 +tests/std/json.test.luau:82:21-30 tests/std/json.test.luau:82:21-27 tests/std/json.test.luau:82:21-27 -tests/std/json.test.luau:82:21-30 tests/std/process.test.luau:39:18-18 tests/std/process.test.luau:46:18-18 tests/std/process.test.luau:55:17-18 @@ -314,15 +330,15 @@ tests/std/process.test.luau:123:41-42 tests/std/process.test.luau:126:43-44 tests/std/process.test.luau:130:41-53 tests/std/process.test.luau:134:33-48 -tests/std/syntax/parser.test.luau:11:10-24 -tests/std/syntax/parser.test.luau:12:10-15 -tests/std/syntax/parser.test.luau:36:24-29 -tests/std/syntax/parser.test.luau:50:24-29 -tests/std/syntax/parser.test.luau:64:24-29 -tests/std/syntax/parser.test.luau:67:24-29 -tests/std/syntax/parser.test.luau:81:24-29 -tests/std/syntax/parser.test.luau:84:24-29 -tests/std/syntax/parser.test.luau:531:48-62 +tests/std/syntax/parser.test.luau:12:10-24 +tests/std/syntax/parser.test.luau:13:10-15 +tests/std/syntax/parser.test.luau:37:24-29 +tests/std/syntax/parser.test.luau:51:24-29 +tests/std/syntax/parser.test.luau:65:24-29 +tests/std/syntax/parser.test.luau:68:24-29 +tests/std/syntax/parser.test.luau:82:24-29 +tests/std/syntax/parser.test.luau:85:24-29 +tests/std/syntax/parser.test.luau:535:48-62 tests/std/syntax/printer.test.luau:9:16-29 tests/std/syntax/printer.test.luau:23:10-26 tests/std/syntax/printer.test.luau:45:8-13 From 79f6dfe03573d89211f814742b4a1b3b759ac6cd Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 27 Jan 2026 14:11:51 -0800 Subject: [PATCH 303/642] Adds dev readme to docs (#759) Adds README describing how to run docsite locally for testing/editing documentation --- docs/.vitepress/config.mts | 2 +- docs/README.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 docs/README.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 920b54cf0..4c9cba894 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -28,6 +28,6 @@ export default withSidebar( underscoreToSpace: true, sortMenusByFrontmatterOrder: true, frontmatterOrderDefaultValue: 100, - excludeByGlobPattern: ['**/test/**'], + excludeByGlobPattern: ['**/test/**', 'README.md'], } ) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..6b0c5718f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,14 @@ +# Editing Lute Documentation + +Lute's docs are generated from using [VitePress](https://vitepress.dev/). To run the docsite locally, you should have Node.js and NPM installed. Then, use the following to start running it locally: + +1. `cd docs` +2. `npm install` +3. `npx vitepress build` +4. `npm run preview` + +The terminal output should then tell you which `localhost` port the docsite is running at. + +## Regenerating documentation files + +If you want to regenerate the documentation files in `docs/reference`, you will need to run ` doc -o ./ ../definitions ../lute/std/libs` from the `docs` folder using a version of `lute` which supports `lute doc`. From 97692a5da965a7cf4c0fb6e27c91a5dca2214702 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:51:43 +0000 Subject: [PATCH 304/642] Update Lute to 0.1.0-nightly.20260123 (#757) **Lute**: Updated from `0.1.0-nightly.20260116` to `0.1.0-nightly.20260123` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260123 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 0e92d6802..ea214a2df 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260116" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260123" } diff --git a/rokit.toml b/rokit.toml index d8cb0de6a..9be996fee 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20260116" +lute = "luau-lang/lute@0.1.0-nightly.20260123" From d20c8ec5ebd51bd2aa59b3c0b0a441ee1507887c Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 27 Jan 2026 17:23:19 -0800 Subject: [PATCH 305/642] Makes the tools/check.luau script deterministic (#763) --- tools/check-faillist.txt | 2 +- tools/check.luau | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 89c75f12a..d4fa5b14c 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -311,9 +311,9 @@ tests/std/json.test.luau:81:16-23 tests/std/json.test.luau:81:16-23 tests/std/json.test.luau:81:17-23 tests/std/json.test.luau:81:17-23 -tests/std/json.test.luau:82:21-30 tests/std/json.test.luau:82:21-27 tests/std/json.test.luau:82:21-27 +tests/std/json.test.luau:82:21-30 tests/std/process.test.luau:39:18-18 tests/std/process.test.luau:46:18-18 tests/std/process.test.luau:55:17-18 diff --git a/tools/check.luau b/tools/check.luau index 4c2121259..0816ecb09 100644 --- a/tools/check.luau +++ b/tools/check.luau @@ -118,7 +118,10 @@ local function sortErrors(errors: { ErrorEntry }): { ErrorEntry } if a.line ~= b.line then return a.line < b.line end - return a.col < b.col + if a.col ~= b.col then + return a.col < b.col + end + return a.colEnd < b.colEnd end) return errors end From f8383b29dcd0f6d1a1b6da6d23a062bb51705b1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:43:49 -0800 Subject: [PATCH 306/642] Update Luau to 0.706 (#756) **Luau**: Updated from `0.705` to `0.706` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.706 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- extern/luau.tune | 4 ++-- tools/check-faillist.txt | 33 +++++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index e70cb35e0..171f248f8 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.705" -revision = "93c83a46d7c6c1b040e58e82ef167340a7db1aef" +branch = "0.706" +revision = "416fc1ae1f8bf017f1f75c11c50b5bf6645f2fd2" diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index d4fa5b14c..6047603e9 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -6,10 +6,14 @@ batteries/difftext/myersdiff.luau:76:8-22 batteries/difftext/printdiff.luau:70:10-77 batteries/difftext/printdiff.luau:72:10-73 batteries/difftext/printdiff.luau:74:10-73 -batteries/pp.luau:100:2-11 +batteries/pp.luau:97:3-14 batteries/toml.luau:27:14-16 batteries/toml.luau:40:14-16 +batteries/toml.luau:45:17-39 batteries/toml.luau:45:36-38 +batteries/toml.luau:60:15-33 +batteries/toml.luau:68:14-32 +batteries/toml.luau:75:2-29 batteries/toml.luau:75:26-28 batteries/toml.luau:133:33-41 batteries/toml.luau:143:23-25 @@ -28,7 +32,6 @@ examples/lints/almost_swapped.luau:83:5-16 examples/lints/almost_swapped.luau:104:9-12 examples/lints/divide_by_zero.luau:38:9-12 examples/net_example.luau:8:2-48 -examples/net_example.luau:8:2-48 examples/net_example.luau:13:33-43 examples/net_example.luau:15:24-34 examples/net_example.luau:16:24-34 @@ -82,8 +85,8 @@ lute/cli/commands/lint/printer.luau:58:8-52 lute/cli/commands/lint/rules/parenthesized_conditions.luau:22:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:36:6-17 lute/cli/commands/lint/rules/parenthesized_conditions.luau:61:4-15 -lute/cli/commands/pkg/src/extern/pp.luau:100:2-11 -lute/cli/commands/pkg/src/extern/pp.luau:100:2-11 +lute/cli/commands/pkg/src/extern/pp.luau:97:3-14 +lute/cli/commands/pkg/src/extern/pp.luau:97:3-14 lute/cli/commands/pkg/src/extern/unzip/crc.luau:25:42-59 lute/cli/commands/pkg/src/extern/unzip/crc.luau:25:42-59 lute/cli/commands/pkg/src/extern/unzip/init.luau:44:46-100 @@ -134,14 +137,26 @@ lute/std/libs/syntax/printer.luau:42:25-29 lute/std/libs/syntax/printer.luau:42:25-29 lute/std/libs/syntax/printer.luau:42:50-54 lute/std/libs/syntax/printer.luau:42:50-54 +lute/std/libs/syntax/printer.luau:111:3-15 +lute/std/libs/syntax/printer.luau:111:3-15 +lute/std/libs/syntax/printer.luau:155:3-26 +lute/std/libs/syntax/printer.luau:155:3-26 lute/std/libs/syntax/printer.luau:203:2-19 lute/std/libs/syntax/printer.luau:203:2-19 +lute/std/libs/syntax/query.luau:41:25-25 +lute/std/libs/syntax/query.luau:41:25-25 +lute/std/libs/syntax/query.luau:41:25-25 +lute/std/libs/syntax/query.luau:41:25-25 lute/std/libs/syntax/query.luau:83:29-100 lute/std/libs/syntax/query.luau:83:29-100 lute/std/libs/syntax/query.luau:136:24-31 lute/std/libs/syntax/query.luau:136:24-31 lute/std/libs/syntax/query.luau:136:45-52 lute/std/libs/syntax/query.luau:136:45-52 +lute/std/libs/syntax/query.luau:136:59-61 +lute/std/libs/syntax/query.luau:136:59-61 +lute/std/libs/syntax/query.luau:136:59-61 +lute/std/libs/syntax/query.luau:136:59-61 lute/std/libs/syntax/query.luau:143:9-20 lute/std/libs/syntax/query.luau:143:9-20 lute/std/libs/syntax/query.luau:145:13-28 @@ -158,10 +173,14 @@ lute/std/libs/syntax/utils/init.luau:234:27-27 lute/std/libs/syntax/utils/init.luau:234:27-27 lute/std/libs/syntax/utils/init.luau:238:12-20 lute/std/libs/syntax/utils/init.luau:238:12-20 +lute/std/libs/syntax/utils/trivia.luau:7:38-58 +lute/std/libs/syntax/utils/trivia.luau:7:38-58 lute/std/libs/syntax/utils/trivia.luau:7:38-100 lute/std/libs/syntax/utils/trivia.luau:7:38-100 lute/std/libs/syntax/utils/trivia.luau:8:13-21 lute/std/libs/syntax/utils/trivia.luau:8:13-21 +lute/std/libs/syntax/utils/trivia.luau:30:38-58 +lute/std/libs/syntax/utils/trivia.luau:30:38-58 lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:31:13-21 @@ -196,6 +215,10 @@ lute/std/libs/syntax/visitor.luau:966:25-29 lute/std/libs/syntax/visitor.luau:966:25-29 lute/std/libs/syntax/visitor.luau:970:21-25 lute/std/libs/syntax/visitor.luau:970:21-25 +lute/std/libs/syntax/visitor.luau:984:21-25 +lute/std/libs/syntax/visitor.luau:984:21-25 +lute/std/libs/syntax/visitor.luau:984:21-25 +lute/std/libs/syntax/visitor.luau:984:21-25 lute/std/libs/syntax/visitor.luau:991:17-21 lute/std/libs/syntax/visitor.luau:991:17-21 lute/std/libs/syntax/visitor.luau:1019:9-20 @@ -506,6 +529,8 @@ tests/std/syntax/printer.test.luau:838:8-13 tests/std/syntax/printer.test.luau:859:8-13 tests/std/syntax/printer.test.luau:887:23-31 tests/std/syntax/printer.test.luau:894:8-13 +tests/std/syntax/query.test.luau:41:7-7 +tests/std/syntax/query.test.luau:41:7-7 tests/std/tableext.test.luau:57:18-18 tests/std/tableext.test.luau:62:18-18 tests/std/tableext.test.luau:63:18-18 From f88aa0483d8cb396d855fd6d58e8f2693ff3a790 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:57:27 -0800 Subject: [PATCH 307/642] Loom: Rename `loom` folder to `loom-core` (#762) To help distinguish between "the implementation of Loom that a given runtime uses" and the generic, core implementation of Loom, we rename `loom` to `loom-core`. I've been calling it this already, but I want to align the naming. Example: #761. --- lute/cli/commands/pkg/init.luau | 2 +- .../pkg/{src => loom-core}/extern/pp.luau | 0 .../extern/unzip/LICENSE.md | 0 .../{src => loom-core}/extern/unzip/crc.luau | 0 .../extern/unzip/inflate.luau | 0 .../{src => loom-core}/extern/unzip/init.luau | 0 .../extern/unzip/utils/path.luau | 0 .../extern/unzip/utils/validate_crc.luau | 0 .../loom/loom.luau => loom-core/init.luau} | 6 ++-- .../src}/commands/install.luau | 0 .../src}/lockfile/lockfile.luau | 0 .../loom => loom-core/src}/loomtypes.luau | 0 .../src}/manifest/projectmanifest.luau | 0 .../src}/packagesource/git.luau | 0 .../src}/resolver/package.luau | 0 .../src}/resolver/resolution.luau | 0 .../loom => loom-core/src}/utils/fsutils.luau | 0 tools/check-faillist.txt | 32 +++++++++---------- 18 files changed, 20 insertions(+), 20 deletions(-) rename lute/cli/commands/pkg/{src => loom-core}/extern/pp.luau (100%) rename lute/cli/commands/pkg/{src => loom-core}/extern/unzip/LICENSE.md (100%) rename lute/cli/commands/pkg/{src => loom-core}/extern/unzip/crc.luau (100%) rename lute/cli/commands/pkg/{src => loom-core}/extern/unzip/inflate.luau (100%) rename lute/cli/commands/pkg/{src => loom-core}/extern/unzip/init.luau (100%) rename lute/cli/commands/pkg/{src => loom-core}/extern/unzip/utils/path.luau (100%) rename lute/cli/commands/pkg/{src => loom-core}/extern/unzip/utils/validate_crc.luau (100%) rename lute/cli/commands/pkg/{src/loom/loom.luau => loom-core/init.luau} (53%) rename lute/cli/commands/pkg/{src/loom => loom-core/src}/commands/install.luau (100%) rename lute/cli/commands/pkg/{src/loom => loom-core/src}/lockfile/lockfile.luau (100%) rename lute/cli/commands/pkg/{src/loom => loom-core/src}/loomtypes.luau (100%) rename lute/cli/commands/pkg/{src/loom => loom-core/src}/manifest/projectmanifest.luau (100%) rename lute/cli/commands/pkg/{src/loom => loom-core/src}/packagesource/git.luau (100%) rename lute/cli/commands/pkg/{src/loom => loom-core/src}/resolver/package.luau (100%) rename lute/cli/commands/pkg/{src/loom => loom-core/src}/resolver/resolution.luau (100%) rename lute/cli/commands/pkg/{src/loom => loom-core/src}/utils/fsutils.luau (100%) diff --git a/lute/cli/commands/pkg/init.luau b/lute/cli/commands/pkg/init.luau index 8177e82da..978494139 100644 --- a/lute/cli/commands/pkg/init.luau +++ b/lute/cli/commands/pkg/init.luau @@ -1,6 +1,6 @@ local args = { ... } -local loom = require("@self/src/loom/loom") +local loom = require("@self/loom-core") if args[1] == "install" then loom:install() diff --git a/lute/cli/commands/pkg/src/extern/pp.luau b/lute/cli/commands/pkg/loom-core/extern/pp.luau similarity index 100% rename from lute/cli/commands/pkg/src/extern/pp.luau rename to lute/cli/commands/pkg/loom-core/extern/pp.luau diff --git a/lute/cli/commands/pkg/src/extern/unzip/LICENSE.md b/lute/cli/commands/pkg/loom-core/extern/unzip/LICENSE.md similarity index 100% rename from lute/cli/commands/pkg/src/extern/unzip/LICENSE.md rename to lute/cli/commands/pkg/loom-core/extern/unzip/LICENSE.md diff --git a/lute/cli/commands/pkg/src/extern/unzip/crc.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau similarity index 100% rename from lute/cli/commands/pkg/src/extern/unzip/crc.luau rename to lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau diff --git a/lute/cli/commands/pkg/src/extern/unzip/inflate.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/inflate.luau similarity index 100% rename from lute/cli/commands/pkg/src/extern/unzip/inflate.luau rename to lute/cli/commands/pkg/loom-core/extern/unzip/inflate.luau diff --git a/lute/cli/commands/pkg/src/extern/unzip/init.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/init.luau similarity index 100% rename from lute/cli/commands/pkg/src/extern/unzip/init.luau rename to lute/cli/commands/pkg/loom-core/extern/unzip/init.luau diff --git a/lute/cli/commands/pkg/src/extern/unzip/utils/path.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau similarity index 100% rename from lute/cli/commands/pkg/src/extern/unzip/utils/path.luau rename to lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau diff --git a/lute/cli/commands/pkg/src/extern/unzip/utils/validate_crc.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/utils/validate_crc.luau similarity index 100% rename from lute/cli/commands/pkg/src/extern/unzip/utils/validate_crc.luau rename to lute/cli/commands/pkg/loom-core/extern/unzip/utils/validate_crc.luau diff --git a/lute/cli/commands/pkg/src/loom/loom.luau b/lute/cli/commands/pkg/loom-core/init.luau similarity index 53% rename from lute/cli/commands/pkg/src/loom/loom.luau rename to lute/cli/commands/pkg/loom-core/init.luau index 9432f55e8..d56e560cf 100644 --- a/lute/cli/commands/pkg/src/loom/loom.luau +++ b/lute/cli/commands/pkg/loom-core/init.luau @@ -1,6 +1,6 @@ -local git = require("./packagesource/git") -local types = require("./loomtypes") -local install = require("./commands/install") +local git = require("@self/src/packagesource/git") +local types = require("@self/src/loomtypes") +local install = require("@self/src/commands/install") local loom = {} :: types.Loom diff --git a/lute/cli/commands/pkg/src/loom/commands/install.luau b/lute/cli/commands/pkg/loom-core/src/commands/install.luau similarity index 100% rename from lute/cli/commands/pkg/src/loom/commands/install.luau rename to lute/cli/commands/pkg/loom-core/src/commands/install.luau diff --git a/lute/cli/commands/pkg/src/loom/lockfile/lockfile.luau b/lute/cli/commands/pkg/loom-core/src/lockfile/lockfile.luau similarity index 100% rename from lute/cli/commands/pkg/src/loom/lockfile/lockfile.luau rename to lute/cli/commands/pkg/loom-core/src/lockfile/lockfile.luau diff --git a/lute/cli/commands/pkg/src/loom/loomtypes.luau b/lute/cli/commands/pkg/loom-core/src/loomtypes.luau similarity index 100% rename from lute/cli/commands/pkg/src/loom/loomtypes.luau rename to lute/cli/commands/pkg/loom-core/src/loomtypes.luau diff --git a/lute/cli/commands/pkg/src/loom/manifest/projectmanifest.luau b/lute/cli/commands/pkg/loom-core/src/manifest/projectmanifest.luau similarity index 100% rename from lute/cli/commands/pkg/src/loom/manifest/projectmanifest.luau rename to lute/cli/commands/pkg/loom-core/src/manifest/projectmanifest.luau diff --git a/lute/cli/commands/pkg/src/loom/packagesource/git.luau b/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau similarity index 100% rename from lute/cli/commands/pkg/src/loom/packagesource/git.luau rename to lute/cli/commands/pkg/loom-core/src/packagesource/git.luau diff --git a/lute/cli/commands/pkg/src/loom/resolver/package.luau b/lute/cli/commands/pkg/loom-core/src/resolver/package.luau similarity index 100% rename from lute/cli/commands/pkg/src/loom/resolver/package.luau rename to lute/cli/commands/pkg/loom-core/src/resolver/package.luau diff --git a/lute/cli/commands/pkg/src/loom/resolver/resolution.luau b/lute/cli/commands/pkg/loom-core/src/resolver/resolution.luau similarity index 100% rename from lute/cli/commands/pkg/src/loom/resolver/resolution.luau rename to lute/cli/commands/pkg/loom-core/src/resolver/resolution.luau diff --git a/lute/cli/commands/pkg/src/loom/utils/fsutils.luau b/lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau similarity index 100% rename from lute/cli/commands/pkg/src/loom/utils/fsutils.luau rename to lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 6047603e9..681509677 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -85,22 +85,22 @@ lute/cli/commands/lint/printer.luau:58:8-52 lute/cli/commands/lint/rules/parenthesized_conditions.luau:22:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:36:6-17 lute/cli/commands/lint/rules/parenthesized_conditions.luau:61:4-15 -lute/cli/commands/pkg/src/extern/pp.luau:97:3-14 -lute/cli/commands/pkg/src/extern/pp.luau:97:3-14 -lute/cli/commands/pkg/src/extern/unzip/crc.luau:25:42-59 -lute/cli/commands/pkg/src/extern/unzip/crc.luau:25:42-59 -lute/cli/commands/pkg/src/extern/unzip/init.luau:44:46-100 -lute/cli/commands/pkg/src/extern/unzip/init.luau:44:46-100 -lute/cli/commands/pkg/src/extern/unzip/init.luau:660:2-11 -lute/cli/commands/pkg/src/extern/unzip/init.luau:660:2-11 -lute/cli/commands/pkg/src/extern/unzip/init.luau:664:10-24 -lute/cli/commands/pkg/src/extern/unzip/init.luau:664:10-24 -lute/cli/commands/pkg/src/extern/unzip/init.luau:832:8-21 -lute/cli/commands/pkg/src/extern/unzip/init.luau:832:8-21 -lute/cli/commands/pkg/src/extern/unzip/init.luau:923:79-94 -lute/cli/commands/pkg/src/extern/unzip/init.luau:923:79-94 -lute/cli/commands/pkg/src/extern/unzip/utils/path.luau:47:3-38 -lute/cli/commands/pkg/src/extern/unzip/utils/path.luau:47:3-38 +lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 +lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 +lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau:25:42-59 +lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau:25:42-59 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:44:46-100 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:44:46-100 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:660:2-11 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:660:2-11 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:664:10-24 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:664:10-24 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:832:8-21 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:832:8-21 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:923:79-94 +lute/cli/commands/pkg/loom-core/extern/unzip/init.luau:923:79-94 +lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau:47:3-38 +lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau:47:3-38 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:62:6-17 From 3afcabebccfd6bc1e572dd891807205c1340baac Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 28 Jan 2026 10:44:04 -0800 Subject: [PATCH 308/642] Adds documentation for default lint rules (#760) I had to change a side bar setting so that underscores are preserved in the page titles (and also specify titles in the front matter of some rules because underscores were being deleted for some reason). --- docs/.gitignore | 1 + docs/.vitepress/config.mts | 1 - docs/cli/index.md | 2 +- docs/cli/lint/almost_swapped.md | 25 +++++++++++++++ docs/cli/lint/constant_table_comparison.md | 36 ++++++++++++++++++++++ docs/cli/lint/divide_by_zero.md | 31 +++++++++++++++++++ docs/cli/{lint.md => lint/index.md} | 3 +- docs/cli/lint/parenthesized_conditions.md | 25 +++++++++++++++ lute/cli/commands/lint/rules/README.md | 4 ++- 9 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 docs/cli/lint/almost_swapped.md create mode 100644 docs/cli/lint/constant_table_comparison.md create mode 100644 docs/cli/lint/divide_by_zero.md rename docs/cli/{lint.md => lint/index.md} (84%) create mode 100644 docs/cli/lint/parenthesized_conditions.md diff --git a/docs/.gitignore b/docs/.gitignore index f035fb0c1..b15a7d5d6 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -2,3 +2,4 @@ node_modules .vitepress/cache dist reference +docs/ \ No newline at end of file diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 4c9cba894..ece88b805 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -25,7 +25,6 @@ export default withSidebar( useTitleFromFileHeading: true, useTitleFromFrontmatter: true, hyphenToSpace: true, - underscoreToSpace: true, sortMenusByFrontmatterOrder: true, frontmatterOrderDefaultValue: 100, excludeByGlobPattern: ['**/test/**', 'README.md'], diff --git a/docs/cli/index.md b/docs/cli/index.md index f7fdd60c9..55e6597e9 100755 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -20,7 +20,7 @@ If no command is specified, `run` is used by default. | ------- | ----------- | | [check](./check) | Type check Luau files. | | [compile](./compile) | Compile a Luau script into a standalone executable. | -| [lint](./lint) | Lint the specified Luau file using the specified lint rule(s) or using the default rules. | +| [lint](./lint/index.md) | Lint the specified Luau file using the specified lint rule(s) or using the default rules. | | [run](./run) | Run a Luau script. | | [setup](./setup) | Generate type definition files for the language server. | | [test](./test) | Run tests discovered in .test.luau and .spec.luau files. | diff --git a/docs/cli/lint/almost_swapped.md b/docs/cli/lint/almost_swapped.md new file mode 100644 index 000000000..560727583 --- /dev/null +++ b/docs/cli/lint/almost_swapped.md @@ -0,0 +1,25 @@ +# almost_swapped + +This lint rule checks for instances of attempted swaps (i.e. `a = b; b = a`). + +## Why this is discouraged + +This will never result in the expected behavior. +In the example above, the value of `b` will be assigned to `a`. +Then, this same value gets assigned back to `b` (a no-op), resulting in both identifiers being bound the original value of `b`. +You should instead use Luau's multiple assigment syntax, which will swap as expected (i.e. `a, b = b, a`). + +## Example violations + +`almost_swapped` will warn on the following: + +```luau +a = b +b = a +``` + +You should instead do: + +```luau +a, b = b, a +``` diff --git a/docs/cli/lint/constant_table_comparison.md b/docs/cli/lint/constant_table_comparison.md new file mode 100644 index 000000000..29725dae2 --- /dev/null +++ b/docs/cli/lint/constant_table_comparison.md @@ -0,0 +1,36 @@ +--- +title: constant_table_comparison +--- +# constant_table_comparison + +This lint rule checks for instances of attempting to compare to table literals. + +## Why this is discouraged + +This will never result in the expected behavior. +In Luau, tables are stored and compared by *reference*, rather than by value. +This means that when you compare two variables containing tables, Luau will compare whether the two variables point to the same table in memory, rather than recursively comparing their keys and values. +When you compare to a table literal (i.e. `if x == { a = 3 } then ... end`), Luau will create a new table with a single entry (`a = 3`) in memory and check whether `x` references that table. +Since the table is newly created (and there wasn't even the chance to reassign `x` to that table) this comparison will always evaluate to `false`. + +## Example violations + +`constant_table_comparison` will warn on the following: + +```luau +if x == {} then + ... +elseif x ~= {} then + ... +end +``` + +You should instead do: + +```luau +if next(x) == nil then + ... +elseif next(x) ~= nil then + ... +end +``` diff --git a/docs/cli/lint/divide_by_zero.md b/docs/cli/lint/divide_by_zero.md new file mode 100644 index 000000000..1613e49ce --- /dev/null +++ b/docs/cli/lint/divide_by_zero.md @@ -0,0 +1,31 @@ +--- +title: divide_by_zero +--- +# divide_by_zero + +This lint rule checks for instances of divison, floor division, or taking the remainder with respect to the literal value 0. + +## Why this is discouraged + +Division and floor division by 0 will generally give `inf` or `-inf` (unless the dividend is 0). +The direct use of `math.huge` or `-math.huge` instead is encouraged. +Taking remainder with respect to 0 (i.e. `3 % 0`) will give `NaN`. +We instead encourage the use of `0 / 0` until such time as a `NaN` constant is added to `math`. + +## Example violations + +`divide_by_zero` will warn on the following: + +```luau +local x = 3 / 0 +local y = -4 // 0 +local z = 24 % 0 +``` + +You should instead do: + +```luau +local x = math.huge +local y = -math.huge +local z = 0 / 0 +``` diff --git a/docs/cli/lint.md b/docs/cli/lint/index.md similarity index 84% rename from docs/cli/lint.md rename to docs/cli/lint/index.md index 154ef8e15..41e0fc472 100755 --- a/docs/cli/lint.md +++ b/docs/cli/lint/index.md @@ -4,7 +4,8 @@ As a linter, it works to statically analyze the user's code to warn them about common pitfalls they may be falling into, or to nudge them away from discouraged coding practices. It is _programmable_ meaning that you can write a new lint rule for your Luau code _in Luau_. It's also built on top of the official Luau language stack, allowing it to leverage the same parser used by Luau and Roblox, unlike third-party linters that rely on separate, custom parser implementations. -The examples folder contains two instances of sample lint rules, and you can find the full suite of `lute lint`'s default rules [here](https://github.com/luau-lang/lute/tree/primary/lute/cli/commands/lint/rules). +The examples folder contains two instances of sample lint rules. +You can find the full suite of `lute lint`'s default rules documented as sub-pages of this page and their source code [here](https://github.com/luau-lang/lute/tree/primary/lute/cli/commands/lint/rules). ## Usage diff --git a/docs/cli/lint/parenthesized_conditions.md b/docs/cli/lint/parenthesized_conditions.md new file mode 100644 index 000000000..7dd0ceeff --- /dev/null +++ b/docs/cli/lint/parenthesized_conditions.md @@ -0,0 +1,25 @@ +# parenthesized_conditions + +This lint rule checks for instances where the conditions of syntactic structures like if statements, while loops, and repeat loops have parenthesized conditions. + +## Why this is discouraged + +Although there is no semantic difference, the parentheses are unidiomatic and may detract from readability. + +## Example violations + +`parenthesized_conditions` will warn on the following: + +```luau +if (x > 5) then + ... +end +``` + +You should instead do: + +```luau +if x > 5 then + ... +end +``` diff --git a/lute/cli/commands/lint/rules/README.md b/lute/cli/commands/lint/rules/README.md index cd3b3a505..c45432202 100644 --- a/lute/cli/commands/lint/rules/README.md +++ b/lute/cli/commands/lint/rules/README.md @@ -1 +1,3 @@ -To add a new default lint rule to `lute lint`, create a module or luau file defining the rule in this folder. Then, add the name of the rule to the DEFAULT_RULES constant in `lint/init.luau`, and add a test for the rule in `tests/cli/lint.test.luau`. The expected type of a lint rule is specified in `lint/types.luau`. \ No newline at end of file +To add a new default lint rule to `lute lint`, create a module or luau file defining the rule in this folder. +Then, add the name of the rule to the DEFAULT_RULES constant in `lint/init.luau`, add a test for the rule in `tests/cli/lint.test.luau`, and add documentation for it in `docs/cli/lint`. +The expected type of a lint rule is specified in `lint/types.luau`. \ No newline at end of file From 046e9544cc607877d2ab96fdcb58cd04f5c779c9 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:44:33 -0800 Subject: [PATCH 309/642] Fix bug preventing `lute pkg run` from detecting files with `require-by-string` semantics (#764) In this code, we created `validPath` from `filePath` using require-by-string semantics, but we forgot to pass `validPath` along when converting it into an absolute path. This manifests when doing `lute pkg run ./run/this/script`, where we're trying to run `./run/this/script.luau`. --- lute/cli/src/climain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 953622953..e0de94957 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -403,7 +403,7 @@ int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness reporter.reportError("Error: Failed to get current working directory.\n"); return 1; } - validPath = normalizePath(joinPaths(*cwd, filePath)); + validPath = normalizePath(joinPaths(*cwd, validPath)); } std::optional lockfile = getAbsolutePathToNearestLockfile(validPath); From 889c9546be65a622061ec5981780b9b13279def9 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 28 Jan 2026 16:15:51 -0800 Subject: [PATCH 310/642] Removes the `AstFunctionBody` type (#765) This PR removes the AstFunctionBody AST type, which has been bugging me for a while. It was making writing code which manipulates function AST nodes unwieldy, since you had to know that a lot of the function metadata lived in the `body` field of `AstExprFunc`, rather than on the expr node itself. As I was making this change, I realized that it existed because a lot of AST machinery (namely serialization and visiting) needs to happen in program-lexicographical order but I was able to work around this requirement. --- definitions/luau.luau | 34 +++----- lute/luau/src/luau.cpp | 106 +++++++++++++---------- lute/std/libs/syntax/init.luau | 4 +- lute/std/libs/syntax/types.luau | 4 +- lute/std/libs/syntax/utils/init.luau | 2 +- lute/std/libs/syntax/visitor.luau | 112 ++++++++++++------------ tests/std/syntax/parser.test.luau | 124 +++++++++++++-------------- tests/std/syntax/printer.test.luau | 4 +- tools/check-faillist.txt | 73 ++++++++-------- 9 files changed, 234 insertions(+), 229 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 3b605073b..fd81a15b9 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -129,14 +129,18 @@ export type AstExprIndexExpr = { closebrackets: Token<"]">, } --- I don't like that we have this type; a lot of its data should live in the parents, which should then have `AstStatBlock` as its body -export type AstFunctionBody = { +export type AstExprFunction = { + location: span, + kind: "expr", + tag: "function", + attributes: { AstAttribute }, + functionkeyword: Token<"function">, opengenerics: Token<"<">?, generics: Punctuated?, genericpacks: Punctuated?, closegenerics: Token<">">?, - self: AstLocal?, openparens: Token<"(">, + self: AstLocal?, parameters: Punctuated, vararg: Token<"...">?, varargcolon: Token<":">?, @@ -148,15 +152,6 @@ export type AstFunctionBody = { endkeyword: Token<"end">, } -export type AstExprAnonymousFunction = { - location: span, - kind: "expr", - tag: "function", - attributes: { AstAttribute }, - functionkeyword: Token<"function">, - body: AstFunctionBody, -} - -- helper types for items contained in an AstExprTable, not actually AstExprs themselves export type AstExprTableItemList = { location: span, @@ -266,7 +261,7 @@ export type AstExpr = { kind: "expr" } & ( | AstExprCall | AstExprIndexName | AstExprIndexExpr - | AstExprAnonymousFunction + | AstExprFunction | AstExprTable | AstExprUnary | AstExprBinary @@ -411,21 +406,17 @@ export type AstStatFunction = { location: span, kind: "stat", tag: "function", - attributes: { AstAttribute }, - functionkeyword: Token<"function">, name: AstExpr, - body: AstFunctionBody, + func: AstExprFunction, } export type AstStatLocalFunction = { location: span, kind: "stat", tag: "localfunction", - attributes: { AstAttribute }, localkeyword: Token<"local">, - functionkeyword: Token<"function">, name: AstLocal, - body: AstFunctionBody, + func: AstExprFunction, } export type AstStatTypeAlias = { @@ -443,15 +434,15 @@ export type AstStatTypeAlias = { type: AstType, } +-- A type function export type AstStatTypeFunction = { location: span, kind: "stat", tag: "typefunction", export: Token<"export">?, type: Token<"type">, - functionkeyword: Token<"function">, name: Token, - body: AstFunctionBody, + body: AstExprFunction, } export type AstStat = { kind: "stat" } & ( @@ -610,6 +601,7 @@ export type AstTypeFunctionParameter = { type: AstType, } +-- A type representing a Luau function export type AstTypeFunction = { location: span, kind: "type", diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 2d86f5068..8fe9557f8 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -550,7 +550,7 @@ struct AstSerialize : public Luau::AstVisitor lua_pushstring(L, Luau::toString(op).data()); } - // preambleSize should encode the size of the fields we're setting up for _all_ nodes. + // preambleSize should encode the size of the fields we're setting up for _all_ nodes. It consists of tag, kind, and location. static const size_t preambleSize = 3; void serializeNodePreamble(Luau::AstNode* node, const char* tag, const char* kind) { @@ -960,12 +960,54 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "closebrackets"); } - void serializeFunctionBody(Luau::AstExprFunction* node) + enum FunctionSerializationState { - const auto cstNode = lookupCstNode(node); + AttributesAndFunctionKeywordSerialized, + FunctionKeywordSerialized, + NothingSerialized + }; + // Serialization needs to happen in program-lexical order, so attributes and function keyword might have been serialized already + void serialize(Luau::AstExprFunction* node, FunctionSerializationState state = NothingSerialized) + { lua_rawcheckstack(L, 3); - lua_createtable(L, 0, 15); + lua_createtable(L, 0, preambleSize + 17); + + serializeNodePreamble(node, "function", "expr"); + + const auto cstNode = lookupCstNode(node); + + switch (state) + { + case AttributesAndFunctionKeywordSerialized: + // Attributes and function keyword are on the stack, so we just need to set them in the function expr table + // This is only called from serialize AstStatFunction or AstStatLocalFunction, so we assume the stack has the following structure: + // -3: attributes + // -2: functionkeyword + // -1: function expr table (created above) + lua_insert(L, -3); + lua_setfield(L, -3, "functionkeyword"); + lua_setfield(L, -2, "attributes"); + break; + case FunctionKeywordSerialized: + // Function keyword is on the stack, so we just need to set it in the function expr table + // This is only called from serialize AstStatTypeFunction, so we assume the stack has the following structure: + // -2: functionkeyword + // -1: function expr table (created above) + lua_insert(L, -2); + lua_setfield(L, -2, "functionkeyword"); + + lua_newtable(L); + lua_setfield(L, -2, "attributes"); + break; + case NothingSerialized: + serializeAttributes(node->attributes); + lua_setfield(L, -2, "attributes"); + + serializeToken(cstNode->functionKeywordPosition, "function"); + lua_setfield(L, -2, "functionkeyword"); + break; + } if (node->generics.size > 0 || node->genericPacks.size > 0) { @@ -1046,25 +1088,6 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "endkeyword"); } - void serialize(Luau::AstExprFunction* node) - { - lua_rawcheckstack(L, 3); - lua_createtable(L, 0, preambleSize + 3); - - serializeNodePreamble(node, "function", "expr"); - - serializeAttributes(node->attributes); - lua_setfield(L, -2, "attributes"); - - const auto cstNode = lookupCstNode(node); - - serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionkeyword"); - - serializeFunctionBody(node); - lua_setfield(L, -2, "body"); - } - void serialize(Luau::AstExprTable* node) { const auto cstNode = lookupCstNode(node); @@ -1598,49 +1621,47 @@ struct AstSerialize : public Luau::AstVisitor void serializeStat(Luau::AstStatFunction* node) { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 4); + lua_rawcheckstack(L, 4); + lua_createtable(L, 0, preambleSize + 2); serializeNodePreamble(node, "function", "stat"); const auto cstNode = lookupCstNode(node); + // Serialization needs to happen in program-lexical order, so we serialize attributes and function keyword first serializeAttributes(node->func->attributes); - lua_setfield(L, -2, "attributes"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionkeyword"); node->name->visit(this); - lua_setfield(L, -2, "name"); + lua_setfield(L, -4, "name"); - serializeFunctionBody(node->func); - lua_setfield(L, -2, "body"); + serialize(node->func, AttributesAndFunctionKeywordSerialized); + lua_setfield(L, -2, "func"); } void serializeStat(Luau::AstStatLocalFunction* node) { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 5); + lua_rawcheckstack(L, 4); + lua_createtable(L, 0, preambleSize + 3); serializeNodePreamble(node, "localfunction", "stat"); + // Serialization needs to happen in program-lexical order, so we serialize attributes and function keyword before the func expr serializeAttributes(node->func->attributes); - lua_setfield(L, -2, "attributes"); const auto cstNode = lookupCstNode(node); serializeToken(cstNode->localKeywordPosition, "local"); - lua_setfield(L, -2, "localkeyword"); + lua_setfield(L, -3, "localkeyword"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionkeyword"); serialize(node->name); - lua_setfield(L, -2, "name"); + lua_setfield(L, -4, "name"); - serializeFunctionBody(node->func); - lua_setfield(L, -2, "body"); + serialize(node->func, AttributesAndFunctionKeywordSerialized); + lua_setfield(L, -2, "func"); } static Luau::AstArray splitArray(Luau::AstArray arr, size_t index) @@ -1696,8 +1717,8 @@ struct AstSerialize : public Luau::AstVisitor void serializeStat(Luau::AstStatTypeFunction* node) { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, preambleSize + 5); + lua_rawcheckstack(L, 3); + lua_createtable(L, 0, preambleSize + 4); const auto cstNode = lookupCstNode(node); @@ -1713,12 +1734,11 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "type"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionkeyword"); serializeToken(node->nameLocation.begin, node->name.value); - lua_setfield(L, -2, "name"); + lua_setfield(L, -3, "name"); - serializeFunctionBody(node->body); + serialize(node->body, FunctionKeywordSerialized); lua_setfield(L, -2, "body"); } diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index 246f127d3..306d6f9fa 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -59,9 +59,7 @@ export type AstExprIndexName = types.AstExprIndexName export type AstExprIndexExpr = types.AstExprIndexExpr -export type AstFunctionBody = types.AstFunctionBody - -export type AstExprAnonymousFunction = types.AstExprAnonymousFunction +export type AstExprFunction = types.AstExprFunction export type AstExprTableItemList = types.AstExprTableItemList diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index bb3be3adc..aded489c4 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -39,9 +39,7 @@ export type AstExprIndexName = luau.AstExprIndexName export type AstExprIndexExpr = luau.AstExprIndexExpr -export type AstFunctionBody = luau.AstFunctionBody - -export type AstExprAnonymousFunction = luau.AstExprAnonymousFunction +export type AstExprFunction = luau.AstExprFunction export type AstExprTableItemList = luau.AstExprTableItemList diff --git a/lute/std/libs/syntax/utils/init.luau b/lute/std/libs/syntax/utils/init.luau index 1518450b1..a43991fd6 100644 --- a/lute/std/libs/syntax/utils/init.luau +++ b/lute/std/libs/syntax/utils/init.luau @@ -50,7 +50,7 @@ function utils.isExprIndexExpr(n: types.AstNode): types.AstExprIndexExpr? return if n.kind == "expr" and n.tag == "index" then n else nil end -function utils.isExprAnonymousFunction(n: types.AstNode): types.AstExprAnonymousFunction? +function utils.isExprFunction(n: types.AstNode): types.AstExprFunction? return if n.kind == "expr" and n.tag == "function" then n else nil end diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index fd90b82d7..c68d14cb8 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -39,7 +39,7 @@ export type Visitor = { visitExprCall: (types.AstExprCall) -> boolean, visitExprUnary: (types.AstExprUnary) -> boolean, visitExprBinary: (types.AstExprBinary) -> boolean, - visitExprAnonymousFunction: (types.AstExprAnonymousFunction) -> boolean, + visitExprFunction: (types.AstExprFunction) -> boolean, visitExprTableItem: (types.AstExprTableItem) -> boolean, visitExprTable: (types.AstExprTable) -> boolean, visitExprIndexName: (types.AstExprIndexName) -> boolean, @@ -110,7 +110,7 @@ local defaultVisitor: Visitor = { visitExprCall = alwaysVisit :: any, visitExprUnary = alwaysVisit :: any, visitExprBinary = alwaysVisit :: any, - visitExprAnonymousFunction = alwaysVisit :: any, + visitExprFunction = alwaysVisit :: any, visitExprTableItem = alwaysVisit :: any, visitExprTable = alwaysVisit :: any, visitExprIndexName = alwaysVisit :: any, @@ -435,77 +435,83 @@ local function visitExprBinary(node: types.AstExprBinary, visitor: Visitor) end end -local function visitStatFunctionBody(node: types.AstFunctionBody, visitor: Visitor) - if node.opengenerics then - visitToken(node.opengenerics, visitor) - end - if node.generics then - visitPunctuated(node.generics, visitor, visitGeneric) - end - if node.genericpacks then - visitPunctuated(node.genericpacks, visitor, visitGenericPack) - end - if node.closegenerics then - visitToken(node.closegenerics, visitor) - end - visitToken(node.openparens, visitor) - visitPunctuated(node.parameters, visitor, visitLocal) - if node.vararg then - visitToken(node.vararg, visitor) - end - if node.varargcolon then - visitToken(node.varargcolon, visitor) - end - if node.varargannotation then - visitTypePack(node.varargannotation, visitor) - end - visitToken(node.closeparens, visitor) - if node.returnspecifier then - visitToken(node.returnspecifier, visitor) - end - if node.returnannotation then - visitTypePack(node.returnannotation, visitor) - end - visitStatBlock(node.body, visitor) - visitToken(node.endkeyword, visitor) -end - local function visitAttribute(node: types.AstAttribute, visitor: Visitor) if visitor.visitAttribute(node) then visitToken(node, visitor) end end -local function visitExprAnonymousFunction(node: types.AstExprAnonymousFunction, visitor: Visitor) - if visitor.visitExprAnonymousFunction(node) then - for _, attribute in node.attributes do - visitAttribute(attribute, visitor) +local function visitExprFunction( + node: types.AstExprFunction, + visitor: Visitor, + shouldVisitAttributes: boolean?, + shouldVisitFunctionKeyword: boolean? +) + if visitor.visitExprFunction(node) then + if shouldVisitAttributes ~= false then + for _, attribute in node.attributes do + visitAttribute(attribute, visitor) + end + end + if shouldVisitFunctionKeyword ~= false then + visitToken(node.functionkeyword, visitor) + end + if node.opengenerics then + visitToken(node.opengenerics, visitor) + end + if node.generics then + visitPunctuated(node.generics, visitor, visitGeneric) + end + if node.genericpacks then + visitPunctuated(node.genericpacks, visitor, visitGenericPack) + end + if node.closegenerics then + visitToken(node.closegenerics, visitor) + end + visitToken(node.openparens, visitor) + visitPunctuated(node.parameters, visitor, visitLocal) + if node.vararg then + visitToken(node.vararg, visitor) + end + if node.varargcolon then + visitToken(node.varargcolon, visitor) end - visitToken(node.functionkeyword, visitor) - visitStatFunctionBody(node.body, visitor) + if node.varargannotation then + visitTypePack(node.varargannotation, visitor) + end + visitToken(node.closeparens, visitor) + if node.returnspecifier then + visitToken(node.returnspecifier, visitor) + end + if node.returnannotation then + visitTypePack(node.returnannotation, visitor) + end + visitStatBlock(node.body, visitor) + visitToken(node.endkeyword, visitor) end end local function visitStatFunction(node: types.AstStatFunction, visitor: Visitor) if visitor.visitStatFunction(node) then - for _, attribute in node.attributes do + -- We need to visit in lexical order + for _, attribute in node.func.attributes do visitAttribute(attribute, visitor) end - visitToken(node.functionkeyword, visitor) + visitToken(node.func.functionkeyword, visitor) visitExpr(node.name, visitor) - visitStatFunctionBody(node.body, visitor) + visitExprFunction(node.func, visitor, false, false) end end local function visitStatLocalFunction(node: types.AstStatLocalFunction, visitor: Visitor) if visitor.visitStatLocalFunction(node) then - for _, attribute in node.attributes do + for _, attribute in node.func.attributes do visitAttribute(attribute, visitor) end visitToken(node.localkeyword, visitor) - visitToken(node.functionkeyword, visitor) + visitToken(node.func.functionkeyword, visitor) visitLocal(node.name, visitor) - visitStatFunctionBody(node.body, visitor) + visitExprFunction(node.func, visitor, false, false) end end @@ -515,9 +521,9 @@ local function visitStatTypeFunction(node: types.AstStatTypeFunction, visitor: V visitToken(node.export, visitor) end visitToken(node.type, visitor) - visitToken(node.functionkeyword, visitor) + visitToken(node.body.functionkeyword, visitor) visitToken(node.name, visitor) - visitStatFunctionBody(node.body, visitor) + visitExprFunction(node.body, visitor, true, false) end end @@ -826,7 +832,7 @@ function visitExpr(expression: types.AstExpr, visitor: Visitor) elseif expression.tag == "binary" then visitExprBinary(expression, visitor) elseif expression.tag == "function" then - visitExprAnonymousFunction(expression, visitor) + visitExprFunction(expression, visitor) elseif expression.tag == "table" then visitExprTable(expression, visitor) elseif expression.tag == "indexname" then @@ -962,7 +968,7 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitExprCall = visit, visitExprUnary = visit, visitExprBinary = visit, - visitExprAnonymousFunction = visit, + visitExprFunction = visit, visitExprTableItem = visit, visitExprTable = visit, visitExprIndexName = visit, diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 37514e425..45179f559 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -2,15 +2,14 @@ local fs = require("@std/fs") local path = require("@std/path") local strext = require("@std/stringext") local test = require("@std/test") -local asserts = require("@std/test/assert") +local testTypes = require("@std/test/types") local parser = require("@std/syntax/parser") local printer = require("@std/syntax/printer") local syntax = require("@std/syntax") -local T = require("@std/luau") local function assertEqualsLocation( - assert: asserts.asserts, - actual: T.span, + assert: testTypes.asserts, + actual: syntax.span, startLine: number, startColumn: number, endLine: number, @@ -184,7 +183,7 @@ test.suite("Parser", function(suite) { attr = "@deprecated", code = code:gsub("checked", "deprecated") }, } - for _, subcase in ipairs(subcases) do + for _, subcase in subcases do local block = parser.parseblock(subcase.code) assert.eq(#block.statements, 1) @@ -192,7 +191,7 @@ test.suite("Parser", function(suite) assert.eq(l.tag, "localfunction") local lf = l :: syntax.AstStatLocalFunction - assert.eq(lf.attributes[1].text, subcase.attr) + assert.eq(lf.func.attributes[1].text, subcase.attr) end end) end) @@ -326,84 +325,81 @@ test.suite("parseExpr", function(suite) assert.eq((indexExpr :: syntax.AstExprIndexExpr).closebrackets.text, "]") end) - suite:case("parseExprAnonymousFunction", function(assert) + suite:case("parseExprFunction", function(assert) local anonFuncExpr = parser.parseexpr("function(a, b) return a + b end") assert.eq(anonFuncExpr.tag, "function") assert.eq(anonFuncExpr.kind, "expr") - local funcExpr = anonFuncExpr :: syntax.AstExprAnonymousFunction + local funcExpr = anonFuncExpr :: syntax.AstExprFunction assert.eq(#funcExpr.attributes, 0) assert.eq(funcExpr.functionkeyword.text, "function") - - local funcBody = funcExpr.body - assert.eq(funcBody.opengenerics, nil) - assert.eq(funcBody.generics, nil) - assert.eq(funcBody.genericpacks, nil) - assert.eq(funcBody.closegenerics, nil) - assert.eq(funcBody.self, nil) - assert.eq(funcBody.openparens.text, "(") - assert.eq(#funcBody.parameters, 2) - assert.eq(funcBody.parameters[1].node.name.text, "a") - assert.eq(funcBody.parameters[2].node.name.text, "b") - assert.eq(funcBody.vararg, nil) - assert.eq(funcBody.varargcolon, nil) - assert.eq(funcBody.varargannotation, nil) - assert.eq(funcBody.closeparens.text, ")") - assert.eq(funcBody.returnspecifier, nil) - assert.eq(funcBody.returnannotation, nil) - assert.eq(funcBody.endkeyword.text, "end") + assert.eq(funcExpr.opengenerics, nil) + assert.eq(funcExpr.generics, nil) + assert.eq(funcExpr.genericpacks, nil) + assert.eq(funcExpr.closegenerics, nil) + assert.eq(funcExpr.self, nil) + assert.eq(funcExpr.openparens.text, "(") + assert.eq(#funcExpr.parameters, 2) + assert.eq(funcExpr.parameters[1].node.name.text, "a") + assert.eq(funcExpr.parameters[2].node.name.text, "b") + assert.eq(funcExpr.vararg, nil) + assert.eq(funcExpr.varargcolon, nil) + assert.eq(funcExpr.varargannotation, nil) + assert.eq(funcExpr.closeparens.text, ")") + assert.eq(funcExpr.returnspecifier, nil) + assert.eq(funcExpr.returnannotation, nil) + assert.eq(funcExpr.endkeyword.text, "end") anonFuncExpr = parser.parseexpr("function(a: A, ...: B...): A return a end") assert.eq(anonFuncExpr.tag, "function") assert.eq(anonFuncExpr.kind, "expr") - funcExpr = anonFuncExpr :: syntax.AstExprAnonymousFunction + funcExpr = anonFuncExpr :: syntax.AstExprFunction assert.eq(#funcExpr.attributes, 0) assert.eq(funcExpr.functionkeyword.text, "function") - funcBody = funcExpr.body - assert.neq(funcBody.opengenerics, nil) - assert.neq(funcBody.generics, nil) + assert.neq(funcExpr.opengenerics, nil) + assert.neq(funcExpr.generics, nil) - assert.eq(#funcBody.generics :: syntax.Punctuated, 1) - local gen = (funcBody.generics :: syntax.Punctuated)[1].node + assert.eq(#funcExpr.generics :: syntax.Punctuated, 1) + local gen = (funcExpr.generics :: syntax.Punctuated)[1].node assert.eq(gen.name.text, "A") assert.eq(gen.equals, nil) assert.eq(gen.default, nil) - assert.neq(funcBody.genericpacks, nil) - assert.eq(#funcBody.genericpacks :: syntax.Punctuated, 1) - local genPack = (funcBody.genericpacks :: syntax.Punctuated)[1].node + assert.neq(funcExpr.genericpacks, nil) + assert.eq(#funcExpr.genericpacks :: syntax.Punctuated, 1) + + local genPack = (funcExpr.genericpacks :: syntax.Punctuated)[1].node assert.eq(genPack.name.text, "B") assert.eq(genPack.ellipsis.text, "...") assert.eq(genPack.equals, nil) assert.eq(genPack.default, nil) - assert.neq(funcBody.closegenerics, nil) - assert.eq((funcBody.closegenerics :: syntax.Token<">">).text, ">") - - assert.eq(funcBody.openparens.text, "(") - assert.eq(#funcBody.parameters, 1) + assert.neq(funcExpr.closegenerics, nil) + assert.eq((funcExpr.closegenerics :: syntax.Token<">">).text, ">") - local param = funcBody.parameters[1].node + assert.eq(funcExpr.openparens.text, "(") + assert.eq(#funcExpr.parameters, 1) + local param = funcExpr.parameters[1].node assert.eq(param.name.text, "a") assert.neq(param.colon, nil) assert.eq((param.colon :: syntax.Token<":">).text, ":") assert.neq(param.annotation, nil) assert.eq((param.annotation :: syntax.AstType).tag, "reference") - assert.neq(funcBody.vararg, nil) - assert.eq((funcBody.vararg :: syntax.Token<"...">).text, "...") - assert.neq(funcBody.varargcolon, nil) - assert.eq((funcBody.varargcolon :: syntax.Token<":">).text, ":") - assert.neq(funcBody.varargannotation, nil) - assert.eq((funcBody.varargannotation :: syntax.AstTypePack).tag, "generic") - - assert.eq(funcBody.closeparens.text, ")") - assert.neq(funcBody.returnspecifier, nil) - assert.eq((funcBody.returnspecifier :: syntax.Token<":">).text, ":") - assert.neq(funcBody.returnannotation, nil) - assert.eq((funcBody.returnannotation :: syntax.AstTypePack).tag, "explicit") + assert.neq(funcExpr.vararg, nil) + assert.eq((funcExpr.vararg :: syntax.Token<"...">).text, "...") + assert.neq(funcExpr.varargcolon, nil) + assert.eq((funcExpr.varargcolon :: syntax.Token<":">).text, ":") + assert.neq(funcExpr.varargannotation, nil) + assert.eq((funcExpr.varargannotation :: syntax.AstTypePack).tag, "generic") + + assert.eq(funcExpr.closeparens.text, ")") + assert.neq(funcExpr.returnspecifier, nil) + assert.eq((funcExpr.returnspecifier :: syntax.Token<":">).text, ":") + assert.neq(funcExpr.returnannotation, nil) + assert.eq((funcExpr.returnannotation :: syntax.AstTypePack).tag, "explicit") end) suite:case("parseExprTable", function(assert) @@ -532,7 +528,7 @@ foo = bar, end) end) -local function checkIsBlock(node: any, assert: asserts.asserts) +local function checkIsBlock(node: any, assert: testTypes.asserts) assert.eq(node.kind, "stat") assert.eq(node.tag, "block") end @@ -803,14 +799,14 @@ test.suite("parse", function(suite) local funcStat = block.statements[1] :: syntax.AstStatFunction assert.eq(funcStat.tag, "function") - assert.eq(#funcStat.attributes, 3) - assert.eq((funcStat.attributes[1] :: syntax.AstAttribute).text, "@checked") - assert.eq((funcStat.attributes[2] :: syntax.AstAttribute).text, "@native") - assert.eq((funcStat.attributes[3] :: syntax.AstAttribute).text, "@deprecated") - assert.eq(funcStat.functionkeyword.text, "function") + assert.eq(#funcStat.func.attributes, 3) + assert.eq((funcStat.func.attributes[1] :: syntax.AstAttribute).text, "@checked") + assert.eq((funcStat.func.attributes[2] :: syntax.AstAttribute).text, "@native") + assert.eq((funcStat.func.attributes[3] :: syntax.AstAttribute).text, "@deprecated") + assert.eq(funcStat.func.functionkeyword.text, "function") assert.eq(funcStat.name.tag, "global") assert.eq((funcStat.name :: syntax.AstExprGlobal).name.text, "add") - -- body parsing tested in parseExprAnonymousFunction + -- body parsing tested in parseExprFunction end) suite:case("parseStatLocalFunction", function(assert) @@ -820,9 +816,9 @@ test.suite("parse", function(suite) local localFuncStat = block.statements[1] :: syntax.AstStatLocalFunction assert.eq(localFuncStat.tag, "localfunction") - assert.eq(#localFuncStat.attributes, 0) + assert.eq(#localFuncStat.func.attributes, 0) assert.eq(localFuncStat.localkeyword.text, "local") - assert.eq(localFuncStat.functionkeyword.text, "function") + assert.eq(localFuncStat.func.functionkeyword.text, "function") assert.eq(localFuncStat.name.name.text, "multiply") end) @@ -869,7 +865,7 @@ test.suite("parse", function(suite) assert.eq(typeFunc.tag, "typefunction") assert.eq(typeFunc.export, nil) assert.eq(typeFunc.type.text, "type") - assert.eq(typeFunc.functionkeyword.text, "function") + assert.eq(typeFunc.body.functionkeyword.text, "function") assert.eq(typeFunc.name.text, "foo") block = parser.parseblock("export type function foo() return types.number end") @@ -883,7 +879,7 @@ test.suite("parse", function(suite) end) test.suite("parse types", function(suite) - -- AstGenericType and AstGenericTypePack are tested in parseExprAnonymousFunction + -- AstGenericType and AstGenericTypePack are tested in parseExprFunction suite:case("testTypeReference", function(assert) local block = parser.parseblock("type MyString = string") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 1a86b7b24..93ac60ebb 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -250,13 +250,13 @@ hello['world'] -- after]] end, assert) end) - suite:case("expranonymousfunction_trivia", function(assert) + suite:case("exprfunction_trivia", function(assert) local source = [[local x = function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprAnonymousFunction):replace(function() + return query.findallfromroot(ast, syntaxUtils.isExprFunction):replace(function() return "replaced" end) end, assert) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 681509677..24ae54d44 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -197,38 +197,42 @@ lute/std/libs/syntax/visitor.luau:388:3-12 lute/std/libs/syntax/visitor.luau:388:3-12 lute/std/libs/syntax/visitor.luau:406:3-12 lute/std/libs/syntax/visitor.luau:406:3-12 -lute/std/libs/syntax/visitor.luau:475:3-12 -lute/std/libs/syntax/visitor.luau:475:3-12 -lute/std/libs/syntax/visitor.luau:650:3-12 -lute/std/libs/syntax/visitor.luau:650:3-12 +lute/std/libs/syntax/visitor.luau:440:3-12 +lute/std/libs/syntax/visitor.luau:440:3-12 lute/std/libs/syntax/visitor.luau:656:3-12 lute/std/libs/syntax/visitor.luau:656:3-12 -lute/std/libs/syntax/visitor.luau:679:3-12 -lute/std/libs/syntax/visitor.luau:679:3-12 -lute/std/libs/syntax/visitor.luau:938:24-28 -lute/std/libs/syntax/visitor.luau:938:24-28 -lute/std/libs/syntax/visitor.luau:947:35-39 -lute/std/libs/syntax/visitor.luau:947:35-39 -lute/std/libs/syntax/visitor.luau:959:19-23 -lute/std/libs/syntax/visitor.luau:959:19-23 -lute/std/libs/syntax/visitor.luau:966:25-29 -lute/std/libs/syntax/visitor.luau:966:25-29 -lute/std/libs/syntax/visitor.luau:970:21-25 -lute/std/libs/syntax/visitor.luau:970:21-25 -lute/std/libs/syntax/visitor.luau:984:21-25 -lute/std/libs/syntax/visitor.luau:984:21-25 -lute/std/libs/syntax/visitor.luau:984:21-25 -lute/std/libs/syntax/visitor.luau:984:21-25 -lute/std/libs/syntax/visitor.luau:991:17-21 -lute/std/libs/syntax/visitor.luau:991:17-21 -lute/std/libs/syntax/visitor.luau:1019:9-20 -lute/std/libs/syntax/visitor.luau:1019:9-20 -lute/std/libs/syntax/visitor.luau:1020:3-12 -lute/std/libs/syntax/visitor.luau:1020:3-12 -lute/std/libs/syntax/visitor.luau:1021:9-24 -lute/std/libs/syntax/visitor.luau:1021:9-24 -lute/std/libs/syntax/visitor.luau:1022:22-25 -lute/std/libs/syntax/visitor.luau:1022:22-25 +lute/std/libs/syntax/visitor.luau:662:3-12 +lute/std/libs/syntax/visitor.luau:662:3-12 +lute/std/libs/syntax/visitor.luau:685:3-12 +lute/std/libs/syntax/visitor.luau:685:3-12 +lute/std/libs/syntax/visitor.luau:944:24-28 +lute/std/libs/syntax/visitor.luau:944:24-28 +lute/std/libs/syntax/visitor.luau:953:35-39 +lute/std/libs/syntax/visitor.luau:953:35-39 +lute/std/libs/syntax/visitor.luau:961:28-32 +lute/std/libs/syntax/visitor.luau:961:28-32 +lute/std/libs/syntax/visitor.luau:961:28-32 +lute/std/libs/syntax/visitor.luau:961:28-32 +lute/std/libs/syntax/visitor.luau:965:19-23 +lute/std/libs/syntax/visitor.luau:965:19-23 +lute/std/libs/syntax/visitor.luau:972:25-29 +lute/std/libs/syntax/visitor.luau:972:25-29 +lute/std/libs/syntax/visitor.luau:976:21-25 +lute/std/libs/syntax/visitor.luau:976:21-25 +lute/std/libs/syntax/visitor.luau:990:21-25 +lute/std/libs/syntax/visitor.luau:990:21-25 +lute/std/libs/syntax/visitor.luau:990:21-25 +lute/std/libs/syntax/visitor.luau:990:21-25 +lute/std/libs/syntax/visitor.luau:997:17-21 +lute/std/libs/syntax/visitor.luau:997:17-21 +lute/std/libs/syntax/visitor.luau:1025:9-20 +lute/std/libs/syntax/visitor.luau:1025:9-20 +lute/std/libs/syntax/visitor.luau:1026:3-12 +lute/std/libs/syntax/visitor.luau:1026:3-12 +lute/std/libs/syntax/visitor.luau:1027:9-24 +lute/std/libs/syntax/visitor.luau:1027:9-24 +lute/std/libs/syntax/visitor.luau:1028:22-25 +lute/std/libs/syntax/visitor.luau:1028:22-25 lute/std/libs/test/failure.luau:18:35-38 lute/std/libs/test/failure.luau:18:35-38 lute/std/libs/test/reporter.luau:10:19-32 @@ -353,15 +357,6 @@ tests/std/process.test.luau:123:41-42 tests/std/process.test.luau:126:43-44 tests/std/process.test.luau:130:41-53 tests/std/process.test.luau:134:33-48 -tests/std/syntax/parser.test.luau:12:10-24 -tests/std/syntax/parser.test.luau:13:10-15 -tests/std/syntax/parser.test.luau:37:24-29 -tests/std/syntax/parser.test.luau:51:24-29 -tests/std/syntax/parser.test.luau:65:24-29 -tests/std/syntax/parser.test.luau:68:24-29 -tests/std/syntax/parser.test.luau:82:24-29 -tests/std/syntax/parser.test.luau:85:24-29 -tests/std/syntax/parser.test.luau:535:48-62 tests/std/syntax/printer.test.luau:9:16-29 tests/std/syntax/printer.test.luau:23:10-26 tests/std/syntax/printer.test.luau:45:8-13 From 22aa4b6d6b567eef5f2d202228432f362069cca0 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 29 Jan 2026 11:14:12 -0800 Subject: [PATCH 311/642] Updates `AstStatDo` to have an `AstStatBlock` as its body (#766) Pretty much what the title says. I initially thought I had to wait for upstream Luau to add `AstStatDo`, but I realized I could just rework serialization to have an extra `AstStatBlock` node. This makes `AstStatDo`'s signature a little more idiomatic and match other syntactic constructs like `AstStatIf` and `AstStatWhile`. --- definitions/luau.luau | 2 +- lute/luau/src/luau.cpp | 24 +++++++- lute/std/libs/syntax/visitor.luau | 4 +- tests/std/syntax/parser.test.luau | 3 +- tools/check-faillist.txt | 96 +++++++++++++++---------------- 5 files changed, 75 insertions(+), 54 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index fd81a15b9..28d1ba4ef 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -282,7 +282,7 @@ export type AstStatDo = { kind: "stat", tag: "do", dokeyword: Token<"do">, - body: { AstStat }, -- TODO: change to AstStatBlock when Luau adds AstStatDo on the C++ side + body: AstStatBlock, endkeyword: Token<"end">, } diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 8fe9557f8..a94569442 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1287,7 +1287,7 @@ struct AstSerialize : public Luau::AstVisitor if (cstDo) { - lua_rawcheckstack(L, 2); + lua_rawcheckstack(L, 3); lua_createtable(L, 0, preambleSize + 3); serializeNodePreamble(node, "do", "stat"); @@ -1295,7 +1295,29 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->location.begin, "do"); lua_setfield(L, -2, "dokeyword"); + // In lieu of a C++ AstStatBlock object to recurse on, manually construct a Luau AstStatBlock table + lua_createtable(L, 0, preambleSize + 1); + + lua_pushstring(L, "block"); + lua_setfield(L, -2, "tag"); + + lua_pushstring(L, "stat"); + lua_setfield(L, -2, "kind"); + + Luau::Location blockLocation; + if (node->body.size > 0) + { + blockLocation.begin = node->body.data[0]->location.begin; + blockLocation.end = node->body.data[node->body.size - 1]->location.end; + } + else + blockLocation = node->location; + + withLocation(blockLocation); + serializeStats(node->body); + lua_setfield(L, -2, "statements"); + lua_setfield(L, -2, "body"); serializeToken(cstDo->endPosition, "end"); diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index c68d14cb8..f0bb8abf1 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -186,9 +186,7 @@ end local function visitStatDo(node: types.AstStatDo, visitor: Visitor) if visitor.visitStatDo(node) then visitToken(node.dokeyword, visitor) - for _, statement in node.body do - visitStatement(statement, visitor) - end + visitStatBlock(node.body, visitor) visitToken(node.endkeyword, visitor) end end diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 45179f559..f7f158096 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -548,7 +548,8 @@ test.suite("parse", function(suite) local doStat = block.statements[1] :: syntax.AstStatDo assert.eq(doStat.tag, "do") assert.eq(doStat.dokeyword.text, "do") - assert.eq(#doStat.body, 1) + checkIsBlock(doStat.body, assert) + assert.eq(#doStat.body.statements, 1) assert.eq(doStat.endkeyword.text, "end") end) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 24ae54d44..7cefcc362 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -185,54 +185,54 @@ lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:31:13-21 lute/std/libs/syntax/utils/trivia.luau:31:13-21 -lute/std/libs/syntax/visitor.luau:239:3-12 -lute/std/libs/syntax/visitor.luau:239:3-12 -lute/std/libs/syntax/visitor.luau:245:3-12 -lute/std/libs/syntax/visitor.luau:245:3-12 -lute/std/libs/syntax/visitor.luau:376:3-12 -lute/std/libs/syntax/visitor.luau:376:3-12 -lute/std/libs/syntax/visitor.luau:382:3-12 -lute/std/libs/syntax/visitor.luau:382:3-12 -lute/std/libs/syntax/visitor.luau:388:3-12 -lute/std/libs/syntax/visitor.luau:388:3-12 -lute/std/libs/syntax/visitor.luau:406:3-12 -lute/std/libs/syntax/visitor.luau:406:3-12 -lute/std/libs/syntax/visitor.luau:440:3-12 -lute/std/libs/syntax/visitor.luau:440:3-12 -lute/std/libs/syntax/visitor.luau:656:3-12 -lute/std/libs/syntax/visitor.luau:656:3-12 -lute/std/libs/syntax/visitor.luau:662:3-12 -lute/std/libs/syntax/visitor.luau:662:3-12 -lute/std/libs/syntax/visitor.luau:685:3-12 -lute/std/libs/syntax/visitor.luau:685:3-12 -lute/std/libs/syntax/visitor.luau:944:24-28 -lute/std/libs/syntax/visitor.luau:944:24-28 -lute/std/libs/syntax/visitor.luau:953:35-39 -lute/std/libs/syntax/visitor.luau:953:35-39 -lute/std/libs/syntax/visitor.luau:961:28-32 -lute/std/libs/syntax/visitor.luau:961:28-32 -lute/std/libs/syntax/visitor.luau:961:28-32 -lute/std/libs/syntax/visitor.luau:961:28-32 -lute/std/libs/syntax/visitor.luau:965:19-23 -lute/std/libs/syntax/visitor.luau:965:19-23 -lute/std/libs/syntax/visitor.luau:972:25-29 -lute/std/libs/syntax/visitor.luau:972:25-29 -lute/std/libs/syntax/visitor.luau:976:21-25 -lute/std/libs/syntax/visitor.luau:976:21-25 -lute/std/libs/syntax/visitor.luau:990:21-25 -lute/std/libs/syntax/visitor.luau:990:21-25 -lute/std/libs/syntax/visitor.luau:990:21-25 -lute/std/libs/syntax/visitor.luau:990:21-25 -lute/std/libs/syntax/visitor.luau:997:17-21 -lute/std/libs/syntax/visitor.luau:997:17-21 -lute/std/libs/syntax/visitor.luau:1025:9-20 -lute/std/libs/syntax/visitor.luau:1025:9-20 -lute/std/libs/syntax/visitor.luau:1026:3-12 -lute/std/libs/syntax/visitor.luau:1026:3-12 -lute/std/libs/syntax/visitor.luau:1027:9-24 -lute/std/libs/syntax/visitor.luau:1027:9-24 -lute/std/libs/syntax/visitor.luau:1028:22-25 -lute/std/libs/syntax/visitor.luau:1028:22-25 +lute/std/libs/syntax/visitor.luau:237:3-12 +lute/std/libs/syntax/visitor.luau:237:3-12 +lute/std/libs/syntax/visitor.luau:243:3-12 +lute/std/libs/syntax/visitor.luau:243:3-12 +lute/std/libs/syntax/visitor.luau:374:3-12 +lute/std/libs/syntax/visitor.luau:374:3-12 +lute/std/libs/syntax/visitor.luau:380:3-12 +lute/std/libs/syntax/visitor.luau:380:3-12 +lute/std/libs/syntax/visitor.luau:386:3-12 +lute/std/libs/syntax/visitor.luau:386:3-12 +lute/std/libs/syntax/visitor.luau:404:3-12 +lute/std/libs/syntax/visitor.luau:404:3-12 +lute/std/libs/syntax/visitor.luau:438:3-12 +lute/std/libs/syntax/visitor.luau:438:3-12 +lute/std/libs/syntax/visitor.luau:654:3-12 +lute/std/libs/syntax/visitor.luau:654:3-12 +lute/std/libs/syntax/visitor.luau:660:3-12 +lute/std/libs/syntax/visitor.luau:660:3-12 +lute/std/libs/syntax/visitor.luau:683:3-12 +lute/std/libs/syntax/visitor.luau:683:3-12 +lute/std/libs/syntax/visitor.luau:942:24-28 +lute/std/libs/syntax/visitor.luau:942:24-28 +lute/std/libs/syntax/visitor.luau:951:35-39 +lute/std/libs/syntax/visitor.luau:951:35-39 +lute/std/libs/syntax/visitor.luau:959:28-32 +lute/std/libs/syntax/visitor.luau:959:28-32 +lute/std/libs/syntax/visitor.luau:959:28-32 +lute/std/libs/syntax/visitor.luau:959:28-32 +lute/std/libs/syntax/visitor.luau:963:19-23 +lute/std/libs/syntax/visitor.luau:963:19-23 +lute/std/libs/syntax/visitor.luau:970:25-29 +lute/std/libs/syntax/visitor.luau:970:25-29 +lute/std/libs/syntax/visitor.luau:974:21-25 +lute/std/libs/syntax/visitor.luau:974:21-25 +lute/std/libs/syntax/visitor.luau:988:21-25 +lute/std/libs/syntax/visitor.luau:988:21-25 +lute/std/libs/syntax/visitor.luau:988:21-25 +lute/std/libs/syntax/visitor.luau:988:21-25 +lute/std/libs/syntax/visitor.luau:995:17-21 +lute/std/libs/syntax/visitor.luau:995:17-21 +lute/std/libs/syntax/visitor.luau:1023:9-20 +lute/std/libs/syntax/visitor.luau:1023:9-20 +lute/std/libs/syntax/visitor.luau:1024:3-12 +lute/std/libs/syntax/visitor.luau:1024:3-12 +lute/std/libs/syntax/visitor.luau:1025:9-24 +lute/std/libs/syntax/visitor.luau:1025:9-24 +lute/std/libs/syntax/visitor.luau:1026:22-25 +lute/std/libs/syntax/visitor.luau:1026:22-25 lute/std/libs/test/failure.luau:18:35-38 lute/std/libs/test/failure.luau:18:35-38 lute/std/libs/test/reporter.luau:10:19-32 From 5671753c33d9304e9ca614cb8c3dec65d0ec228b Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:26:04 -0800 Subject: [PATCH 312/642] CI improvements: no unnecessary bootstrapping and Slack bot integration (#768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR aims to balance fast PR checks with ensuring that we never ship broken binaries. PRs: - Bootstrapped builds (i.e. 3 sequential fresh builds) no longer run as part of PR gated commit checks. This improves velocity, especially for stacked PRs, where each upstream PR being merged in requires a new run of the full CI suite for downstream PRs. - Actual speedup: - ~9.5 minutes down to ~6.5 minutes (~1.5x) when we get unlucky with a slow MacOS runner. - ~9.5 minutes down to ~5 minutes (~1.9x) in the typical case. When a PR is merged in to `primary`: - We re-run all the same checks, _including all bootstrapped builds_. If a build-breaking issue made it through PR checks without being detected, it will be caught here. This leads us to: The new "Lute Build Stability" Slack bot: when CI status at the tip of the `primary` branch _changes_ (to prevent noise), we send a Slack message to an internal channel with one of these messages. - 🚨 Lute CI has started failing - ✅ Lute CI is passing again To avoid automatically shipping a broken release, the nightly release pipeline early-exits (skipping the release for that day) until CI is passing again. --- .github/workflows/ci.yml | 60 ++++++++++++++++++++++++++++++++--- .github/workflows/release.yml | 11 +++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e19e5e0bf..d3dc496e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,25 +22,42 @@ jobs: include: - os: windows-latest options: --c-compiler cl.exe --cxx-compiler cl.exe + run-on-event: all + - os: macos-latest + options: --enable-sanitizers + run-on-event: all - os: ubuntu-latest options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers + run-on-event: pull_request - os: ubuntu-latest options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers - - os: macos-latest - options: --enable-sanitizers + run-on-event: pull_request - os: ubuntu-latest options: --c-compiler gcc --cxx-compiler g++ --enable-sanitizers use-bootstrap: true + run-on-event: push - os: ubuntu-latest options: --c-compiler clang --cxx-compiler clang++ --enable-sanitizers use-bootstrap: true + run-on-event: push fail-fast: false steps: + - name: Determine if this matrix job should run + id: should_run + shell: bash + run: | + if [[ "${{ matrix.run-on-event }}" == "all" || "${{ matrix.run-on-event }}" == "${{ github.event_name }}" ]]; then + echo "proceed=true" >> "$GITHUB_OUTPUT" + else + echo "proceed=false" >> "$GITHUB_OUTPUT" + fi + - uses: actions/checkout@v4 - name: Setup and Build Lute id: build_lute + if: ${{ steps.should_run.outputs.proceed == 'true' }} uses: ./.github/actions/setup-and-build with: target: Lute.CLI @@ -50,9 +67,11 @@ jobs: use-bootstrap: ${{ matrix.use-bootstrap || 'false' }} - name: Use CI .luaurc file + if: ${{ steps.should_run.outputs.proceed == 'true' }} run: rm .luaurc && mv .luaurc.ci .luaurc - name: Run Luau Tests (POSIX) + if: ${{ steps.should_run.outputs.proceed == 'true' }} run: ${{ steps.build_lute.outputs.exe_path }} test run-lute-cxx-unittests: @@ -119,7 +138,6 @@ jobs: config: debug options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} - use-bootstrap: true - name: Setup Node.js uses: actions/setup-node@v4 @@ -155,7 +173,6 @@ jobs: config: debug options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} - use-bootstrap: true - name: Run Lute Lint run: ${{ steps.build_lute.outputs.exe_path }} lint -v . @@ -174,7 +191,6 @@ jobs: target: Lute.CLI config: debug token: ${{ secrets.GITHUB_TOKEN }} - use-bootstrap: true - name: Run Lute Check run: ${{ steps.build_lute.outputs.exe_path }} tools/check.luau ${{ steps.build_lute.outputs.exe_path }} @@ -186,10 +202,44 @@ jobs: if: ${{ always() }} steps: - name: Aggregator + id: aggregator run: | if [ "${{ needs.run-lutecli-luau-tests.result }}" == "success" ] && [ "${{ needs.run-lute-cxx-unittests.result }}" == "success" ] && [ "${{ needs.check-format.result }}" == "success" ] && [ "${{ needs.build-docs-site.result }}" == "success" ] && [ "${{ needs.lute-lint.result }}" == "success" ] && [ "${{ needs.lute-check.result }}" == "success" ]; then echo "All jobs succeeded" + echo "result=success" >> "$GITHUB_OUTPUT" else echo "One or more jobs failed" + echo "result=failure" >> "$GITHUB_OUTPUT" exit 1 fi + - name: Post-aggregation notification + if: > + always() && + github.event_name == 'push' && + github.ref == 'refs/heads/primary' + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + run: | + LAST_STATUS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/luau-lang/lute/actions/workflows/ci.yml/runs?branch=primary&status=completed&per_page=1" \ + | jq -r '.workflow_runs[0].conclusion') + + if [[ "$LAST_STATUS" == "${{ steps.aggregator.outputs.result }}" ]]; then + echo "No status change detected. Skipping notification." + exit 0 + elif [[ "$LAST_STATUS" == "failure" ]]; then + TEXT=":white_check_mark: *Lute CI is passing again*" + elif [[ "$LAST_STATUS" == "success" ]]; then + TEXT=":rotating_light: *Lute CI has started failing*" + fi + + echo Sending Slack notification: "$TEXT" + + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + curl -sS -o /dev/null -X POST https://slack.com/api/chat.postMessage \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H "Content-Type: application/json; charset=utf-8" \ + --data "{ + \"channel\": \"C062MQ6TZQT\", + \"text\": \"$TEXT (<$RUN_URL|view workflow run>)\" + }" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f31837d7f..23dffaa6d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,17 @@ jobs: contents: read steps: + - name: Validate CI status + run: | + LAST_STATUS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/luau-lang/lute/actions/workflows/ci.yml/runs?branch=primary&status=completed&per_page=1" \ + | jq -r '.workflow_runs[0].conclusion') + + if [[ "$LAST_STATUS" != "success" ]]; then + echo "The latest CI run on 'primary' branch did not succeed. Aborting release." + exit 1 + fi + - name: Checkout code uses: actions/checkout@v4 with: From 35c3a7aa3775cee10f76a994e5cd9aadf780bf2a Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:40:13 -0800 Subject: [PATCH 313/642] Testing: break bootstrap builds on `primary` (#770) Plan to revert after confirming that our notification system is working as intended. --- tools/bootstrap.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh index 6dd717cb0..76c88a1c5 100755 --- a/tools/bootstrap.sh +++ b/tools/bootstrap.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash - +exit 1 # intentionally break bootstrap builds set -e install_requested=false From 4bab870b00664ead31edcca8aa4e6eae9f32fd79 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:59:19 -0800 Subject: [PATCH 314/642] Testing: fix bootstrap builds on primary (#771) Reverts #770. --- tools/bootstrap.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh index 76c88a1c5..6dd717cb0 100755 --- a/tools/bootstrap.sh +++ b/tools/bootstrap.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -exit 1 # intentionally break bootstrap builds + set -e install_requested=false From e44b735219215611aaa837948e8f7cd4d0bc57b9 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:21:30 -0800 Subject: [PATCH 315/642] Infra: store Slack channel ID as secret (#772) Allow us to easily swap this out if needed without needing to make a code change. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3dc496e3..ba804157d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -235,11 +235,12 @@ jobs: echo Sending Slack notification: "$TEXT" + CHANNEL_ID="${{ secrets.SLACK_CHANNEL_ID }}" RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" curl -sS -o /dev/null -X POST https://slack.com/api/chat.postMessage \ -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ -H "Content-Type: application/json; charset=utf-8" \ --data "{ - \"channel\": \"C062MQ6TZQT\", + \"channel\": \"$CHANNEL_ID\", \"text\": \"$TEXT (<$RUN_URL|view workflow run>)\" }" From 5ace6e7cef07fdd79f60c989badda07270e361e6 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:12:56 -0800 Subject: [PATCH 316/642] Adds `TypeSerializer` and updates `luau.typeofmodule` to return a `TypePack` of `Type`s (#769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem `luau.typeofmodule` currently returns a `string` representing the module's return type, which is hard for users to actually use ### Solution To move towards a useable Luau documentation generator (and the `lute doc` CLI), this PR exposes a similar version of Luau's `Type` and `TypePack` data structures (but it's just a table of the `Luau::Type` properties), which can then be used to represent a given module's return type as a data structure instead of a giant string. We can then serialize with the exposed functions `serializeType(TypeId ty, lua_State* L)` or `serializeTypePack(TypePackId tp, lua_State* L)`. It's very similar to how `TypeFunctionRuntime` and `TypeFunctionRuntimeBuilder` work, but we weren't able to use it because it's interlaced with memory allocation and runtime stuff that clashes with Lute runtime things So most of this is wrapper code of the `Luau::TypeVisitor`, with the `serialize()` functions following a similar pattern as the `AstSerializer`, but these ones are based on the below `type` signature (and see https://luau.org/types-library/). ```lua export type Type = { tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "buffer" | "thread" | "singleton" | "negation" | "union" | "intersection" | "table" | "metatable" | "function" | "extern" | "generic", -- for singleton type value: string | boolean | nil, -- for negation type inner: Type, -- for union and intersection types components: { Type }, -- for table type properties: { [string]: { read: Type?, write: Type? } }, indexer: { index: Type }?, -- TODO: add readresult and writeresult -- TODO: readindexer: { index: type, result: type } }?, -- TODO: writeindexer: { index: type, result: type } }?, metatable: Type?, -- for function type parameters: { head: { Type }?, tail: Type }, returns: { head: { Type }?, tail: Type }, generics: { Type }, genericpacks: { TypePack }, -- for extern type -- 'properties', 'metatable' are shared with table type parent: Type?, -- for generic type name: string?, ispack: boolean, } export type TypePack = { head: { Type }?, tail: Type?, } ``` so `luau.typeofmodule: TypePack` and users can parse this table to process the type of a module! Additionally, `TypeSerializer` doesn't serialize some types like * `BoundType`, `FreeType`, `ErrorType`, `NoRefineType`, `BlockedType`, `PendingExpansionType`, `TypeFunctionInstanceType` * `BoundTypePack`, `FreeTypePack`, `ErrorTypePack`, `BlockedTypePack`, `TypeFunctionInstanceTypePack` since these aren't user facing ### Future Work 1. Most likely a `typevisitor` library (like `visitor.luau`) to help traverse and parse the `TypePack` and `Type` types, because the updated example looks ugly 2. This implementation does just represent everything in a table of values, so it's not the full userdata where you can call `tabletype:properties()` for ex, but I think building on that in a subsequent PR is reasonable? (i.e., more how `Span` works I think?) 😄 --- definitions/luau.luau | 59 +- .../get_module_return_type.luau | 39 +- lute/luau/CMakeLists.txt | 2 + lute/luau/include/lute/type.h | 13 + lute/luau/src/luau.cpp | 12 +- lute/luau/src/type.cpp | 575 ++++++++++++++++++ lute/std/libs/luau.luau | 4 +- lute/std/libs/syntax/types.luau | 4 + tests/std/luau.test.luau | 121 +++- 9 files changed, 806 insertions(+), 23 deletions(-) create mode 100644 lute/luau/include/lute/type.h create mode 100644 lute/luau/src/type.cpp diff --git a/definitions/luau.luau b/definitions/luau.luau index 28d1ba4ef..fb708ff59 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -686,8 +686,63 @@ function luau.resolverequire(path: string, fromchunkname: string): string error("not implemented") end --- For now, we return a string representation of the module's type, but we will expand it to some Luau data structure representation of Type (similar to the AST types) in a subsequent PR. -function luau.typeofmodule(modulepath: string): string +export type Type = { + tag: "nil" + | "unknown" + | "never" + | "any" + | "boolean" + | "number" + | "string" + | "buffer" + | "thread" + | "singleton" + | "negation" + | "union" + | "intersection" + | "table" + | "metatable" + | "function" + | "extern" + | "generic", + + -- for singleton type + value: string | boolean | nil, + + -- for negation type + inner: Type, + + -- for union and intersection types + components: { Type }, + + -- for table type + properties: { [string]: { read: Type?, write: Type? } }, + indexer: { index: Type }?, -- TODO: add readresult and writeresult + -- TODO: readindexer: { index: type, result: type } }?, + -- TODO: writeindexer: { index: type, result: type } }?, + metatable: Type?, + + -- for function type + parameters: { head: { Type }?, tail: Type }, + returns: { head: { Type }?, tail: Type }, + generics: { Type }, + genericpacks: { TypePack }, + + -- for extern type + -- 'properties', 'metatable' are shared with table type + parent: Type?, + + -- for generic type + name: string?, + ispack: boolean, +} + +export type TypePack = { + head: { Type }?, + tail: Type?, +} + +function luau.typeofmodule(modulepath: string): TypePack? error("not implemented") end diff --git a/examples/module_return_type/get_module_return_type.luau b/examples/module_return_type/get_module_return_type.luau index 3b30613ff..9154a84a3 100644 --- a/examples/module_return_type/get_module_return_type.luau +++ b/examples/module_return_type/get_module_return_type.luau @@ -1,11 +1,44 @@ local luau = require("@std/luau") +local path = require("@std/path") +local process = require("@std/process") -local moduleTypeString = luau.typeofmodule("examples/module_return_type/mainmodule.luau") +local filePath = path.join(process.cwd(), "examples", "module_return_type", "mainmodule.luau") +local returnType = luau.typeofmodule(filePath) -print("Module return type\n", moduleTypeString) +if not returnType then + error("Failed to get module return type") +end + +if not returnType.head then + error("Return type head is nil") +end + +for _, retType in ipairs(returnType.head) do + if retType.tag ~= "table" then + error("Expected return type to be a table") + end + + if not retType.properties then + error("Return type table has no properties") + end + + for name, prop in pairs(retType.properties) do + if prop.read then + print(name .. ": " .. prop.read.tag) + elseif prop.write then + print(name .. ": " .. prop.write.tag) + else + print(name .. ": (no read or write type)") + end + end +end -- Output: --- Module return type +-- func1: function +-- strings: table +-- _metadata: table + +-- Actual Module return type -- { -- _metadata: { -- id: string, diff --git a/lute/luau/CMakeLists.txt b/lute/luau/CMakeLists.txt index efe530aa9..18274ee6a 100644 --- a/lute/luau/CMakeLists.txt +++ b/lute/luau/CMakeLists.txt @@ -5,11 +5,13 @@ target_sources(Lute.Luau PRIVATE include/lute/luau.h include/lute/moduleresolver.h include/lute/resolverequire.h + include/lute/type.h src/configresolver.cpp src/luau.cpp src/moduleresolver.cpp src/resolverequire.cpp + src/type.cpp ) target_compile_features(Lute.Luau PUBLIC cxx_std_17) diff --git a/lute/luau/include/lute/type.h b/lute/luau/include/lute/type.h new file mode 100644 index 000000000..8dd543979 --- /dev/null +++ b/lute/luau/include/lute/type.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Luau/TypeFwd.h" + +#include "lua.h" + +namespace Luau +{ + +void serializeType(TypeId ty, lua_State* L); +void serializeTypePack(TypePackId tp, lua_State* L); + +} // namespace Luau diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index a94569442..c8a2481dc 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -3,6 +3,7 @@ #include "lute/configresolver.h" #include "lute/moduleresolver.h" #include "lute/userdatas.h" +#include "lute/type.h" #include "Luau/Ast.h" #include "Luau/BuiltinDefinitions.h" @@ -2910,16 +2911,9 @@ int typeofmodule_luau(lua_State* L) return 1; } - // For now, we return a string representation of the module's type, but we will expand it to some Luau data structure representation of Type - // (similar to the AST types) in a subsequent PR. - Luau::ToStringOptions opts; - opts.exhaustive = true; - opts.useLineBreaks = true; - opts.functionTypeArguments = true; - opts.scope = modulePtr->getModuleScope(); + // Serialize and push the return type + serializeTypePack(modulePtr->returnType, L); - std::string moduleTypeStr = Luau::toString(modulePtr->returnType, opts); - lua_pushlstring(L, moduleTypeStr.c_str(), moduleTypeStr.length()); return 1; } diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp new file mode 100644 index 000000000..b83b36241 --- /dev/null +++ b/lute/luau/src/type.cpp @@ -0,0 +1,575 @@ +#include "lute/type.h" + +#include "Luau/Type.h" +#include "Luau/TypeFwd.h" +#include "Luau/VisitType.h" + +#include "lua.h" + +namespace Luau +{ + +// Serializer for runtime types, modeled after AstSerialize but for TypeId/TypePackId +struct TypeSerialize final : public Luau::TypeVisitor +{ + lua_State* L; + + explicit TypeSerialize(lua_State* L) + : Luau::TypeVisitor{"TypeSerialize", /*skipBoundTypes=*/false} + , L(L) + { + } + + // Helper to push the "tag" field + void pushTag(const char* tag) + { + lua_pushstring(L, tag); + lua_setfield(L, -2, "tag"); + } + + // Helper to push a Name, Property pair into the properties table + void pushProperty(const std::string& name, const Property& prop) + { + // push the Name string as the key for the property + lua_pushstring(L, name.c_str()); + + // Create property value table with read/write types + lua_createtable(L, 0, 2); + if (prop.readTy) + { + traverse(*prop.readTy); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "read"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "read"); + } + + if (prop.writeTy) + { + traverse(*prop.writeTy); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "write"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "write"); + } + + lua_settable(L, -3); + } + + // Helper to set the "properties" field for table and extern types + void setPropertiesFields(const std::map& props) + { + lua_createtable(L, 0, props.size()); + + for (const auto& [name, prop] : props) + pushProperty(name, prop); + + lua_setfield(L, -2, "properties"); + } + + // Luau type + // tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "buffer" | "thread" | "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "extern" | "generic" + void serialize(TypeId ty, const PrimitiveType& ptv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 1); + + const char* tag = nullptr; + switch (ptv.type) + { + case PrimitiveType::NilType: + tag = "nil"; + break; + case PrimitiveType::Boolean: + tag = "boolean"; + break; + case PrimitiveType::Number: + tag = "number"; + break; + case PrimitiveType::String: + tag = "string"; + break; + case PrimitiveType::Thread: + tag = "thread"; + break; + case PrimitiveType::Buffer: + tag = "buffer"; + break; + default: + lua_error(L); + } + + pushTag(tag); + } + + void serialize(TypeId ty, const AnyType& atv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 1); + + pushTag("any"); + } + + void serialize(TypeId ty, const UnknownType& utv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 1); + + pushTag("unknown"); + } + + void serialize(TypeId ty, const NeverType& ntv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 1); + + pushTag("never"); + } + + // Luau singleton type: + // value: string | boolean | nil + void serialize(TypeId ty, const SingletonType& stv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 2); + + pushTag("singleton"); + + if (auto boolSingleton = get(&stv)) + { + lua_pushboolean(L, boolSingleton->value); + lua_setfield(L, -2, "value"); + } + else if (auto strSingleton = get(&stv)) + { + lua_pushstring(L, strSingleton->value.c_str()); + lua_setfield(L, -2, "value"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "value"); + } + } + + // Luau negation type: + // inner: type + void serialize(TypeId ty, const NegationType& ntv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 2); + + pushTag("negation"); + + traverse(ntv.ty); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "inner"); + } + + // Luau union type: + // components: { type } + void serialize(TypeId ty, const UnionType& utv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 2); + + pushTag("union"); + + lua_createtable(L, utv.options.size(), 0); + for (size_t i = 0; i < utv.options.size(); i++) + { + traverse(utv.options[i]); + lua_rawcheckstack(L, 2); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "components"); + } + + // Luau intersection type: + // components: { type } + void serialize(TypeId ty, const IntersectionType& itv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 2); + + pushTag("intersection"); + + lua_createtable(L, itv.parts.size(), 0); + for (size_t i = 0; i < itv.parts.size(); i++) + { + traverse(itv.parts[i]); + lua_rawcheckstack(L, 2); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "components"); + } + + // Luau generic type: + // name: string? + // ispack: boolean + void serialize(TypeId ty, const GenericType& gtv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 3); + + pushTag("generic"); + + if (!gtv.name.empty()) + { + lua_pushstring(L, gtv.name.c_str()); + lua_setfield(L, -2, "name"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "name"); + } + + lua_pushboolean(L, false); // GenericType is not a pack + lua_setfield(L, -2, "ispack"); + } + + // Luau function type: + // parameters: { head: {type}?, tail: type? }, + // returns: { head: {type}?, tail: type? }, + // generics: {type}, + void serialize(TypeId ty, const FunctionType& ftv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 4); + + pushTag("function"); + + // Parameters + traverse(ftv.argTypes); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "parameters"); + + // Returns + traverse(ftv.retTypes); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "returns"); + + // Generics + lua_createtable(L, ftv.generics.size(), 0); + for (size_t i = 0; i < ftv.generics.size(); i++) + { + traverse(ftv.generics[i]); + lua_rawcheckstack(L, 2); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "generics"); + + // Generic packs + lua_createtable(L, ftv.genericPacks.size(), 0); + for (size_t i = 0; i < ftv.genericPacks.size(); i++) + { + traverse(ftv.genericPacks[i]); + lua_rawcheckstack(L, 2); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "genericpacks"); + } + + // Luau table type: + // properties: { string: { read: type?, write: type? } }, + // indexer: { index: type, readresult: type, writeresult: type }?, + // [TODO] readindexer: { index: type, result: type } }?, + // [TODO] writeindexer: { index: type, result: type } }?, + void serialize(TypeId ty, const TableType& ttv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 4); + + pushTag("table"); + + // Properties + if (!ttv.props.empty()) + { + setPropertiesFields(ttv.props); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "properties"); + } + + // Indexer + if (ttv.indexer) + { + lua_createtable(L, 0, 3); + traverse(ttv.indexer->indexType); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "index"); + + // TODO: implement readindexer and writeindexer from ttv.indexer->indexResultType + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "indexer"); + } + } + + // Luau metatable type: + // metatable: type? + void serialize(TypeId ty, const MetatableType& mtv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 2); + + pushTag("metatable"); + + traverse(mtv.table); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "table"); + + traverse(mtv.metatable); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "metatable"); + } + + // Luau extern type: + // 'properties', 'metatable' are shared with table type + // parent: type? + void serialize(TypeId ty, const ExternType& etv) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 3); + + pushTag("extern"); + + // Properties + if (!etv.props.empty()) + { + setPropertiesFields(etv.props); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "properties"); + } + + // Parent + if (etv.parent) + { + traverse(*etv.parent); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "parent"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "parent"); + } + + // Metatable + if (etv.metatable) + { + traverse(*etv.metatable); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "metatable"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "metatable"); + } + } + + // Luau TypePack is { head: {type}?, tail: type? } + void serialize(TypePackId tp, const TypePack& pack) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 2); + + // Head + if (!pack.head.empty()) + { + lua_createtable(L, int(pack.head.size()), 0); + for (size_t i = 0; i < pack.head.size(); i++) + { + traverse(pack.head[i]); + lua_rawcheckstack(L, 2); + lua_rawseti(L, -2, int(i + 1)); + } + lua_setfield(L, -2, "head"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "head"); + } + + // Tail + if (pack.tail) + { + traverse(*pack.tail); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "tail"); + } + else + { + lua_pushnil(L); + lua_setfield(L, -2, "tail"); + } + } + + // Luau VariadicTypePack is { head: nil, tail: type } + void serialize(TypePackId tp, const VariadicTypePack& vtp) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 2); + + // Head is nil + lua_pushnil(L); + lua_setfield(L, -2, "head"); + + // Tail is the type + traverse(vtp.ty); + lua_rawcheckstack(L, 2); + lua_setfield(L, -2, "tail"); + } + + // Luau GenericTypePack is { head: nil, tail: generic type with ispack=true } + void serialize(TypePackId tp, const GenericTypePack& gtp) + { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, 3); + + // Head is nil + lua_pushnil(L); + lua_setfield(L, -2, "head"); + + // Tail is generic type + lua_createtable(L, 0, 3); + lua_pushstring(L, "generic"); + lua_setfield(L, -2, "tag"); + if (!gtp.name.empty()) + { + lua_pushstring(L, gtp.name.c_str()); + lua_setfield(L, -2, "name"); + } + lua_pushboolean(L, true); // ispack = true for generic type packs + lua_setfield(L, -2, "ispack"); + lua_setfield(L, -2, "tail"); + } + + // Luau Type visitors + bool visit(TypeId ty, const PrimitiveType& ptv) override + { + serialize(ty, ptv); + return false; + } + + bool visit(TypeId ty, const AnyType& atv) override + { + serialize(ty, atv); + return false; + } + + bool visit(TypeId ty, const UnknownType& utv) override + { + serialize(ty, utv); + return false; + } + + bool visit(TypeId ty, const NeverType& ntv) override + { + serialize(ty, ntv); + return false; + } + + bool visit(TypeId ty, const SingletonType& stv) override + { + serialize(ty, stv); + return false; + } + + bool visit(TypeId ty, const NegationType& ntv) override + { + serialize(ty, ntv); + return false; + } + + bool visit(TypeId ty, const UnionType& utv) override + { + serialize(ty, utv); + return false; + } + + bool visit(TypeId ty, const IntersectionType& itv) override + { + serialize(ty, itv); + return false; + } + + bool visit(TypeId ty, const GenericType& gtv) override + { + serialize(ty, gtv); + return false; + } + + bool visit(TypeId ty, const FunctionType& ftv) override + { + serialize(ty, ftv); + return false; + } + + bool visit(TypeId ty, const TableType& ttv) override + { + serialize(ty, ttv); + return false; + } + + bool visit(TypeId ty, const MetatableType& mtv) override + { + serialize(ty, mtv); + return false; + } + + bool visit(TypeId ty, const ExternType& etv) override + { + serialize(ty, etv); + return false; + } + + // Luau TypePack visitors + bool visit(TypePackId tp, const TypePack& pack) override + { + serialize(tp, pack); + return false; + } + + bool visit(TypePackId tp, const VariadicTypePack& vtp) override + { + serialize(tp, vtp); + return false; + } + + bool visit(TypePackId tp, const GenericTypePack& gtp) override + { + serialize(tp, gtp); + return false; + } +}; + +void serializeType(TypeId ty, lua_State* L) +{ + TypeSerialize serializer(L); + serializer.traverse(ty); +} + +void serializeTypePack(TypePackId tp, lua_State* L) +{ + TypeSerialize serializer(L); + serializer.traverse(tp); +} + +} // namespace Luau diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 932801aaf..909146a02 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -32,7 +32,9 @@ function luau.loadbypath(requirePath: path.pathlike, env: { [any]: any }?): any return luau.load(migrationBytecode, requirePathStr, env)() end -function luau.typeofmodule(modulepath: path.pathlike): string +export type TypePack = luteLuau.TypePack + +function luau.typeofmodule(modulepath: path.pathlike): TypePack? local modulePathStr = if typeof(modulepath) == "string" then modulepath else path.format(modulepath) return luteLuau.typeofmodule(modulePathStr) diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index aded489c4..7e6797758 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -160,4 +160,8 @@ export type ParseResult = luau.ParseResult export type replacement = string | AstNode export type replacements = { [AstNode]: replacement } +export type Type = luau.Type + +export type TypePack = luau.TypePack + return {} diff --git a/tests/std/luau.test.luau b/tests/std/luau.test.luau index 0d542cdcc..09cbd472e 100644 --- a/tests/std/luau.test.luau +++ b/tests/std/luau.test.luau @@ -7,7 +7,7 @@ local test = require("@std/test") local tmpDir = system.tmpdir() test.suite("LuauTypeOfModuleSuite", function(suite) - suite:case("typeofmodule_returns_as_string_no_require", function(assert) + suite:case("typeofmodule_returns_table", function(assert) local testPath = path.join(tmpDir, "typeofmodule_test.luau") local testFile = fs.open(testPath, "w+") fs.write( @@ -18,19 +18,124 @@ test.suite("LuauTypeOfModuleSuite", function(suite) end return { - example_string = example_string, + example_string_func = example_string, } ]] ) fs.close(testFile) - local moduleTypeString = luau.typeofmodule(testPath) - local expectedOutputString = [[{ - example_string: () -> string -}]] + local typePack = luau.typeofmodule(testPath) + assert.neq(typePack, nil) + if not typePack then + error("typePack should not be nil") + end - -- Basic check for now until the data structure is created - assert.eq(moduleTypeString, expectedOutputString) + -- Verify TypePack structure: { head: {type}?, tail: type? } + assert.neq(typePack.head, nil) + if not typePack.head then + error("typePack.head should not be nil") + end + assert.eq(typePack.tail, nil) -- No tail for simple return + + -- Verify head is an array with one element (the table type) + assert.eq(#typePack.head, 1) + + local returnType = typePack.head[1] + assert.neq(returnType, nil) + assert.eq(returnType.tag, "table") + + -- Verify table has properties + assert.neq(returnType.properties, nil) + + -- Find the example_string_func property + local foundProperty = false + for name, prop in pairs(returnType.properties) do + if name == "example_string_func" then + foundProperty = true + assert.neq(prop.read, nil) + local funcType = prop.read + if not funcType then + error("Function type should not be nil") + end + + assert.eq(funcType.tag, "function") + -- Verify function returns string + assert.neq(funcType.returns, nil) + assert.neq(funcType.returns.head, nil) + if not funcType.returns.head then + error("Function.returns.head should not be nil") + end + + assert.eq(#funcType.returns.head, 1) + assert.eq(funcType.returns.head[1].tag, "string") + break + end + end + assert.eq(foundProperty, true, "Should find example_string_func property") + end) + + suite:case("typeofmodule_returns_typepack_with_multiple_values", function(assert) + local testPath = path.join(tmpDir, "typeofmodule_multiple.luau") + local testFile = fs.open(testPath, "w+") + fs.write( + testFile, + [[ + return "hello", 42, true + ]] + ) + fs.close(testFile) + + local typePack = luau.typeofmodule(testPath) + assert.neq(typePack, nil) + if not typePack then + error("typePack should not be nil") + end + + assert.neq(typePack.head, nil) + if not typePack.head then + error("typePack.head should not be nil") + end + assert.eq(#typePack.head, 3) + + -- Verify types: string, number, boolean + assert.eq(typePack.head[1].tag, "string") + assert.eq(typePack.head[2].tag, "number") + assert.eq(typePack.head[3].tag, "boolean") + assert.eq(typePack.tail, nil) + end) + + suite:case("typeofmodule_returns_typepack_with_variadic_tail", function(assert) + local testPath = path.join(tmpDir, "typeofmodule_variadic.luau") + local testFile = fs.open(testPath, "w+") + fs.write( + testFile, + [[ + local function variadic(...: string): ...string + return ... + end + return variadic + ]] + ) + fs.close(testFile) + + local typePack = luau.typeofmodule(testPath) + assert.neq(typePack, nil) + if not typePack then + error("typePack should not be nil") + end + + assert.neq(typePack.head, nil) + if not typePack.head then + error("typePack.head should not be nil") + end + assert.eq(#typePack.head, 1) + + local funcType = typePack.head[1] + assert.eq(funcType.tag, "function") + assert.neq(funcType.returns, nil) + -- Variadic return should have tail set to the variadic type + assert.neq(funcType.returns.tail, nil) + assert.eq(funcType.returns.tail.tag, "string") end) suite:case("resolverequire", function(assert) From 076ac50c5ef11e6cf26081713b6ad3de9eb53bec Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 2 Feb 2026 17:22:43 -0800 Subject: [PATCH 317/642] Adds suggested fixes for divide by zero lint rule (#779) Suggests `math.huge` and `0 / 0` in lieu of a `math.nan` --- .../commands/lint/rules/divide_by_zero.luau | 26 ++++++- tests/cli/lint.test.luau | 72 ++++++++++++++++++- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau index a3a34f853..3a958d29d 100644 --- a/lute/cli/commands/lint/rules/divide_by_zero.luau +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -7,6 +7,10 @@ local utils = require("@std/syntax/utils") local name = "divide_by_zero" local message = "Division by zero detected." +local function isZeroLiteral(expr: syntax.AstExpr): boolean + return expr.kind == "expr" and expr.tag == "number" and expr.value == 0 +end + local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } return query .findallfromroot(ast, utils.isExprBinary) @@ -14,18 +18,36 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" end) :filter(function(bin) - return bin.rhsoperand.kind == "expr" and bin.rhsoperand.tag == "number" and bin.rhsoperand.value == 0 + return isZeroLiteral(bin.rhsoperand) end) :maptoarray( function( n: syntax.AstExprBinary - ): lintTypes.LintViolation -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation + ): lintTypes.LintViolation? -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation + local suggestedfix = nil + if n.operator.text == "/" or n.operator.text == "//" then + if isZeroLiteral(n.lhsoperand) then + return nil + elseif n.lhsoperand.tag == "unary" and n.lhsoperand.operator.text == "-" then + if isZeroLiteral(n.lhsoperand.operand) then + suggestedfix = table.freeze({ fix = "0 / 0" }) + else + suggestedfix = table.freeze({ fix = "-math.huge" }) + end + else + suggestedfix = table.freeze({ fix = "math.huge" }) + end + elseif n.operator.text == "%" then + suggestedfix = table.freeze({ fix = "0 / 0" }) + end + return { lintname = name, location = n.location, message = message, severity = "warning", sourcepath = sourcepath, + suggestedfix = suggestedfix, } end ) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 732f0f93d..f1e1f3aae 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -141,6 +141,43 @@ violator.luau:1:11-16 ── fs.remove(violatorPath) end) + suite:case("divide_by_zero auto-fix", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 1 / 0 +local y = -3 // 0 +local z = 4 % 0 +local nan = 0 / 0 +local negnan = -0 / 0 +]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "--auto-fix", violatorPath }) + + -- Check + print(result.stdout) + assert.eq(result.exitcode, 0) -- After applying the auto-fix, there are no more violations + + local fixedContents = fs.readfiletostring(violatorPath) + local expectedFixedContents = [[ +local x = math.huge +local y = -math.huge +local z = 0 / 0 +local nan = 0 / 0 +local negnan = 0 / 0 +]] + assert.eq(fixedContents, expectedFixedContents) + + -- Teardown + fs.remove(violatorPath) + end) + suite:case("almost_swapped", function(assert) -- Setup -- Create a file that violates the rule @@ -997,6 +1034,19 @@ local x = 1 / 0 "character": 15, "line": 0 } + }, + "suggestedfix": { + "range": { + "start": { + "character": 10, + "line": 0 + }, + "end": { + "character": 15, + "line": 0 + } + }, + "fix": "math.huge" } } ], @@ -1225,6 +1275,19 @@ b = a "character": 15, "line": 0 } + }, + "suggestedfix": { + "range": { + "start": { + "character": 10, + "line": 0 + }, + "end": { + "character": 15, + "line": 0 + } + }, + "fix": "math.huge" } } ]]=] @@ -1431,7 +1494,7 @@ until 5 == 5 a = b b = a -local x = 3 / 0 +local x = t == { x = 3 } ]] ) @@ -1446,11 +1509,14 @@ local x = 3 / 0 local expectedFixedContents = [[ a, b = b, a -local x = 3 / 0 +local x = t == { x = 3 } ]] assert.eq(fixedContents, expectedFixedContents) - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + assert.strcontains( + result.stdout, + "warning[constant_table_comparison]: Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false." + ) assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") From 8ad3cc5226281d37dfaaf4959291175e3a89324a Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:38:36 -0800 Subject: [PATCH 318/642] Cleans up `TypeSerialize` (#780) Some cleaning up of `TypeSerialize` after talking with @Vighnesh-V and @vrn-sn Changes: * update API of public `serializeType` and `serializeTypePack` funcs to match luau definitions & sets the table to be read only * removing check stacks that come after internal `serialize` traverse functions bc upon further discussion and implementation of all of the traverse functions in https://github.com/luau-lang/lute/pull/778, we don't need to add the extra checks, we will just error if we reach a type or typepack that we aren't serializing * renaming raw checkstacks to be regular `lua_checkstack` and making sure the size is just enough (each `lua_pushxxx` followed by a `lua_setfield` removes space on the top of the `lua_state` so we only need to allocate a spot for non-temporary pushes (i.e., `lua_createtable`) and a slot to do the temporary pushes) * updating setPropertiesFields to also check its stack before pushing --- lute/luau/include/lute/type.h | 4 +- lute/luau/src/luau.cpp | 2 +- lute/luau/src/type.cpp | 163 ++++++++++++---------------------- 3 files changed, 62 insertions(+), 107 deletions(-) diff --git a/lute/luau/include/lute/type.h b/lute/luau/include/lute/type.h index 8dd543979..5ccbcde1f 100644 --- a/lute/luau/include/lute/type.h +++ b/lute/luau/include/lute/type.h @@ -7,7 +7,7 @@ namespace Luau { -void serializeType(TypeId ty, lua_State* L); -void serializeTypePack(TypePackId tp, lua_State* L); +int serializeType(lua_State* L, TypeId ty); +int serializeTypePack(lua_State* L, TypePackId tp); } // namespace Luau diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index c8a2481dc..ba3930cbe 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -2912,7 +2912,7 @@ int typeofmodule_luau(lua_State* L) } // Serialize and push the return type - serializeTypePack(modulePtr->returnType, L); + serializeTypePack(L, modulePtr->returnType); return 1; } diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp index b83b36241..37d1cacc0 100644 --- a/lute/luau/src/type.cpp +++ b/lute/luau/src/type.cpp @@ -5,10 +5,17 @@ #include "Luau/VisitType.h" #include "lua.h" +#include "lualib.h" namespace Luau { +static void checkStack(lua_State* L, int n) +{ + if (!lua_checkstack(L, n)) + luaL_error(L, "stack overflow while serializing type"); +} + // Serializer for runtime types, modeled after AstSerialize but for TypeId/TypePackId struct TypeSerialize final : public Luau::TypeVisitor { @@ -20,7 +27,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { } - // Helper to push the "tag" field + // Helper to push the "tag" field that every Luau type has void pushTag(const char* tag) { lua_pushstring(L, tag); @@ -30,34 +37,24 @@ struct TypeSerialize final : public Luau::TypeVisitor // Helper to push a Name, Property pair into the properties table void pushProperty(const std::string& name, const Property& prop) { + checkStack(L, 3); // 1 for key + 1 for value table + 1 for traverse/nil + // push the Name string as the key for the property lua_pushstring(L, name.c_str()); // Create property value table with read/write types lua_createtable(L, 0, 2); if (prop.readTy) - { traverse(*prop.readTy); - lua_rawcheckstack(L, 2); - lua_setfield(L, -2, "read"); - } else - { lua_pushnil(L); - lua_setfield(L, -2, "read"); - } + lua_setfield(L, -2, "read"); if (prop.writeTy) - { traverse(*prop.writeTy); - lua_rawcheckstack(L, 2); - lua_setfield(L, -2, "write"); - } else - { lua_pushnil(L); - lua_setfield(L, -2, "write"); - } + lua_setfield(L, -2, "write"); lua_settable(L, -3); } @@ -65,6 +62,7 @@ struct TypeSerialize final : public Luau::TypeVisitor // Helper to set the "properties" field for table and extern types void setPropertiesFields(const std::map& props) { + checkStack(L, 1); // 1 for properties table lua_createtable(L, 0, props.size()); for (const auto& [name, prop] : props) @@ -73,11 +71,11 @@ struct TypeSerialize final : public Luau::TypeVisitor lua_setfield(L, -2, "properties"); } - // Luau type + // Luau primitive type // tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "buffer" | "thread" | "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "extern" | "generic" void serialize(TypeId ty, const PrimitiveType& ptv) { - lua_rawcheckstack(L, 2); + checkStack(L, 2); // 1 for table + 1 for space to push/set remaining fields lua_createtable(L, 0, 1); const char* tag = nullptr; @@ -102,7 +100,7 @@ struct TypeSerialize final : public Luau::TypeVisitor tag = "buffer"; break; default: - lua_error(L); + luaL_error(L, "unexpected primitive type"); } pushTag(tag); @@ -110,7 +108,7 @@ struct TypeSerialize final : public Luau::TypeVisitor void serialize(TypeId ty, const AnyType& atv) { - lua_rawcheckstack(L, 2); + checkStack(L, 2); lua_createtable(L, 0, 1); pushTag("any"); @@ -118,7 +116,7 @@ struct TypeSerialize final : public Luau::TypeVisitor void serialize(TypeId ty, const UnknownType& utv) { - lua_rawcheckstack(L, 2); + checkStack(L, 2); lua_createtable(L, 0, 1); pushTag("unknown"); @@ -126,7 +124,7 @@ struct TypeSerialize final : public Luau::TypeVisitor void serialize(TypeId ty, const NeverType& ntv) { - lua_rawcheckstack(L, 2); + checkStack(L, 2); lua_createtable(L, 0, 1); pushTag("never"); @@ -136,39 +134,30 @@ struct TypeSerialize final : public Luau::TypeVisitor // value: string | boolean | nil void serialize(TypeId ty, const SingletonType& stv) { - lua_rawcheckstack(L, 2); + checkStack(L, 2); lua_createtable(L, 0, 2); pushTag("singleton"); if (auto boolSingleton = get(&stv)) - { lua_pushboolean(L, boolSingleton->value); - lua_setfield(L, -2, "value"); - } else if (auto strSingleton = get(&stv)) - { lua_pushstring(L, strSingleton->value.c_str()); - lua_setfield(L, -2, "value"); - } else - { lua_pushnil(L); - lua_setfield(L, -2, "value"); - } + lua_setfield(L, -2, "value"); } // Luau negation type: // inner: type void serialize(TypeId ty, const NegationType& ntv) { - lua_rawcheckstack(L, 2); + checkStack(L, 2); lua_createtable(L, 0, 2); pushTag("negation"); traverse(ntv.ty); - lua_rawcheckstack(L, 2); lua_setfield(L, -2, "inner"); } @@ -176,7 +165,7 @@ struct TypeSerialize final : public Luau::TypeVisitor // components: { type } void serialize(TypeId ty, const UnionType& utv) { - lua_rawcheckstack(L, 2); + checkStack(L, 3); // 1 for table + 1 for components table + 1 for traverse lua_createtable(L, 0, 2); pushTag("union"); @@ -185,7 +174,6 @@ struct TypeSerialize final : public Luau::TypeVisitor for (size_t i = 0; i < utv.options.size(); i++) { traverse(utv.options[i]); - lua_rawcheckstack(L, 2); lua_rawseti(L, -2, i + 1); } lua_setfield(L, -2, "components"); @@ -195,7 +183,7 @@ struct TypeSerialize final : public Luau::TypeVisitor // components: { type } void serialize(TypeId ty, const IntersectionType& itv) { - lua_rawcheckstack(L, 2); + checkStack(L, 3); // 1 for table + 1 for components table + 1 for traverse lua_createtable(L, 0, 2); pushTag("intersection"); @@ -204,7 +192,6 @@ struct TypeSerialize final : public Luau::TypeVisitor for (size_t i = 0; i < itv.parts.size(); i++) { traverse(itv.parts[i]); - lua_rawcheckstack(L, 2); lua_rawseti(L, -2, i + 1); } lua_setfield(L, -2, "components"); @@ -215,21 +202,16 @@ struct TypeSerialize final : public Luau::TypeVisitor // ispack: boolean void serialize(TypeId ty, const GenericType& gtv) { - lua_rawcheckstack(L, 2); + checkStack(L, 2); lua_createtable(L, 0, 3); pushTag("generic"); if (!gtv.name.empty()) - { lua_pushstring(L, gtv.name.c_str()); - lua_setfield(L, -2, "name"); - } else - { lua_pushnil(L); - lua_setfield(L, -2, "name"); - } + lua_setfield(L, -2, "name"); lua_pushboolean(L, false); // GenericType is not a pack lua_setfield(L, -2, "ispack"); @@ -239,21 +221,20 @@ struct TypeSerialize final : public Luau::TypeVisitor // parameters: { head: {type}?, tail: type? }, // returns: { head: {type}?, tail: type? }, // generics: {type}, + // genericpacks: {typepack} void serialize(TypeId ty, const FunctionType& ftv) { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 4); + checkStack(L, 3); // max 1 for table + 1 for subtable + 1 for traverse + lua_createtable(L, 0, 5); pushTag("function"); // Parameters traverse(ftv.argTypes); - lua_rawcheckstack(L, 2); lua_setfield(L, -2, "parameters"); // Returns traverse(ftv.retTypes); - lua_rawcheckstack(L, 2); lua_setfield(L, -2, "returns"); // Generics @@ -261,7 +242,6 @@ struct TypeSerialize final : public Luau::TypeVisitor for (size_t i = 0; i < ftv.generics.size(); i++) { traverse(ftv.generics[i]); - lua_rawcheckstack(L, 2); lua_rawseti(L, -2, i + 1); } lua_setfield(L, -2, "generics"); @@ -271,7 +251,6 @@ struct TypeSerialize final : public Luau::TypeVisitor for (size_t i = 0; i < ftv.genericPacks.size(); i++) { traverse(ftv.genericPacks[i]); - lua_rawcheckstack(L, 2); lua_rawseti(L, -2, i + 1); } lua_setfield(L, -2, "genericpacks"); @@ -284,8 +263,8 @@ struct TypeSerialize final : public Luau::TypeVisitor // [TODO] writeindexer: { index: type, result: type } }?, void serialize(TypeId ty, const TableType& ttv) { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 4); + checkStack(L, 2); + lua_createtable(L, 0, 3); pushTag("table"); @@ -305,8 +284,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { lua_createtable(L, 0, 3); traverse(ttv.indexer->indexType); - lua_rawcheckstack(L, 2); - lua_setfield(L, -2, "index"); + lua_setfield(L, -2, "indexer"); // TODO: implement readindexer and writeindexer from ttv.indexer->indexResultType } @@ -318,20 +296,19 @@ struct TypeSerialize final : public Luau::TypeVisitor } // Luau metatable type: + // table: type, // metatable: type? void serialize(TypeId ty, const MetatableType& mtv) { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 2); + checkStack(L, 2); + lua_createtable(L, 0, 3); pushTag("metatable"); traverse(mtv.table); - lua_rawcheckstack(L, 2); lua_setfield(L, -2, "table"); traverse(mtv.metatable); - lua_rawcheckstack(L, 2); lua_setfield(L, -2, "metatable"); } @@ -340,8 +317,8 @@ struct TypeSerialize final : public Luau::TypeVisitor // parent: type? void serialize(TypeId ty, const ExternType& etv) { - lua_rawcheckstack(L, 2); - lua_createtable(L, 0, 3); + checkStack(L, 2); + lua_createtable(L, 0, 4); pushTag("extern"); @@ -358,35 +335,23 @@ struct TypeSerialize final : public Luau::TypeVisitor // Parent if (etv.parent) - { traverse(*etv.parent); - lua_rawcheckstack(L, 2); - lua_setfield(L, -2, "parent"); - } else - { lua_pushnil(L); - lua_setfield(L, -2, "parent"); - } + lua_setfield(L, -2, "parent"); // Metatable if (etv.metatable) - { traverse(*etv.metatable); - lua_rawcheckstack(L, 2); - lua_setfield(L, -2, "metatable"); - } else - { lua_pushnil(L); - lua_setfield(L, -2, "metatable"); - } + lua_setfield(L, -2, "metatable"); } // Luau TypePack is { head: {type}?, tail: type? } void serialize(TypePackId tp, const TypePack& pack) { - lua_rawcheckstack(L, 2); + checkStack(L, 3); // 1 for root table + 1 for subtable + 1 for traverse lua_createtable(L, 0, 2); // Head @@ -396,35 +361,27 @@ struct TypeSerialize final : public Luau::TypeVisitor for (size_t i = 0; i < pack.head.size(); i++) { traverse(pack.head[i]); - lua_rawcheckstack(L, 2); lua_rawseti(L, -2, int(i + 1)); } - lua_setfield(L, -2, "head"); } else { lua_pushnil(L); - lua_setfield(L, -2, "head"); } + lua_setfield(L, -2, "head"); // Tail if (pack.tail) - { traverse(*pack.tail); - lua_rawcheckstack(L, 2); - lua_setfield(L, -2, "tail"); - } else - { lua_pushnil(L); - lua_setfield(L, -2, "tail"); - } + lua_setfield(L, -2, "tail"); } // Luau VariadicTypePack is { head: nil, tail: type } void serialize(TypePackId tp, const VariadicTypePack& vtp) { - lua_rawcheckstack(L, 2); + checkStack(L, 2); lua_createtable(L, 0, 2); // Head is nil @@ -433,32 +390,26 @@ struct TypeSerialize final : public Luau::TypeVisitor // Tail is the type traverse(vtp.ty); - lua_rawcheckstack(L, 2); lua_setfield(L, -2, "tail"); } - // Luau GenericTypePack is { head: nil, tail: generic type with ispack=true } + // Luau GenericTypePack is + // name: string? + // ispack: boolean void serialize(TypePackId tp, const GenericTypePack& gtp) { - lua_rawcheckstack(L, 2); + checkStack(L, 2); lua_createtable(L, 0, 3); + pushTag("generic"); - // Head is nil - lua_pushnil(L); - lua_setfield(L, -2, "head"); - - // Tail is generic type - lua_createtable(L, 0, 3); - lua_pushstring(L, "generic"); - lua_setfield(L, -2, "tag"); if (!gtp.name.empty()) - { lua_pushstring(L, gtp.name.c_str()); - lua_setfield(L, -2, "name"); - } - lua_pushboolean(L, true); // ispack = true for generic type packs + else + lua_pushnil(L); + lua_setfield(L, -2, "name"); + + lua_pushboolean(L, true); // GenericTypePack is a pack lua_setfield(L, -2, "ispack"); - lua_setfield(L, -2, "tail"); } // Luau Type visitors @@ -560,16 +511,20 @@ struct TypeSerialize final : public Luau::TypeVisitor } }; -void serializeType(TypeId ty, lua_State* L) +int serializeType(lua_State* L, TypeId ty) { TypeSerialize serializer(L); serializer.traverse(ty); + lua_setreadonly(L, -1, 1); + return 1; } -void serializeTypePack(TypePackId tp, lua_State* L) +int serializeTypePack(lua_State* L, TypePackId tp) { TypeSerialize serializer(L); serializer.traverse(tp); + lua_setreadonly(L, -1, 1); + return 1; } } // namespace Luau From bf9740c3ad3781b0726be97e46495fa65460d94e Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 3 Feb 2026 11:52:48 -0800 Subject: [PATCH 319/642] Ensures that the lute runtime initializes it's own event loop (#781) We need to support parallelism in Lute, and to do that each spawned runtime must have it's own event loop. This is because the uv_loop_t is not safe to share between threads. --- lute/fs/src/fs.cpp | 8 +++---- lute/fs/src/fs_impl.cpp | 32 +++++++++++++-------------- lute/io/src/io.cpp | 2 +- lute/net/src/net.cpp | 2 +- lute/process/src/process.cpp | 20 ++++++++--------- lute/runtime/include/lute/UVRequest.h | 7 ++++++ lute/runtime/include/lute/runtime.h | 6 +++++ lute/runtime/include/lute/uvutils.h | 2 ++ lute/runtime/src/runtime.cpp | 29 ++++++++++++++++++++---- lute/task/src/task.cpp | 20 ++++++++++------- 10 files changed, 83 insertions(+), 45 deletions(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 6dd63cae2..974627748 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -9,13 +9,12 @@ #include "uv.h" #include - -#include "fs_impl.h" - #include #include #include +#include "fs_impl.h" + namespace fs { @@ -264,7 +263,6 @@ static int closeWatchHandle(lua_State* L) { luaL_checktype(L, 1, LUA_TUSERDATA); auto* handle = static_cast(lua_touserdatatagged(L, 1, kWatchHandleTag)); - if (!handle) { luaL_errorL(L, "Invalid fs event handle"); @@ -287,7 +285,7 @@ int fs_watch(lua_State* L) event->callbackReference = std::make_shared(L, 2); event->handle.data = event; - int init_err = uv_fs_event_init(uv_default_loop(), &event->handle); + int init_err = uv_fs_event_init(getRuntimeLoop(L), &event->handle); if (init_err) { diff --git a/lute/fs/src/fs_impl.cpp b/lute/fs/src/fs_impl.cpp index 91f5f7d67..f63744f0c 100644 --- a/lute/fs/src/fs_impl.cpp +++ b/lute/fs/src/fs_impl.cpp @@ -114,7 +114,7 @@ int open_impl(lua_State* L, const char* path, int flags, int mode) { uvutils::ScopedUVRequest req(L); uv_fs_open( - uv_default_loop(), + req->getLoop(), &req->req, path, flags, @@ -175,7 +175,7 @@ void FSRead::readCallback(uv_fs_t* req) std::fill(r->chunk.begin(), r->chunk.end(), 0); uvutils::ScopedUVRequest scopedReq{std::move(r)}; - uv_fs_read(uv_default_loop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSRead::readCallback); + uv_fs_read(scopedReq->getLoop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSRead::readCallback); } void FSWrite::writeCallback(uv_fs_t* req) @@ -208,7 +208,7 @@ void FSWrite::writeCallback(uv_fs_t* req) w->iov = uv_buf_init(w->chunk.data(), chunkSize); uvutils::ScopedUVRequest scopedReq{std::move(w)}; - uv_fs_write(uv_default_loop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSWrite::writeCallback); + uv_fs_write(scopedReq->getLoop(), &scopedReq->req, scopedReq->file->fd.value(), &scopedReq->iov, 1, -1, FSWrite::writeCallback); } int read_impl(lua_State* L, UVFile* handle) @@ -219,7 +219,7 @@ int read_impl(lua_State* L, UVFile* handle) } uvutils::ScopedUVRequest req{L, handle}; - uv_fs_read(uv_default_loop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSRead::readCallback); + uv_fs_read(req->getLoop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSRead::readCallback); // Automatically releases when req goes out of scope return lua_yield(L, 0); } @@ -238,7 +238,7 @@ int write_impl(lua_State* L, UVFile* handle, const char* toWrite, size_t numByte std::copy(req->toWrite.begin(), req->toWrite.begin() + chunkSize, req->chunk.begin()); req->iov = uv_buf_init(req->chunk.data(), chunkSize); - uv_fs_write(uv_default_loop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSWrite::writeCallback); + uv_fs_write(req->getLoop(), &req->req, handle->fd.value(), &req->iov, 1, -1, FSWrite::writeCallback); return lua_yield(L, 0); } @@ -252,7 +252,7 @@ int close_impl(lua_State* L, UVFile* handle) uvutils::ScopedUVRequest req{L, handle}; uv_fs_close( - uv_default_loop(), + req->getLoop(), &req->req, handle->fd.value(), [](uv_fs_t* req) @@ -282,7 +282,7 @@ int remove_impl(lua_State* L, const char* path) { uvutils::ScopedUVRequest req(L); uv_fs_unlink( - uv_default_loop(), + req->getLoop(), &req->req, path, [](uv_fs_t* req) @@ -359,7 +359,7 @@ int stat_impl(lua_State* L, const char* path) { uvutils::ScopedUVRequest req(L); uv_fs_stat( - uv_default_loop(), + req->getLoop(), &req->req, path, [](uv_fs_t* req) @@ -418,7 +418,7 @@ int exists_impl(lua_State* L, const char* path) { uvutils::ScopedUVRequest req(L); uv_fs_access( - uv_default_loop(), + req->getLoop(), &req->req, path, F_OK, @@ -450,7 +450,7 @@ int type_impl(lua_State* L, const char* path) { uvutils::ScopedUVRequest req(L); uv_fs_stat( - uv_default_loop(), + req->getLoop(), &req->req, path, [](uv_fs_t* req) @@ -482,7 +482,7 @@ int link_impl(lua_State* L, const char* path, const char* dest) { uvutils::ScopedUVRequest req{L, path, dest}; uv_fs_link( - uv_default_loop(), + req->getLoop(), &req->req, path, dest, @@ -518,7 +518,7 @@ int symlink_impl(lua_State* L, const char* path, const char* dest) #endif uv_fs_symlink( - uv_default_loop(), + req->getLoop(), &req->req, path, dest, @@ -550,7 +550,7 @@ int copy_impl(lua_State* L, const char* path, const char* dest) { uvutils::ScopedUVRequest req{L, path, dest}; uv_fs_copyfile( - uv_default_loop(), + req->getLoop(), &req->req, path, dest, @@ -582,7 +582,7 @@ int mkdir_impl(lua_State* L, const char* path, int mode) { uvutils::ScopedUVRequest req(L); uv_fs_mkdir( - uv_default_loop(), + req->getLoop(), &req->req, path, mode, @@ -613,7 +613,7 @@ int rmdir_impl(lua_State* L, const char* path) { uvutils::ScopedUVRequest req(L); uv_fs_rmdir( - uv_default_loop(), + req->getLoop(), &req->req, path, [](uv_fs_t* req) @@ -643,7 +643,7 @@ int listdir_impl(lua_State* L, const char* path) { uvutils::ScopedUVRequest req(L); uv_fs_scandir( - uv_default_loop(), + req->getLoop(), &req->req, path, 0, diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp index 926302da1..1cc627998 100644 --- a/lute/io/src/io.cpp +++ b/lute/io/src/io.cpp @@ -82,7 +82,7 @@ static void onTtyRead(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) int read(lua_State* L) { auto handle = std::make_shared(); - handle->loop = uv_default_loop(); + handle->loop = getRuntimeLoop(L); handle->resumeToken = getResumeToken(L); handle->self = handle; diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 6314674b6..498d024fc 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -530,7 +530,7 @@ bool closeServer(int serverId) int lua_serve(lua_State* L) { - uWS::Loop::get(uv_default_loop()); + uWS::Loop::get(getRuntimeLoop(L)); std::string hostname = "127.0.0.1"; int port = 3000; diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 701513fca..4290bfe7f 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -157,7 +157,7 @@ struct ProcessOptions std::string cwd; std::string stdioKind; std::map env; - std::string customShell; // only used by system() + std::string customShell; // only used by system() }; static void onProcessExit(uv_process_t* process, int64_t exitStatus, int termSignal) @@ -225,7 +225,7 @@ const std::string kStdioKindNone = "none"; int executionHelper(lua_State* L, std::vector args, ProcessOptions opts) { auto handle = std::make_shared(); - handle->loop = uv_default_loop(); + handle->loop = getRuntimeLoop(L); handle->self = handle; uv_process_options_t options = {}; @@ -363,7 +363,7 @@ ProcessOptions parseOptions(lua_State* L, int index) lua_getfield(L, index, "cwd"); if (!lua_isnil(L, -1)) { - opts.cwd = luaL_checkstring(L,-1); + opts.cwd = luaL_checkstring(L, -1); } lua_pop(L, 1); @@ -436,13 +436,13 @@ int system(lua_State* L) ProcessOptions opts = parseOptions(L, 2); #ifdef _WIN32 - const char* shellVar = "COMSPEC"; - const char* shellFallback = "cmd.exe"; - const char* shellArg = "/c"; + const char* shellVar = "COMSPEC"; + const char* shellFallback = "cmd.exe"; + const char* shellArg = "/c"; #else - const char* shellVar = "SHELL"; - const char* shellFallback = "/bin/sh"; - const char* shellArg = "-c"; + const char* shellVar = "SHELL"; + const char* shellFallback = "/bin/sh"; + const char* shellArg = "-c"; #endif std::string resolvedShell; @@ -458,7 +458,7 @@ int system(lua_State* L) resolvedShell = opts.customShell; } - return executionHelper(L, { resolvedShell, shellArg, command }, opts); + return executionHelper(L, {resolvedShell, shellArg, command}, opts); } int homedir(lua_State* L) diff --git a/lute/runtime/include/lute/UVRequest.h b/lute/runtime/include/lute/UVRequest.h index a8006b6c1..d86e2d1e1 100644 --- a/lute/runtime/include/lute/UVRequest.h +++ b/lute/runtime/include/lute/UVRequest.h @@ -30,6 +30,7 @@ struct UVRequest { UVRequest(lua_State* L) : token(getResumeToken(L)) + , loop(getRuntimeLoop(L)) { req.data = this; } @@ -68,8 +69,14 @@ struct UVRequest cleanup_uv_req(&req); } + uv_loop_t* getLoop() + { + return loop; + } + ResumeToken token; ReqT req; + uv_loop_t* loop; }; diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index b3635d3bc..daf05c2cf 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -5,6 +5,8 @@ #include "Luau/Variant.h" #include "Luau/VecDeque.h" +#include "uv.h" + #include #include #include @@ -72,6 +74,8 @@ struct Runtime void addPendingToken(); void releasePendingToken(); + uv_loop_t* getEventLoop(); + // VM for this runtime std::unique_ptr globalState; @@ -93,9 +97,11 @@ struct Runtime std::thread runLoopThread; std::atomic activeTokens; + uv_loop_t eventLoop; }; Runtime* getRuntime(lua_State* L); +uv_loop_t* getRuntimeLoop(lua_State* L); struct ResumeTokenData; using ResumeToken = std::shared_ptr; diff --git a/lute/runtime/include/lute/uvutils.h b/lute/runtime/include/lute/uvutils.h index 32f81455f..905be2dbe 100644 --- a/lute/runtime/include/lute/uvutils.h +++ b/lute/runtime/include/lute/uvutils.h @@ -5,6 +5,8 @@ #include #include +#define LUTE_ASSERT(expr) LUAU_ASSERT(expr) + namespace uvutils { diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 5d5de2136..9240f91e2 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -1,5 +1,8 @@ #include "lute/runtime.h" +#include "lute/uvutils.h" + +#include "Luau/Common.h" #include "Luau/Require.h" #include "lua.h" @@ -20,8 +23,14 @@ Runtime::Runtime() : globalState(nullptr, lua_close_checked) , dataCopy(nullptr, lua_close_checked) { + stop.store(false); activeTokens.store(0); + + if (uv_loop_init(&eventLoop) < 0) + { + LUTE_ASSERT("Couldn't initialize runtime event loop"); + } } Runtime::~Runtime() @@ -36,6 +45,9 @@ Runtime::~Runtime() if (runLoopThread.joinable()) runLoopThread.join(); + // At this point, Runtime::hasWork will have returned false (i.e uv_loop_alive is false) + // This means there are no outstanding handles, or file descriptors or work, to do, and we can exit + uv_loop_close(&eventLoop); } bool Runtime::hasWork() @@ -44,13 +56,13 @@ bool Runtime::hasWork() // Unfortunately, we do currently have some places where we add/release // tokens that don't correspond to libuv activity, so for now we keep both. // uv_ref/unref could be used to patch tokens into the libuv loop itself. - return hasContinuations() || hasThreads() || activeTokens.load() != 0 || uv_loop_alive(uv_default_loop()); + return hasContinuations() || hasThreads() || activeTokens.load() != 0 || uv_loop_alive(getEventLoop()); } RuntimeStep Runtime::runOnce() { uv_run_mode mode = (hasContinuations() || hasThreads()) ? UV_RUN_NOWAIT : UV_RUN_ONCE; - uv_run(uv_default_loop(), mode); + uv_run(getEventLoop(), mode); // Complete all C++ continuations std::vector> copy; @@ -234,8 +246,7 @@ void Runtime::scheduleLuauResume(std::shared_ptr ref, std::function f) { - auto loop = uv_default_loop(); - + auto loop = getEventLoop(); uv_work_t* work = new uv_work_t(); work->data = new decltype(f)(std::move(f)); @@ -267,11 +278,21 @@ void Runtime::releasePendingToken() assert(before > 0); } +uv_loop_t* Runtime::getEventLoop() +{ + return &eventLoop; +} + Runtime* getRuntime(lua_State* L) { return static_cast(lua_getthreaddata(lua_mainthread(L))); } +uv_loop_t* getRuntimeLoop(lua_State* L) +{ + return getRuntime(L)->getEventLoop(); +} + void ResumeTokenData::fail(std::string error) { assert(!completed); diff --git a/lute/task/src/task.cpp b/lute/task/src/task.cpp index 23e1c8658..30e07b9f7 100644 --- a/lute/task/src/task.cpp +++ b/lute/task/src/task.cpp @@ -47,10 +47,10 @@ struct WaitData static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaTimeOnStack, int nargs) { WaitData* yield = new WaitData(); - uv_timer_init(uv_default_loop(), &yield->uvTimer); + uv_timer_init(getRuntimeLoop(L), &yield->uvTimer); yield->resumptionToken = getResumeToken(L); - yield->startedAtMs = uv_now(uv_default_loop()); + yield->startedAtMs = uv_now(getRuntimeLoop(L)); yield->uvTimer.data = yield; yield->putDeltaTimeOnStack = putDeltaTimeOnStack; yield->nargs = nargs; @@ -67,12 +67,16 @@ static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaT int stackReturnAmount = yield->putDeltaTimeOnStack ? yield->nargs + 1 : yield->nargs; if (yield->putDeltaTimeOnStack) - lua_pushnumber(L, static_cast(uv_now(uv_default_loop()) - yield->startedAtMs) / 1000.0); - - uv_close(reinterpret_cast(&yield->uvTimer), [](uv_handle_t* handle) { - WaitData* yield = static_cast(handle->data); - delete yield; - }); + lua_pushnumber(L, static_cast(uv_now(getRuntimeLoop(L)) - yield->startedAtMs) / 1000.0); + + uv_close( + reinterpret_cast(&yield->uvTimer), + [](uv_handle_t* handle) + { + WaitData* yield = static_cast(handle->data); + delete yield; + } + ); return stackReturnAmount; } ); From acf35e970bfa61d673458f764afc8775beb9a708 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 3 Feb 2026 16:18:37 -0800 Subject: [PATCH 320/642] Removes unused step from release CI (#782) Noticed there was an unused step. Also did some reformatting to be more consistent with the rest of the file --- .github/workflows/release.yml | 47 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23dffaa6d..1d1314776 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Build and Release on: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" workflow_dispatch: inputs: nightly: @@ -19,7 +19,7 @@ jobs: nightly_tag: ${{ steps.set_nightly.outputs.nightly_tag }} permissions: contents: read - + steps: - name: Validate CI status run: | @@ -98,27 +98,28 @@ jobs: runs-on: ${{ matrix.os }} steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set nightly version suffix - if: needs.check.outputs.nightly_tag - run: echo "LUTE_VERSION_SUFFIX=${{ needs.check.outputs.nightly_tag }}" >> "$GITHUB_ENV" - - - name: Setup and Build Lute - id: build_lute - uses: ./.github/actions/setup-and-build - with: - target: Lute.CLI - config: release - options: ${{ matrix.options }} - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - path: ${{ steps.build_lute.outputs.exe_path }} - name: ${{ matrix.artifact_name }} + - name: Checkout code + uses: actions/checkout@v4 + + # Used for lute --version + - name: Set nightly version suffix + if: needs.check.outputs.nightly_tag + run: echo "LUTE_VERSION_SUFFIX=${{ needs.check.outputs.nightly_tag }}" >> "$GITHUB_ENV" + + - name: Setup and Build Lute + id: build_lute + uses: ./.github/actions/setup-and-build + with: + target: Lute.CLI + config: release + options: ${{ matrix.options }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + path: ${{ steps.build_lute.outputs.exe_path }} + name: ${{ matrix.artifact_name }} release: needs: [check, build] From 9561ac40808fc53595676ae887b9faa933ff9dbb Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:27:17 -0800 Subject: [PATCH 321/642] Adds tests to TypeSerializer pt 1 (#778) (following up from https://github.com/luau-lang/lute/pull/769) tests tests tests for `TypeSerializer` and adds error handling for types that we shouldn't serialize, so all overrides are implemented now there will be a pt 2 for: FunctionType, TableType, MetatableType, ExternType, TypePack, VariadicTypePack, GenericTypePack tests --- lute/luau/src/type.cpp | 90 +++++++++ tests/CMakeLists.txt | 4 + tests/src/typeserializefixture.cpp | 15 ++ tests/src/typeserializefixture.h | 15 ++ tests/src/typeserializer.test.cpp | 311 +++++++++++++++++++++++++++++ 5 files changed, 435 insertions(+) create mode 100644 tests/src/typeserializefixture.cpp create mode 100644 tests/src/typeserializefixture.h create mode 100644 tests/src/typeserializer.test.cpp diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp index 37d1cacc0..33e612e7b 100644 --- a/lute/luau/src/type.cpp +++ b/lute/luau/src/type.cpp @@ -509,6 +509,96 @@ struct TypeSerialize final : public Luau::TypeVisitor serialize(tp, gtp); return false; } + + // Non-Serialized Types + + bool visit(TypeId ty) override + { + // NOTE: `TypeSerialize` should explicitly visit _all_ types and type packs, + // otherwise it's prone to serializing types that should not be serialized. + LUAU_ASSERT(false); + LUAU_UNREACHABLE(); + } + + bool visit(TypeId ty, const BoundType& btv) override + { + luaL_error(L, "TypeSerialize: cannot serialize BoundType"); + return false; + } + + bool visit(TypeId ty, const FreeType& ftv) override + { + luaL_error(L, "TypeSerialize: cannot serialize FreeType"); + return false; + } + + bool visit(TypeId ty, const ErrorType& etv) override + { + luaL_error(L, "TypeSerialize: cannot serialize ErrorType"); + return false; + } + + bool visit(TypeId ty, const NoRefineType& nrt) override + { + luaL_error(L, "TypeSerialize: cannot serialize NoRefineType"); + return false; + } + + bool visit(TypeId ty, const BlockedType& btv) override + { + luaL_error(L, "TypeSerialize: cannot serialize BlockedType"); + return false; + } + + bool visit(TypeId ty, const PendingExpansionType& petv) override + { + luaL_error(L, "TypeSerialize: cannot serialize PendingExpansionType"); + return false; + } + + bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override + { + luaL_error(L, "TypeSerialize: cannot serialize TypeFunctionInstanceType"); + return false; + } + + bool visit(TypePackId tp) override + { + // NOTE: `TypeSerialize` should explicitly visit _all_ types and type packs, + // otherwise it's prone to serializing type packs that should not be serialized. + LUAU_ASSERT(false); + LUAU_UNREACHABLE(); + } + + bool visit(TypePackId tp, const BoundTypePack& btp) override + { + luaL_error(L, "TypeSerialize: cannot serialize BoundTypePack"); + return false; + } + + bool visit(TypePackId tp, const FreeTypePack& ftp) override + { + luaL_error(L, "TypeSerialize: cannot serialize FreeTypePack"); + return false; + } + + bool visit(TypePackId tp, const ErrorTypePack& etp) override + { + luaL_error(L, "TypeSerialize: cannot serialize ErrorTypePack"); + return false; + } + + bool visit(TypePackId tp, const BlockedTypePack& btp) override + { + luaL_error(L, "TypeSerialize: cannot serialize BlockedTypePack"); + return false; + } + + bool visit(TypePackId tp, const TypeFunctionInstanceTypePack& tfitp) override + { + luaL_error(L, "TypeSerialize: cannot serialize TypeFunctionInstanceTypePack"); + return false; + } }; int serializeType(lua_State* L, TypeId ty) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cdc6d2dce..c820c42bd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,6 +15,9 @@ target_sources(Lute.Test PRIVATE src/luteprojectroot.cpp src/testreporter.cpp + src/typeserializefixture.h + src/typeserializefixture.cpp + # Test files src/compile.test.cpp src/configresolver.test.cpp @@ -23,6 +26,7 @@ target_sources(Lute.Test PRIVATE src/moduleresolver.test.cpp src/packagerequire.test.cpp src/require.test.cpp + src/typeserializer.test.cpp src/staticrequires.test.cpp src/stdsystem.test.cpp src/typecheck.test.cpp) diff --git a/tests/src/typeserializefixture.cpp b/tests/src/typeserializefixture.cpp new file mode 100644 index 000000000..db4707337 --- /dev/null +++ b/tests/src/typeserializefixture.cpp @@ -0,0 +1,15 @@ +#include "typeserializefixture.h" + +#include "lua.h" +#include "lualib.h" + +TypeSerializeFixture::TypeSerializeFixture() +{ + L = luaL_newstate(); + luaL_openlibs(L); +} + +TypeSerializeFixture::~TypeSerializeFixture() +{ + lua_close(L); +} \ No newline at end of file diff --git a/tests/src/typeserializefixture.h b/tests/src/typeserializefixture.h new file mode 100644 index 000000000..a871a237e --- /dev/null +++ b/tests/src/typeserializefixture.h @@ -0,0 +1,15 @@ +#pragma once + +#include "lutefixture.h" +#include "lua.h" +#include "Luau/TypeArena.h" + +class TypeSerializeFixture : public LuteFixture +{ +public: + TypeSerializeFixture(); + ~TypeSerializeFixture(); + + lua_State* L = nullptr; + Luau::TypeArena arena; +}; \ No newline at end of file diff --git a/tests/src/typeserializer.test.cpp b/tests/src/typeserializer.test.cpp new file mode 100644 index 000000000..84a55bc98 --- /dev/null +++ b/tests/src/typeserializer.test.cpp @@ -0,0 +1,311 @@ +#include "typeserializefixture.h" + +#include "doctest.h" + +#include "lute/type.h" + +#include "Luau/TypeArena.h" +#include "Luau/TypeFunction.h" + +using namespace Luau; + +static void requireStringField(lua_State* L, const char* field, const char* expected) +{ + lua_getfield(L, -1, field); + REQUIRE(lua_isstring(L, -1)); + CHECK(std::string(lua_tostring(L, -1)) == expected); + lua_pop(L, 1); // string field +} + +static void requireBoolField(lua_State* L, const char* field, bool expected) +{ + lua_getfield(L, -1, field); + REQUIRE(lua_isboolean(L, -1)); + CHECK(lua_toboolean(L, -1) == expected); + lua_pop(L, 1); // bool field +} + +// Primitive Types + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_nil_type") +{ + TypeId ty = arena.addType(PrimitiveType{PrimitiveType::NilType}); + + // We need to first ensure we have enough stack space to serialize. + // The stack size corresponds to the max depth needed (see lute/type.cpp). + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "nil"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_boolean_type") +{ + TypeId ty = arena.addType(PrimitiveType{PrimitiveType::Boolean}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "boolean"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_number_type") +{ + TypeId ty = arena.addType(PrimitiveType{PrimitiveType::Number}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "number"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_string_type") +{ + TypeId ty = arena.addType(PrimitiveType{PrimitiveType::String}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "string"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_thread_type") +{ + TypeId ty = arena.addType(PrimitiveType{PrimitiveType::Thread}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "thread"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_buffer_type") +{ + TypeId ty = arena.addType(PrimitiveType{PrimitiveType::Buffer}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "buffer"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_any_type") +{ + TypeId ty = arena.addType(AnyType{}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "any"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_unknown_type") +{ + TypeId ty = arena.addType(UnknownType{}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "unknown"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_never_type") +{ + TypeId ty = arena.addType(NeverType{}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "never"); +} + +// Singleton Types + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_string_singleton") +{ + TypeId ty = arena.addType(SingletonType{StringSingleton{"hello"}}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "singleton"); + requireStringField(L, "value", "hello"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_boolean_singleton") +{ + TypeId ty = arena.addType(SingletonType{BooleanSingleton{true}}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "singleton"); + requireBoolField(L, "value", true); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_negation_type") +{ + TypeId ty = arena.addType(NegationType{arena.addType(PrimitiveType{PrimitiveType::Number})}); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "negation"); + + lua_getfield(L, -1, "inner"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "number"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_union_type") +{ + TypeId numberTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + TypeId stringTy = arena.addType(PrimitiveType{PrimitiveType::String}); + + TypeId unionTy = arena.addType(UnionType{{numberTy, stringTy}}); + + lua_checkstack(L, 3); + REQUIRE_EQ(Luau::serializeType(L, unionTy), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "union"); + + lua_getfield(L, -1, "components"); + REQUIRE(lua_istable(L, -1)); + + lua_rawgeti(L, -1, 1); + requireStringField(L, "tag", "number"); + lua_pop(L, 1); + + lua_rawgeti(L, -1, 2); + requireStringField(L, "tag", "string"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_intersection_type") +{ + TypeId numberTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + TypeId stringTy = arena.addType(PrimitiveType{PrimitiveType::String}); + + TypeId intersectionTy = arena.addType(IntersectionType{{numberTy, stringTy}}); + + lua_checkstack(L, 3); // Ensure enough stack for serialization. This would be size 3 + REQUIRE_EQ(Luau::serializeType(L, intersectionTy), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "intersection"); + + lua_getfield(L, -1, "components"); + REQUIRE(lua_istable(L, -1)); + + lua_rawgeti(L, -1, 1); + requireStringField(L, "tag", "number"); + lua_pop(L, 1); + + lua_rawgeti(L, -1, 2); + requireStringField(L, "tag", "string"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_generic_type") +{ + GenericType gtp; + gtp.name = "T"; + + TypeId ty = arena.addType(gtp); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "generic"); + requireStringField(L, "name", "T"); + requireBoolField(L, "ispack", false); +} + +// TODO: FunctionType, TableType, MetatableType, ExternType, TypePack, VariadicTypePack, GenericTypePack tests + +// Non-Serialized Types + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_free_type_should_error") +{ + TypeId ty = arena.addType(FreeType{TypeLevel{1}, nullptr, nullptr}); + + CHECK_THROWS_WITH_AS(Luau::serializeType(L, ty), "TypeSerialize: cannot serialize FreeType", std::exception); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_error_type_should_error") +{ + TypeId ty = arena.addType(ErrorType{}); + + CHECK_THROWS_WITH_AS(Luau::serializeType(L, ty), "TypeSerialize: cannot serialize ErrorType", std::exception); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_norefine_type_should_error") +{ + TypeId ty = arena.addType(NoRefineType{}); + + CHECK_THROWS_WITH_AS(Luau::serializeType(L, ty), "TypeSerialize: cannot serialize NoRefineType", std::exception); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_blocked_type_should_error") +{ + TypeId ty = arena.addType(BlockedType{}); + + CHECK_THROWS_WITH_AS(Luau::serializeType(L, ty), "TypeSerialize: cannot serialize BlockedType", std::exception); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_pending_expansion_type_should_error") +{ + + TypeId ty = arena.addType(PendingExpansionType{std::nullopt, AstName("fakename"), {}, {}}); + + CHECK_THROWS_WITH_AS(Luau::serializeType(L, ty), "TypeSerialize: cannot serialize PendingExpansionType", std::exception); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_typefunctioninstance_type_should_error") +{ + TypeFunction user{"tf", nullptr}; + TypeFunctionInstanceType tftt{ + NotNull{&user}, + std::vector{}, // Type Function Arguments + {}, + {AstName{"fakename"}}, // Type Function Name + {}, + }; + TypeId ty = arena.addType(tftt); + + CHECK_THROWS_WITH_AS(Luau::serializeType(L, ty), "TypeSerialize: cannot serialize TypeFunctionInstanceType", std::exception); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_free_typepack_should_error") +{ + TypeLevel level = TypeLevel{1}; + TypePackId tp = arena.addTypePack(FreeTypePack{level}); + + CHECK_THROWS_WITH_AS(Luau::serializeTypePack(L, tp), "TypeSerialize: cannot serialize FreeTypePack", std::exception); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_error_typepack_should_error") +{ + TypePackId tp = arena.addTypePack(ErrorTypePack{}); + + CHECK_THROWS_WITH_AS(Luau::serializeTypePack(L, tp), "TypeSerialize: cannot serialize ErrorTypePack", std::exception); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_blocked_typepack_should_error") +{ + TypePackId tp = arena.addTypePack(BlockedTypePack{}); + + CHECK_THROWS_WITH_AS(Luau::serializeTypePack(L, tp), "TypeSerialize: cannot serialize BlockedTypePack", std::exception); +} From df96f331d7d1166313cca7d98a47e34c440e167d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:54:27 -0800 Subject: [PATCH 322/642] Update Luau to 0.707 (#774) **Luau**: Updated from `0.706` to `0.707` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.707 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- extern/luau.tune | 4 ++-- lute/luau/src/resolverequire.cpp | 13 +++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 171f248f8..c8f7864a5 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.706" -revision = "416fc1ae1f8bf017f1f75c11c50b5bf6645f2fd2" +branch = "0.707" +revision = "544d3fff6279841c85d101755933e361adcf7fdc" diff --git a/lute/luau/src/resolverequire.cpp b/lute/luau/src/resolverequire.cpp index 0c885d0c4..fef59cf5a 100644 --- a/lute/luau/src/resolverequire.cpp +++ b/lute/luau/src/resolverequire.cpp @@ -18,9 +18,7 @@ class FileVfsContext : public Luau::Require::NavigationContext public: FileVfsContext(std::string requirerChunkname); - std::string getRequirerIdentifier() const override; - - NavigateResult reset(const std::string& identifier) override; + NavigateResult resetToRequirer() override; NavigateResult jumpToAlias(const std::string& path) override; NavigateResult toParent() override; @@ -82,14 +80,9 @@ FileVfsContext::FileVfsContext(std::string requirerChunkname) { } -std::string FileVfsContext::getRequirerIdentifier() const -{ - return requirerChunkname; -} - -NC::NavigateResult FileVfsContext::reset(const std::string& identifier) +NC::NavigateResult FileVfsContext::resetToRequirer() { - return convert(vfs.resetToPath(identifier)); + return convert(vfs.resetToPath(requirerChunkname)); } NC::NavigateResult FileVfsContext::jumpToAlias(const std::string& path) From 6d3b9960e637edb0c19639871d1aa5fabd5030d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:54:37 -0800 Subject: [PATCH 323/642] Update Lute to 0.1.0-nightly.20260130 (#775) **Lute**: Updated from `0.1.0-nightly.20260123` to `0.1.0-nightly.20260130` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260130 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index ea214a2df..231e43482 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260123" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260130" } diff --git a/rokit.toml b/rokit.toml index 9be996fee..901083a89 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20260123" +lute = "luau-lang/lute@0.1.0-nightly.20260130" From 4c0f2dbe1baa0afb2e76fd606ed7d637dcc4b612 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:15:25 -0800 Subject: [PATCH 324/642] Fixes std/path metatable type errors (#785) Fixes a type error in path lib's `posix` and `win32` files (came up when I was trying to serialize the type of these modules), thx for helping fix @skanosue ! --- lute/std/libs/path/pathinterface.luau | 2 +- lute/std/libs/path/posix/init.luau | 3 ++- lute/std/libs/path/win32/init.luau | 3 ++- tools/check-faillist.txt | 4 ---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lute/std/libs/path/pathinterface.luau b/lute/std/libs/path/pathinterface.luau index 4bd143b01..430a1c71a 100644 --- a/lute/std/libs/path/pathinterface.luau +++ b/lute/std/libs/path/pathinterface.luau @@ -1,5 +1,5 @@ export type pathinterface = { - __tostring: () -> string, + __tostring: (self: pathinterface) -> string, } return table.freeze({}) diff --git a/lute/std/libs/path/posix/init.luau b/lute/std/libs/path/posix/init.luau index 0652b0b64..47e63c3f2 100644 --- a/lute/std/libs/path/posix/init.luau +++ b/lute/std/libs/path/posix/init.luau @@ -1,11 +1,12 @@ local process = require("@lute/process") local posixtypes = require("@self/types") +local pathinterface = require("./pathinterface") export type path = posixtypes.path export type pathlike = posixtypes.pathlike local posix = {} -posix.pathmt = {} :: posixtypes.pathMT +posix.pathmt = {} :: pathinterface.pathinterface function posix.basename(path: pathlike): string? path = if typeof(path) == "string" then posix.parse(path) else path diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index abf723d4f..10f1e5a3d 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -1,12 +1,13 @@ local process = require("@lute/process") local win32types = require("@self/types") +local pathinterface = require("./pathinterface") export type pathkind = win32types.pathkind export type path = win32types.path export type pathlike = win32types.pathlike local win32 = {} -win32.pathmt = {} :: win32types.pathMT +win32.pathmt = {} :: pathinterface.pathinterface function win32.basename(path: pathlike): string? path = if typeof(path) == "string" then win32.parse(path) else path diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 7cefcc362..6ba42f8b1 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -127,10 +127,6 @@ lute/std/libs/fs.luau:184:52-68 lute/std/libs/fs.luau:184:52-68 lute/std/libs/fs.luau:192:11-17 lute/std/libs/fs.luau:192:11-17 -lute/std/libs/path/posix/init.luau:8:22-38 -lute/std/libs/path/posix/init.luau:8:22-38 -lute/std/libs/path/win32/init.luau:9:22-38 -lute/std/libs/path/win32/init.luau:9:22-38 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:42:25-29 From 64c44a875fa174ea47dd966337a8db8051c8d326 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:32:24 +0000 Subject: [PATCH 325/642] Update Lute to 0.1.0-nightly.20260205 (#789) **Lute**: Updated from `0.1.0-nightly.20260130` to `0.1.0-nightly.20260205` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260205 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 231e43482..b4ea22895 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260130" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260205" } diff --git a/rokit.toml b/rokit.toml index 901083a89..59b255ac7 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20260130" +lute = "luau-lang/lute@0.1.0-nightly.20260205" From d2cbbaed81b9fc524b997d507380a4af68f67968 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:40:01 +0000 Subject: [PATCH 326/642] Update Luau to 0.708 (#788) **Luau**: Updated from `0.707` to `0.708` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.708 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> --- extern/luau.tune | 4 ++-- tools/check-faillist.txt | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index c8f7864a5..c0af44e1b 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.707" -revision = "544d3fff6279841c85d101755933e361adcf7fdc" +branch = "0.708" +revision = "54a2ea00831df4c791e6cfc896a98da75d1ae126" diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 6ba42f8b1..cd95ba3a5 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -9,11 +9,7 @@ batteries/difftext/printdiff.luau:74:10-73 batteries/pp.luau:97:3-14 batteries/toml.luau:27:14-16 batteries/toml.luau:40:14-16 -batteries/toml.luau:45:17-39 batteries/toml.luau:45:36-38 -batteries/toml.luau:60:15-33 -batteries/toml.luau:68:14-32 -batteries/toml.luau:75:2-29 batteries/toml.luau:75:26-28 batteries/toml.luau:133:33-41 batteries/toml.luau:143:23-25 @@ -539,7 +535,6 @@ tests/std/test.test.luau:282:4-9 tests/std/test.test.luau:299:4-9 tests/std/test.test.luau:318:4-9 tests/std/test.test.luau:337:4-9 -tools/luthier.luau:139:10-34 tools/luthier.luau:162:2-26 tools/luthier.luau:201:3-12 tools/luthier.luau:202:11-25 From 146a5db49969474b81036a478b2aacbbdbabd874 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:35:58 -0800 Subject: [PATCH 327/642] Try to fix `ubuntu-22.04-arm` release by bootstrapping builds (#791) Resolves https://github.com/luau-lang/lute/issues/790. In a fresh Docker container with GCC-11 (installed with `build-essentials`), we cannot run any release binaries going back several months. We believe that GitHub was silently supporting GCC-13 (more relevant: `GLIBCXX_3.4.31` and `GLIBCXX_3.4.32`) until a few days ago. They seem to have removed support recently, so our `ubuntu-22.04-arm` runner can't run binaries that we released in the past. To fix this, we'll bootstrap this build. --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d1314776..26f09ef4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,6 +87,7 @@ jobs: - os: ubuntu-22.04-arm artifact_name: lute-linux-aarch64 options: --c-compiler clang --cxx-compiler clang++ + use-bootstrap: true - os: macos-latest artifact_name: lute-macos-aarch64 - os: windows-latest @@ -114,6 +115,7 @@ jobs: config: release options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} + use-bootstrap: ${{ matrix.use-bootstrap || 'false' }} - name: Upload Artifact uses: actions/upload-artifact@v4 From 7e9a8ccda4a26afefd407a712eaa048ba5a5cf5b Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:25:16 -0800 Subject: [PATCH 328/642] Infra: fix bootstrap builds not using bootstrapped Lute (#793) Found another bug in our bootstrapping logic: we bootstrap two builds and then use the Foreman-installed Lute for the third build. Aliases only live for a single shell instance, so they don't persist between steps. We no longer invoke Foreman if bootstrapping, and we use a `$LUTE` environment variable to point to the correct executable. Context: broken release build successfully built the first two but failed to build the third since it was calling into Foreman's binary (https://github.com/luau-lang/lute/actions/runs/21840297510/job/63022500151). --- .github/actions/setup-and-build/action.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index ecfc89091..cbb340201 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -30,6 +30,7 @@ runs: using: "composite" steps: - name: Install tools + if: ${{ inputs.use-bootstrap == 'false' }} uses: Roblox/setup-foreman@v3 with: token: ${{ inputs.token }} @@ -64,31 +65,34 @@ runs: path: extern key: extern-${{ hashFiles('extern/*.tune') }} - - name: Bootstrap Lute - if: ${{ inputs.use-bootstrap == 'true' }} + - name: Bootstrap Lute (if needed) and set LUTE shell: bash run: | - echo "Bootstrapping Lute..." - ./tools/bootstrap.sh - alias lute="./build/lute0" + if [[ "${{ inputs.use-bootstrap }}" == "true" ]]; then + echo "Bootstrapping Lute..." + ./tools/bootstrap.sh + echo "LUTE=./build/lute0" >> $GITHUB_ENV + else + echo "LUTE=lute" >> $GITHUB_ENV + fi - name: Fetch ${{ inputs.target }} if: steps.cache-extern.outputs.cache-hit != 'true' shell: bash - run: lute ./tools/luthier.luau fetch ${{ inputs.target }} + run: $LUTE ./tools/luthier.luau fetch ${{ inputs.target }} - name: Configure ${{ inputs.target }} shell: bash - run: lute ./tools/luthier.luau configure ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} + run: $LUTE ./tools/luthier.luau configure ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} - name: Build ${{ inputs.target }} shell: bash - run: lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} + run: $LUTE ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} ${{ inputs.options }} - name: Get Artifact Path id: artifact_path shell: bash run: | - exe_path=$(lute ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} --which) + exe_path=$($LUTE ./tools/luthier.luau build ${{ inputs.target }} --config ${{ inputs.config }} --which) echo "Executable path: $exe_path" # Debug print echo "exe_path=$exe_path" >> "$GITHUB_OUTPUT" From b8f00e1a44464745012d0957682686c111ebdc2e Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:13:10 -0800 Subject: [PATCH 329/642] Adds handling cyclic type references in `TypeSerializer` + tests (#787) More follow up on `TypeSerializer` improvements. This PR creates a reference table in `TypeSerializer` to handle cyclic & shared type references, since Luau types can contain cycles (e.g., `type Node = { value: number, next: Node }` ) When we're serializing the types to luau tables, we need to detect and handle these cycles to avoid infinite recursion or missing serializations. To do this, I think we can maintain a mapping of each `TypeId`/`TypePackId` to its serialized table. So when we encounter a type we've already serialized, detected by the `cycle` functions provided by the `Luau::TypeVisitor`, we retrieve and reuse the existing table instead of creating a new one, preserving the cyclic structure in the serialization. Also adds test case for this cycle, as well as `FunctionType`, and `TableType` tests --- lute/luau/src/type.cpp | 76 +++++++++++ tests/src/typeserializer.test.cpp | 206 +++++++++++++++++++++++++++++- 2 files changed, 281 insertions(+), 1 deletion(-) diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp index 33e612e7b..037683c8d 100644 --- a/lute/luau/src/type.cpp +++ b/lute/luau/src/type.cpp @@ -1,5 +1,6 @@ #include "lute/type.h" +#include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeFwd.h" #include "Luau/VisitType.h" @@ -20,11 +21,67 @@ static void checkStack(lua_State* L, int n) struct TypeSerialize final : public Luau::TypeVisitor { lua_State* L; + int refsTableIndex; // Reference table for handling cyclic & shared type references. Maps TypeId/TypePackId (as lightuserdata) to the corresponding serialized table on the stack. explicit TypeSerialize(lua_State* L) : Luau::TypeVisitor{"TypeSerialize", /*skipBoundTypes=*/false} , L(L) { + // Create the refs table and store a reference to it. This table will be used to track already serialized types to handle cycles and shared references. + lua_createtable(L, 0, 0); + refsTableIndex = lua_gettop(L); + } + + ~TypeSerialize() + { + // Pop the refs table from the stack when the serializer is destroyed + if (refsTableIndex > 0 && lua_gettop(L) >= refsTableIndex) + lua_remove(L, refsTableIndex); + else + luaL_error(L, "TypeSerialize: reference table index corrupted during serialization"); + } + + void cycle(TypeId ty) override + { + checkStack(L, 1); + lua_pushlightuserdata(L, (void*)ty); + lua_gettable(L, refsTableIndex); + + if (lua_isnil(L, -1)) + { + luaL_error(L, "TypeSerialize: cycle detected while serializing type, but TypeId %s was not found in refs table", Luau::toString(ty).data()); + } + } + + void cycle(TypePackId tp) override + { + checkStack(L, 1); + lua_pushlightuserdata(L, (void*)tp); + lua_gettable(L, refsTableIndex); + + if (lua_isnil(L, -1)) + { + luaL_error(L, "TypeSerialize: cycle detected while serializing type pack, but TypePackId %s was not found in refs table", Luau::toString(tp).data()); + } + } + + // Register this type's serialized table in the refs table, using the TypeId as the key. + // Called after creating the serialized table for a type, but before traversing any child types. + void registerType(TypeId ty) + { + checkStack(L, 3); + lua_pushlightuserdata(L, (void*)ty); // TypeId pointer as key + lua_pushvalue(L, -2); // Reference to the serialized type table as value + lua_rawset(L, refsTableIndex); // refs[ty] = serializedTable + } + + // Similar to registerType but for TypePackId + void registerTypePack(TypePackId tp) + { + checkStack(L, 3); + lua_pushlightuserdata(L, (void*)tp); + lua_pushvalue(L, -2); + lua_rawset(L, refsTableIndex); } // Helper to push the "tag" field that every Luau type has @@ -77,6 +134,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); // 1 for table + 1 for space to push/set remaining fields lua_createtable(L, 0, 1); + registerType(ty); const char* tag = nullptr; switch (ptv.type) @@ -110,6 +168,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 1); + registerType(ty); pushTag("any"); } @@ -118,6 +177,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 1); + registerType(ty); pushTag("unknown"); } @@ -126,6 +186,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 1); + registerType(ty); pushTag("never"); } @@ -136,6 +197,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 2); + registerType(ty); pushTag("singleton"); @@ -154,6 +216,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 2); + registerType(ty); pushTag("negation"); @@ -167,6 +230,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 3); // 1 for table + 1 for components table + 1 for traverse lua_createtable(L, 0, 2); + registerType(ty); pushTag("union"); @@ -185,6 +249,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 3); // 1 for table + 1 for components table + 1 for traverse lua_createtable(L, 0, 2); + registerType(ty); pushTag("intersection"); @@ -204,6 +269,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 3); + registerType(ty); pushTag("generic"); @@ -226,6 +292,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 3); // max 1 for table + 1 for subtable + 1 for traverse lua_createtable(L, 0, 5); + registerType(ty); pushTag("function"); @@ -265,6 +332,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 3); + registerType(ty); pushTag("table"); @@ -284,6 +352,8 @@ struct TypeSerialize final : public Luau::TypeVisitor { lua_createtable(L, 0, 3); traverse(ttv.indexer->indexType); + lua_setfield(L, -2, "index"); + lua_setfield(L, -2, "indexer"); // TODO: implement readindexer and writeindexer from ttv.indexer->indexResultType @@ -302,6 +372,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 3); + registerType(ty); pushTag("metatable"); @@ -319,6 +390,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 4); + registerType(ty); pushTag("extern"); @@ -353,6 +425,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 3); // 1 for root table + 1 for subtable + 1 for traverse lua_createtable(L, 0, 2); + registerTypePack(tp); // Head if (!pack.head.empty()) @@ -383,6 +456,7 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 2); + registerTypePack(tp); // Head is nil lua_pushnil(L); @@ -400,6 +474,8 @@ struct TypeSerialize final : public Luau::TypeVisitor { checkStack(L, 2); lua_createtable(L, 0, 3); + registerTypePack(tp); + pushTag("generic"); if (!gtp.name.empty()) diff --git a/tests/src/typeserializer.test.cpp b/tests/src/typeserializer.test.cpp index 84a55bc98..62892e62a 100644 --- a/tests/src/typeserializer.test.cpp +++ b/tests/src/typeserializer.test.cpp @@ -34,6 +34,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_nil_type") // We need to first ensure we have enough stack space to serialize. // The stack size corresponds to the max depth needed (see lute/type.cpp). lua_checkstack(L, 2); + + // { tag: "nil" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -45,6 +47,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_boolean_type") TypeId ty = arena.addType(PrimitiveType{PrimitiveType::Boolean}); lua_checkstack(L, 2); + + // { tag: "boolean" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -56,6 +60,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_number_type") TypeId ty = arena.addType(PrimitiveType{PrimitiveType::Number}); lua_checkstack(L, 2); + + // { tag: "number" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -67,6 +73,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_string_type") TypeId ty = arena.addType(PrimitiveType{PrimitiveType::String}); lua_checkstack(L, 2); + + // { tag: "string" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -78,6 +86,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_thread_type") TypeId ty = arena.addType(PrimitiveType{PrimitiveType::Thread}); lua_checkstack(L, 2); + + // { tag: "thread" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -89,6 +99,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_primitive_buffer_type") TypeId ty = arena.addType(PrimitiveType{PrimitiveType::Buffer}); lua_checkstack(L, 2); + + // { tag: "buffer" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -100,6 +112,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_any_type") TypeId ty = arena.addType(AnyType{}); lua_checkstack(L, 2); + + // { tag: "any" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -111,6 +125,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_unknown_type") TypeId ty = arena.addType(UnknownType{}); lua_checkstack(L, 2); + + // { tag: "unknown" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -122,6 +138,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_never_type") TypeId ty = arena.addType(NeverType{}); lua_checkstack(L, 2); + + // { tag: "never" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -135,6 +153,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_string_singleton") TypeId ty = arena.addType(SingletonType{StringSingleton{"hello"}}); lua_checkstack(L, 2); + + // { tag: "singleton", value: "hello" } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -147,6 +167,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_boolean_singleton") TypeId ty = arena.addType(SingletonType{BooleanSingleton{true}}); lua_checkstack(L, 2); + + // { tag: "singleton", value: true } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -159,6 +181,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_negation_type") TypeId ty = arena.addType(NegationType{arena.addType(PrimitiveType{PrimitiveType::Number})}); lua_checkstack(L, 2); + + // { tag: "negation", inner: { tag: "number" } } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -177,6 +201,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_union_type") TypeId unionTy = arena.addType(UnionType{{numberTy, stringTy}}); lua_checkstack(L, 3); + + // { tag: "union", components: { { tag: "number" }, { tag: "string" } } } REQUIRE_EQ(Luau::serializeType(L, unionTy), 1); REQUIRE(lua_istable(L, -1)); @@ -201,6 +227,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_intersection_type") TypeId intersectionTy = arena.addType(IntersectionType{{numberTy, stringTy}}); lua_checkstack(L, 3); // Ensure enough stack for serialization. This would be size 3 + + // { tag: "intersection", components: { { tag: "number" }, { tag: "string" } } } REQUIRE_EQ(Luau::serializeType(L, intersectionTy), 1); REQUIRE(lua_istable(L, -1)); @@ -225,6 +253,8 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_generic_type") TypeId ty = arena.addType(gtp); lua_checkstack(L, 2); + + // { tag: "generic", name: "T", ispack: false } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -233,7 +263,181 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_generic_type") requireBoolField(L, "ispack", false); } -// TODO: FunctionType, TableType, MetatableType, ExternType, TypePack, VariadicTypePack, GenericTypePack tests +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_function_type") +{ + GenericType gtp; + gtp.name = "T"; + TypeId genericTy = arena.addType(gtp); + TypeId numberTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + + TypePackId argTypes = arena.addTypePack(TypePack{{numberTy}, std::nullopt}); + TypePackId retTypes = arena.addTypePack(TypePack{{}, std::nullopt}); + + FunctionType ftv{{genericTy}, {}, argTypes, retTypes}; + TypeId ty = arena.addType(ftv); + + lua_checkstack(L, 3); + + // (number) -> () + // { tag: "function", parameters: { head: { { tag: "number" } }, tail: nil }, returns: { head: nil, tail: nil }, generics: { { tag: "generic", name: "T", ispack: false } }, genericpacks: {} } + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "function"); + + lua_getfield(L, -1, "parameters"); + REQUIRE(lua_istable(L, -1)); + + lua_getfield(L, -1, "head"); + REQUIRE(lua_istable(L, -1)); + lua_rawgeti(L, -1, 1); + requireStringField(L, "tag", "number"); + lua_pop(L, 2); // head[0], head + + lua_getfield(L, -1, "tail"); + REQUIRE(lua_isnil(L, -1)); + lua_pop(L, 2); // tail, parameters + + lua_getfield(L, -1, "returns"); + REQUIRE(lua_istable(L, -1)); + lua_getfield(L, -1, "head"); + REQUIRE(lua_isnil(L, -1)); // no return types + lua_pop(L, 1); // head + lua_getfield(L, -1, "tail"); + REQUIRE(lua_isnil(L, -1)); // no return types + lua_pop(L, 2); // tail, returns + + lua_getfield(L, -1, "generics"); + REQUIRE(lua_istable(L, -1)); + lua_rawgeti(L, -1, 1); + requireStringField(L, "tag", "generic"); + requireStringField(L, "name", "T"); + requireBoolField(L, "ispack", false); + lua_pop(L, 2); // generics, function + + lua_getfield(L, -1, "genericpacks"); + REQUIRE(lua_istable(L, -1)); + REQUIRE(lua_objlen(L, -1) == 0); // no generic packs +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_table_type_with_properties") +{ + TypeId numberTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + TypeId stringTy = arena.addType(PrimitiveType{PrimitiveType::String}); + + TableType ttv; + Property prop; + prop.readTy = numberTy; + prop.writeTy = stringTy; + ttv.props["x"] = prop; + + TypeId ty = arena.addType(ttv); + + lua_checkstack(L, 3); + + // { read x: number, write x: string } + // { tag: "table", properties: { x: { read: { tag: "number" }, write: { tag: "string" } } } } + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "table"); + + lua_getfield(L, -1, "properties"); + REQUIRE(lua_istable(L, -1)); + + lua_getfield(L, -1, "x"); + REQUIRE(lua_istable(L, -1)); + + lua_getfield(L, -1, "read"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "number"); + lua_pop(L, 1); // read + + lua_getfield(L, -1, "write"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "string"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_table_type_with_nil_property") +{ + TypeId numberTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + + TableType ttv; + Property prop; + prop.readTy = numberTy; + prop.writeTy = std::nullopt; // write type is nil + ttv.props["x"] = prop; + + TypeId ty = arena.addType(ttv); + + lua_checkstack(L, 3); + + // { tag: "table", properties: { x: { read: { tag: "number" }, write: nil } } } + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "table"); + + lua_getfield(L, -1, "properties"); + REQUIRE(lua_istable(L, -1)); + + lua_getfield(L, -1, "x"); + REQUIRE(lua_istable(L, -1)); + + lua_getfield(L, -1, "read"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "number"); + lua_pop(L, 1); // read + + lua_getfield(L, -1, "write"); + REQUIRE(lua_isnil(L, -1)); // write type is nil +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_cyclic_table_type") +{ + // Create a cyclic type: type Node = { value: number, next: Node } + TypeId nodeType = arena.addType(TableType{}); + TableType* table = getMutable(nodeType); + + // Add the 'value' property + TypeId numberType = arena.addType(PrimitiveType{PrimitiveType::Number}); + table->props["value"] = Property::readonly(numberType); + + // Add the 'next' property that references itself (creating the cycle) + table->props["next"] = Property::readonly(nodeType); + + lua_checkstack(L, 3); + + // { tag: "table", properties: { value: { read: { tag: "number" } }, next: { read: } } } + REQUIRE_EQ(Luau::serializeType(L, nodeType), 1); + + REQUIRE(lua_istable(L, -1)); + int root = lua_absindex(L, -1); + + requireStringField(L, "tag", "table"); + + // Check properties field exists + lua_getfield(L, -1, "properties"); + REQUIRE(lua_istable(L, -1)); + + // Check 'value' property + lua_getfield(L, -1, "value"); + REQUIRE(lua_istable(L, -1)); + lua_getfield(L, -1, "read"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "number"); + lua_pop(L, 2); // pop read type and value property + + // Check 'next' property - this should be a cyclic reference + lua_getfield(L, -1, "next"); + REQUIRE(lua_istable(L, -1)); + lua_getfield(L, -1, "read"); + REQUIRE(lua_istable(L, -1)); + + REQUIRE(lua_equal(L, root, -1)); // The 'next' property's read type should be the same table as the root (cycle) +} + +// TODO: MetatableType, ExternType, TypePack, VariadicTypePack, GenericTypePack tests // Non-Serialized Types From ac7e7fb9a482ed41cfea54892a843af361a93952 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 9 Feb 2026 15:45:48 -0800 Subject: [PATCH 330/642] lute check doesn't need to check parser test examples (#795) --- tools/check-faillist.txt | 61 ---------------------------------------- tools/check.luau | 7 ++++- 2 files changed, 6 insertions(+), 62 deletions(-) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index cd95ba3a5..13566d7b0 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -243,67 +243,6 @@ tests/batteries/collections/deque.test.luau:91:13-16 tests/cli/check.test.luau:1:7-8 tests/cli/check.test.luau:4:7-12 tests/cli/test.test.luau:3:7-12 -tests/parserExamples/assignment-1.luau:1:1-1 -tests/parserExamples/assignment-1.luau:3:1-1 -tests/parserExamples/assignment-1.luau:3:4-4 -tests/parserExamples/assignment-1.luau:5:7-7 -tests/parserExamples/assignment-1.luau:5:13-13 -tests/parserExamples/assignment-1.luau:5:16-16 -tests/parserExamples/assignment-1.luau:5:23-23 -tests/parserExamples/assignment-1.luau:5:31-31 -tests/parserExamples/assignment-1.luau:5:38-38 -tests/parserExamples/assignment-1.luau:5:43-46 -tests/parserExamples/attributes-1.luau:2:16-18 -tests/parserExamples/attributes-1.luau:5:10-12 -tests/parserExamples/attributes-1.luau:7:7-7 -tests/parserExamples/function-declaration-1.luau:2:2-5 -tests/parserExamples/function-declaration-1.luau:5:10-12 -tests/parserExamples/function-declaration-2.luau:2:10-10 -tests/parserExamples/function-declaration-3.luau:1:10-10 -tests/parserExamples/function-declaration-3.luau:3:10-10 -tests/parserExamples/function-declaration-3.luau:5:10-10 -tests/parserExamples/function-declaration-3.luau:7:10-10 -tests/parserExamples/function-declaration-3.luau:9:10-10 -tests/parserExamples/function-declaration-4.luau:1:10-10 -tests/parserExamples/function-declaration-4.luau:3:10-10 -tests/parserExamples/function-declaration-4.luau:5:10-10 -tests/parserExamples/function-declaration-4.luau:7:1-24 -tests/parserExamples/function-declaration-4.luau:7:10-10 -tests/parserExamples/generic-for-loop-1.luau:1:27-30 -tests/parserExamples/generic-for-loop-1.luau:2:2-5 -tests/parserExamples/generic-for-loop-1.luau:5:27-30 -tests/parserExamples/generic-for-loop-1.luau:6:2-5 -tests/parserExamples/generic-for-loop-1.luau:9:21-24 -tests/parserExamples/generic-for-loop-1.luau:10:2-5 -tests/parserExamples/if-expression-1.luau:1:7-7 -tests/parserExamples/if-expression-1.luau:2:7-7 -tests/parserExamples/if-expression-2.luau:2:7-7 -tests/parserExamples/if-expression-2.luau:3:7-10 -tests/parserExamples/if-expression-2.luau:5:8-11 -tests/parserExamples/if-expression-2.luau:6:8-11 -tests/parserExamples/interpolated-string-1.luau:1:7-7 -tests/parserExamples/interpolated-string-1.luau:2:7-7 -tests/parserExamples/interpolated-string-1.luau:3:7-7 -tests/parserExamples/interpolated-string-1.luau:4:7-7 -tests/parserExamples/interpolated-string-2.luau:2:7-7 -tests/parserExamples/interpolated-string-2.luau:7:9-15 -tests/parserExamples/interpolated-string-2.luau:7:24-30 -tests/parserExamples/local-assignment-1.luau:1:7-7 -tests/parserExamples/local-assignment-1.luau:2:7-7 -tests/parserExamples/local-assignment-1.luau:2:10-10 -tests/parserExamples/local-assignment-1.luau:3:7-7 -tests/parserExamples/local-function-declaration-1.luau:1:16-16 -tests/parserExamples/local-function-declaration-1.luau:2:2-5 -tests/parserExamples/numeric-for-loop-1.luau:2:2-5 -tests/parserExamples/numeric-for-loop-1.luau:4:9-13 -tests/parserExamples/numeric-for-loop-1.luau:4:16-20 -tests/parserExamples/repeat-until-1.luau:2:2-5 -tests/parserExamples/repeat-until-1.luau:3:7-15 -tests/parserExamples/table-1.luau:1:7-7 -tests/parserExamples/table-2.luau:1:7-7 -tests/parserExamples/type-assertion-1.luau:1:7-7 -tests/parserExamples/while-1.luau:1:7-15 -tests/parserExamples/while-1.luau:2:2-5 tests/runtime/crypto.test.luau:66:26-30 tests/runtime/crypto.test.luau:108:56-68 tests/runtime/crypto.test.luau:112:72-84 diff --git a/tools/check.luau b/tools/check.luau index 0816ecb09..1eecc3596 100644 --- a/tools/check.luau +++ b/tools/check.luau @@ -11,7 +11,12 @@ local TYPECHECK_PATHS = { "examples", "lute/cli", "lute/std/libs", - "tests", + "tests/batteries", + "tests/cli", + "tests/lute", + "tests/runtime", + "tests/src", + "tests/std", "tools", } From 846e92edfb9651b5f1653acda01c10c056e4bbe9 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 10 Feb 2026 09:48:49 -0800 Subject: [PATCH 331/642] Adds a lint rule for unused locals (#786) Adds a new lint rule to flag unused locals! Notable differences from the builtin Luau linter is that loop variables and function arguments will now be warned on if unused (you can still prefix with `_` to silence). Some followup work I'd like to do: - Classify builtin APIs by whether they actually read their arguments, like Selene does. For example, `table.insert` shouldn't actually count as having "used" its first argument. - Use @wmccrthy's work on lint configs to add configuration options to the rule - Add a lint rule or extend this one to flag unused globals --- batteries/difftext/printdiff.luau | 8 +- batteries/pp.luau | 2 +- definitions/crypto.luau | 1 + definitions/fs.luau | 1 + definitions/luau.luau | 1 + definitions/net.luau | 1 + definitions/process.luau | 1 + definitions/task.luau | 1 + definitions/time.luau | 1 + definitions/vm.luau | 1 + examples/a.luau | 2 +- examples/docs/test_module.luau | 1 + examples/parallel_serve.luau | 2 +- examples/parallel_serve_helper.luau | 2 +- examples/parallel_sort.luau | 2 +- examples/profile_test.luau | 2 +- examples/query.luau | 2 +- examples/serve.luau | 2 +- examples/system_lib.luau | 2 +- examples/task-resume.luau | 2 +- examples/time_instants.luau | 2 +- examples/transformee.luau | 2 +- lute/cli/commands/lint/init.luau | 3 +- .../commands/lint/rules/unused_variable.luau | 317 ++++++ .../cli/commands/pkg/loom-core/extern/pp.luau | 2 +- .../src/manifest/projectmanifest.luau | 2 +- .../pkg/loom-core/src/packagesource/git.luau | 4 +- lute/cli/commands/test/finder.luau | 1 - lute/cli/commands/test/init.luau | 5 - lute/std/libs/fs.luau | 4 +- lute/std/libs/json.luau | 4 +- lute/std/libs/path/posix/init.luau | 2 +- lute/std/libs/path/win32/init.luau | 2 +- lute/std/libs/syntax/query.luau | 8 +- lute/std/libs/syntax/visitor.luau | 30 +- lute/std/libs/task.luau | 4 +- tests/cli/check.test.luau | 2 - tests/cli/discovery/smoke.test.luau | 2 +- tests/cli/lint.test.luau | 946 +++++++++++++++++- tests/cli/test.test.luau | 1 - tests/parserExamples/attributes-1.luau | 1 + .../parserExamples/compound-assignment-1.luau | 1 + .../function-declaration-2.luau | 1 + .../function-declaration-4.luau | 1 + tests/parserExamples/if-expression-1.luau | 1 + tests/parserExamples/if-expression-2.luau | 2 +- .../parserExamples/interpolated-string-1.luau | 1 + .../parserExamples/interpolated-string-2.luau | 2 +- tests/parserExamples/local-assignment-1.luau | 1 + .../local-function-declaration-1.luau | 2 +- tests/parserExamples/table-1.luau | 2 +- tests/parserExamples/table-2.luau | 2 +- tests/parserExamples/type-assertion-1.luau | 2 +- .../config_tests/tilde_config/main.luau | 2 +- tests/src/staticrequires/main.luau | 1 + tests/std/net.test.luau | 2 +- tests/std/syntax/printer.test.luau | 18 +- tests/std/tableext.test.luau | 4 +- tools/check-faillist.txt | 465 +++++---- tools/luthier.luau | 2 - 60 files changed, 1564 insertions(+), 329 deletions(-) create mode 100644 lute/cli/commands/lint/rules/unused_variable.luau diff --git a/batteries/difftext/printdiff.luau b/batteries/difftext/printdiff.luau index 88d7b109a..b7e59fcd5 100644 --- a/batteries/difftext/printdiff.luau +++ b/batteries/difftext/printdiff.luau @@ -116,7 +116,7 @@ local function printDiffByLineDetailed(a: string, b: string, includeLineNumbers: table.insert(result, brightGreen("+ ") .. destLine) end else - for j, deleted in deletes do + for _, deleted in deletes do if includeLineNumbers then table.insert( result, @@ -129,7 +129,7 @@ local function printDiffByLineDetailed(a: string, b: string, includeLineNumbers: table.insert(result, brightRed("- " .. deleted)) end end - for j, added in adds do + for _, added in adds do if includeLineNumbers then table.insert( result, @@ -180,7 +180,7 @@ local function prettydiff( local maxLineNumberLen = math.floor(math.log10(maxNumLines)) + 1 local oldLine, newLine = 1, 1 - for i, op in diff do + for _, op in diff do if op.key == "ADD" then table.insert( result, @@ -202,7 +202,7 @@ local function prettydiff( end end else - for i, op in diff do + for _, op in diff do if op.key == "ADD" then table.insert(result, brightGreen("+ " .. op.text)) elseif op.key == "DELETE" then diff --git a/batteries/pp.luau b/batteries/pp.luau index 413668cdf..bf0e91627 100644 --- a/batteries/pp.luau +++ b/batteries/pp.luau @@ -114,7 +114,7 @@ local function traverseTable( local inSequence = false local previousKey = 0 - for idx, key in keys do + for _, key in keys do if type(key) == "number" and key > 0 and key - 1 == previousKey then previousKey = key inSequence = true diff --git a/definitions/crypto.luau b/definitions/crypto.luau index b3a37fab3..da336d9db 100644 --- a/definitions/crypto.luau +++ b/definitions/crypto.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local crypto = {} export type hash = { __hash: T } diff --git a/definitions/fs.luau b/definitions/fs.luau index 088762b7a..9782efd24 100644 --- a/definitions/fs.luau +++ b/definitions/fs.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local fs = {} export type FileHandle = { diff --git a/definitions/luau.luau b/definitions/luau.luau index fb708ff59..d4af28bd7 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local luau = {} -- this is a userdata, not a table, but it has this interface diff --git a/definitions/net.luau b/definitions/net.luau index 3135efb52..d2c135815 100644 --- a/definitions/net.luau +++ b/definitions/net.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local net = {} export type Metadata = { diff --git a/definitions/process.luau b/definitions/process.luau index 9a21bad1e..30e384607 100644 --- a/definitions/process.luau +++ b/definitions/process.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) export type StdioKind = "default" | "inherit" | "none" export type ProcessRunOptions = { diff --git a/definitions/task.luau b/definitions/task.luau index 3b80c6f34..92cb2e320 100644 --- a/definitions/task.luau +++ b/definitions/task.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local time = require("./time") local task = {} diff --git a/definitions/time.luau b/definitions/time.luau index 163a35061..e45d0e44f 100644 --- a/definitions/time.luau +++ b/definitions/time.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local time = {} local duration_ops = {} diff --git a/definitions/vm.luau b/definitions/vm.luau index 4132d748b..b824da67e 100644 --- a/definitions/vm.luau +++ b/definitions/vm.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local vm = {} function vm.create(path: string): { [any]: any } diff --git a/examples/a.luau b/examples/a.luau index f55243aad..120f018f9 100644 --- a/examples/a.luau +++ b/examples/a.luau @@ -1 +1 @@ -local x = require("./b") +local _ = require("./b") diff --git a/examples/docs/test_module.luau b/examples/docs/test_module.luau index 3d35c1b10..2dafce5a5 100644 --- a/examples/docs/test_module.luau +++ b/examples/docs/test_module.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) -- This file creates a mock module called test_module with random mock functions and different types of comments to test the reference doc generator reference.luau -- See output documentation file generated in this folder `test_module.md` diff --git a/examples/parallel_serve.luau b/examples/parallel_serve.luau index 917693d6a..7aa531031 100644 --- a/examples/parallel_serve.luau +++ b/examples/parallel_serve.luau @@ -3,7 +3,7 @@ local task = require("@std/task") local threadCount = 8 -for i = 1, threadCount do +for _ = 1, threadCount do task.create(vm.create("./parallel_serve_helper").serve) end diff --git a/examples/parallel_serve_helper.luau b/examples/parallel_serve_helper.luau index 3b9fad02e..efaedfc3a 100644 --- a/examples/parallel_serve_helper.luau +++ b/examples/parallel_serve_helper.luau @@ -3,7 +3,7 @@ local net = require("@lute/net") return { serve = function() net.serve({ - handler = function(req) + handler = function(_) return "Hello, lute!" end, reuseport = true, diff --git a/examples/parallel_sort.luau b/examples/parallel_sort.luau index 17636f05d..abcb30d51 100644 --- a/examples/parallel_sort.luau +++ b/examples/parallel_sort.luau @@ -44,7 +44,7 @@ end local threadCount = 8 local threads = {} -for i = 1, threadCount do +for _ = 1, threadCount do table.insert(threads, vm.create("./parallel_sort_helper")) end diff --git a/examples/profile_test.luau b/examples/profile_test.luau index 46616e853..3d062c4e3 100644 --- a/examples/profile_test.luau +++ b/examples/profile_test.luau @@ -36,7 +36,7 @@ end local function middleWork(x: number): number local sum = 0 - for i = 1, 10 do + for _ = 1, 10 do sum += innerWork(x) end return sum diff --git a/examples/query.luau b/examples/query.luau index 68360f8a6..e823fc9b6 100644 --- a/examples/query.luau +++ b/examples/query.luau @@ -20,6 +20,6 @@ local argIndexNames = query.map(p, function(call: syntax.AstExprCall) return call.arguments[1].node :: syntax.AstExprIndexName end) -local replacements = argIndexNames:replace(function(node) +local _replacements = argIndexNames:replace(function(_) return "NewComponent" end) diff --git a/examples/serve.luau b/examples/serve.luau index 828b93f6c..6f2d69ce5 100644 --- a/examples/serve.luau +++ b/examples/serve.luau @@ -3,7 +3,7 @@ local net = require("@lute/net") print("Starting server...") -- Start the server (non-blocking) -local server = net.serve(function(req) +local server = net.serve(function(_) return "Hello, lute!" end) diff --git a/examples/system_lib.luau b/examples/system_lib.luau index 96957006b..830f071ba 100644 --- a/examples/system_lib.luau +++ b/examples/system_lib.luau @@ -13,7 +13,7 @@ print( ) ) -for i, cpu in system.cpus() do +for _i, _cpu in system.cpus() do -- print(`[core {string.format("%.02i", i)}] speed: {cpu.speed}, model: {cpu.model}`) -- for key, time in cpu.times do diff --git a/examples/task-resume.luau b/examples/task-resume.luau index dc245bbc8..8e8b6e1fb 100644 --- a/examples/task-resume.luau +++ b/examples/task-resume.luau @@ -9,6 +9,6 @@ end) task.resume(c) task.resume(c, "world", "meow for good measure") -local t = coroutine.running() +local _ = coroutine.running() task.spawn(print, 3, 2, 1) diff --git a/examples/time_instants.luau b/examples/time_instants.luau index 75afd5e2a..44878d259 100644 --- a/examples/time_instants.luau +++ b/examples/time_instants.luau @@ -1,7 +1,7 @@ local time = require("@lute/time") local start = time.now() -for i = 1, 10000 do +for _ = 1, 10000 do vector.dot(vector.one, vector.zero) end local stop = time.now() diff --git a/examples/transformee.luau b/examples/transformee.luau index e99c87e57..7c79e57bb 100644 --- a/examples/transformee.luau +++ b/examples/transformee.luau @@ -1,3 +1,3 @@ local x = math.sqrt(-1) -- x ~= x should become math.isnan(x) -local b = x ~= x +local _b = x ~= x diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index d0be255b4..ba70801fa 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -13,7 +13,8 @@ local triviaUtils = require("@std/syntax/utils/trivia") local types = require("@self/types") local visitor = require("@std/syntax/visitor") -local DEFAULT_RULES = { "almost_swapped", "constant_table_comparison", "divide_by_zero", "parenthesized_conditions" } +local DEFAULT_RULES = + { "almost_swapped", "constant_table_comparison", "divide_by_zero", "parenthesized_conditions", "unused_variable" } local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" local VERBOSE = false diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau new file mode 100644 index 000000000..c687fd3c9 --- /dev/null +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -0,0 +1,317 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local syntax = require("@std/syntax") +local tableext = require("@std/tableext") +local visitorLib = require("@std/syntax/visitor") + +local name = "unused_variable" +local message = "Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + +local function isRequireCall(expr: syntax.AstExpr) + return expr.tag == "call" and expr.func.tag == "global" and expr.func.name.text == "require" +end + +local function unusedLocals(block: syntax.AstStatBlock): { syntax.AstLocal } + local visitor: visitorLib.Visitor & { + unusedLocals: { syntax.AstLocal }, + scopes: { { [syntax.AstLocal]: boolean } }, + ignoreReferences: { [syntax.AstLocal]: true }, + requiredModules: { [string]: syntax.AstLocal }, + insideMethodContext: boolean, + inLValueContext: boolean, + } = + visitorLib.create() :: any + visitor.unusedLocals = {} + visitor.scopes = {} + visitor.ignoreReferences = {} + visitor.requiredModules = {} + -- Used to avoid lookups to `self` + visitor.insideMethodContext = false + visitor.inLValueContext = false + + local function popAndSaveUnusedLocals(_: syntax.AstNode) + -- Pop scope and save unused locals + local scope = table.remove(visitor.scopes) + if not scope then + error("Expected a scope to pop") + end + + for l, visited in scope do + if not visited and l.name.text:sub(1, 1) ~= "_" then + table.insert(visitor.unusedLocals, l) + end + end + end + + local function markVisited(l: syntax.AstLocal) + -- Mark local as visited in closest scope + for i = #visitor.scopes, 1, -1 do + if visitor.scopes[i][l] ~= nil then + visitor.scopes[i][l] = true + return + end + end + + error("Local variable not found in any scope") + end + + function visitor.visitStatBlock(_: syntax.AstStatBlock) + -- Push a fresh scope + table.insert(visitor.scopes, {} :: { [syntax.AstLocal]: boolean }) + return true + end + + visitor.visitStatBlockEnd = popAndSaveUnusedLocals + + function visitor.visitStatLocalDeclaration(stat: syntax.AstStatLocal) + -- For each (var, value) pair, ignore references to the var in the value + for i, localVar in stat.variables do + if stat.values[i] then + if isRequireCall(stat.values[i].node) then + visitor.requiredModules[localVar.node.name.text] = localVar.node + else + visitor.ignoreReferences[localVar.node] = true + if localVar.node.annotation then + visitorLib.visittype(localVar.node.annotation, visitor) + end + visitorLib.visitexpression(stat.values[i].node, visitor) + visitor.ignoreReferences[localVar.node] = nil + end + end + end + + -- Mark each var as unused in the current scope + for _, localVar in stat.variables do + visitor.scopes[#visitor.scopes][localVar.node] = false + end + + return false + end + + function visitor.visitStatFor(stat: syntax.AstStatFor) + -- Push a scope for the loop variable + table.insert(visitor.scopes, { [stat.variable] = false }) + return true + end + + visitor.visitStatForEnd = popAndSaveUnusedLocals + + function visitor.visitStatForIn(stat: syntax.AstStatForIn) + -- Push a scope for the loop variables + local newScope: { [syntax.AstLocal]: boolean } = {} + for _, var in stat.variables do + newScope[var.node] = false + end + table.insert(visitor.scopes, newScope) + return true + end + + visitor.visitStatForInEnd = popAndSaveUnusedLocals + + function visitor.visitStatAssign(stat: syntax.AstStatAssign) + local i = 1 + local callFound = false + local prevInLValueContext = visitor.inLValueContext + while i <= math.min(#stat.variables, #stat.values) do + local var = stat.variables[i].node + local val = stat.values[i].node + -- An unused local doesn't become used just because it's used to assign to itself, eg: + -- local x + -- x = x + 1 + -- However, calls, can assign to multiple variables, so we can't assume variables and values correspond 1-1 once we see a call + if var.tag == "local" and not callFound then + visitor.ignoreReferences[var["local"]] = true + + visitorLib.visitexpression(val, visitor) + + visitor.ignoreReferences[var["local"]] = nil + else + visitor.inLValueContext = true + visitorLib.visitexpression(var, visitor) + visitor.inLValueContext = prevInLValueContext + + visitorLib.visitexpression(val, visitor) + end + + if val.tag == "call" then + callFound = true + end + + i = i + 1 + end + + -- If we see a call, we no longer know if variables correspond 1-1 with values, since functions can return multiple values + -- In this case, we stop caring about ignoring references, and only check as many values as there are variables + + return false + end + + function visitor.visitStatCompoundAssign(stat: syntax.AstStatCompoundAssign) + -- An unused local doesn't become used just because it's used to assign to itself, eg: + -- local x + -- x += x + 1 + if stat.variable.tag == "local" then + visitor.ignoreReferences[stat.variable["local"]] = true + visitorLib.visitexpression(stat.value, visitor) + visitor.ignoreReferences[stat.variable["local"]] = nil + else + local prevInLValueContext = visitor.inLValueContext + visitor.inLValueContext = true + visitorLib.visitexpression(stat.variable, visitor) + visitor.inLValueContext = prevInLValueContext + + visitorLib.visitexpression(stat.value, visitor) + end + + return false + end + + function visitor.visitStatFunction(stat: syntax.AstStatFunction) + -- This could be reassigning to a local, in which case we need to apply the same logic as visitStatAssign + if stat.name.tag == "local" then + -- If this shadows an unused local in the current scope, we need to mark it as unused + if visitor.scopes[#visitor.scopes][stat.name["local"]] == false then + table.insert(visitor.unusedLocals, stat.name["local"]) + end + + visitor.ignoreReferences[stat.name["local"]] = true + visitorLib.visitexpression(stat.func, visitor) + visitor.ignoreReferences[stat.name["local"]] = nil + elseif stat.name.tag == "indexname" and stat.name.accessor.text == ":" then + local prevMethodContext = visitor.insideMethodContext + visitor.insideMethodContext = true + + visitorLib.visitexpression(stat.func, visitor) + + visitor.insideMethodContext = prevMethodContext + elseif stat.name.tag == "index" then + local prevInLValueContext = visitor.inLValueContext + visitor.inLValueContext = true + visitorLib.visitexpression(stat.name, visitor) + visitor.inLValueContext = prevInLValueContext + + visitorLib.visitexpression(stat.func, visitor) + else + visitorLib.visitexpression(stat.func, visitor) + end + + return false + end + + function visitor.visitStatLocalFunction(stat: syntax.AstStatLocalFunction) + -- Recursive functions don't count as references to themselves + visitor.ignoreReferences[stat.name] = true + + visitorLib.visitexpression(stat.func, visitor) + + visitor.ignoreReferences[stat.name] = nil + + visitor.scopes[#visitor.scopes][stat.name] = false + + return false + end + + function visitor.visitStatRepeat(stat: syntax.AstStatRepeat) + -- Push a fresh scope + table.insert(visitor.scopes, {} :: { [syntax.AstLocal]: boolean }) + + -- Manually visit the block's states so we can keep the scope around + for _, bodyStat in stat.body.statements do + visitorLib.visitstatement(bodyStat, visitor) + end + + -- Visit the condition with the same scope as the body + visitorLib.visitexpression(stat.condition, visitor) + + popAndSaveUnusedLocals(stat) + + return false + end + + function visitor.visitExprLocal(node: syntax.AstExprLocal) + if node.token.text == "self" and visitor.insideMethodContext then + return false + end + + local astlocal = node["local"] + + -- We only ignore writes to locals in the same scope + -- We don't want to warn writes to upvalues as unused + if visitor.inLValueContext and visitor.scopes[#visitor.scopes][astlocal] ~= nil then + return false + end + + if visitor.ignoreReferences[astlocal] then + return false + end + + markVisited(astlocal) + + return false + end + + function visitor.visitExprFunction(node: syntax.AstExprFunction) + -- Push a scope for the function parameters + local newScope: { [syntax.AstLocal]: boolean } = {} + + for _, param in node.parameters do + newScope[param.node] = false + end + + table.insert(visitor.scopes, newScope) + + return true + end + + visitor.visitExprFunctionEnd = popAndSaveUnusedLocals + + function visitor.visitTypeReference(node: syntax.AstTypeReference) + if not node.prefix then + return true + end + + local requiredModule = visitor.requiredModules[node.prefix.text] + + if not requiredModule then + return true + end + + markVisited(requiredModule) + + return true + end + + function visitor.visitExprIndexExpr(node: syntax.AstExprIndexExpr) + visitorLib.visitexpression(node.expression, visitor) + + local prevInLValueContext = visitor.inLValueContext + visitor.inLValueContext = false + visitorLib.visitexpression(node.index, visitor) + visitor.inLValueContext = prevInLValueContext + + return false + end + + visitorLib.visitblock(block, visitor) + + return visitor.unusedLocals +end + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } + return tableext.map(unusedLocals(ast), function(l) + return table.freeze({ + lintname = name, + location = l.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + }) :: lintTypes.LintViolation + end) +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/lute/cli/commands/pkg/loom-core/extern/pp.luau b/lute/cli/commands/pkg/loom-core/extern/pp.luau index 413668cdf..bf0e91627 100644 --- a/lute/cli/commands/pkg/loom-core/extern/pp.luau +++ b/lute/cli/commands/pkg/loom-core/extern/pp.luau @@ -114,7 +114,7 @@ local function traverseTable( local inSequence = false local previousKey = 0 - for idx, key in keys do + for _, key in keys do if type(key) == "number" and key > 0 and key - 1 == previousKey then previousKey = key inSequence = true diff --git a/lute/cli/commands/pkg/loom-core/src/manifest/projectmanifest.luau b/lute/cli/commands/pkg/loom-core/src/manifest/projectmanifest.luau index 429ca67f1..f435ff2c3 100644 --- a/lute/cli/commands/pkg/loom-core/src/manifest/projectmanifest.luau +++ b/lute/cli/commands/pkg/loom-core/src/manifest/projectmanifest.luau @@ -35,7 +35,7 @@ function fromTable(table: any): ProjectManifest assert(package.version ~= nil and type(package.version) == "string", "Expected 'rev' field in package manifest") if package.dependencies ~= nil then local dependencies = package.dependencies - for name, spec in dependencies do + for _, spec in dependencies do assert(spec.rev ~= nil and type(spec.rev) == "string", "Expected 'rev' field in dependency spec") assert( spec.sourceKind ~= nil and type(spec.sourceKind) == "string", diff --git a/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau b/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau index 9835f3d1f..9bd9ccbe2 100644 --- a/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau +++ b/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau @@ -43,10 +43,10 @@ local function sanitizePath(path: string): string return normalized end -function Git:installZipArchive(name: string, version: string, content: string) +function Git:installZipArchive(name: string, _version: string, content: string) local reader = unzip.load(buffer.fromstring(content)) - reader:walk(function(entry, depth) + reader:walk(function(entry, _depth) if entry.isDirectory then return end diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index 318b8ec11..1b938dcd9 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -2,7 +2,6 @@ local ps = require("@std/process") local fs = require("@std/fs") local path = require("@std/path") local stringext = require("@std/stringext") -local tableext = require("@std/tableext") local function istestfile(filename: string): boolean return stringext.hassuffix(filename, ".test.luau") or stringext.hassuffix(filename, ".spec.luau") diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index ea3c01d9d..a07f7d3b9 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -46,11 +46,6 @@ end local function listtests() -- Gets all registered tests using the shared test instance local registered = test._registered() - local totalTests = #registered.anonymous - - for _, suite in registered.suites do - totalTests += #suite.cases - end print("Anonymous:") for _, tc in registered.anonymous do diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index c9e791ce8..39d04b4e0 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -83,14 +83,14 @@ function fslib.watch(path: pathlike): watcher end) return { - next = function(self: watcher): watchevent? + next = function(_self: watcher): watchevent? if #queue == 0 then return nil end local item = table.remove(queue, 1) return item.event end, - close = function(self: watcher): () + close = function(_self: watcher): () if handle then handle:close() end diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 14502f5e9..0fc1c4fd0 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -55,7 +55,7 @@ local function writeSpaces(state: SerializerState) if state.prettyPrint then checkState(state, state.depth * 4) - for i = 1, state.depth do + for _ = 1, state.depth do buffer.writeu32(state.buf, state.cursor, 0x_20_20_20_20) state.cursor += 4 end @@ -101,7 +101,7 @@ local function serializeString(state: SerializerState, str: string) writeByte(state, string.byte('"')) - for pos, codepoint in utf8.codes(str) do + for _, codepoint in utf8.codes(str) do if ESCAPE_MAP[codepoint] then writeByte(state, string.byte("\\")) writeByte(state, ESCAPE_MAP[codepoint]) diff --git a/lute/std/libs/path/posix/init.luau b/lute/std/libs/path/posix/init.luau index 47e63c3f2..b1af1aa13 100644 --- a/lute/std/libs/path/posix/init.luau +++ b/lute/std/libs/path/posix/init.luau @@ -221,7 +221,7 @@ function posix.relative(from: pathlike, to: pathlike): path local relativeParts = {} if #from.parts > commonPrefixLength then - for i = commonPrefixLength + 1, #from.parts do + for _ = commonPrefixLength + 1, #from.parts do table.insert(relativeParts, "..") end end diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index 10f1e5a3d..53b863dff 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -257,7 +257,7 @@ function win32.relative(from: pathlike, to: pathlike): path local relativeParts = {} if #from.parts > commonPrefixLength then - for i = commonPrefixLength + 1, #from.parts do + for _ = commonPrefixLength + 1, #from.parts do table.insert(relativeParts, "..") end end diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 4be2b7ac4..28dfbaf90 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -74,12 +74,12 @@ local function newSelectVisitor(nodes: { T }, fn: (node) -> T?): visitor.Visi end local selectVisitor = visitor.create(visit) - selectVisitor.visitStatBlockEnd = function(n) end - selectVisitor.visitStatLocalDeclarationEnd = function(n) end - selectVisitor.visitExpr = function(n) + selectVisitor.visitStatBlockEnd = function(_) end + selectVisitor.visitStatLocalDeclarationEnd = function(_) end + selectVisitor.visitExpr = function(_) return true end - selectVisitor.visitExprEnd = function(n) end + selectVisitor.visitExprEnd = function(_) end selectVisitor.visitToken = function(n) if utils.isBaseToken(n) then -- only visit if it is a base token, so we don't double count diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index f0bb8abf1..3d5e7c9ae 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -19,7 +19,9 @@ export type Visitor = { visitStatLocalDeclaration: (types.AstStatLocal) -> boolean, visitStatLocalDeclarationEnd: (types.AstStatLocal) -> (), visitStatFor: (types.AstStatFor) -> boolean, + visitStatForEnd: (types.AstStatFor) -> (), visitStatForIn: (types.AstStatForIn) -> boolean, + visitStatForInEnd: (types.AstStatForIn) -> (), visitStatAssign: (types.AstStatAssign) -> boolean, visitStatCompoundAssign: (types.AstStatCompoundAssign) -> boolean, visitStatFunction: (types.AstStatFunction) -> boolean, @@ -40,6 +42,7 @@ export type Visitor = { visitExprUnary: (types.AstExprUnary) -> boolean, visitExprBinary: (types.AstExprBinary) -> boolean, visitExprFunction: (types.AstExprFunction) -> boolean, + visitExprFunctionEnd: (types.AstExprFunction) -> (), visitExprTableItem: (types.AstExprTableItem) -> boolean, visitExprTable: (types.AstExprTable) -> boolean, visitExprIndexName: (types.AstExprIndexName) -> boolean, @@ -90,7 +93,9 @@ local defaultVisitor: Visitor = { visitStatLocalDeclaration = alwaysVisit :: any, visitStatLocalDeclarationEnd = alwaysVisit :: any, visitStatFor = alwaysVisit :: any, + visitStatForEnd = alwaysVisit :: any, visitStatForIn = alwaysVisit :: any, + visitStatForInEnd = alwaysVisit :: any, visitStatAssign = alwaysVisit :: any, visitStatCompoundAssign = alwaysVisit :: any, visitStatFunction = alwaysVisit :: any, @@ -111,6 +116,7 @@ local defaultVisitor: Visitor = { visitExprUnary = alwaysVisit :: any, visitExprBinary = alwaysVisit :: any, visitExprFunction = alwaysVisit :: any, + visitExprFunctionEnd = alwaysVisit :: any, visitExprTableItem = alwaysVisit :: any, visitExprTable = alwaysVisit :: any, visitExprIndexName = alwaysVisit :: any, @@ -281,6 +287,7 @@ local function visitStatFor(node: types.AstStatFor, visitor: Visitor) visitToken(node.dokeyword, visitor) visitStatBlock(node.body, visitor) visitToken(node.endkeyword, visitor) + visitor.visitStatForEnd(node) end end @@ -293,6 +300,7 @@ local function visitStatForIn(node: types.AstStatForIn, visitor: Visitor) visitToken(node.dokeyword, visitor) visitStatBlock(node.body, visitor) visitToken(node.endkeyword, visitor) + visitor.visitStatForInEnd(node) end end @@ -486,6 +494,7 @@ local function visitExprFunction( end visitStatBlock(node.body, visitor) visitToken(node.endkeyword, visitor) + visitor.visitExprFunctionEnd(node) end end @@ -939,7 +948,9 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor if visit then return { visitStatBlock = visit, - visitStatBlockEnd = visit, + visitStatBlockEnd = function(s) + visit(s) + end, visitStatDo = visit, visitStatIf = visit, visitStatWhile = visit, @@ -948,9 +959,17 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitStatContinue = visit, visitStatReturn = visit, visitStatLocalDeclaration = visit, - visitStatLocalDeclarationEnd = visit, + visitStatLocalDeclarationEnd = function(s) + visit(s) + end, visitStatFor = visit, + visitStatForEnd = function(s) + visit(s) + end, visitStatForIn = visit, + visitStatForInEnd = function(s) + visit(s) + end, visitStatAssign = visit, visitStatCompoundAssign = visit, visitStatFunction = visit, @@ -960,13 +979,18 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitStatExpr = visit, visitExpr = visit, - visitExprEnd = visit, + visitExprEnd = function(s) + visit(s) + end, visitExprLocal = visit, visitExprGlobal = visit, visitExprCall = visit, visitExprUnary = visit, visitExprBinary = visit, visitExprFunction = visit, + visitExprFunctionEnd = function(s) + visit(s) + end, visitExprTableItem = visit, visitExprTable = visit, visitExprIndexName = visit, diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index aede1082e..7ce9e525d 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -56,7 +56,7 @@ function tasklib.awaitall(...: task) while not done do done = true - for i, v in tasks do + for _, v in tasks do if v.success == nil then done = false task.defer() @@ -66,7 +66,7 @@ function tasklib.awaitall(...: task) local results = {} - for i, v in tasks do + for _, v in tasks do if v.success then table.insert(results, v.result) else diff --git a/tests/cli/check.test.luau b/tests/cli/check.test.luau index ed33c3756..7a6fd5838 100644 --- a/tests/cli/check.test.luau +++ b/tests/cli/check.test.luau @@ -1,7 +1,5 @@ -local fs = require("@std/fs") local path = require("@std/path") local process = require("@std/process") -local system = require("@std/system") local test = require("@std/test") local lutePath = path.format(process.execpath()) diff --git a/tests/cli/discovery/smoke.test.luau b/tests/cli/discovery/smoke.test.luau index eb7a52fdf..92e551962 100644 --- a/tests/cli/discovery/smoke.test.luau +++ b/tests/cli/discovery/smoke.test.luau @@ -10,4 +10,4 @@ test.suite("SmokeSuite", function(suite) end) end) -test.case("Passing", function(assert) end) +test.case("Passing", function(_) end) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index f1e1f3aae..6ad190d43 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -13,6 +13,7 @@ local defaultRulesPaths = { constant_table_comparison = path.format(path.join(defaultRulesFolder, "constant_table_comparison.luau")), divide_by_zero = path.format(path.join(defaultRulesFolder, "divide_by_zero.luau")), parenthesized_conditions = path.format(path.join(defaultRulesFolder, "parenthesized_conditions.luau")), + unused_variable = path.format(path.join(defaultRulesFolder, "unused_variable.luau")), } local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" @@ -148,11 +149,11 @@ violator.luau:1:11-16 ── fs.writestringtofile( violatorPath, [[ -local x = 1 / 0 -local y = -3 // 0 -local z = 4 % 0 -local nan = 0 / 0 -local negnan = -0 / 0 +local _x = 1 / 0 +local _y = -3 // 0 +local _z = 4 % 0 +local _nan = 0 / 0 +local _negnan = -0 / 0 ]] ) @@ -161,16 +162,15 @@ local negnan = -0 / 0 local result = process.run({ lutePath, "lint", "--auto-fix", violatorPath }) -- Check - print(result.stdout) assert.eq(result.exitcode, 0) -- After applying the auto-fix, there are no more violations local fixedContents = fs.readfiletostring(violatorPath) local expectedFixedContents = [[ -local x = math.huge -local y = -math.huge -local z = 0 / 0 -local nan = 0 / 0 -local negnan = 0 / 0 +local _x = math.huge +local _y = -math.huge +local _z = 0 / 0 +local _nan = 0 / 0 +local _negnan = 0 / 0 ]] assert.eq(fixedContents, expectedFixedContents) @@ -974,9 +974,9 @@ violator.luau:3:1-4:6 ── violatorPath, [[ -- lute-lint-ignore(divide_by_zero) -local function foo() - local x = 1/0 - local function bar() +local function _foo() + local _x = 1/0 + local function _bar() -- lute-lint-ignore(almost_swapped) a = b b = a @@ -1020,6 +1020,22 @@ local x = 1 / 0 "items": [ { "items": [ + { + "message": "Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + "source": "lute lint", + "code": "unused_variable", + "severity": 2, + "range": { + "start": { + "character": 6, + "line": 0 + }, + "end": { + "character": 7, + "line": 0 + } + } + }, { "message": "Division by zero detected.", "source": "lute lint", @@ -1238,7 +1254,7 @@ b = a end) suite:case("lute lint string input with no violations", function(assert) - local inputString = "local x = 1 + 2" + local inputString = "x = 1 + 2" -- Do -- Run the linter on string input @@ -1261,6 +1277,22 @@ b = a local expected = [=[ [ + { + "message": "Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + "source": "lute lint", + "code": "unused_variable", + "severity": 2, + "range": { + "start": { + "character": 6, + "line": 0 + }, + "end": { + "character": 7, + "line": 0 + } + } + }, { "message": "Division by zero detected.", "source": "lute lint", @@ -1698,4 +1730,888 @@ local e = next(t) ~= nil ]]=] assert.strcontains(result.stdout, expected) end) + + suite:case("unused_variable_1", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 3 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local x = 3 + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_2", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local _ = 3 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + ) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_3", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +for i = 3, 4 do + print(3) +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:5-6 ── + │ + 1 │ for i = 3, 4 do + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_4", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local t = {1, 2, 3} +for i, v in t do + print(3) +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 2 + ) + + local expected = [[ +violator.luau:2:5-6 ── + │ + 2 │ for i, v in t do + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:2:8-9 ── + │ + 2 │ for i, v in t do + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_5", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +x = x + 1 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local x = 4 + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_6", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +local y = 5 +y, x = y, x + 1 +print(y) + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local x = 4 + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_7", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +x = x + 1 +print(x) + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + ) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_8", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +x += 1 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local x = 4 + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_9", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +x += 1 +print(x) + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + ) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_10", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +x += 1 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local x = 4 + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_11", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local function _(x) + return nil +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:18-19 ── + │ + 1 │ local function _(x) + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_12", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +function x() + return 5 +end +x() + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local x = 4 + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_13", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +local x = 5 +print(x) + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local x = 4 + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_14", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +local function x() + return 5 +end +x() + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local x = 4 + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_15", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 4 +local x = 5 + x + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:2:7-8 ── + │ + 2 │ local x = 5 + x + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_16", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local lib = require("whatever") +local _x : lib.num = 3 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + ) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_17", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +for k, v in t do + t1[k] = v +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + ) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_18", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +return { + add = function(a, b) + return a + b + end, +} + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + ) + + assert.strnotcontains(result.stdout, "Local variable not found in any scope") + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_19", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local T = {} +function T:new() + self.hello = "world" +end + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + assert.strnotcontains(result.stdout, "Local variable not found in any scope") + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local T = {} + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_20", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local a, b = foo() +a, b = foo() + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 2 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local a, b = foo() + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + expected = [[ +violator.luau:1:10-11 ── + │ + 1 │ local a, b = foo() + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_21", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +local x = 3 +local y = 1 +y = 3, x +print(y) + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 1) + + assert.strcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + nil, + 1 + ) + + local expected = [[ +violator.luau:1:7-8 ── + │ + 1 │ local x = 3 + │ ^ + │ +]] + assert.strcontains(result.stdout, expected) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_22", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [[ +repeat + local x = 1 +until x == 1 + ]] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains(result.stdout, "Local variable not found in any scope") + assert.strnotcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + ) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_23", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [=[ +for i = 0, num - 1 do + t.table[lengths[off + i]] += 1 +end + ]=] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + ) + + -- Teardown + fs.remove(violatorPath) + end) + + suite:case("unused_variable_24", function(assert) + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile( + violatorPath, + [=[ +function foo(t: { num: number }) + t.num = 3 +end + ]=] + ) + + -- Do + -- Run the transformer on the transformee + local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) + + -- Check + assert.eq(result.exitcode, 0) + + assert.strnotcontains( + result.stdout, + "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." + ) + + -- Teardown + fs.remove(violatorPath) + end) end) diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 1e088ef4a..434b21590 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -1,6 +1,5 @@ local path = require("@std/path") local process = require("@std/process") -local system = require("@std/system") local test = require("@std/test") local lutePath = path.format(process.execpath()) diff --git a/tests/parserExamples/attributes-1.luau b/tests/parserExamples/attributes-1.luau index bfcc99b11..33fc5a98d 100644 --- a/tests/parserExamples/attributes-1.luau +++ b/tests/parserExamples/attributes-1.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) @native local function foo() end diff --git a/tests/parserExamples/compound-assignment-1.luau b/tests/parserExamples/compound-assignment-1.luau index 0dca2b146..16d300d00 100644 --- a/tests/parserExamples/compound-assignment-1.luau +++ b/tests/parserExamples/compound-assignment-1.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local x = 1 local y = 2 diff --git a/tests/parserExamples/function-declaration-2.luau b/tests/parserExamples/function-declaration-2.luau index 3e8e51c97..d1566faac 100644 --- a/tests/parserExamples/function-declaration-2.luau +++ b/tests/parserExamples/function-declaration-2.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) -- stylua: ignore function x(x , y, z) end diff --git a/tests/parserExamples/function-declaration-4.luau b/tests/parserExamples/function-declaration-4.luau index 143e693a8..291fba256 100644 --- a/tests/parserExamples/function-declaration-4.luau +++ b/tests/parserExamples/function-declaration-4.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) function a(x: number) end function b(x: number, y: number) end diff --git a/tests/parserExamples/if-expression-1.luau b/tests/parserExamples/if-expression-1.luau index a0b67da2a..a9199424b 100644 --- a/tests/parserExamples/if-expression-1.luau +++ b/tests/parserExamples/if-expression-1.luau @@ -1,2 +1,3 @@ +-- lute-lint-global-ignore(unused_variable) local x = if true then 1 else 2 local y = if true then 1 elseif false then 2 else 3 diff --git a/tests/parserExamples/if-expression-2.luau b/tests/parserExamples/if-expression-2.luau index 7b1acaeb9..f4dd3bf41 100644 --- a/tests/parserExamples/if-expression-2.luau +++ b/tests/parserExamples/if-expression-2.luau @@ -1,5 +1,5 @@ -- stylua: ignore -local x = if true +local _ = if true then call() else if false then call() diff --git a/tests/parserExamples/interpolated-string-1.luau b/tests/parserExamples/interpolated-string-1.luau index eee1f51b7..433e91da4 100644 --- a/tests/parserExamples/interpolated-string-1.luau +++ b/tests/parserExamples/interpolated-string-1.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local a = `simple` local b = `start{true}` local c = `start{true}end` diff --git a/tests/parserExamples/interpolated-string-2.luau b/tests/parserExamples/interpolated-string-2.luau index 99fb73fa7..7107508d5 100644 --- a/tests/parserExamples/interpolated-string-2.luau +++ b/tests/parserExamples/interpolated-string-2.luau @@ -1,5 +1,5 @@ -- stylua: ignore -local x = +local _ = `test` -- stylua: ignore diff --git a/tests/parserExamples/local-assignment-1.luau b/tests/parserExamples/local-assignment-1.luau index 4eae6b722..a727bc0a3 100644 --- a/tests/parserExamples/local-assignment-1.luau +++ b/tests/parserExamples/local-assignment-1.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) local a = 1 local b, c = 2, nil local d: string = "" diff --git a/tests/parserExamples/local-function-declaration-1.luau b/tests/parserExamples/local-function-declaration-1.luau index fbbbdc20f..6326301ed 100644 --- a/tests/parserExamples/local-function-declaration-1.luau +++ b/tests/parserExamples/local-function-declaration-1.luau @@ -1,3 +1,3 @@ -local function x() +local function _() call(1) end diff --git a/tests/parserExamples/table-1.luau b/tests/parserExamples/table-1.luau index aa8032b78..d6182dba3 100644 --- a/tests/parserExamples/table-1.luau +++ b/tests/parserExamples/table-1.luau @@ -1 +1 @@ -local x = { 1, 2, 3 } +local _ = { 1, 2, 3 } diff --git a/tests/parserExamples/table-2.luau b/tests/parserExamples/table-2.luau index ba16cbe0e..9c38de282 100644 --- a/tests/parserExamples/table-2.luau +++ b/tests/parserExamples/table-2.luau @@ -1,3 +1,3 @@ -local x = { +local _ = { ["test"] = true, } diff --git a/tests/parserExamples/type-assertion-1.luau b/tests/parserExamples/type-assertion-1.luau index d76016dd2..a8a3c0dce 100644 --- a/tests/parserExamples/type-assertion-1.luau +++ b/tests/parserExamples/type-assertion-1.luau @@ -1 +1 @@ -local x = 1 :: number +local _ = 1 :: number diff --git a/tests/src/require/config_tests/tilde_config/main.luau b/tests/src/require/config_tests/tilde_config/main.luau index b0f6c9f22..7133a9d9c 100644 --- a/tests/src/require/config_tests/tilde_config/main.luau +++ b/tests/src/require/config_tests/tilde_config/main.luau @@ -1 +1 @@ -local x = require("@tilde/foo") +local _x = require("@tilde/foo") diff --git a/tests/src/staticrequires/main.luau b/tests/src/staticrequires/main.luau index 0a2244f0a..cd55e308f 100644 --- a/tests/src/staticrequires/main.luau +++ b/tests/src/staticrequires/main.luau @@ -1,3 +1,4 @@ +-- lute-lint-global-ignore(unused_variable) -- test that you can require in an alias aware fashion local example = require("@example/option") -- print(`Required {example.file} from alias @example`) diff --git a/tests/std/net.test.luau b/tests/std/net.test.luau index e1c5eacaf..c4a70a1b4 100644 --- a/tests/std/net.test.luau +++ b/tests/std/net.test.luau @@ -4,7 +4,7 @@ local test = require("@std/test") test.suite("NetTestSuite", function(suite) suite:case("serve_locally_and_request", function(assert) local server = net.serve({ - handler = function(req: net.ReceivedRequest): net.ServerResponse + handler = function(_req: net.ReceivedRequest): net.ServerResponse return { status = 200, body = "Hello, World!", diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 93ac60ebb..18261aa3d 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -6,16 +6,6 @@ local syntaxTypes = require("@std/syntax/types") local syntaxUtils = require("@std/syntax/utils") local test = require("@std/test") -local function compareStrings(result: string, expected: string) - for i = 1, math.max(#result, #expected) do - local rChar = result:sub(i, i):byte() - local eChar = expected:sub(i, i):byte() - if rChar ~= eChar then - print(`Difference at index {i}: result='{rChar}', expected='{eChar}'`) - end - end -end - local function checkReplacement( source: string, expected: string, @@ -67,7 +57,7 @@ test.suite("syntax_printer", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatLocal):replace(function(s) + return query.findallfromroot(ast, syntaxUtils.isStatLocal):replace(function(_) return syntax.parseblock("local name = 1 + 2") end) end, assert) @@ -86,7 +76,7 @@ test.suite("syntax_printer", function(suite) :map(function(assign) return assign.variables[1].node.annotation end) - :replace(function(t) + :replace(function(_) return ann end) end, assert) @@ -107,7 +97,7 @@ test.suite("syntax_printer", function(suite) return ta.type.returntypes end end) - :replace(function(t) + :replace(function(_) return tp end) end, assert) @@ -123,7 +113,7 @@ test.suite("syntax_printer", function(suite) :map(function(node) return node.entries[1] end) - :replace(function(entry) + :replace(function(_) return "b = 2," end) end, assert) diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index c4388153f..75951d181 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -115,7 +115,7 @@ test.suite("TableExt", function(suite) assert.eq(hasGreaterThanFive, false) a = {} - local anyInEmpty = tableext.any(a, function(v: number) + local anyInEmpty = tableext.any(a, function(_: number) return true end) assert.eq(anyInEmpty, false) @@ -134,7 +134,7 @@ test.suite("TableExt", function(suite) assert.eq(allLessThanEight, false) a = {} - local allInEmpty = tableext.all(a, function(v: number) + local allInEmpty = tableext.all(a, function(_: number) return false end) assert.eq(allInEmpty, true) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 13566d7b0..2ae996c77 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -18,10 +18,9 @@ batteries/toml.luau:146:20-24 batteries/toml.luau:146:55-59 batteries/toml.luau:147:24-28 batteries/toml.luau:166:24-28 -examples/a.luau:1:7-7 examples/colorful.luau:1:18-47 examples/directories.luau:3:16-25 -examples/docs/test_module.luau:30:7-12 +examples/docs/test_module.luau:31:7-12 examples/json.luau:13:7-24 examples/json.luau:13:7-24 examples/lints/almost_swapped.luau:83:5-16 @@ -55,10 +54,7 @@ examples/process.luau:13:24-34 examples/process.luau:17:7-15 examples/process.luau:18:7-15 examples/process.luau:19:7-15 -examples/query.luau:23:7-18 examples/task-delay.luau:3:15-37 -examples/task-resume.luau:12:7-7 -examples/transformee.luau:3:7-7 examples/transformer.luau:33:38-100 lute/cli/commands/doc/init.luau:203:2-11 lute/cli/commands/doc/init.luau:204:10-24 @@ -73,7 +69,7 @@ lute/cli/commands/lib/cli.luau:144:36-75 lute/cli/commands/lib/cli.luau:144:36-75 lute/cli/commands/lib/cli.luau:148:6-45 lute/cli/commands/lib/cli.luau:148:6-45 -lute/cli/commands/lint/init.luau:51:10-18 +lute/cli/commands/lint/init.luau:52:10-18 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 @@ -101,9 +97,7 @@ lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:62:6-17 lute/cli/commands/test/filter.luau:62:6-17 -lute/cli/commands/test/finder.luau:5:7-14 -lute/cli/commands/test/finder.luau:5:7-14 -lute/cli/commands/test/init.luau:126:42-44 +lute/cli/commands/test/init.luau:121:42-44 lute/cli/commands/transform/init.luau:16:26-30 lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:47:26-37 @@ -177,54 +171,48 @@ lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:31:13-21 lute/std/libs/syntax/utils/trivia.luau:31:13-21 -lute/std/libs/syntax/visitor.luau:237:3-12 -lute/std/libs/syntax/visitor.luau:237:3-12 lute/std/libs/syntax/visitor.luau:243:3-12 lute/std/libs/syntax/visitor.luau:243:3-12 -lute/std/libs/syntax/visitor.luau:374:3-12 -lute/std/libs/syntax/visitor.luau:374:3-12 -lute/std/libs/syntax/visitor.luau:380:3-12 -lute/std/libs/syntax/visitor.luau:380:3-12 -lute/std/libs/syntax/visitor.luau:386:3-12 -lute/std/libs/syntax/visitor.luau:386:3-12 -lute/std/libs/syntax/visitor.luau:404:3-12 -lute/std/libs/syntax/visitor.luau:404:3-12 -lute/std/libs/syntax/visitor.luau:438:3-12 -lute/std/libs/syntax/visitor.luau:438:3-12 -lute/std/libs/syntax/visitor.luau:654:3-12 -lute/std/libs/syntax/visitor.luau:654:3-12 -lute/std/libs/syntax/visitor.luau:660:3-12 -lute/std/libs/syntax/visitor.luau:660:3-12 -lute/std/libs/syntax/visitor.luau:683:3-12 -lute/std/libs/syntax/visitor.luau:683:3-12 -lute/std/libs/syntax/visitor.luau:942:24-28 -lute/std/libs/syntax/visitor.luau:942:24-28 -lute/std/libs/syntax/visitor.luau:951:35-39 -lute/std/libs/syntax/visitor.luau:951:35-39 -lute/std/libs/syntax/visitor.luau:959:28-32 -lute/std/libs/syntax/visitor.luau:959:28-32 -lute/std/libs/syntax/visitor.luau:959:28-32 -lute/std/libs/syntax/visitor.luau:959:28-32 -lute/std/libs/syntax/visitor.luau:963:19-23 -lute/std/libs/syntax/visitor.luau:963:19-23 -lute/std/libs/syntax/visitor.luau:970:25-29 -lute/std/libs/syntax/visitor.luau:970:25-29 -lute/std/libs/syntax/visitor.luau:974:21-25 -lute/std/libs/syntax/visitor.luau:974:21-25 -lute/std/libs/syntax/visitor.luau:988:21-25 -lute/std/libs/syntax/visitor.luau:988:21-25 -lute/std/libs/syntax/visitor.luau:988:21-25 -lute/std/libs/syntax/visitor.luau:988:21-25 -lute/std/libs/syntax/visitor.luau:995:17-21 -lute/std/libs/syntax/visitor.luau:995:17-21 -lute/std/libs/syntax/visitor.luau:1023:9-20 -lute/std/libs/syntax/visitor.luau:1023:9-20 -lute/std/libs/syntax/visitor.luau:1024:3-12 -lute/std/libs/syntax/visitor.luau:1024:3-12 -lute/std/libs/syntax/visitor.luau:1025:9-24 -lute/std/libs/syntax/visitor.luau:1025:9-24 -lute/std/libs/syntax/visitor.luau:1026:22-25 -lute/std/libs/syntax/visitor.luau:1026:22-25 +lute/std/libs/syntax/visitor.luau:249:3-12 +lute/std/libs/syntax/visitor.luau:249:3-12 +lute/std/libs/syntax/visitor.luau:382:3-12 +lute/std/libs/syntax/visitor.luau:382:3-12 +lute/std/libs/syntax/visitor.luau:388:3-12 +lute/std/libs/syntax/visitor.luau:388:3-12 +lute/std/libs/syntax/visitor.luau:394:3-12 +lute/std/libs/syntax/visitor.luau:394:3-12 +lute/std/libs/syntax/visitor.luau:412:3-12 +lute/std/libs/syntax/visitor.luau:412:3-12 +lute/std/libs/syntax/visitor.luau:446:3-12 +lute/std/libs/syntax/visitor.luau:446:3-12 +lute/std/libs/syntax/visitor.luau:663:3-12 +lute/std/libs/syntax/visitor.luau:663:3-12 +lute/std/libs/syntax/visitor.luau:669:3-12 +lute/std/libs/syntax/visitor.luau:669:3-12 +lute/std/libs/syntax/visitor.luau:692:3-12 +lute/std/libs/syntax/visitor.luau:692:3-12 +lute/std/libs/syntax/visitor.luau:978:28-32 +lute/std/libs/syntax/visitor.luau:978:28-32 +lute/std/libs/syntax/visitor.luau:978:28-32 +lute/std/libs/syntax/visitor.luau:978:28-32 +lute/std/libs/syntax/visitor.luau:994:25-29 +lute/std/libs/syntax/visitor.luau:994:25-29 +lute/std/libs/syntax/visitor.luau:998:21-25 +lute/std/libs/syntax/visitor.luau:998:21-25 +lute/std/libs/syntax/visitor.luau:1012:21-25 +lute/std/libs/syntax/visitor.luau:1012:21-25 +lute/std/libs/syntax/visitor.luau:1012:21-25 +lute/std/libs/syntax/visitor.luau:1012:21-25 +lute/std/libs/syntax/visitor.luau:1019:17-21 +lute/std/libs/syntax/visitor.luau:1019:17-21 +lute/std/libs/syntax/visitor.luau:1047:9-20 +lute/std/libs/syntax/visitor.luau:1047:9-20 +lute/std/libs/syntax/visitor.luau:1048:3-12 +lute/std/libs/syntax/visitor.luau:1048:3-12 +lute/std/libs/syntax/visitor.luau:1049:9-24 +lute/std/libs/syntax/visitor.luau:1049:9-24 +lute/std/libs/syntax/visitor.luau:1050:22-25 +lute/std/libs/syntax/visitor.luau:1050:22-25 lute/std/libs/test/failure.luau:18:35-38 lute/std/libs/test/failure.luau:18:35-38 lute/std/libs/test/reporter.luau:10:19-32 @@ -240,9 +228,6 @@ tests/batteries/collections/deque.test.luau:61:13-16 tests/batteries/collections/deque.test.luau:81:13-16 tests/batteries/collections/deque.test.luau:86:13-16 tests/batteries/collections/deque.test.luau:91:13-16 -tests/cli/check.test.luau:1:7-8 -tests/cli/check.test.luau:4:7-12 -tests/cli/test.test.luau:3:7-12 tests/runtime/crypto.test.luau:66:26-30 tests/runtime/crypto.test.luau:108:56-68 tests/runtime/crypto.test.luau:112:72-84 @@ -250,13 +235,12 @@ tests/src/packages/package_aware_require/dep/module.luau:1:16-30 tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau:1:16-38 tests/src/require/config_tests/config_ambiguity/requirer.luau:1:8-22 tests/src/require/config_tests/config_cannot_be_required/requirer.luau:1:8-27 -tests/src/require/config_tests/tilde_config/main.luau:1:7-7 -tests/src/require/config_tests/tilde_config/main.luau:1:11-31 +tests/src/require/config_tests/tilde_config/main.luau:1:12-32 tests/src/require/without_config/ambiguous_directory_requirer.luau:1:16-58 tests/src/require/without_config/ambiguous_file_requirer.luau:1:16-53 tests/src/staticrequires/circular_a.luau:2:11-33 tests/src/staticrequires/circular_b.luau:2:11-33 -tests/src/staticrequires/main.luau:2:7-13 +tests/src/staticrequires/main.luau:3:7-13 tests/std/fs.test.luau:114:13-24 tests/std/fs.test.luau:133:18-20 tests/std/fs.test.luau:142:18-20 @@ -288,173 +272,172 @@ tests/std/process.test.luau:123:41-42 tests/std/process.test.luau:126:43-44 tests/std/process.test.luau:130:41-53 tests/std/process.test.luau:134:33-48 -tests/std/syntax/printer.test.luau:9:16-29 -tests/std/syntax/printer.test.luau:23:10-26 -tests/std/syntax/printer.test.luau:45:8-13 -tests/std/syntax/printer.test.luau:59:8-13 -tests/std/syntax/printer.test.luau:73:8-13 -tests/std/syntax/printer.test.luau:92:8-13 -tests/std/syntax/printer.test.luau:105:10-100 -tests/std/syntax/printer.test.luau:107:7-32 -tests/std/syntax/printer.test.luau:113:8-13 -tests/std/syntax/printer.test.luau:129:8-13 -tests/std/syntax/printer.test.luau:141:8-13 -tests/std/syntax/printer.test.luau:153:8-13 -tests/std/syntax/printer.test.luau:165:8-13 -tests/std/syntax/printer.test.luau:177:8-13 -tests/std/syntax/printer.test.luau:190:8-13 -tests/std/syntax/printer.test.luau:202:8-13 -tests/std/syntax/printer.test.luau:214:8-13 -tests/std/syntax/printer.test.luau:226:8-13 -tests/std/syntax/printer.test.luau:238:8-13 -tests/std/syntax/printer.test.luau:250:8-13 -tests/std/syntax/printer.test.luau:262:8-13 -tests/std/syntax/printer.test.luau:274:8-13 -tests/std/syntax/printer.test.luau:286:8-13 -tests/std/syntax/printer.test.luau:298:8-13 -tests/std/syntax/printer.test.luau:310:8-13 -tests/std/syntax/printer.test.luau:322:8-13 -tests/std/syntax/printer.test.luau:334:8-13 -tests/std/syntax/printer.test.luau:346:8-13 -tests/std/syntax/printer.test.luau:359:8-13 -tests/std/syntax/printer.test.luau:376:8-13 -tests/std/syntax/printer.test.luau:391:8-13 -tests/std/syntax/printer.test.luau:406:8-13 -tests/std/syntax/printer.test.luau:421:8-13 -tests/std/syntax/printer.test.luau:436:8-13 -tests/std/syntax/printer.test.luau:449:8-13 -tests/std/syntax/printer.test.luau:462:8-13 -tests/std/syntax/printer.test.luau:475:8-13 -tests/std/syntax/printer.test.luau:490:8-13 -tests/std/syntax/printer.test.luau:505:8-13 -tests/std/syntax/printer.test.luau:519:8-13 -tests/std/syntax/printer.test.luau:533:8-13 -tests/std/syntax/printer.test.luau:548:8-13 -tests/std/syntax/printer.test.luau:563:8-13 -tests/std/syntax/printer.test.luau:576:8-13 -tests/std/syntax/printer.test.luau:589:8-13 -tests/std/syntax/printer.test.luau:603:8-13 -tests/std/syntax/printer.test.luau:616:8-13 -tests/std/syntax/printer.test.luau:629:8-13 -tests/std/syntax/printer.test.luau:642:8-13 -tests/std/syntax/printer.test.luau:655:8-13 -tests/std/syntax/printer.test.luau:668:8-13 -tests/std/syntax/printer.test.luau:681:8-13 -tests/std/syntax/printer.test.luau:694:8-13 -tests/std/syntax/printer.test.luau:707:8-13 -tests/std/syntax/printer.test.luau:720:8-13 -tests/std/syntax/printer.test.luau:733:8-13 -tests/std/syntax/printer.test.luau:746:8-13 -tests/std/syntax/printer.test.luau:759:8-13 -tests/std/syntax/printer.test.luau:772:8-13 -tests/std/syntax/printer.test.luau:785:8-13 -tests/std/syntax/printer.test.luau:798:8-13 -tests/std/syntax/printer.test.luau:811:8-13 -tests/std/syntax/printer.test.luau:824:8-13 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:835:38-60 -tests/std/syntax/printer.test.luau:838:8-13 -tests/std/syntax/printer.test.luau:859:8-13 -tests/std/syntax/printer.test.luau:887:23-31 -tests/std/syntax/printer.test.luau:894:8-13 +tests/std/syntax/printer.test.luau:13:10-26 +tests/std/syntax/printer.test.luau:35:8-13 +tests/std/syntax/printer.test.luau:49:8-13 +tests/std/syntax/printer.test.luau:63:8-13 +tests/std/syntax/printer.test.luau:82:8-13 +tests/std/syntax/printer.test.luau:95:10-100 +tests/std/syntax/printer.test.luau:97:7-32 +tests/std/syntax/printer.test.luau:103:8-13 +tests/std/syntax/printer.test.luau:119:8-13 +tests/std/syntax/printer.test.luau:131:8-13 +tests/std/syntax/printer.test.luau:143:8-13 +tests/std/syntax/printer.test.luau:155:8-13 +tests/std/syntax/printer.test.luau:167:8-13 +tests/std/syntax/printer.test.luau:180:8-13 +tests/std/syntax/printer.test.luau:192:8-13 +tests/std/syntax/printer.test.luau:204:8-13 +tests/std/syntax/printer.test.luau:216:8-13 +tests/std/syntax/printer.test.luau:228:8-13 +tests/std/syntax/printer.test.luau:240:8-13 +tests/std/syntax/printer.test.luau:252:8-13 +tests/std/syntax/printer.test.luau:264:8-13 +tests/std/syntax/printer.test.luau:276:8-13 +tests/std/syntax/printer.test.luau:288:8-13 +tests/std/syntax/printer.test.luau:300:8-13 +tests/std/syntax/printer.test.luau:312:8-13 +tests/std/syntax/printer.test.luau:324:8-13 +tests/std/syntax/printer.test.luau:336:8-13 +tests/std/syntax/printer.test.luau:349:8-13 +tests/std/syntax/printer.test.luau:366:8-13 +tests/std/syntax/printer.test.luau:381:8-13 +tests/std/syntax/printer.test.luau:396:8-13 +tests/std/syntax/printer.test.luau:411:8-13 +tests/std/syntax/printer.test.luau:426:8-13 +tests/std/syntax/printer.test.luau:439:8-13 +tests/std/syntax/printer.test.luau:452:8-13 +tests/std/syntax/printer.test.luau:465:8-13 +tests/std/syntax/printer.test.luau:480:8-13 +tests/std/syntax/printer.test.luau:495:8-13 +tests/std/syntax/printer.test.luau:509:8-13 +tests/std/syntax/printer.test.luau:523:8-13 +tests/std/syntax/printer.test.luau:538:8-13 +tests/std/syntax/printer.test.luau:553:8-13 +tests/std/syntax/printer.test.luau:566:8-13 +tests/std/syntax/printer.test.luau:579:8-13 +tests/std/syntax/printer.test.luau:593:8-13 +tests/std/syntax/printer.test.luau:606:8-13 +tests/std/syntax/printer.test.luau:619:8-13 +tests/std/syntax/printer.test.luau:632:8-13 +tests/std/syntax/printer.test.luau:645:8-13 +tests/std/syntax/printer.test.luau:658:8-13 +tests/std/syntax/printer.test.luau:671:8-13 +tests/std/syntax/printer.test.luau:684:8-13 +tests/std/syntax/printer.test.luau:697:8-13 +tests/std/syntax/printer.test.luau:710:8-13 +tests/std/syntax/printer.test.luau:723:8-13 +tests/std/syntax/printer.test.luau:736:8-13 +tests/std/syntax/printer.test.luau:749:8-13 +tests/std/syntax/printer.test.luau:762:8-13 +tests/std/syntax/printer.test.luau:775:8-13 +tests/std/syntax/printer.test.luau:788:8-13 +tests/std/syntax/printer.test.luau:801:8-13 +tests/std/syntax/printer.test.luau:814:8-13 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:825:38-60 +tests/std/syntax/printer.test.luau:828:8-13 +tests/std/syntax/printer.test.luau:849:8-13 +tests/std/syntax/printer.test.luau:877:23-31 +tests/std/syntax/printer.test.luau:884:8-13 tests/std/syntax/query.test.luau:41:7-7 tests/std/syntax/query.test.luau:41:7-7 tests/std/tableext.test.luau:57:18-18 @@ -477,17 +460,15 @@ tests/std/test.test.luau:337:4-9 tools/luthier.luau:162:2-26 tools/luthier.luau:201:3-12 tools/luthier.luau:202:11-25 -tools/luthier.luau:243:8-11 -tools/luthier.luau:296:8-11 -tools/luthier.luau:420:24-68 -tools/luthier.luau:429:3-50 -tools/luthier.luau:547:29-34 -tools/luthier.luau:565:18-27 -tools/luthier.luau:595:3-79 -tools/luthier.luau:595:28-78 -tools/luthier.luau:599:3-75 -tools/luthier.luau:599:28-74 -tools/luthier.luau:610:2-48 -tools/luthier.luau:663:2-11 -tools/luthier.luau:664:10-24 -tools/luthier.luau:794:8-42 +tools/luthier.luau:418:24-68 +tools/luthier.luau:427:3-50 +tools/luthier.luau:545:29-34 +tools/luthier.luau:563:18-27 +tools/luthier.luau:593:3-79 +tools/luthier.luau:593:28-78 +tools/luthier.luau:597:3-75 +tools/luthier.luau:597:28-74 +tools/luthier.luau:608:2-48 +tools/luthier.luau:661:2-11 +tools/luthier.luau:662:10-24 +tools/luthier.luau:792:8-42 diff --git a/tools/luthier.luau b/tools/luthier.luau index ba51fccea..119d0a09c 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -240,7 +240,6 @@ end local function getStdLibHash(): string local libsPath = projectRelative("lute", "std", "libs") - local libs = fs.listdir(libsPath) local toHash = "" @@ -293,7 +292,6 @@ local function generateStdLibFilesIfNeeded() }) local libsPath = projectRelative("lute", "std", "libs") - local libs = fs.listdir(libsPath) local numItems = 0 traverseFileTree(libsPath, function(path, relativePath) From d62387d199a2667378e2ac2856cf99e465d8acfa Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:36:54 -0800 Subject: [PATCH 332/642] Remove `lute helloworld` (#797) This was added back in the early days of Lute, when I first implemented the `CliVfs` and we had no other examples of embedded CLI commands: https://github.com/luau-lang/lute/pull/314. We have no need for `lute helloworld` anymore and can clean this up. --- lute/cli/commands/helloworld/helloworld.luau | 1 - lute/cli/commands/helloworld/init.luau | 2 -- 2 files changed, 3 deletions(-) delete mode 100644 lute/cli/commands/helloworld/helloworld.luau delete mode 100644 lute/cli/commands/helloworld/init.luau diff --git a/lute/cli/commands/helloworld/helloworld.luau b/lute/cli/commands/helloworld/helloworld.luau deleted file mode 100644 index 6312118cf..000000000 --- a/lute/cli/commands/helloworld/helloworld.luau +++ /dev/null @@ -1 +0,0 @@ -return { "Hello, world!" } diff --git a/lute/cli/commands/helloworld/init.luau b/lute/cli/commands/helloworld/init.luau deleted file mode 100644 index d412b61e2..000000000 --- a/lute/cli/commands/helloworld/init.luau +++ /dev/null @@ -1,2 +0,0 @@ -local result = require("@self/helloworld") -print(result[1]) From fd2715122a9d8ec84b85c38d4a64465eaa6319d6 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 10 Feb 2026 10:37:51 -0800 Subject: [PATCH 333/642] Updates lute lint to not parse outputs of fs walker (#794) a small code cleanup doesn't need to be in release notes --- lute/cli/commands/lint/init.luau | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index ba70801fa..89d720211 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -352,14 +352,12 @@ local function lintPaths( local curr = walker() while curr ~= nil do - local inputFilePath = pathLib.parse(curr) - - local success, err = pcall(lintFile, inputFilePath, lintRules, autofixEnabled) + local success, err = pcall(lintFile, curr, lintRules, autofixEnabled) if success then -- Violations are returned as the second return value from pcall - allViolations[inputFilePath] = err + allViolations[curr] = err else - print(`Error linting file '{inputFilePath}': {err}`) + print(`Error linting file '{curr}': {err}`) end curr = walker() From bb9a62abe92c8e239946bdc52045235610b3e47e Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 10 Feb 2026 11:01:10 -0800 Subject: [PATCH 334/642] Bundles the batteries utilities for use by lute cli commands (#792) This PR: - Adds a pass to the build script that bundles the `batteries` library into C++ - Enables the CLI VFS to return required modules from the `@batteries` alias, for use only by lute cli commands - Updates the `doc` and `lint` subcommands to use the @batteries code. - Also fixed a couple of type checking errors in luthier --- CMakeLists.txt | 1 + lute/batteries/CMakeLists.txt | 18 +++ lute/batteries/include/lute/clibatteries.h | 20 +++ lute/batteries/src/clibatteries.cpp | 23 +++ lute/cli/commands/doc/init.luau | 2 +- lute/cli/commands/lib/cli.luau | 175 --------------------- lute/cli/commands/lint/init.luau | 2 +- lute/require/CMakeLists.txt | 2 +- lute/require/include/lute/clivfs.h | 8 + lute/require/src/clivfs.cpp | 66 ++++++-- lute/require/src/requirevfs.cpp | 5 + tools/check-faillist.txt | 34 ++-- tools/luthier.luau | 118 +++++++++++++- 13 files changed, 255 insertions(+), 219 deletions(-) create mode 100644 lute/batteries/CMakeLists.txt create mode 100644 lute/batteries/include/lute/clibatteries.h create mode 100644 lute/batteries/src/clibatteries.cpp delete mode 100644 lute/cli/commands/lib/cli.luau diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ce241956..fbb211eb4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,6 +130,7 @@ else() endif() # libraries +add_subdirectory(lute/batteries) add_subdirectory(lute/common) add_subdirectory(lute/require) add_subdirectory(lute/runtime) diff --git a/lute/batteries/CMakeLists.txt b/lute/batteries/CMakeLists.txt new file mode 100644 index 000000000..83d7e046f --- /dev/null +++ b/lute/batteries/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(Lute.Batteries STATIC) + +target_sources(Lute.Batteries PRIVATE + generated/batteries.h + generated/batteries.cpp + + include/lute/clibatteries.h + src/clibatteries.cpp +) + +target_compile_features(Lute.Batteries PUBLIC cxx_std_17) +target_include_directories(Lute.Batteries PUBLIC include generated) +target_link_libraries(Lute.Batteries PRIVATE uv_a) +target_compile_options(Lute.Batteries PRIVATE ${LUTE_OPTIONS}) + +set(BATTERIES_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") +file(MAKE_DIRECTORY "${BATTERIES_GENERATED_INCLUDE_DIR}/lute") + diff --git a/lute/batteries/include/lute/clibatteries.h b/lute/batteries/include/lute/clibatteries.h new file mode 100644 index 000000000..55d8ea760 --- /dev/null +++ b/lute/batteries/include/lute/clibatteries.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +enum class BatteryModuleType +{ + Module, + Directory, + NotFound, +}; + +struct BatteryModuleResult +{ + BatteryModuleType type; + std::string_view contents; +}; + +BatteryModuleResult getBatteryModule(std::string_view path); diff --git a/lute/batteries/src/clibatteries.cpp b/lute/batteries/src/clibatteries.cpp new file mode 100644 index 000000000..041b4144b --- /dev/null +++ b/lute/batteries/src/clibatteries.cpp @@ -0,0 +1,23 @@ +#include "lute/clibatteries.h" + +// This file provides batteries for the cli commands + +#include + +#include "batteries.h" + +BatteryModuleResult getBatteryModule(std::string_view path) +{ + for (const auto& [pathInLib, contents] : lutebatteries) + { + if (path != pathInLib) + continue; + + if (contents == "#directory") + return {BatteryModuleType::Directory}; + else + return {BatteryModuleType::Module, contents}; + } + + return {BatteryModuleType::NotFound}; +} diff --git a/lute/cli/commands/doc/init.luau b/lute/cli/commands/doc/init.luau index 4575efab7..38b2d5ee3 100644 --- a/lute/cli/commands/doc/init.luau +++ b/lute/cli/commands/doc/init.luau @@ -1,4 +1,4 @@ -local cli = require("./lib/cli") +local cli = require("@batteries/cli") local fs = require("@std/fs") local path = require("@std/path") local process = require("@std/process") diff --git a/lute/cli/commands/lib/cli.luau b/lute/cli/commands/lib/cli.luau deleted file mode 100644 index 178637f82..000000000 --- a/lute/cli/commands/lib/cli.luau +++ /dev/null @@ -1,175 +0,0 @@ -local cli = {} -cli.__index = cli - -type ArgKind = "positional" | "flag" | "option" -type ArgOptions = { - help: string?, - aliases: { string }?, - default: string?, - required: boolean?, -} - -type ArgData = { - name: string, - kind: ArgKind, - options: ArgOptions, -} - -type ParseResult = { - values: { [string]: string }, - flags: { [string]: boolean }, - fwdArgs: { string }, -} - -type ParserData = { - arguments: { [number]: ArgData }, - positional: { [number]: ArgData }, - parsed: ParseResult, -} - -type ParserInterface = typeof(cli) -export type Parser = setmetatable - -function cli.parser(): Parser - local self = { - arguments = {}, - positional = {}, - parsed = { values = {}, flags = {}, fwdArgs = {} }, - } - - return setmetatable(self, cli) -end - -function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions): () - local argument = { - name = name, - kind = kind, - options = options or { aliases = {}, required = false }, - } - - table.insert(self.arguments, argument) - if kind == "positional" then - table.insert(self.positional, argument) - end -end - -function cli.parse(self: Parser, args: { string }): () - local i = 0 - local pos_index = 1 - - while i < #args do - i += 1 - - local arg = args[i] - - -- if the argument is exactly "--", we pass everything along - if arg == "--" then - table.move(args, i + 1, #args, 1, self.parsed.fwdArgs) - break - end - - -- if the argument starts with two dashes, we're parsing either a flag or an option - if string.sub(arg, 1, 2) == "--" then - local name = string.sub(arg, 3) - local found = false - - for _, argument in self.arguments do - local aliases = argument.options.aliases or {} - - if argument.name == name or table.find(aliases, name) then - found = true - - if argument.kind == "option" then - -- advance past the argument - i += 1 - - assert(i <= #args, "Missing value for argument: " .. argument.name) - self.parsed.values[argument.name] = args[i] - - break - end - - self.parsed.flags[argument.name] = true - break - end - end - - assert(found, "Unknown argument: " .. name) - continue - end - - -- if the argument starts with a single dash, we're parsing a flag - if string.sub(arg, 1, 1) == "-" then - local flags = string.sub(arg, 2) - - for j = 1, #flags do - local name = string.sub(flags, j, j) - local found = false - for _, argument in self.arguments do - local aliases = argument.options.aliases or {} - if argument.name == name or table.find(aliases, name) then - found = true - if argument.kind == "option" then - i += 1 - assert(i <= #args, "Missing value for argument: " .. argument.name) - self.parsed.values[argument.name] = args[i] - else - self.parsed.flags[argument.name] = true - end - break - end - end - - assert(found, "Unknown argument: " .. name) - end - - continue - end - - -- if we have positional arguments left, we can take this argument as one - if pos_index <= #self.positional then - self.parsed.values[self.positional[pos_index].name] = arg - pos_index += 1 - continue - end - - -- otherwise, the argument is forwarded on - table.insert(self.parsed.fwdArgs, arg) - end - - -- check that all required arguments are present, and set any default values as needed - for _, argument in self.arguments do - assert(argument) - - if argument.options.required and self.parsed.values[argument.name] == nil then - assert(self.parsed.values[argument.name], "Missing required argument: " .. argument.name) - end - - if self.parsed.values[argument.name] == nil and argument.options.default then - self.parsed.values[argument.name] = argument.options.default - end - end -end - -function cli.get(self: Parser, name: string): string? - return self.parsed.values[name] -end - -function cli.has(self: Parser, name: string): boolean - return self.parsed.flags[name] ~= nil -end - -function cli.forwarded(self: Parser): { string }? - return self.parsed.fwdArgs -end - -function cli.help(self: Parser): () - print("Usage:") - for _, argument in self.arguments do - local aliasStr = table.concat(argument.options.aliases or {}, ", ") - local reqStr = if argument.options.required then "(required) " else "" - print(string.format(" --%s (-%s) - %s%s", argument.name, aliasStr, reqStr, argument.options.help or "")) - end -end - -return table.freeze(cli) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 89d720211..db7b01daa 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -1,4 +1,4 @@ -local cli = require("./lib/cli") +local cli = require("@batteries/cli") local files = require("./lib/files") local fs = require("@std/fs") local json = require("@std/json") diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index d5bd07276..894f4358f 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -28,5 +28,5 @@ target_sources(Lute.Require PRIVATE target_compile_features(Lute.Require PUBLIC cxx_std_17) target_include_directories(Lute.Require PUBLIC include) -target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Std Lute.CLI.Commands Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Runtime) +target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Std Lute.CLI.Commands Lute.Batteries Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Runtime) target_compile_options(Lute.Require PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/require/include/lute/clivfs.h b/lute/require/include/lute/clivfs.h index c1c33b20b..b2a8d48b1 100644 --- a/lute/require/include/lute/clivfs.h +++ b/lute/require/include/lute/clivfs.h @@ -11,6 +11,7 @@ class CliVfs NavigationStatus toParent(); NavigationStatus toChild(const std::string& name); + NavigationStatus toAliasFallback(std::string_view aliasUnprefixed); bool isModulePresent() const; std::string getIdentifier() const; @@ -21,4 +22,11 @@ class CliVfs private: std::optional modulePath; + + enum class Tracking + { + Batteries, + CLI + }; + Tracking alias = Tracking::CLI; }; diff --git a/lute/require/src/clivfs.cpp b/lute/require/src/clivfs.cpp index ce3c7f6b7..787721121 100644 --- a/lute/require/src/clivfs.cpp +++ b/lute/require/src/clivfs.cpp @@ -1,11 +1,41 @@ #include "lute/clivfs.h" +#include "lute/clibatteries.h" #include "lute/clicommands.h" +#include "lute/modulepath.h" #include "Luau/Common.h" #include +constexpr std::string_view kBatteriesAliasPrefix = "@batteries"; +constexpr std::string_view kBatteriesPrefix = "batteries"; + +constexpr std::string_view kCliAliasPrefix = "@cli"; + +static bool isBatteryModule(const std::string& path) +{ + BatteryModuleResult result = getBatteryModule(path); + return result.type == BatteryModuleType::Module; +} + +static std::optional readBatteryModule(const std::string& path) +{ + BatteryModuleResult result = getBatteryModule(path); + if (result.type == BatteryModuleType::Module) + return std::string(result.contents); + return std::nullopt; +} + +static bool isBatteryDirectory(const std::string& path) +{ + if (path == kBatteriesAliasPrefix) + return true; + + BatteryModuleResult result = getBatteryModule(path); + return result.type == BatteryModuleType::Directory; +} + static bool isCliModule(const std::string& path) { CliModuleResult result = getCliModule(path); @@ -23,7 +53,7 @@ static std::optional readCliModule(const std::string& path) static bool isCliDirectory(const std::string& path) { - if (path == "@cli") + if (path == kCliAliasPrefix) return true; CliModuleResult result = getCliModule(path); @@ -32,19 +62,34 @@ static bool isCliDirectory(const std::string& path) NavigationStatus CliVfs::resetToPath(const std::string& path) { - if (path == "@cli") + alias = path.rfind(kBatteriesAliasPrefix, 0) == 0 ? Tracking::Batteries : Tracking::CLI; + + if (path == kBatteriesAliasPrefix) { - modulePath = ModulePath::create("@cli", "", isCliModule, isCliDirectory); + modulePath = ModulePath::create(std::string(kBatteriesAliasPrefix), "", isBatteryModule, isBatteryDirectory); return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; } + else + { + auto modulePrefix = std::string(alias == Tracking::Batteries ? kBatteriesAliasPrefix : kCliAliasPrefix); + auto modFunc = alias == Tracking::Batteries ? isBatteryModule : isCliModule; + auto dirFunc = alias == Tracking::Batteries ? isBatteryDirectory : isCliDirectory; + if (path.rfind(modulePrefix, 0) == 0) + { + modulePath = ModulePath::create(modulePrefix, path.substr(modulePrefix.size() + 1), modFunc, dirFunc); + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; + } + } - std::string cliPrefix = "@cli/"; + return NavigationStatus::NotFound; +} - if (path.find_first_of(cliPrefix) != 0) - return NavigationStatus::NotFound; +NavigationStatus CliVfs::toAliasFallback(std::string_view aliasUnprefixed) +{ + if (aliasUnprefixed == kBatteriesPrefix) + return resetToPath(std::string(kBatteriesAliasPrefix)); - modulePath = ModulePath::create("@cli", path.substr(cliPrefix.size()), isCliModule, isCliDirectory); - return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; + return NavigationStatus::NotFound; } NavigationStatus CliVfs::toParent() @@ -61,7 +106,8 @@ NavigationStatus CliVfs::toChild(const std::string& name) bool CliVfs::isModulePresent() const { - return getCliModule(getIdentifier()).type == CliModuleType::Module; + auto id = getIdentifier(); + return alias == Tracking::Batteries ? getBatteryModule(id).type == BatteryModuleType::Module : getCliModule(id).type == CliModuleType::Module; } std::string CliVfs::getIdentifier() const @@ -74,7 +120,7 @@ std::string CliVfs::getIdentifier() const std::optional CliVfs::getContents(const std::string& path) const { - return readCliModule(path); + return alias == Tracking::Batteries ? readBatteryModule(path) : readCliModule(path); } ConfigStatus CliVfs::getConfigStatus() const diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 278e21081..9d3f66e3c 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -104,6 +104,11 @@ NavigationStatus RequireVfs::toAliasOverride(lua_State* L, std::string_view alia NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) { + if (vfsType == VFSType::Cli) + { + LUAU_ASSERT(cliVfs); + return cliVfs->toAliasFallback(aliasUnprefixed); + } return NavigationStatus::NotFound; } diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 2ae996c77..05a5bc418 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -61,14 +61,6 @@ lute/cli/commands/doc/init.luau:204:10-24 lute/cli/commands/doc/init.luau:310:5-16 lute/cli/commands/doc/init.luau:315:5-16 lute/cli/commands/doc/init.luau:321:10-24 -lute/cli/commands/lib/cli.luau:50:2-13 -lute/cli/commands/lib/cli.luau:50:2-13 -lute/cli/commands/lib/cli.luau:52:3-14 -lute/cli/commands/lib/cli.luau:52:3-14 -lute/cli/commands/lib/cli.luau:144:36-75 -lute/cli/commands/lib/cli.luau:144:36-75 -lute/cli/commands/lib/cli.luau:148:6-45 -lute/cli/commands/lib/cli.luau:148:6-45 lute/cli/commands/lint/init.luau:52:10-18 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 @@ -457,18 +449,14 @@ tests/std/test.test.luau:282:4-9 tests/std/test.test.luau:299:4-9 tests/std/test.test.luau:318:4-9 tests/std/test.test.luau:337:4-9 -tools/luthier.luau:162:2-26 -tools/luthier.luau:201:3-12 -tools/luthier.luau:202:11-25 -tools/luthier.luau:418:24-68 -tools/luthier.luau:427:3-50 -tools/luthier.luau:545:29-34 -tools/luthier.luau:563:18-27 -tools/luthier.luau:593:3-79 -tools/luthier.luau:593:28-78 -tools/luthier.luau:597:3-75 -tools/luthier.luau:597:28-74 -tools/luthier.luau:608:2-48 -tools/luthier.luau:661:2-11 -tools/luthier.luau:662:10-24 -tools/luthier.luau:792:8-42 +tools/luthier.luau:158:2-26 +tools/luthier.luau:197:3-12 +tools/luthier.luau:198:11-25 +tools/luthier.luau:695:3-79 +tools/luthier.luau:695:28-78 +tools/luthier.luau:699:3-75 +tools/luthier.luau:699:28-74 +tools/luthier.luau:710:2-48 +tools/luthier.luau:763:2-11 +tools/luthier.luau:764:10-24 +tools/luthier.luau:894:8-42 diff --git a/tools/luthier.luau b/tools/luthier.luau index 119d0a09c..fa5df2cf5 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -4,13 +4,9 @@ local crypto = require("@lute/crypto") local fs = require("@lute/fs") local process = require("@lute/process") local system = require("@lute/system") - local cli = require("@batteries/cli") local toml = require("@batteries/toml") -local TYPEDEF_PATTERN = "return {%*}" -local DICTIONARY_PATTERN = '["%*"] = [==[%*]==],' - type Tune = { dependency: { name: string, @@ -270,6 +266,108 @@ local function writeLines(file, lines: { string }) fs.write(file, table.concat(lines, "\n") .. "\n") end +local luteBatteriesDir = projectRelative("lute", "batteries", "generated") +local batteriesHashFile = projectRelative("lute", "batteries", "generated", "hash.txt") + +local function getBatteriesHash(): string + local toHash = "" + traverseFileTree("batteries/", function(path, relativePath) + if safeFsType(path) == "file" then + toHash ..= "./" .. relativePath + toHash ..= readFileToString(path) + end + end) + + local digest = crypto.digest(crypto.hash.blake2b256, toHash) + return hexify(digest) +end + +local function isBatteriesUpToDate(): boolean + if safeFsType(batteriesHashFile) ~= "file" then + return false + end + local hash = readFileToString(batteriesHashFile) + return hash == getBatteriesHash() +end + +local function generateBatteriesCommandsIfNeeded() + pcall(fs.mkdir, luteBatteriesDir) + + if isBatteriesUpToDate() then + return + end + + print("Generating code for Batteries, files are out of date.") + + local cpp = fs.open(joinPath(luteBatteriesDir, "batteries.cpp"), "w+") + + writeLines(cpp, { + "// This file is auto-generated by luthier.luau. Do not edit.", + "// Instead, you should modify the source files in `batteries`.", + "", + '#include "batteries.h"', + "", + "constexpr std::pair lutebatteries[] = {", + }) + + local libsPath = projectRelative("batteries") + local numItems = 0 + + traverseFileTree(libsPath, function(path, relativePath) + if #relativePath == 0 then + return -- Skip the root directory + end + + local aliasedPath = "@batteries/" .. relativePath + + if safeFsType(path) == "file" then + local libSrc = readFileToString(path) + if #libSrc > 16_000 then + -- https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026 + -- MSVC can't handle a single string literal being over 16380 single-byte characters, + -- but can if they are adjacent contatenated strings. So, we split the string line-by-line into + -- adjacent substrings. + local lines = string.split(libSrc, "\n") + local generatedLines = table.create(#lines) + for _, line in lines do + local escapedLine = string.gsub(line, "\\", "\\\\") + escapedLine = string.gsub(escapedLine, '"', '\\"') + escapedLine = string.gsub(escapedLine, "\r", "") + table.insert(generatedLines, `"{escapedLine}\\n"`) + end + fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) + else + fs.write(cpp, `\n \{"{aliasedPath}", R"LUTE_BATTERIES({libSrc})LUTE_BATTERIES"},\n`) + end + end + + if safeFsType(path) == "dir" then + fs.write(cpp, `\n \{"{aliasedPath}", "#directory"},\n`) + end + + numItems += 1 + end) + + fs.write(cpp, "};\n") + fs.close(cpp) + + local header = fs.open(joinPath(luteBatteriesDir, "batteries.h"), "w+") + writeLines(header, { + "// This file is auto-generated by luthier.luau. Do not edit.", + "#pragma once", + "", + "#include ", + "#include ", + "", + `extern const std::pair lutebatteries[{numItems}];`, + }) + fs.close(header) + + local hash = fs.open(batteriesHashFile, "w+") + fs.write(hash, getBatteriesHash()) + fs.close(hash) +end + local function generateStdLibFilesIfNeeded() local generatedPath = projectRelative("lute", "std", "src", "generated") pcall(fs.mkdir, generatedPath) @@ -346,6 +444,7 @@ local function generateStdLibFilesIfNeeded() local hash = fs.open(joinPath(generatedPath, "hash.txt"), "w+") fs.write(hash, getStdLibHash()) + fs.close(hash) end @@ -413,9 +512,8 @@ local function generateTypeDefinitionFiles() readFileToString(projectRelative("lute", "cli", "commands", "lint", "types.luau")) local stringDictionary = "" - for key, value in dictionaryEntries do - stringDictionary ..= string.format(DICTIONARY_PATTERN, key, value) + stringDictionary ..= string.format('["%*"] = [==[%*]==],', key, value) end local hash = fs.open(joinPath(generatedPath, "hash.txt"), "w+") @@ -424,7 +522,7 @@ local function generateTypeDefinitionFiles() writeStringToFile( projectRelative("lute", "cli", "commands", "setup", "generated", "definitions.luau"), - string.format(TYPEDEF_PATTERN, stringDictionary) + string.format("return {%*}", stringDictionary) ) end @@ -538,10 +636,12 @@ local function generateFilesIfNeeded() generateStdLibFilesIfNeeded() generateTypeDefinitionFiles() generateCliCommandsFilesIfNeeded() + generateBatteriesCommandsIfNeeded() end local function getExePath(): string local target = args:get("target") + assert(target ~= nil) local exeName = getExeName(target) return joinPath(getProjectPath(), exeName) @@ -560,7 +660,9 @@ local function call(cmd: { string }, passedCwd: string?): number end local function build() - local exeName = getExeName(args:get("target")) + local target = args:get("target") + assert(target ~= nil) + local exeName = getExeName(target) local projectPath = getProjectPath() if args:has("clean") then From af58f26b689330f848ccee9a4dace9eb29815db4 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:36:48 -0500 Subject: [PATCH 335/642] Separate ignore and glob parsing into isolated module (#796) To support the implementation of `lute lint` configuration system, where we'd like to allow users to specify file ignores with an array of globs, this PR separates `ignore.luau` logic used to parse ignores and created ignoreData objects into their own module. There should be no logical change to `IgnoreResolver`, the changes simply ensure we can use `parseIgnoreContents` and `isIgnored` in isolation. --- lute/cli/commands/lib/ignore.luau | 187 +---------------------- lute/cli/commands/lib/parseIgnores.luau | 190 ++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 182 deletions(-) create mode 100644 lute/cli/commands/lib/parseIgnores.luau diff --git a/lute/cli/commands/lib/ignore.luau b/lute/cli/commands/lib/ignore.luau index 6bba90665..5e79cc12f 100644 --- a/lute/cli/commands/lib/ignore.luau +++ b/lute/cli/commands/lib/ignore.luau @@ -1,22 +1,16 @@ local fs = require("@std/fs") local path = require("@std/path") -local stringext = require("@std/stringext") -local system = require("@std/system") - -local separator = if system.win32 then "\\" else "/" +local parseIgnores = require("./parseIgnores") +local isIgnored = parseIgnores.isIgnored +local parseGitignore = parseIgnores.parseGitignore --- Performs ignore resolution on a set of local IgnoreResolver = {} IgnoreResolver.__index = IgnoreResolver -type Glob = { pattern: string, onlyMatchDirectories: boolean, matchAnywhere: boolean } +type Glob = parseIgnores.Glob -type GitignoreData = { - location: string, - ignores: { Glob }, - whitelists: { Glob }, - next: GitignoreData?, -} +type GitignoreData = parseIgnores.GitignoreData type IgnoreResolverData = { _ignoreCache: { [string]: GitignoreData }, @@ -32,177 +26,6 @@ function IgnoreResolver.new(): IgnoreResolver return setmetatable(self, IgnoreResolver) end ---- Converts a glob pattern into a Luau string pattern for efficient matching ---- The output pattern should be matched against a filepath that is relative to the .gitignore location -local function parseGlob(glob: string): Glob - local onlyMatchDirectories = false - - if glob:sub(-1) == "/" then - onlyMatchDirectories = true - glob = glob:sub(1, -2) - end - - local lua_parts = {} - local matchAnywhere = false - local idx = 1 - local len = #glob - - -- If there is a separator at the beginning or middle (or both) of the pattern, then the pattern is relative to - -- the directory level of the particular .gitignore file itself. - -- i.e., anchor the pattern to the start - if glob:find("/") then - table.insert(lua_parts, "^") - if glob:sub(1, 2) == "./" then - idx = 3 - elseif glob:sub(1, 1) == "/" or glob:sub(1, 1) == "." then - idx = 2 - end - else - -- If no leading slash, it can match anywhere, including after a directory. - -- This means it can start at the beginning of the string, or after a slash. - -- We can't match for two distinct patterns bc Lua lacks an OR pattern operator - -- Instead add unique marker we check for in matchesGlob, indicating we match - -- against the two distinct patterns: anchored at beginning, or after a slash - matchAnywhere = true - end - - while idx <= len do - local char = glob:sub(idx, idx) - if char == "*" then - if glob:sub(idx, idx + 1) == "**" then - if idx == 1 and glob:sub(idx + 2, idx + 2) == "/" then - -- Leading '**/' at start of the file. If we've already determined matchAnywhere, - -- we don't need to add an extra character match - if matchAnywhere then - idx = idx + 2 - continue - end - end - if glob:sub(idx + 2, idx + 2) == "/" then - -- ** between slashes (e.g., a/**/b) - -- Zero or more directory segments - match everything, and don't include the following `/` slash - table.insert(lua_parts, ".*") - idx = idx + 3 - else - -- Trailing ** (e.g., foo/**) or just `**` - table.insert(lua_parts, ".*") -- Matches anything, including slashes - idx = idx + 2 - end - else - -- Single asterisk `*` (matches anything except directory separator) - table.insert(lua_parts, `[^{separator}]*`) - idx = idx + 1 - end - elseif char == "?" then - -- Match any single character, except for directory separator - table.insert(lua_parts, `[^{separator}]`) - idx = idx + 1 - elseif char == "-" or char == "." then - table.insert(lua_parts, `%{char}`) - idx = idx + 1 - elseif char == "/" or char == "\\" then - table.insert(lua_parts, separator) - idx = idx + 1 - else - table.insert(lua_parts, char) - idx = idx + 1 - end - end - - -- Add the ending anchor if not already an implicit `.*` from `**` - if glob:sub(-2) ~= "**" then - table.insert(lua_parts, "$") - end - - return { - pattern = table.concat(lua_parts), - onlyMatchDirectories = onlyMatchDirectories, - matchAnywhere = matchAnywhere, - } -end - -local function parseGitignoreContents(location: string, contents: string): GitignoreData - local lines = contents:split("\n") - - local ignoreData: GitignoreData = { - location = location, - ignores = {}, - whitelists = {}, - } - - for _, line in lines do - line = stringext.trim(line) - - if line == "" or stringext.hasprefix(line, "#") then - continue - end - - if stringext.hasprefix(line, "!") then - local globPattern = stringext.removeprefix(line, "!") - local glob = parseGlob(globPattern) - table.insert(ignoreData.whitelists, glob) - else - local glob = parseGlob(line) - table.insert(ignoreData.ignores, glob) - end - end - - return ignoreData -end - -local function parseGitignore(filepath: path.path): GitignoreData - local contents = fs.readfiletostring(path.format(filepath)) - local parentDirectory = if #filepath.parts > 0 then path.dirname(filepath) else nil - assert(parentDirectory) - return parseGitignoreContents(parentDirectory, contents) -end - -local function matchesGlob(glob: Glob, filepath: string, isDirectory: boolean): boolean - if glob.onlyMatchDirectories and not isDirectory then - return false - end - -- If the pattern is marked with matchAnywhere, we need to test - -- both: at start of path OR after any directory separator - if glob.matchAnywhere then - local matchAtStart = filepath:match("^" .. glob.pattern) ~= nil - local matchAfterDir = filepath:match(separator .. glob.pattern) ~= nil - - return matchAtStart or matchAfterDir - else - return filepath:match(glob.pattern) ~= nil - end -end - -local function isIgnored(ignoreData: GitignoreData, filepath: string, isDirectory: boolean) - local matched, ignored = false, false - - local relativePath = path.format(path.relative(ignoreData.location, filepath)) - - for _, ignore in ignoreData.ignores do - if matchesGlob(ignore, relativePath, isDirectory) then - matched = true - ignored = true - break - end - end - - if ignored then - for _, whitelist in ignoreData.whitelists do - if matchesGlob(whitelist, filepath, isDirectory) then - matched = true - ignored = false - break - end - end - end - - if not matched and ignoreData.next then - return isIgnored(ignoreData.next, filepath, isDirectory) - end - - return ignored -end - --- Checks whether the given file matches an ignore function IgnoreResolver.isIgnoredFile(self: IgnoreResolver, filepath: path.path) local directory = if #filepath.parts > 0 then path.dirname(filepath) else nil diff --git a/lute/cli/commands/lib/parseIgnores.luau b/lute/cli/commands/lib/parseIgnores.luau new file mode 100644 index 000000000..42bb076bb --- /dev/null +++ b/lute/cli/commands/lib/parseIgnores.luau @@ -0,0 +1,190 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local stringext = require("@std/stringext") +local system = require("@std/system") + +export type Glob = { pattern: string, onlyMatchDirectories: boolean, matchAnywhere: boolean } + +export type GitignoreData = { + location: string, + ignores: { Glob }, + whitelists: { Glob }, + next: GitignoreData?, +} + +local separator = if system.win32 then "\\" else "/" + +local parseIgnores = {} + +--- Converts a glob pattern into a Luau string pattern for efficient matching +--- The output pattern should be matched against a filepath that is relative to the .gitignore location +local function parseGlob(glob: string): Glob + local onlyMatchDirectories = false + + if glob:sub(-1) == "/" then + onlyMatchDirectories = true + glob = glob:sub(1, -2) + end + + local lua_parts = {} + local matchAnywhere = false + local idx = 1 + local len = #glob + + -- If there is a separator at the beginning or middle (or both) of the pattern, then the pattern is relative to + -- the directory level of the particular .gitignore file itself. + -- i.e., anchor the pattern to the start + if glob:find("/") then + table.insert(lua_parts, "^") + if glob:sub(1, 2) == "./" then + idx = 3 + elseif glob:sub(1, 1) == "/" or glob:sub(1, 1) == "." then + idx = 2 + end + else + -- If no leading slash, it can match anywhere, including after a directory. + -- This means it can start at the beginning of the string, or after a slash. + -- We can't match for two distinct patterns bc Lua lacks an OR pattern operator + -- Instead add unique marker we check for in matchesGlob, indicating we match + -- against the two distinct patterns: anchored at beginning, or after a slash + matchAnywhere = true + end + + while idx <= len do + local char = glob:sub(idx, idx) + if char == "*" then + if glob:sub(idx, idx + 1) == "**" then + if idx == 1 and glob:sub(idx + 2, idx + 2) == "/" then + -- Leading '**/' at start of the file. If we've already determined matchAnywhere, + -- we don't need to add an extra character match + if matchAnywhere then + idx = idx + 2 + continue + end + end + if glob:sub(idx + 2, idx + 2) == "/" then + -- ** between slashes (e.g., a/**/b) + -- Zero or more directory segments - match everything, and don't include the following `/` slash + table.insert(lua_parts, ".*") + idx = idx + 3 + else + -- Trailing ** (e.g., foo/**) or just `**` + table.insert(lua_parts, ".*") -- Matches anything, including slashes + idx = idx + 2 + end + else + -- Single asterisk `*` (matches anything except directory separator) + table.insert(lua_parts, `[^{separator}]*`) + idx = idx + 1 + end + elseif char == "?" then + -- Match any single character, except for directory separator + table.insert(lua_parts, `[^{separator}]`) + idx = idx + 1 + elseif char == "-" or char == "." then + table.insert(lua_parts, `%{char}`) + idx = idx + 1 + elseif char == "/" or char == "\\" then + table.insert(lua_parts, separator) + idx = idx + 1 + else + table.insert(lua_parts, char) + idx = idx + 1 + end + end + + -- Add the ending anchor if not already an implicit `.*` from `**` + if glob:sub(-2) ~= "**" then + table.insert(lua_parts, "$") + end + + return { + pattern = table.concat(lua_parts), + onlyMatchDirectories = onlyMatchDirectories, + matchAnywhere = matchAnywhere, + } +end + +function parseIgnores.parseIgnoreContents(location: string, contents: string | { string }): GitignoreData + local lines = if typeof(contents) == "string" then contents:split("\n") else contents + + local ignoreData: GitignoreData = { + location = location, + ignores = {}, + whitelists = {}, + } + + for _, line in lines do + line = stringext.trim(line) + + if line == "" or stringext.hasprefix(line, "#") then + continue + end + + if stringext.hasprefix(line, "!") then + local globPattern = stringext.removeprefix(line, "!") + local glob = parseGlob(globPattern) + table.insert(ignoreData.whitelists, glob) + else + local glob = parseGlob(line) + table.insert(ignoreData.ignores, glob) + end + end + + return ignoreData +end + +function parseIgnores.parseGitignore(filepath: path.path): GitignoreData + local contents = fs.readfiletostring(path.format(filepath)) + local parentDirectory = if #filepath.parts > 0 then path.dirname(filepath) else nil + assert(parentDirectory) + return parseIgnores.parseIgnoreContents(parentDirectory, contents) +end + +function parseIgnores.matchesGlob(glob: Glob, filepath: string, isDirectory: boolean): boolean + if glob.onlyMatchDirectories and not isDirectory then + return false + end + -- If the pattern is marked with matchAnywhere, we need to test + -- both: at start of path OR after any directory separator + if glob.matchAnywhere then + local matchAtStart = filepath:match("^" .. glob.pattern) ~= nil + local matchAfterDir = filepath:match(separator .. glob.pattern) ~= nil + + return matchAtStart or matchAfterDir + else + return filepath:match(glob.pattern) ~= nil + end +end + +function parseIgnores.isIgnored(ignoreData: GitignoreData, filepath: string, isDirectory: boolean) + local matched, ignored = false, false + + local relativePath = path.format(path.relative(ignoreData.location, filepath)) + + for _, ignore in ignoreData.ignores do + if parseIgnores.matchesGlob(ignore, relativePath, isDirectory) then + matched = true + ignored = true + break + end + end + + if ignored then + for _, whitelist in ignoreData.whitelists do + if parseIgnores.matchesGlob(whitelist, filepath, isDirectory) then + matched = true + ignored = false + break + end + end + end + + if not matched and ignoreData.next then + return parseIgnores.isIgnored(ignoreData.next, filepath, isDirectory) + end + + return ignored +end + +return table.freeze(parseIgnores) From 06ef228aaaae0a01cfc6b373adb2a7252b6de0e6 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 10 Feb 2026 13:06:55 -0800 Subject: [PATCH 336/642] Fixes bootstrap builds failing due to not creating an empty batteries file (#798) Added the batteries templates and updated the bootstrap script. --- tools/bootstrap.sh | 7 +++++++ tools/templates/batteries_header.h | 7 +++++++ tools/templates/batteries_impl.cpp | 6 ++++++ 3 files changed, 20 insertions(+) create mode 100644 tools/templates/batteries_header.h create mode 100644 tools/templates/batteries_impl.cpp diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh index 6dd717cb0..9fae2ac7b 100755 --- a/tools/bootstrap.sh +++ b/tools/bootstrap.sh @@ -75,6 +75,13 @@ mkdir -p lute/cli/generated exe cp ./tools/templates/cli_impl.cpp ./lute/cli/generated/commands.cpp exe cp ./tools/templates/cli_header.h ./lute/cli/generated/commands.h +# place empty versions of the batteries library +rm -rf lute/batteries/generated +mkdir -p lute/batteries/generated + +exe cp ./tools/templates/batteries_impl.cpp ./lute/batteries/generated/batteries.cpp +exe cp ./tools/templates/batteries_header.h ./lute/batteries/generated/batteries.h + ## configure the build system for lute0 os_type="$(uname)" BUILD_ROOT="" diff --git a/tools/templates/batteries_header.h b/tools/templates/batteries_header.h new file mode 100644 index 000000000..d99eadb9e --- /dev/null +++ b/tools/templates/batteries_header.h @@ -0,0 +1,7 @@ +// This file is auto-generated by luthier.luau. Do not edit. +#pragma once + +#include +#include + +extern const std::pair lutebatteries[1]; diff --git a/tools/templates/batteries_impl.cpp b/tools/templates/batteries_impl.cpp new file mode 100644 index 000000000..068b40bf0 --- /dev/null +++ b/tools/templates/batteries_impl.cpp @@ -0,0 +1,6 @@ +// This file is auto-generated by luthier.luau. Do not edit. +// Instead, you should modify the source files in `batteries`. + +#include "batteries.h" + +constexpr std::pair lutebatteries[] = {{"", ""}}; From bf407e1253cd3e710c6021d44ca08e478265a3e6 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:45:40 -0800 Subject: [PATCH 337/642] Net library: fix undefined behavior from calling `writeFunction` with wrong expected parameter type (#800) Attempted fix for #799's failing CI run ([link](https://github.com/luau-lang/lute/actions/runs/21885285925/job/63179451762)). --- lute/net/src/net.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 498d024fc..866f2c029 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -37,13 +37,11 @@ struct CurlResponse } }; -static size_t writeFunction(void* contents, size_t size, size_t nmemb, void* context) +static size_t writeFunction(char* ptr, size_t size, size_t nmemb, void* userdata) { - std::vector& target = *(std::vector*)context; + std::vector* target = static_cast*>(userdata); size_t fullsize = size * nmemb; - - target.insert(target.end(), (char*)contents, (char*)contents + fullsize); - + target->insert(target->end(), ptr, ptr + fullsize); return fullsize; } From 56bf72d89012c7d16b616cee04345c08cf421029 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:22:59 -0800 Subject: [PATCH 338/642] Define `LUTE_ASSERT` and `LUTE_UNREACHABLE` in Lute.Common library (#803) Defines `LUTE_ASSERT` and `LUTE_UNREACHABLE` in a new `common.h` file in Lute.Common, and mass replaces all instances of the `LUAU_*` versions to the `LUTE_*` versions. --- lute/cli/src/packagerun.cpp | 3 +- lute/common/CMakeLists.txt | 3 +- lute/common/include/lute/common.h | 6 +++ lute/common/include/lute/fileutils.h | 1 - lute/luau/CMakeLists.txt | 2 +- lute/luau/src/luau.cpp | 67 ++++++++++++++-------------- lute/luau/src/type.cpp | 10 +++-- lute/net/CMakeLists.txt | 2 +- lute/net/src/net.cpp | 3 ++ lute/process/CMakeLists.txt | 2 +- lute/process/src/process.cpp | 3 +- lute/require/CMakeLists.txt | 2 +- lute/require/src/bundlevfs.cpp | 14 +++--- lute/require/src/clivfs.cpp | 9 ++-- lute/require/src/filevfs.cpp | 27 +++++------ lute/require/src/lutevfs.cpp | 13 +++--- lute/require/src/require.cpp | 3 +- lute/require/src/requirevfs.cpp | 47 +++++++++---------- lute/require/src/stdlibvfs.cpp | 9 ++-- lute/require/src/userlandvfs.cpp | 19 ++++---- lute/runtime/CMakeLists.txt | 2 +- lute/runtime/include/lute/uvutils.h | 2 - lute/runtime/src/runtime.cpp | 5 +-- 23 files changed, 136 insertions(+), 118 deletions(-) create mode 100644 lute/common/include/lute/common.h diff --git a/lute/cli/src/packagerun.cpp b/lute/cli/src/packagerun.cpp index f64614eee..e3eb0daf5 100644 --- a/lute/cli/src/packagerun.cpp +++ b/lute/cli/src/packagerun.cpp @@ -1,5 +1,6 @@ #include "lute/packagerun.h" +#include "lute/common.h" #include "lute/userlandvfs.h" #include "Luau/FileUtils.h" @@ -127,7 +128,7 @@ std::pair, std::vector contents = readFile(lockfilePath); if (!contents) diff --git a/lute/common/CMakeLists.txt b/lute/common/CMakeLists.txt index 0d9cc7897..30e434f0b 100644 --- a/lute/common/CMakeLists.txt +++ b/lute/common/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(Lute.Common STATIC) target_sources(Lute.Common PRIVATE + include/lute/common.h include/lute/fileutils.h src/fileutils.cpp @@ -8,5 +9,5 @@ target_sources(Lute.Common PRIVATE target_compile_features(Lute.Common PUBLIC cxx_std_17) target_include_directories(Lute.Common PUBLIC include) -target_link_libraries(Lute.Common PRIVATE Luau.CLI.lib) +target_link_libraries(Lute.Common PRIVATE Luau.CLI.lib Luau.Common) target_compile_options(Lute.Common PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/common/include/lute/common.h b/lute/common/include/lute/common.h new file mode 100644 index 000000000..f5354ddf3 --- /dev/null +++ b/lute/common/include/lute/common.h @@ -0,0 +1,6 @@ +#pragma once + +#include "Luau/Common.h" + +#define LUTE_ASSERT(expr) LUAU_ASSERT(expr) +#define LUTE_UNREACHABLE() LUAU_UNREACHABLE() diff --git a/lute/common/include/lute/fileutils.h b/lute/common/include/lute/fileutils.h index 261a1661f..6edc9aa2d 100644 --- a/lute/common/include/lute/fileutils.h +++ b/lute/common/include/lute/fileutils.h @@ -24,7 +24,6 @@ bool removeFile(const std::string& path); // Remove a directory (must be empty) bool removeDirectory(const std::string& path); - bool isDirectory(const std::string& path); // Get the name of a file without its extension diff --git a/lute/luau/CMakeLists.txt b/lute/luau/CMakeLists.txt index 18274ee6a..ff5fc50a9 100644 --- a/lute/luau/CMakeLists.txt +++ b/lute/luau/CMakeLists.txt @@ -16,5 +16,5 @@ target_sources(Lute.Luau PRIVATE target_compile_features(Lute.Luau PUBLIC cxx_std_17) target_include_directories(Lute.Luau PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Luau PRIVATE Lute.Require Lute.Runtime uv_a Luau.Analysis Luau.Ast Luau.Compiler Luau.CLI.lib Luau.Require Luau.VM) +target_link_libraries(Lute.Luau PRIVATE Lute.Common Lute.Require Lute.Runtime uv_a Luau.Analysis Luau.Ast Luau.Compiler Luau.CLI.lib Luau.Require Luau.VM) target_compile_options(Lute.Luau PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index ba3930cbe..775ca941c 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1,9 +1,10 @@ #include "lute/luau.h" +#include "lute/common.h" #include "lute/configresolver.h" #include "lute/moduleresolver.h" -#include "lute/userdatas.h" #include "lute/type.h" +#include "lute/userdatas.h" #include "Luau/Ast.h" #include "Luau/BuiltinDefinitions.h" @@ -252,7 +253,7 @@ struct AstSerialize : public Luau::AstVisitor Luau::NotNull lookupCstNode(Luau::AstNode* astNode) { const auto cstNode = cstNodeMap.find(astNode); - LUAU_ASSERT(cstNode); + LUTE_ASSERT(cstNode); return Luau::NotNull{(*cstNode)->as()}; } @@ -283,9 +284,9 @@ struct AstSerialize : public Luau::AstVisitor { auto beginPosition = currentPosition; - LUAU_ASSERT(currentPosition < newPos); - LUAU_ASSERT(currentPosition.line < lineOffsets.size()); - LUAU_ASSERT(newPos.line < lineOffsets.size()); + LUTE_ASSERT(currentPosition < newPos); + LUTE_ASSERT(currentPosition.line < lineOffsets.size()); + LUTE_ASSERT(newPos.line < lineOffsets.size()); size_t startOffset = lineOffsets[currentPosition.line] + currentPosition.column; size_t endOffset = lineOffsets[newPos.line] + newPos.column; @@ -313,14 +314,14 @@ struct AstSerialize : public Luau::AstVisitor if (index == std::string::npos) break; } - LUAU_ASSERT(currentPosition == newPos); + LUTE_ASSERT(currentPosition == newPos); return result; } std::vector extractTrivia(const Luau::Position& newPos) { - LUAU_ASSERT(currentPosition <= newPos); + LUTE_ASSERT(currentPosition <= newPos); if (currentPosition == newPos) return {}; @@ -335,8 +336,8 @@ struct AstSerialize : public Luau::AstVisitor result.insert(result.end(), whitespace.begin(), whitespace.end()); } - LUAU_ASSERT(comment.location.begin.line < lineOffsets.size()); - LUAU_ASSERT(comment.location.end.line < lineOffsets.size()); + LUTE_ASSERT(comment.location.begin.line < lineOffsets.size()); + LUTE_ASSERT(comment.location.end.line < lineOffsets.size()); size_t startOffset = lineOffsets[comment.location.begin.line] + comment.location.begin.column; size_t endOffset = lineOffsets[comment.location.end.line] + comment.location.end.column; @@ -345,10 +346,10 @@ struct AstSerialize : public Luau::AstVisitor // TODO: advancePosition is more of a debug check - we can probably just set currentPosition directly here advancePosition(commentText); - LUAU_ASSERT(currentPosition == comment.location.end); + LUTE_ASSERT(currentPosition == comment.location.end); // TODO: currently the text includes the `--` / `--[[` characters, should it? - LUAU_ASSERT(comment.type != Luau::Lexeme::BrokenComment); + LUTE_ASSERT(comment.type != Luau::Lexeme::BrokenComment); auto kind = comment.type == Luau::Lexeme::Comment ? Trivia::SingleLineComment : Trivia::MultiLineComment; result.emplace_back(Trivia{kind, comment.location, commentText}); } @@ -359,7 +360,7 @@ struct AstSerialize : public Luau::AstVisitor result.insert(result.end(), whitespace.begin(), whitespace.end()); } - LUAU_ASSERT(currentPosition == newPos); + LUTE_ASSERT(currentPosition == newPos); return result; } @@ -434,7 +435,7 @@ struct AstSerialize : public Luau::AstVisitor if (local->annotation) { - LUAU_ASSERT(colonPosition); + LUTE_ASSERT(colonPosition); serializeToken(*colonPosition, ":"); } else @@ -459,7 +460,7 @@ struct AstSerialize : public Luau::AstVisitor void serialize(Luau::AstExprTable::Item& item, Luau::CstExprTable::Item* cstNode) { - LUAU_ASSERT(cstNode); + LUTE_ASSERT(cstNode); lua_rawcheckstack(L, 2); @@ -493,7 +494,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(item.key->location.begin, std::string(value.data, value.size).data()); lua_setfield(L, -2, "key"); - LUAU_ASSERT(cstNode->equalsPosition); + LUTE_ASSERT(cstNode->equalsPosition); serializeToken(*cstNode->equalsPosition, "="); lua_setfield(L, -2, "equals"); @@ -509,7 +510,7 @@ struct AstSerialize : public Luau::AstVisitor lua_pushboolean(L, 1); lua_setfield(L, -2, "istableitem"); - LUAU_ASSERT(cstNode->indexerOpenPosition); + LUTE_ASSERT(cstNode->indexerOpenPosition); withLocation(Luau::Location{*cstNode->indexerOpenPosition, item.value->location.end}); serializeToken(*cstNode->indexerOpenPosition, "["); @@ -518,11 +519,11 @@ struct AstSerialize : public Luau::AstVisitor visit(item.key); lua_setfield(L, -2, "key"); - LUAU_ASSERT(cstNode->indexerClosePosition); + LUTE_ASSERT(cstNode->indexerClosePosition); serializeToken(*cstNode->indexerClosePosition, "]"); lua_setfield(L, -2, "indexerclose"); - LUAU_ASSERT(cstNode->equalsPosition); + LUTE_ASSERT(cstNode->equalsPosition); serializeToken(*cstNode->equalsPosition, "="); lua_setfield(L, -2, "equals"); @@ -611,7 +612,7 @@ struct AstSerialize : public Luau::AstVisitor const auto [trailingTrivia, leadingTrivia] = splitTrivia(trivia); lua_getref(L, lastTokenRef); - LUAU_ASSERT(lua_istable(L, -1)); + LUTE_ASSERT(lua_istable(L, -1)); serializeTrivia(trailingTrivia); lua_setfield(L, -2, "trailingtrivia"); @@ -625,7 +626,7 @@ struct AstSerialize : public Luau::AstVisitor { serializeTrivia(trivia); } - LUAU_ASSERT(lua_istable(L, -2)); + LUTE_ASSERT(lua_istable(L, -2)); lua_setfield(L, -2, "leadingtrivia"); size_t textLength = strlen(text); @@ -848,7 +849,7 @@ struct AstSerialize : public Luau::AstVisitor // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually - LUAU_ASSERT(currentPosition <= node->location.end); + LUTE_ASSERT(currentPosition <= node->location.end); currentPosition = node->location.end; } @@ -1382,7 +1383,7 @@ struct AstSerialize : public Luau::AstVisitor if (node->elsebody) { - LUAU_ASSERT(node->elseLocation); + LUTE_ASSERT(node->elseLocation); serializeToken(node->elseLocation->begin, "else"); lua_setfield(L, -2, "elsekeyword"); @@ -1807,12 +1808,12 @@ struct AstSerialize : public Luau::AstVisitor if (node->prefix) { - LUAU_ASSERT(node->prefixLocation); + LUTE_ASSERT(node->prefixLocation); serializeToken(node->prefixLocation->begin, node->prefix->value); lua_setfield(L, -2, "prefix"); - LUAU_ASSERT(cstNode); - LUAU_ASSERT(cstNode->prefixPointPosition); + LUTE_ASSERT(cstNode); + LUTE_ASSERT(cstNode->prefixPointPosition); serializeToken(*cstNode->prefixPointPosition, "."); lua_setfield(L, -2, "prefixpoint"); } @@ -1822,7 +1823,7 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasParameterList) { - LUAU_ASSERT(cstNode); + LUTE_ASSERT(cstNode); serializeToken(cstNode->openParametersPosition, "<"); lua_setfield(L, -2, "openparameters"); @@ -1850,7 +1851,7 @@ struct AstSerialize : public Luau::AstVisitor if (node->indexer->accessLocation) { - LUAU_ASSERT(node->indexer->access != Luau::AstTableAccess::ReadWrite); + LUTE_ASSERT(node->indexer->access != Luau::AstTableAccess::ReadWrite); serializeToken(node->indexer->accessLocation->begin, node->indexer->access == Luau::AstTableAccess::Read ? "read" : "write"); } else @@ -1885,14 +1886,14 @@ struct AstSerialize : public Luau::AstVisitor if (item.kind == Luau::CstTypeTable::Item::Kind::Indexer) { - LUAU_ASSERT(node->indexer); + LUTE_ASSERT(node->indexer); lua_pushstring(L, "indexer"); lua_setfield(L, -2, "kind"); if (node->indexer->accessLocation) { - LUAU_ASSERT(node->indexer->access != Luau::AstTableAccess::ReadWrite); + LUTE_ASSERT(node->indexer->access != Luau::AstTableAccess::ReadWrite); serializeToken(node->indexer->accessLocation->begin, node->indexer->access == Luau::AstTableAccess::Read ? "read" : "write"); } else @@ -1935,7 +1936,7 @@ struct AstSerialize : public Luau::AstVisitor if (prop->accessLocation) { - LUAU_ASSERT(prop->access != Luau::AstTableAccess::ReadWrite); + LUTE_ASSERT(prop->access != Luau::AstTableAccess::ReadWrite); serializeToken(prop->accessLocation->begin, prop->access == Luau::AstTableAccess::Read ? "read" : "write"); } else @@ -1963,7 +1964,7 @@ struct AstSerialize : public Luau::AstVisitor lua_pushstring(L, "double"); break; default: - LUAU_ASSERT(false); + LUTE_ASSERT(false); } lua_setfield(L, -2, "quotestyle"); @@ -2216,13 +2217,13 @@ struct AstSerialize : public Luau::AstVisitor lua_pushstring(L, "double"); break; default: - LUAU_ASSERT(false); + LUTE_ASSERT(false); } lua_setfield(L, -2, "quotestyle"); // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually - LUAU_ASSERT(currentPosition <= node->location.end); + LUTE_ASSERT(currentPosition <= node->location.end); currentPosition = node->location.end; } diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp index 037683c8d..b263872b5 100644 --- a/lute/luau/src/type.cpp +++ b/lute/luau/src/type.cpp @@ -1,5 +1,7 @@ #include "lute/type.h" +#include "lute/common.h" + #include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeFwd.h" @@ -592,8 +594,8 @@ struct TypeSerialize final : public Luau::TypeVisitor { // NOTE: `TypeSerialize` should explicitly visit _all_ types and type packs, // otherwise it's prone to serializing types that should not be serialized. - LUAU_ASSERT(false); - LUAU_UNREACHABLE(); + LUTE_ASSERT(false); + LUTE_UNREACHABLE(); } bool visit(TypeId ty, const BoundType& btv) override @@ -642,8 +644,8 @@ struct TypeSerialize final : public Luau::TypeVisitor { // NOTE: `TypeSerialize` should explicitly visit _all_ types and type packs, // otherwise it's prone to serializing type packs that should not be serialized. - LUAU_ASSERT(false); - LUAU_UNREACHABLE(); + LUTE_ASSERT(false); + LUTE_UNREACHABLE(); } bool visit(TypePackId tp, const BoundTypePack& btp) override diff --git a/lute/net/CMakeLists.txt b/lute/net/CMakeLists.txt index 2b9a9218f..b82fc11d6 100644 --- a/lute/net/CMakeLists.txt +++ b/lute/net/CMakeLists.txt @@ -8,5 +8,5 @@ target_sources(Lute.Net PRIVATE target_compile_features(Lute.Net PUBLIC cxx_std_17) target_include_directories(Lute.Net PUBLIC "include" ${LIBUV_INCLUDE_DIR} ${UWEBSOCKETS_INCLUDE_DIR}) -target_link_libraries(Lute.Net PRIVATE Lute.Runtime Luau.VM uv_a libcurl uWS zlibstatic) +target_link_libraries(Lute.Net PRIVATE Lute.Common Lute.Runtime Luau.VM uv_a libcurl uWS zlibstatic) target_compile_options(Lute.Net PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 866f2c029..f858d963e 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -1,5 +1,6 @@ #include "lute/net.h" +#include "lute/common.h" #include "lute/runtime.h" #include "Luau/DenseHash.h" @@ -40,6 +41,8 @@ struct CurlResponse static size_t writeFunction(char* ptr, size_t size, size_t nmemb, void* userdata) { std::vector* target = static_cast*>(userdata); + LUTE_ASSERT(target); + size_t fullsize = size * nmemb; target->insert(target->end(), ptr, ptr + fullsize); return fullsize; diff --git a/lute/process/CMakeLists.txt b/lute/process/CMakeLists.txt index 449ec1ccf..2dd1391a8 100644 --- a/lute/process/CMakeLists.txt +++ b/lute/process/CMakeLists.txt @@ -8,5 +8,5 @@ target_sources(Lute.Process PRIVATE target_compile_features(Lute.Process PUBLIC cxx_std_17) target_include_directories(Lute.Process PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Process PRIVATE Lute.Runtime Luau.VM uv_a) +target_link_libraries(Lute.Process PRIVATE Lute.Common Lute.Runtime Luau.VM uv_a) target_compile_options(Lute.Process PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 4290bfe7f..2e74ff207 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -1,5 +1,6 @@ #include "lute/process.h" +#include "lute/common.h" #include "lute/runtime.h" #include "lute/uvutils.h" @@ -479,7 +480,7 @@ int exitFunc(lua_State* L) // Exit with the provided code std::exit(exitCode); - LUAU_UNREACHABLE(); + LUTE_UNREACHABLE(); } int cwd(lua_State* L) diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index 894f4358f..79ae54b71 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -28,5 +28,5 @@ target_sources(Lute.Require PRIVATE target_compile_features(Lute.Require PUBLIC cxx_std_17) target_include_directories(Lute.Require PUBLIC include) -target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Std Lute.CLI.Commands Lute.Batteries Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Runtime) +target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Common Lute.Std Lute.CLI.Commands Lute.Batteries Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Runtime) target_compile_options(Lute.Require PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/require/src/bundlevfs.cpp b/lute/require/src/bundlevfs.cpp index f1634dffd..10a55f104 100644 --- a/lute/require/src/bundlevfs.cpp +++ b/lute/require/src/bundlevfs.cpp @@ -1,5 +1,7 @@ #include "lute/bundlevfs.h" +#include "lute/common.h" + #include "Luau/Common.h" #include "Luau/DenseHash.h" @@ -109,13 +111,13 @@ NavigationStatus BundleVfs::resetToPath(const std::string& path) NavigationStatus BundleVfs::toParent() { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toParent(); } NavigationStatus BundleVfs::toChild(const std::string& name) { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toChild(name); } @@ -126,9 +128,9 @@ bool BundleVfs::isModulePresent() const std::string BundleVfs::getIdentifier() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.realPath; } @@ -149,7 +151,7 @@ std::optional BundleVfs::getContents(const std::string& path) const ConfigStatus BundleVfs::getConfigStatus() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); // Get the potential config path for the current module path std::string configPath = modulePath->getPotentialConfigPath(".luaurc"); @@ -167,7 +169,7 @@ ConfigStatus BundleVfs::getConfigStatus() const std::optional BundleVfs::getConfig() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); // Get the potential config path for the current module path std::string configPath = modulePath->getPotentialConfigPath(".luaurc"); diff --git a/lute/require/src/clivfs.cpp b/lute/require/src/clivfs.cpp index 787721121..4052896ac 100644 --- a/lute/require/src/clivfs.cpp +++ b/lute/require/src/clivfs.cpp @@ -2,6 +2,7 @@ #include "lute/clibatteries.h" #include "lute/clicommands.h" +#include "lute/common.h" #include "lute/modulepath.h" #include "Luau/Common.h" @@ -94,13 +95,13 @@ NavigationStatus CliVfs::toAliasFallback(std::string_view aliasUnprefixed) NavigationStatus CliVfs::toParent() { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toParent(); } NavigationStatus CliVfs::toChild(const std::string& name) { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toChild(name); } @@ -112,9 +113,9 @@ bool CliVfs::isModulePresent() const std::string CliVfs::getIdentifier() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.realPath; } diff --git a/lute/require/src/filevfs.cpp b/lute/require/src/filevfs.cpp index 6cab7f446..268bb0d22 100644 --- a/lute/require/src/filevfs.cpp +++ b/lute/require/src/filevfs.cpp @@ -1,5 +1,6 @@ #include "lute/filevfs.h" +#include "lute/common.h" #include "lute/modulepath.h" #include "lute/uvutils.h" @@ -21,7 +22,7 @@ NavigationStatus FileVfs::resetToStdIn() return NavigationStatus::NotFound; size_t firstSlash = cwd->find_first_of("\\/"); - LUAU_ASSERT(firstSlash != std::string::npos); + LUTE_ASSERT(firstSlash != std::string::npos); modulePath = ModulePath::create(cwd->substr(0, firstSlash), cwd->substr(firstSlash + 1), isFile, isDirectory, "./"); return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; @@ -52,7 +53,7 @@ NavigationStatus FileVfs::resetToPath(const std::string& path) if (isAbsolutePath(normalizedPath)) { size_t firstSlash = normalizedPath.find_first_of('/'); - LUAU_ASSERT(firstSlash != std::string::npos); + LUTE_ASSERT(firstSlash != std::string::npos); modulePath = ModulePath::create(normalizedPath.substr(0, firstSlash), normalizedPath.substr(firstSlash + 1), isFile, isDirectory); } @@ -65,7 +66,7 @@ NavigationStatus FileVfs::resetToPath(const std::string& path) std::string joinedPath = normalizePath(*cwd + "/" + normalizedPath); size_t firstSlash = joinedPath.find_first_of("\\/"); - LUAU_ASSERT(firstSlash != std::string::npos); + LUTE_ASSERT(firstSlash != std::string::npos); modulePath = ModulePath::create(joinedPath.substr(0, firstSlash), joinedPath.substr(firstSlash + 1), isFile, isDirectory, normalizedPath); } @@ -75,13 +76,13 @@ NavigationStatus FileVfs::resetToPath(const std::string& path) NavigationStatus FileVfs::toParent() { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toParent(); } NavigationStatus FileVfs::toChild(const std::string& name) { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toChild(name); } @@ -92,17 +93,17 @@ bool FileVfs::isModulePresent() const std::string FileVfs::getFilePath() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.relativePath ? *result.relativePath : result.realPath; } std::string FileVfs::getAbsoluteFilePath() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.realPath; } @@ -113,7 +114,7 @@ std::optional FileVfs::getContents(const std::string& path) const ConfigStatus FileVfs::getConfigStatus() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); bool luaurcExists = isFile(modulePath->getPotentialConfigPath(Luau::kConfigName)); bool luauConfigExists = isFile(modulePath->getPotentialConfigPath(Luau::kLuauConfigName)); @@ -130,15 +131,15 @@ ConfigStatus FileVfs::getConfigStatus() const std::optional FileVfs::getConfig() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ConfigStatus status = getConfigStatus(); - LUAU_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); + LUTE_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); if (status == ConfigStatus::PresentJson) return readFile(modulePath->getPotentialConfigPath(Luau::kConfigName)); else if (status == ConfigStatus::PresentLuau) return readFile(modulePath->getPotentialConfigPath(Luau::kLuauConfigName)); - LUAU_UNREACHABLE(); + LUTE_UNREACHABLE(); } diff --git a/lute/require/src/lutevfs.cpp b/lute/require/src/lutevfs.cpp index 315d10538..d6d9e83fd 100644 --- a/lute/require/src/lutevfs.cpp +++ b/lute/require/src/lutevfs.cpp @@ -1,5 +1,6 @@ #include "lute/lutevfs.h" +#include "lute/common.h" #include "lute/crypto.h" #include "lute/fs.h" #include "lute/io.h" @@ -61,29 +62,29 @@ NavigationStatus LuteVfs::resetToPath(const std::string& path) NavigationStatus LuteVfs::toParent() { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toParent(); } NavigationStatus LuteVfs::toChild(const std::string& name) { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toChild(name); } bool LuteVfs::isModulePresent() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.type == ResolvedRealPath::PathType::File; } std::string LuteVfs::getIdentifier() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.realPath; } diff --git a/lute/require/src/require.cpp b/lute/require/src/require.cpp index a0c0414cb..5f785d63d 100644 --- a/lute/require/src/require.cpp +++ b/lute/require/src/require.cpp @@ -1,5 +1,6 @@ #include "lute/require.h" +#include "lute/common.h" #include "lute/lutevfs.h" #include "lute/modulepath.h" #include "lute/options.h" @@ -154,7 +155,7 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname if (strncmp(loadname, "@lute/", 6) == 0) { const lua_CFunction* func = kLuteModules.find(loadname); - LUAU_ASSERT(func); + LUTE_ASSERT(func); lua_pushcfunction(L, *func, nullptr); lua_call(L, 0, 1); return 1; diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 9d3f66e3c..17a5ff9dd 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -1,6 +1,7 @@ #include "lute/requirevfs.h" #include "lute/bundlevfs.h" +#include "lute/common.h" #include "lute/modulepath.h" #include "lute/stdlibvfs.h" @@ -39,14 +40,14 @@ NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkn if ((requirerChunkname.size() >= 6 && requirerChunkname.substr(0, 6) == "@@cli/")) { vfsType = VFSType::Cli; - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); return cliVfs->resetToPath(std::string(requirerChunkname.substr(1))); } if ((requirerChunkname.size() >= 9 && requirerChunkname.substr(0, 9) == "@@bundle/")) { vfsType = VFSType::Bundle; - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); return bundleVfs->resetToPath(std::string(requirerChunkname.substr(1))); } @@ -75,11 +76,11 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) status = luteVfs.resetToPath(std::string(path)); break; case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); status = cliVfs->resetToPath(std::string(path)); break; case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); status = bundleVfs->resetToPath(std::string(path)); break; } @@ -106,7 +107,7 @@ NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view alia { if (vfsType == VFSType::Cli) { - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); return cliVfs->toAliasFallback(aliasUnprefixed); } return NavigationStatus::NotFound; @@ -127,11 +128,11 @@ NavigationStatus RequireVfs::toParent(lua_State* L) status = luteVfs.toParent(); break; case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); status = cliVfs->toParent(); break; case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); status = bundleVfs->toParent(); break; } @@ -149,10 +150,10 @@ NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) case VFSType::Lute: return luteVfs.toChild(std::string(name)); case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); return cliVfs->toChild(std::string(name)); case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); return bundleVfs->toChild(std::string(name)); } return NavigationStatus::NotFound; @@ -170,10 +171,10 @@ bool RequireVfs::isModulePresent(lua_State* L) const return luteVfs.isModulePresent(); break; case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); return cliVfs->isModulePresent(); case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); return bundleVfs->isModulePresent(); } @@ -196,11 +197,11 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c contents = luteVfs.getContents(loadname); break; case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); contents = cliVfs->getContents(loadname); break; case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); contents = bundleVfs->getContents(loadname); break; } @@ -223,11 +224,11 @@ std::string RequireVfs::getChunkname(lua_State* L) const chunkname = "@" + luteVfs.getIdentifier(); break; case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); chunkname = "@" + cliVfs->getIdentifier(); break; case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); chunkname = "@" + bundleVfs->getIdentifier(); break; } @@ -249,11 +250,11 @@ std::string RequireVfs::getLoadname(lua_State* L) const loadname = luteVfs.getIdentifier(); break; case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); loadname = cliVfs->getIdentifier(); break; case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); loadname = bundleVfs->getIdentifier(); break; } @@ -275,11 +276,11 @@ std::string RequireVfs::getCacheKey(lua_State* L) const cacheKey = luteVfs.getIdentifier(); break; case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); cacheKey = cliVfs->getIdentifier(); break; case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); cacheKey = bundleVfs->getIdentifier(); break; } @@ -301,11 +302,11 @@ ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const status = luteVfs.getConfigStatus(); break; case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); status = cliVfs->getConfigStatus(); break; case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); status = bundleVfs->getConfigStatus(); break; } @@ -327,11 +328,11 @@ std::string RequireVfs::getConfig(lua_State* L) const configContents = luteVfs.getConfig(); break; case VFSType::Cli: - LUAU_ASSERT(cliVfs); + LUTE_ASSERT(cliVfs); configContents = cliVfs->getConfig(); break; case VFSType::Bundle: - LUAU_ASSERT(bundleVfs); + LUTE_ASSERT(bundleVfs); configContents = bundleVfs->getConfig(); break; } diff --git a/lute/require/src/stdlibvfs.cpp b/lute/require/src/stdlibvfs.cpp index ecc2aa4dc..ff262b647 100644 --- a/lute/require/src/stdlibvfs.cpp +++ b/lute/require/src/stdlibvfs.cpp @@ -1,5 +1,6 @@ #include "lute/stdlibvfs.h" +#include "lute/common.h" #include "lute/modulepath.h" #include "lute/stdlib.h" @@ -49,13 +50,13 @@ NavigationStatus StdLibVfs::resetToPath(const std::string& path) NavigationStatus StdLibVfs::toParent() { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toParent(); } NavigationStatus StdLibVfs::toChild(const std::string& name) { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); return modulePath->toChild(name); } @@ -66,9 +67,9 @@ bool StdLibVfs::isModulePresent() const std::string StdLibVfs::getIdentifier() const { - LUAU_ASSERT(modulePath); + LUTE_ASSERT(modulePath); ResolvedRealPath result = modulePath->getRealPath(); - LUAU_ASSERT(result.status == NavigationStatus::Success); + LUTE_ASSERT(result.status == NavigationStatus::Success); return result.realPath; } diff --git a/lute/require/src/userlandvfs.cpp b/lute/require/src/userlandvfs.cpp index c857aa9ea..9fc012605 100644 --- a/lute/require/src/userlandvfs.cpp +++ b/lute/require/src/userlandvfs.cpp @@ -1,5 +1,6 @@ #include "lute/userlandvfs.h" +#include "lute/common.h" #include "lute/modulepath.h" #include "Luau/Common.h" @@ -68,14 +69,14 @@ ConfigStatus Subtree::getConfigStatus() const std::optional Subtree::getConfig() const { ConfigStatus status = getConfigStatus(); - LUAU_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); + LUTE_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); if (status == ConfigStatus::PresentJson) return readFile(currentModulePath.getPotentialConfigPath(Luau::kConfigName)); else if (status == ConfigStatus::PresentLuau) return readFile(currentModulePath.getPotentialConfigPath(Luau::kLuauConfigName)); - LUAU_UNREACHABLE(); + LUTE_UNREACHABLE(); } bool Subtree::isModulePresent() const @@ -141,7 +142,7 @@ NavigationStatus UserlandVfs::toAliasFallback(std::string_view aliasUnprefixed) availableDependencies = directDependencies; break; case VFSType::Subtree: - LUAU_ASSERT(currentSubtree); + LUTE_ASSERT(currentSubtree); availableDependencies = currentSubtree->getInfo().dependencies; } @@ -179,7 +180,7 @@ NavigationStatus UserlandVfs::toParent() status = fileVfs.toParent(); break; case VFSType::Subtree: - LUAU_ASSERT(currentSubtree); + LUTE_ASSERT(currentSubtree); status = currentSubtree->toParent(); break; } @@ -196,7 +197,7 @@ NavigationStatus UserlandVfs::toChild(const std::string& name) status = fileVfs.toChild(name); break; case VFSType::Subtree: - LUAU_ASSERT(currentSubtree); + LUTE_ASSERT(currentSubtree); status = currentSubtree->toChild(name); break; } @@ -212,7 +213,7 @@ ConfigStatus UserlandVfs::getConfigStatus() const status = fileVfs.getConfigStatus(); break; case VFSType::Subtree: - LUAU_ASSERT(currentSubtree); + LUTE_ASSERT(currentSubtree); status = currentSubtree->getConfigStatus(); break; } @@ -228,7 +229,7 @@ std::optional UserlandVfs::getConfig() const config = fileVfs.getConfig(); break; case VFSType::Subtree: - LUAU_ASSERT(currentSubtree); + LUTE_ASSERT(currentSubtree); config = currentSubtree->getConfig(); break; } @@ -244,7 +245,7 @@ bool UserlandVfs::isModulePresent() const isPresent = fileVfs.isModulePresent(); break; case VFSType::Subtree: - LUAU_ASSERT(currentSubtree); + LUTE_ASSERT(currentSubtree); isPresent = currentSubtree->isModulePresent(); break; } @@ -265,7 +266,7 @@ std::string UserlandVfs::getCurrentPath() const path = fileVfs.getAbsoluteFilePath(); break; case VFSType::Subtree: - LUAU_ASSERT(currentSubtree); + LUTE_ASSERT(currentSubtree); path = currentSubtree->getCurrentPath(); break; } diff --git a/lute/runtime/CMakeLists.txt b/lute/runtime/CMakeLists.txt index 4d6cf895a..4d4912ee6 100644 --- a/lute/runtime/CMakeLists.txt +++ b/lute/runtime/CMakeLists.txt @@ -15,5 +15,5 @@ target_sources(Lute.Runtime PRIVATE target_compile_features(Lute.Runtime PUBLIC cxx_std_17) target_include_directories(Lute.Runtime PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Runtime PRIVATE Luau.Common Luau.Require Luau.VM uv_a) +target_link_libraries(Lute.Runtime PRIVATE Luau.Require Luau.VM Lute.Common uv_a) target_compile_options(Lute.Runtime PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/runtime/include/lute/uvutils.h b/lute/runtime/include/lute/uvutils.h index 905be2dbe..32f81455f 100644 --- a/lute/runtime/include/lute/uvutils.h +++ b/lute/runtime/include/lute/uvutils.h @@ -5,8 +5,6 @@ #include #include -#define LUTE_ASSERT(expr) LUAU_ASSERT(expr) - namespace uvutils { diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 9240f91e2..2602b68e9 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -1,9 +1,6 @@ #include "lute/runtime.h" -#include "lute/uvutils.h" - -#include "Luau/Common.h" -#include "Luau/Require.h" +#include "lute/common.h" #include "lua.h" #include "lualib.h" From 21a89fe537fd1472a5165ed0bc1a539d8fd2bf1f Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:47:50 -0800 Subject: [PATCH 339/642] Updates serialization of TypePack, VariadicTypePack, GenericTypePack (#799) And adds tests for these! Adding `tag` for a pack, variadic pack, and generic pack, as well as updating the luau definition to include specific fields for each, like we have for `Type`. And various fixes to the serialization after adding tests and realizing that some of the initial implementation wasn't exactly correct --- definitions/luau.luau | 17 ++++-- lute/luau/src/type.cpp | 23 +++++--- tests/src/typeserializer.test.cpp | 91 ++++++++++++++++++++++++++++++- tests/std/luau.test.luau | 27 ++++++--- 4 files changed, 136 insertions(+), 22 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index d4af28bd7..c28976cb3 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -724,8 +724,8 @@ export type Type = { metatable: Type?, -- for function type - parameters: { head: { Type }?, tail: Type }, - returns: { head: { Type }?, tail: Type }, + parameters: TypePack, + returns: TypePack, generics: { Type }, genericpacks: { TypePack }, @@ -734,13 +734,22 @@ export type Type = { parent: Type?, -- for generic type - name: string?, + name: string, ispack: boolean, } export type TypePack = { + tag: "typepack" | "variadic" | "generic", head: { Type }?, - tail: Type?, + tail: TypePack?, + + -- for variadic type packs + type: Type, + hidden: boolean, + + -- for generic type packs + name: string, + ispack: boolean, } function luau.typeofmodule(modulepath: string): TypePack? diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp index b263872b5..a5716206d 100644 --- a/lute/luau/src/type.cpp +++ b/lute/luau/src/type.cpp @@ -422,13 +422,17 @@ struct TypeSerialize final : public Luau::TypeVisitor lua_setfield(L, -2, "metatable"); } - // Luau TypePack is { head: {type}?, tail: type? } + // Luau TypePack is + // head: {type}? + // tail: typepack? void serialize(TypePackId tp, const TypePack& pack) { checkStack(L, 3); // 1 for root table + 1 for subtable + 1 for traverse - lua_createtable(L, 0, 2); + lua_createtable(L, 0, 3); registerTypePack(tp); + pushTag("typepack"); + // Head if (!pack.head.empty()) { @@ -453,20 +457,21 @@ struct TypeSerialize final : public Luau::TypeVisitor lua_setfield(L, -2, "tail"); } - // Luau VariadicTypePack is { head: nil, tail: type } + // Luau VariadicTypePack is + // type: type + // hidden: boolean void serialize(TypePackId tp, const VariadicTypePack& vtp) { checkStack(L, 2); - lua_createtable(L, 0, 2); + lua_createtable(L, 0, 3); registerTypePack(tp); - // Head is nil - lua_pushnil(L); - lua_setfield(L, -2, "head"); + pushTag("variadic"); - // Tail is the type traverse(vtp.ty); - lua_setfield(L, -2, "tail"); + lua_setfield(L, -2, "type"); + lua_pushboolean(L, vtp.hidden); + lua_setfield(L, -2, "hidden"); } // Luau GenericTypePack is diff --git a/tests/src/typeserializer.test.cpp b/tests/src/typeserializer.test.cpp index 62892e62a..b70c7b81b 100644 --- a/tests/src/typeserializer.test.cpp +++ b/tests/src/typeserializer.test.cpp @@ -437,7 +437,96 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_cyclic_table_type") REQUIRE(lua_equal(L, root, -1)); // The 'next' property's read type should be the same table as the root (cycle) } -// TODO: MetatableType, ExternType, TypePack, VariadicTypePack, GenericTypePack tests +// TODO: MetatableType, ExternType tests + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_simple_type_pack_nil_tail") +{ + TypeId numberTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + TypePackId tp = arena.addTypePack(TypePack{{numberTy}, std::nullopt}); + + lua_checkstack(L, 2); + + // { head: { tag: "number" }, tail: nil } + REQUIRE_EQ(Luau::serializeTypePack(L, tp), 1); + + REQUIRE(lua_istable(L, -1)); + + lua_getfield(L, -1, "head"); + REQUIRE(lua_istable(L, -1)); + lua_rawgeti(L, -1, 1); + requireStringField(L, "tag", "number"); + lua_pop(L, 2); // head[0], head + + lua_getfield(L, -1, "tail"); + REQUIRE(lua_isnil(L, -1)); // no tail +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_type_pack_with_head_and_tail") +{ + TypeId numberTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + TypeId stringTy = arena.addType(PrimitiveType{PrimitiveType::String}); + TypePackId tailTp = arena.addTypePack(TypePack{{stringTy}, std::nullopt}); + TypePackId tp = arena.addTypePack(TypePack{{numberTy}, tailTp}); + + lua_checkstack(L, 3); + + // { head: { tag: "number" }, tail: { head: { tag: "string" }, tail: nil } } + REQUIRE_EQ(Luau::serializeTypePack(L, tp), 1); + + REQUIRE(lua_istable(L, -1)); + + lua_getfield(L, -1, "head"); + REQUIRE(lua_istable(L, -1)); + lua_rawgeti(L, -1, 1); + requireStringField(L, "tag", "number"); + lua_pop(L, 2); // head[0], head + + lua_getfield(L, -1, "tail"); + REQUIRE(lua_istable(L, -1)); + lua_getfield(L, -1, "head"); + REQUIRE(lua_istable(L, -1)); + lua_rawgeti(L, -1, 1); + requireStringField(L, "tag", "string"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_variadic_type_pack") +{ + TypeId numberTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + TypePackId tp = arena.addTypePack(VariadicTypePack{numberTy, true}); + + lua_checkstack(L, 2); + + // { tag: "variadic", type: { tag: "number" }, hidden: true } + REQUIRE_EQ(Luau::serializeTypePack(L, tp), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "variadic"); + + lua_getfield(L, -1, "type"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "number"); + lua_pop(L, 1); // type + + requireBoolField(L, "hidden", true); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_generic_type_pack") +{ + GenericTypePack gtp; + gtp.name = "T"; + + TypePackId tp = arena.addTypePack(gtp); + + lua_checkstack(L, 1); + + // { tag: "generic", name: "T", ispack: true } + REQUIRE_EQ(Luau::serializeTypePack(L, tp), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "generic"); + requireStringField(L, "name", "T"); + requireBoolField(L, "ispack", true); +} // Non-Serialized Types diff --git a/tests/std/luau.test.luau b/tests/std/luau.test.luau index 09cbd472e..3d8001e65 100644 --- a/tests/std/luau.test.luau +++ b/tests/std/luau.test.luau @@ -30,7 +30,9 @@ test.suite("LuauTypeOfModuleSuite", function(suite) error("typePack should not be nil") end - -- Verify TypePack structure: { head: {type}?, tail: type? } + -- Verify TypePack structure: { head: {type}?, tail: typepack? } + assert.eq(typePack.tag, "typepack") + assert.neq(typePack.head, nil) if not typePack.head then error("typePack.head should not be nil") @@ -91,17 +93,19 @@ test.suite("LuauTypeOfModuleSuite", function(suite) error("typePack should not be nil") end + -- Verify TypePack structure: { head: {type}?, tail: typepack? } + assert.eq(typePack.tag, "typepack") + + -- Head: string, number assert.neq(typePack.head, nil) if not typePack.head then error("typePack.head should not be nil") end assert.eq(#typePack.head, 3) - -- Verify types: string, number, boolean assert.eq(typePack.head[1].tag, "string") assert.eq(typePack.head[2].tag, "number") assert.eq(typePack.head[3].tag, "boolean") - assert.eq(typePack.tail, nil) end) suite:case("typeofmodule_returns_typepack_with_variadic_tail", function(assert) @@ -110,10 +114,10 @@ test.suite("LuauTypeOfModuleSuite", function(suite) fs.write( testFile, [[ - local function variadic(...: string): ...string + local function variadic_func(...: string): ...string return ... end - return variadic + return variadic_func ]] ) fs.close(testFile) @@ -124,6 +128,8 @@ test.suite("LuauTypeOfModuleSuite", function(suite) error("typePack should not be nil") end + assert.eq(typePack.tag, "typepack") + assert.neq(typePack.head, nil) if not typePack.head then error("typePack.head should not be nil") @@ -133,9 +139,14 @@ test.suite("LuauTypeOfModuleSuite", function(suite) local funcType = typePack.head[1] assert.eq(funcType.tag, "function") assert.neq(funcType.returns, nil) - -- Variadic return should have tail set to the variadic type - assert.neq(funcType.returns.tail, nil) - assert.eq(funcType.returns.tail.tag, "string") + + assert.eq(funcType.returns.tag, "variadic") + + assert.neq(funcType.returns.type, nil) + if not funcType.returns.type then + error("Function.returns.type should not be nil") + end + assert.eq(funcType.returns.type.tag, "string") end) suite:case("resolverequire", function(assert) From 4fff7fd21efe49303c840c0abc86b69eb6f7b3dc Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 11 Feb 2026 09:51:32 -0800 Subject: [PATCH 340/642] Adds color reporting to `lute test` (#804) This PR uses the richterm battery to implement a simple test reporter that produces color output. Here are some examples of the output on both dark mode and light mode terminals: Failing tests Some failing Light mode --- lute/cli/commands/test/init.luau | 4 +- lute/cli/commands/test/reporter.luau | 65 ++++++++++++++++++++++++++++ tests/cli/test.test.luau | 12 ++--- 3 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 lute/cli/commands/test/reporter.luau diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index a07f7d3b9..811e68e25 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -5,7 +5,7 @@ local path = require("@std/path") local ps = require("@std/process") local testtypes = require("@std/test/types") local runner = require("@std/test/runner") -local reporter = require("@std/test/reporter") +local reporter = require("@self/reporter") local test = require("@std/test") local function printHelp() @@ -119,7 +119,7 @@ local function main(...: string) else local env = test._registered() local filteredEnv = filter.filtertests(env, suiteName, caseName) - runtests(filteredEnv, runner.run, reporter.simple) + runtests(filteredEnv, runner.run, reporter.color) end end diff --git a/lute/cli/commands/test/reporter.luau b/lute/cli/commands/test/reporter.luau new file mode 100644 index 000000000..145875a3f --- /dev/null +++ b/lute/cli/commands/test/reporter.luau @@ -0,0 +1,65 @@ +local rt = require("@batteries/richterm") +local tys = require("@std/test/types") + +local function splitStacktrace(stacktrace: string): { string } + local lines = {} + for line in stacktrace:gmatch("[^\n]+") do + table.insert(lines, line) + end + return lines +end + +-- Styles +local pass = rt.combine(rt.green, rt.bold) +local fail = rt.combine(rt.red, rt.bold) +local testname = rt.combine(rt.bold, rt.white) +local errlabel = rt.yellow +local errmsg = rt.red +local stkframe = rt.red +local location = rt.cyan +local dim = rt.dim +local separator = dim + +local function printFailure(failed: tys.failedtest) + print(fail(" FAIL ") .. testname(failed.test)) + if failed.failure.__tag == "assertion" then + print(location(`\t{failed.failure.filename}:{failed.failure.linenumber}`)) + print(errmsg(`\t{failed.failure.assertion}: {failed.failure.msg}`)) + elseif failed.failure.__tag == "lifecycle" then + print(location(`\t{failed.failure.filename}:{failed.failure.linenumber}`)) + print(errmsg(`\t{failed.failure.hook}: {failed.failure.msg}`)) + else + print(errlabel("\tRuntime error: ") .. errmsg(failed.failure.msg)) + print(stkframe("\tStacktrace:")) + for _, line in splitStacktrace(failed.failure.stacktrace) do + print(stkframe(`\t {line}`)) + end + end +end + +local function printSummary(result: tys.testrunresult) + print(separator(string.rep("─", 50))) + print( + rt.bold("Results: ") + .. pass(`{result.passed} passed`) + .. dim(", ") + .. fail(`{result.failed} failed`) + .. dim(` of {result.total}`) + ) +end + +local function colorReporter(result: tys.testrunresult) + if #result.failures > 0 then + print("") + print(fail("Failures:")) + print("") + for _, failed in result.failures do + printFailure(failed) + print("") + end + end + + printSummary(result) +end + +return { color = colorReporter } diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 434b21590..41daefa24 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -46,8 +46,8 @@ test.suite("LuteTestCommand", function(suite) -- Check that it runs only tests in SmokeSuite assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Total: 2", 1, true), nil) - assert.neq(result.stdout:find("Passed: 2", 1, true), nil) + assert.neq(result.stdout:find("of 2", 1, true), nil) + assert.neq(result.stdout:find("2 passed", 1, true), nil) end) suite:case("lute test filters by case name", function(assert) @@ -56,8 +56,8 @@ test.suite("LuteTestCommand", function(suite) -- Check that it runs only tests named make_pass assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Total: 1", 1, true), nil) - assert.neq(result.stdout:find("Passed: 1", 1, true), nil) + assert.neq(result.stdout:find("of 1", 1, true), nil) + assert.neq(result.stdout:find("1 passed", 1, true), nil) end) suite:case("lute test filters by suite and case name", function(assert) @@ -66,7 +66,7 @@ test.suite("LuteTestCommand", function(suite) -- Check that it runs only make_fail in SmokeSuite assert.eq(result.exitcode, 0) - assert.neq(result.stdout:find("Total: 1", 1, true), nil) - assert.neq(result.stdout:find("Passed: 1", 1, true), nil) + assert.neq(result.stdout:find("of 1", 1, true), nil) + assert.neq(result.stdout:find("1 passed", 1, true), nil) end) end) From cbeda68d8e2247528923bd7fe2a97ca9998b9f17 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 11 Feb 2026 10:15:05 -0800 Subject: [PATCH 341/642] Adds targets to documented lint rules (#802) This allows Mandolin to link to their docs --- lute/cli/commands/lint/rules/almost_swapped.luau | 2 ++ lute/cli/commands/lint/rules/constant_table_comparison.luau | 2 ++ lute/cli/commands/lint/rules/divide_by_zero.luau | 2 ++ lute/cli/commands/lint/rules/parenthesized_conditions.luau | 4 ++++ tests/cli/lint.test.luau | 3 +++ tools/check-faillist.txt | 6 +++--- 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index 5e2ffc60b..c2aab4c62 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -8,6 +8,7 @@ local utils = require("@std/syntax/utils") local name = "almost_swapped" local message = "This looks like a failed attempt to swap." +local target = "https://lute.luau.org/cli/lint/almost_swapped.html" local compFuncs = {} @@ -98,6 +99,7 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp suggestedfix = { fix = suggestedFix, }, + target = target, }) end end diff --git a/lute/cli/commands/lint/rules/constant_table_comparison.luau b/lute/cli/commands/lint/rules/constant_table_comparison.luau index c7bf74b5b..5c579a23c 100644 --- a/lute/cli/commands/lint/rules/constant_table_comparison.luau +++ b/lute/cli/commands/lint/rules/constant_table_comparison.luau @@ -9,6 +9,7 @@ local utils = require("@std/syntax/utils") local name = "constant_table_comparison" local message = "Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false." +local target = "https://lute.luau.org/cli/lint/constant_table_comparison.html" local function isTableLiteral(expr: syntax.AstExpr): boolean return expr.tag == "table" @@ -62,6 +63,7 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp fix = suggestedFix, } else nil, + target = target, } end ) diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau index 3a958d29d..ab44ed9c2 100644 --- a/lute/cli/commands/lint/rules/divide_by_zero.luau +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -6,6 +6,7 @@ local utils = require("@std/syntax/utils") local name = "divide_by_zero" local message = "Division by zero detected." +local target = "https://lute.luau.org/cli/lint/divide_by_zero.html" local function isZeroLiteral(expr: syntax.AstExpr): boolean return expr.kind == "expr" and expr.tag == "number" and expr.value == 0 @@ -48,6 +49,7 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp severity = "warning", sourcepath = sourcepath, suggestedfix = suggestedfix, + target = target, } end ) diff --git a/lute/cli/commands/lint/rules/parenthesized_conditions.luau b/lute/cli/commands/lint/rules/parenthesized_conditions.luau index 22b916b69..746df9137 100644 --- a/lute/cli/commands/lint/rules/parenthesized_conditions.luau +++ b/lute/cli/commands/lint/rules/parenthesized_conditions.luau @@ -6,6 +6,7 @@ local syntaxPrinter = require("@std/syntax/printer") local name = "parenthesized_conditions" local message = "Luau doesn't require parentheses around conditions. You can remove them for readability." +local target = "https://lute.luau.org/cli/lint/parenthesized_conditions.html" local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } local violations: { lintTypes.LintViolation } = {} @@ -28,6 +29,7 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp suggestedfix = { fix = syntaxPrinter.printnode(ifStat.condition.expression), }, + target = target, }) end @@ -42,6 +44,7 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp suggestedfix = { fix = syntaxPrinter.printnode(elseIfStat.condition.expression), }, + target = target, }) end end @@ -67,6 +70,7 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTyp suggestedfix = { fix = syntaxPrinter.printnode((n.condition :: syntax.AstExprGroup).expression), }, + target = target, }) end) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 6ad190d43..d2f50381c 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -1038,6 +1038,7 @@ local x = 1 / 0 }, { "message": "Division by zero detected.", + "codeDescription": "https://lute.luau.org/cli/lint/divide_by_zero.html", "source": "lute lint", "code": "divide_by_zero", "severity": 2, @@ -1295,6 +1296,7 @@ b = a }, { "message": "Division by zero detected.", + "codeDescription": "https://lute.luau.org/cli/lint/divide_by_zero.html", "source": "lute lint", "code": "divide_by_zero", "severity": 2, @@ -1700,6 +1702,7 @@ local e = next(t) ~= nil [ { "message": "This looks like a failed attempt to swap.", + "codeDescription": "https://lute.luau.org/cli/lint/almost_swapped.html", "source": "lute lint", "code": "almost_swapped", "severity": 2, diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 05a5bc418..eb65db760 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -66,9 +66,9 @@ lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 lute/cli/commands/lint/printer.luau:58:8-52 -lute/cli/commands/lint/rules/parenthesized_conditions.luau:22:5-16 -lute/cli/commands/lint/rules/parenthesized_conditions.luau:36:6-17 -lute/cli/commands/lint/rules/parenthesized_conditions.luau:61:4-15 +lute/cli/commands/lint/rules/parenthesized_conditions.luau:23:5-16 +lute/cli/commands/lint/rules/parenthesized_conditions.luau:38:6-17 +lute/cli/commands/lint/rules/parenthesized_conditions.luau:64:4-15 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau:25:42-59 From fd0b4dd07a5f5becfd92259eddbeee825988c2f5 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 11 Feb 2026 10:23:35 -0800 Subject: [PATCH 342/642] Extends AST serialization to support AstExprInstantiate (#801) Addresses #621 --------- Co-authored-by: ariel --- definitions/luau.luau | 13 +++ lute/luau/src/luau.cpp | 34 ++++++++ lute/std/libs/syntax/init.luau | 2 + lute/std/libs/syntax/types.luau | 2 + lute/std/libs/syntax/visitor.luau | 24 ++++++ tests/parserExamples/.styluaignore | 1 + .../explicit-generic-instantiation-1.luau | 5 ++ tests/std/syntax/parser.test.luau | 22 ++++- tools/check-faillist.txt | 80 +++++++++---------- 9 files changed, 140 insertions(+), 43 deletions(-) create mode 100644 tests/parserExamples/.styluaignore create mode 100644 tests/parserExamples/explicit-generic-instantiation-1.luau diff --git a/definitions/luau.luau b/definitions/luau.luau index c28976cb3..a233d0324 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -110,6 +110,18 @@ export type AstExprCall = { argLocation: span, } +export type AstExprInstantiate = { + location: span, + kind: "expr", + tag: "instantiate", + expr: AstExpr, + leftarrow1: Token<"<">, + leftarrow2: Token<"<">, + typearguments: Punctuated, + rightarrow1: Token<">">, + rightarrow2: Token<">">, +} + export type AstExprIndexName = { location: span, kind: "expr", @@ -260,6 +272,7 @@ export type AstExpr = { kind: "expr" } & ( | AstExprGlobal | AstExprVarargs | AstExprCall + | AstExprInstantiate | AstExprIndexName | AstExprIndexExpr | AstExprFunction diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 775ca941c..19e9cdb10 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -921,6 +921,34 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "closeparens"); } + void serialize(Luau::AstExprInstantiate* node) + { + const auto& cstNode = lookupCstNode(node)->instantiation; + + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 6); + + serializeNodePreamble(node, "instantiate", "expr"); + + node->expr->visit(this); + lua_setfield(L, -2, "expr"); + + serializeToken(cstNode.leftArrow1Position, "<"); + lua_setfield(L, -2, "leftarrow1"); + + serializeToken(cstNode.leftArrow2Position, "<"); + lua_setfield(L, -2, "leftarrow2"); + + serializePunctuated(node->typeArguments, cstNode.commaPositions, ","); + lua_setfield(L, -2, "typearguments"); + + serializeToken(cstNode.rightArrow1Position, ">"); + lua_setfield(L, -2, "rightarrow1"); + + serializeToken(cstNode.rightArrow2Position, ">"); + lua_setfield(L, -2, "rightarrow2"); + } + void serialize(Luau::AstExprIndexName* node) { lua_rawcheckstack(L, 2); @@ -2425,6 +2453,12 @@ struct AstSerialize : public Luau::AstVisitor return false; } + bool visit(Luau::AstExprInstantiate* node) override + { + serialize(node); + return false; + } + bool visit(Luau::AstExprIndexName* node) override { serialize(node); diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index 306d6f9fa..d6d1cdf08 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -55,6 +55,8 @@ export type AstExprVarargs = types.AstExprVarargs export type AstExprCall = types.AstExprCall +export type AstExprInstantiate = types.AstExprInstantiate + export type AstExprIndexName = types.AstExprIndexName export type AstExprIndexExpr = types.AstExprIndexExpr diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index 7e6797758..a6cc020a7 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -35,6 +35,8 @@ export type AstExprVarargs = luau.AstExprVarargs export type AstExprCall = luau.AstExprCall +export type AstExprInstantiate = luau.AstExprInstantiate + export type AstExprIndexName = luau.AstExprIndexName export type AstExprIndexExpr = luau.AstExprIndexExpr diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 3d5e7c9ae..a12584cbb 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -43,6 +43,7 @@ export type Visitor = { visitExprBinary: (types.AstExprBinary) -> boolean, visitExprFunction: (types.AstExprFunction) -> boolean, visitExprFunctionEnd: (types.AstExprFunction) -> (), + visitExprInstantiate: (types.AstExprInstantiate) -> boolean, visitExprTableItem: (types.AstExprTableItem) -> boolean, visitExprTable: (types.AstExprTable) -> boolean, visitExprIndexName: (types.AstExprIndexName) -> boolean, @@ -117,6 +118,7 @@ local defaultVisitor: Visitor = { visitExprBinary = alwaysVisit :: any, visitExprFunction = alwaysVisit :: any, visitExprFunctionEnd = alwaysVisit :: any, + visitExprInstantiate = alwaysVisit, visitExprTableItem = alwaysVisit :: any, visitExprTable = alwaysVisit :: any, visitExprIndexName = alwaysVisit :: any, @@ -179,6 +181,14 @@ local function visitLocal(node: types.AstLocal, visitor: Visitor) end end +local function visitTypeOrTypePack(node: types.AstType | types.AstTypePack, visitor: Visitor) + if node.kind == "type" then + visitType(node, visitor) + else + visitTypePack(node, visitor) + end +end + local function visitStatBlock(block: types.AstStatBlock, visitor: Visitor) if visitor.visitStatBlock(block) then for _, statement in block.statements do @@ -498,6 +508,17 @@ local function visitExprFunction( end end +local function visitExprInstantiate(node: types.AstExprInstantiate, visitor: Visitor) + if visitor.visitExprInstantiate(node) then + visitExpr(node.expr, visitor) + visitToken(node.leftarrow1, visitor) + visitToken(node.leftarrow2, visitor) + visitPunctuated(node.typearguments, visitor, visitTypeOrTypePack) + visitToken(node.rightarrow1, visitor) + visitToken(node.rightarrow2, visitor) + end +end + local function visitStatFunction(node: types.AstStatFunction, visitor: Visitor) if visitor.visitStatFunction(node) then -- We need to visit in lexical order @@ -840,6 +861,8 @@ function visitExpr(expression: types.AstExpr, visitor: Visitor) visitExprBinary(expression, visitor) elseif expression.tag == "function" then visitExprFunction(expression, visitor) + elseif expression.tag == "instantiate" then + visitExprInstantiate(expression, visitor) elseif expression.tag == "table" then visitExprTable(expression, visitor) elseif expression.tag == "indexname" then @@ -991,6 +1014,7 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitExprFunctionEnd = function(s) visit(s) end, + visitExprInstantiate = visit, visitExprTableItem = visit, visitExprTable = visit, visitExprIndexName = visit, diff --git a/tests/parserExamples/.styluaignore b/tests/parserExamples/.styluaignore new file mode 100644 index 000000000..dba2c1ef1 --- /dev/null +++ b/tests/parserExamples/.styluaignore @@ -0,0 +1 @@ +explicit-generic-instantiation-1.luau diff --git a/tests/parserExamples/explicit-generic-instantiation-1.luau b/tests/parserExamples/explicit-generic-instantiation-1.luau new file mode 100644 index 000000000..85c2598af --- /dev/null +++ b/tests/parserExamples/explicit-generic-instantiation-1.luau @@ -0,0 +1,5 @@ +function foo(_x: A, ...: B...) + return nil :: any +end + +local _x = foo<>("hello", 1, 2, 3) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index f7f158096..f4a1e4eba 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -127,7 +127,7 @@ test.suite("Parser", function(suite) if entry.type ~= "file" then continue end - if strext.hassuffix(entry.name, ".json") then + if not strext.hassuffix(entry.name, ".luau") then continue end @@ -306,6 +306,26 @@ test.suite("parseExpr", function(suite) assert.eq((callWithArgsExpr :: syntax.AstExprCall).arguments[2].node.tag, "number") end) + suite:case("parseExprInstantiate", function(assert) + local callExpr = parser.parseexpr("foo<>()") + assert.eq(callExpr.kind, "expr") + assert.eq(callExpr.tag, "call") + + local instExpr = (callExpr :: syntax.AstExprCall).func + assert.eq(instExpr.tag, "instantiate") + assert.eq(instExpr.kind, "expr") + assert.eq((instExpr :: syntax.AstExprInstantiate).expr.tag, "global") + assert.eq((instExpr :: syntax.AstExprInstantiate).leftarrow1.text, "<") + assert.eq((instExpr :: syntax.AstExprInstantiate).leftarrow2.text, "<") + assert.eq(#(instExpr :: syntax.AstExprInstantiate).typearguments, 2) + assert.eq(((instExpr :: syntax.AstExprInstantiate).typearguments[1].node).kind, "type") + assert.eq(((instExpr :: syntax.AstExprInstantiate).typearguments[1].node).tag, "reference") + assert.eq(((instExpr :: syntax.AstExprInstantiate).typearguments[2].node).kind, "typepack") + assert.eq(((instExpr :: syntax.AstExprInstantiate).typearguments[2].node).tag, "variadic") + assert.eq((instExpr :: syntax.AstExprInstantiate).rightarrow1.text, ">") + assert.eq((instExpr :: syntax.AstExprInstantiate).rightarrow2.text, ">") + end) + suite:case("parseExprIndexName", function(assert) local indexExpr = parser.parseexpr("obj.field") assert.eq(indexExpr.tag, "indexname") diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index eb65db760..71d6a8677 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -163,48 +163,44 @@ lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:31:13-21 lute/std/libs/syntax/utils/trivia.luau:31:13-21 -lute/std/libs/syntax/visitor.luau:243:3-12 -lute/std/libs/syntax/visitor.luau:243:3-12 -lute/std/libs/syntax/visitor.luau:249:3-12 -lute/std/libs/syntax/visitor.luau:249:3-12 -lute/std/libs/syntax/visitor.luau:382:3-12 -lute/std/libs/syntax/visitor.luau:382:3-12 -lute/std/libs/syntax/visitor.luau:388:3-12 -lute/std/libs/syntax/visitor.luau:388:3-12 -lute/std/libs/syntax/visitor.luau:394:3-12 -lute/std/libs/syntax/visitor.luau:394:3-12 -lute/std/libs/syntax/visitor.luau:412:3-12 -lute/std/libs/syntax/visitor.luau:412:3-12 -lute/std/libs/syntax/visitor.luau:446:3-12 -lute/std/libs/syntax/visitor.luau:446:3-12 -lute/std/libs/syntax/visitor.luau:663:3-12 -lute/std/libs/syntax/visitor.luau:663:3-12 -lute/std/libs/syntax/visitor.luau:669:3-12 -lute/std/libs/syntax/visitor.luau:669:3-12 -lute/std/libs/syntax/visitor.luau:692:3-12 -lute/std/libs/syntax/visitor.luau:692:3-12 -lute/std/libs/syntax/visitor.luau:978:28-32 -lute/std/libs/syntax/visitor.luau:978:28-32 -lute/std/libs/syntax/visitor.luau:978:28-32 -lute/std/libs/syntax/visitor.luau:978:28-32 -lute/std/libs/syntax/visitor.luau:994:25-29 -lute/std/libs/syntax/visitor.luau:994:25-29 -lute/std/libs/syntax/visitor.luau:998:21-25 -lute/std/libs/syntax/visitor.luau:998:21-25 -lute/std/libs/syntax/visitor.luau:1012:21-25 -lute/std/libs/syntax/visitor.luau:1012:21-25 -lute/std/libs/syntax/visitor.luau:1012:21-25 -lute/std/libs/syntax/visitor.luau:1012:21-25 -lute/std/libs/syntax/visitor.luau:1019:17-21 -lute/std/libs/syntax/visitor.luau:1019:17-21 -lute/std/libs/syntax/visitor.luau:1047:9-20 -lute/std/libs/syntax/visitor.luau:1047:9-20 -lute/std/libs/syntax/visitor.luau:1048:3-12 -lute/std/libs/syntax/visitor.luau:1048:3-12 -lute/std/libs/syntax/visitor.luau:1049:9-24 -lute/std/libs/syntax/visitor.luau:1049:9-24 -lute/std/libs/syntax/visitor.luau:1050:22-25 -lute/std/libs/syntax/visitor.luau:1050:22-25 +lute/std/libs/syntax/visitor.luau:253:3-12 +lute/std/libs/syntax/visitor.luau:253:3-12 +lute/std/libs/syntax/visitor.luau:259:3-12 +lute/std/libs/syntax/visitor.luau:259:3-12 +lute/std/libs/syntax/visitor.luau:392:3-12 +lute/std/libs/syntax/visitor.luau:392:3-12 +lute/std/libs/syntax/visitor.luau:398:3-12 +lute/std/libs/syntax/visitor.luau:398:3-12 +lute/std/libs/syntax/visitor.luau:404:3-12 +lute/std/libs/syntax/visitor.luau:404:3-12 +lute/std/libs/syntax/visitor.luau:422:3-12 +lute/std/libs/syntax/visitor.luau:422:3-12 +lute/std/libs/syntax/visitor.luau:456:3-12 +lute/std/libs/syntax/visitor.luau:456:3-12 +lute/std/libs/syntax/visitor.luau:684:3-12 +lute/std/libs/syntax/visitor.luau:684:3-12 +lute/std/libs/syntax/visitor.luau:690:3-12 +lute/std/libs/syntax/visitor.luau:690:3-12 +lute/std/libs/syntax/visitor.luau:713:3-12 +lute/std/libs/syntax/visitor.luau:713:3-12 +lute/std/libs/syntax/visitor.luau:1018:25-29 +lute/std/libs/syntax/visitor.luau:1018:25-29 +lute/std/libs/syntax/visitor.luau:1022:21-25 +lute/std/libs/syntax/visitor.luau:1022:21-25 +lute/std/libs/syntax/visitor.luau:1036:21-25 +lute/std/libs/syntax/visitor.luau:1036:21-25 +lute/std/libs/syntax/visitor.luau:1036:21-25 +lute/std/libs/syntax/visitor.luau:1036:21-25 +lute/std/libs/syntax/visitor.luau:1043:17-21 +lute/std/libs/syntax/visitor.luau:1043:17-21 +lute/std/libs/syntax/visitor.luau:1071:9-20 +lute/std/libs/syntax/visitor.luau:1071:9-20 +lute/std/libs/syntax/visitor.luau:1072:3-12 +lute/std/libs/syntax/visitor.luau:1072:3-12 +lute/std/libs/syntax/visitor.luau:1073:9-24 +lute/std/libs/syntax/visitor.luau:1073:9-24 +lute/std/libs/syntax/visitor.luau:1074:22-25 +lute/std/libs/syntax/visitor.luau:1074:22-25 lute/std/libs/test/failure.luau:18:35-38 lute/std/libs/test/failure.luau:18:35-38 lute/std/libs/test/reporter.luau:10:19-32 From 0086568486a79507afadde5e517f993d3a3291ea Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 11 Feb 2026 10:27:53 -0800 Subject: [PATCH 343/642] Uses batteries argparsing in lute test (#805) Just a small refactor PR - gets rid of the manual(and kind of buggy) arg parsing lute test was doing and replaces it with the `@batteries/cli` arg parser. --- lute/cli/commands/test/init.luau | 80 +++++++------------------------- tools/check-faillist.txt | 1 - 2 files changed, 16 insertions(+), 65 deletions(-) diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index 811e68e25..8ab33ef72 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -1,3 +1,4 @@ +local cli = require("@batteries/cli") local finder = require("@self/finder") local filter = require("@self/filter") local luau = require("@std/luau") @@ -8,30 +9,6 @@ local runner = require("@std/test/runner") local reporter = require("@self/reporter") local test = require("@std/test") -local function printHelp() - print([[ -Usage: lute test [OPTIONS] [PATHS...] - -Run tests discovered in .test.luau and .spec.luau files. - -OPTIONS: - -h, --help Show this help message - --list List all discovered test cases without running them - -s, --suite=SUITE Run only tests in the specified suite - -c, --case=CASE Run only test cases matching the specified name - -PATHS: - Directories or files to search for tests (default: ./tests) - -EXAMPLES: - lute test Run all tests in ./tests - lute test --list List all test cases - lute test -s MyTestSuite Run all tests in MyTestSuite - lute test --suite MyTestSuite --case mytest Run specific test in suite - lute test --case "some case" Run all test cases named "some case" -]]) -end - local function loadtests(testfiles: { path.path }) -- Load all test files first for _, p in testfiles do @@ -71,54 +48,29 @@ local function runtests(env: testtypes.testenvironment, runner: testtypes.testru end local function main(...: string) - local args = { ... } - local testPaths = {} - local isListMode = false - local suiteName: string? = nil - local caseName: string? = nil + local args = cli.parser() - local i = 1 - while i <= #args do - local arg = args[i] + args:add("help", "flag", { help = "Show this help message", aliases = { "h" } }) + args:add("list", "flag", { help = "List all discovered test cases without running them" }) + args:add("suite", "option", { help = "Run only tests in the specified suite", aliases = { "s" } }) + args:add("case", "option", { help = "Run only test cases matching the specified name", aliases = { "c" } }) - if arg == "-h" or arg == "--help" then - printHelp() - return - elseif arg == "--list" then - isListMode = true - i += 1 - elseif arg == "-s" or arg == "--suite" then - i += 1 - if i <= #args then - suiteName = args[i] - i += 1 - else - print(`Error: {arg} requires a value`) - ps.exit(1) - end - elseif arg == "-c" or arg == "--case" then - i += 1 - if i <= #args then - caseName = args[i] - i += 1 - else - print(`Error: {arg} requires a value`) - ps.exit(1) - end - else - table.insert(testPaths, arg) - i += 1 - end + args:parse({ ... }) + + if args:has("help") then + print(args:help()) + return end - local searchpath = if #testPaths > 0 then testPaths else { "./tests" } + local testPaths = args:forwarded() + local searchpath = if testPaths and #testPaths > 0 then testPaths else { "./tests" } loadtests(finder.findtestfiles(searchpath)) - if isListMode then + if args:has("list") then listtests() else - local env = test._registered() - local filteredEnv = filter.filtertests(env, suiteName, caseName) + local env = test._registered() :: testtypes.testenvironment + local filteredEnv = filter.filtertests(env, args:get("suite"), args:get("case")) runtests(filteredEnv, runner.run, reporter.color) end end diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 71d6a8677..8fa2211a5 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -89,7 +89,6 @@ lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:62:6-17 lute/cli/commands/test/filter.luau:62:6-17 -lute/cli/commands/test/init.luau:121:42-44 lute/cli/commands/transform/init.luau:16:26-30 lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:47:26-37 From c245f71ceb01b0c2536f43c229848040544411ff Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:29:44 -0500 Subject: [PATCH 344/642] Boilerplate for Lute Lint Configuration System + Support for Ignores (#784) This PR adds boilerplate for a `lute lint` configuration system, based on the rough spec [here](https://roblox.atlassian.net/wiki/spaces/~7120203c8951ed160f49bdb87cbf260629a70f/pages/4328751232/Spec). This initial PR involves: - adding a `config` arg to the CLI - loading `config` at the start of lute lint execution flow, falling back to an empty table if no config file is given or present in the current working dir - support for the `ignores` field, which configures global file ignores using lua patterns and/or direct paths. --- lute/cli/commands/lint/configutils.luau | 30 +++++ lute/cli/commands/lint/init.luau | 43 ++++++- lute/cli/commands/lint/types.luau | 23 ++++ tests/cli/lint.test.luau | 162 ++++++++++++++++++++++++ tools/check-faillist.txt | 13 +- 5 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 lute/cli/commands/lint/configutils.luau diff --git a/lute/cli/commands/lint/configutils.luau b/lute/cli/commands/lint/configutils.luau new file mode 100644 index 000000000..b29cf0bc4 --- /dev/null +++ b/lute/cli/commands/lint/configutils.luau @@ -0,0 +1,30 @@ +local types = require("./types") + +local function assertIsArray(arr: { any }, type: string, err: string) + for k, v in arr do + assert(typeof(k) == "number", err) + assert(typeof(v) == type, err) + end +end + +-- we expect .config.luau file structured s.t top level table has: table.lute.lint field +local function assertValidConfig(candidate: any) + if candidate.ignores then + assert(typeof(candidate.ignores) == "table", "config.lute.lint.ignores must be an array of filepath globs") + assertIsArray(candidate.ignores, "string", "config.lute.lint.ignores must be an array of filepath globs") + end +end + +local function extractConfig(candidate: any): types.LintConfig + local luteLintConfig = if candidate.lute ~= nil then candidate.lute.lint else nil + if luteLintConfig then + assertValidConfig(luteLintConfig) + return luteLintConfig + end + + return {} +end + +return { + extractConfig = extractConfig, +} diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index db7b01daa..fc78ddd24 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -12,6 +12,8 @@ local tableext = require("@std/tableext") local triviaUtils = require("@std/syntax/utils/trivia") local types = require("@self/types") local visitor = require("@std/syntax/visitor") +local configUtils = require("@self/configutils") +local parseIgnores = require("./lib/parseIgnores") local DEFAULT_RULES = { "almost_swapped", "constant_table_comparison", "divide_by_zero", "parenthesized_conditions", "unused_variable" } @@ -26,6 +28,8 @@ Lint the specified Luau file using the specified lint rule(s) or using the defau OPTIONS: -h, --help Show this help message -v, --verbose Enable verbose output + -c, --config [PATH] Path to configuration file. By default, lute lint will look for a + the .config.luau file in the calling directory. -r, --rules [RULE] Path to a single lint rule or a folder containing lint rules. If a folder is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated @@ -338,7 +342,9 @@ end local function lintPaths( inputFilePaths: { string }, lintRules: { types.LintRule }, - autofixEnabled: boolean + autofixEnabled: boolean, + _lintConfig: types.LintConfig, -- will be used in follow-up PRs + ignoreData: parseIgnores.GitignoreData ): { [pathLib.path]: { types.LintViolation } } if VERBOSE then print("Filtering input files by gitignore") @@ -346,12 +352,22 @@ local function lintPaths( local inputFiles = files.getSourceFiles(inputFilePaths, VERBOSE) local allViolations: { [pathLib.path]: { types.LintViolation } } = {} + local hasIgnores = #ignoreData.ignores > 0 for _, inputPath in inputFiles do local walker = fs.walk(inputPath, { recursive = true }) local curr = walker() while curr ~= nil do + if hasIgnores then + local inputFilePathString = pathLib.format(curr) + + if parseIgnores.isIgnored(ignoreData, inputFilePathString, fs.type(inputFilePathString) == "dir") then + curr = walker() + continue + end + end + local success, err = pcall(lintFile, curr, lintRules, autofixEnabled) if success then -- Violations are returned as the second return value from pcall @@ -370,6 +386,7 @@ end local function main(...: string) local args = cli.parser() + args:add("config", "option", { help = "Path to configuration file", aliases = { "c" } }) args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) args:add("verbose", "flag", { help = "Enable verbose output", aliases = { "v" } }) @@ -401,6 +418,22 @@ local function main(...: string) process.exit(1) end + local lintConfig: types.LintConfig = {} + local configPath = args:get("config") + if configPath == nil then + -- search in calling dir + local cwd = process.cwd() + configPath = pathLib.join(cwd, ".config.luau") + end + if fs.exists(configPath) then + local success, loadedConfig = pcall(luau.loadbypath, configPath) + if success then + lintConfig = configUtils.extractConfig(loadedConfig) + elseif VERBOSE then + print(`Error loading config:\n{loadedConfig}\nProceeding with defaults.`) + end + end + local lintRules: { types.LintRule } local rulePath = args:get("rules") if rulePath == nil then @@ -446,7 +479,13 @@ local function main(...: string) print("Auto-fix is enabled.") end - local allViolations = lintPaths(inputFiles :: { string }, lintRules, autofixEnabled) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed + local ignores = if lintConfig.ignores ~= nil then lintConfig.ignores else {} + local rootLocation = if fs.exists(configPath) + then pathLib.format(pathLib.dirname(configPath)) + else pathLib.format(process.cwd()) + local ignoreData = parseIgnores.parseIgnoreContents(rootLocation, ignores) + + local allViolations = lintPaths(inputFiles :: { string }, lintRules, autofixEnabled, lintConfig, ignoreData) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed if args:has("json") then print(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.object, true)) diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index e7ed05f2b..f88312c57 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -23,4 +23,27 @@ export type LintRule = { read name: string, } +-- export type RuleConfig = { +-- ignores: { string }?, +-- severity: severity?, +-- off: true?, +-- options: unknown?, +-- } + +export type GlobalsConfig = { [string]: unknown } + +export type LintConfig = { + ignores: { path.pathlike }?, -- globs/paths to ignore as we walk lintee files + -- globals: GlobalsConfig?, -- globals to pass down to all rules via context + -- rules: { + -- paths: { path.pathlike }?, -- paths to local rules to use + -- [string]: RuleConfig, + -- }?, +} + +-- export type RuleContext = { +-- globals: GlobalsConfig, +-- options: unknown, +-- } + return {} diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index d2f50381c..4b93dab2d 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -2617,4 +2617,166 @@ end -- Teardown fs.remove(violatorPath) end) + + suite:case("throws error when config has invalid structure", function(assert) + -- local invalidGlobalConfig = [[ + -- return { + -- lute = { + -- lint = { + -- globals = "should be table" + -- } + -- } + -- } + -- ]] + + local invalidIgnoresConfig = [[ + return { + lute = { + lint = { + ignores = "should be table" + } + } + } + ]] + + -- local invalidRulesConfig = [[ + -- return { + -- lute = { + -- lint = { + -- rules = "should be table" + -- } + -- } + -- } + -- ]] + + local configFile = path.join(tmpDir, ".config.luau") + + -- -- fs.writestringtofile(path.format(configFile), invalidGlobalConfig) + -- local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(configFile) }) + -- assert.eq(result.exitcode, 1) + -- assert.strcontains(result.stderr, "config.globals must be a table of [string] : unknown") + + fs.writestringtofile(path.format(configFile), invalidIgnoresConfig) + local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(configFile) }) + assert.eq(result.exitcode, 1) + assert.strcontains(result.stderr, "config.lute.lint.ignores must be an array of filepath globs") + + -- fs.writestringtofile(path.format(configFile), invalidRulesConfig) + -- result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(configFile) }) + -- assert.eq(result.exitcode, 1) + -- assert.strcontains(result.stderr, "config.rules must be a table") + end) + + suite:case("lute lint accounts for configured ignores (config passed as arg)", function(assert) + local testSubDir = path.join(tmpDir, "testDir") + fs.createdirectory(testSubDir) + + local ignoredFile = path.join(testSubDir, "ignored.luau") + local notIgnoredFile = path.join(testSubDir, "valid.luau") + local violatingContents = [[ + a = b + b = a + c = 1 / 0 + ]] + fs.writestringtofile(ignoredFile, violatingContents) + fs.writestringtofile(notIgnoredFile, violatingContents) + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ + return { + lute = { + lint = { + ignores = { "**/ignored.luau" } + } + } + } + ]] + fs.writestringtofile(configFile, configContents) + + local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) + assert.eq(result.exitcode, 1) -- errors but ONLY from valid.luau + assert.strnotcontains(result.stdout, "ignored.luau") + assert.strcontains(result.stdout, "valid.luau") + + fs.removedirectory(testSubDir, { + recursive = true, + }) + end) + + suite:case("lute lint accounts for configured ignores (config auto-detected)", function(assert) + local testSubDir = path.join(tmpDir, "testDir") + fs.createdirectory(testSubDir) + + local ignoredFile = path.join(testSubDir, "ignored.luau") + + local violatingContents = [[ + a = b + b = a + c = 1 / 0 + ]] + fs.writestringtofile(ignoredFile, violatingContents) + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ + return { + lute = { + lint = { + ignores = { "**/ignored.luau" } + } + } + } + ]] + fs.writestringtofile(configFile, configContents) + + local result = process.run({ lutePath, "lint", path.format(testSubDir) }, { + cwd = path.format(testSubDir), + }) + assert.eq(result.exitcode, 0) -- no errors + assert.strnotcontains(result.stdout, "almost_swapped") + assert.strnotcontains(result.stdout, "divide_by_zero") + + fs.removedirectory(testSubDir, { + recursive = true, + }) + end) + + suite:case("lute lint configured ignores supplement (NOT override) existing gitignores", function(assert) + local testSubDir = path.join(tmpDir, "testDir") + fs.createdirectory(testSubDir) + + local ignoredFile = path.join(testSubDir, "ignored.luau") + local notIgnoredFile = path.join(testSubDir, "valid.luau") + local violatingContents = "c = 1 / 0" + fs.writestringtofile(ignoredFile, violatingContents) + fs.writestringtofile(notIgnoredFile, violatingContents) + + local gitIgnore = path.join(testSubDir, ".gitignore") -- gitignore `valid.luau` + fs.writestringtofile( + gitIgnore, + [[ +**/valid.luau + ]] + ) + + local configFile = path.join(testSubDir, ".config.luau") -- configure ignore of `ignored.luau` + local configContents = [[ + return { + lute = { + lint = { + ignores = { "**/ignored.luau" } + } + } + } + ]] + fs.writestringtofile(configFile, configContents) + + local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) + assert.eq(result.exitcode, 0) -- no errors bc we ignore BOTH files in the dir + assert.strnotcontains(result.stdout, "almost_swapped") + assert.strnotcontains(result.stdout, "divide_by_zero") + + fs.removedirectory(testSubDir, { + recursive = true, + }) + end) end) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 8fa2211a5..eebccdabf 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -61,7 +61,18 @@ lute/cli/commands/doc/init.luau:204:10-24 lute/cli/commands/doc/init.luau:310:5-16 lute/cli/commands/doc/init.luau:315:5-16 lute/cli/commands/doc/init.luau:321:10-24 -lute/cli/commands/lint/init.luau:52:10-18 +lute/cli/commands/lint/configutils.luau:19:55-73 +lute/cli/commands/lint/configutils.luau:19:55-73 +lute/cli/commands/lint/init.luau:56:10-18 +lute/cli/commands/lint/init.luau:428:15-24 +lute/cli/commands/lint/init.luau:429:56-65 +lute/cli/commands/lint/init.luau:429:56-65 +lute/cli/commands/lint/init.luau:429:56-65 +lute/cli/commands/lint/init.luau:429:56-65 +lute/cli/commands/lint/init.luau:429:56-65 +lute/cli/commands/lint/init.luau:429:56-65 +lute/cli/commands/lint/init.luau:483:37-46 +lute/cli/commands/lint/init.luau:484:40-49 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 From fe43d3d5f855fa41fb05842b01e6d28f4f1017c9 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 11 Feb 2026 11:57:01 -0800 Subject: [PATCH 345/642] Fixes a few typechecking errors in lute (#806) There is a type error in the std test library caused by the type of the assertions table not being in the `asserts.luau` file anymore. This caused a number of downstream type errors in test scripts, because they were adding annotation for an assertions table that didn't exist in that file. Edit: also fixed an fs.luau tc error. --- lute/std/libs/fs.luau | 3 +- lute/std/libs/test/reporter.luau | 2 - tests/std/syntax/printer.test.luau | 4 +- tests/std/test.test.luau | 4 +- tools/check-faillist.txt | 99 ++---------------------------- 5 files changed, 12 insertions(+), 100 deletions(-) diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 39d04b4e0..985448307 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -77,7 +77,7 @@ end --- Also provides a `close` method to stop watching. --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.watch as `for _ in fs.watch(...) do` directly. A while loop can be used instead. See example/watch_directory.luau for usage. function fslib.watch(path: pathlike): watcher - local queue = {} + local queue: { { filename: pathlib.path, event: watchevent } } = {} local handle = fs.watch(pathlib.format(path), function(filename: string, event: watchevent) table.insert(queue, { filename = pathlib.parse(filename), event = event }) end) @@ -88,6 +88,7 @@ function fslib.watch(path: pathlike): watcher return nil end local item = table.remove(queue, 1) + assert(item) return item.event end, close = function(_self: watcher): () diff --git a/lute/std/libs/test/reporter.luau b/lute/std/libs/test/reporter.luau index 8f0a48a10..5501d151a 100644 --- a/lute/std/libs/test/reporter.luau +++ b/lute/std/libs/test/reporter.luau @@ -3,11 +3,9 @@ -- Standard test reporter library for Luau local types = require("./types") -local assert = require("./assert") type FailedTest = types.failedtest type TestRunResult = types.testrunresult -type Assertions = assert.asserts local reporter = {} diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 18261aa3d..41fb2f942 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -1,4 +1,4 @@ -local assertLib = require("@std/test/assert") +local testtypes = require("@std/test/types") local printer = require("@std/syntax/printer") local query = require("@std/syntax/query") local syntax = require("@std/syntax") @@ -10,7 +10,7 @@ local function checkReplacement( source: string, expected: string, transformFn: (syntaxTypes.AstNode) -> syntaxTypes.replacements?, - assert: assertLib.asserts + assert: testtypes.asserts ) local ast = syntax.parse(source) local replacements = transformFn(ast.root) diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index 3558eb36e..324033056 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -1,4 +1,4 @@ -local assertions = require("@std/test/assert") +local testtypes = require("@std/test/types") local fs = require("@std/fs") local path = require("@std/path") local process = require("@std/process") @@ -12,7 +12,7 @@ local function assertErrorsWithMsg( testName: string, testContents: string, expectedErrMsg: string, - assert: assertions.asserts + assert: testtypes.asserts ) local testFilePath = path.join(tmpDir, `{testName}.test.luau`) fs.writestringtofile(testFilePath, testContents) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index eebccdabf..b99aa05c2 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -105,20 +105,12 @@ lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:75:9-14 lute/cli/commands/transform/lib/arguments.luau:75:9-14 -lute/std/libs/fs.luau:82:3-14 -lute/std/libs/fs.luau:82:3-14 -lute/std/libs/fs.luau:90:17-28 -lute/std/libs/fs.luau:90:17-28 -lute/std/libs/fs.luau:91:11-20 -lute/std/libs/fs.luau:91:11-20 -lute/std/libs/fs.luau:91:11-20 -lute/std/libs/fs.luau:91:11-20 -lute/std/libs/fs.luau:135:34-39 -lute/std/libs/fs.luau:135:34-39 -lute/std/libs/fs.luau:184:52-68 -lute/std/libs/fs.luau:184:52-68 -lute/std/libs/fs.luau:192:11-17 -lute/std/libs/fs.luau:192:11-17 +lute/std/libs/fs.luau:136:34-39 +lute/std/libs/fs.luau:136:34-39 +lute/std/libs/fs.luau:185:52-68 +lute/std/libs/fs.luau:185:52-68 +lute/std/libs/fs.luau:193:11-17 +lute/std/libs/fs.luau:193:11-17 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:42:25-29 @@ -213,8 +205,6 @@ lute/std/libs/syntax/visitor.luau:1074:22-25 lute/std/libs/syntax/visitor.luau:1074:22-25 lute/std/libs/test/failure.luau:18:35-38 lute/std/libs/test/failure.luau:18:35-38 -lute/std/libs/test/reporter.luau:10:19-32 -lute/std/libs/test/reporter.luau:10:19-32 tests/batteries/collections/deque.test.luau:9:13-16 tests/batteries/collections/deque.test.luau:16:13-16 tests/batteries/collections/deque.test.luau:24:13-16 @@ -270,68 +260,8 @@ tests/std/process.test.luau:123:41-42 tests/std/process.test.luau:126:43-44 tests/std/process.test.luau:130:41-53 tests/std/process.test.luau:134:33-48 -tests/std/syntax/printer.test.luau:13:10-26 -tests/std/syntax/printer.test.luau:35:8-13 -tests/std/syntax/printer.test.luau:49:8-13 -tests/std/syntax/printer.test.luau:63:8-13 -tests/std/syntax/printer.test.luau:82:8-13 tests/std/syntax/printer.test.luau:95:10-100 tests/std/syntax/printer.test.luau:97:7-32 -tests/std/syntax/printer.test.luau:103:8-13 -tests/std/syntax/printer.test.luau:119:8-13 -tests/std/syntax/printer.test.luau:131:8-13 -tests/std/syntax/printer.test.luau:143:8-13 -tests/std/syntax/printer.test.luau:155:8-13 -tests/std/syntax/printer.test.luau:167:8-13 -tests/std/syntax/printer.test.luau:180:8-13 -tests/std/syntax/printer.test.luau:192:8-13 -tests/std/syntax/printer.test.luau:204:8-13 -tests/std/syntax/printer.test.luau:216:8-13 -tests/std/syntax/printer.test.luau:228:8-13 -tests/std/syntax/printer.test.luau:240:8-13 -tests/std/syntax/printer.test.luau:252:8-13 -tests/std/syntax/printer.test.luau:264:8-13 -tests/std/syntax/printer.test.luau:276:8-13 -tests/std/syntax/printer.test.luau:288:8-13 -tests/std/syntax/printer.test.luau:300:8-13 -tests/std/syntax/printer.test.luau:312:8-13 -tests/std/syntax/printer.test.luau:324:8-13 -tests/std/syntax/printer.test.luau:336:8-13 -tests/std/syntax/printer.test.luau:349:8-13 -tests/std/syntax/printer.test.luau:366:8-13 -tests/std/syntax/printer.test.luau:381:8-13 -tests/std/syntax/printer.test.luau:396:8-13 -tests/std/syntax/printer.test.luau:411:8-13 -tests/std/syntax/printer.test.luau:426:8-13 -tests/std/syntax/printer.test.luau:439:8-13 -tests/std/syntax/printer.test.luau:452:8-13 -tests/std/syntax/printer.test.luau:465:8-13 -tests/std/syntax/printer.test.luau:480:8-13 -tests/std/syntax/printer.test.luau:495:8-13 -tests/std/syntax/printer.test.luau:509:8-13 -tests/std/syntax/printer.test.luau:523:8-13 -tests/std/syntax/printer.test.luau:538:8-13 -tests/std/syntax/printer.test.luau:553:8-13 -tests/std/syntax/printer.test.luau:566:8-13 -tests/std/syntax/printer.test.luau:579:8-13 -tests/std/syntax/printer.test.luau:593:8-13 -tests/std/syntax/printer.test.luau:606:8-13 -tests/std/syntax/printer.test.luau:619:8-13 -tests/std/syntax/printer.test.luau:632:8-13 -tests/std/syntax/printer.test.luau:645:8-13 -tests/std/syntax/printer.test.luau:658:8-13 -tests/std/syntax/printer.test.luau:671:8-13 -tests/std/syntax/printer.test.luau:684:8-13 -tests/std/syntax/printer.test.luau:697:8-13 -tests/std/syntax/printer.test.luau:710:8-13 -tests/std/syntax/printer.test.luau:723:8-13 -tests/std/syntax/printer.test.luau:736:8-13 -tests/std/syntax/printer.test.luau:749:8-13 -tests/std/syntax/printer.test.luau:762:8-13 -tests/std/syntax/printer.test.luau:775:8-13 -tests/std/syntax/printer.test.luau:788:8-13 -tests/std/syntax/printer.test.luau:801:8-13 -tests/std/syntax/printer.test.luau:814:8-13 tests/std/syntax/printer.test.luau:825:38-60 tests/std/syntax/printer.test.luau:825:38-60 tests/std/syntax/printer.test.luau:825:38-60 @@ -432,29 +362,12 @@ tests/std/syntax/printer.test.luau:825:38-60 tests/std/syntax/printer.test.luau:825:38-60 tests/std/syntax/printer.test.luau:825:38-60 tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:828:8-13 -tests/std/syntax/printer.test.luau:849:8-13 tests/std/syntax/printer.test.luau:877:23-31 -tests/std/syntax/printer.test.luau:884:8-13 tests/std/syntax/query.test.luau:41:7-7 tests/std/syntax/query.test.luau:41:7-7 tests/std/tableext.test.luau:57:18-18 tests/std/tableext.test.luau:62:18-18 tests/std/tableext.test.luau:63:18-18 -tests/std/test.test.luau:15:10-27 -tests/std/test.test.luau:113:4-9 -tests/std/test.test.luau:132:4-9 -tests/std/test.test.luau:149:4-9 -tests/std/test.test.luau:168:4-9 -tests/std/test.test.luau:185:4-9 -tests/std/test.test.luau:204:4-9 -tests/std/test.test.luau:223:4-9 -tests/std/test.test.luau:246:4-9 -tests/std/test.test.luau:263:4-9 -tests/std/test.test.luau:282:4-9 -tests/std/test.test.luau:299:4-9 -tests/std/test.test.luau:318:4-9 -tests/std/test.test.luau:337:4-9 tools/luthier.luau:158:2-26 tools/luthier.luau:197:3-12 tools/luthier.luau:198:11-25 From 7f9871dfa5f6ee1e1c8f1ca400b76d96374f5a66 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 11 Feb 2026 13:54:24 -0800 Subject: [PATCH 346/642] Fixes Windows absolute path parsing to support forward slashes after drive separator (#810) --- lute/std/libs/path/win32/init.luau | 5 +++-- tests/std/path/path.win32.test.luau | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index 53b863dff..850cdd14d 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -195,8 +195,9 @@ function win32.parse(path: pathlike): path kind = "unc" elseif string.match(path, "^%a:") ~= nil then driveLetter = string.sub(path, 1, 1) - -- Windows: starts with drive letter + backslash (C:\) - kind = if string.sub(path, 3, 3) == "\\" then "absolute" else "relative" + -- Windows: starts with drive letter + separator (C:\ or C:/) + local sep = path:sub(3, 3) + kind = if sep == "\\" or sep == "/" then "absolute" else "relative" end local parts = {} diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index 543fcdedf..81b8b5ba1 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -816,4 +816,15 @@ test.suite("PathWin32RelativeSuite", function(suite) assert.eq(result.parts[1], "subfolder") assert.eq(result.parts[2], "file.txt") end) + + suite:case("parse_windows_absolute_path_with_forward_slashes", function(assert) + local result: win32.path = path.win32.parse("C:/Users/username/Documents/file.txt") + assert.eq(result.kind, "absolute") + assert.eq(result.drive, "C") + assert.eq(#result.parts, 4) + assert.eq(result.parts[1], "Users") + assert.eq(result.parts[2], "username") + assert.eq(result.parts[3], "Documents") + assert.eq(result.parts[4], "file.txt") + end) end) From 71edcc8bf41411e0bf5f0ba0a71d4be1a4489136 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:56:36 -0800 Subject: [PATCH 347/642] Updates serialization of MetatableType and ExternType with tests (#814) Fixes and adds tests for `MetatableType` and `ExternType` serialization. ty to ariel for clarifying that a metatable type is just a table type with a metatable field, so updated now! --------- Co-authored-by: Sora Kanosue --- definitions/luau.luau | 1 - lute/luau/src/type.cpp | 7 +-- tests/src/typeserializer.test.cpp | 80 ++++++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index a233d0324..a7bbefe80 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -715,7 +715,6 @@ export type Type = { | "union" | "intersection" | "table" - | "metatable" | "function" | "extern" | "generic", diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp index a5716206d..c98ebe54f 100644 --- a/lute/luau/src/type.cpp +++ b/lute/luau/src/type.cpp @@ -368,18 +368,15 @@ struct TypeSerialize final : public Luau::TypeVisitor } // Luau metatable type: - // table: type, + // same as table type, but with an additional field: // metatable: type? void serialize(TypeId ty, const MetatableType& mtv) { checkStack(L, 2); - lua_createtable(L, 0, 3); + lua_createtable(L, 0, 2); registerType(ty); - pushTag("metatable"); - traverse(mtv.table); - lua_setfield(L, -2, "table"); traverse(mtv.metatable); lua_setfield(L, -2, "metatable"); diff --git a/tests/src/typeserializer.test.cpp b/tests/src/typeserializer.test.cpp index b70c7b81b..aefd660b3 100644 --- a/tests/src/typeserializer.test.cpp +++ b/tests/src/typeserializer.test.cpp @@ -437,7 +437,85 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_cyclic_table_type") REQUIRE(lua_equal(L, root, -1)); // The 'next' property's read type should be the same table as the root (cycle) } -// TODO: MetatableType, ExternType tests +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_metatable_type") +{ + // Create a metatable type: { __index: number } with metatable { __mode: string } + TypeId numberType = arena.addType(PrimitiveType{PrimitiveType::Number}); + TypeId stringType = arena.addType(PrimitiveType{PrimitiveType::String}); + TableType metatable; + metatable.props["__mode"] = Property::readonly(stringType); + TypeId metatableType = arena.addType(metatable); + TableType tty; + tty.props["__index"] = Property::readonly(numberType); + + TypeId ttyType = arena.addType(tty); + TypeId tableType = arena.addType(MetatableType{ttyType, metatableType}); + + lua_checkstack(L, 4); + + // { tag: "table", properties: { __index: { read: { tag: "number" } } } }, metatable: { tag: "table", properties: { __mode: { read: { tag: "string" } } } } + REQUIRE_EQ(Luau::serializeType(L, tableType), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "table"); + + lua_getfield(L, -1, "properties"); + REQUIRE(lua_istable(L, -1)); + + lua_getfield(L, -1, "__index"); + REQUIRE(lua_istable(L, -1)); + + lua_getfield(L, -1, "read"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "number"); + lua_pop(L, 3); // pop read type, __index property, properties field + + lua_getfield(L, -1, "metatable"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "table"); + + lua_getfield(L, -1, "properties"); + REQUIRE(lua_istable(L, -1)); + lua_getfield(L, -1, "__mode"); + REQUIRE(lua_istable(L, -1)); + lua_getfield(L, -1, "read"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "string"); +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_extern_type_with_parent_and_metatable") +{ + TypeId parentType = arena.addType(TableType{}); + TypeId metatableType = arena.addType(TableType{}); + + TypeId ty = arena.addType(ExternType{ + "MyExtern", + {}, + parentType, + metatableType, + {}, + nullptr, + "Module", + std::nullopt + }); + + lua_checkstack(L, 2); + + // { tag: "extern", parent: { tag: "table" }, metatable: { tag: "table" } } + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "extern"); + + lua_getfield(L, -1, "parent"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "table"); + lua_pop(L, 1); // parent + + lua_getfield(L, -1, "metatable"); + REQUIRE(lua_istable(L, -1)); + requireStringField(L, "tag", "table"); +} TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_simple_type_pack_nil_tail") { From e8f7836608f821dbece2e731d7de59e5107bc2b0 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 12 Feb 2026 11:52:00 -0800 Subject: [PATCH 348/642] Adds unused variable docs and links violations to it (#812) Also removed a cast from the unused variable list to report it in the faillist instead --- docs/cli/lint/unused_variable.md | 34 +++++++++++++++++++ .../commands/lint/rules/unused_variable.luau | 6 ++-- tests/cli/lint.test.luau | 2 ++ tools/check-faillist.txt | 1 + 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 docs/cli/lint/unused_variable.md diff --git a/docs/cli/lint/unused_variable.md b/docs/cli/lint/unused_variable.md new file mode 100644 index 000000000..b34fe2481 --- /dev/null +++ b/docs/cli/lint/unused_variable.md @@ -0,0 +1,34 @@ +# unused_variable + +This lint rule checks for instances of unused locals. +Unlike Luau's built-in linter, this also flags instances of unused function parameters and loop variables. +To silence instances of deliberately unused variables, lint warnings can be silenced by prefixing the variable name with `_`. + +## Why this is discouraged + +Unused variables can result in less readable code in the best case, and worse performance in the worst case. + +## Example violations + +`constant_table_comparison` will warn on the following: + +```luau +local function _(x) + return nil +end +``` + +You should instead consider: + +```luau +local function _() + return nil +end +``` + +Or simply silence the warning: +```luau +local function _(_x) + return nil +end +``` \ No newline at end of file diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index c687fd3c9..1babbfd5d 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -6,6 +6,7 @@ local visitorLib = require("@std/syntax/visitor") local name = "unused_variable" local message = "Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." +local target = "https://lute.luau.org/cli/lint/unused_variable.html" local function isRequireCall(expr: syntax.AstExpr) return expr.tag == "call" and expr.func.tag == "global" and expr.func.name.text == "require" @@ -298,14 +299,15 @@ local function unusedLocals(block: syntax.AstStatBlock): { syntax.AstLocal } end local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } - return tableext.map(unusedLocals(ast), function(l) + return tableext.map(unusedLocals(ast), function(l): lintTypes.LintViolation return table.freeze({ lintname = name, location = l.location, message = message, severity = "warning", sourcepath = sourcepath, - }) :: lintTypes.LintViolation + target = target, + }) -- LUAUFIX: Table literal doesn't subtype against lintTypes.LintViolation for some reason end) end diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 4b93dab2d..0eeea0c1e 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -1022,6 +1022,7 @@ local x = 1 / 0 "items": [ { "message": "Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + "codeDescription": "https://lute.luau.org/cli/lint/unused_variable.html", "source": "lute lint", "code": "unused_variable", "severity": 2, @@ -1280,6 +1281,7 @@ b = a [ { "message": "Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", + "codeDescription": "https://lute.luau.org/cli/lint/unused_variable.html", "source": "lute lint", "code": "unused_variable", "severity": 2, diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index b99aa05c2..377259242 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -80,6 +80,7 @@ lute/cli/commands/lint/printer.luau:58:8-52 lute/cli/commands/lint/rules/parenthesized_conditions.luau:23:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:38:6-17 lute/cli/commands/lint/rules/parenthesized_conditions.luau:64:4-15 +lute/cli/commands/lint/rules/unused_variable.luau:303:3-100 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau:25:42-59 From 73d8e169eb1eda39cd96c3220c3a1a59632b61d3 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 12 Feb 2026 11:56:09 -0800 Subject: [PATCH 349/642] Refactors lint tests (#809) Just a refactor, no behavioral changes --- tests/cli/lint.test.luau | 2220 +++++++++++++++----------------------- 1 file changed, 873 insertions(+), 1347 deletions(-) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 0eeea0c1e..15b2d2e5f 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -2,7 +2,9 @@ local fs = require("@std/fs") local path = require("@std/path") local process = require("@std/process") local system = require("@std/system") +local tableext = require("@std/tableext") local test = require("@std/test") +local testTypes = require("@std/test/types") local lutePath = path.format(process.execpath()) local tmpDir = system.tmpdir() @@ -16,8 +18,76 @@ local defaultRulesPaths = { unused_variable = path.format(path.join(defaultRulesFolder, "unused_variable.luau")), } +local LINT_RULE_MESSAGES = { + almost_swapped = "warning[almost_swapped]: This looks like a failed attempt to swap.", + constant_table_comparison = "warning[constant_table_comparison]: Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false.", + divide_by_zero = "warning[divide_by_zero]: Division by zero detected.", + parenthesized_conditions = "info[parenthesized_conditions]: Luau doesn't require parentheses around conditions. You can remove them for readability.", + unused_variable = "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", +} + local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" +type lintTestParams = { + content: string, + expectedExitCode: number, + rulePath: string?, + expectedAutofixContents: string?, + extraOptions: { string }?, + outputExpectations: { { expectedOutput: string, count: number } }?, +} + +local function lintTestHelper(assert: testTypes.asserts, params: lintTestParams): string + -- Setup + -- Create a file that violates the rule + local violatorPath = path.format(path.join(tmpDir, "violator.luau")) + fs.writestringtofile(violatorPath, params.content) + + -- Do + -- Run the transformer on the transformee + local lintArgs = { lutePath, "lint" } + + if params.rulePath then + table.insert(lintArgs, "-r") + table.insert(lintArgs, params.rulePath) + end + + if params.expectedAutofixContents then + table.insert(lintArgs, "--auto-fix") + end + + if params.extraOptions then + tableext.extend(lintArgs, params.extraOptions) + end + + table.insert(lintArgs, violatorPath) + + local result = process.run(lintArgs) + + -- Check + assert.eq(result.exitcode, params.expectedExitCode) + + if params.expectedAutofixContents then + local fixedContents = fs.readfiletostring(violatorPath) + assert.eq(fixedContents, params.expectedAutofixContents) + end + + if params.outputExpectations then + for _, expectation in params.outputExpectations do + if expectation.count == 0 then + assert.strnotcontains(result.stdout, expectation.expectedOutput) + else + assert.strcontains(result.stdout, expectation.expectedOutput, nil, expectation.count) + end + end + end + + -- Teardown + fs.remove(violatorPath) + + return result.stdout +end + test.suite("lute lint", function(suite) suite:beforeeach(function() -- A test that creates rulesDir which fails won't cleanup after itself @@ -56,135 +126,73 @@ test.suite("lute lint", function(suite) end) suite:case("lute lint error code 0 with no violations", function(assert) - local modulePath = path.join(tmpDir, "module") - fs.createdirectory(modulePath) - - local result = process.run({ lutePath, "lint", path.format(modulePath) }) - - assert.eq(result.exitcode, 0) - - assert.eq(result.stdout, "") - - fs.removedirectory(modulePath) - end) - - suite:case("lute lint non verbose", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile(violatorPath, "") - - -- Do - -- Run the linter - local result = process.run({ lutePath, "lint", violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) + local output = lintTestHelper(assert, { content = "", expectedExitCode = 0 }) - assert.eq(result.stdout, "") - - -- Teardown - fs.remove(violatorPath) + assert.eq(output, "") end) suite:case("lute lint verbose", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile(violatorPath, "") - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-v", violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strcontains(result.stdout, "Using default lint rules.") - assert.neq(result.stdout:find("Reading input file '.+violator%.luau'"), nil) - assert.neq(result.stdout:find("Parsing input file '.+violator%.luau'"), nil) - assert.strcontains(result.stdout, "Applying lint rules") - assert.strcontains(result.stdout, "Printing violations from 0 files") + local output = lintTestHelper(assert, { + content = "", + expectedExitCode = 0, + extraOptions = { "-v" }, + outputExpectations = { + { expectedOutput = "Using default lint rules.", count = 1 }, + { expectedOutput = "Applying lint rules", count = 1 }, + { expectedOutput = "Printing violations from 0 files", count = 1 }, + }, + }) - -- Teardown - fs.remove(violatorPath) + assert.neq(output:find("Reading input file '.+violator%.luau'"), nil) + assert.neq(output:find("Parsing input file '.+violator%.luau'"), nil) end) suite:case("divide_by_zero", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 1 / 0 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") - local expected = [[ +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.divide_by_zero, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, + { + expectedOutput = [[ violator.luau:1:11-16 ── │ 1 │ local x = 1 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("divide_by_zero auto-fix", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local _x = 1 / 0 local _y = -3 // 0 local _z = 4 % 0 local _nan = 0 / 0 local _negnan = -0 / 0 -]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "--auto-fix", violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) -- After applying the auto-fix, there are no more violations - - local fixedContents = fs.readfiletostring(violatorPath) - local expectedFixedContents = [[ +]], + expectedExitCode = 0, + expectedAutofixContents = [[ local _x = math.huge local _y = -math.huge local _z = 0 / 0 local _nan = 0 / 0 local _negnan = 0 / 0 -]] - assert.eq(fixedContents, expectedFixedContents) - - -- Teardown - fs.remove(violatorPath) +]], + }) end) suite:case("almost_swapped", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ a = b b = a @@ -201,84 +209,70 @@ if true then t["a"] = t["b"] t["b"] = t["a"] end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.almost_swapped, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - -- We expect 4 warnings, so stdout should be split into 5 parts - assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.", nil, 4) - local expected = [[ +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.almost_swapped, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 4 }, + { + expectedOutput = [[ violator.luau:1:1-2:6 ── │ 1 │ ╭ a = b 2 │ │ b = a │ ╰─────^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = "Suggested fix: a, b = b, a" - - assert.strcontains(result.stdout, expected) - expected = [[ +Suggested fix: a, b = b, a +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:6:1-7:6 ── │ 6 │ ╭ x = y 7 │ │ y = x │ ╰─────^ │ -]] - assert.strcontains(result.stdout, expected) - expected = "Suggested fix: x, y = y, x" - - assert.strcontains(result.stdout, expected) - - expected = [[ +Suggested fix: x, y = y, x +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:10:1-11:10 ── │ 10 │ ╭ t.a = t.b 11 │ │ t.b = t.a │ ╰─────────^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = "Suggested fix: t.a, t.b = t.b, t.a" - - assert.strcontains(result.stdout, expected) - expected = [[ +Suggested fix: t.a, t.b = t.b, t.a +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:14:2-15:17 ── │ 14 │ ╭ t["a"] = t["b"] 15 │ │ t["b"] = t["a"] │ ╰───────────────────^ │ -]] - assert.strcontains(result.stdout, expected) - expected = 'Suggested fix: t["a"], t["b"] = t["b"], t["a"]' - - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +Suggested fix: t["a"], t["b"] = t["b"], t["a"] +]], + count = 1, + }, + }, + }) end) suite:case("almost swapped auto-fix", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ a = b b = a @@ -295,18 +289,9 @@ if true then t["a"] = t["b"] t["b"] = t["a"] end -]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "--auto-fix", violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) -- After applying the auto-fix, there are no more violations - - local fixedContents = fs.readfiletostring(violatorPath) - local expectedFixedContents = [[ +]], + expectedExitCode = 0, + expectedAutofixContents = [[ a, b = b, a local x = 0 @@ -319,14 +304,12 @@ t.a, t.b = t.b, t.a if true then t["a"], t["b"] = t["b"], t["a"] end -]] - assert.eq(fixedContents, expectedFixedContents) - - -- Teardown - fs.remove(violatorPath) +]], + }) end) - suite:case("lute lint multiple rules", function(assert) + suite:case("lute lint custom rules dir", function(assert) + -- TODO: Use new helper after adding option to turn off default rules -- Setup fs.createdirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau @@ -362,7 +345,7 @@ b = a -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) local expected = [[ violator.luau:1:11-16 ── │ @@ -372,7 +355,7 @@ violator.luau:1:11-16 ── ]] assert.strcontains(result.stdout, expected) - assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) expected = [[ violator.luau:3:1-4:6 ── │ @@ -389,6 +372,7 @@ violator.luau:3:1-4:6 ── end) suite:case("lute lint multiple rules but one errors", function(assert) + -- TODO: Use new helper after adding option to turn off default rules -- Setup fs.createdirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau @@ -423,7 +407,7 @@ b = a -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) local expected = [[ violator.luau:1:1-2:6 ── │ @@ -485,7 +469,7 @@ y = x -- Check assert.eq(result.exitcode, 1) - assert.eq(#result.stdout:split("warning[divide_by_zero]: Division by zero detected."), 3) + assert.eq(#result.stdout:split(LINT_RULE_MESSAGES.divide_by_zero), 3) local expected = [[ violator1.luau:1:11-16 ── │ @@ -504,7 +488,7 @@ violator2.lua:1:11-17 ── ]] assert.strcontains(result.stdout, expected) - assert.eq(#result.stdout:split("warning[almost_swapped]: This looks like a failed attempt to swap."), 3) + assert.eq(#result.stdout:split(LINT_RULE_MESSAGES.almost_swapped), 3) expected = [[ violator1.luau:3:1-4:6 ── │ @@ -562,8 +546,8 @@ lintee.luau': @std/syntax/parser.luau:12: parsing failed: ]] assert.strcontains(result.stdout, expected) - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") - assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) -- Teardown fs.remove(lintee) @@ -571,56 +555,45 @@ lintee.luau': @std/syntax/parser.luau:12: parsing failed: end) suite:case("lute lint default rules", function(assert) - -- Create a file that violates the default almost_swapped and divide_by_zero rules - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 1 / 0 a = b b = a - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") - local expected = [[ +]], + expectedExitCode = 1, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, + { + expectedOutput = [[ violator.luau:1:11-16 ── │ 1 │ local x = 1 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") - expected = [[ +]], + count = 1, + }, + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 1 }, + { + expectedOutput = [[ violator.luau:3:1-4:6 ── │ 3 │ ╭ a = b 4 │ │ b = a │ ╰─────^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("lute lint warning suppression", function(assert) - -- Create a file that violates the default almost_swapped and divide_by_zero rules - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ -- lute-lint-ignore(divide_by_zero) local x = 1 / 0 @@ -631,34 +604,58 @@ end a = b b = a - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", violatorPath }) +]], + expectedExitCode = 1, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 0 }, + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 1 }, + { + expectedOutput = [[ +violator.luau:9:1-10:6 ── + │ + 9 │ ╭ a = b + 10 │ │ b = a + │ ╰─────^ + │ +]], + count = 1, + }, + }, + }) + end) - -- Check - assert.eq(result.exitcode, 1) + suite:case("suppression is limited to following statement", function(assert) + lintTestHelper(assert, { + content = [[ +-- lute-lint-ignore(divide_by_zero) +local x = 1 / 0 - assert.strnotcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") +-- lute-lint-ignore(divide_by_zero) +if true then + local y = 10 / 0 +end - assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") - local expected = [[ +a = b +b = a +]], + expectedExitCode = 1, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 0 }, + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 1 }, + { + expectedOutput = [[ violator.luau:9:1-10:6 ── │ 9 │ ╭ a = b 10 │ │ b = a │ ╰─────^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) - end) +]], + count = 1, + }, + }, + }) - suite:case("suppression is limited to following statement", function(assert) -- Setup -- Create a file that violates the rule local violatorPath = path.format(path.join(tmpDir, "violator.luau")) @@ -683,7 +680,7 @@ local z = 1 / 0 -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero, nil, 2) local expected = [[ violator.luau:5:12-18 ── │ @@ -707,12 +704,8 @@ violator.luau:8:11-16 ── end) suite:case("suppression is limited to following statement2", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 1 / 0 -- lute-lint-ignore(divide_by_zero) @@ -721,157 +714,122 @@ if true then end local z = 1 / 0 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.divide_by_zero, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 2 }, + { + expectedOutput = [[ violator.luau:1:11-16 ── │ 1 │ local x = 1 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:8:11-16 ── │ 8 │ local z = 1 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("suppression is limited to following statement3", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local function foo() -- lute-lint-ignore(divide_by_zero) local y = 3 / 0 return 3 / 0 end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.divide_by_zero, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, + { + expectedOutput = [[ violator.luau:5:9-14 ── │ 5 │ return 3 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("suppression is limited to following statement4", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ if true then -- lute-lint-ignore(divide_by_zero) local y = 3 / 0 return 3 / 0 end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.divide_by_zero, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, + { + expectedOutput = [[ violator.luau:5:9-14 ── │ 5 │ return 3 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("suppression is limited to following statement5", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ while true do -- lute-lint-ignore(divide_by_zero) local y = 3 / 0 return 3 / 0 end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.divide_by_zero, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, + { + expectedOutput = [[ violator.luau:5:9-14 ── │ 5 │ return 3 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("suppression is limited to following statement6", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local t = {} for _, _ in t do -- lute-lint-ignore(divide_by_zero) @@ -879,100 +837,68 @@ for _, _ in t do return 3 / 0 end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.divide_by_zero, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, + { + expectedOutput = [[ violator.luau:6:9-14 ── │ 6 │ return 3 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("suppression for multi node violation", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ -- lute-lint-ignore(almost_swapped) a = b b = a - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") - - -- Teardown - fs.remove(violatorPath) + ]], + expectedExitCode = 0, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 0 }, + }, + }) end) suite:case("suppression for multi node violation", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ -- lute-lint-ignore(almost_swapped) a = b b = a a = b - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", violatorPath }) - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.", nil, 1) - - local expected = [[ + ]], + expectedExitCode = 1, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 1 }, + { + expectedOutput = [[ violator.luau:3:1-4:6 ── │ 3 │ ╭ b = a 4 │ │ a = b │ ╰─────^ │ -]] - - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("nested suppressions", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ -- lute-lint-ignore(divide_by_zero) local function _foo() local _x = 1/0 @@ -982,40 +908,25 @@ local function _foo() b = a end end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", violatorPath }) - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") - assert.strnotcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") - - -- Teardown - fs.remove(violatorPath) + ]], + expectedExitCode = 0, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 0 }, + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 0 }, + }, + }) end) suite:case("json output", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 1 / 0 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-j", violatorPath }) - -- Check - assert.eq(result.exitcode, 0) - - local expected = [[ + ]], + expectedExitCode = 0, + extraOptions = { "-j" }, + outputExpectations = { + { + expectedOutput = [[ { "items": [ { @@ -1068,11 +979,11 @@ local x = 1 / 0 } } ], - "uri":]] -- URI has absolute tmpdir path, so we cut off here - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) + "uri":]], -- URI has absolute tmpdir path, so we cut off here + count = 1, + }, + }, + }) end) suite:case("lute lint respects gitignore", function(assert) @@ -1103,7 +1014,7 @@ local y = 10 / 0 -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero, nil, 1) local expected = [[ violator1.luau:1:11-17 ── │ @@ -1127,11 +1038,8 @@ violator2.lua:1:11-17 ── end) suite:case("lute lint report directive", function(assert) - -- Create a file that violates the default almost_swapped and divide_by_zero rules - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 1 / 0 -- lute-lint-ignore(divide_by_zero) if true then @@ -1139,46 +1047,37 @@ if true then -- lute-lint-report(divide_by_zero) local y = 10 / 0 end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 2) - - local expected = [[ + ]], + expectedExitCode = 1, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 2 }, + { + expectedOutput = [[ violator.luau:1:11-16 ── │ 1 │ local x = 1 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:6:12-18 ── │ 6 │ local y = 10 / 0 │ ^^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("lute lint global ignore directive", function(assert) - -- Create a file that violates the default almost_swapped and divide_by_zero rules - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ -- lute-lint-global-ignore(divide_by_zero) local x = 1 / 0 -- lute-lint-report(divide_by_zero) @@ -1187,29 +1086,22 @@ if true then -- lute-lint-ignore(divide_by_zero) local y = 10 / 0 end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) - - local expected = [[ + ]], + expectedExitCode = 1, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, + { + expectedOutput = [[ violator.luau:5:12-17 ── │ 5 │ local z = 3 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("lute lint string input", function(assert) @@ -1225,7 +1117,7 @@ b = a -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) local expected = [[ ┌── input:1:1-2:6 ── │ @@ -1252,7 +1144,7 @@ b = a assert.strcontains(result.stdout, "Parsing global lint ignores in input string") assert.strcontains(result.stdout, "Applying lint rules") assert.strcontains(result.stdout, "Printing violations from string input") - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.") + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) end) suite:case("lute lint string input with no violations", function(assert) @@ -1331,34 +1223,26 @@ b = a end) suite:case("lute lint ignore (table item)", function(assert) - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local y = { -- lute-lint-ignore(divide_by_zero) x = 3 / 0 } local z = 5 / 0 -]] - ) - - local result = process.run({ lutePath, "lint", violatorPath }) - assert.eq(result.exitcode, 1) - assert.strnotcontains(result.stdout, `Error linting file '{violatorPath}'`) - assert.strcontains(result.stdout, "warning[divide_by_zero]: Division by zero detected.", nil, 1) - - fs.remove(violatorPath) +]], + expectedExitCode = 1, + outputExpectations = { + { expectedOutput = "Error linting file", count = 0 }, + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, + }, + }) end) suite:case("parenthesized_conditions", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ if (3 > 0) then print("Hello, world!") elseif (false) then @@ -1374,95 +1258,87 @@ end repeat print("Still nope.") until (5 == 5) - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.parenthesized_conditions, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "info[parenthesized_conditions]: Luau doesn't require parentheses around conditions. You can remove them for readability.", - nil, - 6 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.parenthesized_conditions, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.parenthesized_conditions, + count = 6, + }, + { + expectedOutput = [[ violator.luau:1:4-11 ── │ 1 │ if (3 > 0) then │ ^^^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:3:8-15 ── │ 3 │ elseif (false) then │ ^^^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Have to do this here because the path is a lot shorter on Linux, so there's more dashes to match the length of the line - expected = [[ -violator.luau:7:14-21 ──]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = "violator.luau:7:14-21 ──", + count = 1, + }, + { + expectedOutput = [[ │ 7 │ local x = if (2 < 1) then 10 elseif (true) then 3 else 20 │ ^^^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ -violator.luau:7:37-43 ──]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = "violator.luau:7:37-43 ──", + count = 1, + }, + { + expectedOutput = [[ │ 7 │ local x = if (2 < 1) then 10 elseif (true) then 3 else 20 │ ^^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:9:7-27 ── │ 9 │ while ("hello" == "world") do │ ^^^^^^^^^^^^^^^^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:15:7-15 ── │ 15 │ until (5 == 5) │ ^^^^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("parenthesized_conditions auto-fix", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ if (3 > 0) then print("Hello, world!") elseif (false) then @@ -1478,25 +1354,10 @@ end repeat print("Still nope.") until (5 == 5) -]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ - lutePath, - "lint", - "--auto-fix", - "-r", - defaultRulesPaths.parenthesized_conditions, - violatorPath, - }) - - -- Check - assert.eq(result.exitcode, 0) - - local fixedContents = fs.readfiletostring(violatorPath) - local expectedFixedContents = [[ +]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.parenthesized_conditions, + expectedAutofixContents = [[ if 3 > 0 then print("Hello, world!") elseif false then @@ -1512,179 +1373,123 @@ end repeat print("Still nope.") until 5 == 5 -]] - - assert.eq(fixedContents, expectedFixedContents) - - -- Teardown - fs.remove(violatorPath) +]], + }) end) suite:case("report violations remaining after auto-fix", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ a = b b = a local x = t == { x = 3 } -]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "--auto-fix", violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) -- After applying the auto-fix, there is still a violation - - local fixedContents = fs.readfiletostring(violatorPath) - local expectedFixedContents = [[ +]], + expectedExitCode = 1, + expectedAutofixContents = [[ a, b = b, a local x = t == { x = 3 } -]] - assert.eq(fixedContents, expectedFixedContents) - - assert.strcontains( - result.stdout, - "warning[constant_table_comparison]: Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false." - ) - - assert.strnotcontains(result.stdout, "warning[almost_swapped]: This looks like a failed attempt to swap.") - - -- Teardown - fs.remove(violatorPath) +]], + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.constant_table_comparison, + count = 1, + }, + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 0 }, + }, + }) end) suite:case("constant_table_comparison", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local a = { 1, 2, 3 } == t local b = t == { 4, 5, 6 } local c = { 1 } == t local d = t == ({ 4 } :: { string }) local e = t ~= { 1, 2 } - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = - process.run({ lutePath, "lint", "-r", defaultRulesPaths.constant_table_comparison, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[constant_table_comparison]: Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false.", - nil, - 5 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.constant_table_comparison, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.constant_table_comparison, + count = 5, + }, + { + expectedOutput = [[ violator.luau:1:11-27 ── │ 1 │ local a = { 1, 2, 3 } == t │ ^^^^^^^^^^^^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:2:11-27 ── │ 2 │ local b = t == { 4, 5, 6 } │ ^^^^^^^^^^^^^^^^ │ -]] - - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:3:11-21 ── │ 3 │ local c = { 1 } == t │ ^^^^^^^^^^ │ -]] - - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:4:11-37 ── │ 4 │ local d = t == ({ 4 } :: { string }) │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ │ -]] - - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:5:11-24 ── │ 5 │ local e = t ~= { 1, 2 } │ ^^^^^^^^^^^^^ │ -]] - - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("constant_table_comparison auto-fix", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local a = {} == t local b = t == {} local c = t == ({}) local d = t == ({} :: { string }) local e = t ~= {} -]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ - lutePath, - "lint", - "--auto-fix", - "-r", - defaultRulesPaths.constant_table_comparison, - violatorPath, - }) - - -- Check - assert.eq(result.exitcode, 0) - - local fixedContents = fs.readfiletostring(violatorPath) - local expectedFixedContents = [[ +]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.constant_table_comparison, + expectedAutofixContents = [[ local a = next(t) == nil local b = next(t) == nil local c = next(t) == nil local d = next(t) == nil local e = next(t) ~= nil -]] - - assert.eq(fixedContents, expectedFixedContents) - - -- Teardown - fs.remove(violatorPath) +]], + }) end) suite:case("lute lint json output includes suggestedfix", function(assert) @@ -1737,887 +1542,608 @@ local e = next(t) ~= nil end) suite:case("unused_variable_1", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 3 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local x = 3 │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_2", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local _ = 3 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." - ) - - -- Teardown - fs.remove(violatorPath) + ]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 0, + }, + }, + }) end) suite:case("unused_variable_3", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ for i = 3, 4 do print(3) end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:5-6 ── │ 1 │ for i = 3, 4 do │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_4", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local t = {1, 2, 3} for i, v in t do print(3) end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 2 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 2, + }, + { + expectedOutput = [[ violator.luau:2:5-6 ── │ 2 │ for i, v in t do │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:2:8-9 ── │ 2 │ for i, v in t do │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_5", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 x = x + 1 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local x = 4 │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_6", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 local y = 5 y, x = y, x + 1 print(y) - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local x = 4 │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_7", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 x = x + 1 print(x) - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." - ) - - -- Teardown - fs.remove(violatorPath) + ]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 0, + }, + }, + }) end) suite:case("unused_variable_8", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 x += 1 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local x = 4 │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_9", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 x += 1 print(x) - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." - ) - - -- Teardown - fs.remove(violatorPath) + ]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 0, + }, + }, + }) end) suite:case("unused_variable_10", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 x += 1 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local x = 4 │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_11", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local function _(x) return nil end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:18-19 ── │ 1 │ local function _(x) │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_12", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 function x() return 5 end x() - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local x = 4 │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_13", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 local x = 5 print(x) - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local x = 4 │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_14", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 local function x() return 5 end x() - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local x = 4 │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_15", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 4 local x = 5 + x - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:2:7-8 ── │ 2 │ local x = 5 + x │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_16", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local lib = require("whatever") local _x : lib.num = 3 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." - ) - - -- Teardown - fs.remove(violatorPath) + ]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 0, + }, + }, + }) end) suite:case("unused_variable_17", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ for k, v in t do t1[k] = v end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." - ) - - -- Teardown - fs.remove(violatorPath) + ]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 0, + }, + }, + }) end) suite:case("unused_variable_18", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ return { add = function(a, b) return a + b end, } - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." - ) - - assert.strnotcontains(result.stdout, "Local variable not found in any scope") - - -- Teardown - fs.remove(violatorPath) + ]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 0, + }, + { expectedOutput = "Local variable not found in any scope", count = 0 }, + }, + }) end) suite:case("unused_variable_19", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local T = {} function T:new() self.hello = "world" end - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - assert.strnotcontains(result.stdout, "Local variable not found in any scope") - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { expectedOutput = "Local variable not found in any scope", count = 0 }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local T = {} │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_20", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local a, b = foo() a, b = foo() - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 2 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 2, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local a, b = foo() │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:10-11 ── │ 1 │ local a, b = foo() │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_21", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 3 local y = 1 y = 3, x print(y) - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", - nil, - 1 - ) - - local expected = [[ + ]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ violator.luau:1:7-8 ── │ 1 │ local x = 3 │ ^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("unused_variable_22", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ repeat local x = 1 until x == 1 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains(result.stdout, "Local variable not found in any scope") - assert.strnotcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." - ) - - -- Teardown - fs.remove(violatorPath) + ]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { expectedOutput = "Local variable not found in any scope", count = 0 }, + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 0, + }, + }, + }) end) suite:case("unused_variable_23", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [=[ + lintTestHelper(assert, { + content = [=[ for i = 0, num - 1 do t.table[lengths[off + i]] += 1 end - ]=] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." - ) - - -- Teardown - fs.remove(violatorPath) + ]=], + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 0, + }, + }, + }) end) suite:case("unused_variable_24", function(assert) - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [=[ + lintTestHelper(assert, { + content = [=[ function foo(t: { num: number }) t.num = 3 end - ]=] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.unused_variable, violatorPath }) - - -- Check - assert.eq(result.exitcode, 0) - - assert.strnotcontains( - result.stdout, - "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." - ) - - -- Teardown - fs.remove(violatorPath) + ]=], + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 0, + }, + }, + }) end) suite:case("throws error when config has invalid structure", function(assert) From c7d23fc3c10bc36f77f69996c00cac31bc95c415 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 12 Feb 2026 14:32:39 -0800 Subject: [PATCH 350/642] Introduces an explicit Batteries VFS to handle requires from @std libraries and cli commands. (#813) This is a refactor PR that splits out the Batteries specific behavior from the CliVFS --- lute/cli/include/lute/requiresetup.h | 6 +- lute/cli/src/climain.cpp | 6 +- lute/cli/src/requiresetup.cpp | 53 +++++++++++++-- lute/require/CMakeLists.txt | 2 + lute/require/include/lute/batteriesvfs.h | 24 +++++++ lute/require/include/lute/clivfs.h | 8 --- lute/require/include/lute/requirevfs.h | 3 + lute/require/src/batteriesvfs.cpp | 86 ++++++++++++++++++++++++ lute/require/src/clivfs.cpp | 60 ++--------------- lute/require/src/requirevfs.cpp | 50 ++++++++++++-- tests/src/cliruntimefixture.cpp | 2 +- 11 files changed, 218 insertions(+), 82 deletions(-) create mode 100644 lute/require/include/lute/batteriesvfs.h create mode 100644 lute/require/src/batteriesvfs.cpp diff --git a/lute/cli/include/lute/requiresetup.h b/lute/cli/include/lute/requiresetup.h index 735398515..a8e19a209 100644 --- a/lute/cli/include/lute/requiresetup.h +++ b/lute/cli/include/lute/requiresetup.h @@ -10,9 +10,11 @@ struct lua_State; struct Runtime; -lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit = nullptr); +lua_State* setupRunState(Runtime& runtime, std::function preSandboxInit = nullptr); -lua_State* setupPkgCliState( +lua_State* setupCliCommandState(Runtime& runtime, std::function preSandboxInit = nullptr); + +lua_State* setupPkgRunState( Runtime& runtime, std::vector directDependencies, std::vector> allDependencies diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index e0de94957..b892be876 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -414,11 +414,11 @@ int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness } auto [directDependencies, allDependencies] = getDependenciesFromLockfile(*lockfile); - L = setupPkgCliState(runtime, std::move(directDependencies), std::move(allDependencies)); + L = setupPkgRunState(runtime, std::move(directDependencies), std::move(allDependencies)); } else { - L = setupCliState(runtime); + L = setupRunState(runtime); } bool success = runFile(runtime, validPath.c_str(), L, program_argc, program_argv, reporter, profileOptions); @@ -627,7 +627,7 @@ int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& rep int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv, LuteReporter& reporter) { Runtime runtime; - lua_State* L = setupCliState(runtime); + lua_State* L = setupCliCommandState(runtime); std::string bytecode = Luau::compile(std::string(result.contents), copts()); return runBytecode(runtime, bytecode, "@" + result.path, L, program_argc, program_argv, reporter) ? 0 : 1; diff --git a/lute/cli/src/requiresetup.cpp b/lute/cli/src/requiresetup.cpp index 54e59283f..c1c486f99 100644 --- a/lute/cli/src/requiresetup.cpp +++ b/lute/cli/src/requiresetup.cpp @@ -17,7 +17,32 @@ #include #include -static void* createCliRequireContext(lua_State* L) +static void* createRunRequireContext(lua_State* L) +{ + void* ctx = lua_newuserdatadtor( + L, + sizeof(RequireCtx), + [](void* ptr) + { + std::destroy_at(static_cast(ptr)); + } + ); + + if (!ctx) + luaL_error(L, "unable to allocate RequireCtx"); + + ctx = new (ctx) RequireCtx{std::make_unique()}; + + // Store RequireCtx in the registry to keep it alive for the lifetime of + // this lua_State. Memory address is used as a key to avoid collisions. + lua_pushlightuserdata(L, ctx); + lua_insert(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + + return ctx; +} + +static void* createCliCommandRequireContext(lua_State* L) { void* ctx = lua_newuserdatadtor( L, @@ -42,7 +67,7 @@ static void* createCliRequireContext(lua_State* L) return ctx; } -static void* createPkgRequireContext( +static void* createPkgRunRequireContext( lua_State* L, std::vector directDependencies, std::vector> allDependencies @@ -100,7 +125,23 @@ static void* createBundleRequireContext( return ctx; } -lua_State* setupCliState(Runtime& runtime, std::function preSandboxInit) +lua_State* setupRunState(Runtime& runtime, std::function preSandboxInit) +{ + return setupState( + runtime, + [preSandboxInit = std::move(preSandboxInit)](lua_State* L) + { + if (Luau::CodeGen::isSupported()) + Luau::CodeGen::create(L); + + luaopen_require(L, requireConfigInit, createRunRequireContext(L)); + if (preSandboxInit) + preSandboxInit(L); + } + ); +} + +lua_State* setupCliCommandState(Runtime& runtime, std::function preSandboxInit) { return setupState( runtime, @@ -109,14 +150,14 @@ lua_State* setupCliState(Runtime& runtime, std::function preSa if (Luau::CodeGen::isSupported()) Luau::CodeGen::create(L); - luaopen_require(L, requireConfigInit, createCliRequireContext(L)); + luaopen_require(L, requireConfigInit, createCliCommandRequireContext(L)); if (preSandboxInit) preSandboxInit(L); } ); } -lua_State* setupPkgCliState( +lua_State* setupPkgRunState( Runtime& runtime, std::vector directDependencies, std::vector> allDependencies @@ -129,7 +170,7 @@ lua_State* setupPkgCliState( if (Luau::CodeGen::isSupported()) Luau::CodeGen::create(L); - luaopen_require(L, requireConfigInit, createPkgRequireContext(L, std::move(directDependencies), std::move(allDependencies))); + luaopen_require(L, requireConfigInit, createPkgRunRequireContext(L, std::move(directDependencies), std::move(allDependencies))); } ); } diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index 79ae54b71..44063f1ff 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(Lute.Require) target_sources(Lute.Require PRIVATE + include/lute/batteriesvfs.h include/lute/bundlevfs.h include/lute/clivfs.h include/lute/filevfs.h @@ -13,6 +14,7 @@ target_sources(Lute.Require PRIVATE include/lute/stdlibvfs.h include/lute/userlandvfs.h + src/batteriesvfs.cpp src/bundlevfs.cpp src/clivfs.cpp src/filevfs.cpp diff --git a/lute/require/include/lute/batteriesvfs.h b/lute/require/include/lute/batteriesvfs.h new file mode 100644 index 000000000..510a72f3f --- /dev/null +++ b/lute/require/include/lute/batteriesvfs.h @@ -0,0 +1,24 @@ +#pragma once + +#include "lute/modulepath.h" + +#include + +class BatteriesVfs +{ +public: + NavigationStatus resetToPath(const std::string& path); + + NavigationStatus toParent(); + NavigationStatus toChild(const std::string& path); + + bool isModulePresent() const; + std::string getIdentifier() const; + std::optional getContents(const std::string& path) const; + + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; + +private: + std::optional modulePath; +}; diff --git a/lute/require/include/lute/clivfs.h b/lute/require/include/lute/clivfs.h index b2a8d48b1..c1c33b20b 100644 --- a/lute/require/include/lute/clivfs.h +++ b/lute/require/include/lute/clivfs.h @@ -11,7 +11,6 @@ class CliVfs NavigationStatus toParent(); NavigationStatus toChild(const std::string& name); - NavigationStatus toAliasFallback(std::string_view aliasUnprefixed); bool isModulePresent() const; std::string getIdentifier() const; @@ -22,11 +21,4 @@ class CliVfs private: std::optional modulePath; - - enum class Tracking - { - Batteries, - CLI - }; - Tracking alias = Tracking::CLI; }; diff --git a/lute/require/include/lute/requirevfs.h b/lute/require/include/lute/requirevfs.h index 06292d9ee..2eeb142d4 100644 --- a/lute/require/include/lute/requirevfs.h +++ b/lute/require/include/lute/requirevfs.h @@ -1,5 +1,6 @@ #pragma once +#include "lute/batteriesvfs.h" #include "lute/bundlevfs.h" #include "lute/clivfs.h" #include "lute/filevfs.h" @@ -53,6 +54,7 @@ class RequireVfs : public IRequireVfs Cli, Bundle, Lute, + Batteries, // Only for internal use }; VFSType vfsType = VFSType::Disk; @@ -60,6 +62,7 @@ class RequireVfs : public IRequireVfs FileVfs fileVfs; StdLibVfs stdLibVfs; LuteVfs luteVfs; + BatteriesVfs batteriesVfs; std::optional cliVfs = std::nullopt; std::optional bundleVfs = std::nullopt; }; diff --git a/lute/require/src/batteriesvfs.cpp b/lute/require/src/batteriesvfs.cpp new file mode 100644 index 000000000..662e69c61 --- /dev/null +++ b/lute/require/src/batteriesvfs.cpp @@ -0,0 +1,86 @@ +#include "lute/batteriesvfs.h" + +#include "lute/clibatteries.h" +#include "lute/common.h" +#include "lute/modulepath.h" + +#include "Luau/Common.h" + +#include + +constexpr std::string_view kBatteriesAliasPrefix = "@batteries"; + +static bool isBatteryModule(const std::string& path) +{ + BatteryModuleResult result = getBatteryModule(path); + return result.type == BatteryModuleType::Module; +} + +static std::optional readBatteryModule(const std::string& path) +{ + BatteryModuleResult result = getBatteryModule(path); + if (result.type == BatteryModuleType::Module) + return std::string(result.contents); + + return std::nullopt; +} + +static bool isBatteryDirectory(const std::string& path) +{ + if (path == kBatteriesAliasPrefix) + return true; + + BatteryModuleResult result = getBatteryModule(path); + return result.type == BatteryModuleType::Directory; +} + +NavigationStatus BatteriesVfs::resetToPath(const std::string& path) +{ + std::string filePath = path == kBatteriesAliasPrefix ? "" : path.substr(kBatteriesAliasPrefix.size() + 1); + + if (path.rfind(kBatteriesAliasPrefix, 0) != 0) + return NavigationStatus::NotFound; + + modulePath = ModulePath::create(std::string(kBatteriesAliasPrefix), filePath, isBatteryModule, isBatteryDirectory); + return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; +} + +NavigationStatus BatteriesVfs::toParent() +{ + LUTE_ASSERT(modulePath); + return modulePath->toParent(); +} + +NavigationStatus BatteriesVfs::toChild(const std::string& name) +{ + LUTE_ASSERT(modulePath); + return modulePath->toChild(name); +} + +bool BatteriesVfs::isModulePresent() const +{ + return getBatteryModule(getIdentifier()).type == BatteryModuleType::Module; +} + +std::string BatteriesVfs::getIdentifier() const +{ + LUTE_ASSERT(modulePath); + ResolvedRealPath result = modulePath->getRealPath(); + LUTE_ASSERT(result.status == NavigationStatus::Success); + return result.realPath; +} + +std::optional BatteriesVfs::getContents(const std::string& path) const +{ + return readBatteryModule(path); +} + +ConfigStatus BatteriesVfs::getConfigStatus() const +{ + return ConfigStatus::Absent; +} + +std::optional BatteriesVfs::getConfig() const +{ + return std::nullopt; +} diff --git a/lute/require/src/clivfs.cpp b/lute/require/src/clivfs.cpp index 4052896ac..0b6ba2bff 100644 --- a/lute/require/src/clivfs.cpp +++ b/lute/require/src/clivfs.cpp @@ -1,42 +1,13 @@ #include "lute/clivfs.h" -#include "lute/clibatteries.h" #include "lute/clicommands.h" #include "lute/common.h" #include "lute/modulepath.h" -#include "Luau/Common.h" - #include -constexpr std::string_view kBatteriesAliasPrefix = "@batteries"; -constexpr std::string_view kBatteriesPrefix = "batteries"; - constexpr std::string_view kCliAliasPrefix = "@cli"; -static bool isBatteryModule(const std::string& path) -{ - BatteryModuleResult result = getBatteryModule(path); - return result.type == BatteryModuleType::Module; -} - -static std::optional readBatteryModule(const std::string& path) -{ - BatteryModuleResult result = getBatteryModule(path); - if (result.type == BatteryModuleType::Module) - return std::string(result.contents); - return std::nullopt; -} - -static bool isBatteryDirectory(const std::string& path) -{ - if (path == kBatteriesAliasPrefix) - return true; - - BatteryModuleResult result = getBatteryModule(path); - return result.type == BatteryModuleType::Directory; -} - static bool isCliModule(const std::string& path) { CliModuleResult result = getCliModule(path); @@ -63,32 +34,12 @@ static bool isCliDirectory(const std::string& path) NavigationStatus CliVfs::resetToPath(const std::string& path) { - alias = path.rfind(kBatteriesAliasPrefix, 0) == 0 ? Tracking::Batteries : Tracking::CLI; - - if (path == kBatteriesAliasPrefix) + if (path.rfind(kCliAliasPrefix, 0) == 0) { - modulePath = ModulePath::create(std::string(kBatteriesAliasPrefix), "", isBatteryModule, isBatteryDirectory); + std::string filePath = path == kCliAliasPrefix ? "" : path.substr(kCliAliasPrefix.size() + 1); + modulePath = ModulePath::create(std::string(kCliAliasPrefix), filePath, isCliModule, isCliDirectory); return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; } - else - { - auto modulePrefix = std::string(alias == Tracking::Batteries ? kBatteriesAliasPrefix : kCliAliasPrefix); - auto modFunc = alias == Tracking::Batteries ? isBatteryModule : isCliModule; - auto dirFunc = alias == Tracking::Batteries ? isBatteryDirectory : isCliDirectory; - if (path.rfind(modulePrefix, 0) == 0) - { - modulePath = ModulePath::create(modulePrefix, path.substr(modulePrefix.size() + 1), modFunc, dirFunc); - return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; - } - } - - return NavigationStatus::NotFound; -} - -NavigationStatus CliVfs::toAliasFallback(std::string_view aliasUnprefixed) -{ - if (aliasUnprefixed == kBatteriesPrefix) - return resetToPath(std::string(kBatteriesAliasPrefix)); return NavigationStatus::NotFound; } @@ -107,8 +58,7 @@ NavigationStatus CliVfs::toChild(const std::string& name) bool CliVfs::isModulePresent() const { - auto id = getIdentifier(); - return alias == Tracking::Batteries ? getBatteryModule(id).type == BatteryModuleType::Module : getCliModule(id).type == CliModuleType::Module; + return getCliModule(getIdentifier()).type == CliModuleType::Module; } std::string CliVfs::getIdentifier() const @@ -121,7 +71,7 @@ std::string CliVfs::getIdentifier() const std::optional CliVfs::getContents(const std::string& path) const { - return alias == Tracking::Batteries ? readBatteryModule(path) : readCliModule(path); + return readCliModule(path); } ConfigStatus CliVfs::getConfigStatus() const diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 17a5ff9dd..1dfb26586 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -10,12 +10,14 @@ #include "lualib.h" RequireVfs::RequireVfs(CliVfs cliVfs) - : cliVfs(std::move(cliVfs)) + : vfsType(VFSType::Cli) + , cliVfs(std::move(cliVfs)) { } RequireVfs::RequireVfs(BundleVfs bundleVfs) - : bundleVfs(std::move(bundleVfs)) + : vfsType(VFSType::Bundle) + , bundleVfs(std::move(bundleVfs)) { } @@ -51,6 +53,12 @@ NavigationStatus RequireVfs::reset(lua_State* L, std::string_view requirerChunkn return bundleVfs->resetToPath(std::string(requirerChunkname.substr(1))); } + if ((requirerChunkname.size() >= 12 && requirerChunkname.substr(0, 12) == "@@batteries/")) + { + vfsType = VFSType::Batteries; + return batteriesVfs.resetToPath(std::string(requirerChunkname.substr(1))); + } + vfsType = VFSType::Disk; if (requirerChunkname == "=stdin") return fileVfs.resetToStdIn(); @@ -83,6 +91,9 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) LUTE_ASSERT(bundleVfs); status = bundleVfs->resetToPath(std::string(path)); break; + case VFSType::Batteries: + status = batteriesVfs.resetToPath(std::string(path)); + break; } return status; } @@ -99,17 +110,17 @@ NavigationStatus RequireVfs::toAliasOverride(lua_State* L, std::string_view alia vfsType = VFSType::Lute; return luteVfs.resetToPath("@lute"); } + else if (aliasUnprefixed == "batteries" && (vfsType == VFSType::Cli || vfsType == VFSType::Std || vfsType == VFSType::Batteries)) + { + vfsType = VFSType::Batteries; + return batteriesVfs.resetToPath("@batteries"); + } return NavigationStatus::NotFound; } NavigationStatus RequireVfs::toAliasFallback(lua_State* L, std::string_view aliasUnprefixed) { - if (vfsType == VFSType::Cli) - { - LUTE_ASSERT(cliVfs); - return cliVfs->toAliasFallback(aliasUnprefixed); - } return NavigationStatus::NotFound; } @@ -135,6 +146,9 @@ NavigationStatus RequireVfs::toParent(lua_State* L) LUTE_ASSERT(bundleVfs); status = bundleVfs->toParent(); break; + case VFSType::Batteries: + status = batteriesVfs.toParent(); + break; } return status; } @@ -155,6 +169,8 @@ NavigationStatus RequireVfs::toChild(lua_State* L, std::string_view name) case VFSType::Bundle: LUTE_ASSERT(bundleVfs); return bundleVfs->toChild(std::string(name)); + case VFSType::Batteries: + return batteriesVfs.toChild(std::string(name)); } return NavigationStatus::NotFound; } @@ -176,6 +192,8 @@ bool RequireVfs::isModulePresent(lua_State* L) const case VFSType::Bundle: LUTE_ASSERT(bundleVfs); return bundleVfs->isModulePresent(); + case VFSType::Batteries: + return batteriesVfs.isModulePresent(); } return false; @@ -204,6 +222,9 @@ std::string RequireVfs::getContents(lua_State* L, const std::string& loadname) c LUTE_ASSERT(bundleVfs); contents = bundleVfs->getContents(loadname); break; + case VFSType::Batteries: + contents = batteriesVfs.getContents(loadname); + break; } return contents ? *contents : ""; @@ -231,6 +252,9 @@ std::string RequireVfs::getChunkname(lua_State* L) const LUTE_ASSERT(bundleVfs); chunkname = "@" + bundleVfs->getIdentifier(); break; + case VFSType::Batteries: + chunkname = "@" + batteriesVfs.getIdentifier(); + break; } return chunkname; } @@ -257,6 +281,9 @@ std::string RequireVfs::getLoadname(lua_State* L) const LUTE_ASSERT(bundleVfs); loadname = bundleVfs->getIdentifier(); break; + case VFSType::Batteries: + loadname = batteriesVfs.getIdentifier(); + break; } return loadname; } @@ -283,6 +310,9 @@ std::string RequireVfs::getCacheKey(lua_State* L) const LUTE_ASSERT(bundleVfs); cacheKey = bundleVfs->getIdentifier(); break; + case VFSType::Batteries: + cacheKey = batteriesVfs.getIdentifier(); + break; } return cacheKey; } @@ -309,6 +339,9 @@ ConfigStatus RequireVfs::getConfigStatus(lua_State* L) const LUTE_ASSERT(bundleVfs); status = bundleVfs->getConfigStatus(); break; + case VFSType::Batteries: + status = batteriesVfs.getConfigStatus(); + break; } return status; } @@ -335,6 +368,9 @@ std::string RequireVfs::getConfig(lua_State* L) const LUTE_ASSERT(bundleVfs); configContents = bundleVfs->getConfig(); break; + case VFSType::Batteries: + configContents = batteriesVfs.getConfig(); + break; } return configContents ? *configContents : ""; } diff --git a/tests/src/cliruntimefixture.cpp b/tests/src/cliruntimefixture.cpp index 70b21caf2..4cfec59e1 100644 --- a/tests/src/cliruntimefixture.cpp +++ b/tests/src/cliruntimefixture.cpp @@ -20,7 +20,7 @@ static int report(lua_State* L) CliRuntimeFixture::CliRuntimeFixture() : runtime(std::make_unique()) { - L = setupCliState( + L = setupRunState( *runtime, [rep = reporter.get()](lua_State* L) { From 97b8e2f81694a353e34f0302d28dd293f7aa48b1 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:25:50 -0800 Subject: [PATCH 351/642] Improves fs.watch implementation by using deque (#811) Updates implementation of `fs.watch` with the `deque` from batteries instead of `table.remove(_, 1)`. This still doesn't help with resolvig the perf issues we've been having with `fs.watch` but using the deque is still better than removing from the front of a table --- lute/std/libs/fs.luau | 9 +++++---- tools/check-faillist.txt | 14 ++++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 985448307..c97daf524 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -3,6 +3,7 @@ -- stdlib for `@lute/fs` -- Provides file system utility functions that handles file paths as path types instead of strings +local deque = require("@batteries/collections/deque") local fs = require("@lute/fs") local pathlib = require("@std/path") local sys = require("@std/system") @@ -77,17 +78,17 @@ end --- Also provides a `close` method to stop watching. --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.watch as `for _ in fs.watch(...) do` directly. A while loop can be used instead. See example/watch_directory.luau for usage. function fslib.watch(path: pathlike): watcher - local queue: { { filename: pathlib.path, event: watchevent } } = {} + local deq = deque.new() local handle = fs.watch(pathlib.format(path), function(filename: string, event: watchevent) - table.insert(queue, { filename = pathlib.parse(filename), event = event }) + deq:pushback({ filename = pathlib.parse(filename), event = event }) end) return { next = function(_self: watcher): watchevent? - if #queue == 0 then + if not deq:peekfront() then return nil end - local item = table.remove(queue, 1) + local item = deq:popfront() assert(item) return item.event end, diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 377259242..31c787cc6 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -106,12 +106,14 @@ lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:75:9-14 lute/cli/commands/transform/lib/arguments.luau:75:9-14 -lute/std/libs/fs.luau:136:34-39 -lute/std/libs/fs.luau:136:34-39 -lute/std/libs/fs.luau:185:52-68 -lute/std/libs/fs.luau:185:52-68 -lute/std/libs/fs.luau:193:11-17 -lute/std/libs/fs.luau:193:11-17 +lute/std/libs/fs.luau:93:11-20 +lute/std/libs/fs.luau:93:11-20 +lute/std/libs/fs.luau:137:34-39 +lute/std/libs/fs.luau:137:34-39 +lute/std/libs/fs.luau:186:52-68 +lute/std/libs/fs.luau:186:52-68 +lute/std/libs/fs.luau:194:11-17 +lute/std/libs/fs.luau:194:11-17 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:42:25-29 From 1c69fb62d95ddc8c9a69959b10d5b5c1b78fe17f Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 13 Feb 2026 10:26:10 -0800 Subject: [PATCH 352/642] Adds duplicate keys lint rule (#818) Matches the following Selene rule: https://kampfkarren.github.io/selene/lints/duplicate_keys.html#duplicate_keys --- docs/cli/lint/duplicate_keys.md | 22 +++ docs/cli/lint/unused_variable.md | 2 +- lute/cli/commands/lint/init.luau | 10 +- lute/cli/commands/lint/rules/README.md | 9 +- .../commands/lint/rules/duplicate_keys.luau | 68 ++++++++ tests/cli/lint.test.luau | 153 ++++++++++++++++++ tools/check-faillist.txt | 20 +-- 7 files changed, 269 insertions(+), 15 deletions(-) create mode 100644 docs/cli/lint/duplicate_keys.md create mode 100644 lute/cli/commands/lint/rules/duplicate_keys.luau diff --git a/docs/cli/lint/duplicate_keys.md b/docs/cli/lint/duplicate_keys.md new file mode 100644 index 000000000..6ffdbd1ac --- /dev/null +++ b/docs/cli/lint/duplicate_keys.md @@ -0,0 +1,22 @@ +# duplicate_keys + +This lint rule checks for uses of duplicate keys in table literals. +This includes map-like entries that may collide with array-like entries. + +## Why this is discouraged + +This pattern is most likely the result of a mistake, and the final table value will only use one of the values assigned to a duplicate key. + +## Example violations + +`duplicate_keys` will warn on the following: + +```luau +local t = { + key = 1, + ["key"] = 2, + "array-like-value1", + "array-like-value2", + [2] = "map-like-value" +} +``` diff --git a/docs/cli/lint/unused_variable.md b/docs/cli/lint/unused_variable.md index b34fe2481..fd0efc0cd 100644 --- a/docs/cli/lint/unused_variable.md +++ b/docs/cli/lint/unused_variable.md @@ -10,7 +10,7 @@ Unused variables can result in less readable code in the best case, and worse pe ## Example violations -`constant_table_comparison` will warn on the following: +`unused_variable` will warn on the following: ```luau local function _(x) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index fc78ddd24..7527a6cc5 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -15,8 +15,14 @@ local visitor = require("@std/syntax/visitor") local configUtils = require("@self/configutils") local parseIgnores = require("./lib/parseIgnores") -local DEFAULT_RULES = - { "almost_swapped", "constant_table_comparison", "divide_by_zero", "parenthesized_conditions", "unused_variable" } +local DEFAULT_RULES = { + "almost_swapped", + "constant_table_comparison", + "divide_by_zero", + "duplicate_keys", + "parenthesized_conditions", + "unused_variable", +} local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" local VERBOSE = false diff --git a/lute/cli/commands/lint/rules/README.md b/lute/cli/commands/lint/rules/README.md index c45432202..e936388ab 100644 --- a/lute/cli/commands/lint/rules/README.md +++ b/lute/cli/commands/lint/rules/README.md @@ -1,3 +1,8 @@ -To add a new default lint rule to `lute lint`, create a module or luau file defining the rule in this folder. -Then, add the name of the rule to the DEFAULT_RULES constant in `lint/init.luau`, add a test for the rule in `tests/cli/lint.test.luau`, and add documentation for it in `docs/cli/lint`. +To add a new default lint rule to `lute lint`: +1. Create a module or luau file defining the rule in this folder. +Each violation should report its target as `"https://lute.luau.org/cli/lint/.html"`. +1. Add the name of the rule to the `DEFAULT_RULES` constant in `lint/init.luau` +1. Add a test for the rule in `tests/cli/lint.test.luau` +1. Add documentation for it in `docs/cli/lint`. The doc's title should be the same as your rule's name. + The expected type of a lint rule is specified in `lint/types.luau`. \ No newline at end of file diff --git a/lute/cli/commands/lint/rules/duplicate_keys.luau b/lute/cli/commands/lint/rules/duplicate_keys.luau new file mode 100644 index 000000000..17cfe61e5 --- /dev/null +++ b/lute/cli/commands/lint/rules/duplicate_keys.luau @@ -0,0 +1,68 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "duplicate_keys" +local message = "Duplicate keys detected in table literal." +local target = "https://lute.luau.org/cli/lint/duplicate_keys.html" + +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } + local function makeViolation(location: syntax.span): lintTypes.LintViolation + return { + lintname = name, + location = location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } + end + + return query + .findallfromroot(ast, utils.isExprTable) + :flatmap(function(tableExpr: syntax.AstExprTable): { lintTypes.LintViolation } + local violations = {} + + local seenKeys: { [string | number]: true } = {} + local numArrayElems = 0 + + for _, entry in tableExpr.entries do + if entry.kind == "list" then + numArrayElems += 1 + if seenKeys[numArrayElems] then + table.insert(violations, makeViolation(entry.location)) + end + elseif entry.kind == "record" then + if seenKeys[entry.key.text] then + table.insert(violations, makeViolation(entry.location)) + end + seenKeys[entry.key.text] = true + elseif entry.kind == "general" then + if entry.key.tag == "string" then + if seenKeys[entry.key.text] then + table.insert(violations, makeViolation(entry.location)) + end + seenKeys[entry.key.text] = true + elseif entry.key.tag == "number" then + if seenKeys[entry.key.value] then + table.insert(violations, makeViolation(entry.location)) + elseif numArrayElems > 0 and entry.key.value <= numArrayElems then + table.insert(violations, makeViolation(entry.location)) + end + seenKeys[entry.key.value] = true + end + end + end + + return violations + end).nodes +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 15b2d2e5f..cb9c0c8d1 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -14,6 +14,7 @@ local defaultRulesPaths = { almost_swapped = path.format(path.join(defaultRulesFolder, "almost_swapped.luau")), constant_table_comparison = path.format(path.join(defaultRulesFolder, "constant_table_comparison.luau")), divide_by_zero = path.format(path.join(defaultRulesFolder, "divide_by_zero.luau")), + duplicate_keys = path.format(path.join(defaultRulesFolder, "duplicate_keys.luau")), parenthesized_conditions = path.format(path.join(defaultRulesFolder, "parenthesized_conditions.luau")), unused_variable = path.format(path.join(defaultRulesFolder, "unused_variable.luau")), } @@ -22,6 +23,7 @@ local LINT_RULE_MESSAGES = { almost_swapped = "warning[almost_swapped]: This looks like a failed attempt to swap.", constant_table_comparison = "warning[constant_table_comparison]: Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false.", divide_by_zero = "warning[divide_by_zero]: Division by zero detected.", + duplicate_keys = "warning[duplicate_keys]: Duplicate keys detected in table literal.", parenthesized_conditions = "info[parenthesized_conditions]: Luau doesn't require parentheses around conditions. You can remove them for readability.", unused_variable = "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", } @@ -2307,4 +2309,155 @@ end recursive = true, }) end) + + suite:case("duplicate_keys list duplicates", function(assert) + lintTestHelper(assert, { + content = [[ +local t = { 1, 2, [2] = 3 } +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.duplicate_keys, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, + { + expectedOutput = [[ +violator.luau:1:19-26 ── + │ + 1 │ local t = { 1, 2, [2] = 3 } + │ ^^^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("duplicate_keys record duplicates", function(assert) + lintTestHelper(assert, { + content = [[ +local t = { a = 1, a = 2 } +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.duplicate_keys, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, + { + expectedOutput = [[ +violator.luau:1:20-25 ── + │ + 1 │ local t = { a = 1, a = 2 } + │ ^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("duplicate_keys general string duplicates", function(assert) + lintTestHelper(assert, { + content = [[ +local t = { ["a"] = 1, ["a"] = 2 } +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.duplicate_keys, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, + { + expectedOutput = [[ +violator.luau:1:24-33 ── + │ + 1 │ local t = { ["a"] = 1, ["a"] = 2 } + │ ^^^^^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("duplicate_keys general number duplicates", function(assert) + lintTestHelper(assert, { + content = [[ +local t = { [1] = 1, [1] = 2 } +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.duplicate_keys, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, + { + expectedOutput = [[ +violator.luau:1:22-29 ── + │ + 1 │ local t = { [1] = 1, [1] = 2 } + │ ^^^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("duplicate_keys general number clashes with array part", function(assert) + lintTestHelper(assert, { + content = [[ +local t = { 1, 2, [2] = 3 } +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.duplicate_keys, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, + { + expectedOutput = [[ +violator.luau:1:19-26 ── + │ + 1 │ local t = { 1, 2, [2] = 3 } + │ ^^^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("duplicate_keys general string and record entry duplicates", function(assert) + lintTestHelper(assert, { + content = [[ +local t = { ["a"] = 1, a = 2 } +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.duplicate_keys, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, + { + expectedOutput = [[ +violator.luau:1:24-29 ── + │ + 1 │ local t = { ["a"] = 1, a = 2 } + │ ^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("duplicate_keys doesn't warn on general 0", function(assert) + lintTestHelper(assert, { + content = [[ +local t = { [0] = 1 } +]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.duplicate_keys, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 0 }, + }, + }) + end) end) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 31c787cc6..462fc478f 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -63,16 +63,16 @@ lute/cli/commands/doc/init.luau:315:5-16 lute/cli/commands/doc/init.luau:321:10-24 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 -lute/cli/commands/lint/init.luau:56:10-18 -lute/cli/commands/lint/init.luau:428:15-24 -lute/cli/commands/lint/init.luau:429:56-65 -lute/cli/commands/lint/init.luau:429:56-65 -lute/cli/commands/lint/init.luau:429:56-65 -lute/cli/commands/lint/init.luau:429:56-65 -lute/cli/commands/lint/init.luau:429:56-65 -lute/cli/commands/lint/init.luau:429:56-65 -lute/cli/commands/lint/init.luau:483:37-46 -lute/cli/commands/lint/init.luau:484:40-49 +lute/cli/commands/lint/init.luau:62:10-18 +lute/cli/commands/lint/init.luau:434:15-24 +lute/cli/commands/lint/init.luau:435:56-65 +lute/cli/commands/lint/init.luau:435:56-65 +lute/cli/commands/lint/init.luau:435:56-65 +lute/cli/commands/lint/init.luau:435:56-65 +lute/cli/commands/lint/init.luau:435:56-65 +lute/cli/commands/lint/init.luau:435:56-65 +lute/cli/commands/lint/init.luau:489:37-46 +lute/cli/commands/lint/init.luau:490:40-49 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 From 8bb1058355a6a56b4b33dbf97d06203df9059c3f Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:04:55 -0500 Subject: [PATCH 353/642] Lint Config: Support Passing `Context` thru to Rules (#808) This PR implements two next steps for lute lint's configuration system: rule context and rule-specific ignores. `Context` is a table of `globals` and rule specific `options`, as configured by the user. For example, given a configuration of: ``` { ..., globals = { myGlobal = 1 }, rules = { myRule = { options = { myOption = 1 } } } } ``` we will the following context to each call of `myRule`: ``` { globals = { myGlobal = 1 }, options = { myOption = 1 } } ``` This enables rule authors to write logic w/ configurability in mind, and extend their analyses to use-cases beyond the default. Rule-specific ignores extend the global-lint ignores (implemented in this PR), but are evaluated per-rule. This enables more granular ignore behavior, where we can effectively turn off rules for certain files / paths. --- lute/cli/commands/lint/init.luau | 103 ++++++--- .../commands/lint/rules/almost_swapped.luau | 6 +- .../lint/rules/constant_table_comparison.luau | 6 +- .../commands/lint/rules/divide_by_zero.luau | 6 +- .../commands/lint/rules/duplicate_keys.luau | 6 +- .../lint/rules/parenthesized_conditions.luau | 6 +- .../commands/lint/rules/unused_variable.luau | 6 +- lute/cli/commands/lint/types.luau | 44 ++-- tests/cli/lint.test.luau | 217 ++++++++++++++++-- tools/check-faillist.txt | 27 +-- 10 files changed, 352 insertions(+), 75 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 7527a6cc5..77a48784c 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -71,10 +71,32 @@ local function loadLintRule(path: string): types.LintRule return loaded -- Typecast isn't needed because loaded is refined to any bc of type error in assertIsLintRule end -local function loadLintRules(path: string): { types.LintRule } +local function registerRule( + rule: types.LintRule, + config: types.LintConfig, + rootLocation: string, + rules: { types.LintRule }, + ruleConfigs: types.RuleConfigurations, + ruleIgnores: types.RuleIgnores +) + table.insert(rules, rule) + local ruleConfig: types.RuleConfig = if config.rules ~= nil and config.rules[rule.name] ~= nil + then config.rules[rule.name] + else {} + ruleConfigs[rule.name] = ruleConfig + if ruleConfig.ignores ~= nil then + ruleIgnores[rule.name] = parseIgnores.parseIgnoreContents(rootLocation, ruleConfig.ignores) + end +end + +local function loadLintRules( + path: string, + config: types.LintConfig, + rootLocation: string +): ({ types.LintRule }, types.RuleConfigurations, types.RuleIgnores) assert(fs.exists(path), `Lint rule at path '{path}' does not exist`) - local rules = {} + local rules, ruleConfigs: types.RuleConfigurations, ruleIgnores = {}, {}, {} local queue: { string } = { path } while #queue > 0 do @@ -85,7 +107,7 @@ local function loadLintRules(path: string): { types.LintRule } local currentPathObj = pathLib.parse(currentPath) local initPath = pathLib.format(pathLib.join(currentPathObj, "init.luau")) if fs.exists(initPath) then - table.insert(rules, loadLintRule(initPath)) + registerRule(loadLintRule(initPath), config, rootLocation, rules, ruleConfigs, ruleIgnores) else local entries = fs.listdirectory(currentPath) local childPaths = tableext.map(entries, function(entry) @@ -94,22 +116,25 @@ local function loadLintRules(path: string): { types.LintRule } tableext.extend(queue, childPaths) end elseif pathLib.extname(currentPath) == ".luau" then - table.insert(rules, loadLintRule(currentPath)) + registerRule(loadLintRule(currentPath), config, rootLocation, rules, ruleConfigs, ruleIgnores) end end - return rules + return rules, ruleConfigs, ruleIgnores end -local function loadDefaultRules(): { types.LintRule } - local rules = {} +local function loadDefaultRules( + config: types.LintConfig, + rootLocation: string +): ({ types.LintRule }, types.RuleConfigurations, types.RuleIgnores) + local rules, ruleConfigs, ruleIgnores = {}, {}, {} for _, ruleName in DEFAULT_RULES do local path = `@self/rules/{ruleName}` local rule: types.LintRule = require(path) :: any -- Anycast to silence error from dynamic require assertIsLintRule(rule, path) - table.insert(rules, rule) + registerRule(rule, config, rootLocation, rules, ruleConfigs, ruleIgnores) end - return rules + return rules, ruleConfigs, ruleIgnores end local function parseDirectives(node: syntax.AstNode): { [string]: boolean } @@ -216,7 +241,8 @@ end local function lintString( source: string, lintRules: { types.LintRule }, - inputFilePath: pathLib.path? + inputFilePath: pathLib.path?, + configData: types.RuleConfigData ): { types.LintViolation } if VERBOSE then print(`Parsing input {if inputFilePath then `file '{inputFilePath}'` else "string"}`) @@ -234,7 +260,21 @@ local function lintString( local violations: { types.LintViolation } = {} local directiveCache = {} for _, rule in lintRules do - local success, err = pcall(rule.lint, ast, inputFilePath) + if inputFilePath then + -- check if rule-specific ignores omit the current path + local ruleIgnores = configData.ruleIgnores[rule.name] + if ruleIgnores ~= nil and parseIgnores.isIgnored(ruleIgnores, pathLib.format(inputFilePath), false) then + continue + end + end + + local ruleConfig = configData.ruleConfigs[rule.name] + local context = { + globals = configData.globals, + options = if ruleConfig.options then ruleConfig.options else {}, + } + + local success, err = pcall(rule.lint, ast, inputFilePath, context) if success then -- On success, err contains the returned violations for _, violation in err do @@ -305,14 +345,15 @@ end local function lintFile( inputFilePath: pathLib.path, lintRules: { types.LintRule }, - autofixEnabled: boolean + autofixEnabled: boolean, + configData: types.RuleConfigData ): { types.LintViolation } if VERBOSE then print(`Reading input file '{inputFilePath}'`) end local fileContent = fs.readfiletostring(inputFilePath) - local violations = lintString(fileContent, lintRules, inputFilePath) + local violations = lintString(fileContent, lintRules, inputFilePath, configData) if not autofixEnabled then return violations @@ -342,14 +383,14 @@ local function lintFile( fs.writestringtofile(inputFilePath, newContent) - return lintString(newContent, lintRules, inputFilePath) + return lintString(newContent, lintRules, inputFilePath, configData) end local function lintPaths( inputFilePaths: { string }, lintRules: { types.LintRule }, autofixEnabled: boolean, - _lintConfig: types.LintConfig, -- will be used in follow-up PRs + configData: types.RuleConfigData, ignoreData: parseIgnores.GitignoreData ): { [pathLib.path]: { types.LintViolation } } if VERBOSE then @@ -374,7 +415,7 @@ local function lintPaths( end end - local success, err = pcall(lintFile, curr, lintRules, autofixEnabled) + local success, err = pcall(lintFile, curr, lintRules, autofixEnabled, configData) if success then -- Violations are returned as the second return value from pcall allViolations[curr] = err @@ -441,25 +482,40 @@ local function main(...: string) end local lintRules: { types.LintRule } + local ruleConfigurations: types.RuleConfigurations = {} + local ruleIgnoreData: types.RuleIgnores = {} + + local ignores = if lintConfig.ignores ~= nil then lintConfig.ignores else {} + local rootLocation = if fs.exists(configPath) + then pathLib.format(pathLib.dirname(configPath)) + else pathLib.format(process.cwd()) + local ignoreData = parseIgnores.parseIgnoreContents(rootLocation, ignores) + local rulePath = args:get("rules") if rulePath == nil then if VERBOSE then print("Using default lint rules.") end - lintRules = loadDefaultRules() + lintRules, ruleConfigurations, ruleIgnoreData = loadDefaultRules(lintConfig, rootLocation) else if VERBOSE then print(`Loading lint rule(s) from '{rulePath}'`) end - lintRules = loadLintRules(rulePath) + lintRules, ruleConfigurations, ruleIgnoreData = loadLintRules(rulePath, lintConfig, rootLocation) end + local configContextData = { + globals = if lintConfig.globals ~= nil then lintConfig.globals else {}, + ruleConfigs = ruleConfigurations, + ruleIgnores = ruleIgnoreData, + } + if stringInput ~= nil then if args:has("auto-fix") then print("Auto-fix has no effect on string input.") end - local violations = lintString(stringInput, lintRules) + local violations = lintString(stringInput, lintRules, nil, configContextData) if args:has("json") then local diagnostics = tableext.map(violations, lsp.diagnostic) @@ -485,13 +541,8 @@ local function main(...: string) print("Auto-fix is enabled.") end - local ignores = if lintConfig.ignores ~= nil then lintConfig.ignores else {} - local rootLocation = if fs.exists(configPath) - then pathLib.format(pathLib.dirname(configPath)) - else pathLib.format(process.cwd()) - local ignoreData = parseIgnores.parseIgnoreContents(rootLocation, ignores) - - local allViolations = lintPaths(inputFiles :: { string }, lintRules, autofixEnabled, lintConfig, ignoreData) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed + local allViolations = + lintPaths(inputFiles :: { string }, lintRules, autofixEnabled, configContextData, ignoreData) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed if args:has("json") then print(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.object, true)) diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index c2aab4c62..b5bc9bef4 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -60,7 +60,11 @@ end -- Report instances of attempted swaps like: -- a = b; b = a -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } +local function lint( + ast: syntax.AstStatBlock, + sourcepath: path.path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } local violations = {} local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes diff --git a/lute/cli/commands/lint/rules/constant_table_comparison.luau b/lute/cli/commands/lint/rules/constant_table_comparison.luau index 5c579a23c..9d85519a3 100644 --- a/lute/cli/commands/lint/rules/constant_table_comparison.luau +++ b/lute/cli/commands/lint/rules/constant_table_comparison.luau @@ -29,7 +29,11 @@ local function isEmptyTableLiteral(expr: syntax.AstExpr): boolean end end -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } +local function lint( + ast: syntax.AstStatBlock, + sourcepath: path.path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } return query .findallfromroot(ast, utils.isExprBinary) :filter(function(bin) diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau index ab44ed9c2..bcf705c44 100644 --- a/lute/cli/commands/lint/rules/divide_by_zero.luau +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -12,7 +12,11 @@ local function isZeroLiteral(expr: syntax.AstExpr): boolean return expr.kind == "expr" and expr.tag == "number" and expr.value == 0 end -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } +local function lint( + ast: syntax.AstStatBlock, + sourcepath: path.path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } return query .findallfromroot(ast, utils.isExprBinary) :filter(function(bin) diff --git a/lute/cli/commands/lint/rules/duplicate_keys.luau b/lute/cli/commands/lint/rules/duplicate_keys.luau index 17cfe61e5..6f875909c 100644 --- a/lute/cli/commands/lint/rules/duplicate_keys.luau +++ b/lute/cli/commands/lint/rules/duplicate_keys.luau @@ -8,7 +8,11 @@ local name = "duplicate_keys" local message = "Duplicate keys detected in table literal." local target = "https://lute.luau.org/cli/lint/duplicate_keys.html" -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } +local function lint( + ast: syntax.AstStatBlock, + sourcepath: path.path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } local function makeViolation(location: syntax.span): lintTypes.LintViolation return { lintname = name, diff --git a/lute/cli/commands/lint/rules/parenthesized_conditions.luau b/lute/cli/commands/lint/rules/parenthesized_conditions.luau index 746df9137..2ea279e41 100644 --- a/lute/cli/commands/lint/rules/parenthesized_conditions.luau +++ b/lute/cli/commands/lint/rules/parenthesized_conditions.luau @@ -8,7 +8,11 @@ local name = "parenthesized_conditions" local message = "Luau doesn't require parentheses around conditions. You can remove them for readability." local target = "https://lute.luau.org/cli/lint/parenthesized_conditions.html" -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } +local function lint( + ast: syntax.AstStatBlock, + sourcepath: path.path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } local violations: { lintTypes.LintViolation } = {} query diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index 1babbfd5d..2b785b48e 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -298,7 +298,11 @@ local function unusedLocals(block: syntax.AstStatBlock): { syntax.AstLocal } return visitor.unusedLocals end -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } +local function lint( + ast: syntax.AstStatBlock, + sourcepath: path.path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } return tableext.map(unusedLocals(ast), function(l): lintTypes.LintViolation return table.freeze({ lintname = name, diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index f88312c57..a8416a6a2 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -1,4 +1,5 @@ local syntax = require("@std/syntax") +local parseIgnores = require("../lib/parseIgnores") local path = require("@std/path") export type severity = "error" | "warning" | "info" | "hint" @@ -19,31 +20,42 @@ export type LintViolation = { } export type LintRule = { - read lint: (ast: syntax.AstStatBlock, sourcepath: path.path?) -> { LintViolation }, + read lint: (ast: syntax.AstStatBlock, sourcepath: path.path?, context: RuleContext) -> { LintViolation }, read name: string, } --- export type RuleConfig = { --- ignores: { string }?, --- severity: severity?, --- off: true?, --- options: unknown?, --- } +export type RuleConfig = { + ignores: { string }?, + -- severity: severity?, + -- off: true?, + options: RuleOptions?, +} export type GlobalsConfig = { [string]: unknown } +export type RuleOptions = { [string]: unknown } export type LintConfig = { ignores: { path.pathlike }?, -- globs/paths to ignore as we walk lintee files - -- globals: GlobalsConfig?, -- globals to pass down to all rules via context - -- rules: { - -- paths: { path.pathlike }?, -- paths to local rules to use - -- [string]: RuleConfig, - -- }?, + globals: GlobalsConfig?, -- globals to pass down to all rules via context + rules: { + -- paths: { path.pathlike }?, -- paths to local rules to use + [string]: RuleConfig?, + }?, +} + +export type RuleContext = { + globals: GlobalsConfig, + options: RuleOptions, } --- export type RuleContext = { --- globals: GlobalsConfig, --- options: unknown, --- } +export type RuleConfigurations = { [string]: RuleConfig } + +export type RuleIgnores = { [string]: parseIgnores.GitignoreData } + +export type RuleConfigData = { + globals: GlobalsConfig, + ruleConfigs: RuleConfigurations, + ruleIgnores: RuleIgnores, +} return {} diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index cb9c0c8d1..361242dde 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -2198,7 +2198,7 @@ end end) suite:case("lute lint accounts for configured ignores (config passed as arg)", function(assert) - local testSubDir = path.join(tmpDir, "testDir") + local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) local ignoredFile = path.join(testSubDir, "ignored.luau") @@ -2227,10 +2227,6 @@ end assert.eq(result.exitcode, 1) -- errors but ONLY from valid.luau assert.strnotcontains(result.stdout, "ignored.luau") assert.strcontains(result.stdout, "valid.luau") - - fs.removedirectory(testSubDir, { - recursive = true, - }) end) suite:case("lute lint accounts for configured ignores (config auto-detected)", function(assert) @@ -2264,14 +2260,10 @@ end assert.eq(result.exitcode, 0) -- no errors assert.strnotcontains(result.stdout, "almost_swapped") assert.strnotcontains(result.stdout, "divide_by_zero") - - fs.removedirectory(testSubDir, { - recursive = true, - }) end) suite:case("lute lint configured ignores supplement (NOT override) existing gitignores", function(assert) - local testSubDir = path.join(tmpDir, "testDir") + local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) local ignoredFile = path.join(testSubDir, "ignored.luau") @@ -2304,10 +2296,6 @@ end assert.eq(result.exitcode, 0) -- no errors bc we ignore BOTH files in the dir assert.strnotcontains(result.stdout, "almost_swapped") assert.strnotcontains(result.stdout, "divide_by_zero") - - fs.removedirectory(testSubDir, { - recursive = true, - }) end) suite:case("duplicate_keys list duplicates", function(assert) @@ -2460,4 +2448,205 @@ local t = { [0] = 1 } }, }) end) + + suite:case("lute lint supports configured directory ignores", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + -- Create a nested directory structure with violating files + local nestedDir = path.join(testSubDir, "shouldBeIgnored") + fs.createdirectory(nestedDir) + + local file1 = path.join(nestedDir, "file1.luau") + local file2 = path.join(nestedDir, "file2.luau") + local file3 = path.join(testSubDir, "notIgnored.luau") + + local violatingContents = [[ + a = b + b = a + c = 1 / 0 + ]] + fs.writestringtofile(file1, violatingContents) + fs.writestringtofile(file2, violatingContents) + fs.writestringtofile(file3, violatingContents) + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ + return { + lute = { + lint = { + ignores = { "**/shouldBeIgnored/**" } + } + } + } + ]] + fs.writestringtofile(configFile, configContents) + + local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) + assert.eq(result.exitcode, 1) -- errors but ONLY from notIgnored.luau + + -- Should NOT contain violations from the ignored directory + assert.strnotcontains(result.stdout, "file1.luau") + assert.strnotcontains(result.stdout, "file2.luau") + + -- Should contain violations from the non-ignored file + assert.strcontains(result.stdout, "notIgnored.luau") + assert.strcontains(result.stdout, "divide_by_zero") + assert.strcontains(result.stdout, "almost_swapped") + end) + + suite:case("lute lint passes context through to rules", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + -- custom rule that returns violating with globals and options in the message + local customRuleFile = path.join(testSubDir, "testRule.luau") + local customRuleContents = [[ + return { + name = "testRule", + lint = function(ast, sourcepath, context) + return { + { + lintname = "testRule", + location = ast.location, + severity = "warn", + message = `Globals: {context.globals["testGlobal"]} Options: {context.options["testOption"]}` + } + } + end + } + ]] + fs.writestringtofile(customRuleFile, customRuleContents) + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ + return { + lute = { + lint = { + globals = { + ["testGlobal"] = 1, + }, + rules = { + ["testRule"] = { + options = { + ["testOption"] = "nice", + } + } + } + } + } + } + ]] + fs.writestringtofile(configFile, configContents) + + local result = process.run({ + lutePath, + "lint", + "-c", + path.format(configFile), + "-r", + path.format(customRuleFile), + "-s", + "local x = 1", + }) + + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, "Globals: 1") + assert.strcontains(result.stdout, "Options: nice") + end) + + suite:case("lute lint accounts for rule-specific ignores", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + local violatorFile = path.join(testSubDir, "violator.luau") + fs.writestringtofile( + violatorFile, + [[ + local x = 1 / 0 + local a, b + a = b + b = a +]] + ) + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ +return { + lute = { + lint = { + rules = { + ["divide_by_zero"] = { + ignores = { "**/violator.luau" } + } + } + } + } +} +]] + fs.writestringtofile(configFile, configContents) + + -- assert that violations are present without config + local result = process.run({ lutePath, "lint", path.format(violatorFile) }, { + cwd = path.format(process.homedir()), + }) + assert.eq(result.exitcode, 1) -- should be violations + assert.strcontains(result.stdout, "violator.luau") + assert.strcontains(result.stdout, "[divide_by_zero]") + assert.strcontains(result.stdout, "[almost_swapped]") + + -- with config, there should be violations but NOT for divide_by_zero + result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(violatorFile) }, { + cwd = path.format(process.homedir()), + }) + assert.eq(result.exitcode, 1) -- should be no violations + assert.strnotcontains(result.stdout, "[divide_by_zero]") + assert.strcontains(result.stdout, "[almost_swapped]") + end) + + suite:case("lute lint accounts for rule-specific directory ignores", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + local ignoredDir = path.join(testSubDir, "ignoredDir") + fs.createdirectory(ignoredDir) + + local ignoredFile = path.join(ignoredDir, "violator.luau") + local notIgnoredFile = path.join(testSubDir, "nonIgnored.luau") + + local violatingContents = [[ +local x = 1 / 0 +local a, b +a = b +b = a +]] + fs.writestringtofile(ignoredFile, violatingContents) + fs.writestringtofile(notIgnoredFile, violatingContents) + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ +return { + lute = { + lint = { + rules = { + ["divide_by_zero"] = { + ignores = { "**/ignoredDir/**" } + } + } + } + } +} +]] + fs.writestringtofile(configFile, configContents) + + local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) + assert.eq(result.exitcode, 1) + + -- Both files should still report almost_swapped (the non-ignored rule) + assert.strcontains(result.stdout, "[almost_swapped]", nil, 2) + + -- Only 1 divide_by_zero: from the non-ignored file, not from ignoredDir + assert.strcontains(result.stdout, "[divide_by_zero]", nil, 1) + assert.strcontains(result.stdout, "nonIgnored.luau:1:11-16") -- the non-ignored file's violation + end) end) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 462fc478f..65fa8bc01 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -64,23 +64,24 @@ lute/cli/commands/doc/init.luau:321:10-24 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/init.luau:62:10-18 -lute/cli/commands/lint/init.luau:434:15-24 -lute/cli/commands/lint/init.luau:435:56-65 -lute/cli/commands/lint/init.luau:435:56-65 -lute/cli/commands/lint/init.luau:435:56-65 -lute/cli/commands/lint/init.luau:435:56-65 -lute/cli/commands/lint/init.luau:435:56-65 -lute/cli/commands/lint/init.luau:435:56-65 -lute/cli/commands/lint/init.luau:489:37-46 -lute/cli/commands/lint/init.luau:490:40-49 +lute/cli/commands/lint/init.luau:84:8-30 +lute/cli/commands/lint/init.luau:475:15-24 +lute/cli/commands/lint/init.luau:476:56-65 +lute/cli/commands/lint/init.luau:476:56-65 +lute/cli/commands/lint/init.luau:476:56-65 +lute/cli/commands/lint/init.luau:476:56-65 +lute/cli/commands/lint/init.luau:476:56-65 +lute/cli/commands/lint/init.luau:476:56-65 +lute/cli/commands/lint/init.luau:489:36-45 +lute/cli/commands/lint/init.luau:490:39-48 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 lute/cli/commands/lint/printer.luau:58:8-52 -lute/cli/commands/lint/rules/parenthesized_conditions.luau:23:5-16 -lute/cli/commands/lint/rules/parenthesized_conditions.luau:38:6-17 -lute/cli/commands/lint/rules/parenthesized_conditions.luau:64:4-15 -lute/cli/commands/lint/rules/unused_variable.luau:303:3-100 +lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 +lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 +lute/cli/commands/lint/rules/parenthesized_conditions.luau:68:4-15 +lute/cli/commands/lint/rules/unused_variable.luau:307:3-100 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau:25:42-59 From 394a2118c0d8d971929a45d61411dd2f68de35e6 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 17 Feb 2026 10:51:23 -0800 Subject: [PATCH 354/642] Adds a no-default-lints option to lute lint (#820) Also updates lute lint's behavior to always run default rules unless the option is specified --- docs/cli/lint/index.md | 8 ++ lute/cli/commands/lint/init.luau | 37 +++--- tests/cli/lint.test.luau | 206 +++++++++++++++---------------- tools/check-faillist.txt | 22 ++-- 4 files changed, 141 insertions(+), 132 deletions(-) diff --git a/docs/cli/lint/index.md b/docs/cli/lint/index.md index 41e0fc472..82ec8fc85 100755 --- a/docs/cli/lint/index.md +++ b/docs/cli/lint/index.md @@ -35,6 +35,14 @@ Output lint violations in JSON format matching the LSP diagnostic spec. Lint the provided string input instead of reading from files. +### `--auto-fix` + +Automatically apply fixes for lint violations that provide a suggested fix. Assumes that suggested fixes do not overlap. Does nothing when linting string input. + +### `--no-default-lints` + +Disables the default lint rules. + ## Arguments Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 77a48784c..a6f104e98 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -28,6 +28,7 @@ local VERBOSE = false local function printHelp() print(USAGE) + -- Ordering of options: help + verbose fist, then options with aliases in alphabetical order, then options without aliases in alphabetical order print([[ Lint the specified Luau file using the specified lint rule(s) or using the default rules. @@ -36,14 +37,15 @@ OPTIONS: -v, --verbose Enable verbose output -c, --config [PATH] Path to configuration file. By default, lute lint will look for a the .config.luau file in the calling directory. + -j, --json Output lint violations in JSON format matching the LSP diagnostic spec. -r, --rules [RULE] Path to a single lint rule or a folder containing lint rules. If a folder is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated as individual lint rules. If unspecified, the default lint rules are used. - -j, --json Output lint violations in JSON format matching the LSP diagnostic spec. -s, --string-input Lint the provided string input instead of reading from files. --auto-fix Automatically apply fixes for lint violations that provide a suggested fix. Assumes that suggested fixes do not overlap. + --no-default-lints Disables the default lint rules. PATHS: Path(s) to the Luau file(s) or folders containing Luau files to be linted. Only files with .luau or .lua extensions will be linted. @@ -433,21 +435,22 @@ end local function main(...: string) local args = cli.parser() + args:add( + "auto-fix", + "flag", + { help = "Automatically apply fixes for lint violations that provide a suggested fix" } + ) args:add("config", "option", { help = "Path to configuration file", aliases = { "c" } }) - args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) args:add("help", "flag", { help = "Show help message", aliases = { "h" } }) - args:add("verbose", "flag", { help = "Enable verbose output", aliases = { "v" } }) args:add("json", "flag", { help = "Output lint violations in JSON format", aliases = { "j" } }) + args:add("no-default-lints", "flag", { help = "Disables the default lint rules" }) + args:add("rules", "option", { help = "Linting rule(s) to apply", aliases = { "r" } }) args:add( "string-input", "option", { help = "Lint the provided string input instead of reading from files", aliases = { "s" } } ) - args:add( - "auto-fix", - "flag", - { help = "Automatically apply fixes for lint violations that provide a suggested fix" } - ) + args:add("verbose", "flag", { help = "Enable verbose output", aliases = { "v" } }) args:parse({ ... }) @@ -481,7 +484,7 @@ local function main(...: string) end end - local lintRules: { types.LintRule } + local lintRules: { types.LintRule } = {} local ruleConfigurations: types.RuleConfigurations = {} local ruleIgnoreData: types.RuleIgnores = {} @@ -491,17 +494,23 @@ local function main(...: string) else pathLib.format(process.cwd()) local ignoreData = parseIgnores.parseIgnoreContents(rootLocation, ignores) - local rulePath = args:get("rules") - if rulePath == nil then + if args:has("no-default-lints") then if VERBOSE then - print("Using default lint rules.") + print("Default lint rules have been disabled.") end - lintRules, ruleConfigurations, ruleIgnoreData = loadDefaultRules(lintConfig, rootLocation) else + lintRules, ruleConfigurations, ruleIgnoreData = loadDefaultRules(lintConfig, rootLocation) + end + + local rulePath = args:get("rules") + if rulePath then if VERBOSE then print(`Loading lint rule(s) from '{rulePath}'`) end - lintRules, ruleConfigurations, ruleIgnoreData = loadLintRules(rulePath, lintConfig, rootLocation) + local customRules, customConfigs, customIgnores = loadLintRules(rulePath, lintConfig, rootLocation) + tableext.extend(lintRules, customRules) + tableext.combine(ruleConfigurations, customConfigs) + tableext.combine(ruleIgnoreData, customIgnores) end local configContextData = { diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 361242dde..8ae7168ce 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -35,6 +35,7 @@ type lintTestParams = { expectedExitCode: number, rulePath: string?, expectedAutofixContents: string?, + disableDefaultLints: true?, extraOptions: { string }?, outputExpectations: { { expectedOutput: string, count: number } }?, } @@ -58,6 +59,10 @@ local function lintTestHelper(assert: testTypes.asserts, params: lintTestParams) table.insert(lintArgs, "--auto-fix") end + if params.disableDefaultLints then + table.insert(lintArgs, "--no-default-lints") + end + if params.extraOptions then tableext.extend(lintArgs, params.extraOptions) end @@ -139,7 +144,6 @@ test.suite("lute lint", function(suite) expectedExitCode = 0, extraOptions = { "-v" }, outputExpectations = { - { expectedOutput = "Using default lint rules.", count = 1 }, { expectedOutput = "Applying lint rules", count = 1 }, { expectedOutput = "Printing violations from 0 files", count = 1 }, }, @@ -156,6 +160,7 @@ local x = 1 / 0 ]], expectedExitCode = 1, rulePath = defaultRulesPaths.divide_by_zero, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, { @@ -214,6 +219,7 @@ end ]], expectedExitCode = 1, rulePath = defaultRulesPaths.almost_swapped, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 4 }, { @@ -311,7 +317,6 @@ end end) suite:case("lute lint custom rules dir", function(assert) - -- TODO: Use new helper after adding option to turn off default rules -- Setup fs.createdirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau @@ -328,53 +333,47 @@ not a luau file ]] ) - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ + lintTestHelper(assert, { + content = [[ local x = 1 / 0 a = b b = a - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", rulesDir, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) - local expected = [[ +]], + expectedExitCode = 1, + rulePath = rulesDir, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, + { + expectedOutput = [[ violator.luau:1:11-16 ── │ 1 │ local x = 1 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:3:1-4:6 ── │ 3 │ ╭ a = b 4 │ │ b = a │ ╰─────^ │ -]] - assert.strcontains(result.stdout, expected) +]], + count = 1, + }, + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 1 }, + }, + }) - -- Teardown - fs.remove(violatorPath) fs.removedirectory(rulesDir, { recursive = true }) end) suite:case("lute lint multiple rules but one errors", function(assert) - -- TODO: Use new helper after adding option to turn off default rules -- Setup fs.createdirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau @@ -392,36 +391,30 @@ return table.freeze({ ]] ) - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ -a = b + local output = lintTestHelper(assert, { + content = [[a = b b = a - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", rulesDir, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) - local expected = [[ -violator.luau:1:1-2:6 ── +]], + expectedExitCode = 1, + rulePath = rulesDir, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 1 }, + { + expectedOutput = [[violator.luau:1:1-2:6 ── │ 1 │ ╭ a = b 2 │ │ b = a │ ╰─────^ │ -]] - assert.strcontains(result.stdout, expected) +]], + count = 1, + }, + }, + }) assert.neq( - result.stdout:find( + output:find( "Error applying lint rule 'AlwaysErrors': %.[/\\]lints[/\\]erroring_rule%.luau:4: This rule always errors", 1 ), @@ -429,7 +422,6 @@ violator.luau:1:1-2:6 ── ) -- Teardown - fs.remove(violatorPath) fs.removedirectory(rulesDir, { recursive = true }) end) @@ -465,8 +457,7 @@ y = x -- Do -- Run the transformer on the transformee - local result = - process.run({ lutePath, "lint", "-r", "examples/lints", "-v", violatorPath1, path.format(modulePath) }) + local result = process.run({ lutePath, "lint", "-v", violatorPath1, path.format(modulePath) }) -- Check assert.eq(result.exitcode, 1) @@ -632,77 +623,39 @@ violator.luau:9:1-10:6 ── -- lute-lint-ignore(divide_by_zero) local x = 1 / 0 --- lute-lint-ignore(divide_by_zero) if true then local y = 10 / 0 end -a = b -b = a +local z = 1 / 0 ]], expectedExitCode = 1, + rulePath = defaultRulesPaths.divide_by_zero, + disableDefaultLints = true, outputExpectations = { - { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 0 }, - { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 1 }, + { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 2 }, { expectedOutput = [[ -violator.luau:9:1-10:6 ── - │ - 9 │ ╭ a = b - 10 │ │ b = a - │ ╰─────^ - │ -]], - count = 1, - }, - }, - }) - - -- Setup - -- Create a file that violates the rule - local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile( - violatorPath, - [[ --- lute-lint-ignore(divide_by_zero) -local x = 1 / 0 - -if true then - local y = 10 / 0 -end - -local z = 1 / 0 - ]] - ) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", defaultRulesPaths.divide_by_zero, violatorPath }) - - -- Check - assert.eq(result.exitcode, 1) - - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero, nil, 2) - local expected = [[ violator.luau:5:12-18 ── │ 5 │ local y = 10 / 0 │ ^^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - expected = [[ +]], + count = 1, + }, + { + expectedOutput = [[ violator.luau:8:11-16 ── │ 8 │ local z = 1 / 0 │ ^^^^^ │ -]] - assert.strcontains(result.stdout, expected) - - -- Teardown - fs.remove(violatorPath) +]], + count = 1, + }, + }, + }) end) suite:case("suppression is limited to following statement2", function(assert) @@ -719,6 +672,7 @@ local z = 1 / 0 ]], expectedExitCode = 1, rulePath = defaultRulesPaths.divide_by_zero, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 2 }, { @@ -757,6 +711,7 @@ end ]], expectedExitCode = 1, rulePath = defaultRulesPaths.divide_by_zero, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, { @@ -785,6 +740,7 @@ end ]], expectedExitCode = 1, rulePath = defaultRulesPaths.divide_by_zero, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, { @@ -813,6 +769,7 @@ end ]], expectedExitCode = 1, rulePath = defaultRulesPaths.divide_by_zero, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, { @@ -842,6 +799,7 @@ end ]], expectedExitCode = 1, rulePath = defaultRulesPaths.divide_by_zero, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.divide_by_zero, count = 1 }, { @@ -1141,7 +1099,6 @@ b = a -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "Using default lint rules.") assert.strcontains(result.stdout, "Parsing input string") assert.strcontains(result.stdout, "Parsing global lint ignores in input string") assert.strcontains(result.stdout, "Applying lint rules") @@ -1263,6 +1220,7 @@ until (5 == 5) ]], expectedExitCode = 1, rulePath = defaultRulesPaths.parenthesized_conditions, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.parenthesized_conditions, @@ -1359,6 +1317,7 @@ until (5 == 5) ]], expectedExitCode = 0, rulePath = defaultRulesPaths.parenthesized_conditions, + disableDefaultLints = true, expectedAutofixContents = [[ if 3 > 0 then print("Hello, world!") @@ -1414,6 +1373,7 @@ local e = t ~= { 1, 2 } ]], expectedExitCode = 1, rulePath = defaultRulesPaths.constant_table_comparison, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.constant_table_comparison, @@ -1484,6 +1444,7 @@ local e = t ~= {} ]], expectedExitCode = 0, rulePath = defaultRulesPaths.constant_table_comparison, + disableDefaultLints = true, expectedAutofixContents = [[ local a = next(t) == nil local b = next(t) == nil @@ -1550,6 +1511,7 @@ local x = 3 ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1576,6 +1538,7 @@ local _ = 3 ]], expectedExitCode = 0, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1594,6 +1557,7 @@ end ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1623,6 +1587,7 @@ end ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1660,6 +1625,7 @@ x = x + 1 ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1689,6 +1655,7 @@ print(y) ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1717,6 +1684,7 @@ print(x) ]], expectedExitCode = 0, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1734,6 +1702,7 @@ x += 1 ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1762,6 +1731,7 @@ print(x) ]], expectedExitCode = 0, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1779,6 +1749,7 @@ x += 1 ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1807,6 +1778,7 @@ end ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1837,6 +1809,7 @@ x() ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1865,6 +1838,7 @@ print(x) ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1895,6 +1869,7 @@ x() ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1922,6 +1897,7 @@ local x = 5 + x ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1949,6 +1925,7 @@ local _x : lib.num = 3 ]], expectedExitCode = 0, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1967,6 +1944,7 @@ end ]], expectedExitCode = 0, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -1987,6 +1965,7 @@ return { ]], expectedExitCode = 0, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -2007,6 +1986,7 @@ end ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -2035,6 +2015,7 @@ a, b = foo() ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -2074,6 +2055,7 @@ print(y) ]], expectedExitCode = 1, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -2102,6 +2084,7 @@ until x == 1 ]], expectedExitCode = 0, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = "Local variable not found in any scope", count = 0 }, { @@ -2121,6 +2104,7 @@ end ]=], expectedExitCode = 0, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -2139,6 +2123,7 @@ end ]=], expectedExitCode = 0, rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.unused_variable, @@ -2305,6 +2290,7 @@ local t = { 1, 2, [2] = 3 } ]], expectedExitCode = 1, rulePath = defaultRulesPaths.duplicate_keys, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, { @@ -2328,6 +2314,7 @@ local t = { a = 1, a = 2 } ]], expectedExitCode = 1, rulePath = defaultRulesPaths.duplicate_keys, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, { @@ -2351,6 +2338,7 @@ local t = { ["a"] = 1, ["a"] = 2 } ]], expectedExitCode = 1, rulePath = defaultRulesPaths.duplicate_keys, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, { @@ -2374,6 +2362,7 @@ local t = { [1] = 1, [1] = 2 } ]], expectedExitCode = 1, rulePath = defaultRulesPaths.duplicate_keys, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, { @@ -2397,6 +2386,7 @@ local t = { 1, 2, [2] = 3 } ]], expectedExitCode = 1, rulePath = defaultRulesPaths.duplicate_keys, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, { @@ -2420,6 +2410,7 @@ local t = { ["a"] = 1, a = 2 } ]], expectedExitCode = 1, rulePath = defaultRulesPaths.duplicate_keys, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 1 }, { @@ -2443,6 +2434,7 @@ local t = { [0] = 1 } ]], expectedExitCode = 0, rulePath = defaultRulesPaths.duplicate_keys, + disableDefaultLints = true, outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.duplicate_keys, count = 0 }, }, diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 65fa8bc01..2bd02154a 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -63,17 +63,17 @@ lute/cli/commands/doc/init.luau:315:5-16 lute/cli/commands/doc/init.luau:321:10-24 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 -lute/cli/commands/lint/init.luau:62:10-18 -lute/cli/commands/lint/init.luau:84:8-30 -lute/cli/commands/lint/init.luau:475:15-24 -lute/cli/commands/lint/init.luau:476:56-65 -lute/cli/commands/lint/init.luau:476:56-65 -lute/cli/commands/lint/init.luau:476:56-65 -lute/cli/commands/lint/init.luau:476:56-65 -lute/cli/commands/lint/init.luau:476:56-65 -lute/cli/commands/lint/init.luau:476:56-65 -lute/cli/commands/lint/init.luau:489:36-45 -lute/cli/commands/lint/init.luau:490:39-48 +lute/cli/commands/lint/init.luau:64:10-18 +lute/cli/commands/lint/init.luau:86:8-30 +lute/cli/commands/lint/init.luau:478:15-24 +lute/cli/commands/lint/init.luau:479:56-65 +lute/cli/commands/lint/init.luau:479:56-65 +lute/cli/commands/lint/init.luau:479:56-65 +lute/cli/commands/lint/init.luau:479:56-65 +lute/cli/commands/lint/init.luau:479:56-65 +lute/cli/commands/lint/init.luau:479:56-65 +lute/cli/commands/lint/init.luau:492:36-45 +lute/cli/commands/lint/init.luau:493:39-48 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 From f5b0f8e42ebc448c802f53208f898c03ff2053ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:02:54 +0000 Subject: [PATCH 355/642] Update Lute to 0.1.0-nightly.20260213 (#821) **Lute**: Updated from `0.1.0-nightly.20260205` to `0.1.0-nightly.20260213` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260213 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: skberkeley --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index b4ea22895..7639a2ac3 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260205" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260213" } diff --git a/rokit.toml b/rokit.toml index 59b255ac7..526e2a197 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20260205" +lute = "luau-lang/lute@0.1.0-nightly.20260213" From edf8eda642540167191e8c6df18b42d31b1f94e9 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 17 Feb 2026 12:55:07 -0800 Subject: [PATCH 356/642] Adds unnecessary tag to unused variable lint rule violations (#819) Once the necessary change makes it into Mandolin, this allows us to grey out unused variables --- lute/cli/commands/lint/rules/unused_variable.luau | 1 + tests/cli/lint.test.luau | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index 2b785b48e..7c1d79fd0 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -311,6 +311,7 @@ local function lint( severity = "warning", sourcepath = sourcepath, target = target, + tags = { "unnecessary" }, }) -- LUAUFIX: Table literal doesn't subtype against lintTypes.LintViolation for some reason end) end diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 8ae7168ce..bd06aacb3 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -897,6 +897,9 @@ local x = 1 / 0 "source": "lute lint", "code": "unused_variable", "severity": 2, + "tags": [ + 1 + ], "range": { "start": { "character": 6, @@ -1136,6 +1139,9 @@ b = a "source": "lute lint", "code": "unused_variable", "severity": 2, + "tags": [ + 1 + ], "range": { "start": { "character": 6, From c9efa80b7b000af32741bf6664b6974e885bd40c Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:10:12 -0800 Subject: [PATCH 357/642] Fixes bug when serializing nested `MetatableType` (#825) Found a bug when serializing nested MetatableTypes when testing the `typeofmodule` API on the modules in `definitions/` and it passed the current test cases because I was only testing shallow metatables, but when we have nested metatables like that in `@lute/time` and we have things like ```lua export type Duration = setmetatable Duration, read __sub: (Duration, Duration) -> Duration, read __eq: (Duration, Duration) -> boolean, read __lt: (Duration, Duration) -> boolean, read __le: (Duration, Duration) -> boolean, }> ``` The indices were getting messed up because I was creating an extra table for the metatable, which isn't correct bc we're treating a metatable as just a table with an extra `metatable` field). We don't need to do that because when `traverse(mtv.table)` enters the `serialize(TableType)` case, that already handles creating that table. This fixes any issues I have with calling the Type Serializer on all of the `.luau` files in `definitions/` --- lute/luau/src/type.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp index c98ebe54f..d6ea30bb3 100644 --- a/lute/luau/src/type.cpp +++ b/lute/luau/src/type.cpp @@ -372,11 +372,8 @@ struct TypeSerialize final : public Luau::TypeVisitor // metatable: type? void serialize(TypeId ty, const MetatableType& mtv) { - checkStack(L, 2); - lua_createtable(L, 0, 2); - registerType(ty); - traverse(mtv.table); + registerType(ty); traverse(mtv.metatable); lua_setfield(L, -2, "metatable"); From 7a70d830dea7ae522efe64e645802eec7aa8dfed Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:56:38 -0500 Subject: [PATCH 358/642] support turning rules off via lint config (#824) This PR adds support for the `off` field on a rule's configuration, allowing users to disable rules via lute lint config. --- lute/cli/commands/lint/init.luau | 6 +- lute/cli/commands/lint/types.luau | 2 +- tests/cli/lint.test.luau | 98 ++++++++++++++++++++++++++++++- tools/check-faillist.txt | 20 +++---- 4 files changed, 113 insertions(+), 13 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index a6f104e98..54150311c 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -81,10 +81,14 @@ local function registerRule( ruleConfigs: types.RuleConfigurations, ruleIgnores: types.RuleIgnores ) - table.insert(rules, rule) local ruleConfig: types.RuleConfig = if config.rules ~= nil and config.rules[rule.name] ~= nil then config.rules[rule.name] else {} + if ruleConfig.off then + return + end + + table.insert(rules, rule) ruleConfigs[rule.name] = ruleConfig if ruleConfig.ignores ~= nil then ruleIgnores[rule.name] = parseIgnores.parseIgnoreContents(rootLocation, ruleConfig.ignores) diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index a8416a6a2..8f368b019 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -27,7 +27,7 @@ export type LintRule = { export type RuleConfig = { ignores: { string }?, -- severity: severity?, - -- off: true?, + off: true?, options: RuleOptions?, } diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index bd06aacb3..cbce932bb 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -2221,7 +2221,7 @@ end end) suite:case("lute lint accounts for configured ignores (config auto-detected)", function(assert) - local testSubDir = path.join(tmpDir, "testDir") + local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) local ignoredFile = path.join(testSubDir, "ignored.luau") @@ -2647,4 +2647,100 @@ return { assert.strcontains(result.stdout, "[divide_by_zero]", nil, 1) assert.strcontains(result.stdout, "nonIgnored.luau:1:11-16") -- the non-ignored file's violation end) + + suite:case("default rules configured with off do NOT run", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + local configFile = path.join(tmpDir, ".config.luau") + local configContents = [[ + return { + lute = { + lint = { + rules = { + ["almost_swapped"] = { + off = true, + } + } + } + } + } + ]] + fs.writestringtofile(configFile, configContents) + + local violatingFile = path.join(testSubDir, "violate.luau") + local violationContents = [[ + a = b + b = a + ]] + fs.writestringtofile(violatingFile, violationContents) + + -- confirm file violations with no config + local result = process.run({ lutePath, "lint", path.format(violatingFile) }) + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, "almost_swapped") + + -- confirm file doesn't violate with disabled rule config + result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(violatingFile) }) + assert.eq(result.exitcode, 0) + assert.strnotcontains(result.stdout, "almost_swapped") + end) + + suite:case("custom rules configured with off do NOT run", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + local customRuleFile = path.join(tmpDir, "customRule.luau") + local customRuleContents = [[ + return { + name = "customRule", + lint = function(ast, _sourcepath, _context) + return { + { + lintname = "customRule", + severity = "warning", + message = "customRule", + location = ast.location, + } + } + end + } + ]] + fs.writestringtofile(customRuleFile, customRuleContents) + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ + return { + lute = { + lint = { + rules = { + ["customRule"] = { + off = true, + } + } + } + } + } + ]] + fs.writestringtofile(configFile, configContents) + + -- confirm violations with no config + local result = process.run({ lutePath, "lint", "-r", path.format(customRuleFile), path.format(customRuleFile) }) + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, "customRule") + + -- confirm file doesn't violate with disabled rule config + result = process.run({ + lutePath, + "lint", + "-c", + path.format(configFile), + "-r", + path.format(customRuleFile), + path.format(customRuleFile), + }) + assert.eq(result.exitcode, 0) + assert.strnotcontains(result.stdout, "customRule") + + fs.remove(customRuleFile) + end) end) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 2bd02154a..00a967b3e 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -64,16 +64,16 @@ lute/cli/commands/doc/init.luau:321:10-24 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/init.luau:64:10-18 -lute/cli/commands/lint/init.luau:86:8-30 -lute/cli/commands/lint/init.luau:478:15-24 -lute/cli/commands/lint/init.luau:479:56-65 -lute/cli/commands/lint/init.luau:479:56-65 -lute/cli/commands/lint/init.luau:479:56-65 -lute/cli/commands/lint/init.luau:479:56-65 -lute/cli/commands/lint/init.luau:479:56-65 -lute/cli/commands/lint/init.luau:479:56-65 -lute/cli/commands/lint/init.luau:492:36-45 -lute/cli/commands/lint/init.luau:493:39-48 +lute/cli/commands/lint/init.luau:85:8-30 +lute/cli/commands/lint/init.luau:482:15-24 +lute/cli/commands/lint/init.luau:483:56-65 +lute/cli/commands/lint/init.luau:483:56-65 +lute/cli/commands/lint/init.luau:483:56-65 +lute/cli/commands/lint/init.luau:483:56-65 +lute/cli/commands/lint/init.luau:483:56-65 +lute/cli/commands/lint/init.luau:483:56-65 +lute/cli/commands/lint/init.luau:496:36-45 +lute/cli/commands/lint/init.luau:497:39-48 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 From 7835f9890f8dcfb2fe1486f338ff836f8b38b20b Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:46:17 -0500 Subject: [PATCH 359/642] Support custom rule severity in lint config (#823) This PR adds support for customizing a rule's severity via lute lint configuration. The change is pretty small, and just sets each detected `violation`'s severity according to the configured severity, if it exists, before we add the `violation` to the cumulative `violations` table. --- lute/cli/commands/lint/init.luau | 2 + .../commands/lint/rules/almost_swapped.luau | 2 +- .../commands/lint/rules/unused_variable.luau | 4 +- lute/cli/commands/lint/types.luau | 18 ++++----- tests/cli/lint.test.luau | 38 +++++++++++++++++++ tools/check-faillist.txt | 21 +++++----- 6 files changed, 63 insertions(+), 22 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 54150311c..a2d5951dc 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -281,10 +281,12 @@ local function lintString( } local success, err = pcall(rule.lint, ast, inputFilePath, context) + local overwrittenSeverity = ruleConfig.severity if success then -- On success, err contains the returned violations for _, violation in err do if not violationIsSuppressed(violation, ast, directiveCache, globalIgnores) then + violation.severity = if overwrittenSeverity then overwrittenSeverity else violation.severity table.insert(violations, violation) end end diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index b5bc9bef4..abef8c78b 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -89,7 +89,7 @@ local function lint( local currValStr = stringext.trim(printer.printnode(currVal)) local suggestedFix = `{currVarStr}, {currValStr} = {currValStr}, {currVarStr}` - table.insert(violations, { + table.insert(violations, { -- LUAUFIX: inserted table isn't inferred as a LintViolation lintname = name, location = syntax.span.create({ beginline = currStat.location.beginline, diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index 7c1d79fd0..83ad49bc0 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -304,7 +304,7 @@ local function lint( _context: lintTypes.RuleContext ): { lintTypes.LintViolation } return tableext.map(unusedLocals(ast), function(l): lintTypes.LintViolation - return table.freeze({ + return { lintname = name, location = l.location, message = message, @@ -312,7 +312,7 @@ local function lint( sourcepath = sourcepath, target = target, tags = { "unnecessary" }, - }) -- LUAUFIX: Table literal doesn't subtype against lintTypes.LintViolation for some reason + } -- LUAUFIX: Table literal doesn't subtype against lintTypes.LintViolation for some reason end) end diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index 8f368b019..05630492a 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -6,17 +6,17 @@ export type severity = "error" | "warning" | "info" | "hint" export type tag = "unnecessary" | "deprecated" export type LintViolation = { - read lintname: string, - read location: syntax.span, - read message: string, - read severity: severity, - read sourcepath: path.path?, - read suggestedfix: { + lintname: string, + location: syntax.span, + message: string, + severity: severity, + sourcepath: path.path?, + suggestedfix: { read location: syntax.span?, -- if nil, applies to whole violation location read fix: string, }?, - read target: string?, -- Additional information on the violation (e.g link to docs) - read tags: { tag }?, + target: string?, -- Additional information on the violation (e.g link to docs) + tags: { tag }?, } export type LintRule = { @@ -26,7 +26,7 @@ export type LintRule = { export type RuleConfig = { ignores: { string }?, - -- severity: severity?, + severity: severity?, off: true?, options: RuleOptions?, } diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index cbce932bb..c5b00bceb 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -2743,4 +2743,42 @@ return { fs.remove(customRuleFile) end) + + suite:case("configured rule severity overrides default", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + local violationDir = path.join(testSubDir, "violator") + fs.createdirectory(violationDir) + local violatorFile = path.format(path.join(violationDir, "violator.luau")) + fs.writestringtofile(violatorFile, "local x = 1 / 0") + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ +return { + lute = { + lint = { + rules = { + ["divide_by_zero"] = { + severity = "error" + } + } + } + } +} +]] + fs.writestringtofile(configFile, configContents) + + -- assert that severity is "warning" without config applied + local result = process.run({ lutePath, "lint", violatorFile }) + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, "warning[divide_by_zero]") + assert.strnotcontains(result.stdout, "error[divide_by_zero]") + + -- with config applied, severity is now "error" + result = process.run({ lutePath, "lint", "-c", path.format(configFile), violatorFile }) + assert.eq(result.exitcode, 1) + assert.strnotcontains(result.stdout, "warning[divide_by_zero]") + assert.strcontains(result.stdout, "error[divide_by_zero]") + end) end) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 00a967b3e..5961ed844 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -65,23 +65,24 @@ lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/init.luau:64:10-18 lute/cli/commands/lint/init.luau:85:8-30 -lute/cli/commands/lint/init.luau:482:15-24 -lute/cli/commands/lint/init.luau:483:56-65 -lute/cli/commands/lint/init.luau:483:56-65 -lute/cli/commands/lint/init.luau:483:56-65 -lute/cli/commands/lint/init.luau:483:56-65 -lute/cli/commands/lint/init.luau:483:56-65 -lute/cli/commands/lint/init.luau:483:56-65 -lute/cli/commands/lint/init.luau:496:36-45 -lute/cli/commands/lint/init.luau:497:39-48 +lute/cli/commands/lint/init.luau:484:15-24 +lute/cli/commands/lint/init.luau:485:56-65 +lute/cli/commands/lint/init.luau:485:56-65 +lute/cli/commands/lint/init.luau:485:56-65 +lute/cli/commands/lint/init.luau:485:56-65 +lute/cli/commands/lint/init.luau:485:56-65 +lute/cli/commands/lint/init.luau:485:56-65 +lute/cli/commands/lint/init.luau:498:36-45 +lute/cli/commands/lint/init.luau:499:39-48 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 lute/cli/commands/lint/printer.luau:58:8-52 +lute/cli/commands/lint/rules/almost_swapped.luau:92:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 lute/cli/commands/lint/rules/parenthesized_conditions.luau:68:4-15 -lute/cli/commands/lint/rules/unused_variable.luau:307:3-100 +lute/cli/commands/lint/rules/unused_variable.luau:309:15-24 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau:25:42-59 From 28c16a86ca76c8190221c1cb7f851708cfd25a26 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:35:38 -0500 Subject: [PATCH 360/642] Don't Error when Loading Invalid Lint Rule (#827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lute lint` will fail if any of the paths derived from the passed `-r` arg are _not_ valid lint rules. I think it'd be a nicer UX for consumers to be able to call `lute lint -r {path_to_suite}/src ...` rather than maintaining some dir that is exclusively the rule implementations themselves (which will prob b a bunch of re-exports in most cases anyway). This PR refactors the loading process to silently ignore invalid lint rules, such that `lute lint` will exhaustively load all rules underneath the path passed to `-r`, without requiring each discovered .luau file to be a rule implementation --- lute/cli/commands/lint/init.luau | 56 +++++++++++++++++++++++--------- tools/check-faillist.txt | 22 ++++++------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index a2d5951dc..5de2cf347 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -56,21 +56,41 @@ EXAMPLES: ]]) end -local function assertIsLintRule(rule: unknown, path: string) - assert(rule, `{path} failed to require`) - assert(typeof(rule) == "table", `{path} must return a table`) - assert(typeof(rule.name) == "string", `{path} must return a table with a 'name' string property`) - assert( - typeof(rule.lint) == "function", -- LUAUFIX: type error on loaded.lint because loaded has been refined to { read name: string } - `{path} must return a table with a 'lint' function property` - ) +local function isLintRule(rule: unknown, path: string): boolean + if not rule then + if VERBOSE then + print(`Error loading lint rule from {path}: failed to require`) + end + return false + end + + if typeof(rule) ~= "table" then + if VERBOSE then + print(`Error loading lint rule from {path}: must return a table`) + end + return false + end + + if typeof(rule.name) ~= "string" then + if VERBOSE then + print(`Error loading lint rule from {path}: must return a table with a 'name' string property`) + end + return false + end + + if typeof(rule.lint) ~= "function" then + if VERBOSE then + print(`Error loading lint rule from {path}: must return a table with a 'lint' function property`) + end + return false + end + + return true end -local function loadLintRule(path: string): types.LintRule +local function loadLintRule(path: string): types.LintRule? local loaded = luau.loadbypath(path) - assertIsLintRule(loaded, path) - - return loaded -- Typecast isn't needed because loaded is refined to any bc of type error in assertIsLintRule + return if isLintRule(loaded, path) then loaded else nil -- Typecast isn't needed because loaded is refined to any bc of type error in isLintRule end local function registerRule( @@ -113,7 +133,10 @@ local function loadLintRules( local currentPathObj = pathLib.parse(currentPath) local initPath = pathLib.format(pathLib.join(currentPathObj, "init.luau")) if fs.exists(initPath) then - registerRule(loadLintRule(initPath), config, rootLocation, rules, ruleConfigs, ruleIgnores) + local rule = loadLintRule(initPath) + if rule then + registerRule(rule, config, rootLocation, rules, ruleConfigs, ruleIgnores) + end else local entries = fs.listdirectory(currentPath) local childPaths = tableext.map(entries, function(entry) @@ -122,7 +145,10 @@ local function loadLintRules( tableext.extend(queue, childPaths) end elseif pathLib.extname(currentPath) == ".luau" then - registerRule(loadLintRule(currentPath), config, rootLocation, rules, ruleConfigs, ruleIgnores) + local rule = loadLintRule(currentPath) + if rule then + registerRule(rule, config, rootLocation, rules, ruleConfigs, ruleIgnores) + end end end @@ -137,7 +163,7 @@ local function loadDefaultRules( for _, ruleName in DEFAULT_RULES do local path = `@self/rules/{ruleName}` local rule: types.LintRule = require(path) :: any -- Anycast to silence error from dynamic require - assertIsLintRule(rule, path) + assert(isLintRule(rule, path), `Invalid default lint rule: {path}`) registerRule(rule, config, rootLocation, rules, ruleConfigs, ruleIgnores) end return rules, ruleConfigs, ruleIgnores diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 5961ed844..2901d60b9 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -63,17 +63,17 @@ lute/cli/commands/doc/init.luau:315:5-16 lute/cli/commands/doc/init.luau:321:10-24 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 -lute/cli/commands/lint/init.luau:64:10-18 -lute/cli/commands/lint/init.luau:85:8-30 -lute/cli/commands/lint/init.luau:484:15-24 -lute/cli/commands/lint/init.luau:485:56-65 -lute/cli/commands/lint/init.luau:485:56-65 -lute/cli/commands/lint/init.luau:485:56-65 -lute/cli/commands/lint/init.luau:485:56-65 -lute/cli/commands/lint/init.luau:485:56-65 -lute/cli/commands/lint/init.luau:485:56-65 -lute/cli/commands/lint/init.luau:498:36-45 -lute/cli/commands/lint/init.luau:499:39-48 +lute/cli/commands/lint/init.luau:81:12-20 +lute/cli/commands/lint/init.luau:105:8-30 +lute/cli/commands/lint/init.luau:510:15-24 +lute/cli/commands/lint/init.luau:511:56-65 +lute/cli/commands/lint/init.luau:511:56-65 +lute/cli/commands/lint/init.luau:511:56-65 +lute/cli/commands/lint/init.luau:511:56-65 +lute/cli/commands/lint/init.luau:511:56-65 +lute/cli/commands/lint/init.luau:511:56-65 +lute/cli/commands/lint/init.luau:524:36-45 +lute/cli/commands/lint/init.luau:525:39-48 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 From 4fad1905deab514a623382f14ec305d7a7de9c4c Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 18 Feb 2026 16:13:15 -0800 Subject: [PATCH 361/642] Add support custom rules and config in lint test helper (#831) felt the need for this refactor given the addition of configs to lute lint --- tests/cli/lint.test.luau | 151 ++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 90 deletions(-) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index c5b00bceb..a3117bdcc 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -34,8 +34,10 @@ type lintTestParams = { content: string, expectedExitCode: number, rulePath: string?, + customRuleContents: string?, expectedAutofixContents: string?, disableDefaultLints: true?, + configContents: string?, extraOptions: { string }?, outputExpectations: { { expectedOutput: string, count: number } }?, } @@ -47,12 +49,18 @@ local function lintTestHelper(assert: testTypes.asserts, params: lintTestParams) fs.writestringtofile(violatorPath, params.content) -- Do - -- Run the transformer on the transformee local lintArgs = { lutePath, "lint" } - if params.rulePath then + if params.rulePath and params.customRuleContents then + error("Cannot specify both rulePath and customRuleContents") + elseif params.rulePath then table.insert(lintArgs, "-r") table.insert(lintArgs, params.rulePath) + elseif params.customRuleContents then + local customRulePath = path.format(path.join(tmpDir, "custom_rule.luau")) + fs.writestringtofile(customRulePath, params.customRuleContents) + table.insert(lintArgs, "-r") + table.insert(lintArgs, customRulePath) end if params.expectedAutofixContents then @@ -63,6 +71,14 @@ local function lintTestHelper(assert: testTypes.asserts, params: lintTestParams) table.insert(lintArgs, "--no-default-lints") end + -- Create a config file if specified + if params.configContents then + local configPath = path.format(path.join(tmpDir, ".config.luau")) + fs.writestringtofile(configPath, params.configContents) + table.insert(lintArgs, "-c") + table.insert(lintArgs, configPath) + end + if params.extraOptions then tableext.extend(lintArgs, params.extraOptions) end @@ -89,9 +105,6 @@ local function lintTestHelper(assert: testTypes.asserts, params: lintTestParams) end end - -- Teardown - fs.remove(violatorPath) - return result.stdout end @@ -528,7 +541,7 @@ b = a -- Do -- Run the transformer on the transformee - local result = process.run({ lutePath, "lint", "-r", "examples/lints", lintee, violatorPath }) + local result = process.run({ lutePath, "lint", lintee, violatorPath }) -- Check assert.eq(result.exitcode, 1) @@ -2494,11 +2507,7 @@ local t = { [0] = 1 } end) suite:case("lute lint passes context through to rules", function(assert) - local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) - -- custom rule that returns violating with globals and options in the message - local customRuleFile = path.join(testSubDir, "testRule.luau") local customRuleContents = [[ return { name = "testRule", @@ -2514,9 +2523,7 @@ local t = { [0] = 1 } end } ]] - fs.writestringtofile(customRuleFile, customRuleContents) - local configFile = path.join(testSubDir, ".config.luau") local configContents = [[ return { lute = { @@ -2535,22 +2542,15 @@ local t = { [0] = 1 } } } ]] - fs.writestringtofile(configFile, configContents) - local result = process.run({ - lutePath, - "lint", - "-c", - path.format(configFile), - "-r", - path.format(customRuleFile), - "-s", - "local x = 1", + lintTestHelper(assert, { + content = "local x = 1", + expectedExitCode = 1, + customRuleContents = customRuleContents, + configContents = configContents, + disableDefaultLints = true, + outputExpectations = { { expectedOutput = "Globals: 1 Options: nice", count = 1 } }, }) - - assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "Globals: 1") - assert.strcontains(result.stdout, "Options: nice") end) suite:case("lute lint accounts for rule-specific ignores", function(assert) @@ -2597,7 +2597,7 @@ return { result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(violatorFile) }, { cwd = path.format(process.homedir()), }) - assert.eq(result.exitcode, 1) -- should be no violations + assert.eq(result.exitcode, 1) assert.strnotcontains(result.stdout, "[divide_by_zero]") assert.strcontains(result.stdout, "[almost_swapped]") end) @@ -2649,10 +2649,11 @@ return { end) suite:case("default rules configured with off do NOT run", function(assert) - local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + local violationContents = [[ + a = b + b = a + ]] - local configFile = path.join(tmpDir, ".config.luau") local configContents = [[ return { lute = { @@ -2666,31 +2667,16 @@ return { } } ]] - fs.writestringtofile(configFile, configContents) - local violatingFile = path.join(testSubDir, "violate.luau") - local violationContents = [[ - a = b - b = a - ]] - fs.writestringtofile(violatingFile, violationContents) - - -- confirm file violations with no config - local result = process.run({ lutePath, "lint", path.format(violatingFile) }) - assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "almost_swapped") - - -- confirm file doesn't violate with disabled rule config - result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(violatingFile) }) - assert.eq(result.exitcode, 0) - assert.strnotcontains(result.stdout, "almost_swapped") + lintTestHelper(assert, { + content = violationContents, + expectedExitCode = 0, + configContents = configContents, + outputExpectations = { { expectedOutput = LINT_RULE_MESSAGES.almost_swapped, count = 0 } }, + }) end) suite:case("custom rules configured with off do NOT run", function(assert) - local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) - - local customRuleFile = path.join(tmpDir, "customRule.luau") local customRuleContents = [[ return { name = "customRule", @@ -2706,8 +2692,7 @@ return { end } ]] - fs.writestringtofile(customRuleFile, customRuleContents) - local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ return { lute = { @@ -2721,39 +2706,28 @@ return { } } ]] - fs.writestringtofile(configFile, configContents) -- confirm violations with no config - local result = process.run({ lutePath, "lint", "-r", path.format(customRuleFile), path.format(customRuleFile) }) - assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "customRule") + lintTestHelper(assert, { + content = customRuleContents, + expectedExitCode = 1, + customRuleContents = customRuleContents, + disableDefaultLints = true, + outputExpectations = { { expectedOutput = "customRule", count = 1 } }, + }) -- confirm file doesn't violate with disabled rule config - result = process.run({ - lutePath, - "lint", - "-c", - path.format(configFile), - "-r", - path.format(customRuleFile), - path.format(customRuleFile), + lintTestHelper(assert, { + content = customRuleContents, + expectedExitCode = 0, + customRuleContents = customRuleContents, + disableDefaultLints = true, + configContents = configContents, + outputExpectations = { { expectedOutput = "customRule", count = 0 } }, }) - assert.eq(result.exitcode, 0) - assert.strnotcontains(result.stdout, "customRule") - - fs.remove(customRuleFile) end) suite:case("configured rule severity overrides default", function(assert) - local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) - - local violationDir = path.join(testSubDir, "violator") - fs.createdirectory(violationDir) - local violatorFile = path.format(path.join(violationDir, "violator.luau")) - fs.writestringtofile(violatorFile, "local x = 1 / 0") - - local configFile = path.join(testSubDir, ".config.luau") local configContents = [[ return { lute = { @@ -2767,18 +2741,15 @@ return { } } ]] - fs.writestringtofile(configFile, configContents) - -- assert that severity is "warning" without config applied - local result = process.run({ lutePath, "lint", violatorFile }) - assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "warning[divide_by_zero]") - assert.strnotcontains(result.stdout, "error[divide_by_zero]") - - -- with config applied, severity is now "error" - result = process.run({ lutePath, "lint", "-c", path.format(configFile), violatorFile }) - assert.eq(result.exitcode, 1) - assert.strnotcontains(result.stdout, "warning[divide_by_zero]") - assert.strcontains(result.stdout, "error[divide_by_zero]") + lintTestHelper(assert, { + content = "local _ = 1 / 0", + expectedExitCode = 1, + configContents = configContents, + outputExpectations = { + { expectedOutput = "error[divide_by_zero]", count = 1 }, + { expectedOutput = "warning[divide_by_zero]", count = 0 }, + }, + }) end) end) From 214d09b981d5a22c307f4af82d69ae139e9e7673 Mon Sep 17 00:00:00 2001 From: Hunter Goldstein Date: Thu, 19 Feb 2026 09:44:08 -0800 Subject: [PATCH 362/642] Fix various Luau type errors and update Lute's workspace to enable all flags (#829) I went through `tools/check-faillist.txt` and tried to fix as many errors as I could reasonably find. There are definitely some errors that I've fixed by paving over bugs in the new solver (for example all of the `table.sort` invocations). Additionally I've noted some interesting new solver bugs: * Comparing anything with `nil` should not be an error * Errant warning on `#` on a metatable with `__len` * There are some spots where we need better table types: read-only indexers, for example. * There's a couple refinement bugs in `traverseTable` in `lute/batteries/pp.luau`, one of which seems very new. * We should add an overload to pcall that allows taking a function that returns an empty pack. I've also updated the VSCode workspace to enable all flags in `luau-lsp`: this should help with some reported mismatches between the faillist and folks editor experience. --- batteries/cli.luau | 2 +- batteries/collections/deque.luau | 1 + batteries/difftext/printdiff.luau | 6 +- batteries/pp.luau | 5 +- batteries/toml.luau | 11 +- examples/directories.luau | 2 +- lute.code-workspace | 1 + lute/std/libs/syntax/utils/init.luau | 2 +- lute/std/libs/test/failure.luau | 2 +- tests/runtime/crypto.test.luau | 4 +- tests/std/fs.test.luau | 3 + tests/std/json.test.luau | 16 ++- tests/std/process.test.luau | 34 ++++-- tests/std/syntax/printer.test.luau | 8 +- tools/check-faillist.txt | 161 +-------------------------- tools/luthier.luau | 26 +++-- 16 files changed, 78 insertions(+), 206 deletions(-) diff --git a/batteries/cli.luau b/batteries/cli.luau index 178637f82..5f24a309d 100644 --- a/batteries/cli.luau +++ b/batteries/cli.luau @@ -41,7 +41,7 @@ function cli.parser(): Parser end function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions): () - local argument = { + local argument: ArgData = { name = name, kind = kind, options = options or { aliases = {}, required = false }, diff --git a/batteries/collections/deque.luau b/batteries/collections/deque.luau index a78d085b1..77824b2e8 100644 --- a/batteries/collections/deque.luau +++ b/batteries/collections/deque.luau @@ -8,6 +8,7 @@ export type Deque = { popback: (self: Deque) -> T, peekfront: (self: Deque) -> T?, peekback: (self: Deque) -> T?, + __len: (self: Deque) -> number, } local deque = {} diff --git a/batteries/difftext/printdiff.luau b/batteries/difftext/printdiff.luau index b7e59fcd5..3cc3fa595 100644 --- a/batteries/difftext/printdiff.luau +++ b/batteries/difftext/printdiff.luau @@ -67,11 +67,11 @@ local function formatLineSideHeader( local newStr = tostring(if lineNumbers then lineNumbers.newLine else "") maxNumWidth = maxNumWidth or 1 if operation == "EQUAL" then - return string.format(` %{maxNumWidth}s %{maxNumWidth}s| `, oldStr, newStr) + return string.format(` %{maxNumWidth}s %{maxNumWidth}s| ` :: any, oldStr, newStr) elseif operation == "ADD" then - return string.format(`+ %{maxNumWidth}s %{maxNumWidth}s| `, "", newStr) + return string.format(`+ %{maxNumWidth}s %{maxNumWidth}s| ` :: any, "", newStr) elseif operation == "DELETE" then - return string.format(`- %{maxNumWidth}s %{maxNumWidth}s| `, oldStr, "") + return string.format(`- %{maxNumWidth}s %{maxNumWidth}s| ` :: any, oldStr, "") end error(`formatLineSideHeader called with invalid diff operation key: {operation}`) diff --git a/batteries/pp.luau b/batteries/pp.luau index bf0e91627..e002e1986 100644 --- a/batteries/pp.luau +++ b/batteries/pp.luau @@ -90,14 +90,15 @@ local function traverseTable( local output = "" local indentStr = string.rep(" ", indent) - local keys = {} + -- FIXME(Luau): We shouldn't need this annotation. + local keys: { unknown } = {} -- Collect all keys, not just primitives for key in dataTable do table.insert(keys, key) end - table.sort(keys, function(a: string, b: string): boolean + table.sort(keys, function(a, b): boolean local typeofTableA, typeofTableB = typeof(dataTable[a]), typeof(dataTable[b]) if typeofTableA ~= typeofTableB then diff --git a/batteries/toml.luau b/batteries/toml.luau index 4971c763b..5dda55bce 100644 --- a/batteries/toml.luau +++ b/batteries/toml.luau @@ -23,7 +23,7 @@ local function serializeValue(value: string | number) end end -local function hasNestedTables(tbl: {}) +local function hasNestedTables(tbl: { [unknown]: unknown }) for _, v in tbl do if typeof(v) == "table" and next(v) ~= nil then return true @@ -32,7 +32,7 @@ local function hasNestedTables(tbl: {}) return false end -local function tableToToml(tbl: {}, parent: string) +local function tableToToml(tbl: { [any]: any }, parent: string?) local result = "" local subTables = {} local hasDirectValues = false @@ -71,7 +71,7 @@ local function tableToToml(tbl: {}, parent: string) return result end -local function serialize(tbl: {}): string +local function serialize(tbl: { [any]: any }): string return tableToToml(tbl, nil) end @@ -130,7 +130,7 @@ local function deserialize(input: string) local tablePath = string.match(line, "^%[(.-)%]$") local parent = result - for section in string.gmatch(tablePath, "([^.]+)") do + for section in string.gmatch(tablePath :: string, "([^.]+)") do if not parent[section] then parent[section] = {} end @@ -140,9 +140,10 @@ local function deserialize(input: string) currentTable = parent elseif string.match(line, "^(.-)%s*=%s*(.-)$") then local key, value = string.match(line, "^(.-)%s*=%s*(.-)$") + assert(key and value) key = string.match(key, "^%s*(.-)%s*$") value = string.match(value, "^%s*(.-)%s*$") - + assert(key and value) if string.match(value, '^"(.*)"$') or string.match(value, "^'(.*)'$") then value = string.sub(value, 2, -2) value = string.gsub(value, "\\\\", "\\") diff --git a/examples/directories.luau b/examples/directories.luau index 34c73bb27..932041e7c 100644 --- a/examples/directories.luau +++ b/examples/directories.luau @@ -1,5 +1,5 @@ local fs = require("@std/fs") -for _, file in fs.listdir("./examples") do +for _, file in fs.listdirectory("./examples") do print(`Example {file.name} is a {file.type}`) end diff --git a/lute.code-workspace b/lute.code-workspace index b1a682196..bcbeb3611 100644 --- a/lute.code-workspace +++ b/lute.code-workspace @@ -15,5 +15,6 @@ "luau-lsp.fflags.enableNewSolver": true, "luau-lsp.sourcemap.enabled": false, "luau-lsp.platform.type": "standard", + "luau-lsp.fflags.enableByDefault": true, }, } diff --git a/lute/std/libs/syntax/utils/init.luau b/lute/std/libs/syntax/utils/init.luau index a43991fd6..96d1b24e8 100644 --- a/lute/std/libs/syntax/utils/init.luau +++ b/lute/std/libs/syntax/utils/init.luau @@ -238,7 +238,7 @@ function utils.isBaseToken(n: types.AstNode): types.Token? return if n.istoken and not n.kind then n else nil end -function utils.isAttribute(n: types.AstAttribute): types.AstAttribute? +function utils.isAttribute(n: types.AstNode): types.AstAttribute? return if n.kind == "attribute" then n else nil end diff --git a/lute/std/libs/test/failure.luau b/lute/std/libs/test/failure.luau index 636dc77bc..ae6b62185 100644 --- a/lute/std/libs/test/failure.luau +++ b/lute/std/libs/test/failure.luau @@ -15,7 +15,7 @@ function failures.assertion(msg: string): failure end -- Generates the debug information needed to describe assertion failure locations -function failures.lifecycle(hook: hook, err): failure +function failures.lifecycle(hook: types.hook, err): failure -- we need to go 5 function calls up to get to the actual assertion that invoked this local filename, linenumber = debug.info(3, "sl") return { hook = hook, msg = tostring(err), filename = filename, linenumber = linenumber, __tag = "lifecycle" } diff --git a/tests/runtime/crypto.test.luau b/tests/runtime/crypto.test.luau index 2ce2d8ba7..2ea6b9fe7 100644 --- a/tests/runtime/crypto.test.luau +++ b/tests/runtime/crypto.test.luau @@ -105,11 +105,11 @@ test.suite("CryptoRuntimeTests", function(suite) end) assert.errors(function() - crypto.secretbox.open({ ciphertext = message, key = buffer.create(), nonce = "woof" } :: any) + crypto.secretbox.open({ ciphertext = message, key = buffer.create(0), nonce = "woof" } :: any) end) assert.errors(function() - crypto.secretbox.open({ ciphertext = message, key = "woof", nonce = buffer.create() } :: any) + crypto.secretbox.open({ ciphertext = message, key = "woof", nonce = buffer.create(0) } :: any) end) assert.errors(function() diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index d9d29d8de..9279c0346 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -209,6 +209,9 @@ test.suite("FsSuite", function(suite) local success, err = pcall(function() fs.removedirectory(dir, { recursive = false }) + -- FIXME(Luau): We should add an overload to `pcall` that allows + -- passing a function that returns an empty pack. + return nil end) assert.neq(success, true) diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau index cf3dd7260..848b1e930 100644 --- a/tests/std/json.test.luau +++ b/tests/std/json.test.luau @@ -3,6 +3,7 @@ local path = require("@std/path") local test = require("@std/test") local json = require("@std/json") local stringext = require("@std/stringext") +local failure = require("@std/test/failure") local suiteDir = "tests/std/JSONParsingTestSuite" @@ -75,11 +76,16 @@ test.suite("JSONParsingTestSuite", function(suite) assert.eq(type(obj), "table") assert.eq(obj.a, true) assert.eq(type(obj.b), "table") - if json.asobject(obj.b) then - assert.eq(obj.b.c, true) - assert.eq(type(obj.b.d), "table") - for i = 1, #obj.b.d do - assert.eq(type(obj.b.d[i]), "number") + local obj_b = json.asobject(obj.b) + if obj_b then + assert.eq(obj_b.c, true) + local obj_b_d = obj_b.d + if typeof(obj_b_d) ~= "table" then + failure.assertion(`Expected obj.b.d to be a table, was a {typeof(obj_b_d)}`) + return + end + for i = 1, #obj_b_d do + assert.eq(type(obj_b_d[i]), "number") end end end diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 62f1d73c8..2747481f3 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -75,24 +75,29 @@ test.suite("ProcessSuite", function(suite) process.run({}) end, "process.run requires a non-empty table of arguments") + -- All of these tests intentionally pass values of the incorrect + -- type to `process.run`: this is to ensure that the error + -- handling on the runtime side is correct (for example, does not + -- crash because we expected a table and got a string). + assert.erroreq(function() - process.run({ "echo", "hello" }, { cwd = {} }) + process.run({ "echo", "hello" }, { cwd = {} :: any }) end, "invalid argument #-1 to 'run' (string expected, got table)") assert.erroreq(function() - process.run({ "echo", "hello" }, { stdio = {} }) + process.run({ "echo", "hello" }, { stdio = {} :: any }) end, "invalid argument #-1 to 'run' (string expected, got table)") assert.erroreq(function() - process.run({ "echo", "hello" }, { env = "not-a-table" }) + process.run({ "echo", "hello" }, { env = "not-a-table" :: any }) end, "process option 'env' must be a table") assert.erroreq(function() - process.run({ "echo", "hello" }, "invalid_option") + process.run({ "echo", "hello" }, "invalid_option" :: any) end, "process options must be a table") assert.erroreq(function() - process.run({ "echo", "hello" }, "not-a-table") + process.run({ "echo", "hello" }, "not-a-table" :: any) end, "process options must be a table") assert.erroreq(function() @@ -107,31 +112,36 @@ test.suite("ProcessSuite", function(suite) end) suite:case("process_system_options_error_cases", function(assert) + -- All of these tests intentionally pass values of the incorrect + -- type to `process.system`: this is to ensure that the error + -- handling on the runtime side is correct (for example, does not + -- crash because we expected a table and got a string). + assert.erroreq(function() - process.system() + (process.system :: any)() end, "invalid argument #1 to 'system' (string expected, got nil)") assert.erroreq(function() - process.system({}) + process.system({} :: any) end, "invalid argument #1 to 'system' (string expected, got table)") assert.erroreq(function() - process.system("echo hello", { system = {} }) + process.system("echo hello", { system = {} :: any }) end, "invalid argument #-1 to 'system' (string expected, got table)") assert.erroreq(function() - process.system("echo hello", { cwd = {} }) + process.system("echo hello", { cwd = {} :: any }) end, "invalid argument #-1 to 'system' (string expected, got table)") assert.erroreq(function() - process.system("echo hello", { stdio = {} }) + process.system("echo hello", { stdio = {} :: any }) end, "invalid argument #-1 to 'system' (string expected, got table)") assert.erroreq(function() - process.system("echo hello", { env = "not-a-table" }) + process.system("echo hello", { env = "not-a-table" :: any }) end, "process option 'env' must be a table") assert.erroreq(function() - process.system("echo hello", "invalid_option") + process.system("echo hello", "invalid_option" :: any) end, "process options must be a table") end) end) diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 41fb2f942..5847b0ff5 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -93,9 +93,7 @@ test.suite("syntax_printer", function(suite) return query .findallfromroot(ast, syntaxUtils.isStatTypeAlias) :map(function(ta) - if ta.type.tag == "function" then - return ta.type.returntypes - end + return if ta.type.tag == "function" then ta.type.returntypes else nil end) :replace(function(_) return tp @@ -874,7 +872,9 @@ function foo() return end :flatmap(function(t) local l = {} for _, entry in t.entries do - table.insert(l, entry.key) + if entry.kind ~= "list" then + table.insert(l, entry.key) + end end return l end) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 2901d60b9..74166b159 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -1,25 +1,8 @@ -batteries/cli.luau:50:2-13 -batteries/cli.luau:52:3-14 batteries/cli.luau:144:36-75 batteries/cli.luau:148:6-45 batteries/difftext/myersdiff.luau:76:8-22 -batteries/difftext/printdiff.luau:70:10-77 -batteries/difftext/printdiff.luau:72:10-73 -batteries/difftext/printdiff.luau:74:10-73 -batteries/pp.luau:97:3-14 -batteries/toml.luau:27:14-16 -batteries/toml.luau:40:14-16 -batteries/toml.luau:45:36-38 -batteries/toml.luau:75:26-28 -batteries/toml.luau:133:33-41 -batteries/toml.luau:143:23-25 -batteries/toml.luau:144:25-29 -batteries/toml.luau:146:20-24 -batteries/toml.luau:146:55-59 -batteries/toml.luau:147:24-28 -batteries/toml.luau:166:24-28 +batteries/toml.luau:167:24-28 examples/colorful.luau:1:18-47 -examples/directories.luau:3:16-25 examples/docs/test_module.luau:31:7-12 examples/json.luau:13:7-24 examples/json.luau:13:7-24 @@ -208,8 +191,6 @@ lute/std/libs/syntax/visitor.luau:1073:9-24 lute/std/libs/syntax/visitor.luau:1073:9-24 lute/std/libs/syntax/visitor.luau:1074:22-25 lute/std/libs/syntax/visitor.luau:1074:22-25 -lute/std/libs/test/failure.luau:18:35-38 -lute/std/libs/test/failure.luau:18:35-38 tests/batteries/collections/deque.test.luau:9:13-16 tests/batteries/collections/deque.test.luau:16:13-16 tests/batteries/collections/deque.test.luau:24:13-16 @@ -222,8 +203,6 @@ tests/batteries/collections/deque.test.luau:81:13-16 tests/batteries/collections/deque.test.luau:86:13-16 tests/batteries/collections/deque.test.luau:91:13-16 tests/runtime/crypto.test.luau:66:26-30 -tests/runtime/crypto.test.luau:108:56-68 -tests/runtime/crypto.test.luau:112:72-84 tests/src/packages/package_aware_require/dep/module.luau:1:16-30 tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau:1:16-38 tests/src/require/config_tests/config_ambiguity/requirer.luau:1:8-22 @@ -237,150 +216,12 @@ tests/src/staticrequires/main.luau:3:7-13 tests/std/fs.test.luau:114:13-24 tests/std/fs.test.luau:133:18-20 tests/std/fs.test.luau:142:18-20 -tests/std/fs.test.luau:210:18-20 -tests/std/json.test.luau:79:15-21 -tests/std/json.test.luau:79:15-21 -tests/std/json.test.luau:80:20-26 -tests/std/json.test.luau:80:20-26 -tests/std/json.test.luau:81:16-23 -tests/std/json.test.luau:81:16-23 -tests/std/json.test.luau:81:17-23 -tests/std/json.test.luau:81:17-23 -tests/std/json.test.luau:82:21-27 -tests/std/json.test.luau:82:21-27 -tests/std/json.test.luau:82:21-30 tests/std/process.test.luau:39:18-18 tests/std/process.test.luau:46:18-18 tests/std/process.test.luau:55:17-18 tests/std/process.test.luau:65:18-18 -tests/std/process.test.luau:79:45-46 -tests/std/process.test.luau:83:47-48 -tests/std/process.test.luau:87:45-57 -tests/std/process.test.luau:91:37-52 -tests/std/process.test.luau:95:37-49 -tests/std/process.test.luau:111:4-17 -tests/std/process.test.luau:115:19-20 -tests/std/process.test.luau:119:44-45 -tests/std/process.test.luau:123:41-42 -tests/std/process.test.luau:126:43-44 -tests/std/process.test.luau:130:41-53 -tests/std/process.test.luau:134:33-48 -tests/std/syntax/printer.test.luau:95:10-100 -tests/std/syntax/printer.test.luau:97:7-32 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:825:38-60 -tests/std/syntax/printer.test.luau:877:23-31 tests/std/syntax/query.test.luau:41:7-7 tests/std/syntax/query.test.luau:41:7-7 tests/std/tableext.test.luau:57:18-18 tests/std/tableext.test.luau:62:18-18 tests/std/tableext.test.luau:63:18-18 -tools/luthier.luau:158:2-26 -tools/luthier.luau:197:3-12 -tools/luthier.luau:198:11-25 -tools/luthier.luau:695:3-79 -tools/luthier.luau:695:28-78 -tools/luthier.luau:699:3-75 -tools/luthier.luau:699:28-74 -tools/luthier.luau:710:2-48 -tools/luthier.luau:763:2-11 -tools/luthier.luau:764:10-24 -tools/luthier.luau:894:8-42 diff --git a/tools/luthier.luau b/tools/luthier.luau index fa5df2cf5..e6504fbc9 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -155,7 +155,7 @@ local function getCompiler(): string end local function getConfig(): string - return args:get("config") + return assert(args:get("config")) end local function getBuildDir(): string @@ -194,7 +194,8 @@ local function traverseFileTree(path: string, callback: (string, string) -> ()) callback(currentDir.path, currentDir.relativePath) local contents = fs.listdir(currentDir.path) - table.sort(contents, function(a, b) + -- FIXME(Luau): bidirectional typing + table.sort(contents, function(a: fs.DirectoryEntry, b: fs.DirectoryEntry) return a.name < b.name end) @@ -691,12 +692,14 @@ local function getConfigureArguments() local configArgs = { "-G=Ninja", "-B " .. projectPath, "-DCMAKE_BUILD_TYPE=" .. config, "-DCMAKE_EXPORT_COMPILE_COMMANDS=1" } - if args:get("cxx-compiler") then - table.insert(configArgs, "-DCMAKE_CXX_COMPILER=" .. args:get("cxx-compiler")) + local cxx_compiler = args:get("cxx-compiler") + if cxx_compiler then + table.insert(configArgs, "-DCMAKE_CXX_COMPILER=" .. cxx_compiler) end - if args:get("c-compiler") then - table.insert(configArgs, "-DCMAKE_C_COMPILER=" .. args:get("c-compiler")) + local c_compiler = args:get("c-compiler") + if c_compiler then + table.insert(configArgs, "-DCMAKE_C_COMPILER=" .. c_compiler) end if args:has("enable-sanitizers") then @@ -707,7 +710,9 @@ local function getConfigureArguments() end local function readTuneFile(path: string): Tune - return toml.deserialize(readFileToString(path)) + -- We're promising that the thing we read _actually_ looks like a `Tune`, + -- this could be ill advised. + return toml.deserialize(readFileToString(path)) :: Tune end local function check(exitCode: number) @@ -760,7 +765,8 @@ local function getTuneFilesHash(): string local externPath = projectRelative("extern") local files = fs.listdir(externPath) - table.sort(files, function(a, b) + -- FIXME(Luau): bidirectional typing. + table.sort(files, function(a: fs.DirectoryEntry, b: fs.DirectoryEntry) return a.name < b.name end) @@ -890,8 +896,10 @@ elseif subcommand == "run" or subcommand == "play" then end exitCode = run() -else +elseif subcommand ~= nil then error("Unknown subcommand " .. subcommand) +else + error("No subcommand given.") end if exitCode ~= 0 then From 1d89775ee3eb999b67bb472d8488a34ace2b93e0 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 19 Feb 2026 09:53:56 -0800 Subject: [PATCH 363/642] Separates internal lint types into another file (#832) This ensures that they don't leak into the type definitions generated by `lute setup`. This was previously causing issues because they relied on types related to `.gitignore` parsing, which they were requiring using a relative path which was breaking in the typedef files. --- lute/cli/commands/lint/configutils.luau | 6 ++-- lute/cli/commands/lint/init.luau | 35 ++++++++++++----------- lute/cli/commands/lint/internaltypes.luau | 31 ++++++++++++++++++++ lute/cli/commands/lint/types.luau | 35 +++-------------------- tools/check-faillist.txt | 21 +++++++------- 5 files changed, 66 insertions(+), 62 deletions(-) create mode 100644 lute/cli/commands/lint/internaltypes.luau diff --git a/lute/cli/commands/lint/configutils.luau b/lute/cli/commands/lint/configutils.luau index b29cf0bc4..9a165ed9b 100644 --- a/lute/cli/commands/lint/configutils.luau +++ b/lute/cli/commands/lint/configutils.luau @@ -1,4 +1,4 @@ -local types = require("./types") +local internalTypes = require("./internaltypes") local function assertIsArray(arr: { any }, type: string, err: string) for k, v in arr do @@ -15,8 +15,8 @@ local function assertValidConfig(candidate: any) end end -local function extractConfig(candidate: any): types.LintConfig - local luteLintConfig = if candidate.lute ~= nil then candidate.lute.lint else nil +local function extractConfig(candidate: any): internalTypes.LintConfig + local luteLintConfig = if candidate.lute ~= nil then candidate.lute.lint else nil -- LUAUFIX: I would expect candidate : any to silence this error if luteLintConfig then assertValidConfig(luteLintConfig) return luteLintConfig diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 5de2cf347..07894862c 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -2,6 +2,7 @@ local cli = require("@batteries/cli") local files = require("./lib/files") local fs = require("@std/fs") local json = require("@std/json") +local internalTypes = require("@self/internaltypes") local lsp = require("@self/lsp") local luau = require("@std/luau") local pathLib = require("@std/path") @@ -78,7 +79,7 @@ local function isLintRule(rule: unknown, path: string): boolean return false end - if typeof(rule.lint) ~= "function" then + if typeof(rule.lint) ~= "function" then -- LUAUFIX: rule got refined to { read name : string } so accessing lint errors if VERBOSE then print(`Error loading lint rule from {path}: must return a table with a 'lint' function property`) end @@ -95,14 +96,14 @@ end local function registerRule( rule: types.LintRule, - config: types.LintConfig, + config: internalTypes.LintConfig, rootLocation: string, rules: { types.LintRule }, - ruleConfigs: types.RuleConfigurations, - ruleIgnores: types.RuleIgnores + ruleConfigs: internalTypes.RuleConfigurations, + ruleIgnores: internalTypes.RuleIgnores ) - local ruleConfig: types.RuleConfig = if config.rules ~= nil and config.rules[rule.name] ~= nil - then config.rules[rule.name] + local ruleConfig: internalTypes.RuleConfig = if config.rules ~= nil and config.rules[rule.name] ~= nil + then config.rules[rule.name] :: internalTypes.RuleConfig -- Refinement doesn't support dynamic indexing, so we need to assert the type here else {} if ruleConfig.off then return @@ -117,12 +118,12 @@ end local function loadLintRules( path: string, - config: types.LintConfig, + config: internalTypes.LintConfig, rootLocation: string -): ({ types.LintRule }, types.RuleConfigurations, types.RuleIgnores) +): ({ types.LintRule }, internalTypes.RuleConfigurations, internalTypes.RuleIgnores) assert(fs.exists(path), `Lint rule at path '{path}' does not exist`) - local rules, ruleConfigs: types.RuleConfigurations, ruleIgnores = {}, {}, {} + local rules, ruleConfigs: internalTypes.RuleConfigurations, ruleIgnores = {}, {}, {} local queue: { string } = { path } while #queue > 0 do @@ -156,9 +157,9 @@ local function loadLintRules( end local function loadDefaultRules( - config: types.LintConfig, + config: internalTypes.LintConfig, rootLocation: string -): ({ types.LintRule }, types.RuleConfigurations, types.RuleIgnores) +): ({ types.LintRule }, internalTypes.RuleConfigurations, internalTypes.RuleIgnores) local rules, ruleConfigs, ruleIgnores = {}, {}, {} for _, ruleName in DEFAULT_RULES do local path = `@self/rules/{ruleName}` @@ -274,7 +275,7 @@ local function lintString( source: string, lintRules: { types.LintRule }, inputFilePath: pathLib.path?, - configData: types.RuleConfigData + configData: internalTypes.RuleConfigData ): { types.LintViolation } if VERBOSE then print(`Parsing input {if inputFilePath then `file '{inputFilePath}'` else "string"}`) @@ -380,7 +381,7 @@ local function lintFile( inputFilePath: pathLib.path, lintRules: { types.LintRule }, autofixEnabled: boolean, - configData: types.RuleConfigData + configData: internalTypes.RuleConfigData ): { types.LintViolation } if VERBOSE then print(`Reading input file '{inputFilePath}'`) @@ -424,7 +425,7 @@ local function lintPaths( inputFilePaths: { string }, lintRules: { types.LintRule }, autofixEnabled: boolean, - configData: types.RuleConfigData, + configData: internalTypes.RuleConfigData, ignoreData: parseIgnores.GitignoreData ): { [pathLib.path]: { types.LintViolation } } if VERBOSE then @@ -500,7 +501,7 @@ local function main(...: string) process.exit(1) end - local lintConfig: types.LintConfig = {} + local lintConfig: internalTypes.LintConfig = {} local configPath = args:get("config") if configPath == nil then -- search in calling dir @@ -517,8 +518,8 @@ local function main(...: string) end local lintRules: { types.LintRule } = {} - local ruleConfigurations: types.RuleConfigurations = {} - local ruleIgnoreData: types.RuleIgnores = {} + local ruleConfigurations: internalTypes.RuleConfigurations = {} + local ruleIgnoreData: internalTypes.RuleIgnores = {} local ignores = if lintConfig.ignores ~= nil then lintConfig.ignores else {} local rootLocation = if fs.exists(configPath) diff --git a/lute/cli/commands/lint/internaltypes.luau b/lute/cli/commands/lint/internaltypes.luau new file mode 100644 index 000000000..77e2d8bf1 --- /dev/null +++ b/lute/cli/commands/lint/internaltypes.luau @@ -0,0 +1,31 @@ +local types = require("./types") +local parseIgnores = require("../lib/parseIgnores") +local path = require("@std/path") + +export type RuleConfig = { + ignores: { string }?, + severity: types.severity?, + off: true?, + options: types.RuleOptions?, +} + +export type LintConfig = { + ignores: { path.pathlike }?, -- globs/paths to ignore as we walk lintee files + globals: types.GlobalsConfig?, -- globals to pass down to all rules via context + rules: { + -- paths: { path.pathlike }?, -- paths to local rules to use + [string]: RuleConfig?, + }?, +} + +export type RuleConfigurations = { [string]: RuleConfig } + +export type RuleIgnores = { [string]: parseIgnores.GitignoreData } + +export type RuleConfigData = { + globals: types.GlobalsConfig, + ruleConfigs: RuleConfigurations, + ruleIgnores: RuleIgnores, +} + +return {} diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index 05630492a..d8d6896ba 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -1,6 +1,5 @@ -local syntax = require("@std/syntax") -local parseIgnores = require("../lib/parseIgnores") local path = require("@std/path") +local syntax = require("@std/syntax") export type severity = "error" | "warning" | "info" | "hint" export type tag = "unnecessary" | "deprecated" @@ -19,43 +18,17 @@ export type LintViolation = { tags: { tag }?, } -export type LintRule = { - read lint: (ast: syntax.AstStatBlock, sourcepath: path.path?, context: RuleContext) -> { LintViolation }, - read name: string, -} - -export type RuleConfig = { - ignores: { string }?, - severity: severity?, - off: true?, - options: RuleOptions?, -} - export type GlobalsConfig = { [string]: unknown } export type RuleOptions = { [string]: unknown } -export type LintConfig = { - ignores: { path.pathlike }?, -- globs/paths to ignore as we walk lintee files - globals: GlobalsConfig?, -- globals to pass down to all rules via context - rules: { - -- paths: { path.pathlike }?, -- paths to local rules to use - [string]: RuleConfig?, - }?, -} - export type RuleContext = { globals: GlobalsConfig, options: RuleOptions, } -export type RuleConfigurations = { [string]: RuleConfig } - -export type RuleIgnores = { [string]: parseIgnores.GitignoreData } - -export type RuleConfigData = { - globals: GlobalsConfig, - ruleConfigs: RuleConfigurations, - ruleIgnores: RuleIgnores, +export type LintRule = { + read lint: (ast: syntax.AstStatBlock, sourcepath: path.path?, context: RuleContext) -> { LintViolation }, + read name: string, } return {} diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 74166b159..2c6c18bdd 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -46,17 +46,16 @@ lute/cli/commands/doc/init.luau:315:5-16 lute/cli/commands/doc/init.luau:321:10-24 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 -lute/cli/commands/lint/init.luau:81:12-20 -lute/cli/commands/lint/init.luau:105:8-30 -lute/cli/commands/lint/init.luau:510:15-24 -lute/cli/commands/lint/init.luau:511:56-65 -lute/cli/commands/lint/init.luau:511:56-65 -lute/cli/commands/lint/init.luau:511:56-65 -lute/cli/commands/lint/init.luau:511:56-65 -lute/cli/commands/lint/init.luau:511:56-65 -lute/cli/commands/lint/init.luau:511:56-65 -lute/cli/commands/lint/init.luau:524:36-45 -lute/cli/commands/lint/init.luau:525:39-48 +lute/cli/commands/lint/init.luau:82:12-20 +lute/cli/commands/lint/init.luau:511:15-24 +lute/cli/commands/lint/init.luau:512:56-65 +lute/cli/commands/lint/init.luau:512:56-65 +lute/cli/commands/lint/init.luau:512:56-65 +lute/cli/commands/lint/init.luau:512:56-65 +lute/cli/commands/lint/init.luau:512:56-65 +lute/cli/commands/lint/init.luau:512:56-65 +lute/cli/commands/lint/init.luau:525:36-45 +lute/cli/commands/lint/init.luau:526:39-48 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 From a0e2e926fc22b448ba17ead0f05257a64346aad5 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 19 Feb 2026 13:29:15 -0800 Subject: [PATCH 364/642] Cleans up some Luau AST types (#826) Refactors some Luau AST types to be more consistent. The general naming convention is `Ast`, ie `AstExprBinary`. _Only_ node types which are part of the category in their name should follow this convention, to avoid confusion around whether they are full nodes or not. For example, we previously had `AstTypeFunctionParameter` which I renamed to `AstFunctionTypeParameter` and `AstExprTableItemList` which I renamed to `AstTableExprListItem`. I also simplified some of the top level AST type definitions, since our tagged union inference is stronger now. --- definitions/luau.luau | 38 ++++++++-------- lute/std/libs/syntax/init.luau | 18 ++++---- lute/std/libs/syntax/types.luau | 18 ++++---- lute/std/libs/syntax/utils/init.luau | 2 +- lute/std/libs/syntax/visitor.luau | 18 ++++---- tools/check-faillist.txt | 68 +++++++++++++++++++++++++++- 6 files changed, 112 insertions(+), 50 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index a7bbefe80..b3c9ee726 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -64,6 +64,7 @@ export type AstLocal = { export type AstExprGroup = { location: span, + kind: "expr", tag: "group", openparens: Token<"(">, expression: AstExpr, @@ -166,7 +167,7 @@ export type AstExprFunction = { } -- helper types for items contained in an AstExprTable, not actually AstExprs themselves -export type AstExprTableItemList = { +export type AstTableExprListItem = { location: span, kind: "list", value: AstExpr, @@ -174,7 +175,7 @@ export type AstExprTableItemList = { istableitem: true, } -export type AstExprTableItemRecord = { +export type AstTableExprRecordItem = { location: span, kind: "record", key: Token, @@ -184,7 +185,7 @@ export type AstExprTableItemRecord = { istableitem: true, } -export type AstExprTableItemGeneral = { +export type AstTableExprGeneralItem = { location: span, kind: "general", indexeropen: Token<"[">, @@ -196,14 +197,14 @@ export type AstExprTableItemGeneral = { istableitem: true, } -export type AstExprTableItem = AstExprTableItemList | AstExprTableItemRecord | AstExprTableItemGeneral +export type AstTableExprItem = AstTableExprListItem | AstTableExprRecordItem | AstTableExprGeneralItem export type AstExprTable = { location: span, kind: "expr", tag: "table", openbrace: Token<"{">, - entries: { AstExprTableItem }, + entries: { AstTableExprItem }, closebrace: Token<"}">, } @@ -262,7 +263,7 @@ export type AstExprIfElse = { elseexpr: AstExpr, } -export type AstExpr = { kind: "expr" } & ( +export type AstExpr = | AstExprGroup | AstExprConstantNil | AstExprConstantBool @@ -282,7 +283,6 @@ export type AstExpr = { kind: "expr" } & ( | AstExprInterpString | AstExprTypeAssertion | AstExprIfElse -) export type AstStatBlock = { location: span, @@ -459,7 +459,7 @@ export type AstStatTypeFunction = { body: AstExprFunction, } -export type AstStat = { kind: "stat" } & ( +export type AstStat = | AstStatBlock | AstStatDo | AstStatIf @@ -478,7 +478,6 @@ export type AstStat = { kind: "stat" } & ( | AstStatLocalFunction | AstStatTypeAlias | AstStatTypeFunction -) export type AstGenericType = { tag: "generic", @@ -566,7 +565,7 @@ export type AstTypeArray = { closebrace: Token<"}">, } -export type AstTypeTableItemIndexer = { +export type AstTableTypeItemIndexer = { kind: "indexer", access: Token<"read" | "write">?, indexeropen: Token<"[">, @@ -577,7 +576,7 @@ export type AstTypeTableItemIndexer = { separator: Token<"," | ";">?, } -export type AstTypeTableItemStringProperty = { +export type AstTableTypeItemStringProperty = { kind: "stringproperty", access: Token<"read" | "write">?, indexeropen: Token<"[">, @@ -588,7 +587,7 @@ export type AstTypeTableItemStringProperty = { separator: Token<"," | ";">?, } -export type AstTypeTableItemProperty = { +export type AstTableTypeItemProperty = { kind: "property", access: Token<"read" | "write">?, key: Token, @@ -597,18 +596,18 @@ export type AstTypeTableItemProperty = { separator: Token<"," | ";">?, } -export type AstTypeTableItem = AstTypeTableItemIndexer | AstTypeTableItemStringProperty | AstTypeTableItemProperty +export type AstTableTypeItem = AstTableTypeItemIndexer | AstTableTypeItemStringProperty | AstTableTypeItemProperty export type AstTypeTable = { location: span, kind: "type", tag: "table", openbrace: Token<"{">, - entries: { AstTypeTableItem }, + entries: { AstTableTypeItem }, closebrace: Token<"}">, } -export type AstTypeFunctionParameter = { +export type AstFunctionTypeParameter = { location: span, name: Token?, colon: Token<":">?, @@ -625,14 +624,14 @@ export type AstTypeFunction = { genericpacks: Punctuated?, closegenerics: Token<">">?, openparens: Token<"(">, - parameters: Punctuated, + parameters: Punctuated, vararg: AstTypePack?, closeparens: Token<")">, returnarrow: Token<"->">, returntypes: AstTypePack, } -export type AstType = { kind: "type" } & ( +export type AstType = | AstTypeReference | AstTypeSingletonBool | AstTypeSingletonString @@ -644,7 +643,6 @@ export type AstType = { kind: "type" } & ( | AstTypeArray | AstTypeTable | AstTypeFunction -) export type AstTypePackExplicit = { location: span, @@ -667,9 +665,9 @@ export type AstTypePackVariadic = { type: AstType, } -export type AstTypePack = { kind: "typepack" } & (AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic) +export type AstTypePack = AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic -export type AstNode = { location: span } & (AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute) +export type AstNode = AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute export type ParseResult = { root: AstStatBlock, diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index d6d1cdf08..a5758d94b 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -63,13 +63,13 @@ export type AstExprIndexExpr = types.AstExprIndexExpr export type AstExprFunction = types.AstExprFunction -export type AstExprTableItemList = types.AstExprTableItemList +export type AstTableExprListItem = types.AstTableExprListItem -export type AstExprTableItemRecord = types.AstExprTableItemRecord +export type AstTableExprRecordItem = types.AstTableExprRecordItem -export type AstExprTableItemGeneral = types.AstExprTableItemGeneral +export type AstTableExprGeneralItem = types.AstTableExprGeneralItem -export type AstExprTableItem = types.AstExprTableItem +export type AstTableExprItem = types.AstTableExprItem export type AstExprTable = types.AstExprTable @@ -151,17 +151,17 @@ export type AstTypeIntersection = types.AstTypeIntersection export type AstTypeArray = types.AstTypeArray -export type AstTypeTableItemIndexer = types.AstTypeTableItemIndexer +export type AstTableTypeItemIndexer = types.AstTableTypeItemIndexer -export type AstTypeTableItemStringProperty = types.AstTypeTableItemStringProperty +export type AstTableTypeItemStringProperty = types.AstTableTypeItemStringProperty -export type AstTypeTableItemProperty = types.AstTypeTableItemProperty +export type AstTableTypeItemProperty = types.AstTableTypeItemProperty -export type AstTypeTableItem = types.AstTypeTableItem +export type AstTableTypeItem = types.AstTableTypeItem export type AstTypeTable = types.AstTypeTable -export type AstTypeFunctionParameter = types.AstTypeFunctionParameter +export type AstFunctionTypeParameter = types.AstFunctionTypeParameter export type AstTypeFunction = types.AstTypeFunction diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index a6cc020a7..409246115 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -43,13 +43,13 @@ export type AstExprIndexExpr = luau.AstExprIndexExpr export type AstExprFunction = luau.AstExprFunction -export type AstExprTableItemList = luau.AstExprTableItemList +export type AstTableExprListItem = luau.AstTableExprListItem -export type AstExprTableItemRecord = luau.AstExprTableItemRecord +export type AstTableExprRecordItem = luau.AstTableExprRecordItem -export type AstExprTableItemGeneral = luau.AstExprTableItemGeneral +export type AstTableExprGeneralItem = luau.AstTableExprGeneralItem -export type AstExprTableItem = luau.AstExprTableItem +export type AstTableExprItem = luau.AstTableExprItem export type AstExprTable = luau.AstExprTable @@ -131,17 +131,17 @@ export type AstTypeIntersection = luau.AstTypeIntersection export type AstTypeArray = luau.AstTypeArray -export type AstTypeTableItemIndexer = luau.AstTypeTableItemIndexer +export type AstTableTypeItemIndexer = luau.AstTableTypeItemIndexer -export type AstTypeTableItemStringProperty = luau.AstTypeTableItemStringProperty +export type AstTableTypeItemStringProperty = luau.AstTableTypeItemStringProperty -export type AstTypeTableItemProperty = luau.AstTypeTableItemProperty +export type AstTableTypeItemProperty = luau.AstTableTypeItemProperty -export type AstTypeTableItem = luau.AstTypeTableItem +export type AstTableTypeItem = luau.AstTableTypeItem export type AstTypeTable = luau.AstTypeTable -export type AstTypeFunctionParameter = luau.AstTypeFunctionParameter +export type AstFunctionTypeParameter = luau.AstFunctionTypeParameter export type AstTypeFunction = luau.AstTypeFunction diff --git a/lute/std/libs/syntax/utils/init.luau b/lute/std/libs/syntax/utils/init.luau index 96d1b24e8..f0a2a88da 100644 --- a/lute/std/libs/syntax/utils/init.luau +++ b/lute/std/libs/syntax/utils/init.luau @@ -58,7 +58,7 @@ function utils.isExprTable(n: types.AstNode): types.AstExprTable? return if n.kind == "expr" and n.tag == "table" then n else nil end -function utils.isExprTableItem(n: types.AstNode): types.AstExprTableItem? +function utils.isTableExprItem(n: types.AstNode): types.AstTableExprItem? return if n.istableitem then n else nil end diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index a12584cbb..c0f1f15cc 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -44,7 +44,7 @@ export type Visitor = { visitExprFunction: (types.AstExprFunction) -> boolean, visitExprFunctionEnd: (types.AstExprFunction) -> (), visitExprInstantiate: (types.AstExprInstantiate) -> boolean, - visitExprTableItem: (types.AstExprTableItem) -> boolean, + visitTableExprItem: (types.AstTableExprItem) -> boolean, visitExprTable: (types.AstExprTable) -> boolean, visitExprIndexName: (types.AstExprIndexName) -> boolean, visitExprIndexExpr: (types.AstExprIndexExpr) -> boolean, @@ -119,7 +119,7 @@ local defaultVisitor: Visitor = { visitExprFunction = alwaysVisit :: any, visitExprFunctionEnd = alwaysVisit :: any, visitExprInstantiate = alwaysVisit, - visitExprTableItem = alwaysVisit :: any, + visitTableExprItem = alwaysVisit :: any, visitExprTable = alwaysVisit :: any, visitExprIndexName = alwaysVisit :: any, visitExprIndexExpr = alwaysVisit :: any, @@ -555,8 +555,8 @@ local function visitStatTypeFunction(node: types.AstStatTypeFunction, visitor: V end end -local function visitExprTableItem(node: types.AstExprTableItem, visitor: Visitor) - if visitor.visitExprTableItem(node) then +local function visitTableExprItem(node: types.AstTableExprItem, visitor: Visitor) + if visitor.visitTableExprItem(node) then if node.kind == "list" then visitExpr(node.value, visitor) elseif node.kind == "record" then @@ -583,7 +583,7 @@ local function visitExprTable(node: types.AstExprTable, visitor: Visitor) if visitor.visitExprTable(node) then visitToken(node.openbrace, visitor) for _, item in node.entries do - visitExprTableItem(item, visitor) + visitTableExprItem(item, visitor) end visitToken(node.closebrace, visitor) end @@ -771,7 +771,7 @@ local function visitTypeTable(node: types.AstTypeTable, visitor: Visitor) end end -local function visitTypeFunctionParameter(node: types.AstTypeFunctionParameter, visitor) +local function visitFunctionTypeParameter(node: types.AstFunctionTypeParameter, visitor) if node.name then visitToken(node.name, visitor) end @@ -796,7 +796,7 @@ local function visitTypeFunction(node: types.AstTypeFunction, visitor: Visitor) visitToken(node.closegenerics, visitor) end visitToken(node.openparens, visitor) - visitPunctuated(node.parameters, visitor, visitTypeFunctionParameter) + visitPunctuated(node.parameters, visitor, visitFunctionTypeParameter) if node.vararg then visitTypePack(node.vararg, visitor) end @@ -1015,7 +1015,7 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visit(s) end, visitExprInstantiate = visit, - visitExprTableItem = visit, + visitTableExprItem = visit, visitExprTable = visit, visitExprIndexName = visit, visitExprIndexExpr = visit, @@ -1071,7 +1071,7 @@ local function visit(node: types.AstNode, visitor: Visitor) elseif node.istoken then visitToken(node, visitor) elseif node.istableitem then - visitExprTableItem(node, visitor) + visitTableExprItem(node, visitor) else exhaustiveMatch(node.kind) end diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 2c6c18bdd..b1a066052 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -124,6 +124,10 @@ lute/std/libs/syntax/query.luau:136:59-61 lute/std/libs/syntax/query.luau:136:59-61 lute/std/libs/syntax/query.luau:136:59-61 lute/std/libs/syntax/query.luau:136:59-61 +lute/std/libs/syntax/query.luau:142:13-28 +lute/std/libs/syntax/query.luau:142:13-28 +lute/std/libs/syntax/query.luau:142:13-28 +lute/std/libs/syntax/query.luau:142:13-28 lute/std/libs/syntax/query.luau:143:9-20 lute/std/libs/syntax/query.luau:143:9-20 lute/std/libs/syntax/query.luau:145:13-28 @@ -174,8 +178,6 @@ lute/std/libs/syntax/visitor.luau:713:3-12 lute/std/libs/syntax/visitor.luau:713:3-12 lute/std/libs/syntax/visitor.luau:1018:25-29 lute/std/libs/syntax/visitor.luau:1018:25-29 -lute/std/libs/syntax/visitor.luau:1022:21-25 -lute/std/libs/syntax/visitor.luau:1022:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 @@ -219,6 +221,68 @@ tests/std/process.test.luau:39:18-18 tests/std/process.test.luau:46:18-18 tests/std/process.test.luau:55:17-18 tests/std/process.test.luau:65:18-18 +tests/std/syntax/printer.test.luau:32:11-31 +tests/std/syntax/printer.test.luau:46:11-31 +tests/std/syntax/printer.test.luau:60:11-31 +tests/std/syntax/printer.test.luau:74:11-100 +tests/std/syntax/printer.test.luau:93:11-100 +tests/std/syntax/printer.test.luau:109:11-100 +tests/std/syntax/printer.test.luau:126:11-31 +tests/std/syntax/printer.test.luau:138:11-31 +tests/std/syntax/printer.test.luau:150:11-31 +tests/std/syntax/printer.test.luau:162:11-31 +tests/std/syntax/printer.test.luau:175:11-31 +tests/std/syntax/printer.test.luau:187:11-31 +tests/std/syntax/printer.test.luau:199:11-31 +tests/std/syntax/printer.test.luau:211:11-31 +tests/std/syntax/printer.test.luau:223:11-31 +tests/std/syntax/printer.test.luau:235:11-31 +tests/std/syntax/printer.test.luau:247:11-31 +tests/std/syntax/printer.test.luau:259:11-31 +tests/std/syntax/printer.test.luau:271:11-31 +tests/std/syntax/printer.test.luau:283:11-31 +tests/std/syntax/printer.test.luau:295:11-31 +tests/std/syntax/printer.test.luau:307:11-31 +tests/std/syntax/printer.test.luau:319:11-31 +tests/std/syntax/printer.test.luau:331:11-31 +tests/std/syntax/printer.test.luau:344:11-31 +tests/std/syntax/printer.test.luau:361:11-31 +tests/std/syntax/printer.test.luau:376:11-31 +tests/std/syntax/printer.test.luau:391:11-31 +tests/std/syntax/printer.test.luau:406:11-31 +tests/std/syntax/printer.test.luau:421:11-31 +tests/std/syntax/printer.test.luau:434:11-31 +tests/std/syntax/printer.test.luau:447:11-31 +tests/std/syntax/printer.test.luau:460:11-31 +tests/std/syntax/printer.test.luau:475:11-31 +tests/std/syntax/printer.test.luau:490:11-31 +tests/std/syntax/printer.test.luau:504:11-31 +tests/std/syntax/printer.test.luau:518:11-31 +tests/std/syntax/printer.test.luau:533:11-31 +tests/std/syntax/printer.test.luau:548:11-31 +tests/std/syntax/printer.test.luau:561:11-31 +tests/std/syntax/printer.test.luau:574:11-31 +tests/std/syntax/printer.test.luau:588:11-31 +tests/std/syntax/printer.test.luau:601:11-31 +tests/std/syntax/printer.test.luau:614:11-31 +tests/std/syntax/printer.test.luau:627:11-31 +tests/std/syntax/printer.test.luau:640:11-31 +tests/std/syntax/printer.test.luau:653:11-31 +tests/std/syntax/printer.test.luau:666:11-31 +tests/std/syntax/printer.test.luau:679:11-31 +tests/std/syntax/printer.test.luau:692:11-31 +tests/std/syntax/printer.test.luau:705:11-31 +tests/std/syntax/printer.test.luau:718:11-31 +tests/std/syntax/printer.test.luau:731:11-31 +tests/std/syntax/printer.test.luau:744:11-31 +tests/std/syntax/printer.test.luau:757:11-31 +tests/std/syntax/printer.test.luau:770:11-31 +tests/std/syntax/printer.test.luau:783:11-31 +tests/std/syntax/printer.test.luau:796:11-31 +tests/std/syntax/printer.test.luau:809:11-31 +tests/std/syntax/printer.test.luau:823:11-31 +tests/std/syntax/printer.test.luau:839:11-100 +tests/std/syntax/printer.test.luau:870:11-100 tests/std/syntax/query.test.luau:41:7-7 tests/std/syntax/query.test.luau:41:7-7 tests/std/tableext.test.luau:57:18-18 From 9defcd034fb215bd79f5ddeae561b7555a1a58de Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:14:05 -0500 Subject: [PATCH 365/642] Lute Lint: ensure `rootLocation` is always absolute (#836) When running `lute lint ./ -c ./config.lint.luau` locally, Vighnesh ran into an issue with the configs ignore logic: ``` @std/path/posix/init.luau:209: Cannot compute relative path between absolute and relative paths stacktrace: [C] function error @std/path/posix/init.luau:209 function relative @std/path/init.luau:83 function relative @cli/lib/parseIgnores.luau:165 function isIgnored @cli/lint/init.luau:448 function lintPaths @cli/lint/init.luau:588 function main @cli/lint/init.luau:611 ``` Since Vighnesh passed a relative path for the `-c` arg, the `configPath` and `rootLocation` derived from it in the main linting flow were both using relative paths, thus our error. As the linter walks the fs, it checks each against our `ignoreData` to determine if the current path should be ignored / linted. All of these walked paths are absolute, which is ensured by [this logic](https://github.com/luau-lang/lute/blob/primary/lute/cli/commands/lib/files.luau#L55-L57) in `getSourceFiles` (where we derive lintee filepaths from). Thus, if `rootLocation` is relative, an error will be thrown. To avoid this, we can normalize the `rootLocation` to be absolute **always** --- lute/cli/commands/lint/init.luau | 6 ++- tests/cli/lint.test.luau | 68 ++++++++++++++++++++++++++++++++ tools/check-faillist.txt | 18 ++++----- 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 07894862c..1e06c4418 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -507,7 +507,11 @@ local function main(...: string) -- search in calling dir local cwd = process.cwd() configPath = pathLib.join(cwd, ".config.luau") + elseif fs.type(configPath) ~= "file" then + print(`Error: Configuration path must point to a file, not {fs.type(configPath)}`) + process.exit(1) end + if fs.exists(configPath) then local success, loadedConfig = pcall(luau.loadbypath, configPath) if success then @@ -523,7 +527,7 @@ local function main(...: string) local ignores = if lintConfig.ignores ~= nil then lintConfig.ignores else {} local rootLocation = if fs.exists(configPath) - then pathLib.format(pathLib.dirname(configPath)) + then pathLib.format(pathLib.resolve(pathLib.dirname(configPath))) else pathLib.format(process.cwd()) local ignoreData = parseIgnores.parseIgnoreContents(rootLocation, ignores) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index a3117bdcc..455574265 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -2506,6 +2506,74 @@ local t = { [0] = 1 } assert.strcontains(result.stdout, "almost_swapped") end) + suite:case("lute lint handles ignores with relative config path", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + local ignoredFile = path.join(testSubDir, "ignored.luau") + local notIgnoredFile = path.join(testSubDir, "valid.luau") + local violatingContents = [[ + a = b + b = a + c = 1 / 0 + ]] + fs.writestringtofile(ignoredFile, violatingContents) + fs.writestringtofile(notIgnoredFile, violatingContents) + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ + return { + lute = { + lint = { + ignores = { "**/ignored.luau" } + } + } + } + ]] + fs.writestringtofile(configFile, configContents) + + -- Pass config as a relative path, setting cwd to ensure relative config path is correct + local result = process.run({ lutePath, "lint", "-c", ".config.luau", "." }, { + cwd = path.format(testSubDir), + }) + assert.eq(result.exitcode, 1) -- errors but ONLY from valid.luau + assert.strnotcontains(result.stdout, "ignored.luau") + assert.strcontains(result.stdout, "valid.luau") + end) + + suite:case("lute lint handles ignores with absolute config path", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + local ignoredFile = path.join(testSubDir, "ignored.luau") + local notIgnoredFile = path.join(testSubDir, "valid.luau") + local violatingContents = [[ + a = b + b = a + c = 1 / 0 + ]] + fs.writestringtofile(ignoredFile, violatingContents) + fs.writestringtofile(notIgnoredFile, violatingContents) + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = [[ + return { + lute = { + lint = { + ignores = { "**/ignored.luau" } + } + } + } + ]] + fs.writestringtofile(configFile, configContents) + + -- Pass config as an absolute path, with cwd set to a different directory (homedir) + local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) + assert.eq(result.exitcode, 1) -- errors but ONLY from valid.luau + assert.strnotcontains(result.stdout, "ignored.luau") + assert.strcontains(result.stdout, "valid.luau") + end) + suite:case("lute lint passes context through to rules", function(assert) -- custom rule that returns violating with globals and options in the message local customRuleContents = [[ diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index b1a066052..eb97f7558 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -47,15 +47,15 @@ lute/cli/commands/doc/init.luau:321:10-24 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/init.luau:82:12-20 -lute/cli/commands/lint/init.luau:511:15-24 -lute/cli/commands/lint/init.luau:512:56-65 -lute/cli/commands/lint/init.luau:512:56-65 -lute/cli/commands/lint/init.luau:512:56-65 -lute/cli/commands/lint/init.luau:512:56-65 -lute/cli/commands/lint/init.luau:512:56-65 -lute/cli/commands/lint/init.luau:512:56-65 -lute/cli/commands/lint/init.luau:525:36-45 -lute/cli/commands/lint/init.luau:526:39-48 +lute/cli/commands/lint/init.luau:515:15-24 +lute/cli/commands/lint/init.luau:516:56-65 +lute/cli/commands/lint/init.luau:516:56-65 +lute/cli/commands/lint/init.luau:516:56-65 +lute/cli/commands/lint/init.luau:516:56-65 +lute/cli/commands/lint/init.luau:516:56-65 +lute/cli/commands/lint/init.luau:516:56-65 +lute/cli/commands/lint/init.luau:529:36-45 +lute/cli/commands/lint/init.luau:530:55-64 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 From 36bd95fc98f156bad8645565f64d6581d667bd92 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 20 Feb 2026 10:00:51 -0800 Subject: [PATCH 366/642] Updates AST nodes to only have read props (#837) This is in preparation for the serializer actually freezing AST node tables --- definitions/luau.luau | 835 +++++++++++++++--------------- examples/transformer.luau | 95 ---- lute/std/libs/syntax/printer.luau | 5 +- lute/std/libs/syntax/visitor.luau | 12 +- tests/cli/transform.test.luau | 24 - tools/check-faillist.txt | 35 -- 6 files changed, 436 insertions(+), 570 deletions(-) delete mode 100644 examples/transformer.luau diff --git a/definitions/luau.luau b/definitions/luau.luau index b3c9ee726..008152d02 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -3,13 +3,13 @@ local luau = {} -- this is a userdata, not a table, but it has this interface type spandata = { - beginline: number, - begincolumn: number, - endline: number, - endcolumn: number, + read beginline: number, + read begincolumn: number, + read endline: number, + read endcolumn: number, } type spanMT = { - __lt: (a: spandata, b: spandata) -> boolean, + read __lt: (a: spandata, b: spandata) -> boolean, } export type span = setmetatable @@ -20,247 +20,256 @@ function luau.span.create(tbl: { beginline: number, begincolumn: number, endline end export type Whitespace = { - tag: "whitespace", - location: span, - text: string, + read tag: "whitespace", + read location: span, + read text: string, } export type SingleLineComment = { - tag: "comment", - location: span, - text: string, + read tag: "comment", + read location: span, + read text: string, } export type MultiLineComment = { - tag: "blockcomment", - location: span, - text: string, + read tag: "blockcomment", + read location: span, + read text: string, -- TODO: depth: number, } export type Trivia = Whitespace | SingleLineComment | MultiLineComment export type Token = { - leadingtrivia: { Trivia }, - location: span, - text: Kind, - trailingtrivia: { Trivia }, - istoken: true, + read leadingtrivia: { Trivia }, + read location: span, + read text: Kind, + read trailingtrivia: { Trivia }, + read istoken: true, } -export type Eof = Token<""> & { tag: "eof" } +export type Eof = Token<""> & { read tag: "eof" } -export type Pair = { node: T, separator: Token? } +export type Pair = { read node: T, read separator: Token? } export type Punctuated = { Pair } export type AstLocal = { - location: span, - kind: "local", - name: Token, - colon: Token<":">?, - annotation: AstType?, - shadows: AstLocal?, + read location: span, + read kind: "local", + read name: Token, + read colon: Token<":">?, + read annotation: AstType?, + read shadows: AstLocal?, } export type AstExprGroup = { - location: span, - kind: "expr", - tag: "group", - openparens: Token<"(">, - expression: AstExpr, - closeparens: Token<")">, + read location: span, + read kind: "expr", + read tag: "group", + read openparens: Token<"(">, + read expression: AstExpr, + read closeparens: Token<")">, } -export type AstExprConstantNil = Token<"nil"> & { location: span, kind: "expr", tag: "nil" } +export type AstExprConstantNil = Token<"nil"> & { read location: span, read kind: "expr", read tag: "nil" } -export type AstExprConstantBool = - Token<"true" | "false"> - & { location: span, kind: "expr", tag: "boolean", value: boolean } +export type AstExprConstantBool = Token<"true" | "false"> & { + read location: span, + read kind: "expr", + read tag: "boolean", + read value: boolean, +} + +export type AstExprConstantNumber = Token & { + read location: span, + read kind: "expr", + read tag: "number", + read value: number, +} -export type AstExprConstantNumber = Token & { location: span, kind: "expr", tag: "number", value: number } export type AstExprConstantString = Token & { - location: span, - kind: "expr", - tag: "string", - quotestyle: "single" | "double" | "block" | "interp", - blockdepth: number, + read location: span, + read kind: "expr", + read tag: "string", + read quotestyle: "single" | "double" | "block" | "interp", + read blockdepth: number, } export type AstExprLocal = { - location: span, - kind: "expr", - tag: "local", - token: Token, - ["local"]: AstLocal, - upvalue: boolean, + read location: span, + read kind: "expr", + read tag: "local", + read token: Token, + read ["local"]: AstLocal, + read upvalue: boolean, } -export type AstExprGlobal = { location: span, kind: "expr", tag: "global", name: Token } +export type AstExprGlobal = { read location: span, read kind: "expr", read tag: "global", read name: Token } -export type AstExprVarargs = Token<"..."> & { location: span, kind: "expr", tag: "vararg" } +export type AstExprVarargs = Token<"..."> & { read location: span, read kind: "expr", read tag: "vararg" } export type AstExprCall = { - location: span, - kind: "expr", - tag: "call", - func: AstExpr, - openparens: Token<"(">?, - arguments: Punctuated, - closeparens: Token<")">?, - self: boolean, - argLocation: span, + read location: span, + read kind: "expr", + read tag: "call", + read func: AstExpr, + read openparens: Token<"(">?, + read arguments: Punctuated, + read closeparens: Token<")">?, + read self: boolean, + read argLocation: span, } export type AstExprInstantiate = { - location: span, - kind: "expr", - tag: "instantiate", - expr: AstExpr, - leftarrow1: Token<"<">, - leftarrow2: Token<"<">, - typearguments: Punctuated, - rightarrow1: Token<">">, - rightarrow2: Token<">">, + read location: span, + read kind: "expr", + read tag: "instantiate", + read expr: AstExpr, + read leftarrow1: Token<"<">, + read leftarrow2: Token<"<">, + read typearguments: Punctuated, + read rightarrow1: Token<">">, + read rightarrow2: Token<">">, } export type AstExprIndexName = { - location: span, - kind: "expr", - tag: "indexname", - expression: AstExpr, - accessor: Token<"." | ":">, - index: Token, - indexlocation: span, + read location: span, + read kind: "expr", + read tag: "indexname", + read expression: AstExpr, + read accessor: Token<"." | ":">, + read index: Token, + read indexlocation: span, } export type AstExprIndexExpr = { - location: span, - kind: "expr", - tag: "index", - expression: AstExpr, - openbrackets: Token<"[">, - index: AstExpr, - closebrackets: Token<"]">, + read location: span, + read kind: "expr", + read tag: "index", + read expression: AstExpr, + read openbrackets: Token<"[">, + read index: AstExpr, + read closebrackets: Token<"]">, } export type AstExprFunction = { - location: span, - kind: "expr", - tag: "function", - attributes: { AstAttribute }, - functionkeyword: Token<"function">, - opengenerics: Token<"<">?, - generics: Punctuated?, - genericpacks: Punctuated?, - closegenerics: Token<">">?, - openparens: Token<"(">, - self: AstLocal?, - parameters: Punctuated, - vararg: Token<"...">?, - varargcolon: Token<":">?, - varargannotation: AstTypePack?, - closeparens: Token<")">, - returnspecifier: Token<":">?, - returnannotation: AstTypePack?, - body: AstStatBlock, - endkeyword: Token<"end">, + read location: span, + read kind: "expr", + read tag: "function", + read attributes: { AstAttribute }, + read functionkeyword: Token<"function">, + read opengenerics: Token<"<">?, + read generics: Punctuated?, + read genericpacks: Punctuated?, + read closegenerics: Token<">">?, + read openparens: Token<"(">, + read self: AstLocal?, + read parameters: Punctuated, + read vararg: Token<"...">?, + read varargcolon: Token<":">?, + read varargannotation: AstTypePack?, + read closeparens: Token<")">, + read returnspecifier: Token<":">?, + read returnannotation: AstTypePack?, + read body: AstStatBlock, + read endkeyword: Token<"end">, } -- helper types for items contained in an AstExprTable, not actually AstExprs themselves export type AstTableExprListItem = { - location: span, - kind: "list", - value: AstExpr, - separator: Token<"," | ";">?, - istableitem: true, + read location: span, + read kind: "list", + read value: AstExpr, + read separator: Token<"," | ";">?, + read istableitem: true, } export type AstTableExprRecordItem = { - location: span, - kind: "record", - key: Token, - equals: Token<"=">, - value: AstExpr, - separator: Token<"," | ";">?, - istableitem: true, + read location: span, + read kind: "record", + read key: Token, + read equals: Token<"=">, + read value: AstExpr, + read separator: Token<"," | ";">?, + read istableitem: true, } export type AstTableExprGeneralItem = { - location: span, - kind: "general", - indexeropen: Token<"[">, - key: AstExpr, - indexerclose: Token<"]">, - equals: Token<"=">, - value: AstExpr, - separator: Token<"," | ";">?, - istableitem: true, + read location: span, + read kind: "general", + read indexeropen: Token<"[">, + read key: AstExpr, + read indexerclose: Token<"]">, + read equals: Token<"=">, + read value: AstExpr, + read separator: Token<"," | ";">?, + read istableitem: true, } export type AstTableExprItem = AstTableExprListItem | AstTableExprRecordItem | AstTableExprGeneralItem export type AstExprTable = { - location: span, - kind: "expr", - tag: "table", - openbrace: Token<"{">, - entries: { AstTableExprItem }, - closebrace: Token<"}">, + read location: span, + read kind: "expr", + read tag: "table", + read openbrace: Token<"{">, + read entries: { AstTableExprItem }, + read closebrace: Token<"}">, } export type AstExprUnary = { - location: span, - kind: "expr", - tag: "unary", - operator: Token<"not" | "-" | "#">, - operand: AstExpr, + read location: span, + read kind: "expr", + read tag: "unary", + read operator: Token<"not" | "-" | "#">, + read operand: AstExpr, } export type AstExprBinary = { - location: span, - kind: "expr", - tag: "binary", - lhsoperand: AstExpr, - operator: Token, -- TODO: enforce token type - rhsoperand: AstExpr, + read location: span, + read kind: "expr", + read tag: "binary", + read lhsoperand: AstExpr, + read operator: Token, + read rhsoperand: AstExpr, } export type AstExprInterpString = { - location: span, - kind: "expr", - tag: "interpolatedstring", - strings: { Token }, - expressions: { AstExpr }, + read location: span, + read kind: "expr", + read tag: "interpolatedstring", + read strings: { Token }, + read expressions: { AstExpr }, } export type AstExprTypeAssertion = { - location: span, - kind: "expr", - tag: "cast", - operand: AstExpr, - operator: Token<"::">, - annotation: AstType, + read location: span, + read kind: "expr", + read tag: "cast", + read operand: AstExpr, + read operator: Token<"::">, + read annotation: AstType, } -- helper type for elseif clauses of an if-else expression, not actually an AstExpr itself export type AstElseIfExpr = { - elseifkeyword: Token<"elseif">, - condition: AstExpr, - thenkeyword: Token<"then">, - thenexpr: AstExpr, + read elseifkeyword: Token<"elseif">, + read condition: AstExpr, + read thenkeyword: Token<"then">, + read thenexpr: AstExpr, } export type AstExprIfElse = { - location: span, - kind: "expr", - tag: "conditional", - ifkeyword: Token<"if">, - condition: AstExpr, - thenkeyword: Token<"then">, - thenexpr: AstExpr, - elseifs: { AstElseIfExpr }, - elsekeyword: Token<"else">, - elseexpr: AstExpr, + read location: span, + read kind: "expr", + read tag: "conditional", + read ifkeyword: Token<"if">, + read condition: AstExpr, + read thenkeyword: Token<"then">, + read thenexpr: AstExpr, + read elseifs: { AstElseIfExpr }, + read elsekeyword: Token<"else">, + read elseexpr: AstExpr, } export type AstExpr = @@ -285,178 +294,183 @@ export type AstExpr = | AstExprIfElse export type AstStatBlock = { - location: span, - kind: "stat", - tag: "block", - statements: { AstStat }, + read location: span, + read kind: "stat", + read tag: "block", + read statements: { AstStat }, } export type AstStatDo = { - location: span, - kind: "stat", - tag: "do", - dokeyword: Token<"do">, - body: AstStatBlock, - endkeyword: Token<"end">, + read location: span, + read kind: "stat", + read tag: "do", + read dokeyword: Token<"do">, + read body: AstStatBlock, + read endkeyword: Token<"end">, } export type AstElseIfStat = { - elseifkeyword: Token<"elseif">, - condition: AstExpr, - thenkeyword: Token<"then">, - thenblock: AstStatBlock, + read elseifkeyword: Token<"elseif">, + read condition: AstExpr, + read thenkeyword: Token<"then">, + read thenblock: AstStatBlock, } export type AstStatIf = { - location: span, - kind: "stat", - tag: "conditional", - ifkeyword: Token<"if">, - condition: AstExpr, - thenkeyword: Token<"then">, - thenblock: AstStatBlock, - elseifs: { AstElseIfStat }, - elsekeyword: Token<"else">?, -- TODO: this could be elseif! - elseblock: AstStatBlock?, - endkeyword: Token<"end">, + read location: span, + read kind: "stat", + read tag: "conditional", + read ifkeyword: Token<"if">, + read condition: AstExpr, + read thenkeyword: Token<"then">, + read thenblock: AstStatBlock, + read elseifs: { AstElseIfStat }, + read elsekeyword: Token<"else">?, -- TODO: This could be elseif! + read elseblock: AstStatBlock?, + read endkeyword: Token<"end">, } export type AstStatWhile = { - location: span, - kind: "stat", - tag: "while", - whilekeyword: Token<"while">, - condition: AstExpr, - dokeyword: Token<"do">, - body: AstStatBlock, - endkeyword: Token<"end">, + read location: span, + read kind: "stat", + read tag: "while", + read whilekeyword: Token<"while">, + read condition: AstExpr, + read dokeyword: Token<"do">, + read body: AstStatBlock, + read endkeyword: Token<"end">, } export type AstStatRepeat = { - location: span, - kind: "stat", - tag: "repeat", - repeatkeyword: Token<"repeat">, - body: AstStatBlock, - untilkeyword: Token<"until">, - condition: AstExpr, + read location: span, + read kind: "stat", + read tag: "repeat", + read repeatkeyword: Token<"repeat">, + read body: AstStatBlock, + read untilkeyword: Token<"until">, + read condition: AstExpr, } -export type AstStatBreak = Token<"break"> & { location: span, kind: "stat", tag: "break" } +export type AstStatBreak = Token<"break"> & { read location: span, read kind: "stat", read tag: "break" } -export type AstStatContinue = Token<"continue"> & { location: span, kind: "stat", tag: "continue" } +export type AstStatContinue = Token<"continue"> & { read location: span, read kind: "stat", read tag: "continue" } export type AstStatReturn = { - location: span, - kind: "stat", - tag: "return", - returnkeyword: Token<"return">, - expressions: Punctuated, + read location: span, + read kind: "stat", + read tag: "return", + read returnkeyword: Token<"return">, + read expressions: Punctuated, } -export type AstStatExpr = { location: span, kind: "stat", tag: "expression", expression: AstExpr } +export type AstStatExpr = { + read location: span, + read kind: "stat", + read tag: "expression", + read expression: AstExpr, +} export type AstStatLocal = { - location: span, - kind: "stat", - tag: "local", - localkeyword: Token<"local">, - variables: Punctuated, - equals: Token<"=">?, - values: Punctuated, + read location: span, + read kind: "stat", + read tag: "local", + read localkeyword: Token<"local">, + read variables: Punctuated, + read equals: Token<"=">?, + read values: Punctuated, } export type AstStatFor = { - location: span, - kind: "stat", - tag: "for", - forkeyword: Token<"for">, - variable: AstLocal, - equals: Token<"=">, - from: AstExpr, - tocomma: Token<",">, - to: AstExpr, - stepcomma: Token<",">?, - step: AstExpr?, - dokeyword: Token<"do">, - body: AstStatBlock, - endkeyword: Token<"end">, + read location: span, + read kind: "stat", + read tag: "for", + read forkeyword: Token<"for">, + read variable: AstLocal, + read equals: Token<"=">, + read from: AstExpr, + read tocomma: Token<",">, + read to: AstExpr, + read stepcomma: Token<",">?, + read step: AstExpr?, + read dokeyword: Token<"do">, + read body: AstStatBlock, + read endkeyword: Token<"end">, } export type AstStatForIn = { - location: span, - kind: "stat", - tag: "forin", - forkeyword: Token<"for">, - variables: Punctuated, - inkeyword: Token<"in">, - values: Punctuated, - dokeyword: Token<"do">, - body: AstStatBlock, - endkeyword: Token<"end">, + read location: span, + read kind: "stat", + read tag: "forin", + read forkeyword: Token<"for">, + read variables: Punctuated, + read inkeyword: Token<"in">, + read values: Punctuated, + read dokeyword: Token<"do">, + read body: AstStatBlock, + read endkeyword: Token<"end">, } export type AstStatAssign = { - location: span, - kind: "stat", - tag: "assign", - variables: Punctuated, - equals: Token<"=">, - values: Punctuated, + read location: span, + read kind: "stat", + read tag: "assign", + read variables: Punctuated, + read equals: Token<"=">, + read values: Punctuated, } export type AstStatCompoundAssign = { - location: span, - kind: "stat", - tag: "compoundassign", - variable: AstExpr, - operand: Token, -- TODO: enforce token type, - value: AstExpr, + read location: span, + read kind: "stat", + read tag: "compoundassign", + read variable: AstExpr, + read operand: Token, -- TODO: Enforce token type + read value: AstExpr, } -export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { location: span, kind: "attribute" } +export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { read location: span, read kind: "attribute" } export type AstStatFunction = { - location: span, - kind: "stat", - tag: "function", - name: AstExpr, - func: AstExprFunction, + read location: span, + read kind: "stat", + read tag: "function", + read name: AstExpr, + read func: AstExprFunction, } export type AstStatLocalFunction = { - location: span, - kind: "stat", - tag: "localfunction", - localkeyword: Token<"local">, - name: AstLocal, - func: AstExprFunction, + read location: span, + read kind: "stat", + read tag: "localfunction", + read localkeyword: Token<"local">, + read name: AstLocal, + read func: AstExprFunction, } export type AstStatTypeAlias = { - location: span, - kind: "stat", - tag: "typealias", - export: Token<"export">?, - typetoken: Token<"type">, - name: Token, - opengenerics: Token<"<">?, - generics: Punctuated?, - genericpacks: Punctuated?, - closegenerics: Token<">">?, - equals: Token<"=">, - type: AstType, + read location: span, + read kind: "stat", + read tag: "typealias", + read export: Token<"export">?, + read typetoken: Token<"type">, + read name: Token, + read opengenerics: Token<"<">?, + read generics: Punctuated?, + read genericpacks: Punctuated?, + read closegenerics: Token<">">?, + read equals: Token<"=">, + read type: AstType, } -- A type function export type AstStatTypeFunction = { - location: span, - kind: "stat", - tag: "typefunction", - export: Token<"export">?, - type: Token<"type">, - name: Token, - body: AstExprFunction, + read location: span, + read kind: "stat", + read tag: "typefunction", + read export: Token<"export">?, + read type: Token<"type">, + read name: Token, + read body: AstExprFunction, } export type AstStat = @@ -480,155 +494,158 @@ export type AstStat = | AstStatTypeFunction export type AstGenericType = { - tag: "generic", - name: Token, - equals: Token<"=">?, - default: AstType?, + read tag: "generic", + read name: Token, + read equals: Token<"=">?, + read default: AstType?, } export type AstGenericTypePack = { - tag: "genericpack", - name: Token, - ellipsis: Token<"...">, - equals: Token<"=">?, - default: AstTypePack?, + read tag: "genericpack", + read name: Token, + read ellipsis: Token<"...">, + read equals: Token<"=">?, + read default: AstTypePack?, } export type AstTypeReference = { - location: span, - kind: "type", - tag: "reference", - prefix: Token?, - prefixpoint: Token<".">?, - name: Token, - openparameters: Token<"<">?, - parameters: Punctuated?, - closeparameters: Token<">">?, + read location: span, + read kind: "type", + read tag: "reference", + read prefix: Token?, + read prefixpoint: Token<".">?, + read name: Token, + read openparameters: Token<"<">?, + read parameters: Punctuated?, + read closeparameters: Token<">">?, } -export type AstTypeSingletonBool = - Token<"true" | "false"> - & { location: span, kind: "type", tag: "boolean", value: boolean } +export type AstTypeSingletonBool = Token<"true" | "false"> & { + read location: span, + read kind: "type", + read tag: "boolean", + read value: boolean, +} export type AstTypeSingletonString = Token & { - location: span, - kind: "type", - tag: "string", - quotestyle: "single" | "double", + read location: span, + read kind: "type", + read tag: "string", + read quotestyle: "single" | "double", } export type AstTypeTypeof = { - location: span, - kind: "type", - tag: "typeof", - typeof: Token<"typeof">, - openparens: Token<"(">, - expression: AstExpr, - closeparens: Token<")">, + read location: span, + read kind: "type", + read tag: "typeof", + read typeof: Token<"typeof">, + read openparens: Token<"(">, + read expression: AstExpr, + read closeparens: Token<")">, } export type AstTypeGroup = { - location: span, - kind: "type", - tag: "group", - openparens: Token<"(">, - type: AstType, - closeparens: Token<")">, + read location: span, + read kind: "type", + read tag: "group", + read openparens: Token<"(">, + read type: AstType, + read closeparens: Token<")">, } -export type AstTypeOptional = Token<"?"> & { location: span, kind: "type", tag: "optional" } +export type AstTypeOptional = Token<"?"> & { read location: span, read kind: "type", read tag: "optional" } export type AstTypeUnion = { - location: span, - kind: "type", - tag: "union", - leading: Token<"|">?, + read location: span, + read kind: "type", + read tag: "union", + read leading: Token<"|">?, -- Separator may be nil for AstTypeOptional - types: Punctuated, + read types: Punctuated, } export type AstTypeIntersection = { - location: span, - kind: "type", - tag: "intersection", - leading: Token<"&">?, - types: Punctuated, + read location: span, + read kind: "type", + read tag: "intersection", + read leading: Token<"&">?, + read types: Punctuated, } export type AstTypeArray = { - location: span, - kind: "type", - tag: "array", - openbrace: Token<"{">, - access: Token<"read" | "write">?, - type: AstType, - closebrace: Token<"}">, + read location: span, + read kind: "type", + read tag: "array", + read openbrace: Token<"{">, + read access: Token<"read" | "write">?, + read type: AstType, + read closebrace: Token<"}">, } export type AstTableTypeItemIndexer = { - kind: "indexer", - access: Token<"read" | "write">?, - indexeropen: Token<"[">, - key: AstType, - indexerclose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, + read kind: "indexer", + read access: Token<"read" | "write">?, + read indexeropen: Token<"[">, + read key: AstType, + read indexerclose: Token<"]">, + read colon: Token<":">, + read value: AstType, + read separator: Token<"," | ";">?, } export type AstTableTypeItemStringProperty = { - kind: "stringproperty", - access: Token<"read" | "write">?, - indexeropen: Token<"[">, - key: AstTypeSingletonString, - indexerclose: Token<"]">, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, + read kind: "stringproperty", + read access: Token<"read" | "write">?, + read indexeropen: Token<"[">, + read key: AstTypeSingletonString, + read indexerclose: Token<"]">, + read colon: Token<":">, + read value: AstType, + read separator: Token<"," | ";">?, } export type AstTableTypeItemProperty = { - kind: "property", - access: Token<"read" | "write">?, - key: Token, - colon: Token<":">, - value: AstType, - separator: Token<"," | ";">?, + read kind: "property", + read access: Token<"read" | "write">?, + read key: Token, + read colon: Token<":">, + read value: AstType, + read separator: Token<"," | ";">?, } export type AstTableTypeItem = AstTableTypeItemIndexer | AstTableTypeItemStringProperty | AstTableTypeItemProperty export type AstTypeTable = { - location: span, - kind: "type", - tag: "table", - openbrace: Token<"{">, - entries: { AstTableTypeItem }, - closebrace: Token<"}">, + read location: span, + read kind: "type", + read tag: "table", + read openbrace: Token<"{">, + read entries: { AstTableTypeItem }, + read closebrace: Token<"}">, } export type AstFunctionTypeParameter = { - location: span, - name: Token?, - colon: Token<":">?, - type: AstType, + read location: span, + read name: Token?, + read colon: Token<":">?, + read type: AstType, } -- A type representing a Luau function export type AstTypeFunction = { - location: span, - kind: "type", - tag: "function", - opengenerics: Token<"<">?, - generics: Punctuated?, - genericpacks: Punctuated?, - closegenerics: Token<">">?, - openparens: Token<"(">, - parameters: Punctuated, - vararg: AstTypePack?, - closeparens: Token<")">, - returnarrow: Token<"->">, - returntypes: AstTypePack, + read location: span, + read kind: "type", + read tag: "function", + read opengenerics: Token<"<">?, + read generics: Punctuated?, + read genericpacks: Punctuated?, + read closegenerics: Token<">">?, + read openparens: Token<"(">, + read parameters: Punctuated, + read vararg: AstTypePack?, + read closeparens: Token<")">, + read returnarrow: Token<"->">, + read returntypes: AstTypePack, } export type AstType = @@ -645,24 +662,30 @@ export type AstType = | AstTypeFunction export type AstTypePackExplicit = { - location: span, - kind: "typepack", - tag: "explicit", - openparens: Token<"(">?, - types: Punctuated, - tailtype: AstTypePack?, - closeparens: Token<")">?, + read location: span, + read kind: "typepack", + read tag: "explicit", + read openparens: Token<"(">?, + read types: Punctuated, + read tailtype: AstTypePack?, + read closeparens: Token<")">?, } -export type AstTypePackGeneric = { location: span, kind: "typepack", tag: "generic", name: Token, ellipsis: Token<"..."> } +export type AstTypePackGeneric = { + read location: span, + read kind: "typepack", + read tag: "generic", + read name: Token, + read ellipsis: Token<"...">, +} export type AstTypePackVariadic = { - location: span, - kind: "typepack", - tag: "variadic", + read location: span, + read kind: "typepack", + read tag: "variadic", --- May be nil when present as the vararg annotation in a function body - ellipsis: Token<"...">?, - type: AstType, + read ellipsis: Token<"...">?, + read type: AstType, } export type AstTypePack = AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic @@ -670,10 +693,10 @@ export type AstTypePack = AstTypePackExplicit | AstTypePackGeneric | AstTypePack export type AstNode = AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute export type ParseResult = { - root: AstStatBlock, - eof: Eof, - lines: number, - lineoffsets: { number }, + read root: AstStatBlock, + read eof: Eof, + read lines: number, + read lineoffsets: { number }, } function luau.parse(source: string): ParseResult diff --git a/examples/transformer.luau b/examples/transformer.luau deleted file mode 100644 index b70c33aab..000000000 --- a/examples/transformer.luau +++ /dev/null @@ -1,95 +0,0 @@ -local printer = require("@std/syntax/printer") -local visitor = require("@std/syntax/visitor") -local syntax = require("@std/syntax") - -local function transformation(ctx) - local v = visitor.create() - - v.visitExprBinary = function(node: syntax.AstExprBinary) - if node.operator.text ~= "~=" then - return true - end - - if node.lhsoperand.tag ~= "local" or node.rhsoperand.tag ~= "local" then - return true - end - - if node.lhsoperand.token.text ~= node.rhsoperand.token.text then - return true - end - - local operand: syntax.AstExprLocal = node.lhsoperand - operand.token.leadingtrivia = {} - operand.token.trailingtrivia = {} - local location: syntax.span = operand.token.location; - - -- transform node into an AstExprCall - (node :: any).operator = nil - (node :: any).lhsoperand = nil - (node :: any).rhsoperand = nil - - ((node :: any) :: syntax.AstExprCall).tag = "call" - - local func: syntax.AstExprGlobal = { - kind = "expr", - tag = "global", - name = { - istoken = true, - leadingtrivia = {}, - location = location, - text = "math.isnan", - trailingtrivia = {}, - }, - } - ((node :: any) :: syntax.AstExprCall).func = func - - local openparens: syntax.Token<"("> = { - istoken = true, - leadingtrivia = {}, - location = syntax.span.create({ - beginline = location.beginline, - begincolumn = location.begincolumn + #"math.isnan", - endline = location.endline, - endcolumn = location.endcolumn + #"math.isnan" + 1, - }), - text = "(", - trailingtrivia = {}, - } - ((node :: any) :: syntax.AstExprCall).openparens = openparens - - local arguments: syntax.Punctuated = { { node = operand } } - ((node :: any) :: syntax.AstExprCall).arguments = arguments - - local closeparens: syntax.Token<")"> = { - istoken = true, - leadingtrivia = {}, - location = syntax.span.create({ - beginline = location.beginline, - begincolumn = location.begincolumn + #"math.isnan(" + #operand.token.text, - endline = location.endline, - endcolumn = location.endcolumn + #"math.isnan(" + #operand.token.text + 1, - }), - text = ")", - trailingtrivia = {}, - } - ((node :: any) :: syntax.AstExprCall).closeparens = closeparens; - - ((node :: any) :: syntax.AstExprCall).self = false - - local argLocation: syntax.span = syntax.span.create({ - beginline = location.beginline, - begincolumn = location.begincolumn + #"math.isnan(", - endline = location.endline, - endcolumn = location.begincolumn + #"math.isnan(" + #operand.token.text, - }); - ((node :: any) :: syntax.AstExprCall).argLocation = argLocation - - return false - end - - visitor.visitblock(ctx.parseresult.root, v) - - return printer.printfile(ctx.parseresult) -end - -return transformation diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 564878096..30ad6804d 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -194,10 +194,7 @@ function printerLib.printnode(node: types.AstNode, replacements: types.replaceme return buffer.readstring(printer.result, 0, printer.cursor) end -function printerLib.printfile( - result: { root: types.AstStatBlock, eof: types.Eof }, - replacements: types.replacements? -): string +function printerLib.printfile(result: types.ParseResult, replacements: types.replacements?): string local printer = printVisitor(replacements) visitor.visitblock(result.root, printer) visitor.visittoken(result.eof, printer) diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index c0f1f15cc..689af444d 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -1015,7 +1015,7 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visit(s) end, visitExprInstantiate = visit, - visitTableExprItem = visit, + visitTableExprItem = visit :: (types.AstTableExprItem) -> boolean, visitExprTable = visit, visitExprIndexName = visit, visitExprIndexExpr = visit, @@ -1040,7 +1040,7 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitTypePackGeneric = visit, visitTypePackVariadic = visit, - visitToken = visit, + visitToken = visit :: (types.Token) -> boolean, visitExprConstantNil = visit, visitExprConstantString = visit, visitExprConstantBool = visit, @@ -1068,10 +1068,10 @@ local function visit(node: types.AstNode, visitor: Visitor) visitLocal(node, visitor) elseif node.kind == "attribute" then visitAttribute(node, visitor) - elseif node.istoken then - visitToken(node, visitor) - elseif node.istableitem then - visitTableExprItem(node, visitor) + elseif (node :: types.Token).istoken then + visitToken(node :: types.Token, visitor) + elseif (node :: types.AstTableExprItem).istableitem then + visitTableExprItem(node :: types.AstTableExprItem, visitor) else exhaustiveMatch(node.kind) end diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index f4d37b601..529836f99 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -30,30 +30,6 @@ test.suite("lute transform", function(suite) end end) - suite:case("transform visitor style", function(assert) - -- Setup - -- Copy examples/transformer.luau to build/transform_tests/transformer.luau - local transformerExample = path.format(path.join("examples", "transformer.luau")) - local transformerDest = path.format(path.join(tmpDir, "transformer.luau")) - fs.copy(transformerExample, transformerDest) - - -- Do - -- Run the transformer on the transformee - local result = process.run({ lutePath, "transform", transformerDest, transformeePath }) - - -- Check - assert.eq(result.exitcode, 0) - - local transformeeHandle = fs.open(transformeePath, "r") - local transformeeContent = fs.read(transformeeHandle) - assert.eq(transformeeContent:find("x ~= x"), nil) - assert.strcontains(transformeeContent, "math.isnan(x)") - fs.close(transformeeHandle) - - -- Teardown - fs.remove(transformerDest) - end) - suite:case("transform query style", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index eb97f7558..ccae8c4fb 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -38,7 +38,6 @@ examples/process.luau:17:7-15 examples/process.luau:18:7-15 examples/process.luau:19:7-15 examples/task-delay.luau:3:15-37 -examples/transformer.luau:33:38-100 lute/cli/commands/doc/init.luau:203:2-11 lute/cli/commands/doc/init.luau:204:10-24 lute/cli/commands/doc/init.luau:310:5-16 @@ -108,8 +107,6 @@ lute/std/libs/syntax/printer.luau:111:3-15 lute/std/libs/syntax/printer.luau:111:3-15 lute/std/libs/syntax/printer.luau:155:3-26 lute/std/libs/syntax/printer.luau:155:3-26 -lute/std/libs/syntax/printer.luau:203:2-19 -lute/std/libs/syntax/printer.luau:203:2-19 lute/std/libs/syntax/query.luau:41:25-25 lute/std/libs/syntax/query.luau:41:25-25 lute/std/libs/syntax/query.luau:41:25-25 @@ -156,42 +153,10 @@ lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:30:38-100 lute/std/libs/syntax/utils/trivia.luau:31:13-21 lute/std/libs/syntax/utils/trivia.luau:31:13-21 -lute/std/libs/syntax/visitor.luau:253:3-12 -lute/std/libs/syntax/visitor.luau:253:3-12 -lute/std/libs/syntax/visitor.luau:259:3-12 -lute/std/libs/syntax/visitor.luau:259:3-12 -lute/std/libs/syntax/visitor.luau:392:3-12 -lute/std/libs/syntax/visitor.luau:392:3-12 -lute/std/libs/syntax/visitor.luau:398:3-12 -lute/std/libs/syntax/visitor.luau:398:3-12 -lute/std/libs/syntax/visitor.luau:404:3-12 -lute/std/libs/syntax/visitor.luau:404:3-12 -lute/std/libs/syntax/visitor.luau:422:3-12 -lute/std/libs/syntax/visitor.luau:422:3-12 -lute/std/libs/syntax/visitor.luau:456:3-12 -lute/std/libs/syntax/visitor.luau:456:3-12 -lute/std/libs/syntax/visitor.luau:684:3-12 -lute/std/libs/syntax/visitor.luau:684:3-12 -lute/std/libs/syntax/visitor.luau:690:3-12 -lute/std/libs/syntax/visitor.luau:690:3-12 -lute/std/libs/syntax/visitor.luau:713:3-12 -lute/std/libs/syntax/visitor.luau:713:3-12 -lute/std/libs/syntax/visitor.luau:1018:25-29 -lute/std/libs/syntax/visitor.luau:1018:25-29 lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 -lute/std/libs/syntax/visitor.luau:1043:17-21 -lute/std/libs/syntax/visitor.luau:1043:17-21 -lute/std/libs/syntax/visitor.luau:1071:9-20 -lute/std/libs/syntax/visitor.luau:1071:9-20 -lute/std/libs/syntax/visitor.luau:1072:3-12 -lute/std/libs/syntax/visitor.luau:1072:3-12 -lute/std/libs/syntax/visitor.luau:1073:9-24 -lute/std/libs/syntax/visitor.luau:1073:9-24 -lute/std/libs/syntax/visitor.luau:1074:22-25 -lute/std/libs/syntax/visitor.luau:1074:22-25 tests/batteries/collections/deque.test.luau:9:13-16 tests/batteries/collections/deque.test.luau:16:13-16 tests/batteries/collections/deque.test.luau:24:13-16 From ed6e075979b85c4fd91f7648ac9a11a041a89e4c Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 20 Feb 2026 11:47:17 -0800 Subject: [PATCH 367/642] Adds a snapshot testing framework for lute test (#833) This PR adds a snapshot testing framework to `lute test`, that runs `*.snap.luau` files, captures their output, and compares it to a corresponding `.snap` file on disk. Under the hood, this just takes the `lute` exec path, and executes `lute run` on the `*.snap.luau` file. What this PR does: - Normalizing path separators to POSIX as well as replaces ocurrences of cwd() with . This means that tests can run on any machine, any platform, and will hopefully produce the same output. - Color output - Diffs indicating the delta between the snapshot and the actual invocation. - Support for interactively resolving differences - Examples - I've rewritten the standard library tests for `@std/test` to use this framework. Not in this PR(but 100% coming in upcoming ones): - Support for customizing the arguments passed to `lute` - @hgoldstein has suggested a design where each directory of snapshot files has an ARGS configuration(or equivalent) that specifies arguments on a per directory basis. I am thinking about using a .luau file to do the configuration for this instead. - I would need the above feature in order to start effectively testing this framework, since that would let me generate snapshots of the output of this command itself. - Adding this to CI - Rewriting tests that serialize things to /tmp/ to use this --- .github/workflows/ci.yml | 2 +- lint.config.luau | 7 + lute/cli/commands/test/finder.luau | 26 +++- lute/cli/commands/test/init.luau | 35 ++++- lute/cli/commands/test/reporter.luau | 45 +++++- lute/cli/commands/test/snap/runner.luau | 89 +++++++++++ lute/cli/commands/test/snap/styles.luau | 10 ++ lute/cli/commands/test/snap/types.luau | 29 ++++ lute/cli/commands/test/snap/updater.luau | 139 ++++++++++++++++++ .../assert_buffereq_unequal_length.snap | 17 +++ .../assert_buffereq_unequal_length.snap.luau | 9 ++ .../assert_eq_error_msg_in_case.snap | 17 +++ .../assert_eq_error_msg_in_case.snap.luau | 7 + .../assert_eq_error_msg_in_suite.snap | 17 +++ .../assert_eq_error_msg_in_suite.snap.luau | 9 ++ .../snapshots/assert_error_eq_no_error.snap | 17 +++ .../assert_error_eq_no_error.snap.luau | 9 ++ .../assert_erroreq_error_message.snap | 17 +++ .../assert_erroreq_error_message.snap.luau | 11 ++ .../assert_neq_error_message_in_case.snap | 17 +++ ...assert_neq_error_message_in_case.snap.luau | 7 + ...assert_strcontains_instances_negative.snap | 17 +++ ...t_strcontains_instances_negative.snap.luau | 7 + ...t_strcontains_multiple_instance_fails.snap | 17 +++ ...contains_multiple_instance_fails.snap.luau | 9 ++ ...ert_strcontains_single_instance_fails.snap | 17 +++ ...trcontains_single_instance_fails.snap.luau | 9 ++ .../assert_tableeq_error_message_in_case.snap | 17 +++ ...rt_tableeq_error_message_in_case.snap.luau | 7 + ...assert_tableeq_error_message_in_suite.snap | 17 +++ ...t_tableeq_error_message_in_suite.snap.luau | 9 ++ .../assertneq_error_message_in_suite.snap | 17 +++ ...assertneq_error_message_in_suite.snap.luau | 9 ++ .../std/snapshots/buffereq_failure_suite.snap | 17 +++ .../buffereq_failure_suite.snap.luau | 13 ++ .../runtime_error_doesnt_report_xpcall.snap | 25 ++++ ...ntime_error_doesnt_report_xpcall.snap.luau | 10 ++ ...ntime_error_doesnt_report_xpcall_case.snap | 25 ++++ ..._error_doesnt_report_xpcall_case.snap.luau | 8 + tools/check.luau | 14 +- 40 files changed, 790 insertions(+), 10 deletions(-) create mode 100644 lint.config.luau create mode 100644 lute/cli/commands/test/snap/runner.luau create mode 100644 lute/cli/commands/test/snap/styles.luau create mode 100644 lute/cli/commands/test/snap/types.luau create mode 100644 lute/cli/commands/test/snap/updater.luau create mode 100755 tests/std/snapshots/assert_buffereq_unequal_length.snap create mode 100644 tests/std/snapshots/assert_buffereq_unequal_length.snap.luau create mode 100755 tests/std/snapshots/assert_eq_error_msg_in_case.snap create mode 100644 tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau create mode 100755 tests/std/snapshots/assert_eq_error_msg_in_suite.snap create mode 100644 tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau create mode 100755 tests/std/snapshots/assert_error_eq_no_error.snap create mode 100644 tests/std/snapshots/assert_error_eq_no_error.snap.luau create mode 100755 tests/std/snapshots/assert_erroreq_error_message.snap create mode 100644 tests/std/snapshots/assert_erroreq_error_message.snap.luau create mode 100755 tests/std/snapshots/assert_neq_error_message_in_case.snap create mode 100644 tests/std/snapshots/assert_neq_error_message_in_case.snap.luau create mode 100755 tests/std/snapshots/assert_strcontains_instances_negative.snap create mode 100644 tests/std/snapshots/assert_strcontains_instances_negative.snap.luau create mode 100755 tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap create mode 100644 tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau create mode 100755 tests/std/snapshots/assert_strcontains_single_instance_fails.snap create mode 100644 tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau create mode 100755 tests/std/snapshots/assert_tableeq_error_message_in_case.snap create mode 100644 tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau create mode 100755 tests/std/snapshots/assert_tableeq_error_message_in_suite.snap create mode 100644 tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau create mode 100755 tests/std/snapshots/assertneq_error_message_in_suite.snap create mode 100644 tests/std/snapshots/assertneq_error_message_in_suite.snap.luau create mode 100755 tests/std/snapshots/buffereq_failure_suite.snap create mode 100644 tests/std/snapshots/buffereq_failure_suite.snap.luau create mode 100755 tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap create mode 100644 tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau create mode 100755 tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap create mode 100644 tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba804157d..d813c23b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,7 +175,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Run Lute Lint - run: ${{ steps.build_lute.outputs.exe_path }} lint -v . + run: ${{ steps.build_lute.outputs.exe_path }} lint -v . -c lint.config.luau lute-check: runs-on: ubuntu-latest diff --git a/lint.config.luau b/lint.config.luau new file mode 100644 index 000000000..c26c4101d --- /dev/null +++ b/lint.config.luau @@ -0,0 +1,7 @@ +return { + lute = { + lint = { + ignores = { "**/*.snap.luau" }, + }, + }, +} diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index 1b938dcd9..52a23dad4 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -7,13 +7,17 @@ local function istestfile(filename: string): boolean return stringext.hassuffix(filename, ".test.luau") or stringext.hassuffix(filename, ".spec.luau") end -local function findtestfiles(paths: { string }): { path.path } +local function issnapfile(filename: string): boolean + return stringext.hassuffix(filename, ".snap.luau") +end + +local function findfiles(paths: { string }, predicate: (string) -> boolean): { path.path } local files = {} - local function findfiles(p: path.pathlike) + local function walk(p: path.pathlike) local it = fs.walk(path.normalize(p), { recursive = true }) local file = it() while file do - if fs.type(file) ~= "dir" and istestfile(path.format(file)) then + if fs.type(file) ~= "dir" and predicate(path.format(file)) then table.insert(files, path.normalize(file)) end file = it() @@ -22,10 +26,22 @@ local function findtestfiles(paths: { string }): { path.path } local cwd = ps.cwd() for _, p in paths do - findfiles(path.join(cwd, p)) + if path.isabsolute(p) then + walk(p) + else + walk(path.join(cwd, p)) + end end return files end -return table.freeze({ findtestfiles = findtestfiles }) +local function findtestfiles(paths: { string }): { path.path } + return findfiles(paths, istestfile) +end + +local function findsnapfiles(paths: { string }): { path.path } + return findfiles(paths, issnapfile) +end + +return table.freeze({ findtestfiles = findtestfiles, findsnapfiles = findsnapfiles }) diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index 8ab33ef72..8e1b595c1 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -8,6 +8,8 @@ local testtypes = require("@std/test/types") local runner = require("@std/test/runner") local reporter = require("@self/reporter") local test = require("@std/test") +local snaprunner = require("@self/snap/runner") +local updater = require("@self/snap/updater") local function loadtests(testfiles: { path.path }) -- Load all test files first @@ -47,6 +49,20 @@ local function runtests(env: testtypes.testenvironment, runner: testtypes.testru ps.exit(0) end +local function runsnapshots(searchpaths: { string }, update: boolean, interactive: boolean) + local files = finder.findsnapfiles(searchpaths) + + if #files == 0 then + print("No snapshot files (*.snap.luau) found.") + ps.exit(0) + return + end + + local result = snaprunner.runsnapshots(files) + reporter.snap(result) + updater.updatesnapshot(result, update, interactive) +end + local function main(...: string) local args = cli.parser() @@ -54,6 +70,8 @@ local function main(...: string) args:add("list", "flag", { help = "List all discovered test cases without running them" }) args:add("suite", "option", { help = "Run only tests in the specified suite", aliases = { "s" } }) args:add("case", "option", { help = "Run only test cases matching the specified name", aliases = { "c" } }) + args:add("interactive", "flag", { help = "Interactively accept/reject snapshot changes", aliases = { "i" } }) + args:add("update", "flag", { help = "Auto-accept all snapshot changes", aliases = { "u" } }) args:parse({ ... }) @@ -62,8 +80,21 @@ local function main(...: string) return end - local testPaths = args:forwarded() - local searchpath = if testPaths and #testPaths > 0 then testPaths else { "./tests" } + local forwarded = args:forwarded() + -- Check if this is a snapshot subcommand + if forwarded and #forwarded > 0 and forwarded[1] == "snapshots" then + if #forwarded == 1 then + runsnapshots({ "./tests" }, args:has("update"), args:has("interactive")) + else + local searchpaths = {} + for idx = 2, #forwarded do + table.insert(searchpaths, forwarded[idx]) + end + runsnapshots(searchpaths, args:has("update"), args:has("interactive")) + end + end + + local searchpath = if forwarded and #forwarded > 0 then forwarded else { "./tests" } loadtests(finder.findtestfiles(searchpath)) if args:has("list") then diff --git a/lute/cli/commands/test/reporter.luau b/lute/cli/commands/test/reporter.luau index 145875a3f..bfd22d8d5 100644 --- a/lute/cli/commands/test/reporter.luau +++ b/lute/cli/commands/test/reporter.luau @@ -1,5 +1,8 @@ local rt = require("@batteries/richterm") +local path = require("@std/path") local tys = require("@std/test/types") +local snapty = require("./snap/types") +local snapsty = require("./snap/styles") local function splitStacktrace(stacktrace: string): { string } local lines = {} @@ -62,4 +65,44 @@ local function colorReporter(result: tys.testrunresult) printSummary(result) end -return { color = colorReporter } +local function snapReporter(result: snapty.snapresult) + local total = #result.passed + #result.failed + #result.new + + for _, p in result.passed do + print(snapsty.pass("\tPASS ") .. snapsty.filepath(path.format(p.filepath))) + end + + for _, n in result.new do + print(snapsty.newstyle("\tNEW ") .. snapsty.filepath(path.format(n.filepath))) + end + + if #result.failed > 0 then + print("") + print(snapsty.fail("Failures:")) + print("") + for _, f in result.failed do + print(snapsty.fail("\tFAIL ") .. snapsty.filepath(path.format(f.filepath))) + print("") + if f.stderr and f.stderr ~= "" then + print(snapsty.dim(`Failed to execute test process with err {f.stderr}`)) + print("") + end + + print(f.diff) + print("") + end + end + + print(separator(string.rep("─", 50))) + local parts = {} + table.insert(parts, snapsty.pass(`{#result.passed} passed`)) + if #result.new > 0 then + table.insert(parts, snapsty.newstyle(`{#result.new} new`)) + end + if #result.failed > 0 then + table.insert(parts, snapsty.fail(`{#result.failed} failed`)) + end + print(snapsty.bold("Snapshots: ") .. table.concat(parts, snapsty.dim(", ")) .. snapsty.dim(` of {total}`)) +end + +return table.freeze({ color = colorReporter, snap = snapReporter }) diff --git a/lute/cli/commands/test/snap/runner.luau b/lute/cli/commands/test/snap/runner.luau new file mode 100644 index 000000000..c7be400e3 --- /dev/null +++ b/lute/cli/commands/test/snap/runner.luau @@ -0,0 +1,89 @@ +local process = require("@std/process") +local path = require("@std/path") +local fs = require("@std/fs") +local difftext = require("@batteries/difftext") +local strext = require("@std/stringext") +local snapty = require("./types") + +-- Derive the .snap artifact path from a .snap.luau file path. +-- e.g. /foo/bar.snap.luau -> /foo/bar.snap +local function artifactpath(filepath: path.path): string + local f = path.format(filepath) + assert(strext.hassuffix(f, ".snap.luau"), `expected {f} to have suffix .snap.luau`) + return strext.removesuffix(f, ".luau") -- strips ".luau" +end + +local function posixify(str) + local posixy, _ = string.gsub(str, [[\]], "/") + return posixy +end + +local function normalizeSnapshotOutput(output: string, normalizedCwd: string): string + local nobackslash = posixify(output) + local replacedCwd, _ = string.gsub(nobackslash, normalizedCwd, "") + return replacedCwd +end + +local runner = {} + +function runner.runsnapshots(files: { path.path }): snapty.snapresult + local result: snapty.snapresult = { + passed = {}, + failed = {}, + new = {}, + } + + local execpath = path.format(process.execpath()) + + -- Compute the normalized current working directory exactly once + --[[ + Paths can have '.' characters in them, but gsub takes patterns as arguments and a '.' will match any single character. + E.g. Interpreting "/foo/bar.baz" as a pattern, will match "/foo/bar[any char]baz". + We need to replace any explicit '.' in a string that will be used as a pattern, with the escaped '.' + Gsub takes two patterns as its second and third argument, so we'll need to replace all '.' in the string(represented by the pattern '%.') + with the '.' matching pattern '%.'(itself represented by escaping the %, '%%.'). + ]] + -- + local cwd = posixify(path.format(process.cwd())) + cwd, _ = string.gsub(cwd, "%.", "%%.") + + for _, filepath in files do + local artifactpath = artifactpath(filepath) + + local proc = process.run({ execpath, path.format(filepath) }) + local actual = normalizeSnapshotOutput(proc.stdout, cwd) + + if not fs.exists(artifactpath) then + -- No artifact file yet — this is a new snapshot + table.insert(result.new, { + filepath = filepath, + artifactpath = artifactpath, + actual = actual, + }) + continue + end + + local expected = fs.readfiletostring(artifactpath) + + if actual == expected then + table.insert(result.passed, { filepath = filepath, artifactpath = artifactpath }) + else + local diff = difftext.prettydiff(expected, actual, { + detailed = true, + includeLineNumbers = true, + }) + table.insert(result.failed, { + filepath = filepath, + artifactpath = artifactpath, + actual = actual, + expected = expected, + diff = diff, + stderr = if not proc.ok then proc.stderr else "", + }) + end + end + + return result +end + +return table.freeze(runner) diff --git a/lute/cli/commands/test/snap/styles.luau b/lute/cli/commands/test/snap/styles.luau new file mode 100644 index 000000000..70f32b8b3 --- /dev/null +++ b/lute/cli/commands/test/snap/styles.luau @@ -0,0 +1,10 @@ +local rt = require("@batteries/richterm") + +return table.freeze({ + bold = rt.bold, + filepath = rt.cyan, + dim = rt.dim, + pass = rt.combine(rt.green, rt.bold), + fail = rt.red, + newstyle = rt.combine(rt.yellow, rt.bold), +}) diff --git a/lute/cli/commands/test/snap/types.luau b/lute/cli/commands/test/snap/types.luau new file mode 100644 index 000000000..87eec5f62 --- /dev/null +++ b/lute/cli/commands/test/snap/types.luau @@ -0,0 +1,29 @@ +local path = require("@std/path") + +export type snappass = { + filepath: path.path, + artifactpath: string, +} + +export type snapfail = { + filepath: path.path, + artifactpath: string, + actual: string, + expected: string, + diff: string, + stderr: string?, +} + +export type snapnew = { + filepath: path.path, + artifactpath: string, + actual: string, +} + +export type snapresult = { + passed: { snappass }, + failed: { snapfail }, + new: { snapnew }, +} + +return {} diff --git a/lute/cli/commands/test/snap/updater.luau b/lute/cli/commands/test/snap/updater.luau new file mode 100644 index 000000000..7b9473010 --- /dev/null +++ b/lute/cli/commands/test/snap/updater.luau @@ -0,0 +1,139 @@ +local fs = require("@std/fs") +local snapty = require("./types") +local io = require("@std/io") +local path = require("@std/path") +local sty = require("./styles") +local ps = require("@std/process") +local strext = require("@std/stringext") + +-- Write content directly to a .snap artifact file. +-- Used both for creating new snapshots and updating existing ones. +local function writeSnapshot(snappath: string, content: string): boolean + local ok = pcall(fs.writestringtofile, snappath, content) + return ok +end + +local function acceptAll(result: snapty.snapresult) + -- Auto-accept all new snapshots and failures + local accepted = 0 + for _, n in result.new do + local ok = writeSnapshot(n.artifactpath, n.actual) + if ok then + accepted += 1 + else + print(`Failed to create {n.artifactpath}`) + end + end + for _, f in result.failed do + local ok = writeSnapshot(f.artifactpath, f.actual) + if ok then + accepted += 1 + else + print(`Failed to update {f.artifactpath}`) + end + end + print(`\nUpdated {accepted} snapshot(s).`) +end + +local function acceptInteractive(failures: { snapty.snapfail }, news: { snapty.snapnew }): number + local function prompt(acceptAll: boolean): string + if acceptAll then + return "a" + end + local prompt = sty.dim("[a]ccept [r]eject [A]ccept all > ") + local choice = io.input(prompt) + return strext.trim(choice) + end + + local function updateArtifact(choice, artifactpath, content): boolean + local created = false + if choice == "a" then + created = writeSnapshot(artifactpath, content) + if created then + print(sty.pass("\tCreated ") .. sty.filepath(artifactpath)) + else + print(sty.fail("\tFailed to write ") .. sty.filepath(artifactpath)) + end + else + print(sty.dim("\tSkipped")) + end + return created + end + + local accepted = 0 + + local acceptAll = false + -- Process new snapshots + + for i, n in news do + print(sty.bold(`\nNew Snapshot {i}/{#news}: `) .. sty.filepath(path.format(n.filepath))) + print(sty.dim("\tNo .snap file exists. Actual output:")) + print("") + print(n.actual) + print("") + + local ok = false + if acceptAll then + ok = updateArtifact("a", n.artifactpath, n.actual) + else + local choice = prompt(acceptAll) + if choice == "A" then + acceptAll = true + choice = "a" + end + ok = updateArtifact(choice, n.artifactpath, n.actual) + end + + accepted += if ok then 1 else 0 + end + + for i, f in failures do + print(sty.bold(`\nSnapshot {i}/{#failures}: `) .. sty.filepath(path.format(f.filepath))) + print("") + print(f.diff) + print("") + local ok = false + if acceptAll then + ok = updateArtifact("a", f.artifactpath, f.actual) + else + local choice = prompt(acceptAll) + if choice == "A" then + acceptAll = true + choice = "a" + end + ok = updateArtifact(choice, f.artifactpath, f.actual) + end + accepted += if ok then 1 else 0 + end + + return accepted +end + +local update = {} + +function update.updatesnapshot(result: snapty.snapresult, update: boolean, interactive: boolean) + local hasNew = #result.new > 0 + local hasFailed = #result.failed > 0 + if hasNew or hasFailed then + -- Auto-accept both failed and new output + if update then + acceptAll(result) + elseif interactive then + local accepted = acceptInteractive(result.failed, result.new) + print(`\nUpdated {accepted} snapshot(s).`) + else + if hasNew then + print(`\n{#result.new} new snapshot(s) found.`) + end + if hasFailed then + print(`{#result.failed} snapshot(s) failed.`) + end + + print(`Run with --interactive to review or --update to accept all`) + ps.exit(1) + end + end + ps.exit(0) +end + +return table.freeze(update) diff --git a/tests/std/snapshots/assert_buffereq_unequal_length.snap b/tests/std/snapshots/assert_buffereq_unequal_length.snap new file mode 100755 index 000000000..91e854ba3 --- /dev/null +++ b/tests/std/snapshots/assert_buffereq_unequal_length.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ buffereq_error + /tests/std/snapshots/assert_buffereq_unequal_length.snap.luau:6 + buffereq: buffers are of unequal length + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau b/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau new file mode 100644 index 000000000..77cf73021 --- /dev/null +++ b/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau @@ -0,0 +1,9 @@ +local test = require("@std/test") + +test.case("buffereq_error", function(assert) + local buf1 = buffer.create(1) + local buf2 = buffer.create(2) + assert.buffereq(buf1, buf2) +end) + +test.run() diff --git a/tests/std/snapshots/assert_eq_error_msg_in_case.snap b/tests/std/snapshots/assert_eq_error_msg_in_case.snap new file mode 100755 index 000000000..ec8872eb3 --- /dev/null +++ b/tests/std/snapshots/assert_eq_error_msg_in_case.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ eq_error + /tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau:4 + eq: 1 ~= 2 + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau b/tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau new file mode 100644 index 000000000..35896d6b0 --- /dev/null +++ b/tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau @@ -0,0 +1,7 @@ +local test = require("@std/test") + +test.case("eq_error", function(assert) + assert.eq(1, 2) +end) + +test.run() diff --git a/tests/std/snapshots/assert_eq_error_msg_in_suite.snap b/tests/std/snapshots/assert_eq_error_msg_in_suite.snap new file mode 100755 index 000000000..7b1ea31c3 --- /dev/null +++ b/tests/std/snapshots/assert_eq_error_msg_in_suite.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ eq_failure_suite.eq_error + /tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau:5 + eq: 1 ~= 2 + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau b/tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau new file mode 100644 index 000000000..781b4bf1a --- /dev/null +++ b/tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau @@ -0,0 +1,9 @@ +local test = require("@std/test") + +test.suite("eq_failure_suite", function(suite) + suite:case("eq_error", function(assert) + assert.eq(1, 2) + end) +end) + +test.run() diff --git a/tests/std/snapshots/assert_error_eq_no_error.snap b/tests/std/snapshots/assert_error_eq_no_error.snap new file mode 100755 index 000000000..4f5c7019a --- /dev/null +++ b/tests/std/snapshots/assert_error_eq_no_error.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ erroreq_error + /tests/std/snapshots/assert_error_eq_no_error.snap.luau:4 + erroreq: Function did not error as expected. + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_error_eq_no_error.snap.luau b/tests/std/snapshots/assert_error_eq_no_error.snap.luau new file mode 100644 index 000000000..e823a2404 --- /dev/null +++ b/tests/std/snapshots/assert_error_eq_no_error.snap.luau @@ -0,0 +1,9 @@ +local test = require("@std/test") + +test.case("erroreq_error", function(assert) + assert.erroreq(function() + return + end, "expected message") +end) + +test.run() diff --git a/tests/std/snapshots/assert_erroreq_error_message.snap b/tests/std/snapshots/assert_erroreq_error_message.snap new file mode 100755 index 000000000..5e6d9b8ec --- /dev/null +++ b/tests/std/snapshots/assert_erroreq_error_message.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ erroreq_failure_suite.erroreq_error + /tests/std/snapshots/assert_erroreq_error_message.snap.luau:5 + erroreq: Expected suffix of error message "expected message", but got "/tests/std/snapshots/assert_erroreq_error_message.snap.luau:6: wrong message" + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_erroreq_error_message.snap.luau b/tests/std/snapshots/assert_erroreq_error_message.snap.luau new file mode 100644 index 000000000..f70606c46 --- /dev/null +++ b/tests/std/snapshots/assert_erroreq_error_message.snap.luau @@ -0,0 +1,11 @@ +local test = require("@std/test") + +test.suite("erroreq_failure_suite", function(suite) + suite:case("erroreq_error", function(assert) + assert.erroreq(function() + error("wrong message") + end, "expected message") + end) +end) + +test.run() diff --git a/tests/std/snapshots/assert_neq_error_message_in_case.snap b/tests/std/snapshots/assert_neq_error_message_in_case.snap new file mode 100755 index 000000000..87c06895f --- /dev/null +++ b/tests/std/snapshots/assert_neq_error_message_in_case.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ neq_error + /tests/std/snapshots/assert_neq_error_message_in_case.snap.luau:4 + neq: 1 == 1 + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_neq_error_message_in_case.snap.luau b/tests/std/snapshots/assert_neq_error_message_in_case.snap.luau new file mode 100644 index 000000000..1cc64666d --- /dev/null +++ b/tests/std/snapshots/assert_neq_error_message_in_case.snap.luau @@ -0,0 +1,7 @@ +local test = require("@std/test") + +test.case("neq_error", function(assert) + assert.neq(1, 1) +end) + +test.run() diff --git a/tests/std/snapshots/assert_strcontains_instances_negative.snap b/tests/std/snapshots/assert_strcontains_instances_negative.snap new file mode 100755 index 000000000..bf3f6cbad --- /dev/null +++ b/tests/std/snapshots/assert_strcontains_instances_negative.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ strcontains_error + /tests/std/snapshots/assert_strcontains_instances_negative.snap.luau:4 + strcontains: instances must be greater than 0 + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau b/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau new file mode 100644 index 000000000..cf716e4ce --- /dev/null +++ b/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau @@ -0,0 +1,7 @@ +local test = require("@std/test") + +test.case("strcontains_error", function(assert) + assert.strcontains("hello world", "goodbye", nil, -1) +end) + +test.run() diff --git a/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap new file mode 100755 index 000000000..1838d16d7 --- /dev/null +++ b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ strcontains_failure_suite.strcontains_error + /tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau:5 + strcontains: Expected "hello world" to contain "goodbye". + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau new file mode 100644 index 000000000..9831f751d --- /dev/null +++ b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau @@ -0,0 +1,9 @@ +local test = require("@std/test") + +test.suite("strcontains_failure_suite", function(suite) + suite:case("strcontains_error", function(assert) + assert.strcontains("hello world", "goodbye") + end) +end) + +test.run() diff --git a/tests/std/snapshots/assert_strcontains_single_instance_fails.snap b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap new file mode 100755 index 000000000..947429995 --- /dev/null +++ b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ strcontains_failure_suite.strcontains_error + /tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau:5 + strcontains: Expected "hello world" to contain "goodbye". + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau new file mode 100644 index 000000000..9831f751d --- /dev/null +++ b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau @@ -0,0 +1,9 @@ +local test = require("@std/test") + +test.suite("strcontains_failure_suite", function(suite) + suite:case("strcontains_error", function(assert) + assert.strcontains("hello world", "goodbye") + end) +end) + +test.run() diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_case.snap b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap new file mode 100755 index 000000000..6b150e603 --- /dev/null +++ b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ tableeq_error + /tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau:4 + tableeq: lhs[a] = 1 but rhs[a] = 2 + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau new file mode 100644 index 000000000..9d55def06 --- /dev/null +++ b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau @@ -0,0 +1,7 @@ +local test = require("@std/test") + +test.case("tableeq_error", function(assert) + assert.tableeq({ a = 1 }, { a = 2 }) +end) + +test.run() diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap new file mode 100755 index 000000000..82d5a0f22 --- /dev/null +++ b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ tableeq_failure_suite.tableeq_error + /tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau:5 + tableeq: lhs[a] = 1 but rhs[a] = 2 + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau new file mode 100644 index 000000000..5de8ba608 --- /dev/null +++ b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau @@ -0,0 +1,9 @@ +local test = require("@std/test") + +test.suite("tableeq_failure_suite", function(suite) + suite:case("tableeq_error", function(assert) + assert.tableeq({ a = 1 }, { a = 2 }) + end) +end) + +test.run() diff --git a/tests/std/snapshots/assertneq_error_message_in_suite.snap b/tests/std/snapshots/assertneq_error_message_in_suite.snap new file mode 100755 index 000000000..1d7712b08 --- /dev/null +++ b/tests/std/snapshots/assertneq_error_message_in_suite.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ neq_failure_suite.neq_error + /tests/std/snapshots/assertneq_error_message_in_suite.snap.luau:5 + neq: 1 == 1 + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/assertneq_error_message_in_suite.snap.luau b/tests/std/snapshots/assertneq_error_message_in_suite.snap.luau new file mode 100644 index 000000000..3418f8570 --- /dev/null +++ b/tests/std/snapshots/assertneq_error_message_in_suite.snap.luau @@ -0,0 +1,9 @@ +local test = require("@std/test") + +test.suite("neq_failure_suite", function(suite) + suite:case("neq_error", function(assert) + assert.neq(1, 1) + end) +end) + +test.run() diff --git a/tests/std/snapshots/buffereq_failure_suite.snap b/tests/std/snapshots/buffereq_failure_suite.snap new file mode 100755 index 000000000..ecb961346 --- /dev/null +++ b/tests/std/snapshots/buffereq_failure_suite.snap @@ -0,0 +1,17 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ buffereq_failure_suite.buffereq_error + /tests/std/snapshots/buffereq_failure_suite.snap.luau:9 + buffereq: lhs[0] = 54, but rhs[0] = 55 + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/buffereq_failure_suite.snap.luau b/tests/std/snapshots/buffereq_failure_suite.snap.luau new file mode 100644 index 000000000..2062ba1f7 --- /dev/null +++ b/tests/std/snapshots/buffereq_failure_suite.snap.luau @@ -0,0 +1,13 @@ +local test = require("@std/test") + +test.suite("buffereq_failure_suite", function(suite) + suite:case("buffereq_error", function(assert) + local buf1 = buffer.create(2) + local buf2 = buffer.create(2) + buffer.writeu8(buf1, 0, 84) + buffer.writeu8(buf2, 0, 85) + assert.buffereq(buf1, buf2) + end) +end) + +test.run() diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap new file mode 100755 index 000000000..7609a2b89 --- /dev/null +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap @@ -0,0 +1,25 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ nil_field_suite.access_field_of_nil + Runtime error: /tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6: attempt to index nil with 'field' +Stacktrace: +/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6: attempt to index nil with 'field' +@std/test/failure.luau:27 function runtimeerror +@std/test/runner.luau:117 function handlefailure +/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6 +@std/test/runner.luau:130 function run +@std/test/init.luau:103 function run +/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:10 + + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau new file mode 100644 index 000000000..9d6ccc412 --- /dev/null +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau @@ -0,0 +1,10 @@ +local test = require("@std/test") + +test.suite("nil_field_suite", function(suite) + suite:case("access_field_of_nil", function(assert) + local t = nil + local value = t.field -- This will cause an error + end) +end) + +test.run() diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap new file mode 100755 index 000000000..b70a72f72 --- /dev/null +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap @@ -0,0 +1,25 @@ + +================================================== +TEST RESULTS +================================================== + +Failed Tests (1): + +❌ access_field_of_nil + Runtime error: /tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5: attempt to index nil with 'field' +Stacktrace: +/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5: attempt to index nil with 'field' +@std/test/failure.luau:27 function runtimeerror +@std/test/runner.luau:87 function handlefailure +/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5 +@std/test/runner.luau:93 function run +@std/test/init.luau:103 function run +/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:8 + + +-------------------------------------------------- +Total: 1 +Passed: 0 ✓ +Failed: 1 ✗ +================================================== + diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau new file mode 100644 index 000000000..e4ef6289a --- /dev/null +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau @@ -0,0 +1,8 @@ +local test = require("@std/test") + +test.case("access_field_of_nil", function(assert) + local t = nil + local value = t.field -- This will cause an error +end) + +test.run() diff --git a/tools/check.luau b/tools/check.luau index 1eecc3596..979970db9 100644 --- a/tools/check.luau +++ b/tools/check.luau @@ -20,13 +20,25 @@ local TYPECHECK_PATHS = { "tools", } +local EXCLUDE_TYPECHECK = { + "tests/std/snapshots", +} + local function collectFiles(dir: string): { string } local files: { string } = {} local it = fs.walk(dir, { recursive = true }) local p = it() while p do local pathStr = path.format(p) - if fs.type(p) == "file" and string.match(pathStr, "%.luau$") then + local excluded = false + for _, excl in EXCLUDE_TYPECHECK do + if string.find(pathStr, excl, 1, true) then + excluded = true + break + end + end + + if not excluded and fs.type(p) == "file" and string.match(pathStr, "%.luau$") then table.insert(files, pathStr) end p = it() From 4ecb0b2f8d50fb9b5dcb185cd8a95eab06490792 Mon Sep 17 00:00:00 2001 From: Hunter Goldstein Date: Mon, 23 Feb 2026 08:36:25 -0800 Subject: [PATCH 368/642] typing: various random fixes / comment rewrites (#841) Did another pass of the faillist. I've added some more detailed comments around the spots where `table.insert` is falling over, but most of these are not legitimate errors / are more "nothing is going wrong but we have to hold Luau's hand." --- examples/docs/test_module.luau | 2 +- examples/json.luau | 2 +- examples/lints/almost_swapped.luau | 35 +++++++++++++++++++----------- examples/lints/divide_by_zero.luau | 8 +++---- examples/process.luau | 2 +- lute/std/libs/fs.luau | 8 +++++-- lute/std/libs/test/assert.luau | 2 +- lute/std/libs/test/types.luau | 4 +++- tests/runtime/crypto.test.luau | 4 +++- tests/src/staticrequires/main.luau | 2 +- tests/std/fs.test.luau | 6 ++++- tools/check-faillist.txt | 30 +++++++------------------ 12 files changed, 56 insertions(+), 49 deletions(-) diff --git a/examples/docs/test_module.luau b/examples/docs/test_module.luau index 2dafce5a5..b0987532c 100644 --- a/examples/docs/test_module.luau +++ b/examples/docs/test_module.luau @@ -28,7 +28,7 @@ end ]] --- this should also not be captured because there is non function or property code -local ignore = "value" +local _ignore = "value" --[=[ Block comment with equals above function declaration diff --git a/examples/json.luau b/examples/json.luau index 1ff086df3..c2c7e97c0 100644 --- a/examples/json.luau +++ b/examples/json.luau @@ -9,5 +9,5 @@ print(serialize) local deserialized = json.deserialize([[{ "hello": "world" }]]) - +deserialized = assert(json.asobject(deserialized)) print(deserialized.hello) diff --git a/examples/lints/almost_swapped.luau b/examples/lints/almost_swapped.luau index 4cce1e31e..3c6a06acf 100644 --- a/examples/lints/almost_swapped.luau +++ b/examples/lints/almost_swapped.luau @@ -59,7 +59,7 @@ end -- Report instances of attempted swaps like: -- a = b; b = a -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } local violations = {} local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes @@ -80,18 +80,27 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintType local nextVar, nextVal = nextStat.variables[1].node, nextStat.values[1].node if compFuncs.refExprsSame(currVar, nextVal) and compFuncs.refExprsSame(nextVar, currVal) then - table.insert(violations, { -- LUAUFIX: severity isn't inferred as a singleton, so table.insert is mad - lintname = name, - location = syntax.span.create({ - beginline = currStat.location.beginline, - begincolumn = currStat.location.begincolumn, - endline = nextStat.location.endline, - endcolumn = nextStat.location.endcolumn, - }), - message = message, - severity = "warning", - sourcepath = sourcepath, - }) + -- FIXME(Luau): + -- 1. We don't infer "warning" as a singleton. + -- 2. Bidirectional type checking of tables does not kick in. + table.insert( + violations, + { + lintname = name, + location = syntax.span.create({ + beginline = currStat.location.beginline, + begincolumn = currStat.location.begincolumn, + endline = nextStat.location.endline, + endcolumn = nextStat.location.endcolumn, + }), + message = message, + severity = "warning", + sourcepath = sourcepath, + -- We should be able to remove this assertion + -- once we can bidirectionally infer instantiated + -- generic functions. + } :: lintTypes.LintViolation + ) end end end diff --git a/examples/lints/divide_by_zero.luau b/examples/lints/divide_by_zero.luau index d4e6068fa..04794f292 100644 --- a/examples/lints/divide_by_zero.luau +++ b/examples/lints/divide_by_zero.luau @@ -9,7 +9,7 @@ local utils = require("@std/syntax/utils") local name = "divide_by_zero" local message = "Division by zero detected." -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintTypes.LintViolation } +local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } return query .findallfromroot(ast, utils.isExprBinary) :filter(function(bin) @@ -19,9 +19,9 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.path): { lintType return bin.rhsoperand.kind == "expr" and bin.rhsoperand.tag == "number" and bin.rhsoperand.value == 0 end) :maptoarray( - function( - n: syntax.AstExprBinary - ): lintTypes.LintViolation -- LUAUFIX: Bidiretional inference of generics should let us not need this annotation + -- FIXME(Luau): We would need bidirectional inference of generics + -- and return types to remove this annotation. + function(n): lintTypes.LintViolation return { lintname = name, location = n.location, diff --git a/examples/process.luau b/examples/process.luau index c1b5dd4f4..a851ab2d0 100644 --- a/examples/process.luau +++ b/examples/process.luau @@ -1,7 +1,7 @@ local process = require("@std/process") local task = require("@std/task") -process.system("echo hello", { cwd = {} }) +process.system("echo hello", {}) local result = process.run({ "echo", "Hello, lute!" }) diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index c97daf524..a9b61001b 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -78,8 +78,12 @@ end --- Also provides a `close` method to stop watching. --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.watch as `for _ in fs.watch(...) do` directly. A while loop can be used instead. See example/watch_directory.luau for usage. function fslib.watch(path: pathlike): watcher - local deq = deque.new() - local handle = fs.watch(pathlib.format(path), function(filename: string, event: watchevent) + -- In the future this could be written with explicit instantiation syntax + -- as in: + -- + -- deque.new<<{ filename: path, event: watchevent }>>() + local deq = deque.new(nil :: { filename: path, event: watchevent }?) + local handle = fs.watch(pathlib.format(path), function(filename, event) deq:pushback({ filename = pathlib.parse(filename), event = event }) end) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index cf3f1939c..16f8df0b4 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -90,7 +90,7 @@ local function errors(callback: () -> (), msg: string?): failure? end -- Table equality assertionsion with deep comparison -local function tableeq(lhs: { [unknown]: unknown }, rhs: { [unknown]: unknown }): failure? +local function tableeq(lhs: { [any]: any }, rhs: { [any]: any }): failure? local equal, msg = tablesEqual(lhs, rhs, "") if equal then return nil diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index 2a00284ab..06293724f 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -30,7 +30,9 @@ export type asserts = { eq: (T, T, string?) -> failure?, neq: (T, T, string?) -> failure?, errors: (() -> (), string?) -> failure?, - tableeq: ({ [unknown]: unknown }, { [unknown]: unknown }) -> failure?, + -- FIXME(Luau): We would like this to be two read-only indexers, but the + -- best option we have otherwise is two error suppressing indexers. + tableeq: ({ [any]: any }, { [any]: any }) -> failure?, buffereq: (buffer, buffer) -> failure?, erroreq: ((A...) -> ...unknown, string, A...) -> failure?, strcontains: (string, string, string?, number?) -> failure?, diff --git a/tests/runtime/crypto.test.luau b/tests/runtime/crypto.test.luau index 2ea6b9fe7..0f0cca7c3 100644 --- a/tests/runtime/crypto.test.luau +++ b/tests/runtime/crypto.test.luau @@ -63,7 +63,9 @@ test.suite("CryptoRuntimeTests", function(suite) } assert.errors(function() - crypto.secretbox.open(other) + -- We are testing that this fails when `ciphertext` is a string + -- rather than a buffer, so we must lie to Luau. + crypto.secretbox.open(other :: any) end) end) diff --git a/tests/src/staticrequires/main.luau b/tests/src/staticrequires/main.luau index cd55e308f..75777e04f 100644 --- a/tests/src/staticrequires/main.luau +++ b/tests/src/staticrequires/main.luau @@ -1,6 +1,6 @@ -- lute-lint-global-ignore(unused_variable) -- test that you can require in an alias aware fashion -local example = require("@example/option") +local _example = require("@example/option") -- print(`Required {example.file} from alias @example`) -- Entry point that requires other modules diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 9279c0346..77958e4b7 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -1,3 +1,4 @@ +local failure = require("@std/test/failure") local fs = require("@std/fs") local path = require("@std/path") local system = require("@std/system") @@ -110,7 +111,10 @@ test.suite("FsSuite", function(suite) until event print("Watch iterator time taken: " .. (os.clock() - start)) - assert.neq(event, nil) + if event == nil then + failure.assertion("watcher event was nil") + return + end assert.eq(event.change or event.rename, true) watcher:close() diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index ccae8c4fb..e9e58b336 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -3,12 +3,6 @@ batteries/cli.luau:148:6-45 batteries/difftext/myersdiff.luau:76:8-22 batteries/toml.luau:167:24-28 examples/colorful.luau:1:18-47 -examples/docs/test_module.luau:31:7-12 -examples/json.luau:13:7-24 -examples/json.luau:13:7-24 -examples/lints/almost_swapped.luau:83:5-16 -examples/lints/almost_swapped.luau:104:9-12 -examples/lints/divide_by_zero.luau:38:9-12 examples/net_example.luau:8:2-48 examples/net_example.luau:13:33-43 examples/net_example.luau:15:24-34 @@ -30,7 +24,6 @@ examples/parallel_sort.luau:85:9-13 examples/parallel_sort.luau:90:24-25 examples/parallel_sort.luau:91:9-13 examples/parallel_sort_helper.luau:4:11-15 -examples/process.luau:4:38-39 examples/process.luau:11:24-34 examples/process.luau:12:24-34 examples/process.luau:13:24-34 @@ -89,14 +82,12 @@ lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:75:9-14 lute/cli/commands/transform/lib/arguments.luau:75:9-14 -lute/std/libs/fs.luau:93:11-20 -lute/std/libs/fs.luau:93:11-20 -lute/std/libs/fs.luau:137:34-39 -lute/std/libs/fs.luau:137:34-39 -lute/std/libs/fs.luau:186:52-68 -lute/std/libs/fs.luau:186:52-68 -lute/std/libs/fs.luau:194:11-17 -lute/std/libs/fs.luau:194:11-17 +lute/std/libs/fs.luau:141:34-39 +lute/std/libs/fs.luau:141:34-39 +lute/std/libs/fs.luau:190:52-68 +lute/std/libs/fs.luau:190:52-68 +lute/std/libs/fs.luau:198:11-17 +lute/std/libs/fs.luau:198:11-17 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:42:25-29 @@ -168,7 +159,6 @@ tests/batteries/collections/deque.test.luau:61:13-16 tests/batteries/collections/deque.test.luau:81:13-16 tests/batteries/collections/deque.test.luau:86:13-16 tests/batteries/collections/deque.test.luau:91:13-16 -tests/runtime/crypto.test.luau:66:26-30 tests/src/packages/package_aware_require/dep/module.luau:1:16-30 tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau:1:16-38 tests/src/require/config_tests/config_ambiguity/requirer.luau:1:8-22 @@ -178,10 +168,8 @@ tests/src/require/without_config/ambiguous_directory_requirer.luau:1:16-58 tests/src/require/without_config/ambiguous_file_requirer.luau:1:16-53 tests/src/staticrequires/circular_a.luau:2:11-33 tests/src/staticrequires/circular_b.luau:2:11-33 -tests/src/staticrequires/main.luau:3:7-13 -tests/std/fs.test.luau:114:13-24 -tests/std/fs.test.luau:133:18-20 -tests/std/fs.test.luau:142:18-20 +tests/std/fs.test.luau:137:18-20 +tests/std/fs.test.luau:146:18-20 tests/std/process.test.luau:39:18-18 tests/std/process.test.luau:46:18-18 tests/std/process.test.luau:55:17-18 @@ -250,6 +238,4 @@ tests/std/syntax/printer.test.luau:839:11-100 tests/std/syntax/printer.test.luau:870:11-100 tests/std/syntax/query.test.luau:41:7-7 tests/std/syntax/query.test.luau:41:7-7 -tests/std/tableext.test.luau:57:18-18 -tests/std/tableext.test.luau:62:18-18 tests/std/tableext.test.luau:63:18-18 From ada5bd2a1cec990313baffc81f7bc46c41f29d40 Mon Sep 17 00:00:00 2001 From: Hunter Goldstein Date: Mon, 23 Feb 2026 08:46:08 -0800 Subject: [PATCH 369/642] typing: rewrite `deque` to indicate it has a metatable (#840) `deque` is written in the current Luau OOP style: slap a metatable with `__index` and you're off to the races. The typing doesn't indicate this, which creates errant lint warnings of: ``` Using '#' on a table without an array part is likely a bug ``` Originally I thought this was an Analysis issue, but I realized that we aren't even _telling_ Analysis that `deque` has a metatable, so it thinks `__len` is just a normal table property. I added one or two annotations as a smoke check against horribly breaking the typing of `deque`. --- batteries/collections/deque.luau | 12 ++++-------- tests/batteries/collections/deque.test.luau | 4 ++-- tools/check-faillist.txt | 12 ------------ 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/batteries/collections/deque.luau b/batteries/collections/deque.luau index 77824b2e8..d9ab15e7d 100644 --- a/batteries/collections/deque.luau +++ b/batteries/collections/deque.luau @@ -1,17 +1,13 @@ -export type Deque = { +type DequeData = { _head: number, _tail: number, _values: { [number]: T }, - pushfront: (self: Deque, value: T) -> (), - pushback: (self: Deque, value: T) -> (), - popfront: (self: Deque) -> T, - popback: (self: Deque) -> T, - peekfront: (self: Deque) -> T?, - peekback: (self: Deque) -> T?, - __len: (self: Deque) -> number, } local deque = {} + +export type Deque = setmetatable, typeof(deque)> + deque.__index = deque deque.__len = function(self: Deque) return self._tail - self._head + 1 diff --git a/tests/batteries/collections/deque.test.luau b/tests/batteries/collections/deque.test.luau index 590fe709a..e2cc0e050 100644 --- a/tests/batteries/collections/deque.test.luau +++ b/tests/batteries/collections/deque.test.luau @@ -34,7 +34,7 @@ test.suite("deque", function(suite) suite:case("deque:popfront", function(assert) local deq = deque.new(2) - local popped = deq:popfront() + local popped: number = deq:popfront() assert.eq(popped, 2) assert.eq(#deq, 0) assert.eq(deq:peekfront(), nil) @@ -43,7 +43,7 @@ test.suite("deque", function(suite) suite:case("deque:popback", function(assert) local deq = deque.new(2) - local popped = deq:popback() + local popped: number = deq:popback() assert.eq(popped, 2) assert.eq(#deq, 0) assert.eq(deq:peekfront(), nil) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index e9e58b336..1125a649b 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -1,6 +1,5 @@ batteries/cli.luau:144:36-75 batteries/cli.luau:148:6-45 -batteries/difftext/myersdiff.luau:76:8-22 batteries/toml.luau:167:24-28 examples/colorful.luau:1:18-47 examples/net_example.luau:8:2-48 @@ -148,17 +147,6 @@ lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 -tests/batteries/collections/deque.test.luau:9:13-16 -tests/batteries/collections/deque.test.luau:16:13-16 -tests/batteries/collections/deque.test.luau:24:13-16 -tests/batteries/collections/deque.test.luau:32:13-16 -tests/batteries/collections/deque.test.luau:39:13-16 -tests/batteries/collections/deque.test.luau:48:13-16 -tests/batteries/collections/deque.test.luau:55:13-16 -tests/batteries/collections/deque.test.luau:61:13-16 -tests/batteries/collections/deque.test.luau:81:13-16 -tests/batteries/collections/deque.test.luau:86:13-16 -tests/batteries/collections/deque.test.luau:91:13-16 tests/src/packages/package_aware_require/dep/module.luau:1:16-30 tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau:1:16-38 tests/src/require/config_tests/config_ambiguity/requirer.luau:1:8-22 From d3ab7a81840aa62e887d5566fbe66f560b1f3227 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:30:52 -0500 Subject: [PATCH 370/642] Write `.luaurc` to generated typedefs folder (#844) There is no `.luaurc` file included in the generated type definitions folder. Files within the folder using any lute-reserved alias, like the syntax types, will yield errors when consumed. This PR ensures `lute setup` generates a `.luaurc` file in the type definitions --- lute/cli/commands/setup/init.luau | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index a0bd745ae..9a3fc069a 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -30,6 +30,9 @@ for key, value in definitions :: { [string]: string } do fs.writestringtofile(filePath, value) end +local typedefsLuauRcPath = path.join(BASE_PATH, ".luaurc") +fs.writestringtofile(typedefsLuauRcPath, json.serialize(TEMPLATE_RC_FILE, true)) + print(`Successfully wrote type definition files to {BASE_PATH_PRETTY}`) if not table.find(args, "--with-luaurc") then From ece8ccecfafb8c021855adda87fe94cbb4bfd2d5 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 26 Feb 2026 13:20:00 -0800 Subject: [PATCH 371/642] Updates unused_variable lint rule to allow configuring write-only APIs (#834) This supports reporting patterns like the following, where `t` is unused because it's only inserted into: ```luau local t = {} table.insert(t, 2) ``` --- .../commands/lint/rules/unused_variable.luau | 130 ++++++- tests/cli/lint.test.luau | 316 ++++++++++++++++++ tools/check-faillist.txt | 5 +- 3 files changed, 445 insertions(+), 6 deletions(-) diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index 83ad49bc0..3d83a0db6 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -8,11 +8,47 @@ local name = "unused_variable" local message = "Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused." local target = "https://lute.luau.org/cli/lint/unused_variable.html" +type leaf = { tag: "leaf", vals: { [number]: true } } + +type branch = { tag: "branch", entries: { [string]: leaf } } + +-- We assume that we only need 2 levels of depth, since there currently no APIs nested more than 2 levels. +-- If we need more levels in the future, we can generalize, but this greatly simplifies the logic. +type smallTrie = { tag: "root", entries: { [string]: branch | leaf } } + local function isRequireCall(expr: syntax.AstExpr) return expr.tag == "call" and expr.func.tag == "global" and expr.func.name.text == "require" end -local function unusedLocals(block: syntax.AstStatBlock): { syntax.AstLocal } +local function getWriteOnlyArgs(func: syntax.AstExpr, writeOnlyAPIs: smallTrie): { [number]: true }? + if func.tag == "global" then + local entry = writeOnlyAPIs.entries[func.name.text] + + if not entry or entry.tag ~= "leaf" then + return nil + end + + return entry.vals + elseif + (func.tag == "indexname" and func.expression.tag == "global") + or (func.tag == "index" and func.expression.tag == "global" and func.index.tag == "string") -- LUAUFIX: Refinement bug here: func should be narrowed to AstExprIndexExpr (CLI-190889) + then + func = func :: syntax.AstExprIndexName | syntax.AstExprIndexExpr + local branch = writeOnlyAPIs.entries[(func.expression :: syntax.AstExprGlobal).name.text] + + if not branch or branch.tag ~= "branch" then + return nil + end + + local entry = branch.entries[(func.index :: syntax.Token).text] + + return if entry then entry.vals else nil + else + return nil + end +end + +local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie): { syntax.AstLocal } local visitor: visitorLib.Visitor & { unusedLocals: { syntax.AstLocal }, scopes: { { [syntax.AstLocal]: boolean } }, @@ -293,26 +329,110 @@ local function unusedLocals(block: syntax.AstStatBlock): { syntax.AstLocal } return false end + function visitor.visitExprCall(node: syntax.AstExprCall) + local writeOnlyIndices = getWriteOnlyArgs(node.func, writeOnlyAPIs) + + if not writeOnlyIndices then + return true + end + + visitorLib.visitexpression(node.func, visitor) + + for i, arg in node.arguments do + if writeOnlyIndices[i] then + local prevInLValueContext = visitor.inLValueContext + visitor.inLValueContext = true + visitorLib.visitexpression(arg.node, visitor) + visitor.inLValueContext = prevInLValueContext + else + visitorLib.visitexpression(arg.node, visitor) + end + end + + return false + end + visitorLib.visitblock(block, visitor) return visitor.unusedLocals end +local function isPositiveInt(v: unknown): boolean + return type(v) == "number" and v > 0 and math.floor(v) == v +end + local function lint( ast: syntax.AstStatBlock, sourcepath: path.path?, - _context: lintTypes.RuleContext + context: lintTypes.RuleContext ): { lintTypes.LintViolation } - return tableext.map(unusedLocals(ast), function(l): lintTypes.LintViolation + -- Validate context.options, which should be of form { ["api"] = { number } }, + -- where the number keys represent argument indices which only write to their passed values. + for api, args in context.options do + local errorMessage = + `Expected options for API '{api}' to be an array of argument indices which only write to their passed values` + if type(args) ~= "table" then + error(errorMessage) + end + + for k, v in args do + if not isPositiveInt(k) or not isPositiveInt(v) then + error(errorMessage) + end + end + end + + local writeOnlyAPIs: smallTrie = { + tag = "root", + entries = { + table = { tag = "branch", entries = { insert = { tag = "leaf", vals = { [1] = true } } } }, + }, + } + + for k, v in context.options do + local parts = k:split(".") + if #parts < 1 or #parts > 2 then + error(`Invalid API '{k}' in options. Only APIs up to 2 levels deep are supported.`) + end + + if #parts == 1 then + local entry = writeOnlyAPIs.entries[parts[1]] + + if entry then + error( + `API '{parts[1]}' is already defined as {if entry.tag == "branch" then "part of " else ""}a write-only API, so '{k}' cannot be added as a write-only API.` + ) + end + + writeOnlyAPIs.entries[parts[1]] = { tag = "leaf", vals = tableext.toset(v :: { number }) } -- We validate v above, so the cast is ok + else + if not writeOnlyAPIs.entries[parts[1]] then + writeOnlyAPIs.entries[parts[1]] = { + tag = "branch", + entries = { [parts[2]] = { tag = "leaf", vals = tableext.toset(v :: { number }) } }, + } + else + local branch = writeOnlyAPIs.entries[parts[1]] + if branch.tag ~= "branch" then + error( + `API '{parts[1]}' is already defined as a write-only API, so '{k}' cannot be added as a write-only API.` + ) + end + branch.entries[parts[2]] = { tag = "leaf", vals = tableext.toset(v :: { number }) } -- We validate v above, so the cast is ok + end + end + end + + return tableext.map(unusedLocals(ast, writeOnlyAPIs), function(l): lintTypes.LintViolation return { lintname = name, - location = l.location, + location = l.location, -- LUAUFIX: Bidirectional inference should let us know that l : syntax.AstLocal message = message, severity = "warning", sourcepath = sourcepath, target = target, tags = { "unnecessary" }, - } -- LUAUFIX: Table literal doesn't subtype against lintTypes.LintViolation for some reason + } end) end diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 455574265..e31d64341 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -2152,6 +2152,322 @@ end }) end) + suite:case("unused_variable with table.insert default write-only API", function(assert) + lintTestHelper(assert, { + content = [[ +local t = {} +table.insert(t, 1) +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ +violator.luau:1:7-8 ── + │ + 1 │ local t = {} + │ ^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("unused_variable with custom write-only API (single level)", function(assert) + lintTestHelper(assert, { + content = [[ +local output = {} +myWrite(output, "data") +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, + configContents = [[ +return { + lute = { + lint = { + rules = { + ["unused_variable"] = { + options = { + myWrite = { 1 } + } + } + } + } + } +} + ]], + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ +violator.luau:1:7-13 ── + │ + 1 │ local output = {} + │ ^^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("unused_variable with custom write-only API (nested)", function(assert) + lintTestHelper(assert, { + content = [[ +local buffer = {} +fs.write(buffer, "content") +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, + configContents = [[ +return { + lute = { + lint = { + rules = { + ["unused_variable"] = { + options = { + ["fs.write"] = { 1 } + } + } + } + } + } +} + ]], + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ +violator.luau:1:7-13 ── + │ + 1 │ local buffer = {} + │ ^^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("unused_variable with custom write-only API (nested, index expr access)", function(assert) + lintTestHelper(assert, { + content = [[ +local buffer = {} +fs["write"](buffer, "content") +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, + configContents = [[ +return { + lute = { + lint = { + rules = { + ["unused_variable"] = { + options = { + ["fs.write"] = { 1 } + } + } + } + } + } +} + ]], + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ +violator.luau:1:7-13 ── + │ + 1 │ local buffer = {} + │ ^^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("unused_variable with multiple write-only args", function(assert) + lintTestHelper(assert, { + content = [[ +local a = {} +local b = {} +myMultiWrite(a, b, "data") +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, + configContents = [[ +return { + lute = { + lint = { + rules = { + ["unused_variable"] = { + options = { + myMultiWrite = { 1, 2 } + } + } + } + } + } +} + ]], + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 2, + }, + { + expectedOutput = [[ +violator.luau:1:7-8 ── + │ + 1 │ local a = {} + │ ^ + │ +]], + count = 1, + }, + { + expectedOutput = [[ +violator.luau:2:7-8 ── + │ + 2 │ local b = {} + │ ^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("unused_variable with write-only arg at position 2", function(assert) + lintTestHelper(assert, { + content = [[ +local config = "read" +local output = {} +myFunc(config, output) +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, + configContents = [[ +return { + lute = { + lint = { + rules = { + ["unused_variable"] = { + options = { + myFunc = { 2 } + } + } + } + } + } +} + ]], + outputExpectations = { + { + expectedOutput = LINT_RULE_MESSAGES.unused_variable, + count = 1, + }, + { + expectedOutput = [[ +violator.luau:2:7-13 ── + │ + 2 │ local output = {} + │ ^^^^^^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("unused_variable with deep nesting errors", function(assert) + lintTestHelper(assert, { + content = "local x = 1", + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, + configContents = [[ +return { + lute = { + lint = { + rules = { + ["unused_variable"] = { + options = { + ["deeply.nested.api"] = { 1 } + } + } + } + } + } +} + ]], + outputExpectations = { + { + expectedOutput = "Invalid API 'deeply.nested.api' in options. Only APIs up to 2 levels deep are supported.", + count = 1, + }, + }, + }) + end) + + suite:case("unused_variable with conflicting API definitions errors", function(assert) + lintTestHelper(assert, { + content = "local x = 1", + expectedExitCode = 0, + rulePath = defaultRulesPaths.unused_variable, + disableDefaultLints = true, + configContents = [[ +return { + lute = { + lint = { + rules = { + ["unused_variable"] = { + options = { + myApi = { 1 }, + ["myApi.nested"] = { 1 } + } + } + } + } + } +} + ]], + outputExpectations = { + { + expectedOutput = "API 'myApi' is already defined as part of a write-only API, so 'myApi' cannot be added as a write-only API.", + count = 1, + }, + }, + }) + end) + suite:case("throws error when config has invalid structure", function(assert) -- local invalidGlobalConfig = [[ -- return { diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 1125a649b..9528525db 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -55,7 +55,10 @@ lute/cli/commands/lint/rules/almost_swapped.luau:92:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 lute/cli/commands/lint/rules/parenthesized_conditions.luau:68:4-15 -lute/cli/commands/lint/rules/unused_variable.luau:309:15-24 +lute/cli/commands/lint/rules/unused_variable.luau:34:31-49 +lute/cli/commands/lint/rules/unused_variable.luau:34:67-76 +lute/cli/commands/lint/rules/unused_variable.luau:34:67-80 +lute/cli/commands/lint/rules/unused_variable.luau:429:15-24 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau:25:42-59 From 540be0cb385db2c8d754faf9d8871b251920d32c Mon Sep 17 00:00:00 2001 From: ariel Date: Thu, 26 Feb 2026 14:41:22 -0800 Subject: [PATCH 372/642] Move crypto `runtime` tests into `lute` (#848) We seem to have ended up with two separate folders for runtime tests authored in luau, which seems weird and we can obviously clean that up. --- tests/{runtime => lute}/crypto.test.luau | 0 tools/check-faillist.txt | 3 --- tools/check.luau | 1 - 3 files changed, 4 deletions(-) rename tests/{runtime => lute}/crypto.test.luau (100%) diff --git a/tests/runtime/crypto.test.luau b/tests/lute/crypto.test.luau similarity index 100% rename from tests/runtime/crypto.test.luau rename to tests/lute/crypto.test.luau diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 9528525db..7b49e5d1a 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -55,9 +55,6 @@ lute/cli/commands/lint/rules/almost_swapped.luau:92:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 lute/cli/commands/lint/rules/parenthesized_conditions.luau:68:4-15 -lute/cli/commands/lint/rules/unused_variable.luau:34:31-49 -lute/cli/commands/lint/rules/unused_variable.luau:34:67-76 -lute/cli/commands/lint/rules/unused_variable.luau:34:67-80 lute/cli/commands/lint/rules/unused_variable.luau:429:15-24 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 lute/cli/commands/pkg/loom-core/extern/pp.luau:97:3-14 diff --git a/tools/check.luau b/tools/check.luau index 979970db9..9f5e68686 100644 --- a/tools/check.luau +++ b/tools/check.luau @@ -14,7 +14,6 @@ local TYPECHECK_PATHS = { "tests/batteries", "tests/cli", "tests/lute", - "tests/runtime", "tests/src", "tests/std", "tools", From 8a4a73b9924c18befdf9182f90d1e2885ab1b626 Mon Sep 17 00:00:00 2001 From: ariel Date: Thu, 26 Feb 2026 15:03:26 -0800 Subject: [PATCH 373/642] Implement comparison operators for instants. (#847) Resolves #846 by implementing the three comparison metamethods for `Instants` in Lute. --- definitions/time.luau | 3 +++ examples/time_instants.luau | 3 +++ lute/time/src/time.cpp | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/definitions/time.luau b/definitions/time.luau index e45d0e44f..41c1eac7b 100644 --- a/definitions/time.luau +++ b/definitions/time.luau @@ -109,6 +109,9 @@ export type Instant = setmetatable Duration, + read __eq: (Instant, Instant) -> boolean, + read __lt: (Instant, Instant) -> boolean, + read __le: (Instant, Instant) -> boolean, }> return time diff --git a/examples/time_instants.luau b/examples/time_instants.luau index 44878d259..00abc4535 100644 --- a/examples/time_instants.luau +++ b/examples/time_instants.luau @@ -6,4 +6,7 @@ for _ = 1, 10000 do end local stop = time.now() +assert(stop > start) +assert(stop ~= start) +assert(start == start) print((stop - start):tomicroseconds()) diff --git a/lute/time/src/time.cpp b/lute/time/src/time.cpp index c73c3219d..052610fd4 100644 --- a/lute/time/src/time.cpp +++ b/lute/time/src/time.cpp @@ -242,6 +242,33 @@ static int instant__sub(lua_State* L) return createDurationFromSeconds(L, static_cast(diffTimespecs(left, right))); } +static int instant__eq(lua_State* L) +{ + uv_timespec64_t left = getTimespecFromInstant(L, 1); + uv_timespec64_t right = getTimespecFromInstant(L, 2); + + lua_pushboolean(L, left.tv_sec == right.tv_sec && left.tv_nsec == right.tv_nsec); + return 1; +} + +static int instant__lt(lua_State* L) +{ + uv_timespec64_t left = getTimespecFromInstant(L, 1); + uv_timespec64_t right = getTimespecFromInstant(L, 2); + + lua_pushboolean(L, left.tv_sec < right.tv_sec || (left.tv_sec == right.tv_sec && left.tv_nsec < right.tv_nsec)); + return 1; +} + +static int instant__le(lua_State* L) +{ + uv_timespec64_t left = getTimespecFromInstant(L, 1); + uv_timespec64_t right = getTimespecFromInstant(L, 2); + + lua_pushboolean(L, left.tv_sec < right.tv_sec || (left.tv_sec == right.tv_sec && left.tv_nsec <= right.tv_nsec)); + return 1; +} + namespace duration { int lua_nanoseconds(lua_State* L) @@ -476,6 +503,15 @@ static void init_instant_lib(lua_State* L) lua_pushcfunction(L, instant__sub, "Instant__sub"); lua_setfield(L, -2, "__sub"); + lua_pushcfunction(L, instant__eq, "Instant__eq"); + lua_setfield(L, -2, "__eq"); + + lua_pushcfunction(L, instant__lt, "Instant__lt"); + lua_setfield(L, -2, "__lt"); + + lua_pushcfunction(L, instant__le, "Instant__le"); + lua_setfield(L, -2, "__le"); + // __index table lua_createtable(L, 0, 2); From ef4fbc493a8abdd8bc7c2ebb7289808e26cbe2b1 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:14:37 -0500 Subject: [PATCH 374/642] Support Loading Local Rules from Lint Config (#822) This PR adds supports for loading rules from paths specified in the lute lint config file under `config.lute.lint.rules.paths`. --- lute/cli/commands/lint/init.luau | 15 +- lute/cli/commands/lint/internaltypes.luau | 4 +- tests/cli/lint.test.luau | 164 ++++++++++++++++++++-- 3 files changed, 171 insertions(+), 12 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 1e06c4418..55d6c742c 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -102,8 +102,8 @@ local function registerRule( ruleConfigs: internalTypes.RuleConfigurations, ruleIgnores: internalTypes.RuleIgnores ) - local ruleConfig: internalTypes.RuleConfig = if config.rules ~= nil and config.rules[rule.name] ~= nil - then config.rules[rule.name] :: internalTypes.RuleConfig -- Refinement doesn't support dynamic indexing, so we need to assert the type here + local ruleConfig: internalTypes.RuleConfig = if config.ruleconfigs ~= nil + then config.ruleconfigs[rule.name] or {} else {} if ruleConfig.off then return @@ -550,6 +550,17 @@ local function main(...: string) tableext.combine(ruleIgnoreData, customIgnores) end + -- load local rules from config rules path + local configuredRulePaths: { pathLib.pathlike } = if lintConfig.rulepaths then lintConfig.rulepaths else {} + for _, path in configuredRulePaths do + local resolvedPath = pathLib.resolve(rootLocation, path) + local loadedRules, loadedConfigs, loadedIgnoreData = + loadLintRules(pathLib.format(resolvedPath), lintConfig, rootLocation) + tableext.extend(lintRules, loadedRules) + tableext.combine(ruleConfigurations, loadedConfigs) + tableext.combine(ruleIgnoreData, loadedIgnoreData) + end + local configContextData = { globals = if lintConfig.globals ~= nil then lintConfig.globals else {}, ruleConfigs = ruleConfigurations, diff --git a/lute/cli/commands/lint/internaltypes.luau b/lute/cli/commands/lint/internaltypes.luau index 77e2d8bf1..3faf34121 100644 --- a/lute/cli/commands/lint/internaltypes.luau +++ b/lute/cli/commands/lint/internaltypes.luau @@ -12,8 +12,8 @@ export type RuleConfig = { export type LintConfig = { ignores: { path.pathlike }?, -- globs/paths to ignore as we walk lintee files globals: types.GlobalsConfig?, -- globals to pass down to all rules via context - rules: { - -- paths: { path.pathlike }?, -- paths to local rules to use + rulepaths: { path.pathlike }?, -- paths to local rules to use + ruleconfigs: { [string]: RuleConfig?, }?, } diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index e31d64341..19909c114 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -34,6 +34,7 @@ type lintTestParams = { content: string, expectedExitCode: number, rulePath: string?, + processOptions: process.processrunoptions?, customRuleContents: string?, expectedAutofixContents: string?, disableDefaultLints: true?, @@ -85,7 +86,7 @@ local function lintTestHelper(assert: testTypes.asserts, params: lintTestParams) table.insert(lintArgs, violatorPath) - local result = process.run(lintArgs) + local result = process.run(lintArgs, params.processOptions) -- Check assert.eq(result.exitcode, params.expectedExitCode) @@ -2493,7 +2494,7 @@ return { -- return { -- lute = { -- lint = { - -- rules = "should be table" + -- ruleConfigs "should be table" -- } -- } -- } @@ -2915,7 +2916,7 @@ local t = { [0] = 1 } globals = { ["testGlobal"] = 1, }, - rules = { + ruleconfigs = { ["testRule"] = { options = { ["testOption"] = "nice", @@ -2957,7 +2958,7 @@ local t = { [0] = 1 } return { lute = { lint = { - rules = { + ruleconfigs = { ["divide_by_zero"] = { ignores = { "**/violator.luau" } } @@ -3010,7 +3011,7 @@ b = a return { lute = { lint = { - rules = { + ruleconfigs = { ["divide_by_zero"] = { ignores = { "**/ignoredDir/**" } } @@ -3042,7 +3043,7 @@ return { return { lute = { lint = { - rules = { + ruleconfigs = { ["almost_swapped"] = { off = true, } @@ -3081,7 +3082,7 @@ return { return { lute = { lint = { - rules = { + ruleconfigs = { ["customRule"] = { off = true, } @@ -3116,7 +3117,7 @@ return { return { lute = { lint = { - rules = { + ruleconfigs = { ["divide_by_zero"] = { severity = "error" } @@ -3136,4 +3137,151 @@ return { }, }) end) + + suite:case("rules given by config.rulepaths are loaded (in addition to CLI-passed rules) and ran", function(assert) + local ruleTemplate = [[ +return { + name = %q, + lint = function(ast, sourcepath, context) + return { + { + lintname = %q, + severity = "warn", + message = %q, + location = ast.location + } + } + end +} + ]] + -- Create the config-provided rules (loaded via config.rulepaths, NOT via -r) + local configuredAbsoluteRulePath = path.format(path.join(tmpDir, "configuredAbsRule.luau")) + fs.writestringtofile( + configuredAbsoluteRulePath, + (ruleTemplate :: any):format( + "configuredAbsoluteRule", + "configuredAbsoluteRule", + "configuredAbsoluteRule ran." + ) + ) + local configuredRelativePath = path.format(path.join(tmpDir, "configuredRelativeRule.luau")) + fs.writestringtofile( + configuredRelativePath, + (ruleTemplate :: any):format( + "configuredRelativeRule", + "configuredRelativeRule", + "configuredRelativeRule ran." + ) + ) + + -- The CLI-provided rule is passed via customRuleContents (which uses -r), + -- while the config-provided rules are referenced via config.rulepaths + lintTestHelper(assert, { + content = "local x = 1", + expectedExitCode = 1, + customRuleContents = [[ +return { + name = "cliRule", + lint = function(ast, sourcepath, context) + return { + { + lintname = "cliRule", + severity = "warn", + message = "CLI Rule Ran.", + location = ast.location + } + } + end +} +]], + configContents = ([[ +return { + lute = { + lint = { + rulepaths = { %q, "./configuredRelativeRule.luau" }, + } + } +} +]]):format(configuredAbsoluteRulePath), + disableDefaultLints = true, + outputExpectations = { + -- both config-given and CLI-given rules should have ran + { expectedOutput = "configuredAbsoluteRule ran.", count = 1 }, + { expectedOutput = "configuredRelativeRule ran.", count = 1 }, + { expectedOutput = "CLI Rule Ran.", count = 1 }, + }, + processOptions = { + cwd = path.format(tmpDir), + }, + }) + end) + + suite:case( + "lute lint properly loads rule-specific configuration for rules provided by config.rulepaths", + function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createdirectory(testSubDir) + + local configuredRulePath = path.join(testSubDir, "configuredRule.luau") + local configuredRuleContents = [[ + return { + name = "configuredRule", + lint = function(ast, sourcepath, context) + return { + { + lintname = "configuredRule", + severity = "warn", + message = `Configured Rule Ran with options.a = {context.options.a}`, + location = ast.location, + sourcepath = sourcepath, + } + } + end + } + ]] + fs.writestringtofile(configuredRulePath, configuredRuleContents) + + local configFile = path.join(testSubDir, ".config.luau") + local configContents = ([[ + return { + lute = { + lint = { + rulepaths = { %q }, + ruleconfigs = { + ["configuredRule"] = { + ignores = { "**/ignoredDir/**" }, + options = { + a = 1, + } + } + } + } + } + } + ]]):format(path.format(configuredRulePath)) + fs.writestringtofile(configFile, configContents) + + local ignoredDir = path.join(testSubDir, "ignoredDir") + local notIgnoredDir = path.join(testSubDir, "not_ignored") + fs.createdirectory(ignoredDir) + fs.createdirectory(notIgnoredDir) + local ignoredFile = path.join(ignoredDir, "ignored.luau") + local notIgnoredFile = path.join(notIgnoredDir, "valid.luau") + local violatingContents = "print('hello world')" + fs.writestringtofile(ignoredFile, violatingContents) + fs.writestringtofile(notIgnoredFile, violatingContents) + + local result = process.run({ + lutePath, + "lint", + "-c", + path.format(configFile), + path.format(testSubDir), + }) + assert.eq(result.exitcode, 1) -- errors + assert.strnotcontains(result.stdout, "ignored.luau") + assert.strcontains(result.stdout, "valid.luau") + assert.strcontains(result.stdout, "Configured Rule Ran with options.a = 1") -- options are correctly passed through + end + ) end) From 22d808bb44fd9dabce7dc455896784391e07162a Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:58:20 -0500 Subject: [PATCH 375/642] Add basic docs on lute lint's configuration system (#843) --- docs/cli/lint/index.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/cli/lint/index.md b/docs/cli/lint/index.md index 82ec8fc85..eeb42b45c 100755 --- a/docs/cli/lint/index.md +++ b/docs/cli/lint/index.md @@ -27,6 +27,42 @@ Enable verbose output Path to a single lint rule or a folder containing lint rules. If a folder is provided, any subfolders containing init.luau files will be treated as modules exporting lint rules, while all other .luau files will be treated as individual lint rules. If unspecified, the default lint rules are used. +### `-c, --config [CONFIG]` + +Path to file containing lute lint configuration table. If unspecified, lute lint will look for a `.config.luau` file in the working directory from which it is invoked. + +Configuration expects the following structure: +```luau +type RuleName = string + +type Config = { + lute: { + lint: { + -- Array of globs (.gitignore style) to EXEMPT from linting + ignores: { string }?, + -- Table of string:unknown pairs; globals are passed down to each rule + globals: { [string]: unknown }?, + -- Array of paths from which to load local lint rules (similar to -r CLI option) + rulepaths: { [string] }?, + -- Specify per-rule options and overrides + ruleconfigs: { + [RuleName]: { + -- Array of globs (.gitignore style) that are EXEMPT from this rule + ignores: { string }?, + -- Override a rule's default severity + severity: ("warn" | "error" | "info" | "hint")?, + -- Pass custom options through to a rule; + -- see specific rule's implementation / docs for expected options structure + options: { [string]: unknown }?, + -- Disable a rule + off: boolean?, + }, + }?, + }?, + }?, +} +``` + ### `-j, --json` Output lint violations in JSON format matching the LSP diagnostic spec. From a6b90ff0eca67f01e2b351c82cd4e7cf2b641b32 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:15:44 -0500 Subject: [PATCH 376/642] Fix Failing Tests (#850) Tests added in this recently merged [PR](https://github.com/luau-lang/lute/pull/834) (merged a couple of hours ago) were broken by this [PR](https://github.com/luau-lang/lute/pull/822) that merged one hour ago but had passed CI a few days and not been updated since. These failures are [blocking release ](https://github.com/luau-lang/lute/actions/runs/22466935866) This PR fixes said tests --- tests/cli/lint.test.luau | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 19909c114..0f18c2e7d 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -2194,7 +2194,7 @@ myWrite(output, "data") return { lute = { lint = { - rules = { + ruleconfigs = { ["unused_variable"] = { options = { myWrite = { 1 } @@ -2237,7 +2237,7 @@ fs.write(buffer, "content") return { lute = { lint = { - rules = { + ruleconfigs = { ["unused_variable"] = { options = { ["fs.write"] = { 1 } @@ -2280,7 +2280,7 @@ fs["write"](buffer, "content") return { lute = { lint = { - rules = { + ruleconfigs = { ["unused_variable"] = { options = { ["fs.write"] = { 1 } @@ -2324,7 +2324,7 @@ myMultiWrite(a, b, "data") return { lute = { lint = { - rules = { + ruleconfigs = { ["unused_variable"] = { options = { myMultiWrite = { 1, 2 } @@ -2378,7 +2378,7 @@ myFunc(config, output) return { lute = { lint = { - rules = { + ruleconfigs = { ["unused_variable"] = { options = { myFunc = { 2 } @@ -2418,7 +2418,7 @@ violator.luau:2:7-13 ── return { lute = { lint = { - rules = { + ruleconfigs = { ["unused_variable"] = { options = { ["deeply.nested.api"] = { 1 } @@ -2448,7 +2448,7 @@ return { return { lute = { lint = { - rules = { + ruleconfigs = { ["unused_variable"] = { options = { myApi = { 1 }, From 9e25dc23ad946091d4391f624eb79680304baa16 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 27 Feb 2026 08:49:59 -0800 Subject: [PATCH 377/642] Fix some @std/fs typechecking errors (#849) Not too interesting here. One of these is a case where we do some platform specific stuff, so Luau can't actually refine paths based on this information. --- lute/std/libs/fs.luau | 6 ++++-- tools/check-faillist.txt | 8 ++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index a9b61001b..746e33a55 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -7,6 +7,7 @@ local deque = require("@batteries/collections/deque") local fs = require("@lute/fs") local pathlib = require("@std/path") local sys = require("@std/system") +local win32paths = require("@std/path/win32") local fslib = {} @@ -20,6 +21,7 @@ export type watchevent = fs.WatchEvent type pathlike = pathlib.pathlike type path = pathlib.path +type win32path = win32paths.path export type createdirectoryoptions = { makeparents: boolean?, @@ -138,7 +140,7 @@ function fslib.createdirectory(path: pathlike, options: createdirectoryoptions?) if pathlib.isabsolute(path) then if sys.win32 then -- Windows path - get the drive root like "C:\" - subdir = pathlib.win32.drive(parsed) + subdir = pathlib.win32.drive(parsed :: win32path) else -- POSIX absolute path - start from root "/" subdir = pathlib.format({ absolute = true, parts = {} }) @@ -178,7 +180,7 @@ end --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. See example/walk_directory.luau for usage. function fslib.walk(path: pathlike, options: walkoptions?): () -> path? - local queue = { path } + local queue: { path } = { pathlib.parse(path) } return function() while #queue > 0 do diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 7b49e5d1a..2148fbd57 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -81,12 +81,8 @@ lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:47:26-37 lute/cli/commands/transform/lib/arguments.luau:75:9-14 lute/cli/commands/transform/lib/arguments.luau:75:9-14 -lute/std/libs/fs.luau:141:34-39 -lute/std/libs/fs.luau:141:34-39 -lute/std/libs/fs.luau:190:52-68 -lute/std/libs/fs.luau:190:52-68 -lute/std/libs/fs.luau:198:11-17 -lute/std/libs/fs.luau:198:11-17 +lute/std/libs/fs.luau:192:52-68 +lute/std/libs/fs.luau:192:52-68 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:41:23-27 lute/std/libs/syntax/printer.luau:42:25-29 From baf8d2702c95fbbdd416cc2e9ca167ef2e10ae59 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 2 Mar 2026 11:13:11 -0800 Subject: [PATCH 378/642] Adds empty if block lint rule (#835) Matches https://kampfkarren.github.io/selene/lints/empty_if.html --------- Co-authored-by: ariel --- docs/cli/lint/empty_if_block.md | 65 ++++++ lute/cli/commands/lint/init.luau | 3 +- lute/cli/commands/lint/rules/README.md | 2 +- .../commands/lint/rules/empty_if_block.luau | 134 ++++++++++++ tests/cli/lint.test.luau | 198 ++++++++++++++++++ tools/check-faillist.txt | 20 +- 6 files changed, 410 insertions(+), 12 deletions(-) create mode 100644 docs/cli/lint/empty_if_block.md create mode 100644 lute/cli/commands/lint/rules/empty_if_block.luau diff --git a/docs/cli/lint/empty_if_block.md b/docs/cli/lint/empty_if_block.md new file mode 100644 index 000000000..4b46d8e80 --- /dev/null +++ b/docs/cli/lint/empty_if_block.md @@ -0,0 +1,65 @@ +--- +title: empty_if_block +--- +# empty_if_block + +This lint rule checks for empty blocks in `if`, `elseif`, and `else` statements. + +## Why this is discouraged + +Empty conditionals are indicative of incomplete or poorly written code and detract from readability. + +## Options + +This rule accepts the following configuration options: + +- `comments_count` (boolean, default: `false`): When set to `true`, blocks that contain only comments are not considered empty and will not trigger a warning. + +## Example violations + +`empty_if_block` will warn on the following: + +```luau +if condition then +end + +if condition then + -- Empty then block +elseif otherCondition then + doSomething() +end + +if condition then + doSomething() +else + -- Empty else block +end +``` + +## Configuration + +To treat blocks with only comments as non-empty, configure the rule with `comments_count: true`: + +```luau +-- This will not trigger a warning when comments_count is true +if condition then + -- TODO: implement this later +end +``` + +### Sample `.config.luau` file +```luau +return { + lute = { + lint = { + rules = { + empty_if_block = { + options = { + comments_count = true, + }, + }, + } + } + }, +} +``` diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 55d6c742c..b12a98b95 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -21,6 +21,7 @@ local DEFAULT_RULES = { "constant_table_comparison", "divide_by_zero", "duplicate_keys", + "empty_if_block", "parenthesized_conditions", "unused_variable", } @@ -324,7 +325,7 @@ local function lintString( table.sort( violations, - function(a: types.LintViolation, b: types.LintViolation) -- LUAUFIX: bidirecitonal inference should mean that annotations on a and b aren't needed + function(a: types.LintViolation, b: types.LintViolation) -- LUAUFIX: bidirectional inference should mean that annotations on a and b aren't needed return a.location < b.location end ) diff --git a/lute/cli/commands/lint/rules/README.md b/lute/cli/commands/lint/rules/README.md index e936388ab..0dd7b1700 100644 --- a/lute/cli/commands/lint/rules/README.md +++ b/lute/cli/commands/lint/rules/README.md @@ -2,7 +2,7 @@ To add a new default lint rule to `lute lint`: 1. Create a module or luau file defining the rule in this folder. Each violation should report its target as `"https://lute.luau.org/cli/lint/.html"`. 1. Add the name of the rule to the `DEFAULT_RULES` constant in `lint/init.luau` -1. Add a test for the rule in `tests/cli/lint.test.luau` +1. Add tests for the rule in `tests/cli/lint.test.luau` 1. Add documentation for it in `docs/cli/lint`. The doc's title should be the same as your rule's name. The expected type of a lint rule is specified in `lint/types.luau`. \ No newline at end of file diff --git a/lute/cli/commands/lint/rules/empty_if_block.luau b/lute/cli/commands/lint/rules/empty_if_block.luau new file mode 100644 index 000000000..2c893fd04 --- /dev/null +++ b/lute/cli/commands/lint/rules/empty_if_block.luau @@ -0,0 +1,134 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") + +local name = "empty_if_block" +local message = "Empty if block detected. Consider removing it or adding to it." +local target = "https://lute.luau.org/cli/lint/empty_if_block.html" + +local function containsComment(trivia: { syntax.Trivia }): boolean + for _, triv in trivia do + if triv.tag ~= "whitespace" then + return true + end + end + return false +end + +local function isBlockEmpty( + block: syntax.AstStatBlock, + commentsCount: boolean, + leadingTrivia: { syntax.Trivia }, + trailingTrivia: { syntax.Trivia } +): boolean + -- If the block has statements, it's not empty + if #block.statements > 0 then + return false + end + + -- If comments_count is false, we consider blocks with only comments as empty + if not commentsCount then + return true + end + + -- if comments_count is true, the block is not empty when it contains comments. + if containsComment(leadingTrivia) or containsComment(trailingTrivia) then + return false + end + + return true +end + +local function lint( + ast: syntax.AstStatBlock, + sourcepath: path.path?, + context: lintTypes.RuleContext +): { lintTypes.LintViolation } + -- Get the comments_count option from the rule configuration + local commentsCount = false + if context.options and context.options.comments_count then + commentsCount = context.options.comments_count :: boolean + end + + -- Find all if statements + return query.findallfromroot(ast, utils.isStatIf):map(function(ifStat): lintTypes.LintViolation? + -- Check if the then block is empty + if + isBlockEmpty( + ifStat.thenblock, + commentsCount, + ifStat.thenkeyword.trailingtrivia, + if #ifStat.elseifs > 0 -- trailingToken + then ifStat.elseifs[1].elseifkeyword.leadingtrivia + elseif ifStat.elsekeyword then ifStat.elsekeyword.leadingtrivia + else ifStat.endkeyword.leadingtrivia + ) + then + return { + lintname = name, + location = ifStat.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } + end + + -- Check elseif blocks + for i, elseIfStat in ifStat.elseifs do + if + isBlockEmpty( + elseIfStat.thenblock, + commentsCount, + elseIfStat.thenkeyword.trailingtrivia, + if i < #ifStat.elseifs -- trailingToken + then ifStat.elseifs[i + 1].elseifkeyword.leadingtrivia + elseif ifStat.elsekeyword then ifStat.elsekeyword.leadingtrivia + else ifStat.endkeyword.leadingtrivia + ) + then + return { + lintname = name, + location = ifStat.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } + end + end + + -- Check else block if it exists + if ifStat.elseblock then + assert(ifStat.elsekeyword, "elseblock should have an elsekeyword") + if + isBlockEmpty( + ifStat.elseblock, + commentsCount, + ifStat.elsekeyword.trailingtrivia, + ifStat.endkeyword.leadingtrivia + ) + then + return { + lintname = name, + location = ifStat.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } + end + end + + return nil + end).nodes +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 0f18c2e7d..010892820 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -15,6 +15,7 @@ local defaultRulesPaths = { constant_table_comparison = path.format(path.join(defaultRulesFolder, "constant_table_comparison.luau")), divide_by_zero = path.format(path.join(defaultRulesFolder, "divide_by_zero.luau")), duplicate_keys = path.format(path.join(defaultRulesFolder, "duplicate_keys.luau")), + empty_if_block = path.format(path.join(defaultRulesFolder, "empty_if_block.luau")), parenthesized_conditions = path.format(path.join(defaultRulesFolder, "parenthesized_conditions.luau")), unused_variable = path.format(path.join(defaultRulesFolder, "unused_variable.luau")), } @@ -24,6 +25,7 @@ local LINT_RULE_MESSAGES = { constant_table_comparison = "warning[constant_table_comparison]: Constant table comparison detected. Comparing a table reference to a table literal will always evaluate to false.", divide_by_zero = "warning[divide_by_zero]: Division by zero detected.", duplicate_keys = "warning[duplicate_keys]: Duplicate keys detected in table literal.", + empty_if_block = "warning[empty_if_block]: Empty if block detected. Consider removing it or adding to it.", parenthesized_conditions = "info[parenthesized_conditions]: Luau doesn't require parentheses around conditions. You can remove them for readability.", unused_variable = "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", } @@ -3284,4 +3286,200 @@ return { assert.strcontains(result.stdout, "Configured Rule Ran with options.a = 1") -- options are correctly passed through end ) + + suite:case("empty_if_block basic", function(assert) + lintTestHelper(assert, { + content = [[ +if true then +end +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.empty_if_block, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.empty_if_block, count = 1 }, + { + expectedOutput = [[ +violator.luau:1:1-2:4 ── + │ + 1 │ ╭ if true then + 2 │ │ end + │ ╰────────────^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("empty_if_block with non empty block", function(assert) + lintTestHelper(assert, { + content = [[ +if true then + print("hello") +end +]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.empty_if_block, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.empty_if_block, count = 0 }, + }, + }) + end) + + suite:case("empty_if_block with empty else", function(assert) + lintTestHelper(assert, { + content = [[ +if true then + print("hello") +else +end +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.empty_if_block, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.empty_if_block, count = 1 }, + { + expectedOutput = [[ +violator.luau:1:1-4:4 ── + │ + 1 │ ╭ if true then + 2 │ │ print("hello") + 3 │ │ else + 4 │ │ end + │ ╰──────────────────^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("empty_if_block with empty elseifs", function(assert) + lintTestHelper(assert, { + content = [[ +if true then + print("hello") +elseif true then +elseif false then +end +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.empty_if_block, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.empty_if_block, count = 1 }, + { + expectedOutput = [[ +violator.luau:1:1-5:4 ── + │ + 1 │ ╭ if true then + 2 │ │ print("hello") + 3 │ │ elseif true then + 4 │ │ elseif false then + 5 │ │ end + │ ╰──────────────────^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("empty_if_block with comments (comments_count = false)", function(assert) + lintTestHelper(assert, { + content = [[ +if true then + -- This is a comment +end + +if false then + -- Another comment + -- More comments +else + -- Comment in else +end +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.empty_if_block, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.empty_if_block, count = 2 }, + { + expectedOutput = [[ +violator.luau:1:1-3:4 ── + │ + 1 │ ╭ if true then + 2 │ │ -- This is a comment + 3 │ │ end + │ ╰────────────────────────^ + │ +]], + count = 1, + }, + { + expectedOutput = [[ +violator.luau:5:1-10:4 ── + │ + 5 │ ╭ if false then + 6 │ │ -- Another comment + 7 │ │ -- More comments + 8 │ │ else + 9 │ │ -- Comment in else + 10 │ │ end + │ ╰──────────────────────^ + │ +]], + count = 1, + }, + }, + }) + end) + + suite:case("empty_if_block with comments (comments_count = true)", function(assert) + local configContents = [[ +return { + lute = { lint = { ruleconfigs = { + empty_if_block = { + options = { + comments_count = true, + }, + }, + } } }, +} +]] + lintTestHelper(assert, { + content = [[ +if true then + -- This is a comment +end + +if false then + -- comment +else + -- Comment in else +end + +if true then + --comment +elseif true then + -- Comment in elseif +elseif false then + -- Comment in elseif +end +]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.empty_if_block, + disableDefaultLints = true, + configContents = configContents, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.empty_if_block, count = 0 }, + }, + }) + end) end) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 2148fbd57..fc5429416 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -37,16 +37,16 @@ lute/cli/commands/doc/init.luau:315:5-16 lute/cli/commands/doc/init.luau:321:10-24 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 -lute/cli/commands/lint/init.luau:82:12-20 -lute/cli/commands/lint/init.luau:515:15-24 -lute/cli/commands/lint/init.luau:516:56-65 -lute/cli/commands/lint/init.luau:516:56-65 -lute/cli/commands/lint/init.luau:516:56-65 -lute/cli/commands/lint/init.luau:516:56-65 -lute/cli/commands/lint/init.luau:516:56-65 -lute/cli/commands/lint/init.luau:516:56-65 -lute/cli/commands/lint/init.luau:529:36-45 -lute/cli/commands/lint/init.luau:530:55-64 +lute/cli/commands/lint/init.luau:83:12-20 +lute/cli/commands/lint/init.luau:516:15-24 +lute/cli/commands/lint/init.luau:517:56-65 +lute/cli/commands/lint/init.luau:517:56-65 +lute/cli/commands/lint/init.luau:517:56-65 +lute/cli/commands/lint/init.luau:517:56-65 +lute/cli/commands/lint/init.luau:517:56-65 +lute/cli/commands/lint/init.luau:517:56-65 +lute/cli/commands/lint/init.luau:530:36-45 +lute/cli/commands/lint/init.luau:531:55-64 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:51:7-62 lute/cli/commands/lint/printer.luau:58:8-52 From adff4658f3dac54c5d17c3bcb58bae66cbc652b4 Mon Sep 17 00:00:00 2001 From: Hunter Goldstein Date: Wed, 4 Mar 2026 16:58:33 -0800 Subject: [PATCH 379/642] chore: configure Auto Assign Github bot to assign PR authors as "assignees" (#842) AFACT: "assignee" is whatever we want it to be, and the team has signaled that they'd like to automatically be assigned to their own PRs s.t. they show up in our project kanban board. --------- Co-authored-by: ariel --- .github/auto_assign.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/auto_assign.yml diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 000000000..ff74e01df --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,2 @@ +# Set to author to set pr creator as assignee +addAssignees: author From cc98e3bc2ac6c7cde9366942fe7b2aaccacd7cae Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 5 Mar 2026 11:42:32 -0800 Subject: [PATCH 380/642] Fixes fs.open not creating a file when opening said file in 'w' mode. (#855) Resolves #758 --- lute/fs/src/fs.cpp | 3 ++- tests/std/fs.test.luau | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 974627748..6d82df4ae 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -47,7 +47,8 @@ std::optional setFlags(const char* c, int* openFlags) *openFlags |= O_RDONLY; break; case 'w': - *openFlags |= O_WRONLY | O_TRUNC; + *openFlags |= O_WRONLY | O_TRUNC | O_CREAT; + modeFlags = 0666; break; case 'x': *openFlags |= O_CREAT | O_EXCL; diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 77958e4b7..055dd22b7 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -165,6 +165,24 @@ test.suite("FsSuite", function(suite) fs.remove(file) end) + suite:case("open_write_creates_new_file", function(assert) + local file = path.join(tmpdir, "new_write_only.txt") + + -- Ensure file does not exist + if fs.exists(file) then + fs.remove(file) + end + + local h = fs.open(file, "w") + assert.eq(fs.exists(file), true) + fs.write(h, "hello") + fs.close(h) + + assert.eq(fs.readfiletostring(file), "hello") + + fs.remove(file) + end) + suite:case("removedirectory_non_recursive", function(assert) local dir = path.join(tmpdir, "empty_dir") fs.createdirectory(dir, { makeparents = true }) From 574b5c58af9f64ea25daf3773ab2223f9a5c2cec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:34:57 -0800 Subject: [PATCH 381/642] Update Luau to 0.710 (#851) **Luau**: Updated from `0.708` to `0.710` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.710 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: ariel --- extern/luau.tune | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index c0af44e1b..f47395b1a 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.708" -revision = "54a2ea00831df4c791e6cfc896a98da75d1ae126" +branch = "0.710" +revision = "964580112c1b59b19c4985dcc66be79162355b12" From 35e12a73f68eacbd0dcf539e6bc7e7cc18515732 Mon Sep 17 00:00:00 2001 From: ariel Date: Thu, 5 Mar 2026 12:40:12 -0800 Subject: [PATCH 382/642] lute/luau: make `span`s into an ordinary frozen table (#854) In #595, we implemented a `span` userdata to replace the table with two locations in order to speed up the overall performance of the parsing library. I rabbitholed a little bit when I did this because, upon further reflection, the performance benefit is only from the fact that we were previously doing three heap allocations per span versus one with the change. The userdata added some extra complexity and "weirdness" to the API, and I'd like to clean things up before a proper stable release. Toward that end, this PR just makes `span` into an ordinary frozen table. It still supports comparison via `<` and it still has the same fields, but it's just a table! --- definitions/luau.luau | 1 - lute/luau/src/luau.cpp | 132 ++++++++++++++------------ lute/runtime/include/lute/userdatas.h | 1 - 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 008152d02..5ae567889 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -1,7 +1,6 @@ -- lute-lint-global-ignore(unused_variable) local luau = {} --- this is a userdata, not a table, but it has this interface type spandata = { read beginline: number, read begincolumn: number, diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 19e9cdb10..b74689994 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -121,8 +121,6 @@ struct Trivia std::string_view text; }; -// the userdata version of `Luau::Location` because exposing this as a table was, unfortunately, very impractical -// it happens too much all over the entire AST to do reasonably. struct Span { uint32_t beginLine; @@ -131,38 +129,73 @@ struct Span uint32_t endColumn; }; +static Span checkSpan(lua_State* L, int index) +{ + lua_checkstack(L, 1); + + if (!lua_istable(L, index)) + luaL_typeerror(L, index, "span"); + + if (lua_getfield(L, index, "beginline") == LUA_TNIL) + luaL_typeerror(L, index, "span"); + int beginLine = lua_tonumber(L, -1); + lua_pop(L, 1); + + if (lua_getfield(L, index, "begincolumn") == LUA_TNIL) + luaL_typeerror(L, index, "span"); + int beginColumn = lua_tonumber(L, -1); + lua_pop(L, 1); + + if (lua_getfield(L, index, "endline") == LUA_TNIL) + luaL_typeerror(L, index, "span"); + int endLine = lua_tonumber(L, -1); + lua_pop(L, 1); + + if (lua_getfield(L, index, "endcolumn") == LUA_TNIL) + luaL_typeerror(L, index, "span"); + int endColumn = lua_tonumber(L, -1); + lua_pop(L, 1); + + return {static_cast(beginLine), static_cast(beginColumn), static_cast(endLine), static_cast(endColumn)}; +} + static int createSpan(lua_State* L) { int argumentCount = lua_gettop(L); if (argumentCount != 1) luaL_error(L, "%s: expected 1 argument, but got %d", kSpanCreateName, argumentCount); - // read all three of the required fields out of the table - lua_getfield(L, 1, "beginline"); - lua_getfield(L, 1, "begincolumn"); - lua_getfield(L, 1, "endline"); - lua_getfield(L, 1, "endcolumn"); + lua_checkstack(L, 2); + + // check that the argument is compliant with the span interface! + Span span = checkSpan(L, 1); + + lua_createtable(L, 0, 4); + + lua_pushinteger(L, span.beginLine); + lua_setfield(L, -2, "beginline"); - double beginline = luaL_checknumber(L, 2); - double begincolumn = luaL_checknumber(L, 3); - double endline = luaL_checknumber(L, 4); - double endcolumn = luaL_checknumber(L, 5); + lua_pushinteger(L, span.beginColumn); + lua_setfield(L, -2, "begincolumn"); - Span* span = static_cast(lua_newuserdatatagged(L, sizeof(Span), kSpanTag)); + lua_pushinteger(L, span.endLine); + lua_setfield(L, -2, "endline"); - span->beginLine = static_cast(beginline); - span->beginColumn = static_cast(begincolumn); - span->endLine = static_cast(endline); - span->endColumn = static_cast(endcolumn); + lua_pushinteger(L, span.endColumn); + lua_setfield(L, -2, "endcolumn"); luaL_getmetatable(L, kSpanType); lua_setmetatable(L, -2); + lua_setreadonly(L, -1, 1); + return 1; } static int makeSpanLibrary(lua_State* L) { + lua_checkstack(L, 2); + lua_createtable(L, 0, 1); lua_pushcfunction(L, luau::createSpan, "create"); @@ -173,47 +206,20 @@ static int makeSpanLibrary(lua_State* L) return 1; } -static int indexSpan(lua_State* L) -{ - const Span* span = static_cast(luaL_checkudata(L, 1, kSpanType)); - - const char* fieldName = luaL_checkstring(L, 2); - - if (std::strcmp(fieldName, "beginline") == 0) - { - lua_pushnumber(L, span->beginLine); - return 1; - } - else if (std::strcmp(fieldName, "begincolumn") == 0) - { - lua_pushnumber(L, span->beginColumn); - return 1; - } - else if (std::strcmp(fieldName, "endline") == 0) - { - lua_pushnumber(L, span->endLine); - return 1; - } - else if (std::strcmp(fieldName, "endcolumn") == 0) - { - lua_pushnumber(L, span->endColumn); - return 1; - } - - return 0; -} static int ltSpan(lua_State* L) { - const Span* lhs = static_cast(luaL_checkudata(L, 1, kSpanType)); - const Span* rhs = static_cast(luaL_checkudata(L, 2, kSpanType)); + Span lhs = checkSpan(L, 1); + Span rhs = checkSpan(L, 2); + + lua_checkstack(L, 2); // Compare beginnings, and if they're equal, compare ends - if (lhs->beginLine < rhs->beginLine || (lhs->beginLine == rhs->beginLine && lhs->beginColumn < rhs->beginColumn)) + if (lhs.beginLine < rhs.beginLine || (lhs.beginLine == rhs.beginLine && lhs.beginColumn < rhs.beginColumn)) lua_pushboolean(L, 1); - else if (lhs->beginLine == rhs->beginLine && lhs->beginColumn == rhs->beginColumn) + else if (lhs.beginLine == rhs.beginLine && lhs.beginColumn == rhs.beginColumn) { - if (lhs->endLine < rhs->endLine || (lhs->endLine == rhs->endLine && lhs->endColumn < rhs->endColumn)) + if (lhs.endLine < rhs.endLine || (lhs.endLine == rhs.endLine && lhs.endColumn < rhs.endColumn)) lua_pushboolean(L, 1); else lua_pushboolean(L, 0); @@ -389,15 +395,24 @@ struct AstSerialize : public Luau::AstVisitor { lua_rawcheckstack(L, 2); - Span* span = static_cast(lua_newuserdatatagged(L, sizeof(Span), kSpanTag)); + lua_createtable(L, 0, 4); + + lua_pushinteger(L, location.begin.line + 1); + lua_setfield(L, -2, "beginline"); + + lua_pushinteger(L, location.begin.column + 1); + lua_setfield(L, -2, "begincolumn"); + + lua_pushinteger(L, location.end.line + 1); + lua_setfield(L, -2, "endline"); - span->beginLine = location.begin.line + 1; - span->beginColumn = location.begin.column + 1; - span->endLine = location.end.line + 1; - span->endColumn = location.end.column + 1; + lua_pushinteger(L, location.end.column + 1); + lua_setfield(L, -2, "endcolumn"); luaL_getmetatable(L, kSpanType); lua_setmetatable(L, -2); + + lua_setreadonly(L, -1, 1); } void serialize(Luau::AstName& name) @@ -2970,13 +2985,6 @@ static int initLuauLibrary(lua_State* L) luaL_newmetatable(L, kSpanType); - // Set __type - lua_pushstring(L, kSpanType); - lua_setfield(L, -2, "__type"); - - lua_pushcfunction(L, luau::indexSpan, "span.__index"); - lua_setfield(L, -2, "__index"); - lua_pushcfunction(L, luau::ltSpan, "span.__lt"); lua_setfield(L, -2, "__lt"); diff --git a/lute/runtime/include/lute/userdatas.h b/lute/runtime/include/lute/userdatas.h index b0eb73c5d..5cc6153d9 100644 --- a/lute/runtime/include/lute/userdatas.h +++ b/lute/runtime/include/lute/userdatas.h @@ -5,4 +5,3 @@ constexpr int kDurationTag = 127; constexpr int kInstantTag = 126; constexpr int kWatchHandleTag = 125; constexpr int kHashFunctionTag = 124; -constexpr int kSpanTag = 123; From f2032d5734660d1dc8a5d548e1df89823140fdb4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:41:10 +0000 Subject: [PATCH 383/642] Update Lute to 0.1.0-nightly.20260220 (#838) **Lute**: Updated from `0.1.0-nightly.20260213` to `0.1.0-nightly.20260220` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260220 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: ariel --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 7639a2ac3..1dc711277 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260213" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260227" } diff --git a/rokit.toml b/rokit.toml index 526e2a197..36d9cb52d 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.3.0" -lute = "luau-lang/lute@0.1.0-nightly.20260213" +lute = "luau-lang/lute@0.1.0-nightly.20260227" From 0c4310e70b7f5465f02719ee351b602ad760107b Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 5 Mar 2026 15:01:58 -0800 Subject: [PATCH 384/642] Refactors luthier - this should abstract away the hashing logic (#856) There is a lot of duplicate code in the luthier script for a) hashing a directory of luau files b) checking if things have changed, c)writing a set of c++ files that embed those files in. This PR tries to automate some of this. --- tools/luthier.luau | 327 +++++++++------------------------------------ 1 file changed, 66 insertions(+), 261 deletions(-) diff --git a/tools/luthier.luau b/tools/luthier.luau index e6504fbc9..5385d4751 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -235,177 +235,84 @@ local function traverseFileTree(path: string, callback: (string, string) -> ()) end end -local function getStdLibHash(): string - local libsPath = projectRelative("lute", "std", "libs") - - local toHash = "" - - traverseFileTree(libsPath, function(path, relativePath) - if safeFsType(path) == "file" then - toHash ..= "./" .. relativePath - toHash ..= readFileToString(path) - end - end) - - local digest = crypto.digest(crypto.hash.blake2b256, toHash) - return hexify(digest) -end - -local function isGeneratedStdLibUpToDate(): boolean - local hashFile = projectRelative("lute", "std", "src", "generated", "hash.txt") - - if safeFsType(hashFile) ~= "file" then - return false - end - - local hash = readFileToString(hashFile) - - return hash == getStdLibHash() -end - local function writeLines(file, lines: { string }) fs.write(file, table.concat(lines, "\n") .. "\n") end -local luteBatteriesDir = projectRelative("lute", "batteries", "generated") -local batteriesHashFile = projectRelative("lute", "batteries", "generated", "hash.txt") - -local function getBatteriesHash(): string +local function hashDirectory(path: string): string local toHash = "" - traverseFileTree("batteries/", function(path, relativePath) - if safeFsType(path) == "file" then + traverseFileTree(path, function(filePath, relativePath) + if safeFsType(filePath) == "file" then toHash ..= "./" .. relativePath - toHash ..= readFileToString(path) + toHash ..= readFileToString(filePath) end end) - local digest = crypto.digest(crypto.hash.blake2b256, toHash) return hexify(digest) end -local function isBatteriesUpToDate(): boolean - if safeFsType(batteriesHashFile) ~= "file" then +local function isHashUpToDate(hashFile: string, currentHash: string): boolean + if safeFsType(hashFile) ~= "file" then return false end - local hash = readFileToString(batteriesHashFile) - return hash == getBatteriesHash() + return readFileToString(hashFile) == currentHash end -local function generateBatteriesCommandsIfNeeded() - pcall(fs.mkdir, luteBatteriesDir) - - if isBatteriesUpToDate() then - return - end - - print("Generating code for Batteries, files are out of date.") - - local cpp = fs.open(joinPath(luteBatteriesDir, "batteries.cpp"), "w+") - - writeLines(cpp, { - "// This file is auto-generated by luthier.luau. Do not edit.", - "// Instead, you should modify the source files in `batteries`.", - "", - '#include "batteries.h"', - "", - "constexpr std::pair lutebatteries[] = {", - }) - - local libsPath = projectRelative("batteries") - local numItems = 0 - - traverseFileTree(libsPath, function(path, relativePath) - if #relativePath == 0 then - return -- Skip the root directory - end - - local aliasedPath = "@batteries/" .. relativePath - - if safeFsType(path) == "file" then - local libSrc = readFileToString(path) - if #libSrc > 16_000 then - -- https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026 - -- MSVC can't handle a single string literal being over 16380 single-byte characters, - -- but can if they are adjacent contatenated strings. So, we split the string line-by-line into - -- adjacent substrings. - local lines = string.split(libSrc, "\n") - local generatedLines = table.create(#lines) - for _, line in lines do - local escapedLine = string.gsub(line, "\\", "\\\\") - escapedLine = string.gsub(escapedLine, '"', '\\"') - escapedLine = string.gsub(escapedLine, "\r", "") - table.insert(generatedLines, `"{escapedLine}\\n"`) - end - fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) - else - fs.write(cpp, `\n \{"{aliasedPath}", R"LUTE_BATTERIES({libSrc})LUTE_BATTERIES"},\n`) - end - end - - if safeFsType(path) == "dir" then - fs.write(cpp, `\n \{"{aliasedPath}", "#directory"},\n`) - end - - numItems += 1 - end) - - fs.write(cpp, "};\n") - fs.close(cpp) +type EmbeddedModuleConfig = { + sourcePath: string, + outputDir: string, + alias: string, + arrayName: string, + moduleName: string, +} - local header = fs.open(joinPath(luteBatteriesDir, "batteries.h"), "w+") - writeLines(header, { - "// This file is auto-generated by luthier.luau. Do not edit.", - "#pragma once", - "", - "#include ", - "#include ", - "", - `extern const std::pair lutebatteries[{numItems}];`, - }) - fs.close(header) +local function generateEmbeddedModulesIfNeeded(config: EmbeddedModuleConfig) + pcall(fs.mkdir, config.outputDir) - local hash = fs.open(batteriesHashFile, "w+") - fs.write(hash, getBatteriesHash()) - fs.close(hash) -end - -local function generateStdLibFilesIfNeeded() - local generatedPath = projectRelative("lute", "std", "src", "generated") - pcall(fs.mkdir, generatedPath) + local hashFile = joinPath(config.outputDir, "hash.txt") + local currentHash = hashDirectory(config.sourcePath) - if isGeneratedStdLibUpToDate() then + if isHashUpToDate(hashFile, currentHash) then return end + local aliasWithSlash = config.alias .. "/" + local logMsg = `Generating code for embedded {aliasWithSlash} modules, files are out of date` + print(logMsg) + local cppFile = config.moduleName .. ".cpp" + local headerFile = config.moduleName .. ".h" - print("Generating code for @std libraries, files are out of date.") + local delimiter = string.upper(config.moduleName) .. "_MOD" + if #delimiter >= 16 then + print(`Delimiter {delimiter} cannot be greater than length 16. Please shorten the embedded module name`) + process.exit(1) + end - local cpp = fs.open(joinPath(generatedPath, "modules.cpp"), "w+") + local cpp = fs.open(joinPath(config.outputDir, cppFile), "w+") writeLines(cpp, { "// This file is auto-generated by luthier.luau. Do not edit.", - "// Instead, you should modify the source files in `std/libs`.", + `// Instead, you should modify the source files in \`{config.sourcePath}\`.`, "", - '#include "modules.h"', + `#include "{headerFile}"`, "", - "constexpr std::pair lutestdlib[] = {", + `constexpr std::pair {config.arrayName}[] = \{`, }) - local libsPath = projectRelative("lute", "std", "libs") local numItems = 0 - traverseFileTree(libsPath, function(path, relativePath) + traverseFileTree(config.sourcePath, function(path, relativePath) if #relativePath == 0 then return -- Skip the root directory end - local aliasedPath = "@std/" .. relativePath + local aliasedPath = aliasWithSlash .. relativePath if safeFsType(path) == "file" then local libSrc = readFileToString(path) if #libSrc > 16_000 then -- https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026 -- MSVC can't handle a single string literal being over 16380 single-byte characters, - -- but can if they are adjacent contatenated strings. So, we split the string line-by-line into + -- but can if they are adjacent concatenated strings. So, we split the string line-by-line into -- adjacent substrings. local lines = string.split(libSrc, "\n") local generatedLines = table.create(#lines) @@ -417,7 +324,7 @@ local function generateStdLibFilesIfNeeded() end fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) else - fs.write(cpp, `\n \{"{aliasedPath}", R"LUTE_STDLIB({libSrc})LUTE_STDLIB"},\n`) + fs.write(cpp, `\n \{"{aliasedPath}", R"{delimiter}({libSrc}){delimiter}"},\n`) end end @@ -431,7 +338,7 @@ local function generateStdLibFilesIfNeeded() fs.write(cpp, "};\n") fs.close(cpp) - local header = fs.open(joinPath(generatedPath, "modules.h"), "w+") + local header = fs.open(joinPath(config.outputDir, headerFile), "w+") writeLines(header, { "// This file is auto-generated by luthier.luau. Do not edit.", "#pragma once", @@ -439,14 +346,11 @@ local function generateStdLibFilesIfNeeded() "#include ", "#include ", "", - `extern const std::pair lutestdlib[{numItems}];`, + `extern const std::pair {config.arrayName}[{numItems}];`, }) fs.close(header) - local hash = fs.open(joinPath(generatedPath, "hash.txt"), "w+") - fs.write(hash, getStdLibHash()) - - fs.close(hash) + writeStringToFile(hashFile, currentHash) end local function getTypeDefinitionsHash(): string @@ -471,23 +375,14 @@ local function getTypeDefinitionsHash(): string return hexify(digest) end -local function isGeneratedTypeDefinitionsUpToDate(): boolean - local hashFile = projectRelative("lute", "cli", "commands", "setup", "generated", "hash.txt") - - if safeFsType(hashFile) ~= "file" then - return false - end - - local hash = readFileToString(hashFile) - - return hash == getTypeDefinitionsHash() -end - local function generateTypeDefinitionFiles() local generatedPath = projectRelative("lute", "cli", "commands", "setup", "generated") pcall(fs.mkdir, generatedPath) - if isGeneratedTypeDefinitionsUpToDate() then + local hashFile = joinPath(generatedPath, "hash.txt") + local currentHash = getTypeDefinitionsHash() + + if isHashUpToDate(hashFile, currentHash) then return end @@ -517,9 +412,7 @@ local function generateTypeDefinitionFiles() stringDictionary ..= string.format('["%*"] = [==[%*]==],', key, value) end - local hash = fs.open(joinPath(generatedPath, "hash.txt"), "w+") - fs.write(hash, getTypeDefinitionsHash()) - fs.close(hash) + writeStringToFile(hashFile, currentHash) writeStringToFile( projectRelative("lute", "cli", "commands", "setup", "generated", "definitions.luau"), @@ -527,117 +420,29 @@ local function generateTypeDefinitionFiles() ) end -local function getCliCommandsHash() - local libsPath = projectRelative("lute", "cli", "commands") - local toHash = "" - - traverseFileTree(libsPath, function(path, relativePath) - if safeFsType(path) == "file" then - toHash ..= "./" .. relativePath - toHash ..= readFileToString(path) - end - end) - - local digest = crypto.digest(crypto.hash.blake2b256, toHash) - return hexify(digest) -end - -local function isGeneratedCliCommandsUpToDate() - local hashFile = projectRelative("lute", "cli", "generated", "hash.txt") - - if safeFsType(hashFile) ~= "file" then - return false - end - - local hash = readFileToString(hashFile) - - return hash == getCliCommandsHash() -end - -local function generateCliCommandsFilesIfNeeded() - local generatedPath = projectRelative("lute", "cli", "generated") - pcall(fs.mkdir, generatedPath) - - if isGeneratedCliCommandsUpToDate() then - return - end - - print("Generating code for CLI commands, files are out of date.") - - local cpp = fs.open(joinPath(generatedPath, "commands.cpp"), "w+") - - writeLines(cpp, { - "// This file is auto-generated by luthier.luau. Do not edit.", - "// Instead, you should modify the source files in `cli/commands`.", - "", - '#include "commands.h"', - "", - "constexpr std::pair luteclicommands[] = {", - }) - - local libsPath = projectRelative("lute", "cli", "commands") - local numItems = 0 - - traverseFileTree(libsPath, function(path, relativePath) - if #relativePath == 0 then - return -- Skip the root directory - end - - local aliasedPath = "@cli/" .. relativePath - - if safeFsType(path) == "file" then - local libSrc = readFileToString(path) - if #libSrc > 16_000 then - -- https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026 - -- MSVC can't handle a single string literal being over 16380 single-byte characters, - -- but can if they are adjacent contatenated strings. So, we split the string line-by-line into - -- adjacent substrings. - local lines = string.split(libSrc, "\n") - local generatedLines = table.create(#lines) - for _, line in lines do - local escapedLine = string.gsub(line, "\\", "\\\\") - escapedLine = string.gsub(escapedLine, '"', '\\"') - escapedLine = string.gsub(escapedLine, "\r", "") - table.insert(generatedLines, `"{escapedLine}\\n"`) - end - fs.write(cpp, `\n \{"{aliasedPath}", {table.concat(generatedLines, "\n")}},\n`) - else - fs.write(cpp, `\n \{"{aliasedPath}", R"LUTE_CMD({libSrc})LUTE_CMD"},\n`) - end - end - - if safeFsType(path) == "dir" then - fs.write(cpp, `\n \{"{aliasedPath}", "#directory"},\n`) - end - - numItems += 1 - end) - - fs.write(cpp, "};\n") - fs.close(cpp) - - local header = fs.open(joinPath(generatedPath, "commands.h"), "w+") - writeLines(header, { - "// This file is auto-generated by luthier.luau. Do not edit.", - "#pragma once", - "", - "#include ", - "#include ", - "", - `extern const std::pair luteclicommands[{numItems}];`, - }) - fs.close(header) - - local hash = fs.open(joinPath(generatedPath, "hash.txt"), "w+") - fs.write(hash, getCliCommandsHash()) - fs.close(hash) -end - local function generateFilesIfNeeded() - generateStdLibFilesIfNeeded() + generateEmbeddedModulesIfNeeded({ + sourcePath = projectRelative("lute", "std", "libs"), + outputDir = projectRelative("lute", "std", "src", "generated"), + alias = "@std", + arrayName = "lutestdlib", + moduleName = "modules", + }) generateTypeDefinitionFiles() - generateCliCommandsFilesIfNeeded() - generateBatteriesCommandsIfNeeded() + generateEmbeddedModulesIfNeeded({ + sourcePath = projectRelative("lute", "cli", "commands"), + outputDir = projectRelative("lute", "cli", "generated"), + alias = "@cli", + arrayName = "luteclicommands", + moduleName = "commands", + }) + generateEmbeddedModulesIfNeeded({ + sourcePath = projectRelative("batteries"), + outputDir = projectRelative("lute", "batteries", "generated"), + alias = "@batteries", + arrayName = "lutebatteries", + moduleName = "batteries", + }) end local function getExePath(): string From 1089c21f18edc971dc95dd4646a25ac3c0f47370 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 6 Mar 2026 10:06:11 -0800 Subject: [PATCH 385/642] Embeds `lute` modules so that we can surface the typedefinition files for typechecking. (#859) This PR adds a target that provides lute internal type definitions. This will be used in a follow up PR to embed the standard library for typechecking. --- CMakeLists.txt | 1 + lute/definitions/CMakeLists.txt | 13 +++++++++++ lute/definitions/include/lute/lutemodules.h | 18 ++++++++++++++++ lute/definitions/src/lutemodules.cpp | 24 +++++++++++++++++++++ lute/require/CMakeLists.txt | 2 +- tools/bootstrap.sh | 7 ++++++ tools/luthier.luau | 7 ++++++ tools/templates/definitions_header.h | 8 +++++++ tools/templates/definitions_impl.cpp | 6 ++++++ 9 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 lute/definitions/CMakeLists.txt create mode 100644 lute/definitions/include/lute/lutemodules.h create mode 100644 lute/definitions/src/lutemodules.cpp create mode 100644 tools/templates/definitions_header.h create mode 100644 tools/templates/definitions_impl.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fbb211eb4..87a248603 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,7 @@ endif() # libraries add_subdirectory(lute/batteries) add_subdirectory(lute/common) +add_subdirectory(lute/definitions) add_subdirectory(lute/require) add_subdirectory(lute/runtime) add_subdirectory(lute/crypto) diff --git a/lute/definitions/CMakeLists.txt b/lute/definitions/CMakeLists.txt new file mode 100644 index 000000000..333a3e0de --- /dev/null +++ b/lute/definitions/CMakeLists.txt @@ -0,0 +1,13 @@ +add_library(Lute.Lute STATIC) + +target_sources(Lute.Lute PRIVATE + include/lute/lutemodules.h + + src/lutemodules.cpp + src/generated/modules.h + src/generated/modules.cpp +) + +target_compile_features(Lute.Lute PUBLIC cxx_std_17) +target_include_directories(Lute.Lute PUBLIC "include") +target_compile_options(Lute.Lute PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/definitions/include/lute/lutemodules.h b/lute/definitions/include/lute/lutemodules.h new file mode 100644 index 000000000..4a02928e7 --- /dev/null +++ b/lute/definitions/include/lute/lutemodules.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +enum class LuteModuleType +{ + Module, + Directory, + NotFound, +}; + +struct LuteModuleResult +{ + LuteModuleType type; + std::string_view contents; +}; + +LuteModuleResult getLuteModule(std::string_view path); diff --git a/lute/definitions/src/lutemodules.cpp b/lute/definitions/src/lutemodules.cpp new file mode 100644 index 000000000..9e77fbf4a --- /dev/null +++ b/lute/definitions/src/lutemodules.cpp @@ -0,0 +1,24 @@ +#include "lute/lutemodules.h" + +// This file provides lutedefinitions and is auto-generated by luthier.luau. +// If it is not present or outdated, re-configure using luthier.luau. +#include "generated/modules.h" + +#include +#include + +LuteModuleResult getLuteModule(std::string_view path) +{ + for (const auto& [pathInLib, contents] : lutedefinitions) + { + if (path != pathInLib) + continue; + + if (contents == "#directory") + return {LuteModuleType::Directory}; + else + return {LuteModuleType::Module, contents}; + } + + return {LuteModuleType::NotFound}; +} diff --git a/lute/require/CMakeLists.txt b/lute/require/CMakeLists.txt index 44063f1ff..014c45701 100644 --- a/lute/require/CMakeLists.txt +++ b/lute/require/CMakeLists.txt @@ -30,5 +30,5 @@ target_sources(Lute.Require PRIVATE target_compile_features(Lute.Require PUBLIC cxx_std_17) target_include_directories(Lute.Require PUBLIC include) -target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Common Lute.Std Lute.CLI.Commands Lute.Batteries Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Runtime) +target_link_libraries(Lute.Require PUBLIC Luau.Require PRIVATE Luau.CLI.lib Luau.Compiler Luau.CodeGen Lute.Common Lute.Std Lute.CLI.Commands Lute.Batteries Lute.Std Lute.Lute Lute.Crypto Lute.Fs Lute.IO Lute.Luau Lute.Net Lute.Task Lute.VM Lute.Process Lute.System Lute.Time Lute.Runtime) target_compile_options(Lute.Require PRIVATE ${LUTE_OPTIONS}) diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh index 9fae2ac7b..72e6384cf 100755 --- a/tools/bootstrap.sh +++ b/tools/bootstrap.sh @@ -82,6 +82,13 @@ mkdir -p lute/batteries/generated exe cp ./tools/templates/batteries_impl.cpp ./lute/batteries/generated/batteries.cpp exe cp ./tools/templates/batteries_header.h ./lute/batteries/generated/batteries.h +# place empty versions of the lute definitions +rm -rf lute/definitions/src/generated +mkdir -p lute/definitions/src/generated + +exe cp ./tools/templates/definitions_impl.cpp ./lute/definitions/src/generated/modules.cpp +exe cp ./tools/templates/definitions_header.h ./lute/definitions/src/generated/modules.h + ## configure the build system for lute0 os_type="$(uname)" BUILD_ROOT="" diff --git a/tools/luthier.luau b/tools/luthier.luau index 5385d4751..8ee72c583 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -443,6 +443,13 @@ local function generateFilesIfNeeded() arrayName = "lutebatteries", moduleName = "batteries", }) + generateEmbeddedModulesIfNeeded({ + sourcePath = projectRelative("definitions"), + outputDir = projectRelative("lute", "definitions", "src", "generated"), + alias = "@lute", + arrayName = "lutedefinitions", + moduleName = "modules", + }) end local function getExePath(): string diff --git a/tools/templates/definitions_header.h b/tools/templates/definitions_header.h new file mode 100644 index 000000000..ee22837fa --- /dev/null +++ b/tools/templates/definitions_header.h @@ -0,0 +1,8 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `definitions`. +#pragma once + +#include +#include + +extern const std::pair lutedefinitions[1]; diff --git a/tools/templates/definitions_impl.cpp b/tools/templates/definitions_impl.cpp new file mode 100644 index 000000000..65253db32 --- /dev/null +++ b/tools/templates/definitions_impl.cpp @@ -0,0 +1,6 @@ +// This file is auto-generated by the bootstrap script. Do not edit. +// Instead, you should modify the source files in `definitions`. + +#include "modules.h" + +constexpr std::pair lutedefinitions[] = {{"", ""}}; From e8cc449b665e61d24b1877f7c998cbbad9915179 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 9 Mar 2026 11:09:31 -0700 Subject: [PATCH 386/642] Freezes parsed AST nodes to be immutable (#839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a prerequisite for making AST APIs available in Engine. I wrote a benchmark where we try to parse every luau file in the `lute` repo and benchmarked three approaches (thanks Hunter for the suggestions): - Manually freezing in the same pass that the AST is serialized - Freezing in an extra pass in the C++ - Freezing using a userland function and the second approach was a nice middleground which didn't sacrifice too much performance while having a stronger correctness guarantee from the simplified code. Numbers for those curious: ``` $hyperfine "./userland_freeze_lute ./scratch1.luau" "./vm_freeze_lute ./scratch1.luau" "./vm_extra_pass_freeze_lute ./scratch1.luau" --warmup 5 --show-output Benchmark 1: ./userland_freeze_lute ./scratch1.luau Time (mean ± σ): 3.230 s ± 0.023 s [User: 2.832 s, System: 0.392 s] Range (min … max): 3.186 s … 3.265 s 10 runs Benchmark 2: ./vm_freeze_lute ./scratch1.luau Time (mean ± σ): 2.562 s ± 0.026 s [User: 2.170 s, System: 0.386 s] Range (min … max): 2.515 s … 2.598 s 10 runs Benchmark 3: ./vm_extra_pass_freeze_lute ./scratch1.luau Time (mean ± σ): 2.875 s ± 0.038 s [User: 2.482 s, System: 0.389 s] Range (min … max): 2.845 s … 2.963 s 10 runs Summary ./vm_freeze_lute ./scratch1.luau ran 1.12 ± 0.02 times faster than ./vm_extra_pass_freeze_lute ./scratch1.luau 1.26 ± 0.02 times faster than ./userland_freeze_lute ./scratch1.luau ``` --- lute/luau/src/luau.cpp | 32 +- tests/std/syntax/parser.test.luau | 702 ++++++++++++++++++++++++++++++ 2 files changed, 733 insertions(+), 1 deletion(-) diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index b74689994..9042a5d52 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -29,6 +29,32 @@ namespace luau { +// Recursively freezes the table at the top of the stack and any descendant tables. +// The table must be at the top of the stack when called. +static void deepFreeze(lua_State* L) +{ + if (!lua_istable(L, -1)) + luaL_error(L, "expected table to freeze"); + + lua_rawcheckstack(L, 2); + + lua_pushnil(L); + while (lua_next(L, -2) != 0) + { + // Freeze the value if it's a table + if (lua_istable(L, -1)) + deepFreeze(L); + + // Pop value, keep key for next iteration + lua_pop(L, 1); + } + + if (lua_getreadonly(L, -1)) + return; + + lua_setreadonly(L, -1, 1); +} + struct StatResult { std::shared_ptr allocator; @@ -131,7 +157,7 @@ struct Span static Span checkSpan(lua_State* L, int index) { - lua_checkstack(L, 1); + lua_checkstack(L, 1); if (!lua_istable(L, index)) luaL_typeerror(L, index, "span"); @@ -2823,6 +2849,8 @@ int luau_parse(lua_State* L) } lua_setfield(L, -2, "lineoffsets"); + deepFreeze(L); + return 1; } @@ -2863,6 +2891,8 @@ int luau_parseexpr(lua_State* L) AstSerialize serializer{L, source, result.parseResult.cstNodeMap, result.parseResult.commentLocations}; serializer.visit(result.parseResult.root); + deepFreeze(L); + return 1; } diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index f4a1e4eba..60d96e72f 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -1241,3 +1241,705 @@ test.suite("parse types", function(suite) assert.eq(variadicType.type.tag, "reference") end) end) + +test.suite("AST_nodes_frozen", function(suite) + local frozenTableMessage = "attempt to modify a readonly table" + + -- Expression nodes + suite:case("AstExprGroup_frozen", function(assert) + local expr = parser.parseexpr("(x)") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "group") + assert.erroreq(function() + (expr :: any).tag = "modified" + end, frozenTableMessage) + end) + + suite:case("AstExprConstantNil_frozen", function(assert) + local expr = parser.parseexpr("nil") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "nil") + assert.erroreq(function() + (expr :: any).value = 123 + end, frozenTableMessage) + end) + + suite:case("AstExprConstantBool_frozen", function(assert) + local expr = parser.parseexpr("true") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "boolean") + assert.erroreq(function() + (expr :: any).value = false + end, frozenTableMessage) + end) + + suite:case("AstExprConstantNumber_frozen", function(assert) + local expr = parser.parseexpr("42") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "number") + assert.erroreq(function() + (expr :: any).value = 99 + end, frozenTableMessage) + end) + + suite:case("AstExprConstantString_frozen", function(assert) + local expr = parser.parseexpr("'hello'") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "string") + assert.erroreq(function() + (expr :: any).quotestyle = "double" + end, frozenTableMessage) + end) + + suite:case("AstExprLocal_frozen", function(assert) + local block = parser.parseblock("local x = 1\nreturn x") + local returnStat = block.statements[2] :: syntax.AstStatReturn + local expr = returnStat.expressions[1].node + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "local") + assert.erroreq(function() + (expr :: any).upvalue = true + end, frozenTableMessage) + end) + + suite:case("AstExprGlobal_frozen", function(assert) + local expr = parser.parseexpr("_G") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "global") + assert.erroreq(function() + (expr :: any).name = nil + end, frozenTableMessage) + end) + + suite:case("AstExprVarargs_frozen", function(assert) + local expr = parser.parseexpr("...") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "vararg") + assert.erroreq(function() + (expr :: any).tag = "modified" + end, frozenTableMessage) + end) + + suite:case("AstExprCall_frozen", function(assert) + local expr = parser.parseexpr("foo(1, 2)") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "call") + assert.erroreq(function() + (expr :: any).func = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((expr :: any).arguments, nil :: any) + end, frozenTableMessage) + end) + + suite:case("AstExprInstantiate_frozen", function(assert) + local callExpr = parser.parseexpr("foo<>()") + assert.eq(callExpr.tag, "call") + local expr = (callExpr :: syntax.AstExprCall).func + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "instantiate") + assert.erroreq(function() + (expr :: any).expr = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((expr :: any).typearguments, nil :: any) + end, frozenTableMessage) + end) + + suite:case("AstExprIndexName_frozen", function(assert) + local expr = parser.parseexpr("obj.field") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "indexname") + assert.erroreq(function() + (expr :: any).expression = nil + end, frozenTableMessage) + end) + + suite:case("AstExprIndexExpr_frozen", function(assert) + local expr = parser.parseexpr("obj[key]") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "index") + assert.erroreq(function() + (expr :: any).expression = nil + end, frozenTableMessage) + end) + + suite:case("AstExprFunction_frozen", function(assert) + local expr = parser.parseexpr("@checked function(x, y) return x + y end") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "function") + assert.erroreq(function() + (expr :: any).body = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((expr :: any).parameters, nil :: any) + end, frozenTableMessage) + assert.erroreq(function() + table.insert((expr :: any).attributes, nil :: any) + end, frozenTableMessage) + assert.erroreq(function() + table.insert((expr :: any).generics, nil :: any) + end, frozenTableMessage) + assert.erroreq(function() + table.insert((expr :: any).genericpacks, nil :: any) + end, frozenTableMessage) + end) + + suite:case("AstExprTable_frozen", function(assert) + local expr = parser.parseexpr("{ a = 1, [2] = 3, 4 }") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "table") + assert.erroreq(function() + (expr :: any).openbrace = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((expr :: any).entries, nil :: any) + end, frozenTableMessage) + assert.erroreq(function() + (expr :: any).entries[1].kind = nil + end, frozenTableMessage) + assert.erroreq(function() + (expr :: any).entries[2].kind = nil + end, frozenTableMessage) + assert.erroreq(function() + (expr :: any).entries[3].kind = nil + end, frozenTableMessage) + end) + + suite:case("AstExprUnary_frozen", function(assert) + local expr = parser.parseexpr("-x") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "unary") + assert.erroreq(function() + (expr :: any).operand = nil + end, frozenTableMessage) + end) + + suite:case("AstExprBinary_frozen", function(assert) + local expr = parser.parseexpr("x + y") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "binary") + assert.erroreq(function() + (expr :: any).lhsoperand = nil + end, frozenTableMessage) + end) + + suite:case("AstExprInterpString_frozen", function(assert) + local expr = parser.parseexpr("`hello {x}`") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "interpolatedstring") + assert.erroreq(function() + table.insert((expr :: any).strings, nil :: any) + end, frozenTableMessage) + assert.erroreq(function() + table.insert((expr :: any).expressions, nil :: any) + end, frozenTableMessage) + end) + + suite:case("AstExprTypeAssertion_frozen", function(assert) + local expr = parser.parseexpr("x :: number") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "cast") + assert.erroreq(function() + (expr :: any).operand = nil + end, frozenTableMessage) + end) + + suite:case("AstExprIfElse_frozen", function(assert) + local expr = parser.parseexpr("if x then 1 elseif y then 2 else 3") + assert.eq(expr.kind, "expr") + assert.eq(expr.tag, "conditional") + assert.erroreq(function() + (expr :: any).condition = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((expr :: any).elseifs, nil :: any) + end, frozenTableMessage) + assert.erroreq(function() + (expr :: any).elseifs[1].condition = nil + end, frozenTableMessage) + end) + + -- Statement nodes + suite:case("AstStatBlock_frozen", function(assert) + local block = parser.parseblock("local x = 1\nlocal y = 2") + assert.eq(block.kind, "stat") + assert.eq(block.tag, "block") + assert.erroreq(function() + table.insert((block :: any).statements, nil :: any) + end, frozenTableMessage) + assert.erroreq(function() + (block :: any).tag = nil + end, frozenTableMessage) + end) + + suite:case("AstStatDo_frozen", function(assert) + local block = parser.parseblock("do local x = 1 end") + local doStat = block.statements[1] :: syntax.AstStatDo + assert.eq(doStat.kind, "stat") + assert.eq(doStat.tag, "do") + assert.erroreq(function() + (doStat :: any).body = nil + end, frozenTableMessage) + end) + + suite:case("AstStatIf_frozen", function(assert) + local block = parser.parseblock("if x then local y = 1 elseif z then local w = 2 else local v = 3 end") + local ifStat = block.statements[1] :: syntax.AstStatIf + assert.eq(ifStat.kind, "stat") + assert.eq(ifStat.tag, "conditional") + assert.erroreq(function() + (ifStat :: any).condition = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((ifStat :: any).elseifs, nil :: any) + end, frozenTableMessage) + assert.erroreq(function() + (ifStat :: any).elseifs[1].condition = nil + end, frozenTableMessage) + end) + + suite:case("AstStatWhile_frozen", function(assert) + local block = parser.parseblock("while x do local y = 1 end") + local whileStat = block.statements[1] :: syntax.AstStatWhile + assert.eq(whileStat.kind, "stat") + assert.eq(whileStat.tag, "while") + assert.erroreq(function() + (whileStat :: any).condition = nil + end, frozenTableMessage) + end) + + suite:case("AstStatRepeat_frozen", function(assert) + local block = parser.parseblock("repeat local x = 1 until x > 10") + local repeatStat = block.statements[1] :: syntax.AstStatRepeat + assert.eq(repeatStat.kind, "stat") + assert.eq(repeatStat.tag, "repeat") + assert.erroreq(function() + (repeatStat :: any).body = nil + end, frozenTableMessage) + end) + + suite:case("AstStatBreak_frozen", function(assert) + local block = parser.parseblock("while true do break end") + local whileStat = block.statements[1] :: syntax.AstStatWhile + local breakStat = whileStat.body.statements[1] :: syntax.AstStatBreak + assert.eq(breakStat.kind, "stat") + assert.eq(breakStat.tag, "break") + assert.erroreq(function() + (breakStat :: any).tag = "modified" + end, frozenTableMessage) + end) + + suite:case("AstStatContinue_frozen", function(assert) + local block = parser.parseblock("while true do continue end") + local whileStat = block.statements[1] :: syntax.AstStatWhile + local continueStat = whileStat.body.statements[1] :: syntax.AstStatContinue + assert.eq(continueStat.kind, "stat") + assert.eq(continueStat.tag, "continue") + assert.erroreq(function() + (continueStat :: any).tag = "modified" + end, frozenTableMessage) + end) + + suite:case("AstStatReturn_frozen", function(assert) + local block = parser.parseblock("return 1, 2, 3") + local returnStat = block.statements[1] :: syntax.AstStatReturn + assert.eq(returnStat.kind, "stat") + assert.eq(returnStat.tag, "return") + assert.erroreq(function() + (returnStat :: any).returnkeyword = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((returnStat :: any).expressions, nil) + end, frozenTableMessage) + end) + + suite:case("AstStatExpr_frozen", function(assert) + local block = parser.parseblock("foo()") + local exprStat = block.statements[1] :: syntax.AstStatExpr + assert.eq(exprStat.kind, "stat") + assert.eq(exprStat.tag, "expression") + assert.erroreq(function() + (exprStat :: any).expression = nil + end, frozenTableMessage) + end) + + suite:case("AstStatLocal_frozen", function(assert) + local block = parser.parseblock("local x, y = 1, 2") + local localStat = block.statements[1] :: syntax.AstStatLocal + assert.eq(localStat.kind, "stat") + assert.eq(localStat.tag, "local") + assert.erroreq(function() + (localStat :: any).localkeyword = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((localStat :: any).variables, nil) + end, frozenTableMessage) + assert.erroreq(function() + table.insert((localStat :: any).values, nil) + end, frozenTableMessage) + end) + + suite:case("AstStatFor_frozen", function(assert) + local block = parser.parseblock("for i = 1, 10, 2 do local x = i end") + local forStat = block.statements[1] :: syntax.AstStatFor + assert.eq(forStat.kind, "stat") + assert.eq(forStat.tag, "for") + assert.erroreq(function() + (forStat :: any).variable = nil + end, frozenTableMessage) + end) + + suite:case("AstStatForIn_frozen", function(assert) + local block = parser.parseblock("for k, v in pairs(t) do local x = k end") + local forInStat = block.statements[1] :: syntax.AstStatForIn + assert.eq(forInStat.kind, "stat") + assert.eq(forInStat.tag, "forin") + assert.erroreq(function() + (forInStat :: any).inkeyword = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((forInStat :: any).variables, nil) + end, frozenTableMessage) + assert.erroreq(function() + table.insert((forInStat :: any).values, nil) + end, frozenTableMessage) + end) + + suite:case("AstStatAssign_frozen", function(assert) + local block = parser.parseblock("x, y = 1, 2") + local assignStat = block.statements[1] :: syntax.AstStatAssign + assert.eq(assignStat.kind, "stat") + assert.eq(assignStat.tag, "assign") + assert.erroreq(function() + (assignStat :: any).equals = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((assignStat :: any).variables, nil) + end, frozenTableMessage) + assert.erroreq(function() + table.insert((assignStat :: any).values, nil) + end, frozenTableMessage) + end) + + suite:case("AstStatCompoundAssign_frozen", function(assert) + local block = parser.parseblock("x += 5") + local compoundStat = block.statements[1] :: syntax.AstStatCompoundAssign + assert.eq(compoundStat.kind, "stat") + assert.eq(compoundStat.tag, "compoundassign") + assert.erroreq(function() + (compoundStat :: any).variable = nil + end, frozenTableMessage) + end) + + suite:case("AstStatFunction_frozen", function(assert) + local block = parser.parseblock("function foo() end") + local funcStat = block.statements[1] :: syntax.AstStatFunction + assert.eq(funcStat.kind, "stat") + assert.eq(funcStat.tag, "function") + assert.erroreq(function() + (funcStat :: any).name = nil + end, frozenTableMessage) + end) + + suite:case("AstStatLocalFunction_frozen", function(assert) + local block = parser.parseblock("local function foo() end") + local localFuncStat = block.statements[1] :: syntax.AstStatLocalFunction + assert.eq(localFuncStat.kind, "stat") + assert.eq(localFuncStat.tag, "localfunction") + assert.erroreq(function() + (localFuncStat :: any).name = nil + end, frozenTableMessage) + end) + + suite:case("AstStatTypeAlias_frozen", function(assert) + local block = parser.parseblock("type Foo = T") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + assert.eq(typeAlias.kind, "stat") + assert.eq(typeAlias.tag, "typealias") + assert.erroreq(function() + (typeAlias :: any).name = nil + end, frozenTableMessage) + end) + + suite:case("AstStatTypeFunction_frozen", function(assert) + local block = parser.parseblock("type function foo() return function() end end") + local typeFunc = block.statements[1] :: syntax.AstStatTypeFunction + assert.eq(typeFunc.kind, "stat") + assert.eq(typeFunc.tag, "typefunction") + assert.erroreq(function() + (typeFunc :: any).name = nil + end, frozenTableMessage) + end) + + -- Type nodes + suite:case("AstTypeReference_frozen", function(assert) + local block = parser.parseblock("type Foo = Bar") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local typeRef = typeAlias.type :: syntax.AstTypeReference + assert.eq(typeRef.kind, "type") + assert.eq(typeRef.tag, "reference") + assert.erroreq(function() + (typeRef :: any).name = nil + end, frozenTableMessage) + end) + + suite:case("AstTypeSingletonBool_frozen", function(assert) + local block = parser.parseblock("type Foo = true") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool + assert.eq(singletonBool.kind, "type") + assert.eq(singletonBool.tag, "boolean") + assert.erroreq(function() + (singletonBool :: any).value = false + end, frozenTableMessage) + end) + + suite:case("AstTypeSingletonString_frozen", function(assert) + local block = parser.parseblock("type Foo = 'hello'") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local singletonStr = typeAlias.type :: syntax.AstTypeSingletonString + assert.eq(singletonStr.kind, "type") + assert.eq(singletonStr.tag, "string") + assert.erroreq(function() + (singletonStr :: any).quotestyle = "double" + end, frozenTableMessage) + end) + + suite:case("AstTypeTypeof_frozen", function(assert) + local block = parser.parseblock("type Foo = typeof(x)") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local typeofType = typeAlias.type :: syntax.AstTypeTypeof + assert.eq(typeofType.kind, "type") + assert.eq(typeofType.tag, "typeof") + assert.erroreq(function() + (typeofType :: any).expression = nil + end, frozenTableMessage) + end) + + suite:case("AstTypeGroup_frozen", function(assert) + local block = parser.parseblock("type Foo = (Bar)") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local groupType = typeAlias.type :: syntax.AstTypeGroup + assert.eq(groupType.kind, "type") + assert.eq(groupType.tag, "group") + assert.erroreq(function() + (groupType :: any).type = nil + end, frozenTableMessage) + end) + + suite:case("AstTypeOptional_frozen", function(assert) + local block = parser.parseblock("type Foo = string?") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local unionType = typeAlias.type :: syntax.AstTypeUnion + local optionalType = unionType.types[2].node :: syntax.AstTypeOptional + assert.eq(optionalType.kind, "type") + assert.eq(optionalType.tag, "optional") + assert.erroreq(function() + (optionalType :: any).kind = "modified" + end, frozenTableMessage) + end) + + suite:case("AstTypeUnion_frozen", function(assert) + local block = parser.parseblock("type Foo = string | number") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local unionType = typeAlias.type :: syntax.AstTypeUnion + assert.eq(unionType.kind, "type") + assert.eq(unionType.tag, "union") + assert.erroreq(function() + (unionType :: any).leading = "modified" + end, frozenTableMessage) + assert.erroreq(function() + table.insert((unionType :: any).types, nil) + end, frozenTableMessage) + end) + + suite:case("AstTypeIntersection_frozen", function(assert) + local block = parser.parseblock("type Foo = A & B") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local intersectionType = typeAlias.type :: syntax.AstTypeIntersection + assert.eq(intersectionType.kind, "type") + assert.eq(intersectionType.tag, "intersection") + assert.erroreq(function() + (intersectionType :: any).leading = "modified" + end, frozenTableMessage) + assert.erroreq(function() + table.insert((intersectionType :: any).types, nil) + end, frozenTableMessage) + end) + + suite:case("AstTypeArray_frozen", function(assert) + local block = parser.parseblock("type Foo = {string}") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local arrayType = typeAlias.type :: syntax.AstTypeArray + assert.eq(arrayType.kind, "type") + assert.eq(arrayType.tag, "array") + assert.erroreq(function() + (arrayType :: any).type = nil + end, frozenTableMessage) + end) + + suite:case("AstTypeTable_frozen", function(assert) + local block = parser.parseblock('type Foo = { a: number, [string]: boolean, ["hello"]: "world" }') + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local tableType = typeAlias.type :: syntax.AstTypeTable + assert.eq(tableType.kind, "type") + assert.eq(tableType.tag, "table") + assert.erroreq(function() + (tableType :: any).openbrace = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((tableType :: any).entries, nil) + end, frozenTableMessage) + assert.erroreq(function() + (tableType :: any).entries[1].kind = nil + end, frozenTableMessage) + assert.erroreq(function() + (tableType :: any).entries[2].kind = nil + end, frozenTableMessage) + assert.erroreq(function() + (tableType :: any).entries[3].kind = nil + end, frozenTableMessage) + end) + + suite:case("AstTypeFunction_frozen", function(assert) + local block = parser.parseblock("type Foo = (x: number) -> string") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local funcType = typeAlias.type :: syntax.AstTypeFunction + assert.eq(funcType.kind, "type") + assert.eq(funcType.tag, "function") + assert.erroreq(function() + (funcType :: any).returnspecifier = nil + end, frozenTableMessage) + assert.erroreq(function() + (funcType :: any).parameters[1].node.name = nil + end, frozenTableMessage) + end) + + suite:case("AstTypePackExplicit_frozen", function(assert) + local block = parser.parseblock("type Foo = (number, string) -> (boolean, string)") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local funcType = typeAlias.type :: syntax.AstTypeFunction + local typePack = funcType.returntypes :: syntax.AstTypePackExplicit + assert.eq(typePack.kind, "typepack") + assert.eq(typePack.tag, "explicit") + assert.erroreq(function() + (typePack :: any).openparens = nil + end, frozenTableMessage) + end) + + suite:case("AstTypePackVariadic_frozen", function(assert) + local block = parser.parseblock("type Foo = () -> ...number") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local funcType = typeAlias.type :: syntax.AstTypeFunction + local typePack = funcType.returntypes :: syntax.AstTypePackVariadic + assert.eq(typePack.kind, "typepack") + assert.eq(typePack.tag, "variadic") + assert.erroreq(function() + (typePack :: any).type = nil + end, frozenTableMessage) + end) + + -- Other node types + suite:case("AstLocal_frozen", function(assert) + local block = parser.parseblock("local x: number = 1") + local localStat = block.statements[1] :: syntax.AstStatLocal + local local_ = localStat.variables[1].node + assert.eq(local_.kind, "local") + assert.erroreq(function() + (local_ :: any).name = nil + end, frozenTableMessage) + end) + + suite:case("AstGenericType_frozen", function(assert) + local block = parser.parseblock("type Foo = T") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local generic = (typeAlias.generics :: any)[1].node + assert.eq(generic.tag, "generic") + assert.erroreq(function() + generic.name = nil + end, frozenTableMessage) + end) + + suite:case("AstGenericTypePack_frozen", function(assert) + local block = parser.parseblock("type Foo = (T, U...) -> ()") + local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias + local genericPack = (typeAlias.genericpacks :: any)[1].node + assert.eq(genericPack.tag, "generic") + assert.eq(genericPack.kind, "typepack") + assert.erroreq(function() + genericPack.name = nil + end, frozenTableMessage) + end) + + -- Token type + suite:case("Token_frozen", function(assert) + local block = parser.parseblock("local x = 1") + local localStat = block.statements[1] :: syntax.AstStatLocal + local token = localStat.localkeyword + assert.eq(token.istoken, true) + assert.erroreq(function() + (token :: any).text = "modified" + end, frozenTableMessage) + assert.erroreq(function() + table.insert((token :: any).leadingtrivia, nil) + end, frozenTableMessage) + assert.erroreq(function() + table.insert((token :: any).trailingtrivia, nil) + end, frozenTableMessage) + end) + + -- Trivia types + suite:case("Trivia_frozen", function(assert) + local block = parser.parseblock([=[ + -- This is a comment + --[[This is a multiline comment + hello + ]] + + local x = 1 + ]=]) + local localStat = block.statements[1] :: syntax.AstStatLocal + local trivias = localStat.localkeyword.leadingtrivia + for _, trivia in trivias do + assert.erroreq(function() + (trivia :: any).text = "modified" + end, frozenTableMessage) + end + end) + + -- ParseResult type + suite:case("ParseResult_frozen", function(assert) + local result = parser.parse("local x = 1") + assert.erroreq(function() + (result :: any).root = nil + end, frozenTableMessage) + assert.erroreq(function() + table.insert((result :: any).lineoffsets, nil) + end, frozenTableMessage) + end) + + -- Pair type (used in Punctuated) + suite:case("Pair_frozen", function(assert) + local block = parser.parseblock("local x, y = 1, 2") + local localStat = block.statements[1] :: syntax.AstStatLocal + local pair = localStat.variables[1] + assert.erroreq(function() + (pair :: any).node = nil + end, frozenTableMessage) + assert.erroreq(function() + (pair :: any).separator = nil + end, frozenTableMessage) + end) + + -- Eof type + suite:case("Eof_frozen", function(assert) + local result = parser.parse("local x = 1") + local eof = result.eof + assert.eq(eof.tag, "eof") + assert.eq(eof.istoken, true) + assert.erroreq(function() + (eof :: any).tag = "modified" + end, frozenTableMessage) + end) +end) From d136b60e0a81c72942c8ca5c8475cbbf856d4003 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 10 Mar 2026 13:02:21 -0700 Subject: [PATCH 387/642] Modifies ParseResult's line offsets to be 1-indexed (#867) It was weird that they were previously 0-indexed since they were used to index into a Luau array. --- lute/luau/src/luau.cpp | 2 +- tests/std/syntax/parser.test.luau | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 9042a5d52..c76f59f94 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -2844,7 +2844,7 @@ int luau_parse(lua_State* L) for (size_t i = 0; i < serializer.lineOffsets.size(); i++) { lua_pushinteger(L, i + 1); - lua_pushnumber(L, serializer.lineOffsets[i]); + lua_pushnumber(L, serializer.lineOffsets[i] + 1); lua_settable(L, -3); } lua_setfield(L, -2, "lineoffsets"); diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 60d96e72f..70d1f6797 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -150,25 +150,25 @@ test.suite("Parser", function(suite) assert.eq(type(result.lineoffsets), "table") assert.eq(#result.lineoffsets, 1) - assert.eq(result.lineoffsets[1], 0) + assert.eq(result.lineoffsets[1], 1) local multiLineCode = "local x = 1\nlocal y = 2\nlocal z = 3" local multiResult = parser.parse(multiLineCode) assert.eq(type(multiResult.lineoffsets), "table") assert.eq(#multiResult.lineoffsets, 3) - assert.eq(multiResult.lineoffsets[1], 0) - assert.eq(multiResult.lineoffsets[2], 12) - assert.eq(multiResult.lineoffsets[3], 24) + assert.eq(multiResult.lineoffsets[1], 1) + assert.eq(multiResult.lineoffsets[2], 13) + assert.eq(multiResult.lineoffsets[3], 25) local emptyLineCode = "local x = 1\n\nlocal y = 2" local emptyResult = parser.parse(emptyLineCode) assert.eq(type(emptyResult.lineoffsets), "table") assert.eq(#emptyResult.lineoffsets, 3) - assert.eq(emptyResult.lineoffsets[1], 0) - assert.eq(emptyResult.lineoffsets[2], 12) - assert.eq(emptyResult.lineoffsets[3], 13) + assert.eq(emptyResult.lineoffsets[1], 1) + assert.eq(emptyResult.lineoffsets[2], 13) + assert.eq(emptyResult.lineoffsets[3], 14) end) suite:case("canParseAttributes", function(assert) From e746f0c6478894365e9cdef62c0cd01989140664 Mon Sep 17 00:00:00 2001 From: ariel Date: Tue, 10 Mar 2026 13:18:31 -0700 Subject: [PATCH 388/642] chore: clean up a bunch of the type errors and analysis warnings in the codebase (#857) I'm trying to burn down a lot of the type errors, clangd warnings, etc. throughout the codebase. This PR does a bunch of that, and leaves comments describing instances of Luau bugs that we had to work around as necessary. The workarounds are typically either a cast or explicit type instantiation that shouldn't be necessary. Some of the changes are not workarounds, and are just cases where a cast was correct to do. --- .lute/luthier.luau | 5 +- batteries/cli.luau | 2 +- batteries/toml.luau | 16 ++-- definitions/task.luau | 2 +- examples/colorful.luau | 16 ++-- examples/parallel_sort.luau | 18 +++-- foreman.toml | 2 +- lute/cli/commands/doc/init.luau | 14 +++- .../cli/commands/pkg/loom-core/extern/pp.luau | 5 +- .../pkg/loom-core/extern/unzip/crc.luau | 2 +- .../pkg/loom-core/extern/unzip/init.luau | 60 +++++++-------- .../loom-core/extern/unzip/utils/path.luau | 2 +- lute/cli/commands/transform/init.luau | 2 +- .../cli/commands/transform/lib/arguments.luau | 2 +- lute/luau/src/luau.cpp | 1 - lute/luau/src/resolverequire.cpp | 1 - lute/std/libs/fs.luau | 3 +- lute/std/libs/json.luau | 28 +++---- lute/std/libs/syntax/utils/trivia.luau | 4 +- lute/std/libs/tableext.luau | 4 +- lute/std/libs/task.luau | 4 +- rokit.toml | 2 +- tests/std/fs.test.luau | 6 ++ tests/std/process.test.luau | 20 +++-- ...ntime_error_doesnt_report_xpcall.snap.luau | 3 +- ..._error_doesnt_report_xpcall_case.snap.luau | 3 +- tests/std/syntax/query.test.luau | 3 +- tools/check-faillist.txt | 73 +------------------ 28 files changed, 128 insertions(+), 175 deletions(-) diff --git a/.lute/luthier.luau b/.lute/luthier.luau index 4c0c1ddbc..9d8e5e1f8 100644 --- a/.lute/luthier.luau +++ b/.lute/luthier.luau @@ -2,9 +2,10 @@ local process = require("@lute/process") -local args = table.pack(...) +-- LUAUFIX: should `...` actually have the type `string...` for command-line environments +local args: { [number]: string, n: number } = table.pack(...) :: any -local cmd = { process.execpath(), "tools/luthier.luau", table.unpack(args, 2, args.n) } +local cmd: {string} = { process.execpath(), "tools/luthier.luau", table.unpack(args, 2, args.n) } local result = process.run(cmd, { stdio = "inherit" }) process.exit(result.exitcode) diff --git a/batteries/cli.luau b/batteries/cli.luau index 5f24a309d..813c201a6 100644 --- a/batteries/cli.luau +++ b/batteries/cli.luau @@ -16,7 +16,7 @@ type ArgData = { } type ParseResult = { - values: { [string]: string }, + values: { [string]: string? }, flags: { [string]: boolean }, fwdArgs: { string }, } diff --git a/batteries/toml.luau b/batteries/toml.luau index 5dda55bce..d1a3a08f8 100644 --- a/batteries/toml.luau +++ b/batteries/toml.luau @@ -6,7 +6,7 @@ type SerializerState = { cursor: number, } -local function serializeValue(value: string | number) +local function serializeValue(value: string | number): string if typeof(value) == "string" then value = string.gsub(value, "\\", "\\\\") value = string.gsub(value, "\n", "\\n") @@ -23,7 +23,7 @@ local function serializeValue(value: string | number) end end -local function hasNestedTables(tbl: { [unknown]: unknown }) +local function hasNestedTables(tbl: { [unknown]: unknown }): boolean for _, v in tbl do if typeof(v) == "table" and next(v) ~= nil then return true @@ -32,7 +32,7 @@ local function hasNestedTables(tbl: { [unknown]: unknown }) return false end -local function tableToToml(tbl: { [any]: any }, parent: string?) +local function tableToToml(tbl: { [any]: any }, parent: string?): string local result = "" local subTables = {} local hasDirectValues = false @@ -81,7 +81,7 @@ type DeserializerState = { cursor: number, } -local function skipWhitespace(state: DeserializerState) +local function skipWhitespace(state: DeserializerState): () local pos = state.cursor while pos <= string.len(state.buf) and string.match(string.sub(state.buf, pos, pos), "%s") do pos += 1 @@ -89,14 +89,14 @@ local function skipWhitespace(state: DeserializerState) state.cursor = pos end -local function readLine(state: DeserializerState) +local function readLine(state: DeserializerState): string local nextLine = string.find(state.buf, "\n", state.cursor) or string.len(state.buf) + 1 local line = string.sub(state.buf, state.cursor, nextLine - 1) state.cursor = nextLine + 1 return line end -local function deserialize(input: string) +local function deserialize(input: string): { [string]: unknown } -- Normalize line endings: Replace Windows (\r\n) and old Mac (\r) with Unix (\n) input = string.gsub(input, "\r\n", "\n") input = string.gsub(input, "\r", "\n") @@ -105,7 +105,7 @@ local function deserialize(input: string) buf = input, cursor = 1, } - local result = {} + local result: { [string]: unknown } = {} local currentTable = result local arrayTables = {} @@ -118,7 +118,7 @@ local function deserialize(input: string) end if string.match(line, "^%[%[(.-)%]%]$") then - local tableName = string.match(line, "^%[%[(.-)%]%]$") + local tableName = string.match(line, "^%[%[(.-)%]%]$") or "" arrayTables[tableName] = arrayTables[tableName] or {} local newEntry = {} diff --git a/definitions/task.luau b/definitions/task.luau index 92cb2e320..68b479a17 100644 --- a/definitions/task.luau +++ b/definitions/task.luau @@ -19,7 +19,7 @@ function task.resume(thread: thread): thread error("unimplemented") end -function task.delay(dur: number | time.Duration, routine: (T...) -> U..., ...: T...): thread +function task.delay(dur: number | time.Duration, routine: thread | ((T...) -> U...), ...: T...): thread error("unimplemented") end diff --git a/examples/colorful.luau b/examples/colorful.luau index e2f0d2021..abfd1b4b4 100644 --- a/examples/colorful.luau +++ b/examples/colorful.luau @@ -1,11 +1,11 @@ -local colorful = require("@batteries/colorful") +local richterm = require("@batteries/richterm") -print(colorful.red("This is red text!")) -print(colorful.bgRed("This is red background!")) -print(colorful.bold("This is bold text!")) -print(colorful.underline("This is underlined text!")) -print(colorful.dim(colorful.italic("This is dim italic text!"))) +print(richterm.red("This is red text!")) +print(richterm.bgRed("This is red background!")) +print(richterm.bold("This is bold text!")) +print(richterm.underline("This is underlined text!")) +print(richterm.dim(richterm.italic("This is dim italic text!"))) -print(colorful.red(`red({colorful.blue(`blue({colorful.green("green")})`)})`)) +print(richterm.red(`red({richterm.blue(`blue({richterm.green("green")})`)})`)) -print(colorful.combine(colorful.red, colorful.bold)("This is bold red text!")) +print(richterm.combine(richterm.red, richterm.bold)("This is bold red text!")) diff --git a/examples/parallel_sort.luau b/examples/parallel_sort.luau index abcb30d51..dc177b901 100644 --- a/examples/parallel_sort.luau +++ b/examples/parallel_sort.luau @@ -2,7 +2,7 @@ local vm = require("@lute/vm") local task = require("@std/task") -local function getslice(t, n, m) +local function getslice(t, n: number, m: number) local size = math.ceil(#t / m) local slice = table.create(size) @@ -10,7 +10,7 @@ local function getslice(t, n, m) return slice end -local function merge(t1, t2, comp) +local function merge(t1: { T }, t2: { T }, comp: (T, T) -> lt) local t = table.create(#t1 + #t2) local pos1, pos2, pos = 1, 1, 1 @@ -42,13 +42,13 @@ local function merge(t1, t2, comp) end local threadCount = 8 -local threads = {} +local threads: { { [any]: any } } = {} for _ = 1, threadCount do table.insert(threads, vm.create("./parallel_sort_helper")) end -local function parallelMergeSort(t, comp) +local function parallelMergeSort(t: { T }, comp: (T, T) -> lt): { T } local slices = {} for i = 1, threadCount do @@ -56,12 +56,13 @@ local function parallelMergeSort(t, comp) end local tomerge = table.pack(task.awaitall(table.unpack(slices))) - tomerge.n = nil + tomerge.n = nil :: any while #tomerge > 1 do local next = {} for i = 1, #tomerge, 2 do - table.insert(next, merge(tomerge[i], tomerge[i + 1], comp)) + -- LUAUFIX: we should not need to explicitly cast these arrays to {T} + table.insert(next, merge(tomerge[i] :: { T }, tomerge[i + 1] :: { T }, comp)) end tomerge = next @@ -81,13 +82,14 @@ end local t2 = table.clone(t) local startseq = os.clock() -table.sort(t, function(a, b) +table.sort(t, function(a: number, b: number) return a < b end) print("sequential in:", os.clock() - startseq) local startparallel = os.clock() -t2 = parallelMergeSort(t2, function(a, b) +-- LUAUFIX: we should not need to explicitly instantiate the type here +t2 = parallelMergeSort<>(t2, function(a, b) return a < b end) print("parallel in:", os.clock() - startparallel) diff --git a/foreman.toml b/foreman.toml index 1dc711277..d907fab5b 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] -stylua = { github = "JohnnyMorganz/StyLua", version = "2.3.0" } +stylua = { github = "JohnnyMorganz/StyLua", version = "2.4.0" } lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260227" } diff --git a/lute/cli/commands/doc/init.luau b/lute/cli/commands/doc/init.luau index 38b2d5ee3..5e0afc735 100644 --- a/lute/cli/commands/doc/init.luau +++ b/lute/cli/commands/doc/init.luau @@ -177,9 +177,11 @@ local function extractPropertySignature( } end +type Definition = { name: string, signature: string, doc: string? } + local function generateMarkdown( moduleName: string, - definitions: { { name: string, signature: string, doc: string? } }, + definitions: { Definition }, filePath: path.pathlike, libraryPath: path.pathlike, requireLibraryAlias: string @@ -200,7 +202,8 @@ local function generateMarkdown( "", } - table.sort(definitions, function(a, b) + -- LUAUFIX: we should not need to explicitly instantiate the type of `table.sort` here + table.sort<>(definitions, function(a, b) return a.name < b.name end) @@ -296,10 +299,12 @@ local function processDirectory( end end +type Module = { name: string, isDir: boolean } + -- Helper to generate index with table of children local function generateLibraryIndex(title: string, alias: string, modulePath: path.pathlike): string local entries = fs.listdirectory(modulePath) - local modules = {} + local modules: { Module } = {} for _, entry in entries do local name = entry.name @@ -317,7 +322,8 @@ local function generateLibraryIndex(title: string, alias: string, modulePath: pa end end - table.sort(modules, function(a, b) + -- LUAUFIX: we should not need to explicitly instantiate the type of `table.sort` + table.sort<>(modules, function(a, b) return a.name < b.name end) diff --git a/lute/cli/commands/pkg/loom-core/extern/pp.luau b/lute/cli/commands/pkg/loom-core/extern/pp.luau index bf0e91627..77ea34311 100644 --- a/lute/cli/commands/pkg/loom-core/extern/pp.luau +++ b/lute/cli/commands/pkg/loom-core/extern/pp.luau @@ -90,14 +90,15 @@ local function traverseTable( local output = "" local indentStr = string.rep(" ", indent) - local keys = {} + local keys: { unknown } = {} -- Collect all keys, not just primitives for key in dataTable do table.insert(keys, key) end - table.sort(keys, function(a: string, b: string): boolean + -- LUAUFIX: we should not need to explicitly instantiate the type of `table.sort` + table.sort<>(keys, function(a, b) local typeofTableA, typeofTableB = typeof(dataTable[a]), typeof(dataTable[b]) if typeofTableA ~= typeofTableB then diff --git a/lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau index 38aac2d4e..3e3976903 100644 --- a/lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau +++ b/lute/cli/commands/pkg/loom-core/extern/unzip/crc.luau @@ -1,4 +1,4 @@ -local CRC32_TABLE = table.create(256) +local CRC32_TABLE = table.create(256) :: { number } -- TODO: Maybe compute this AoT as an optimization -- Initialize the lookup table and lock it in place diff --git a/lute/cli/commands/pkg/loom-core/extern/unzip/init.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/init.luau index d458d92e0..a3a8de27c 100644 --- a/lute/cli/commands/pkg/loom-core/extern/unzip/init.luau +++ b/lute/cli/commands/pkg/loom-core/extern/unzip/init.luau @@ -76,7 +76,7 @@ local MADE_BY_OS_LOOKUP: { [number]: MadeByOS } = { --[=[ @class ZipEntry - + A single entry (a file or a directory) in a ZIP file, and its properties. ]=] local ZipEntry = {} @@ -123,8 +123,8 @@ type ZipEntryInner = { -- stylua: ignore --[=[ - @within ZipEntry - @type MadeByOS "FAT" | "AMIGA" | "VMS" | "UNIX" | "VM/CMS" | "Atari ST" | "OS/2" | "MAC" | "Z-System" | "CP/M" | "NTFS" | "MVS" | "VSE" | "Acorn RISCOS" | "VFAT" | "Alternate MVS" | "BeOS" | "TANDEM" | "OS/400" | "OS/X" | "Unknown" + @within ZipEntry + @type MadeByOS "FAT" | "AMIGA" | "VMS" | "UNIX" | "VM/CMS" | "Atari ST" | "OS/2" | "MAC" | "Z-System" | "CP/M" | "NTFS" | "MVS" | "VSE" | "Acorn RISCOS" | "VFAT" | "Alternate MVS" | "BeOS" | "TANDEM" | "OS/400" | "OS/X" | "Unknown" The OS that created the ZIP. ]=] @@ -166,7 +166,7 @@ export type CompressionMethod = "STORE" | "DEFLATE" @within ZipEntry @private - A set of properties that describe a ZIP entry. Used internally for construction of + A set of properties that describe a ZIP entry. Used internally for construction of [ZipEntry] objects. @field versionMadeBy number -- Version of software and OS that created the ZIP @@ -178,13 +178,13 @@ export type CompressionMethod = "STORE" | "DEFLATE" @field crc number -- CRC32 checksum of the uncompressed data ]=] type ZipEntryProperties = { - versionMadeBy: number, - compressedSize: number, - size: number, - attributes: number, - timestamp: number, - method: CompressionMethod?, - crc: number, + read versionMadeBy: number, + read compressedSize: number, + read size: number, + read attributes: number, + read timestamp: number, + read method: CompressionMethod?, + read crc: number, } --[=[ @@ -239,7 +239,7 @@ end @within ZipEntry @method getPath - Resolves the path of the entry based on its relationship with other entries. It is recommended to use this + Resolves the path of the entry based on its relationship with other entries. It is recommended to use this method instead of accessing the `name` property directly, although they should be equivalent. > [!WARNING] @@ -301,7 +301,7 @@ end --[=[ @within ZipEntry - @method compressionEfficiency + @method compressionEfficiency Calculates the compression efficiency of the entry, or `nil` if the entry is a directory. @@ -396,7 +396,7 @@ type ZipReaderInner = { @function new Creates a new ZipReader instance from the raw bytes of a ZIP file. - + **Errors if the ZIP file is invalid.** @param data buffer -- The buffer containing the raw bytes of the ZIP @@ -430,8 +430,8 @@ end implementation is inspired by that of [async_zip], a Rust library for parsing ZIP files asynchronously. - This method involves buffered reading in reverse and reverse linear searching along those buffers - for the EoCD signature. As a result of the buffered approach, we reduce individual reads when compared + This method involves buffered reading in reverse and reverse linear searching along those buffers + for the EoCD signature. As a result of the buffered approach, we reduce individual reads when compared to reading every single byte sequentially, by a factor of the buffer size (4 KB by default). The buffer size of 4 KB was arrived at because it aligns with many systems' page sizes, and also provides a good balance between read efficiency (not too small), memory usage (not too large) and CPU cache @@ -508,7 +508,7 @@ export type EocdRecord = { using the [ZipReader:findEocdPosition]. **Errors if the ZIP file is invalid.** - + @error "Invalid Central Directory offset or size" @param pos number -- The offset to the End of Central Directory record @@ -559,11 +559,11 @@ end @method parseCentralDirectory @private - Parses the central directory of the ZIP file and populates the `entries` and `directories` + Parses the central directory of the ZIP file and populates the `entries` and `directories` fields. Used internally during initialization of the [ZipReader]. **Errors if the ZIP file is invalid.** - + @error "Invalid Central Directory entry signature" @error "Found different entries than specified in Central Directory" ]=] @@ -649,7 +649,7 @@ end @method buildDirectoryTree @private - Builds the directory tree from the entries. Used internally during initialization of the + Builds the directory tree from the entries. Used internally during initialization of the [ZipReader]. ]=] function ZipReader.buildDirectoryTree(self: ZipReader): () @@ -657,7 +657,7 @@ function ZipReader.buildDirectoryTree(self: ZipReader): () -- directories and files in separate passes over the entries, or sort -- the entries so I handled the directories first -- I decided to do -- the latter - table.sort(self.entries, function(a, b) + table.sort<>(self.entries, function(a, b) if a.isDirectory ~= b.isDirectory then return a.isDirectory end @@ -724,7 +724,7 @@ end --[=[ @within ZipReader @method findEntry - + Finds a [ZipEntry] by its path in the ZIP archive. @param path string -- Path to the entry to find @@ -780,7 +780,7 @@ type ExtractionOptions = { @within ZipReader @method extract - Extracts the specified [ZipEntry] from the ZIP archive. See [ZipReader:extractDirectory] for + Extracts the specified [ZipEntry] from the ZIP archive. See [ZipReader:extractDirectory] for extracting directories. @error "Cannot extract directory" -- If the entry is a directory, use [ZipReader:extractDirectory] instead @@ -821,13 +821,7 @@ function ZipReader.extract(self: ZipReader, entry: ZipEntry, options: Extraction } -- TODO: Use a `Partial` type function for this in the future! - local optionsOrDefault: { - followSymlinks: boolean, - decompress: boolean, - type: "binary" | "text", - skipCrcValidation: boolean, - skipSizeValidation: boolean, - } = if options + local optionsOrDefault = if options then setmetatable(options, { __index = defaultOptions }) :: any else defaultOptions @@ -1023,9 +1017,9 @@ end @interface ZipStatistics @within ZipReader - @field fileCount number -- The number of files in the ZIP - @field dirCount number -- The number of directories in the ZIP - @field totalSize number -- The total size of all files in the ZIP + @field fileCount number -- The number of files in the ZIP + @field dirCount number -- The number of directories in the ZIP + @field totalSize number -- The total size of all files in the ZIP ]=] export type ZipStatistics = { fileCount: number, dirCount: number, totalSize: number } diff --git a/lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau b/lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau index 030aec3d4..552fb1f50 100644 --- a/lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau +++ b/lute/cli/commands/pkg/loom-core/extern/unzip/utils/path.luau @@ -44,7 +44,7 @@ local function replaceBackslashes(input: string, replacement: "/"): string return "\\\\" .. input:sub(3):gsub("\\", replacement) else -- Replace all single backslashes - return input:gsub("\\", replacement) + return (input:gsub("\\", replacement)) end end diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index 5a9bb7863..369ccf582 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -13,7 +13,7 @@ local function exhaustiveMatch(value: never): never end local function loadMigration(path: string): types.Migration - local success, loaded = pcall(luau.loadbypath, path) + local success, loaded = pcall(luau.loadbypath, path, nil) assert(success, `{path} failed to require: {loaded}`) assert(loaded, `{path} is missing a return`) diff --git a/lute/cli/commands/transform/lib/arguments.luau b/lute/cli/commands/transform/lib/arguments.luau index 833a4e6ce..c625ecd72 100644 --- a/lute/cli/commands/transform/lib/arguments.luau +++ b/lute/cli/commands/transform/lib/arguments.luau @@ -27,7 +27,7 @@ local function parse(arguments: { string }): Config migrationPath = nil :: string?, migrationOptions = {}, filePaths = {}, - outputFile = nil, + outputFile = nil :: string?, } local i = 1 diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index c76f59f94..fa3e26848 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -4,7 +4,6 @@ #include "lute/configresolver.h" #include "lute/moduleresolver.h" #include "lute/type.h" -#include "lute/userdatas.h" #include "Luau/Ast.h" #include "Luau/BuiltinDefinitions.h" diff --git a/lute/luau/src/resolverequire.cpp b/lute/luau/src/resolverequire.cpp index fef59cf5a..1c0e2b2fc 100644 --- a/lute/luau/src/resolverequire.cpp +++ b/lute/luau/src/resolverequire.cpp @@ -3,7 +3,6 @@ #include "lute/filevfs.h" #include "lute/modulepath.h" -#include "Luau/Common.h" #include "Luau/RequireNavigator.h" #include "lua.h" diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 746e33a55..6166f2b3b 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -181,6 +181,7 @@ end --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. See example/walk_directory.luau for usage. function fslib.walk(path: pathlike, options: walkoptions?): () -> path? local queue: { path } = { pathlib.parse(path) } + local recursive = if options then options.recursive else false return function() while #queue > 0 do @@ -189,7 +190,7 @@ function fslib.walk(path: pathlike, options: walkoptions?): () -> path? return nil end - if fslib.type(current) == "dir" and options and options.recursive then -- LUAUFIX: error on options.recursive because it thinks options could still be nil + if fslib.type(current) == "dir" and recursive then local entries = fslib.listdirectory(current) for _, entry in entries do local fullPath = pathlib.join(current, entry.name) diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 0fc1c4fd0..7d9f73fd9 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -25,7 +25,7 @@ type SerializerState = { depth: number, } -local function checkState(state: SerializerState, len: number) +local function checkState(state: SerializerState, len: number): () local curLen = buffer.len(state.buf) if state.cursor + len < curLen then @@ -39,7 +39,7 @@ local function checkState(state: SerializerState, len: number) state.buf = newBuffer end -local function writeByte(state: SerializerState, byte: number) +local function writeByte(state: SerializerState, byte: number): () checkState(state, 1) buffer.writeu8(state.buf, state.cursor, byte) @@ -47,7 +47,7 @@ local function writeByte(state: SerializerState, byte: number) state.cursor += 1 end -local function writeSpaces(state: SerializerState) +local function writeSpaces(state: SerializerState): () if state.depth == 0 then return end @@ -66,7 +66,7 @@ local function writeSpaces(state: SerializerState) end end -local function writeString(state: SerializerState, str: string) +local function writeString(state: SerializerState, str: string): () checkState(state, #str) buffer.writestring(state.buf, state.cursor, str) @@ -86,7 +86,7 @@ local ESCAPE_MAP = { [0x22] = string.byte('"'), } -local function serializeUnicode(codepoint: number) +local function serializeUnicode(codepoint: number): string if codepoint >= 0x10000 then local high = math.floor((codepoint - 0x10000) / 0x400) + 0xD800 local low = ((codepoint - 0x10000) % 0x400) + 0xDC00 @@ -96,7 +96,7 @@ local function serializeUnicode(codepoint: number) return string.format("\\u%04x", codepoint) end -local function serializeString(state: SerializerState, str: string) +local function serializeString(state: SerializerState, str: string): () checkState(state, #str) writeByte(state, string.byte('"')) @@ -115,7 +115,7 @@ local function serializeString(state: SerializerState, str: string) writeByte(state, string.byte('"')) end -local function serializeArray(state: SerializerState, array: array) +local function serializeArray(state: SerializerState, array: array): () state.depth += 1 writeByte(state, string.byte("[")) @@ -150,7 +150,7 @@ local function serializeArray(state: SerializerState, array: array) writeByte(state, string.byte("]")) end -local function serializeTable(state: SerializerState, object: object) +local function serializeTable(state: SerializerState, object: object): () writeByte(state, string.byte("{")) if state.prettyPrint then @@ -195,7 +195,7 @@ local function serializeTable(state: SerializerState, object: object) writeByte(state, string.byte("}")) end -serializeAny = function(state: SerializerState, value: value) +serializeAny = function(state: SerializerState, value: value): () local valueType = type(value) if value == json.null then @@ -238,11 +238,11 @@ local function skipWhitespace(state: DeserializerState): boolean return true end -local function currentByte(state: DeserializerState) - return string.byte(state.src, state.cursor) +local function currentByte(state: DeserializerState): number + return (string.byte(state.src, state.cursor)) end -local function deserializeNumber(state: DeserializerState) +local function deserializeNumber(state: DeserializerState): number? -- Integer part local nStart, nEnd = string.find(state.src, "^-?[1-9]%d*", state.cursor) if not nStart then @@ -522,7 +522,7 @@ end -- user-facing -function json.serialize(value: value, prettyPrint: boolean?) +function json.serialize(value: value, prettyPrint: boolean?): string local state: SerializerState = { buf = buffer.create(bufferSize), cursor = 0, @@ -535,7 +535,7 @@ function json.serialize(value: value, prettyPrint: boolean?) return buffer.readstring(state.buf, 0, state.cursor) end -function json.deserialize(src: string) +function json.deserialize(src: string): (array | object | boolean | number | string)? local state = { src = src, cursor = 0, diff --git a/lute/std/libs/syntax/utils/trivia.luau b/lute/std/libs/syntax/utils/trivia.luau index 4f9a81a32..31ad3885b 100644 --- a/lute/std/libs/syntax/utils/trivia.luau +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -4,7 +4,7 @@ local types = require("../types") local retrieverLib = {} function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } - local q: query.query = query.findallfromroot(n, function(n) + local q = query.findallfromroot<>(n, function(n: types.Token) return if n.istoken ~= nil and n.istoken then n else nil end) @@ -27,7 +27,7 @@ function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } end function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } - local q: query.query = query.findallfromroot(n, function(n) + local q = query.findallfromroot<>(n, function(n: types.Token) return if n.istoken ~= nil and n.istoken then n else nil end) diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau index 41c044410..783f12cc0 100644 --- a/lute/std/libs/tableext.luau +++ b/lute/std/libs/tableext.luau @@ -58,7 +58,7 @@ function tableext.toset(tbl: { T }): { [T]: true } return set end -function tableext.extend(tbl: { T }, ...: { T }) +function tableext.extend(tbl: { T }, ...: { T }): () local extensions = table.pack(...) extensions.n = nil :: any for _, extension in extensions do @@ -68,7 +68,7 @@ function tableext.extend(tbl: { T }, ...: { T }) end end -function tableext.reverse(tbl: { T }, inplace: boolean?) +function tableext.reverse(tbl: { T }, inplace: boolean?): { T } local new = if inplace then tbl else {} local i, j = 1, #tbl while i <= j do diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index 7ce9e525d..b4cb1c53f 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -27,7 +27,7 @@ function tasklib.create(f, ...): task return data :: task end -function tasklib.await(t: task) +function tasklib.await(t: task): any if not t.co then error(`await: argument 1 is not a task`) end @@ -43,7 +43,7 @@ function tasklib.await(t: task) end end -function tasklib.awaitall(...: task) +function tasklib.awaitall(...: task): ...unknown local tasks = table.pack(...) for i, v in ipairs(tasks) do diff --git a/rokit.toml b/rokit.toml index 36d9cb52d..d6f81e584 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] -stylua = "JohnnyMorganz/StyLua@2.3.0" +stylua = "JohnnyMorganz/StyLua@2.4.0" lute = "luau-lang/lute@0.1.0-nightly.20260227" diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 055dd22b7..f921eb967 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -136,6 +136,9 @@ test.suite("FsSuite", function(suite) local nestedDir = path.join(tmpdir, "this", "should", "not", "work") local success, err = pcall(function() fs.createdirectory(nestedDir, { makeparents = false }) + + -- LUAUFIX: pcall's type is wrong, and we can mitigate that by returning nil from this function + return nil end) assert.neq(success, true) assert.neq(err, nil) @@ -145,6 +148,9 @@ test.suite("FsSuite", function(suite) local nestedDir = path.join(tmpdir, "this", "should", "not", "work") local success, err = pcall(function() fs.createdirectory(nestedDir) + + -- LUAUFIX: pcall's type is wrong, and we can mitigate that by returning nil from this function + return nil end) assert.neq(success, true) assert.neq(err, nil) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 2747481f3..2e3355d24 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -29,21 +29,25 @@ test.suite("ProcessSuite", function(suite) suite:case("run_system_and_cwd", function(check) local rootdir: string = "/" local root: pathlib.path? = nil + if system.win32 then root = pathlib.win32.drive(process.cwd() :: windowsPath.path) rootdir = pathlib.format(root) end - local r = process.run({ "pwd" }, { cwd = rootdir }) + + -- LUAUFIX: we shouldn't need to upcast the result of `process.system` to `{ [any]: any }` + local r = process.run({ "pwd" }, { cwd = rootdir }) :: { [any]: any } + if system.win32 then assert(root ~= nil and (root :: windowsPath.path).drive ~= nil) - check.tableeq(r, { -- LUAUFIX: ProcessResult Date: Tue, 10 Mar 2026 20:27:56 +0000 Subject: [PATCH 389/642] Update Lute to 0.1.0-nightly.20260306 (#861) **Lute**: Updated from `0.1.0-nightly.20260227` to `0.1.0-nightly.20260306` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260306 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: ariel --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index d907fab5b..c9c2b8297 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.4.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260227" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260306" } diff --git a/rokit.toml b/rokit.toml index d6f81e584..3091ff40b 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.4.0" -lute = "luau-lang/lute@0.1.0-nightly.20260227" +lute = "luau-lang/lute@0.1.0-nightly.20260306" From 2a3e3ede8cd0356a6bb1068048b0d742bec8e8e2 Mon Sep 17 00:00:00 2001 From: Andrey Zhabsky Date: Tue, 10 Mar 2026 22:39:35 +0200 Subject: [PATCH 390/642] Adds `process.args` property (#866) Adds `process.args` property for getting the arguments passed into `lute`. Resolves #234. Now we can run this code: ```luau local process = require("@std/process") local function printArgs() print(`path: {process.args[1]}, hello {process.args[2]}!`) end printArgs() ``` via: ```sh $ lute run .lute/args_property.luau world ``` and get: ```console path: .lute/args_property.luau, hello world! ``` --------- Co-authored-by: ariel --- definitions/process.luau | 1 + lute/cli/src/climain.cpp | 4 ++++ lute/process/src/process.cpp | 20 +++++++++++++++++++- lute/runtime/include/lute/runtime.h | 3 +++ lute/std/libs/process.luau | 1 + tests/std/process.test.luau | 4 ++++ 6 files changed, 32 insertions(+), 1 deletion(-) diff --git a/definitions/process.luau b/definitions/process.luau index 30e384607..231f3c399 100644 --- a/definitions/process.luau +++ b/definitions/process.luau @@ -27,6 +27,7 @@ export type ProcessResult = { local process = {} +process.args = {} :: { string } process.env = {} :: { [string]: string } function process.homedir(): string diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index b892be876..a147491e9 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -170,6 +170,10 @@ bool runBytecode( return false; } + runtime.args.clear(); + for (int i = 0; i < program_argc; ++i) + runtime.args.emplace_back(program_argv[i]); + runtime.GL = GL; runtime.runningThreads.push_back({true, getRefForThread(L), program_argc}); diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index 2e74ff207..b3d7e9ed5 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -678,7 +678,25 @@ int luteopen_process(lua_State* L) lua_setmetatable(L, -2); lua_setfield(L, -2, "env"); - lua_setreadonly(L, -1, 1); + // Create process.args table + Runtime* runtime = getRuntime(L); + if (runtime) + { + lua_createtable(L, static_cast(runtime->args.size()), 0); + for (int i = 0; i < static_cast(runtime->args.size()); ++i) + { + lua_pushlstring(L, runtime->args[i].c_str(), runtime->args[i].size()); + lua_rawseti(L, -2, i + 1); + } + } + else + { + lua_createtable(L, 0, 0); + } + lua_setreadonly(L, -1, 1); // args table + lua_setfield(L, -2, "args"); + + lua_setreadonly(L, -1, 1); // process table return 1; } diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index daf05c2cf..1c13b02cc 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -87,6 +87,9 @@ struct Runtime Luau::VecDeque runningThreads; + // CLI arguments passed after the script filename + std::vector args; + private: std::mutex continuationMutex; std::vector> continuations; diff --git a/lute/std/libs/process.luau b/lute/std/libs/process.luau index 8f8a8883b..443b0cee6 100644 --- a/lute/std/libs/process.luau +++ b/lute/std/libs/process.luau @@ -40,6 +40,7 @@ function processlib.execpath(): path end -- re-exports +processlib.args = process.args processlib.env = process.env return table.freeze(processlib) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 2e3355d24..f2ade0633 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -5,6 +5,10 @@ local test = require("@std/test") local windowsPath = require("@std/path/win32") test.suite("ProcessSuite", function(suite) + suite:case("process_args_is_table", function(assert) + assert.eq(type(process.args), "table") + end) + suite:case("homedir_and_cwd_and_execpath", function(assert) local hd = process.homedir() assert.neq(pathlib.format(hd), "") From 324a211e4a711710650bcde57a7996e88001a297 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:57:11 +0000 Subject: [PATCH 391/642] Update Luau to 0.711 (#860) **Luau**: Updated from `0.710` to `0.711` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.711 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: ariel --- extern/luau.tune | 4 ++-- lute/cli/commands/lint/rules/unused_variable.luau | 2 +- tools/check-faillist.txt | 7 ++----- tools/check.luau | 3 ++- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index f47395b1a..1991d9162 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.710" -revision = "964580112c1b59b19c4985dcc66be79162355b12" +branch = "0.711" +revision = "004d88ff2b05758947ea1d1f4919e7bb2192b880" diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index 3d83a0db6..44fac6199 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -423,7 +423,7 @@ local function lint( end end - return tableext.map(unusedLocals(ast, writeOnlyAPIs), function(l): lintTypes.LintViolation + return tableext.map(unusedLocals(ast, writeOnlyAPIs), function(l: syntax.AstLocal): lintTypes.LintViolation return { lintname = name, location = l.location, -- LUAUFIX: Bidirectional inference should let us know that l : syntax.AstLocal diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 6d36cd501..37f79aaa3 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -10,9 +10,11 @@ examples/process.luau:13:24-34 examples/process.luau:17:7-15 examples/process.luau:18:7-15 examples/process.luau:19:7-15 +examples/task-resume.luau:3:38-100 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/init.luau:83:12-20 +lute/cli/commands/lint/init.luau:311:61-67 lute/cli/commands/lint/init.luau:516:15-24 lute/cli/commands/lint/init.luau:517:56-65 lute/cli/commands/lint/init.luau:517:56-65 @@ -22,15 +24,10 @@ lute/cli/commands/lint/init.luau:517:56-65 lute/cli/commands/lint/init.luau:517:56-65 lute/cli/commands/lint/init.luau:530:36-45 lute/cli/commands/lint/init.luau:531:55-64 -lute/cli/commands/lint/printer.luau:51:7-62 -lute/cli/commands/lint/printer.luau:51:7-62 -lute/cli/commands/lint/printer.luau:58:8-52 -lute/cli/commands/lint/printer.luau:58:8-52 lute/cli/commands/lint/rules/almost_swapped.luau:92:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 lute/cli/commands/lint/rules/parenthesized_conditions.luau:68:4-15 -lute/cli/commands/lint/rules/unused_variable.luau:429:15-24 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:62:6-17 diff --git a/tools/check.luau b/tools/check.luau index 9f5e68686..64f5cf28c 100644 --- a/tools/check.luau +++ b/tools/check.luau @@ -56,7 +56,8 @@ local function getAllFiles(): { string } end local function readFaillist(): string - local ok, file = pcall(fs.open, FAILLIST_PATH, "r") + -- LUAUFIX: the type of pcall is breaking things here, so we must cast + local ok: boolean, file: fs.file = (pcall :: any)(fs.open, FAILLIST_PATH, "r") if not ok or not file then return "" end From 93a18677dca27a989a786d88ef6b631400ff058c Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 10 Mar 2026 16:24:59 -0700 Subject: [PATCH 392/642] Updates luau test case style to be a bit more consistent (#868) Test Suites are now PascalCase, and test cases should now be snake_case. --- tests/batteries/collections/deque.test.luau | 20 +++--- tests/batteries/difftext.test.luau | 68 ++++++++++----------- tests/cli/check.test.luau | 2 +- tests/cli/compile.test.luau | 4 +- tests/cli/doc.test.luau | 10 +-- tests/cli/lib/files.test.luau | 18 +++--- tests/cli/loadbypath.test.luau | 2 +- tests/cli/run.test.luau | 4 +- tests/cli/test.test.luau | 8 +-- tests/cli/transform.test.luau | 8 +-- tests/std/syntax/printer.test.luau | 2 +- tests/std/syntax/query.test.luau | 2 +- tests/std/test.test.luau | 4 +- 13 files changed, 76 insertions(+), 76 deletions(-) diff --git a/tests/batteries/collections/deque.test.luau b/tests/batteries/collections/deque.test.luau index e2cc0e050..2b07c740a 100644 --- a/tests/batteries/collections/deque.test.luau +++ b/tests/batteries/collections/deque.test.luau @@ -1,22 +1,22 @@ local deque = require("@batteries/collections/deque") local test = require("@std/test") -test.suite("deque", function(suite) - suite:case("deque.new with no value creates empty deque", function(assert) +test.suite("Deque", function(suite) + suite:case("deque_new_with_no_value_creates_empty_deque", function(assert) local deq = deque.new() assert.eq(deq:peekfront(), nil) assert.eq(deq:peekback(), nil) assert.eq(#deq, 0) end) - suite:case("deque.new with values creates deque with one element", function(assert) + suite:case("deque_new_with_values_creates_deque_with_one_element", function(assert) local deq = deque.new(1) assert.eq(deq:peekfront(), 1) assert.eq(deq:peekback(), 1) assert.eq(#deq, 1) end) - suite:case("deque:pushfront", function(assert) + suite:case("deque_pushfront", function(assert) local deq = deque.new(2) deq:pushfront(1) assert.eq(deq:peekfront(), 1) @@ -24,7 +24,7 @@ test.suite("deque", function(suite) assert.eq(#deq, 2) end) - suite:case("deque:pushback", function(assert) + suite:case("deque_pushback", function(assert) local deq = deque.new(2) deq:pushback(1) assert.eq(deq:peekfront(), 2) @@ -32,7 +32,7 @@ test.suite("deque", function(suite) assert.eq(#deq, 2) end) - suite:case("deque:popfront", function(assert) + suite:case("deque_popfront", function(assert) local deq = deque.new(2) local popped: number = deq:popfront() assert.eq(popped, 2) @@ -41,7 +41,7 @@ test.suite("deque", function(suite) assert.eq(deq:peekback(), nil) end) - suite:case("deque:popback", function(assert) + suite:case("deque_popback", function(assert) local deq = deque.new(2) local popped: number = deq:popback() assert.eq(popped, 2) @@ -50,19 +50,19 @@ test.suite("deque", function(suite) assert.eq(deq:peekback(), nil) end) - suite:case("deque:peekfront", function(assert) + suite:case("deque_peekfront", function(assert) local deq = deque.new(1) assert.eq(#deq, 1) assert.eq(deq:peekfront(), 1) end) - suite:case("deque:peekback", function(assert) + suite:case("deque_peekback", function(assert) local deq = deque.new(1) assert.eq(#deq, 1) assert.eq(deq:peekback(), 1) end) - suite:case("popping from empty deque", function(assert) + suite:case("popping_from_empty_deque", function(assert) local deq = deque.new() assert.erroreq(function() deq:popback() diff --git a/tests/batteries/difftext.test.luau b/tests/batteries/difftext.test.luau index 37908dab4..f9281cc7f 100644 --- a/tests/batteries/difftext.test.luau +++ b/tests/batteries/difftext.test.luau @@ -7,8 +7,8 @@ local diff = difftext.diff -- Each diff operation has structure: { key: "EQUAL" | "ADD" | "DELETE", text: string } -- Order matters - the sequence of operations defines the transformation -test.suite("myersdiff - bychar", function(suite) - suite:case("identical strings", function(assert) +test.suite("MyersDiffByChar", function(suite) + suite:case("identical_strings", function(assert) local a = "abc" local b = "abc" local diffResult = diff(a, b, { byChar = true }) @@ -21,7 +21,7 @@ test.suite("myersdiff - bychar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("single character change in middle", function(assert) + suite:case("single_character_change_in_middle", function(assert) local a = "abc" local b = "axc" local diffResult = diff(a, b, { byChar = true }) @@ -37,7 +37,7 @@ test.suite("myersdiff - bychar", function(suite) assert.eq(diffResult[4].text, "c") end) - suite:case("character addition at start", function(assert) + suite:case("character_addition_at_start", function(assert) local a = "bc" local b = "abc" local diffResult = diff(a, b, { byChar = true }) @@ -51,7 +51,7 @@ test.suite("myersdiff - bychar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("character addition at end", function(assert) + suite:case("character_addition_at_end", function(assert) local a = "ab" local b = "abc" local diffResult = diff(a, b, { byChar = true }) @@ -65,7 +65,7 @@ test.suite("myersdiff - bychar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("character deletion at start", function(assert) + suite:case("character_deletion_at_start", function(assert) local a = "abc" local b = "bc" local diffResult = diff(a, b, { byChar = true }) @@ -79,7 +79,7 @@ test.suite("myersdiff - bychar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("character deletion at end", function(assert) + suite:case("character_deletion_at_end", function(assert) local a = "abc" local b = "ab" local diffResult = diff(a, b, { byChar = true }) @@ -93,7 +93,7 @@ test.suite("myersdiff - bychar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("multiple consecutive changes", function(assert) + suite:case("multiple_consecutive_changes", function(assert) local a = "abc" local b = "xyz" local diffResult = diff(a, b, { byChar = true }) @@ -113,7 +113,7 @@ test.suite("myersdiff - bychar", function(suite) assert.eq(diffResult[6].text, "z") end) - suite:case("empty to non-empty", function(assert) + suite:case("empty_to_non_empty", function(assert) local a = "" local b = "hi" local diffResult = diff(a, b, { byChar = true }) @@ -125,7 +125,7 @@ test.suite("myersdiff - bychar", function(suite) assert.eq(diffResult[2].text, "i") end) - suite:case("non-empty to empty", function(assert) + suite:case("non_empty_to_empty", function(assert) local a = "hi" local b = "" local diffResult = diff(a, b, { byChar = true }) @@ -138,8 +138,8 @@ test.suite("myersdiff - bychar", function(suite) end) end) -test.suite("myersdiff - byline", function(suite) - suite:case("identical multiline strings", function(assert) +test.suite("MyersDiffByLine", function(suite) + suite:case("identical_multiline_strings", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline2\nline3" local diffResult = diff(a, b) @@ -153,7 +153,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("add line at start", function(assert) + suite:case("add_line_at_start", function(assert) local a = "line2\nline3" local b = "line1\nline2\nline3" local diffResult = diff(a, b) @@ -167,7 +167,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("add line at end", function(assert) + suite:case("add_line_at_end", function(assert) local a = "line1\nline2" local b = "line1\nline2\nline3" local diffResult = diff(a, b) @@ -181,7 +181,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("add line in middle", function(assert) + suite:case("add_line_in_middle", function(assert) local a = "line1\nline3" local b = "line1\nline2\nline3" local diffResult = diff(a, b) @@ -195,7 +195,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("delete line at start", function(assert) + suite:case("delete_line_at_start", function(assert) local a = "line1\nline2\nline3" local b = "line2\nline3" local diffResult = diff(a, b) @@ -209,7 +209,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("delete line at end", function(assert) + suite:case("delete_line_at_end", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline2" local diffResult = diff(a, b) @@ -223,7 +223,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("delete line in middle", function(assert) + suite:case("delete_line_in_middle", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline3" local diffResult = diff(a, b) @@ -237,7 +237,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("modify single line", function(assert) + suite:case("modify_single_line", function(assert) local a = "line1\nline2\nline3" local b = "line1\nmodified\nline3" local diffResult = diff(a, b) @@ -253,7 +253,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[4].text, "line3") end) - suite:case("multiple line changes", function(assert) + suite:case("multiple_line_changes", function(assert) local a = "A\nB\nC\nD" local b = "A\nX\nC\nY\nD" local diffResult = diff(a, b) @@ -273,7 +273,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[6].text, "D") end) - suite:case("empty to multiline", function(assert) + suite:case("empty_to_multiline", function(assert) local a = "" local b = "line1\nline2" local diffResult = diff(a, b) @@ -287,7 +287,7 @@ test.suite("myersdiff - byline", function(suite) assert.eq(diffResult[3].text, "line2") end) - suite:case("multiline to empty", function(assert) + suite:case("multiline_to_empty", function(assert) local a = "line1\nline2" local b = "" local diffResult = diff(a, b) @@ -302,7 +302,7 @@ test.suite("myersdiff - byline", function(suite) end) end) -test.suite("printdiff", function(suite) +test.suite("PrintDiff", function(suite) local function composeDetailedRemoval(str: string, changedCharPos: { [number]: true }, sideHeader: string?) local composed = richterm.brightRed(`- {sideHeader or ""}`) for i = 1, #str do @@ -327,7 +327,7 @@ test.suite("printdiff", function(suite) return composed end - suite:case("printdiff default - simple addition", function(assert) + suite:case("printdiff_default_simple_addition", function(assert) local a = "line1\nline2" local b = "line1\nline2\nline3" local result = difftext.prettydiff(a, b) @@ -339,7 +339,7 @@ test.suite("printdiff", function(suite) assert.eq(lines[3], richterm.brightGreen("+ line3")) end) - suite:case("printdiff default - simple deletion", function(assert) + suite:case("printdiff_default_simple_deletion", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline3" local result = difftext.prettydiff(a, b) @@ -351,7 +351,7 @@ test.suite("printdiff", function(suite) assert.eq(lines[3], " line3") end) - suite:case("printdiff default - modification", function(assert) + suite:case("printdiff_default_modification", function(assert) local a = "line1\nline2\nline3" local b = "line1\nmodified\nline3" local result = difftext.prettydiff(a, b) @@ -364,7 +364,7 @@ test.suite("printdiff", function(suite) assert.eq(lines[4], " line3") end) - suite:case("printdiff default - unchanged lines", function(assert) + suite:case("printdiff_default_unchanged_lines", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline2\nline3" local result = difftext.prettydiff(a, b) @@ -376,7 +376,7 @@ test.suite("printdiff", function(suite) assert.eq(lines[3], " line3") end) - suite:case("printdiff default - multiple changes", function(assert) + suite:case("printdiff_default_multiple_changes", function(assert) local a = "A\nB\nC\nD" local b = "A\nX\nC\nY\nD" local result = difftext.prettydiff(a, b) @@ -391,7 +391,7 @@ test.suite("printdiff", function(suite) assert.eq(lines[6], " D") end) - suite:case("printdiff default - with line numbers", function(assert) + suite:case("printdiff_default_with_line_numbers", function(assert) local a = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" local b = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" local result = difftext.prettydiff(a, b, { @@ -409,7 +409,7 @@ test.suite("printdiff", function(suite) end end) - suite:case("printdiff detailed - single line change", function(assert) + suite:case("printdiff_detailed_single_line_change", function(assert) local a = "hello world" local b = "hello wirld" local result = difftext.prettydiff(a, b, { detailed = true }) @@ -425,7 +425,7 @@ test.suite("printdiff", function(suite) assert.eq(lines[2], composeDetailedAddition("hello wirld", { [8] = true })) end) - suite:case("printdiff detailed - multiple line changes", function(assert) + suite:case("printdiff_detailed_multiple_line_changes", function(assert) local a = "line1\nold line\nline3" local b = "line1\nnew line\nline3" local result = difftext.prettydiff(a, b, { detailed = true }) @@ -445,7 +445,7 @@ test.suite("printdiff", function(suite) assert.eq(lines[4], " line3") end) - suite:case("printdiff detailed - unchanged lines", function(assert) + suite:case("printdiff_detailed_unchanged_lines", function(assert) local a = "line1\nline2" local b = "line1\nline2" local result = difftext.prettydiff(a, b, { detailed = true }) @@ -456,7 +456,7 @@ test.suite("printdiff", function(suite) assert.eq(lines[2], " line2") end) - suite:case("printdiff detailed - with line numbers", function(assert) + suite:case("printdiff_detailed_with_line_numbers", function(assert) local a = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" local b = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nlineten" local result = difftext.prettydiff(a, b, { @@ -474,7 +474,7 @@ test.suite("printdiff", function(suite) assert.eq(lines[11], composeDetailedAddition("lineten", { [5] = true, [6] = true, [7] = true }, " 10| ")) end) - suite:case("printdiff detailed - sanity check w/ 3 digits of line numbers", function(assert) + suite:case("printdiff_detailed_sanity_check_w/_3_digits_of_line_numbers", function(assert) local a = "unmodified\n" .. string.rep("line\n", 99) .. "unmodified" local b = "modified\n" .. string.rep("line\n", 99) .. "modified" local result = difftext.prettydiff(a, b, { diff --git a/tests/cli/check.test.luau b/tests/cli/check.test.luau index 7a6fd5838..340602893 100644 --- a/tests/cli/check.test.luau +++ b/tests/cli/check.test.luau @@ -5,7 +5,7 @@ local test = require("@std/test") local lutePath = path.format(process.execpath()) test.suite("LuteTypeCheck", function(suite) - suite:case("UsesNewSolver", function(assert) + suite:case("uses_new_solver", function(assert) local testFilePath = path.format(path.join("tests", "src", "staticrequires", "newsolver.luau")) local result = process.run({ lutePath, "check", testFilePath }) diff --git a/tests/cli/compile.test.luau b/tests/cli/compile.test.luau index d50631a86..73b6d9dec 100644 --- a/tests/cli/compile.test.luau +++ b/tests/cli/compile.test.luau @@ -23,8 +23,8 @@ local COMPILEE_CONTENTS = [[return "Hello world"]] local lutePath = process.execpath() local tmpDir = system.tmpdir() -test.suite("Lute CLI Compile", function(suite) - suite:case("Run compile", function(check) +test.suite("LuteCliCompile", function(suite) + suite:case("run_compile", function(check) -- Setup -- Create files local compilerPath = path.join(tmpDir, "compiler.luau") diff --git a/tests/cli/doc.test.luau b/tests/cli/doc.test.luau index 8143522ec..dc4e349b9 100644 --- a/tests/cli/doc.test.luau +++ b/tests/cli/doc.test.luau @@ -11,8 +11,8 @@ local lutePath = path.format(process.execpath()) local USAGE = "Usage: lute doc [OPTIONS]" -- Will add specific module test case (see examples/docs) once --module option is added -test.suite("lute-doc", function(suite) - suite:case("lute-doc-help-message", function(assert) +test.suite("LuteDoc", function(suite) + suite:case("lute_doc_help_message", function(assert) local result = process.run({ lutePath, "doc", "-h" }) assert.eq(result.exitcode, 0) @@ -29,21 +29,21 @@ test.suite("lute-doc", function(suite) assert.strcontains(result.stdout, USAGE) end) - suite:case("lute-doc-no-modules-provided", function(assert) + suite:case("lute_doc_no_modules_provided", function(assert) local result = process.system(`{lutePath} doc`) assert.eq(result.exitcode, 1) assert.strcontains(result.stderr, "Error: No module paths specified.") end) - suite:case("lute-doc-default-directory", function(assert) + suite:case("lute_doc_default_directory", function(assert) local result = process.system(`{lutePath} doc definitions lute/std/libs`) assert.eq(result.exitcode, 0) assert.strcontains(result.stdout, "Documentation generation complete!") end) - suite:case("lute-doc-specified-directory", function(assert) + suite:case("lute_doc_specified_directory", function(assert) if fs.exists(tmpOutputDir) then fs.removedirectory(tmpOutputDir, { recursive = true }) end diff --git a/tests/cli/lib/files.test.luau b/tests/cli/lib/files.test.luau index 15843aa2d..02064a967 100644 --- a/tests/cli/lib/files.test.luau +++ b/tests/cli/lib/files.test.luau @@ -7,14 +7,14 @@ local test = require("@std/test") local tmpdir = system.tmpdir() local testPath = path.join(tmpdir, "gitignore_test") -test.suite("cli/lib/files", function(suite) +test.suite("CliLibFiles", function(suite) suite:beforeeach(function() if fs.exists(testPath) then fs.removedirectory(testPath, { recursive = true }) end end) - suite:case("parse simple file pattern", function(assert) + suite:case("parse_simple_file_pattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -47,7 +47,7 @@ test.suite("cli/lib/files", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse directory pattern", function(assert) + suite:case("parse_directory_pattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -82,7 +82,7 @@ test.suite("cli/lib/files", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse negation pattern", function(assert) + suite:case("parse_negation_pattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -109,7 +109,7 @@ test.suite("cli/lib/files", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse leading double asterisk pattern", function(assert) + suite:case("parse_leading_double_asterisk_pattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -139,7 +139,7 @@ test.suite("cli/lib/files", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse trailing double asterisk pattern", function(assert) + suite:case("parse_trailing_double_asterisk_pattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -173,7 +173,7 @@ test.suite("cli/lib/files", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse middle double asterisk pattern", function(assert) + suite:case("parse_middle_double_asterisk_pattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -210,7 +210,7 @@ test.suite("cli/lib/files", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse rooted pattern", function(assert) + suite:case("parse_rooted_pattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -241,7 +241,7 @@ test.suite("cli/lib/files", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse question mark pattern", function(assert) + suite:case("parse_question_mark_pattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") diff --git a/tests/cli/loadbypath.test.luau b/tests/cli/loadbypath.test.luau index cde7065d4..7d52b6537 100644 --- a/tests/cli/loadbypath.test.luau +++ b/tests/cli/loadbypath.test.luau @@ -21,7 +21,7 @@ local tmpDirStr = system.tmpdir() local testDir = path.join(tmpDirStr, "lute_require_test") local testDirStr = path.format(testDir) -test.suite("Lute CLI Run", function(suite) +test.suite("LuteCliRun", function(suite) suite:beforeall(function() if not fs.exists(testDirStr) then fs.createdirectory(testDirStr) diff --git a/tests/cli/run.test.luau b/tests/cli/run.test.luau index b1ae8babe..c6e393d6d 100644 --- a/tests/cli/run.test.luau +++ b/tests/cli/run.test.luau @@ -10,14 +10,14 @@ local rundir = pathlib.join(tmpdir, "lute-run") local lutePath = pathlib.format(process.execpath()) -test.suite("lute-run", function(suite) +test.suite("LuteRun", function(suite) suite:beforeeach(function() if fs.exists(rundir) then fs.removedirectory(rundir, { recursive = true }) end end) - suite:case("same-named file and directory", function(assert) + suite:case("same_named_file_and_directory", function(assert) -- Setup fs.createdirectory(rundir) fs.createdirectory(pathlib.join(rundir, "hello")) diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 41daefa24..fc274c762 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -14,7 +14,7 @@ SmokeSuite: ]] test.suite("LuteTestCommand", function(suite) - suite:case("lute test help message", function(assert) + suite:case("lute_test_help_message", function(assert) -- Run lute test with -h flag local result = process.run({ lutePath, "test", "-h" }) @@ -23,7 +23,7 @@ test.suite("LuteTestCommand", function(suite) assert.strcontains(result.stdout, "Usage:") end) - suite:case("lute test runs tests in discovery folder", function(assert) + suite:case("lute_test_runs_tests_in_discovery_folder", function(assert) -- Run lute test on tests/cli/discovery folder local result = process.run({ lutePath, "test", "tests/cli/discovery" }) @@ -50,7 +50,7 @@ test.suite("LuteTestCommand", function(suite) assert.neq(result.stdout:find("2 passed", 1, true), nil) end) - suite:case("lute test filters by case name", function(assert) + suite:case("lute_test_filters_by_case_name", function(assert) -- Run lute test with --case flag local result = process.run({ lutePath, "test", "--case", "make_pass", "tests/cli/discovery" }) @@ -60,7 +60,7 @@ test.suite("LuteTestCommand", function(suite) assert.neq(result.stdout:find("1 passed", 1, true), nil) end) - suite:case("lute test filters by suite and case name", function(assert) + suite:case("lute_test_filters_by_suite_and_case_name", function(assert) -- Run lute test with both --suite and --case flags local result = process.run({ lutePath, "test", "-s", "SmokeSuite", "-c", "make_fail", "tests/cli/discovery" }) diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index 529836f99..1c55beb38 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -14,7 +14,7 @@ local tmpDir = system.tmpdir() local transformeePath = path.format(path.join(tmpDir, "transformee.luau")) local outputPath = path.format(path.join(tmpDir, "transformee_output.luau")) -test.suite("lute transform", function(suite) +test.suite("LuteTransform", function(suite) suite:beforeeach(function() -- Create a transformee file local transformeeHandle = fs.open(transformeePath, "w+") @@ -30,7 +30,7 @@ test.suite("lute transform", function(suite) end end) - suite:case("transform query style", function(assert) + suite:case("transform_query_style", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau local transformerExample = path.format(path.join("examples", "query_transformer.luau")) @@ -54,7 +54,7 @@ test.suite("lute transform", function(suite) fs.remove(transformerDest) end) - suite:case("transform output directory happy path", function(assert) + suite:case("transform_output_directory_happy_path", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau local transformerExample = path.format(path.join("examples", "query_transformer.luau")) @@ -89,7 +89,7 @@ test.suite("lute transform", function(suite) fs.remove(outputPath) end) - suite:case("transform output creates file if it doesn't exist", function(assert) + suite:case("transform_output_creates_file_if_it_doesnt_exist", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau local transformerExample = path.format(path.join("examples", "query_transformer.luau")) diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 5847b0ff5..a7d6f0888 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -20,7 +20,7 @@ local function checkReplacement( assert.eq(result, expected) end -test.suite("syntax_printer", function(suite) +test.suite("SyntaxPrinter", function(suite) suite:case("string_replacement", function(assert) local source = [[ local name = "World" diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index f123b63b1..e1fd6a396 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -3,7 +3,7 @@ local query = require("@std/syntax/query") local test = require("@std/test") local utils = require("@std/syntax/utils") -test.suite("AST Query", function(suite) +test.suite("AstQuery", function(suite) suite:case("filter", function(assert) local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index 324033056..dc5a828c4 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -26,7 +26,7 @@ local function assertErrorsWithMsg( end test.suite("LuteStdTestFramework", function(suite) - suite:case("lute doesn't report xpcall as error when accessing field of nil in suite", function(assert) + suite:case("lute_doesnt_report_xpcall_as_error_when_accessing_field_of_nil_in_suite", function(assert) -- Setup local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") fs.writestringtofile( @@ -62,7 +62,7 @@ test.run() fs.remove(testFilePath) end) - suite:case("lute doesn't report xpcall as error when accessing field of nil in case", function(assert) + suite:case("lute_doesnt_report_xpcall_as_error_when_accessing_field_of_nil_in_case", function(assert) -- Setup local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") fs.writestringtofile( From 689712fb1550e49da1ed911fa52c03c14cb8b854 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 11 Mar 2026 11:37:24 -0700 Subject: [PATCH 393/642] camelCases luau test case names (#869) --- tests/batteries/base64.test.luau | 8 +- tests/batteries/collections/deque.test.luau | 18 +- tests/batteries/difftext.test.luau | 62 +++---- tests/cli/check.test.luau | 2 +- tests/cli/compile.test.luau | 2 +- tests/cli/discovery/example.spec.luau | 2 +- tests/cli/discovery/smoke.test.luau | 6 +- tests/cli/doc.test.luau | 8 +- tests/cli/lib/files.test.luau | 16 +- tests/cli/lint.test.luau | 194 +++++++++---------- tests/cli/run.test.luau | 2 +- tests/cli/test.test.luau | 24 +-- tests/cli/transform.test.luau | 6 +- tests/lute/crypto.test.luau | 14 +- tests/lute/task.test.luau | 4 +- tests/lute/vm.test.luau | 2 +- tests/std/fs.test.luau | 30 +-- tests/std/json.test.luau | 4 +- tests/std/luau.test.luau | 6 +- tests/std/net.test.luau | 2 +- tests/std/path/path.posix.test.luau | 154 +++++++-------- tests/std/path/path.win32.test.luau | 196 ++++++++++---------- tests/std/process.test.luau | 18 +- tests/std/stringext.test.luau | 4 +- tests/std/syntax/parser.test.luau | 118 ++++++------ tests/std/syntax/printer.test.luau | 124 ++++++------- tests/std/syntax/query.test.luau | 12 +- tests/std/tableext.test.luau | 2 +- tests/std/test.test.luau | 60 +++--- tests/std/time.test.luau | 20 +- 30 files changed, 560 insertions(+), 560 deletions(-) diff --git a/tests/batteries/base64.test.luau b/tests/batteries/base64.test.luau index 5e5f4be68..7ceefed25 100644 --- a/tests/batteries/base64.test.luau +++ b/tests/batteries/base64.test.luau @@ -45,19 +45,19 @@ test.suite("Base64", function(suite) local input = vector[1] local expected_encoded = vector[2] - suite:case("Encode: '" .. input .. "'", function(assert) + suite:case("encode: '" .. input .. "'", function(assert) local encoded = enc(input) assert.eq(expected_encoded, encoded) end) - suite:case("Decode: '" .. expected_encoded .. "'", function(assert) + suite:case("decode: '" .. expected_encoded .. "'", function(assert) local decoded = dec(expected_encoded) assert.eq(input, decoded) end) end for _, invalid in invalidInputs do - suite:case("Invalid Decode: '" .. invalid .. "'", function(assert) + suite:case("invalidDecode: '" .. invalid .. "'", function(assert) assert.errors(function() dec(invalid) end) @@ -65,7 +65,7 @@ test.suite("Base64", function(suite) end for _, invalid in invalidLastBytes do - suite:case("Invalid Decode (last bytes): '" .. invalid .. "'", function(assert) + suite:case("invalidDecodeLastBytes: '" .. invalid .. "'", function(assert) assert.errors(function() dec(invalid) end) diff --git a/tests/batteries/collections/deque.test.luau b/tests/batteries/collections/deque.test.luau index 2b07c740a..d43b0a587 100644 --- a/tests/batteries/collections/deque.test.luau +++ b/tests/batteries/collections/deque.test.luau @@ -2,21 +2,21 @@ local deque = require("@batteries/collections/deque") local test = require("@std/test") test.suite("Deque", function(suite) - suite:case("deque_new_with_no_value_creates_empty_deque", function(assert) + suite:case("dequeNewWithNoValueCreatesEmptyDeque", function(assert) local deq = deque.new() assert.eq(deq:peekfront(), nil) assert.eq(deq:peekback(), nil) assert.eq(#deq, 0) end) - suite:case("deque_new_with_values_creates_deque_with_one_element", function(assert) + suite:case("dequeNewWithValuesCreatesDequeWithOneElement", function(assert) local deq = deque.new(1) assert.eq(deq:peekfront(), 1) assert.eq(deq:peekback(), 1) assert.eq(#deq, 1) end) - suite:case("deque_pushfront", function(assert) + suite:case("dequePushFront", function(assert) local deq = deque.new(2) deq:pushfront(1) assert.eq(deq:peekfront(), 1) @@ -24,7 +24,7 @@ test.suite("Deque", function(suite) assert.eq(#deq, 2) end) - suite:case("deque_pushback", function(assert) + suite:case("dequePushBack", function(assert) local deq = deque.new(2) deq:pushback(1) assert.eq(deq:peekfront(), 2) @@ -32,7 +32,7 @@ test.suite("Deque", function(suite) assert.eq(#deq, 2) end) - suite:case("deque_popfront", function(assert) + suite:case("dequePopFront", function(assert) local deq = deque.new(2) local popped: number = deq:popfront() assert.eq(popped, 2) @@ -41,7 +41,7 @@ test.suite("Deque", function(suite) assert.eq(deq:peekback(), nil) end) - suite:case("deque_popback", function(assert) + suite:case("dequePopBack", function(assert) local deq = deque.new(2) local popped: number = deq:popback() assert.eq(popped, 2) @@ -50,19 +50,19 @@ test.suite("Deque", function(suite) assert.eq(deq:peekback(), nil) end) - suite:case("deque_peekfront", function(assert) + suite:case("dequePeekFront", function(assert) local deq = deque.new(1) assert.eq(#deq, 1) assert.eq(deq:peekfront(), 1) end) - suite:case("deque_peekback", function(assert) + suite:case("dequePeekBack", function(assert) local deq = deque.new(1) assert.eq(#deq, 1) assert.eq(deq:peekback(), 1) end) - suite:case("popping_from_empty_deque", function(assert) + suite:case("poppingFromEmptyDeque", function(assert) local deq = deque.new() assert.erroreq(function() deq:popback() diff --git a/tests/batteries/difftext.test.luau b/tests/batteries/difftext.test.luau index f9281cc7f..1dd9bc7ed 100644 --- a/tests/batteries/difftext.test.luau +++ b/tests/batteries/difftext.test.luau @@ -8,7 +8,7 @@ local diff = difftext.diff -- Order matters - the sequence of operations defines the transformation test.suite("MyersDiffByChar", function(suite) - suite:case("identical_strings", function(assert) + suite:case("identicalStrings", function(assert) local a = "abc" local b = "abc" local diffResult = diff(a, b, { byChar = true }) @@ -21,7 +21,7 @@ test.suite("MyersDiffByChar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("single_character_change_in_middle", function(assert) + suite:case("singleCharacterChangeInMiddle", function(assert) local a = "abc" local b = "axc" local diffResult = diff(a, b, { byChar = true }) @@ -37,7 +37,7 @@ test.suite("MyersDiffByChar", function(suite) assert.eq(diffResult[4].text, "c") end) - suite:case("character_addition_at_start", function(assert) + suite:case("characterAdditionAtStart", function(assert) local a = "bc" local b = "abc" local diffResult = diff(a, b, { byChar = true }) @@ -51,7 +51,7 @@ test.suite("MyersDiffByChar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("character_addition_at_end", function(assert) + suite:case("characterAdditionAtEnd", function(assert) local a = "ab" local b = "abc" local diffResult = diff(a, b, { byChar = true }) @@ -65,7 +65,7 @@ test.suite("MyersDiffByChar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("character_deletion_at_start", function(assert) + suite:case("characterDeletionAtStart", function(assert) local a = "abc" local b = "bc" local diffResult = diff(a, b, { byChar = true }) @@ -79,7 +79,7 @@ test.suite("MyersDiffByChar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("character_deletion_at_end", function(assert) + suite:case("characterDeletionAtEnd", function(assert) local a = "abc" local b = "ab" local diffResult = diff(a, b, { byChar = true }) @@ -93,7 +93,7 @@ test.suite("MyersDiffByChar", function(suite) assert.eq(diffResult[3].text, "c") end) - suite:case("multiple_consecutive_changes", function(assert) + suite:case("multipleConsecutiveChanges", function(assert) local a = "abc" local b = "xyz" local diffResult = diff(a, b, { byChar = true }) @@ -113,7 +113,7 @@ test.suite("MyersDiffByChar", function(suite) assert.eq(diffResult[6].text, "z") end) - suite:case("empty_to_non_empty", function(assert) + suite:case("emptyToNonEmpty", function(assert) local a = "" local b = "hi" local diffResult = diff(a, b, { byChar = true }) @@ -125,7 +125,7 @@ test.suite("MyersDiffByChar", function(suite) assert.eq(diffResult[2].text, "i") end) - suite:case("non_empty_to_empty", function(assert) + suite:case("nonEmptyToEmpty", function(assert) local a = "hi" local b = "" local diffResult = diff(a, b, { byChar = true }) @@ -139,7 +139,7 @@ test.suite("MyersDiffByChar", function(suite) end) test.suite("MyersDiffByLine", function(suite) - suite:case("identical_multiline_strings", function(assert) + suite:case("identicalMultilineStrings", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline2\nline3" local diffResult = diff(a, b) @@ -153,7 +153,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("add_line_at_start", function(assert) + suite:case("addLineAtStart", function(assert) local a = "line2\nline3" local b = "line1\nline2\nline3" local diffResult = diff(a, b) @@ -167,7 +167,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("add_line_at_end", function(assert) + suite:case("addLineAtEnd", function(assert) local a = "line1\nline2" local b = "line1\nline2\nline3" local diffResult = diff(a, b) @@ -181,7 +181,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("add_line_in_middle", function(assert) + suite:case("addLineInMiddle", function(assert) local a = "line1\nline3" local b = "line1\nline2\nline3" local diffResult = diff(a, b) @@ -195,7 +195,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("delete_line_at_start", function(assert) + suite:case("deleteLineAtStart", function(assert) local a = "line1\nline2\nline3" local b = "line2\nline3" local diffResult = diff(a, b) @@ -209,7 +209,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("delete_line_at_end", function(assert) + suite:case("deleteLineAtEnd", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline2" local diffResult = diff(a, b) @@ -223,7 +223,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("delete_line_in_middle", function(assert) + suite:case("deleteLineInMiddle", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline3" local diffResult = diff(a, b) @@ -237,7 +237,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[3].text, "line3") end) - suite:case("modify_single_line", function(assert) + suite:case("modifySingleLine", function(assert) local a = "line1\nline2\nline3" local b = "line1\nmodified\nline3" local diffResult = diff(a, b) @@ -253,7 +253,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[4].text, "line3") end) - suite:case("multiple_line_changes", function(assert) + suite:case("multipleLineChanges", function(assert) local a = "A\nB\nC\nD" local b = "A\nX\nC\nY\nD" local diffResult = diff(a, b) @@ -273,7 +273,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[6].text, "D") end) - suite:case("empty_to_multiline", function(assert) + suite:case("emptyToMultiline", function(assert) local a = "" local b = "line1\nline2" local diffResult = diff(a, b) @@ -287,7 +287,7 @@ test.suite("MyersDiffByLine", function(suite) assert.eq(diffResult[3].text, "line2") end) - suite:case("multiline_to_empty", function(assert) + suite:case("multilineToEmpty", function(assert) local a = "line1\nline2" local b = "" local diffResult = diff(a, b) @@ -327,7 +327,7 @@ test.suite("PrintDiff", function(suite) return composed end - suite:case("printdiff_default_simple_addition", function(assert) + suite:case("printdiffDefaultSimpleAddition", function(assert) local a = "line1\nline2" local b = "line1\nline2\nline3" local result = difftext.prettydiff(a, b) @@ -339,7 +339,7 @@ test.suite("PrintDiff", function(suite) assert.eq(lines[3], richterm.brightGreen("+ line3")) end) - suite:case("printdiff_default_simple_deletion", function(assert) + suite:case("printdiffDefaultSimpleDeletion", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline3" local result = difftext.prettydiff(a, b) @@ -351,7 +351,7 @@ test.suite("PrintDiff", function(suite) assert.eq(lines[3], " line3") end) - suite:case("printdiff_default_modification", function(assert) + suite:case("printdiffDefaultModification", function(assert) local a = "line1\nline2\nline3" local b = "line1\nmodified\nline3" local result = difftext.prettydiff(a, b) @@ -364,7 +364,7 @@ test.suite("PrintDiff", function(suite) assert.eq(lines[4], " line3") end) - suite:case("printdiff_default_unchanged_lines", function(assert) + suite:case("printdiffDefaultUnchangedLines", function(assert) local a = "line1\nline2\nline3" local b = "line1\nline2\nline3" local result = difftext.prettydiff(a, b) @@ -376,7 +376,7 @@ test.suite("PrintDiff", function(suite) assert.eq(lines[3], " line3") end) - suite:case("printdiff_default_multiple_changes", function(assert) + suite:case("printdiffDefaultMultipleChanges", function(assert) local a = "A\nB\nC\nD" local b = "A\nX\nC\nY\nD" local result = difftext.prettydiff(a, b) @@ -391,7 +391,7 @@ test.suite("PrintDiff", function(suite) assert.eq(lines[6], " D") end) - suite:case("printdiff_default_with_line_numbers", function(assert) + suite:case("printdiffDefaultWithLineNumbers", function(assert) local a = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" local b = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" local result = difftext.prettydiff(a, b, { @@ -409,7 +409,7 @@ test.suite("PrintDiff", function(suite) end end) - suite:case("printdiff_detailed_single_line_change", function(assert) + suite:case("printdiffDetailedSingleLineChange", function(assert) local a = "hello world" local b = "hello wirld" local result = difftext.prettydiff(a, b, { detailed = true }) @@ -425,7 +425,7 @@ test.suite("PrintDiff", function(suite) assert.eq(lines[2], composeDetailedAddition("hello wirld", { [8] = true })) end) - suite:case("printdiff_detailed_multiple_line_changes", function(assert) + suite:case("printdiffDetailedMultipleLineChanges", function(assert) local a = "line1\nold line\nline3" local b = "line1\nnew line\nline3" local result = difftext.prettydiff(a, b, { detailed = true }) @@ -445,7 +445,7 @@ test.suite("PrintDiff", function(suite) assert.eq(lines[4], " line3") end) - suite:case("printdiff_detailed_unchanged_lines", function(assert) + suite:case("printdiffDetailedUnchangedLines", function(assert) local a = "line1\nline2" local b = "line1\nline2" local result = difftext.prettydiff(a, b, { detailed = true }) @@ -456,7 +456,7 @@ test.suite("PrintDiff", function(suite) assert.eq(lines[2], " line2") end) - suite:case("printdiff_detailed_with_line_numbers", function(assert) + suite:case("printdiffDetailedWithLineNumbers", function(assert) local a = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10" local b = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nlineten" local result = difftext.prettydiff(a, b, { @@ -474,7 +474,7 @@ test.suite("PrintDiff", function(suite) assert.eq(lines[11], composeDetailedAddition("lineten", { [5] = true, [6] = true, [7] = true }, " 10| ")) end) - suite:case("printdiff_detailed_sanity_check_w/_3_digits_of_line_numbers", function(assert) + suite:case("printdiffDetailedSanityCheckW/3DigitsOfLineNumbers", function(assert) local a = "unmodified\n" .. string.rep("line\n", 99) .. "unmodified" local b = "modified\n" .. string.rep("line\n", 99) .. "modified" local result = difftext.prettydiff(a, b, { diff --git a/tests/cli/check.test.luau b/tests/cli/check.test.luau index 340602893..402cef6cd 100644 --- a/tests/cli/check.test.luau +++ b/tests/cli/check.test.luau @@ -5,7 +5,7 @@ local test = require("@std/test") local lutePath = path.format(process.execpath()) test.suite("LuteTypeCheck", function(suite) - suite:case("uses_new_solver", function(assert) + suite:case("usesNewSolver", function(assert) local testFilePath = path.format(path.join("tests", "src", "staticrequires", "newsolver.luau")) local result = process.run({ lutePath, "check", testFilePath }) diff --git a/tests/cli/compile.test.luau b/tests/cli/compile.test.luau index 73b6d9dec..e5574bb6e 100644 --- a/tests/cli/compile.test.luau +++ b/tests/cli/compile.test.luau @@ -24,7 +24,7 @@ local lutePath = process.execpath() local tmpDir = system.tmpdir() test.suite("LuteCliCompile", function(suite) - suite:case("run_compile", function(check) + suite:case("runCompile", function(check) -- Setup -- Create files local compilerPath = path.join(tmpDir, "compiler.luau") diff --git a/tests/cli/discovery/example.spec.luau b/tests/cli/discovery/example.spec.luau index e1280ef91..8d8e26157 100644 --- a/tests/cli/discovery/example.spec.luau +++ b/tests/cli/discovery/example.spec.luau @@ -1,7 +1,7 @@ local test = require("@std/test") test.suite("ExampleSuite", function(suite) - suite:case("should pass", function(assert) + suite:case("shouldPass", function(assert) assert.eq(2 + 2, 4) end) end) diff --git a/tests/cli/discovery/smoke.test.luau b/tests/cli/discovery/smoke.test.luau index 92e551962..198eaf540 100644 --- a/tests/cli/discovery/smoke.test.luau +++ b/tests/cli/discovery/smoke.test.luau @@ -1,13 +1,13 @@ local test = require("@std/test") test.suite("SmokeSuite", function(suite) - suite:case("make_fail", function(assert) + suite:case("makeFail", function(assert) -- assert.eq(1, 2) assert.eq(1, 1) -- comment this and uncomment above to make the test fail end) - suite:case("make_pass", function(assert) + suite:case("makePass", function(assert) assert.eq(1, 1) end) end) -test.case("Passing", function(_) end) +test.case("passing", function(_) end) diff --git a/tests/cli/doc.test.luau b/tests/cli/doc.test.luau index dc4e349b9..5a458898a 100644 --- a/tests/cli/doc.test.luau +++ b/tests/cli/doc.test.luau @@ -12,7 +12,7 @@ local USAGE = "Usage: lute doc [OPTIONS]" -- Will add specific module test case (see examples/docs) once --module option is added test.suite("LuteDoc", function(suite) - suite:case("lute_doc_help_message", function(assert) + suite:case("luteDocHelpMessage", function(assert) local result = process.run({ lutePath, "doc", "-h" }) assert.eq(result.exitcode, 0) @@ -29,21 +29,21 @@ test.suite("LuteDoc", function(suite) assert.strcontains(result.stdout, USAGE) end) - suite:case("lute_doc_no_modules_provided", function(assert) + suite:case("luteDocNoModulesProvided", function(assert) local result = process.system(`{lutePath} doc`) assert.eq(result.exitcode, 1) assert.strcontains(result.stderr, "Error: No module paths specified.") end) - suite:case("lute_doc_default_directory", function(assert) + suite:case("luteDocDefaultDirectory", function(assert) local result = process.system(`{lutePath} doc definitions lute/std/libs`) assert.eq(result.exitcode, 0) assert.strcontains(result.stdout, "Documentation generation complete!") end) - suite:case("lute_doc_specified_directory", function(assert) + suite:case("luteDocSpecifiedDirectory", function(assert) if fs.exists(tmpOutputDir) then fs.removedirectory(tmpOutputDir, { recursive = true }) end diff --git a/tests/cli/lib/files.test.luau b/tests/cli/lib/files.test.luau index 02064a967..f5510a8b7 100644 --- a/tests/cli/lib/files.test.luau +++ b/tests/cli/lib/files.test.luau @@ -14,7 +14,7 @@ test.suite("CliLibFiles", function(suite) end end) - suite:case("parse_simple_file_pattern", function(assert) + suite:case("parseSimpleFilePattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -47,7 +47,7 @@ test.suite("CliLibFiles", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse_directory_pattern", function(assert) + suite:case("parseDirectoryPattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -82,7 +82,7 @@ test.suite("CliLibFiles", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse_negation_pattern", function(assert) + suite:case("parseNegationPattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -109,7 +109,7 @@ test.suite("CliLibFiles", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse_leading_double_asterisk_pattern", function(assert) + suite:case("parseLeadingDoubleAsteriskPattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -139,7 +139,7 @@ test.suite("CliLibFiles", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse_trailing_double_asterisk_pattern", function(assert) + suite:case("parseTrailingDoubleAsteriskPattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -173,7 +173,7 @@ test.suite("CliLibFiles", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse_middle_double_asterisk_pattern", function(assert) + suite:case("parseMiddleDoubleAsteriskPattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -210,7 +210,7 @@ test.suite("CliLibFiles", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse_rooted_pattern", function(assert) + suite:case("parseRootedPattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") @@ -241,7 +241,7 @@ test.suite("CliLibFiles", function(suite) fs.removedirectory(testPath, { recursive = true }) end) - suite:case("parse_question_mark_pattern", function(assert) + suite:case("parseQuestionMarkPattern", function(assert) -- Setup fs.createdirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 010892820..f5c556203 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -124,7 +124,7 @@ test.suite("lute lint", function(suite) end end) - suite:case("lute lint help message", function(assert) + suite:case("luteLintHelpMessage", function(assert) local result = process.run({ lutePath, "lint", "-h" }) assert.eq(result.exitcode, 0) @@ -141,20 +141,20 @@ test.suite("lute lint", function(suite) assert.strcontains(result.stdout, USAGE) end) - suite:case("lute lint no input file", function(assert) + suite:case("luteLintNoInputFile", function(assert) local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) assert.eq(result.exitcode, 1) assert.strcontains(result.stdout, "Error: No input files or string input specified.") end) - suite:case("lute lint error code 0 with no violations", function(assert) + suite:case("luteLintErrorCode0WithNoViolations", function(assert) local output = lintTestHelper(assert, { content = "", expectedExitCode = 0 }) assert.eq(output, "") end) - suite:case("lute lint verbose", function(assert) + suite:case("luteLintVerbose", function(assert) local output = lintTestHelper(assert, { content = "", expectedExitCode = 0, @@ -169,7 +169,7 @@ test.suite("lute lint", function(suite) assert.neq(output:find("Parsing input file '.+violator%.luau'"), nil) end) - suite:case("divide_by_zero", function(assert) + suite:case("divideByZero", function(assert) lintTestHelper(assert, { content = [[ local x = 1 / 0 @@ -193,7 +193,7 @@ violator.luau:1:11-16 ── }) end) - suite:case("divide_by_zero auto-fix", function(assert) + suite:case("divideByZeroAutoFix", function(assert) lintTestHelper(assert, { content = [[ local _x = 1 / 0 @@ -213,7 +213,7 @@ local _negnan = 0 / 0 }) end) - suite:case("almost_swapped", function(assert) + suite:case("almostSwapped", function(assert) lintTestHelper(assert, { content = [[ a = b @@ -294,7 +294,7 @@ Suggested fix: t["a"], t["b"] = t["b"], t["a"] }) end) - suite:case("almost swapped auto-fix", function(assert) + suite:case("almostSwappedAutoFix", function(assert) lintTestHelper(assert, { content = [[ a = b @@ -332,7 +332,7 @@ end }) end) - suite:case("lute lint custom rules dir", function(assert) + suite:case("luteLintCustomRulesDir", function(assert) -- Setup fs.createdirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau @@ -389,7 +389,7 @@ violator.luau:3:1-4:6 ── fs.removedirectory(rulesDir, { recursive = true }) end) - suite:case("lute lint multiple rules but one errors", function(assert) + suite:case("luteLintMultipleRulesButOneErrors", function(assert) -- Setup fs.createdirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau @@ -441,7 +441,7 @@ b = a fs.removedirectory(rulesDir, { recursive = true }) end) - suite:case("lute lint multiple input files", function(assert) + suite:case("luteLintMultipleInputFiles", function(assert) -- Setup -- Create multiple files that violate the rule local violatorPath1 = path.format(path.join(tmpDir, "violator1.luau")) @@ -525,7 +525,7 @@ violator2.lua:5:1-6:6 ── fs.removedirectory(modulePath, { recursive = true }) end) - suite:case("lute lint multiple files with errors recovers", function(assert) + suite:case("luteLintMultipleFilesWithErrorsRecovers", function(assert) -- Setup -- Create multiple files that violate the rule local lintee = path.format(path.join(tmpDir, "lintee.luau")) @@ -563,7 +563,7 @@ lintee.luau': @std/syntax/parser.luau:12: parsing failed: fs.remove(violatorPath) end) - suite:case("lute lint default rules", function(assert) + suite:case("luteLintDefaultRules", function(assert) lintTestHelper(assert, { content = [[ local x = 1 / 0 @@ -600,7 +600,7 @@ violator.luau:3:1-4:6 ── }) end) - suite:case("lute lint warning suppression", function(assert) + suite:case("luteLintWarningSuppression", function(assert) lintTestHelper(assert, { content = [[ -- lute-lint-ignore(divide_by_zero) @@ -633,7 +633,7 @@ violator.luau:9:1-10:6 ── }) end) - suite:case("suppression is limited to following statement", function(assert) + suite:case("suppressionIsLimitedToFollowingStatement", function(assert) lintTestHelper(assert, { content = [[ -- lute-lint-ignore(divide_by_zero) @@ -674,7 +674,7 @@ violator.luau:8:11-16 ── }) end) - suite:case("suppression is limited to following statement2", function(assert) + suite:case("suppressionIsLimitedToFollowingStatement2", function(assert) lintTestHelper(assert, { content = [[ local x = 1 / 0 @@ -715,7 +715,7 @@ violator.luau:8:11-16 ── }) end) - suite:case("suppression is limited to following statement3", function(assert) + suite:case("suppressionIsLimitedToFollowingStatement3", function(assert) lintTestHelper(assert, { content = [[ local function foo() @@ -744,7 +744,7 @@ violator.luau:5:9-14 ── }) end) - suite:case("suppression is limited to following statement4", function(assert) + suite:case("suppressionIsLimitedToFollowingStatement4", function(assert) lintTestHelper(assert, { content = [[ if true then @@ -773,7 +773,7 @@ violator.luau:5:9-14 ── }) end) - suite:case("suppression is limited to following statement5", function(assert) + suite:case("suppressionIsLimitedToFollowingStatement5", function(assert) lintTestHelper(assert, { content = [[ while true do @@ -802,7 +802,7 @@ violator.luau:5:9-14 ── }) end) - suite:case("suppression is limited to following statement6", function(assert) + suite:case("suppressionIsLimitedToFollowingStatement6", function(assert) lintTestHelper(assert, { content = [[ local t = {} @@ -832,7 +832,7 @@ violator.luau:6:9-14 ── }) end) - suite:case("suppression for multi node violation", function(assert) + suite:case("suppressionForMultiNodeViolation", function(assert) lintTestHelper(assert, { content = [[ -- lute-lint-ignore(almost_swapped) @@ -846,7 +846,7 @@ b = a }) end) - suite:case("suppression for multi node violation", function(assert) + suite:case("suppressionForMultiNodeViolationPartial", function(assert) lintTestHelper(assert, { content = [[ -- lute-lint-ignore(almost_swapped) @@ -872,7 +872,7 @@ violator.luau:3:1-4:6 ── }) end) - suite:case("nested suppressions", function(assert) + suite:case("nestedSuppressions", function(assert) lintTestHelper(assert, { content = [[ -- lute-lint-ignore(divide_by_zero) @@ -893,7 +893,7 @@ end }) end) - suite:case("json output", function(assert) + suite:case("jsonOutput", function(assert) lintTestHelper(assert, { content = [[ local x = 1 / 0 @@ -965,7 +965,7 @@ local x = 1 / 0 }) end) - suite:case("lute lint respects gitignore", function(assert) + suite:case("luteLintRespectsGitignore", function(assert) -- Setup -- Create a module directory with a .gitignore that ignores one file local modulePath = path.join(tmpDir, "module") @@ -1016,7 +1016,7 @@ violator2.lua:1:11-17 ── fs.removedirectory(modulePath, { recursive = true }) end) - suite:case("lute lint report directive", function(assert) + suite:case("luteLintReportDirective", function(assert) lintTestHelper(assert, { content = [[ local x = 1 / 0 @@ -1054,7 +1054,7 @@ violator.luau:6:12-18 ── }) end) - suite:case("lute lint global ignore directive", function(assert) + suite:case("luteLintGlobalIgnoreDirective", function(assert) lintTestHelper(assert, { content = [[ -- lute-lint-global-ignore(divide_by_zero) @@ -1083,7 +1083,7 @@ violator.luau:5:12-17 ── }) end) - suite:case("lute lint string input", function(assert) + suite:case("luteLintStringInput", function(assert) local inputString = [[ a = b b = a @@ -1108,7 +1108,7 @@ b = a assert.strcontains(result.stdout, expected) end) - suite:case("lute lint string input verbose", function(assert) + suite:case("luteLintStringInputVerbose", function(assert) local inputString = "local x = 1 / 0" -- Do @@ -1125,7 +1125,7 @@ b = a assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) end) - suite:case("lute lint string input with no violations", function(assert) + suite:case("luteLintStringInputWithNoViolations", function(assert) local inputString = "x = 1 + 2" -- Do @@ -1137,7 +1137,7 @@ b = a assert.strcontains(result.stdout, "No lint violations found.") end) - suite:case("lute lint string input json output", function(assert) + suite:case("luteLintStringInputJsonOutput", function(assert) local inputString = "local x = 1 / 0" -- Do @@ -1203,7 +1203,7 @@ b = a assert.strcontains(result.stdout, expected) end) - suite:case("lute lint ignore (table item)", function(assert) + suite:case("luteLintIgnoreTableItem", function(assert) lintTestHelper(assert, { content = [[ local y = { @@ -1221,7 +1221,7 @@ local z = 5 / 0 }) end) - suite:case("parenthesized_conditions", function(assert) + suite:case("parenthesizedConditions", function(assert) lintTestHelper(assert, { content = [[ if (3 > 0) then @@ -1318,7 +1318,7 @@ violator.luau:15:7-15 ── }) end) - suite:case("parenthesized_conditions auto-fix", function(assert) + suite:case("parenthesizedConditionsAutoFix", function(assert) lintTestHelper(assert, { content = [[ if (3 > 0) then @@ -1360,7 +1360,7 @@ until 5 == 5 }) end) - suite:case("report violations remaining after auto-fix", function(assert) + suite:case("reportViolationsRemainingAfterAutoFix", function(assert) lintTestHelper(assert, { content = [[ a = b @@ -1384,7 +1384,7 @@ local x = t == { x = 3 } }) end) - suite:case("constant_table_comparison", function(assert) + suite:case("constantTableComparison", function(assert) lintTestHelper(assert, { content = [[ local a = { 1, 2, 3 } == t @@ -1455,7 +1455,7 @@ violator.luau:5:11-24 ── }) end) - suite:case("constant_table_comparison auto-fix", function(assert) + suite:case("constantTableComparisonAutoFix", function(assert) lintTestHelper(assert, { content = [[ local a = {} == t @@ -1477,7 +1477,7 @@ local e = next(t) ~= nil }) end) - suite:case("lute lint json output includes suggestedfix", function(assert) + suite:case("luteLintJsonOutputIncludesSuggestedfix", function(assert) local inputString = [[ a = b b = a @@ -1526,7 +1526,7 @@ local e = next(t) ~= nil assert.strcontains(result.stdout, expected) end) - suite:case("unused_variable_1", function(assert) + suite:case("unusedVariable1", function(assert) lintTestHelper(assert, { content = [[ local x = 3 @@ -1553,7 +1553,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_2", function(assert) + suite:case("unusedVariable2", function(assert) lintTestHelper(assert, { content = [[ local _ = 3 @@ -1570,7 +1570,7 @@ local _ = 3 }) end) - suite:case("unused_variable_3", function(assert) + suite:case("unusedVariable3", function(assert) lintTestHelper(assert, { content = [[ for i = 3, 4 do @@ -1599,7 +1599,7 @@ violator.luau:1:5-6 ── }) end) - suite:case("unused_variable_4", function(assert) + suite:case("unusedVariable4", function(assert) lintTestHelper(assert, { content = [[ local t = {1, 2, 3} @@ -1639,7 +1639,7 @@ violator.luau:2:8-9 ── }) end) - suite:case("unused_variable_5", function(assert) + suite:case("unusedVariable5", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1667,7 +1667,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_6", function(assert) + suite:case("unusedVariable6", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1697,7 +1697,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_7", function(assert) + suite:case("unusedVariable7", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1716,7 +1716,7 @@ print(x) }) end) - suite:case("unused_variable_8", function(assert) + suite:case("unusedVariable8", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1744,7 +1744,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_9", function(assert) + suite:case("unusedVariable9", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1763,7 +1763,7 @@ print(x) }) end) - suite:case("unused_variable_10", function(assert) + suite:case("unusedVariable10", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1791,7 +1791,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_11", function(assert) + suite:case("unusedVariable11", function(assert) lintTestHelper(assert, { content = [[ local function _(x) @@ -1820,7 +1820,7 @@ violator.luau:1:18-19 ── }) end) - suite:case("unused_variable_12", function(assert) + suite:case("unusedVariable12", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1851,7 +1851,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_13", function(assert) + suite:case("unusedVariable13", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1880,7 +1880,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_14", function(assert) + suite:case("unusedVariable14", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1911,7 +1911,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_15", function(assert) + suite:case("unusedVariable15", function(assert) lintTestHelper(assert, { content = [[ local x = 4 @@ -1939,7 +1939,7 @@ violator.luau:2:7-8 ── }) end) - suite:case("unused_variable_16", function(assert) + suite:case("unusedVariable16", function(assert) lintTestHelper(assert, { content = [[ local lib = require("whatever") @@ -1957,7 +1957,7 @@ local _x : lib.num = 3 }) end) - suite:case("unused_variable_17", function(assert) + suite:case("unusedVariable17", function(assert) lintTestHelper(assert, { content = [[ for k, v in t do @@ -1976,7 +1976,7 @@ end }) end) - suite:case("unused_variable_18", function(assert) + suite:case("unusedVariable18", function(assert) lintTestHelper(assert, { content = [[ return { @@ -1998,7 +1998,7 @@ return { }) end) - suite:case("unused_variable_19", function(assert) + suite:case("unusedVariable19", function(assert) lintTestHelper(assert, { content = [[ local T = {} @@ -2029,7 +2029,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_20", function(assert) + suite:case("unusedVariable20", function(assert) lintTestHelper(assert, { content = [[ local a, b = foo() @@ -2067,7 +2067,7 @@ violator.luau:1:10-11 ── }) end) - suite:case("unused_variable_21", function(assert) + suite:case("unusedVariable21", function(assert) lintTestHelper(assert, { content = [[ local x = 3 @@ -2097,7 +2097,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable_22", function(assert) + suite:case("unusedVariable22", function(assert) lintTestHelper(assert, { content = [[ repeat @@ -2117,7 +2117,7 @@ until x == 1 }) end) - suite:case("unused_variable_23", function(assert) + suite:case("unusedVariable23", function(assert) lintTestHelper(assert, { content = [=[ for i = 0, num - 1 do @@ -2136,7 +2136,7 @@ end }) end) - suite:case("unused_variable_24", function(assert) + suite:case("unusedVariable24", function(assert) lintTestHelper(assert, { content = [=[ function foo(t: { num: number }) @@ -2155,7 +2155,7 @@ end }) end) - suite:case("unused_variable with table.insert default write-only API", function(assert) + suite:case("unusedVariableWithTableInsertDefaultWriteOnlyApi", function(assert) lintTestHelper(assert, { content = [[ local t = {} @@ -2183,7 +2183,7 @@ violator.luau:1:7-8 ── }) end) - suite:case("unused_variable with custom write-only API (single level)", function(assert) + suite:case("unusedVariableWithCustomWriteOnlyApiSingleLevel", function(assert) lintTestHelper(assert, { content = [[ local output = {} @@ -2226,7 +2226,7 @@ violator.luau:1:7-13 ── }) end) - suite:case("unused_variable with custom write-only API (nested)", function(assert) + suite:case("unusedVariableWithCustomWriteOnlyApiNested", function(assert) lintTestHelper(assert, { content = [[ local buffer = {} @@ -2269,7 +2269,7 @@ violator.luau:1:7-13 ── }) end) - suite:case("unused_variable with custom write-only API (nested, index expr access)", function(assert) + suite:case("unusedVariableWithCustomWriteOnlyApiNestedIndexExprAccess", function(assert) lintTestHelper(assert, { content = [[ local buffer = {} @@ -2312,7 +2312,7 @@ violator.luau:1:7-13 ── }) end) - suite:case("unused_variable with multiple write-only args", function(assert) + suite:case("unusedVariableWithMultipleWriteOnlyArgs", function(assert) lintTestHelper(assert, { content = [[ local a = {} @@ -2366,7 +2366,7 @@ violator.luau:2:7-8 ── }) end) - suite:case("unused_variable with write-only arg at position 2", function(assert) + suite:case("unusedVariableWithWriteOnlyArgAtPosition2", function(assert) lintTestHelper(assert, { content = [[ local config = "read" @@ -2410,7 +2410,7 @@ violator.luau:2:7-13 ── }) end) - suite:case("unused_variable with deep nesting errors", function(assert) + suite:case("unusedVariableWithDeepNestingErrors", function(assert) lintTestHelper(assert, { content = "local x = 1", expectedExitCode = 0, @@ -2440,7 +2440,7 @@ return { }) end) - suite:case("unused_variable with conflicting API definitions errors", function(assert) + suite:case("unusedVariableWithConflictingApiDefinitionsErrors", function(assert) lintTestHelper(assert, { content = "local x = 1", expectedExitCode = 0, @@ -2471,7 +2471,7 @@ return { }) end) - suite:case("throws error when config has invalid structure", function(assert) + suite:case("throwsErrorWhenConfigHasInvalidStructure", function(assert) -- local invalidGlobalConfig = [[ -- return { -- lute = { @@ -2520,7 +2520,7 @@ return { -- assert.strcontains(result.stderr, "config.rules must be a table") end) - suite:case("lute lint accounts for configured ignores (config passed as arg)", function(assert) + suite:case("luteLintAccountsForConfiguredIgnoresConfigPassedAsArg", function(assert) local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) @@ -2552,7 +2552,7 @@ return { assert.strcontains(result.stdout, "valid.luau") end) - suite:case("lute lint accounts for configured ignores (config auto-detected)", function(assert) + suite:case("luteLintAccountsForConfiguredIgnoresConfigAutoDetected", function(assert) local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) @@ -2585,7 +2585,7 @@ return { assert.strnotcontains(result.stdout, "divide_by_zero") end) - suite:case("lute lint configured ignores supplement (NOT override) existing gitignores", function(assert) + suite:case("luteLintConfiguredIgnoresSupplementNotOverrideExistingGitignores", function(assert) local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) @@ -2621,7 +2621,7 @@ return { assert.strnotcontains(result.stdout, "divide_by_zero") end) - suite:case("duplicate_keys list duplicates", function(assert) + suite:case("duplicateKeysListDuplicates", function(assert) lintTestHelper(assert, { content = [[ local t = { 1, 2, [2] = 3 } @@ -2645,7 +2645,7 @@ violator.luau:1:19-26 ── }) end) - suite:case("duplicate_keys record duplicates", function(assert) + suite:case("duplicateKeysRecordDuplicates", function(assert) lintTestHelper(assert, { content = [[ local t = { a = 1, a = 2 } @@ -2669,7 +2669,7 @@ violator.luau:1:20-25 ── }) end) - suite:case("duplicate_keys general string duplicates", function(assert) + suite:case("duplicateKeysGeneralStringDuplicates", function(assert) lintTestHelper(assert, { content = [[ local t = { ["a"] = 1, ["a"] = 2 } @@ -2693,7 +2693,7 @@ violator.luau:1:24-33 ── }) end) - suite:case("duplicate_keys general number duplicates", function(assert) + suite:case("duplicateKeysGeneralNumberDuplicates", function(assert) lintTestHelper(assert, { content = [[ local t = { [1] = 1, [1] = 2 } @@ -2717,7 +2717,7 @@ violator.luau:1:22-29 ── }) end) - suite:case("duplicate_keys general number clashes with array part", function(assert) + suite:case("duplicateKeysGeneralNumberClashesWithArrayPart", function(assert) lintTestHelper(assert, { content = [[ local t = { 1, 2, [2] = 3 } @@ -2741,7 +2741,7 @@ violator.luau:1:19-26 ── }) end) - suite:case("duplicate_keys general string and record entry duplicates", function(assert) + suite:case("duplicateKeysGeneralStringAndRecordEntryDuplicates", function(assert) lintTestHelper(assert, { content = [[ local t = { ["a"] = 1, a = 2 } @@ -2765,7 +2765,7 @@ violator.luau:1:24-29 ── }) end) - suite:case("duplicate_keys doesn't warn on general 0", function(assert) + suite:case("duplicateKeysDoesntWarnOnGeneral0", function(assert) lintTestHelper(assert, { content = [[ local t = { [0] = 1 } @@ -2779,7 +2779,7 @@ local t = { [0] = 1 } }) end) - suite:case("lute lint supports configured directory ignores", function(assert) + suite:case("luteLintSupportsConfiguredDirectoryIgnores", function(assert) local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) @@ -2825,7 +2825,7 @@ local t = { [0] = 1 } assert.strcontains(result.stdout, "almost_swapped") end) - suite:case("lute lint handles ignores with relative config path", function(assert) + suite:case("luteLintHandlesIgnoresWithRelativeConfigPath", function(assert) local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) @@ -2860,7 +2860,7 @@ local t = { [0] = 1 } assert.strcontains(result.stdout, "valid.luau") end) - suite:case("lute lint handles ignores with absolute config path", function(assert) + suite:case("luteLintHandlesIgnoresWithAbsoluteConfigPath", function(assert) local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) @@ -2893,7 +2893,7 @@ local t = { [0] = 1 } assert.strcontains(result.stdout, "valid.luau") end) - suite:case("lute lint passes context through to rules", function(assert) + suite:case("luteLintPassesContextThroughToRules", function(assert) -- custom rule that returns violating with globals and options in the message local customRuleContents = [[ return { @@ -2940,7 +2940,7 @@ local t = { [0] = 1 } }) end) - suite:case("lute lint accounts for rule-specific ignores", function(assert) + suite:case("luteLintAccountsForRuleSpecificIgnores", function(assert) local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) @@ -2989,7 +2989,7 @@ return { assert.strcontains(result.stdout, "[almost_swapped]") end) - suite:case("lute lint accounts for rule-specific directory ignores", function(assert) + suite:case("luteLintAccountsForRuleSpecificDirectoryIgnores", function(assert) local testSubDir = path.join(tmpDir, "module") fs.createdirectory(testSubDir) @@ -3035,7 +3035,7 @@ return { assert.strcontains(result.stdout, "nonIgnored.luau:1:11-16") -- the non-ignored file's violation end) - suite:case("default rules configured with off do NOT run", function(assert) + suite:case("defaultRulesConfiguredWithOffDoNotRun", function(assert) local violationContents = [[ a = b b = a @@ -3063,7 +3063,7 @@ return { }) end) - suite:case("custom rules configured with off do NOT run", function(assert) + suite:case("customRulesConfiguredWithOffDoNotRun", function(assert) local customRuleContents = [[ return { name = "customRule", @@ -3114,7 +3114,7 @@ return { }) end) - suite:case("configured rule severity overrides default", function(assert) + suite:case("configuredRuleSeverityOverridesDefault", function(assert) local configContents = [[ return { lute = { @@ -3140,7 +3140,7 @@ return { }) end) - suite:case("rules given by config.rulepaths are loaded (in addition to CLI-passed rules) and ran", function(assert) + suite:case("rulesGivenByConfigRulepathsAreLoadedInAdditionToCliPassedRulesAndRan", function(assert) local ruleTemplate = [[ return { name = %q, @@ -3287,7 +3287,7 @@ return { end ) - suite:case("empty_if_block basic", function(assert) + suite:case("emptyIfBlockBasic", function(assert) lintTestHelper(assert, { content = [[ if true then @@ -3313,7 +3313,7 @@ violator.luau:1:1-2:4 ── }) end) - suite:case("empty_if_block with non empty block", function(assert) + suite:case("emptyIfBlockWithNonEmptyBlock", function(assert) lintTestHelper(assert, { content = [[ if true then @@ -3329,7 +3329,7 @@ end }) end) - suite:case("empty_if_block with empty else", function(assert) + suite:case("emptyIfBlockWithEmptyElse", function(assert) lintTestHelper(assert, { content = [[ if true then @@ -3359,7 +3359,7 @@ violator.luau:1:1-4:4 ── }) end) - suite:case("empty_if_block with empty elseifs", function(assert) + suite:case("emptyIfBlockWithEmptyElseifs", function(assert) lintTestHelper(assert, { content = [[ if true then @@ -3391,7 +3391,7 @@ violator.luau:1:1-5:4 ── }) end) - suite:case("empty_if_block with comments (comments_count = false)", function(assert) + suite:case("emptyIfBlockWithCommentsCommentsCountFalse", function(assert) lintTestHelper(assert, { content = [[ if true then @@ -3441,7 +3441,7 @@ violator.luau:5:1-10:4 ── }) end) - suite:case("empty_if_block with comments (comments_count = true)", function(assert) + suite:case("emptyIfBlockWithCommentsCommentsCountTrue", function(assert) local configContents = [[ return { lute = { lint = { ruleconfigs = { diff --git a/tests/cli/run.test.luau b/tests/cli/run.test.luau index c6e393d6d..1c935b76d 100644 --- a/tests/cli/run.test.luau +++ b/tests/cli/run.test.luau @@ -17,7 +17,7 @@ test.suite("LuteRun", function(suite) end end) - suite:case("same_named_file_and_directory", function(assert) + suite:case("sameNamedFileAndDirectory", function(assert) -- Setup fs.createdirectory(rundir) fs.createdirectory(pathlib.join(rundir, "hello")) diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index fc274c762..7a0d1511d 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -5,16 +5,16 @@ local test = require("@std/test") local lutePath = path.format(process.execpath()) local expected = [[Anonymous: - Passing + passing ExampleSuite: - should pass + shouldPass SmokeSuite: - make_fail - make_pass + makeFail + makePass ]] test.suite("LuteTestCommand", function(suite) - suite:case("lute_test_help_message", function(assert) + suite:case("luteTestHelpMessage", function(assert) -- Run lute test with -h flag local result = process.run({ lutePath, "test", "-h" }) @@ -23,7 +23,7 @@ test.suite("LuteTestCommand", function(suite) assert.strcontains(result.stdout, "Usage:") end) - suite:case("lute_test_runs_tests_in_discovery_folder", function(assert) + suite:case("luteTestRunsTestsInDiscoveryFolder", function(assert) -- Run lute test on tests/cli/discovery folder local result = process.run({ lutePath, "test", "tests/cli/discovery" }) @@ -31,7 +31,7 @@ test.suite("LuteTestCommand", function(suite) assert.eq(result.exitcode, 0) end) - suite:case("lute_test_discovery_custom_directory", function(assert) + suite:case("luteTestDiscoveryCustomDirectory", function(assert) -- Run lute test with custom directory path local result = process.run({ lutePath, "test", "--list", "tests/cli/discovery" }) @@ -40,7 +40,7 @@ test.suite("LuteTestCommand", function(suite) assert.eq(expected, result.stdout) end) - suite:case("filter_by_suite_name", function(assert) + suite:case("filterBySuiteName", function(assert) -- Run lute test with --suite flag local result = process.run({ lutePath, "test", "-s", "SmokeSuite", "tests/cli/discovery" }) @@ -50,9 +50,9 @@ test.suite("LuteTestCommand", function(suite) assert.neq(result.stdout:find("2 passed", 1, true), nil) end) - suite:case("lute_test_filters_by_case_name", function(assert) + suite:case("luteTestFiltersByCaseName", function(assert) -- Run lute test with --case flag - local result = process.run({ lutePath, "test", "--case", "make_pass", "tests/cli/discovery" }) + local result = process.run({ lutePath, "test", "--case", "makePass", "tests/cli/discovery" }) -- Check that it runs only tests named make_pass assert.eq(result.exitcode, 0) @@ -60,9 +60,9 @@ test.suite("LuteTestCommand", function(suite) assert.neq(result.stdout:find("1 passed", 1, true), nil) end) - suite:case("lute_test_filters_by_suite_and_case_name", function(assert) + suite:case("luteTestFiltersBySuiteAndCaseName", function(assert) -- Run lute test with both --suite and --case flags - local result = process.run({ lutePath, "test", "-s", "SmokeSuite", "-c", "make_fail", "tests/cli/discovery" }) + local result = process.run({ lutePath, "test", "-s", "SmokeSuite", "-c", "makeFail", "tests/cli/discovery" }) -- Check that it runs only make_fail in SmokeSuite assert.eq(result.exitcode, 0) diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index 1c55beb38..f6e3e9906 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -30,7 +30,7 @@ test.suite("LuteTransform", function(suite) end end) - suite:case("transform_query_style", function(assert) + suite:case("transformQueryStyle", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau local transformerExample = path.format(path.join("examples", "query_transformer.luau")) @@ -54,7 +54,7 @@ test.suite("LuteTransform", function(suite) fs.remove(transformerDest) end) - suite:case("transform_output_directory_happy_path", function(assert) + suite:case("transformOutputDirectoryHappyPath", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau local transformerExample = path.format(path.join("examples", "query_transformer.luau")) @@ -89,7 +89,7 @@ test.suite("LuteTransform", function(suite) fs.remove(outputPath) end) - suite:case("transform_output_creates_file_if_it_doesnt_exist", function(assert) + suite:case("transformOutputCreatesFileIfItDoesntExist", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau local transformerExample = path.format(path.join("examples", "query_transformer.luau")) diff --git a/tests/lute/crypto.test.luau b/tests/lute/crypto.test.luau index 0f0cca7c3..3b4f11f62 100644 --- a/tests/lute/crypto.test.luau +++ b/tests/lute/crypto.test.luau @@ -3,7 +3,7 @@ local test = require("@std/test") local crypto = require("@lute/crypto") test.suite("CryptoRuntimeTests", function(suite) - suite:case("secretbox_is_a_roundtrip_strings", function(assert) + suite:case("secretboxIsARoundtripStrings", function(assert) local message = "Remember, remember the Fifth of November." local box = crypto.secretbox.seal(message) @@ -13,7 +13,7 @@ test.suite("CryptoRuntimeTests", function(suite) assert.eq(output, message) end) - suite:case("secretbox_is_a_roundtrip_buffers", function(assert) + suite:case("secretboxIsARoundtripBuffers", function(assert) local message = "Gunpowder, treason, and plot." local buf = buffer.create(#message) buffer.writestring(buf, 0, message) @@ -24,7 +24,7 @@ test.suite("CryptoRuntimeTests", function(suite) assert.buffereq(opened, buf) end) - suite:case("secretbox_separate_keygen", function(assert) + suite:case("secretboxSeparateKeygen", function(assert) local key = crypto.secretbox.keygen() local message = "Remember, remember the Fifth of November." @@ -36,7 +36,7 @@ test.suite("CryptoRuntimeTests", function(suite) assert.eq(output, message) end) - suite:case("secretbox_errors_on_tampering", function(assert) + suite:case("secretboxErrorsOnTampering", function(assert) local message = "Gunpowder, treason, and plot." local buf = buffer.create(#message) buffer.writestring(buf, 0, message) @@ -49,7 +49,7 @@ test.suite("CryptoRuntimeTests", function(suite) end) end) - suite:case("secretbox_no_open_string", function(assert) + suite:case("secretboxNoOpenString", function(assert) local message = "I see no reason why gunpowder treason should ever be forgot." local buf = buffer.create(#message) @@ -69,7 +69,7 @@ test.suite("CryptoRuntimeTests", function(suite) end) end) - suite:case("secretbox_missized_key", function(assert) + suite:case("secretboxMissizedKey", function(assert) local message = "I see no reason why gunpowder treason should ever be forgot." local buf = buffer.create(#message) @@ -87,7 +87,7 @@ test.suite("CryptoRuntimeTests", function(suite) end) end) - suite:case("secretbox_on_ill_typed_inputs", function(assert) + suite:case("secretboxOnIllTypedInputs", function(assert) local message = "I see no reason why gunpowder treason should ever be forgot." assert.errors(function() diff --git a/tests/lute/task.test.luau b/tests/lute/task.test.luau index 6dc7c1c33..43638ed5a 100644 --- a/tests/lute/task.test.luau +++ b/tests/lute/task.test.luau @@ -2,7 +2,7 @@ local test = require("@std/test") local task = require("@lute/task") test.suite("LuteTaskSuite", function(suite) - suite:case("simple_task_wait", function(assert) + suite:case("simpleTaskWait", function(assert) local startTime = os.clock() task.wait(0.5) local endTime = os.clock() @@ -11,7 +11,7 @@ test.suite("LuteTaskSuite", function(suite) assert.eq(math.ceil(endTime - startTime) >= 0.5, true) end) - suite:case("task_wait_in_loop", function(assert) + suite:case("taskWaitInLoop", function(assert) local startTime = os.clock() local elapsed = 0 while elapsed < 0.5 do diff --git a/tests/lute/vm.test.luau b/tests/lute/vm.test.luau index d74be1ec9..c2ffb5b1d 100644 --- a/tests/lute/vm.test.luau +++ b/tests/lute/vm.test.luau @@ -5,7 +5,7 @@ local process = require("@std/process") local system = require("@std/system") test.suite("LuteVmSuite", function(suite) - suite:case("child_vm_has_access_to_builtins", function(assert) + suite:case("childVmHasAccessToBuiltins", function(assert) local tmpdir = system.tmpdir() local child_vm = path.join(tmpdir, "child_vm.luau") local parent_vm = path.join(tmpdir, "parent_vm.luau") diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index f921eb967..d24cda960 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -8,7 +8,7 @@ local task = require("@lute/task") local tmpdir = system.tmpdir() test.suite("FsSuite", function(suite) - suite:case("open_read_write_close_and_stat", function(assert) + suite:case("openReadWriteCloseAndStat", function(assert) local file = path.join(tmpdir, "file.txt") local h = fs.open(file, "w+") @@ -26,7 +26,7 @@ test.suite("FsSuite", function(suite) fs.remove(file) end) - suite:case("writestring_and_readfile", function(assert) + suite:case("writeStringAndReadfile", function(assert) local file = path.join(tmpdir, "hello.txt") fs.writestringtofile(file, "hello lute") @@ -36,7 +36,7 @@ test.suite("FsSuite", function(suite) fs.remove(file) end) - suite:case("exists_and_type_file_and_dir", function(assert) + suite:case("existsAndTypeFileAndDir", function(assert) local file = path.join(tmpdir, "roblox.txt") fs.writestringtofile(file, "roblox") @@ -48,7 +48,7 @@ test.suite("FsSuite", function(suite) fs.remove(file) end) - suite:case("mkdir_listdir_rmdir", function(assert) + suite:case("mkdirListdirRmdir", function(assert) local dir = path.join(tmpdir, "subdir") if not fs.exists(dir) then fs.createdirectory(dir, { makeparents = true }) @@ -68,7 +68,7 @@ test.suite("FsSuite", function(suite) fs.removedirectory(dir) end) - suite:case("link_and_symlink_and_copy", function(assert) + suite:case("linkAndSymlinkAndCopy", function(assert) local srcfile = "src.txt" local src = path.join(tmpdir, srcfile) fs.writestringtofile(src, "linkcontent") @@ -91,7 +91,7 @@ test.suite("FsSuite", function(suite) fs.remove(src) end) - suite:case("watch_iterator_on_change", function(assert) + suite:case("watchIteratorOnChange", function(assert) local watchedFileName = "watched.txt" local watched = path.join(tmpdir, watchedFileName) @@ -123,7 +123,7 @@ test.suite("FsSuite", function(suite) end end) - suite:case("createdirectory_makeparents_true", function(assert) + suite:case("createDirectoryMakeparentsTrue", function(assert) local nestedDir = path.join(tmpdir, "nested", "directory") fs.createdirectory(nestedDir, { makeparents = true }) assert.eq(fs.exists(nestedDir), true) @@ -132,7 +132,7 @@ test.suite("FsSuite", function(suite) fs.removedirectory(path.join(tmpdir, "nested")) end) - suite:case("createdirectory_makeparents_false", function(assert) + suite:case("createdirectoryMakeparentsFalse", function(assert) local nestedDir = path.join(tmpdir, "this", "should", "not", "work") local success, err = pcall(function() fs.createdirectory(nestedDir, { makeparents = false }) @@ -144,7 +144,7 @@ test.suite("FsSuite", function(suite) assert.neq(err, nil) end) - suite:case("createdirectory_no_options", function(assert) + suite:case("createDirectoryNoOptions", function(assert) local nestedDir = path.join(tmpdir, "this", "should", "not", "work") local success, err = pcall(function() fs.createdirectory(nestedDir) @@ -156,7 +156,7 @@ test.suite("FsSuite", function(suite) assert.neq(err, nil) end) - suite:case("fs_open_optional_mode", function(assert) + suite:case("fsOpenOptionalMode", function(assert) local file = path.join(tmpdir, "optional_mode.txt") local h1 = fs.open(file, "w+") @@ -171,7 +171,7 @@ test.suite("FsSuite", function(suite) fs.remove(file) end) - suite:case("open_write_creates_new_file", function(assert) + suite:case("openWriteCreatesNewFile", function(assert) local file = path.join(tmpdir, "new_write_only.txt") -- Ensure file does not exist @@ -189,7 +189,7 @@ test.suite("FsSuite", function(suite) fs.remove(file) end) - suite:case("removedirectory_non_recursive", function(assert) + suite:case("removeDirectoryNonRecursive", function(assert) local dir = path.join(tmpdir, "empty_dir") fs.createdirectory(dir, { makeparents = true }) assert.eq(fs.exists(dir), true) @@ -198,7 +198,7 @@ test.suite("FsSuite", function(suite) assert.eq(fs.exists(dir), false) end) - suite:case("removedirectory_recursive", function(assert) + suite:case("removeDirectoryRecursive", function(assert) local dir = path.join(tmpdir, "recursive_test") local subdir1 = path.join(dir, "subdir1") local subdir2 = path.join(dir, "subdir2") @@ -228,7 +228,7 @@ test.suite("FsSuite", function(suite) assert.eq(fs.exists(file3), false) end) - suite:case("removedirectory_recursive_false_with_contents", function(assert) + suite:case("removeDirectoryRecursiveFalseWithContents", function(assert) local dir = path.join(tmpdir, "non_empty_dir") fs.createdirectory(dir, { makeparents = true }) @@ -250,7 +250,7 @@ test.suite("FsSuite", function(suite) fs.removedirectory(dir) end) - suite:case("walk_directory_recursive", function(assert) + suite:case("walkDirectoryRecursive", function(assert) local baseDir = path.join(tmpdir, "walktest") local subDir = path.join(baseDir, "subdir") fs.createdirectory(subDir, { makeparents = true }) diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau index 848b1e930..d900352ca 100644 --- a/tests/std/json.test.luau +++ b/tests/std/json.test.luau @@ -48,7 +48,7 @@ test.suite("JSONParsingTestSuite", function(suite) end) end - suite:case("json_object_and_asobject", function(assert) + suite:case("jsonObjectAndAsObject", function(assert) local obj = json.object({ name = "Alice", age = 30, @@ -58,7 +58,7 @@ test.suite("JSONParsingTestSuite", function(suite) assert.eq(obj.age, 30) end) - suite:case("json_asobject_test", function(assert) + suite:case("jsonAsObjectTest", function(assert) local myjsonstr = [[ { "a": true, diff --git a/tests/std/luau.test.luau b/tests/std/luau.test.luau index 3d8001e65..301ca7c1c 100644 --- a/tests/std/luau.test.luau +++ b/tests/std/luau.test.luau @@ -7,7 +7,7 @@ local test = require("@std/test") local tmpDir = system.tmpdir() test.suite("LuauTypeOfModuleSuite", function(suite) - suite:case("typeofmodule_returns_table", function(assert) + suite:case("typeofmoduleReturnsTable", function(assert) local testPath = path.join(tmpDir, "typeofmodule_test.luau") local testFile = fs.open(testPath, "w+") fs.write( @@ -76,7 +76,7 @@ test.suite("LuauTypeOfModuleSuite", function(suite) assert.eq(foundProperty, true, "Should find example_string_func property") end) - suite:case("typeofmodule_returns_typepack_with_multiple_values", function(assert) + suite:case("typeofmoduleReturnsTypepackWithMultipleValues", function(assert) local testPath = path.join(tmpDir, "typeofmodule_multiple.luau") local testFile = fs.open(testPath, "w+") fs.write( @@ -108,7 +108,7 @@ test.suite("LuauTypeOfModuleSuite", function(suite) assert.eq(typePack.head[3].tag, "boolean") end) - suite:case("typeofmodule_returns_typepack_with_variadic_tail", function(assert) + suite:case("typeofmoduleReturnsTypepackWithVariadicTail", function(assert) local testPath = path.join(tmpDir, "typeofmodule_variadic.luau") local testFile = fs.open(testPath, "w+") fs.write( diff --git a/tests/std/net.test.luau b/tests/std/net.test.luau index c4a70a1b4..4312d4af4 100644 --- a/tests/std/net.test.luau +++ b/tests/std/net.test.luau @@ -2,7 +2,7 @@ local net = require("@lute/net") local test = require("@std/test") test.suite("NetTestSuite", function(suite) - suite:case("serve_locally_and_request", function(assert) + suite:case("serveLocallyAndRequest", function(assert) local server = net.serve({ handler = function(_req: net.ReceivedRequest): net.ServerResponse return { diff --git a/tests/std/path/path.posix.test.luau b/tests/std/path/path.posix.test.luau index f5fcd5f61..8a2f5a5ea 100644 --- a/tests/std/path/path.posix.test.luau +++ b/tests/std/path/path.posix.test.luau @@ -5,7 +5,7 @@ local path = require("@std/path") local posix = require("@std/path/posix") test.suite("PathPosixParseSuite", function(suite) - suite:case("parse_posix_absolute_path", function(assert) + suite:case("parsePosixAbsolutePath", function(assert) local result = path.posix.parse("/home/user/documents/file.txt") assert.eq(result.absolute, true) assert.eq(#result.parts, 4) @@ -15,7 +15,7 @@ test.suite("PathPosixParseSuite", function(suite) assert.eq(result.parts[4], "file.txt") end) - suite:case("parse_posix_relative_path", function(assert) + suite:case("parsePosixRelativePath", function(assert) local result = path.posix.parse("documents/file.txt") assert.eq(result.absolute, false) assert.eq(#result.parts, 2) @@ -23,26 +23,26 @@ test.suite("PathPosixParseSuite", function(suite) assert.eq(result.parts[2], "file.txt") end) - suite:case("parse_empty_string", function(assert) + suite:case("parseEmptyString", function(assert) local result = path.posix.parse("") assert.eq(result.absolute, false) assert.eq(#result.parts, 0) end) - suite:case("parse_root_path", function(assert) + suite:case("parseRootPath", function(assert) local result = path.posix.parse("/") assert.eq(result.absolute, true) assert.eq(#result.parts, 0) end) - suite:case("parse_single_file", function(assert) + suite:case("parseSingleFile", function(assert) local result = path.posix.parse("file.txt") assert.eq(result.absolute, false) assert.eq(#result.parts, 1) assert.eq(result.parts[1], "file.txt") end) - suite:case("parse_pathobj_returns_same", function(assert) + suite:case("parsePathobjReturnsSame", function(assert) local originalPath: posix.path = setmetatable({ parts = { "folder", "file.txt" }, absolute = false, @@ -53,37 +53,37 @@ test.suite("PathPosixParseSuite", function(suite) end) test.suite("PathPosixBasenameSuite", function(suite) - suite:case("basename_posix_absolute_path", function(assert) + suite:case("basenamePosixAbsolutePath", function(assert) local result = path.posix.basename("/home/user/documents/file.txt") assert.eq(result, "file.txt") end) - suite:case("basename_posix_relative_path", function(assert) + suite:case("basenamePosixRelativePath", function(assert) local result = path.posix.basename("documents/file.txt") assert.eq(result, "file.txt") end) - suite:case("basename_single_file", function(assert) + suite:case("basenameSingleFile", function(assert) local result = path.posix.basename("file.txt") assert.eq(result, "file.txt") end) - suite:case("basename_directory_no_extension", function(assert) + suite:case("basenameDirectoryNoExtension", function(assert) local result = path.posix.basename("/home/user/documents") assert.eq(result, "documents") end) - suite:case("basename_empty_path", function(assert) + suite:case("basenameEmptyPath", function(assert) local result = path.posix.basename("") assert.eq(result, nil) end) - suite:case("basename_root_path", function(assert) + suite:case("basenameRootPath", function(assert) local result = path.posix.basename("/") assert.eq(result, nil) end) - suite:case("basename_pathobj", function(assert) + suite:case("basenamePathobj", function(assert) local pathobj: posix.path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, absolute = false, @@ -94,7 +94,7 @@ test.suite("PathPosixBasenameSuite", function(suite) end) test.suite("PathPosixFormatSuite", function(suite) - suite:case("format_posix_absolute_path", function(assert) + suite:case("formatPosixAbsolutePath", function(assert) local pathobj: posix.path = setmetatable({ parts = { "home", "user", "documents", "file.txt" }, absolute = true, @@ -103,7 +103,7 @@ test.suite("PathPosixFormatSuite", function(suite) assert.eq(result, "/home/user/documents/file.txt") end) - suite:case("format_posix_relative_path", function(assert) + suite:case("formatPosixRelativePath", function(assert) local pathobj: posix.path = setmetatable({ parts = { "documents", "file.txt" }, absolute = false, @@ -112,7 +112,7 @@ test.suite("PathPosixFormatSuite", function(suite) assert.eq(result, "documents/file.txt") end) - suite:case("format_empty_relative_path", function(assert) + suite:case("formatEmptyRelativePath", function(assert) local pathobj: posix.path = setmetatable({ parts = {}, absolute = false, @@ -121,7 +121,7 @@ test.suite("PathPosixFormatSuite", function(suite) assert.eq(result, ".") end) - suite:case("format_root_path", function(assert) + suite:case("formatRootPath", function(assert) local pathobj: posix.path = setmetatable({ parts = {}, absolute = true, @@ -130,44 +130,44 @@ test.suite("PathPosixFormatSuite", function(suite) assert.eq(result, "/") end) - suite:case("format_string_returns_same", function(assert) + suite:case("formatStringReturnsSame", function(assert) local result = path.posix.format("/home/user/file.txt") assert.eq(result, "/home/user/file.txt") end) end) test.suite("PathPosixDirnameSuite", function(suite) - suite:case("dirname_posix_absolute_path", function(assert) + suite:case("dirnamePosixAbsolutePath", function(assert) local result = path.posix.dirname("/home/user/documents/file.txt") assert.eq(result, "/home/user/documents") end) - suite:case("dirname_posix_relative_path", function(assert) + suite:case("dirnamePosixRelativePath", function(assert) local result = path.posix.dirname("documents/file.txt") assert.eq(result, "documents") end) - suite:case("dirname_single_file", function(assert) + suite:case("dirnameSingleFile", function(assert) local result = path.posix.dirname("file.txt") assert.eq(result, ".") end) - suite:case("dirname_nested_directory", function(assert) + suite:case("dirnameNestedDirectory", function(assert) local result = path.posix.dirname("/home/user/documents/subfolder") assert.eq(result, "/home/user/documents") end) - suite:case("dirname_empty_path", function(assert) + suite:case("dirnameEmptyPath", function(assert) local result = path.posix.dirname("") assert.eq(result, ".") end) - suite:case("dirname_root_path", function(assert) + suite:case("dirnameRootPath", function(assert) local result = path.posix.dirname("/") assert.eq(result, "/") end) - suite:case("dirname_pathobj", function(assert) + suite:case("dirnamePathobj", function(assert) local pathobj: posix.path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, absolute = false, @@ -178,47 +178,47 @@ test.suite("PathPosixDirnameSuite", function(suite) end) test.suite("PathPosixExtnameSuite", function(suite) - suite:case("extname_simple_extension", function(assert) + suite:case("extnameSimpleExtension", function(assert) local result = path.posix.extname("/home/user/file.txt") assert.eq(result, ".txt") end) - suite:case("extname_multiple_dots", function(assert) + suite:case("extnameMultipleDots", function(assert) local result = path.posix.extname("/home/user/archive.tar.gz") assert.eq(result, ".gz") end) - suite:case("extname_no_extension", function(assert) + suite:case("extnameNoExtension", function(assert) local result = path.posix.extname("/home/user/README") assert.eq(result, "") end) - suite:case("extname_hidden_file_with_extension", function(assert) + suite:case("extnameHiddenFileWithExtension", function(assert) local result = path.posix.extname("/home/user/.bashrc") assert.eq(result, "") end) - suite:case("extname_hidden_file_with_real_extension", function(assert) + suite:case("extnameHiddenFileWithRealExtension", function(assert) local result = path.posix.extname("/home/user/.config.json") assert.eq(result, ".json") end) - suite:case("extname_directory_path", function(assert) + suite:case("extnameDirectoryPath", function(assert) local result = path.posix.extname("/home/user/documents") assert.eq(result, "") end) - suite:case("extname_empty_path", function(assert) + suite:case("extnameEmptyPath", function(assert) local result = path.posix.extname("") assert.eq(result, "") end) - suite:case("extname_root_path", function(assert) + suite:case("extnameRootPath", function(assert) local result = path.posix.extname("/") assert.eq(result, "") end) - suite:case("extname_pathobj", function(assert) + suite:case("extnamePathobj", function(assert) local pathobj: posix.path = setmetatable({ parts = { "folder", "file.pdf" }, absolute = false, @@ -227,44 +227,44 @@ test.suite("PathPosixExtnameSuite", function(suite) assert.eq(result, ".pdf") end) - suite:case("extname_dot_only", function(assert) + suite:case("extnameDotOnly", function(assert) local result = path.posix.extname("/home/user/file.") assert.eq(result, ".") end) - suite:case("extname_filename_is_dot", function(assert) + suite:case("extnameFilenameIsDot", function(assert) local result = path.posix.extname("/home/user/.") assert.eq(result, "") end) - suite:case("extname_filename_is_dotdot", function(assert) + suite:case("extnameFilenameIsDotdot", function(assert) local result = path.posix.extname("/home/user/..") assert.eq(result, "") end) end) test.suite("PathPosixIsAbsoluteSuite", function(suite) - suite:case("absolute_posix_absolute_path", function(assert) + suite:case("absolutePosixAbsolutePath", function(assert) local result = path.posix.isabsolute("/home/user/documents/file.txt") assert.eq(result, true) end) - suite:case("absolute_posix_relative_path", function(assert) + suite:case("absolutePosixRelativePath", function(assert) local result = path.posix.isabsolute("documents/file.txt") assert.eq(result, false) end) - suite:case("absolute_root_path", function(assert) + suite:case("absoluteRootPath", function(assert) local result = path.posix.isabsolute("/") assert.eq(result, true) end) - suite:case("absolute_empty_path", function(assert) + suite:case("absoluteEmptyPath", function(assert) local result = path.posix.isabsolute("") assert.eq(result, false) end) - suite:case("absolute_pathobj_absolute", function(assert) + suite:case("absolutePathobjAbsolute", function(assert) local pathobj: posix.path = setmetatable({ parts = { "home", "user", "file.txt" }, absolute = true, @@ -273,7 +273,7 @@ test.suite("PathPosixIsAbsoluteSuite", function(suite) assert.eq(result, true) end) - suite:case("absolute_pathobj_relative", function(assert) + suite:case("absolutePathobjRelative", function(assert) local pathobj: posix.path = setmetatable({ parts = { "folder", "file.txt" }, absolute = false, @@ -284,7 +284,7 @@ test.suite("PathPosixIsAbsoluteSuite", function(suite) end) test.suite("PathPosixNormalizeSuite", function(suite) - suite:case("normalize_removes_current_directory", function(assert) + suite:case("normalizeRemovesCurrentDirectory", function(assert) local result = path.posix.normalize("/home/./user/documents") assert.eq(result.absolute, true) assert.eq(#result.parts, 3) @@ -293,7 +293,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) assert.eq(result.parts[3], "documents") end) - suite:case("normalize_resolves_parent_directory", function(assert) + suite:case("normalizeResolvesParentDirectory", function(assert) local result = path.posix.normalize("/home/user/../documents") assert.eq(result.absolute, true) assert.eq(#result.parts, 2) @@ -301,7 +301,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) assert.eq(result.parts[2], "documents") end) - suite:case("normalize_multiple_dots", function(assert) + suite:case("normalizeMultipleDots", function(assert) local result = path.posix.normalize("/home/user/./documents/../files/./") assert.eq(result.absolute, true) assert.eq(#result.parts, 3) @@ -310,7 +310,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) assert.eq(result.parts[3], "files") end) - suite:case("normalize_relative_path", function(assert) + suite:case("normalizeRelativePath", function(assert) local result = path.posix.normalize("documents/./subfolder/../file.txt") assert.eq(result.absolute, false) assert.eq(#result.parts, 2) @@ -318,7 +318,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) assert.eq(result.parts[2], "file.txt") end) - suite:case("normalize_removes_empty_segments", function(assert) + suite:case("normalizeRemovesEmptySegments", function(assert) local result = path.posix.normalize("/home//user///documents") assert.eq(result.absolute, true) assert.eq(#result.parts, 3) @@ -327,25 +327,25 @@ test.suite("PathPosixNormalizeSuite", function(suite) assert.eq(result.parts[3], "documents") end) - suite:case("normalize_parent_beyond_root", function(assert) + suite:case("normalizeParentBeyondRoot", function(assert) local result = path.posix.normalize("/home/../..") assert.eq(result.absolute, true) assert.eq(#result.parts, 0) end) - suite:case("normalize_empty_path", function(assert) + suite:case("normalizeEmptyPath", function(assert) local result = path.posix.normalize("") assert.eq(result.absolute, false) assert.eq(#result.parts, 0) end) - suite:case("normalize_only_dots", function(assert) + suite:case("normalizeOnlyDots", function(assert) local result = path.posix.normalize("./././.") assert.eq(result.absolute, false) assert.eq(#result.parts, 0) end) - suite:case("normalize_pathobj", function(assert) + suite:case("normalizePathobj", function(assert) local pathobj: posix.path = setmetatable({ parts = { "home", ".", "user", "..", "documents" }, absolute = true, @@ -357,14 +357,14 @@ test.suite("PathPosixNormalizeSuite", function(suite) assert.eq(result.parts[2], "documents") end) - suite:case("normalize_parent_directory_only", function(assert) + suite:case("normalizeParentDirectoryOnly", function(assert) local result = path.posix.normalize("..") assert.eq(result.absolute, false) assert.eq(#result.parts, 1) assert.eq(result.parts[1], "..") end) - suite:case("normalize_multiple_parent_directories", function(assert) + suite:case("normalizeMultipleParentDirectories", function(assert) local result = path.posix.normalize("../../..") assert.eq(result.absolute, false) assert.eq(#result.parts, 3) @@ -373,7 +373,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) assert.eq(result.parts[3], "..") end) - suite:case("normalize_parent_then_folder", function(assert) + suite:case("normalizeParentThenFolder", function(assert) local result = path.posix.normalize("../folder/file.txt") assert.eq(result.absolute, false) assert.eq(#result.parts, 3) @@ -382,7 +382,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) assert.eq(result.parts[3], "file.txt") end) - suite:case("normalize_multiple_parents_then_folder", function(assert) + suite:case("normalizeMultipleParentsThenFolder", function(assert) local result = path.posix.normalize("../../folder/subfolder") assert.eq(result.absolute, false) assert.eq(#result.parts, 4) @@ -392,7 +392,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) assert.eq(result.parts[4], "subfolder") end) - suite:case("normalize_folder_then_multiple_parents", function(assert) + suite:case("normalizeFolderThenMultipleParents", function(assert) local result = path.posix.normalize("folder/subfolder/../../other") assert.eq(result.absolute, false) assert.eq(#result.parts, 1) @@ -401,7 +401,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) end) test.suite("PathPosixJoinSuite", function(suite) - suite:case("join_absolute_and_relative", function(assert) + suite:case("joinAbsoluteAndRelative", function(assert) local result = path.posix.join("/home/user", "documents", "file.txt") assert.eq(result.absolute, true) assert.eq(#result.parts, 4) @@ -411,7 +411,7 @@ test.suite("PathPosixJoinSuite", function(suite) assert.eq(result.parts[4], "file.txt") end) - suite:case("join_relative_paths", function(assert) + suite:case("joinRelativePaths", function(assert) local result = path.posix.join("documents", "subfolder", "file.txt") assert.eq(result.absolute, false) assert.eq(#result.parts, 3) @@ -420,7 +420,7 @@ test.suite("PathPosixJoinSuite", function(suite) assert.eq(result.parts[3], "file.txt") end) - suite:case("join_empty_parts", function(assert) + suite:case("joinEmptyParts", function(assert) local result = path.posix.join("folder", "", "file.txt") assert.eq(result.absolute, false) assert.eq(#result.parts, 2) @@ -428,13 +428,13 @@ test.suite("PathPosixJoinSuite", function(suite) assert.eq(result.parts[2], "file.txt") end) - suite:case("join_no_arguments", function(assert) + suite:case("joinNoArguments", function(assert) local result = path.posix.join() assert.eq(result.absolute, false) assert.eq(#result.parts, 0) end) - suite:case("join_single_argument", function(assert) + suite:case("joinSingleArgument", function(assert) local result = path.posix.join("/home/user") assert.eq(result.absolute, true) assert.eq(#result.parts, 2) @@ -442,7 +442,7 @@ test.suite("PathPosixJoinSuite", function(suite) assert.eq(result.parts[2], "user") end) - suite:case("join_pathobj_and_string", function(assert) + suite:case("joinPathobjAndString", function(assert) local pathobj: posix.path = setmetatable({ parts = { "home", "user" }, absolute = true, @@ -460,7 +460,7 @@ test.suite("PathPosixJoinSuite", function(suite) assert.eq(pathobj.absolute, true) end) - suite:case("join_error_on_absolute_addend", function(assert) + suite:case("joinErrorOnAbsoluteAddend", function(assert) local success, _ = pcall(function() return path.posix.join("/home/user", "/absolute/path") end) @@ -469,7 +469,7 @@ test.suite("PathPosixJoinSuite", function(suite) end) test.suite("PathPosixResolveSuite", function(suite) - suite:case("resolve_absolute_path", function(assert) + suite:case("resolveAbsolutePath", function(assert) local result = path.posix.resolve("/home/user/documents/file.txt") assert.eq(result.absolute, true) assert.eq(#result.parts, 4) @@ -479,7 +479,7 @@ test.suite("PathPosixResolveSuite", function(suite) assert.eq(result.parts[4], "file.txt") end) - suite:case("resolve_relative_path_from_cwd", function(assert) + suite:case("resolveRelativePathFromCwd", function(assert) -- This test assumes we're in some working directory local result = path.posix.resolve("documents/file.txt") -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / @@ -491,7 +491,7 @@ test.suite("PathPosixResolveSuite", function(suite) assert.eq(result.parts[endIndex], "file.txt") end) - suite:case("resolve_multiple_paths", function(assert) + suite:case("resolveMultiplePaths", function(assert) local result = path.posix.resolve("/home", "user", "../admin", "documents") assert.eq(result.absolute, true) assert.eq(#result.parts, 3) @@ -500,7 +500,7 @@ test.suite("PathPosixResolveSuite", function(suite) assert.eq(result.parts[3], "documents") end) - suite:case("resolve_with_absolute_in_middle", function(assert) + suite:case("resolveWithAbsoluteInMiddle", function(assert) local result = path.posix.resolve("relative", "/absolute/path", "more") assert.eq(result.absolute, true) assert.eq(#result.parts, 3) @@ -509,7 +509,7 @@ test.suite("PathPosixResolveSuite", function(suite) assert.eq(result.parts[3], "more") end) - suite:case("resolve_no_arguments", function(assert) + suite:case("resolveNoArguments", function(assert) -- Should return normalized current working directory local result = path.posix.resolve() -- Absolute Windows paths will be parsed as relative posix paths because they don't start with / @@ -517,7 +517,7 @@ test.suite("PathPosixResolveSuite", function(suite) -- Can't test exact parts since cwd varies end) - suite:case("resolve_empty_strings", function(assert) + suite:case("resolveEmptyStrings", function(assert) local result = path.posix.resolve("", "", "/home/user") assert.eq(result.absolute, true) assert.eq(#result.parts, 2) @@ -525,7 +525,7 @@ test.suite("PathPosixResolveSuite", function(suite) assert.eq(result.parts[2], "user") end) - suite:case("resolve_with_dot_segments", function(assert) + suite:case("resolveWithDotSegments", function(assert) local result = path.posix.resolve("/home/user", "../admin/./documents") assert.eq(result.absolute, true) assert.eq(#result.parts, 3) @@ -536,14 +536,14 @@ test.suite("PathPosixResolveSuite", function(suite) end) test.suite("PathPosixToStringSuite", function(suite) - suite:case("toString_posix_absolute_path", function(assert) + suite:case("toStringPosixAbsolutePath", function(assert) local filePath = "/home/user/documents/file.txt" local pathobj: posix.path = posix.parse(filePath) local result = tostring(pathobj) assert.eq(result, filePath) end) - suite:case("toString_posix_relative_path", function(assert) + suite:case("toStringPosixRelativePath", function(assert) local filePath = "documents/file.txt" local pathobj: posix.path = posix.parse(filePath) local result = tostring(pathobj) @@ -552,7 +552,7 @@ test.suite("PathPosixToStringSuite", function(suite) end) test.suite("PathPosixRelativeSuite", function(suite) - suite:case("relative_same_path", function(assert) + suite:case("relativeSamePath", function(assert) local from = posix.parse("/home/user/documents") local to = posix.parse("/home/user/documents") local result = path.posix.relative(from, to) @@ -560,7 +560,7 @@ test.suite("PathPosixRelativeSuite", function(suite) assert.eq(#result.parts, 0) end) - suite:case("relative_subdirectory", function(assert) + suite:case("relativeSubdirectory", function(assert) local from = posix.parse("/home/user") local to = posix.parse("/home/user/documents/file.txt") local result = path.posix.relative(from, to) @@ -570,7 +570,7 @@ test.suite("PathPosixRelativeSuite", function(suite) assert.eq(result.parts[2], "file.txt") end) - suite:case("relative_parent_directory", function(assert) + suite:case("relativeParentDirectory", function(assert) local from = posix.parse("/home/user/documents") local to = posix.parse("/home/user/file.txt") local result = path.posix.relative(from, to) @@ -580,7 +580,7 @@ test.suite("PathPosixRelativeSuite", function(suite) assert.eq(result.parts[2], "file.txt") end) - suite:case("relative_different_absolute_values", function(assert) + suite:case("relativeDifferentAbsoluteValues", function(assert) local from = posix.parse("/home/user/documents") local to = posix.parse("home/user/file.txt") assert.erroreq(function() diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index 81b8b5ba1..60db50377 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -6,7 +6,7 @@ local path = require("@std/path") local win32 = require("@std/path/win32") test.suite("PathWin32ParseSuite", function(suite) - suite:case("parse_windows_absolute_path", function(assert) + suite:case("parseWindowsAbsolutePath", function(assert) local result: win32.path = path.win32.parse("C:\\Users\\username\\Documents\\file.txt") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -17,7 +17,7 @@ test.suite("PathWin32ParseSuite", function(suite) assert.eq(result.parts[4], "file.txt") end) - suite:case("parse_windows_unc_path", function(assert) + suite:case("parseWindowsUncPath", function(assert) local result: win32.path = path.win32.parse("\\\\server\\share\\folder\\file.txt") assert.eq(result.kind, "unc") assert.eq(result.drive, nil) @@ -28,14 +28,14 @@ test.suite("PathWin32ParseSuite", function(suite) assert.eq(result.parts[4], "file.txt") end) - suite:case("parse_empty_string", function(assert) + suite:case("parseEmptyString", function(assert) local result: win32.path = path.win32.parse("") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) assert.eq(#result.parts, 0) end) - suite:case("parse_single_file", function(assert) + suite:case("parseSingleFile", function(assert) local result: win32.path = path.win32.parse("file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) @@ -43,7 +43,7 @@ test.suite("PathWin32ParseSuite", function(suite) assert.eq(result.parts[1], "file.txt") end) - suite:case("parse_pathobj_returns_same", function(assert) + suite:case("parsePathobjReturnsSame", function(assert) local originalPath: win32.path = setmetatable({ parts = { "folder", "file.txt" }, kind = "relative", @@ -53,7 +53,7 @@ test.suite("PathWin32ParseSuite", function(suite) assert.eq(result, originalPath) end) - suite:case("parse_mixed_separators", function(assert) + suite:case("parseMixedSeparators", function(assert) local result: win32.path = path.win32.parse("/home/user\\documents/file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) @@ -64,7 +64,7 @@ test.suite("PathWin32ParseSuite", function(suite) assert.eq(result.parts[4], "file.txt") end) - suite:case("parse_windows_relative_with_drive", function(assert) + suite:case("parseWindowsRelativeWithDrive", function(assert) local result: win32.path = path.win32.parse("C:Documents\\file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, "C") @@ -75,37 +75,37 @@ test.suite("PathWin32ParseSuite", function(suite) end) test.suite("PathWin32BasenameSuite", function(suite) - suite:case("basename_windows_absolute_path", function(assert) + suite:case("basenameWindowsAbsolutePath", function(assert) local result = path.win32.basename("C:\\Users\\username\\Documents\\file.txt") assert.eq(result, "file.txt") end) - suite:case("basename_windows_unc_path", function(assert) + suite:case("basenameWindowsUncPath", function(assert) local result = path.win32.basename("\\\\server\\share\\folder\\file.txt") assert.eq(result, "file.txt") end) - suite:case("basename_single_file", function(assert) + suite:case("basenameSingleFile", function(assert) local result = path.win32.basename("file.txt") assert.eq(result, "file.txt") end) - suite:case("basename_directory_no_extension", function(assert) + suite:case("basenameDirectoryNoExtension", function(assert) local result = path.win32.basename("C:\\Users\\username\\Documents") assert.eq(result, "Documents") end) - suite:case("basename_empty_path", function(assert) + suite:case("basenameEmptyPath", function(assert) local result = path.win32.basename("") assert.eq(result, nil) end) - suite:case("basename_root_path", function(assert) + suite:case("basenameRootPath", function(assert) local result = path.win32.basename("C:\\") assert.eq(result, nil) end) - suite:case("basename_pathobj", function(assert) + suite:case("basenamePathobj", function(assert) local pathobj: win32.path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, kind = "relative", @@ -115,14 +115,14 @@ test.suite("PathWin32BasenameSuite", function(suite) assert.eq(result, "file.txt") end) - suite:case("basename_mixed_separators", function(assert) + suite:case("basenameMixedSeparators", function(assert) local result = path.win32.basename("/home/user\\documents/file.txt") assert.eq(result, "file.txt") end) end) test.suite("PathWin32FormatSuite", function(suite) - suite:case("format_windows_absolute_path", function(assert) + suite:case("formatWindowsAbsolutePath", function(assert) local pathobj: win32.path = setmetatable({ parts = { "Users", "username", "Documents", "file.txt" }, kind = "absolute", @@ -132,7 +132,7 @@ test.suite("PathWin32FormatSuite", function(suite) assert.eq(result, "C:\\Users\\username\\Documents\\file.txt") end) - suite:case("format_windows_relative_with_drive", function(assert) + suite:case("formatWindowsRelativeWithDrive", function(assert) local pathobj: win32.path = setmetatable({ parts = { "Documents", "file.txt" }, kind = "relative", @@ -142,7 +142,7 @@ test.suite("PathWin32FormatSuite", function(suite) assert.eq(result, "C:Documents\\file.txt") end) - suite:case("format_windows_unc_path", function(assert) + suite:case("formatWindowsUncPath", function(assert) local pathobj: win32.path = setmetatable({ parts = { "server", "share", "folder", "file.txt" }, kind = "unc", @@ -152,7 +152,7 @@ test.suite("PathWin32FormatSuite", function(suite) assert.eq(result, "\\\\server\\share\\folder\\file.txt") end) - suite:case("format_empty_relative_path", function(assert) + suite:case("formatEmptyRelativePath", function(assert) local pathobj: win32.path = setmetatable({ parts = {}, kind = "relative", @@ -162,7 +162,7 @@ test.suite("PathWin32FormatSuite", function(suite) assert.eq(result, ".") end) - suite:case("format_empty_relative_path_with_drive", function(assert) + suite:case("formatEmptyRelativePathWithDrive", function(assert) local pathobj: win32.path = setmetatable({ parts = {}, kind = "relative", @@ -172,7 +172,7 @@ test.suite("PathWin32FormatSuite", function(suite) assert.eq(result, "C:") end) - suite:case("format_root_path_with_drive", function(assert) + suite:case("formatRootPathWithDrive", function(assert) local pathobj: win32.path = setmetatable({ parts = {}, kind = "absolute", @@ -182,39 +182,39 @@ test.suite("PathWin32FormatSuite", function(suite) assert.eq(result, "C:\\") end) - suite:case("format_string_returns_same", function(assert) + suite:case("formatStringReturnsSame", function(assert) local result = path.win32.format("/home/user/file.txt") assert.eq(result, "/home/user/file.txt") end) end) test.suite("PathWin32DirnameSuite", function(suite) - suite:case("dirname_windows_absolute_path", function(assert) + suite:case("dirnameWindowsAbsolutePath", function(assert) local result = path.win32.dirname("C:\\Users\\username\\Documents\\file.txt") assert.eq(result, "C:\\Users\\username\\Documents") end) - suite:case("dirname_windows_unc_path", function(assert) + suite:case("dirnameWindowsUncPath", function(assert) local result = path.win32.dirname("\\\\server\\share\\folder\\file.txt") assert.eq(result, "\\\\server\\share\\folder") end) - suite:case("dirname_single_file", function(assert) + suite:case("dirnameSingleFile", function(assert) local result = path.win32.dirname("file.txt") assert.eq(result, ".") end) - suite:case("dirname_empty_path", function(assert) + suite:case("dirnameEmptyPath", function(assert) local result = path.win32.dirname("") assert.eq(result, ".") end) - suite:case("dirname_windows_root", function(assert) + suite:case("dirnameWindowsRoot", function(assert) local result = path.win32.dirname("C:\\") assert.eq(result, "C:\\") end) - suite:case("dirname_pathobj", function(assert) + suite:case("dirnamePathobj", function(assert) local pathobj: win32.path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, kind = "relative", @@ -224,59 +224,59 @@ test.suite("PathWin32DirnameSuite", function(suite) assert.eq(result, "folder\\subfolder") end) - suite:case("dirname_windows_relative_with_drive", function(assert) + suite:case("dirnameWindowsRelativeWithDrive", function(assert) local result = path.win32.dirname("C:Documents\\file.txt") assert.eq(result, "C:Documents") end) end) test.suite("PathWin32ExtnameSuite", function(suite) - suite:case("extname_simple_extension", function(assert) + suite:case("extnameSimpleExtension", function(assert) local result = path.win32.extname("C:\\Users\\username\\file.txt") assert.eq(result, ".txt") end) - suite:case("extname_multiple_dots", function(assert) + suite:case("extnameMultipleDots", function(assert) local result = path.win32.extname("C:\\Users\\username\\archive.tar.gz") assert.eq(result, ".gz") end) - suite:case("extname_no_extension", function(assert) + suite:case("extnameNoExtension", function(assert) local result = path.win32.extname("C:\\Users\\username\\README") assert.eq(result, "") end) - suite:case("extname_hidden_file_with_extension", function(assert) + suite:case("extnameHiddenFileWithExtension", function(assert) local result = path.win32.extname("C:\\Users\\username\\.bashrc") assert.eq(result, "") end) - suite:case("extname_hidden_file_with_real_extension", function(assert) + suite:case("extnameHiddenFileWithRealExtension", function(assert) local result = path.win32.extname("C:\\Users\\username\\.config.json") assert.eq(result, ".json") end) - suite:case("extname_windows_path", function(assert) + suite:case("extnameWindowsPath", function(assert) local result = path.win32.extname("C:\\Users\\username\\document.docx") assert.eq(result, ".docx") end) - suite:case("extname_directory_path", function(assert) + suite:case("extnameDirectoryPath", function(assert) local result = path.win32.extname("C:\\Users\\username\\documents") assert.eq(result, "") end) - suite:case("extname_empty_path", function(assert) + suite:case("extnameEmptyPath", function(assert) local result = path.win32.extname("") assert.eq(result, "") end) - suite:case("extname_root_path", function(assert) + suite:case("extnameRootPath", function(assert) local result = path.win32.extname("C:\\") assert.eq(result, "") end) - suite:case("extname_pathobj", function(assert) + suite:case("extnamePathobj", function(assert) local pathobj: win32.path = setmetatable({ parts = { "folder", "file.pdf" }, kind = "relative", @@ -286,38 +286,38 @@ test.suite("PathWin32ExtnameSuite", function(suite) assert.eq(result, ".pdf") end) - suite:case("extname_dot_only", function(assert) + suite:case("extnameDotOnly", function(assert) local result = path.win32.extname("C:\\Users\\username\\file.") assert.eq(result, ".") end) - suite:case("extname_filename_is_dot", function(assert) + suite:case("extnameFilenameIsDot", function(assert) local result = path.win32.extname("C:\\Users\\username\\.") assert.eq(result, "") end) - suite:case("extname_filename_is_dotdot", function(assert) + suite:case("extnameFilenameIsDotdot", function(assert) local result = path.win32.extname("C:\\Users\\username\\..") assert.eq(result, "") end) end) test.suite("PathWin32GetDriveSuite", function(suite) - suite:case("getdrive_absolute_path_string", function(assert) + suite:case("getdriveAbsolutePathString", function(assert) local result = path.win32.drive("C:\\Users\\username\\Documents\\file.txt") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") assert.eq(#result.parts, 0) end) - suite:case("getdrive_relative_path_with_drive", function(assert) + suite:case("getdriveRelativePathWithDrive", function(assert) local result = path.win32.drive("D:Documents\\file.txt") assert.eq(result.kind, "absolute") assert.eq(result.drive, "D") assert.eq(#result.parts, 0) end) - suite:case("getdrive_path_object", function(assert) + suite:case("getdrivePathObject", function(assert) local pathobj: win32.path = setmetatable({ parts = { "Users", "username", "Documents" }, kind = "absolute", @@ -329,14 +329,14 @@ test.suite("PathWin32GetDriveSuite", function(suite) assert.eq(#result.parts, 0) end) - suite:case("getdrive_root_path", function(assert) + suite:case("getdriveRootPath", function(assert) local result = path.win32.drive("F:\\") assert.eq(result.kind, "absolute") assert.eq(result.drive, "F") assert.eq(#result.parts, 0) end) - suite:case("getdrive_error_on_empty_path", function(assert) + suite:case("getdriveErrorOnEmptyPath", function(assert) assert.errors(function() path.win32.drive("") end) @@ -344,37 +344,37 @@ test.suite("PathWin32GetDriveSuite", function(suite) end) test.suite("PathWin32IsAbsoluteSuite", function(suite) - suite:case("isAbsolute_windows_absolute_path", function(assert) + suite:case("isAbsoluteWindowsAbsolutePath", function(assert) local result = path.win32.isabsolute("C:\\Users\\username\\Documents\\file.txt") assert.eq(result, true) end) - suite:case("isAbsolute_windows_relative_path", function(assert) + suite:case("isAbsoluteWindowsRelativePath", function(assert) local result = path.win32.isabsolute("Documents\\file.txt") assert.eq(result, false) end) - suite:case("isAbsolute_windows_relative_with_drive", function(assert) + suite:case("isAbsoluteWindowsRelativeWithDrive", function(assert) local result = path.win32.isabsolute("C:Documents\\file.txt") assert.eq(result, false) end) - suite:case("isAbsolute_windows_unc_path", function(assert) + suite:case("isAbsoluteWindowsUncPath", function(assert) local result = path.win32.isabsolute("\\\\server\\share\\folder\\file.txt") assert.eq(result, true) end) - suite:case("isAbsolute_root_path", function(assert) + suite:case("isAbsoluteRootPath", function(assert) local result = path.win32.isabsolute("C:\\") assert.eq(result, true) end) - suite:case("isAbsolute_empty_path", function(assert) + suite:case("isAbsoluteEmptyPath", function(assert) local result = path.win32.isabsolute("") assert.eq(result, false) end) - suite:case("isAbsolute_pathobj_absolute", function(assert) + suite:case("isAbsolutePathobjAbsolute", function(assert) local pathobj: win32.path = setmetatable({ parts = { "home", "user", "file.txt" }, kind = "absolute", @@ -384,7 +384,7 @@ test.suite("PathWin32IsAbsoluteSuite", function(suite) assert.eq(result, true) end) - suite:case("isAbsolute_pathobj_relative", function(assert) + suite:case("isAbsolutePathobjRelative", function(assert) local pathobj: win32.path = setmetatable({ parts = { "folder", "file.txt" }, kind = "relative", @@ -396,7 +396,7 @@ test.suite("PathWin32IsAbsoluteSuite", function(suite) end) test.suite("PathWin32NormalizeSuite", function(suite) - suite:case("normalize_removes_current_directory", function(assert) + suite:case("normalizeRemovesCurrentDirectory", function(assert) local result = path.win32.normalize("C:\\Users\\.\\username\\Documents") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -406,7 +406,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[3], "Documents") end) - suite:case("normalize_resolves_parent_directory", function(assert) + suite:case("normalizeResolvesParentDirectory", function(assert) local result = path.win32.normalize("C:\\Users\\username\\..\\Documents") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -415,7 +415,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[2], "Documents") end) - suite:case("normalize_multiple_dots", function(assert) + suite:case("normalizeMultipleDots", function(assert) local result = path.win32.normalize("C:\\Users\\username\\.\\Documents\\..\\files\\.\\") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -425,7 +425,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[3], "files") end) - suite:case("normalize_relative_path", function(assert) + suite:case("normalizeRelativePath", function(assert) local result = path.win32.normalize("Documents\\.\\subfolder\\..\\file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) @@ -434,7 +434,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[2], "file.txt") end) - suite:case("normalize_removes_empty_segments", function(assert) + suite:case("normalizeRemovesEmptySegments", function(assert) local result = path.win32.normalize("C:\\Users\\\\username\\\\\\Documents") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -444,14 +444,14 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[3], "Documents") end) - suite:case("normalize_parent_beyond_root", function(assert) + suite:case("normalizeParentBeyondRoot", function(assert) local result = path.win32.normalize("C:\\Users\\..\\..\\") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") assert.eq(#result.parts, 0) end) - suite:case("normalize_unc_path", function(assert) + suite:case("normalizeUncPath", function(assert) local result = path.win32.normalize("\\\\server\\share\\.\\folder\\..\\files") assert.eq(result.kind, "unc") assert.eq(result.drive, nil) @@ -461,7 +461,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[3], "files") end) - suite:case("normalize_relative_with_drive", function(assert) + suite:case("normalizeRelativeWithDrive", function(assert) local result = path.win32.normalize("C:Documents\\.\\subfolder\\..\\file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, "C") @@ -470,7 +470,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[2], "file.txt") end) - suite:case("normalize_parent_directory_only", function(assert) + suite:case("normalizeParentDirectoryOnly", function(assert) local result = path.win32.normalize("..") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) @@ -478,7 +478,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[1], "..") end) - suite:case("normalize_multiple_parent_directories", function(assert) + suite:case("normalizeMultipleParentDirectories", function(assert) local result = path.win32.normalize("..\\..\\..") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) @@ -488,7 +488,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[3], "..") end) - suite:case("normalize_parent_then_folder", function(assert) + suite:case("normalizeParentThenFolder", function(assert) local result = path.win32.normalize("..\\folder\\file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) @@ -498,7 +498,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[3], "file.txt") end) - suite:case("normalize_folder_then_multiple_parents", function(assert) + suite:case("normalizeFolderThenMultipleParents", function(assert) local result = path.win32.normalize("folder\\subfolder\\..\\..\\other") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) @@ -506,7 +506,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[1], "other") end) - suite:case("normalize_pathobj", function(assert) + suite:case("normalizePathobj", function(assert) local pathobj: win32.path = setmetatable({ parts = { "Users", ".", "username", "..", "Documents" }, kind = "absolute", @@ -520,7 +520,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) assert.eq(result.parts[2], "Documents") end) - suite:case("normalize_relative_with_drive_and_parent", function(assert) + suite:case("normalizeRelativeWithDriveAndParent", function(assert) local result = path.win32.normalize("C:..\\folder\\file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, "C") @@ -532,7 +532,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) end) test.suite("PathWin32JoinSuite", function(suite) - suite:case("join_absolute_and_relative", function(assert) + suite:case("joinAbsoluteAndRelative", function(assert) local result = path.win32.join("C:\\Users\\username", "Documents", "file.txt") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -543,7 +543,7 @@ test.suite("PathWin32JoinSuite", function(suite) assert.eq(result.parts[4], "file.txt") end) - suite:case("join_relative_paths", function(assert) + suite:case("joinRelativePaths", function(assert) local result = path.win32.join("Documents", "subfolder", "file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) @@ -553,7 +553,7 @@ test.suite("PathWin32JoinSuite", function(suite) assert.eq(result.parts[3], "file.txt") end) - suite:case("join_drive_relative_paths", function(assert) + suite:case("joinDriveRelativePaths", function(assert) local result = path.win32.join("C:Documents", "subfolder", "file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, "C") @@ -563,7 +563,7 @@ test.suite("PathWin32JoinSuite", function(suite) assert.eq(result.parts[3], "file.txt") end) - suite:case("join_unc_and_relative", function(assert) + suite:case("joinUncAndRelative", function(assert) local result = path.win32.join("\\\\server\\share", "folder", "file.txt") assert.eq(result.kind, "unc") assert.eq(result.drive, nil) @@ -574,14 +574,14 @@ test.suite("PathWin32JoinSuite", function(suite) assert.eq(result.parts[4], "file.txt") end) - suite:case("join_no_arguments", function(assert) + suite:case("joinNoArguments", function(assert) local result = path.win32.join() assert.eq(result.kind, "relative") assert.eq(result.drive, nil) assert.eq(#result.parts, 0) end) - suite:case("join_pathobj_and_string", function(assert) + suite:case("joinPathobjAndString", function(assert) local pathobj: win32.path = setmetatable({ parts = { "Users", "username" }, kind = "absolute", @@ -602,28 +602,28 @@ test.suite("PathWin32JoinSuite", function(suite) assert.eq(pathobj.drive, "C") end) - suite:case("join_error_on_absolute_addend", function(assert) + suite:case("joinErrorOnAbsoluteAddend", function(assert) local success, _ = pcall(function() return path.win32.join("C:\\Users", "D:\\absolute\\path") end) assert.eq(success, false) end) - suite:case("join_error_on_unc_addend", function(assert) + suite:case("joinErrorOnUncAddend", function(assert) local success, _ = pcall(function() return path.win32.join("C:\\Users", "\\\\server\\share") end) assert.eq(success, false) end) - suite:case("join_error_on_incompatible_drives", function(assert) + suite:case("joinErrorOnIncompatibleDrives", function(assert) local success, _ = pcall(function() return path.win32.join("C:Documents", "D:other") end) assert.eq(success, false) end) - suite:case("join_compatible_drives", function(assert) + suite:case("joinCompatibleDrives", function(assert) local result = path.win32.join("C:Documents", "C:subfolder") assert.eq(result.kind, "relative") assert.eq(result.drive, "C") @@ -634,7 +634,7 @@ test.suite("PathWin32JoinSuite", function(suite) end) test.suite("PathWin32ResolveSuite", function(suite) - suite:case("resolve_absolute_path", function(assert) + suite:case("resolveAbsolutePath", function(assert) local result = path.win32.resolve("C:\\Users\\username\\Documents\\file.txt") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -645,7 +645,7 @@ test.suite("PathWin32ResolveSuite", function(suite) assert.eq(result.parts[4], "file.txt") end) - suite:case("resolve_relative_path_from_cwd", function(assert) + suite:case("resolveRelativePathFromCwd", function(assert) -- This test assumes we're in some working directory local result = path.win32.resolve("Documents\\file.txt") -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters @@ -658,7 +658,7 @@ test.suite("PathWin32ResolveSuite", function(suite) assert.eq(result.parts[endIndex], "file.txt") end) - suite:case("resolve_multiple_paths", function(assert) + suite:case("resolveMultiplePaths", function(assert) local result = path.win32.resolve("C:\\Users", "username", "..\\admin", "Documents") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -668,7 +668,7 @@ test.suite("PathWin32ResolveSuite", function(suite) assert.eq(result.parts[3], "Documents") end) - suite:case("resolve_with_absolute_in_middle", function(assert) + suite:case("resolveWithAbsoluteInMiddle", function(assert) local result = path.win32.resolve("relative", "D:\\absolute\\path", "more") assert.eq(result.kind, "absolute") assert.eq(result.drive, "D") @@ -678,7 +678,7 @@ test.suite("PathWin32ResolveSuite", function(suite) assert.eq(result.parts[3], "more") end) - suite:case("resolve_unc_path", function(assert) + suite:case("resolveUncPath", function(assert) local result = path.win32.resolve("\\\\server\\share\\folder") assert.eq(result.kind, "unc") assert.eq(result.drive, nil) @@ -688,7 +688,7 @@ test.suite("PathWin32ResolveSuite", function(suite) assert.eq(result.parts[3], "folder") end) - suite:case("resolve_no_arguments", function(assert) + suite:case("resolveNoArguments", function(assert) -- Should return normalized current working directory local result = path.win32.resolve() -- Absolute unix paths will be parsed as relative Windows paths because they don't have drive letters @@ -696,7 +696,7 @@ test.suite("PathWin32ResolveSuite", function(suite) -- Can't test exact parts since cwd varies end) - suite:case("resolve_drive_relative_path", function(assert) + suite:case("resolveDriveRelativePath", function(assert) -- Get the drive letter from the current working directory local cwdPath = path.win32.parse(process.cwd() :: win32.pathlike) local drive = if system.win32 then path.win32.drive(cwdPath).drive :: string else "C" @@ -712,7 +712,7 @@ test.suite("PathWin32ResolveSuite", function(suite) assert.eq(result.parts[endIndex], "file.txt") end) - suite:case("resolve_with_dot_segments", function(assert) + suite:case("resolveWithDotSegments", function(assert) local result = path.win32.resolve("C:\\Users\\username", "..\\admin\\.\\Documents") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -722,7 +722,7 @@ test.suite("PathWin32ResolveSuite", function(suite) assert.eq(result.parts[3], "Documents") end) - suite:case("resolve_empty_strings", function(assert) + suite:case("resolveEmptyStrings", function(assert) local result = path.win32.resolve("", "", "C:\\Users\\username") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") @@ -733,14 +733,14 @@ test.suite("PathWin32ResolveSuite", function(suite) end) test.suite("PathWin32ToStringSuite", function(suite) - suite:case("toString_windows_absolute_path", function(assert) + suite:case("toStringWindowsAbsolutePath", function(assert) local filePath = "C:\\Users\\username\\Documents\\file.txt" local pathObj: win32.path = win32.parse(filePath) local result = tostring(pathObj) assert.eq(result, filePath) end) - suite:case("toString_windows_relative_path", function(assert) + suite:case("toStringWindowsRelativePath", function(assert) local filePath = "C:\\Users\\username\\Documents\\file.txt" local pathObj: win32.path = path.win32.parse(filePath) local result = tostring(pathObj) @@ -749,7 +749,7 @@ test.suite("PathWin32ToStringSuite", function(suite) end) test.suite("PathWin32RelativeSuite", function(suite) - suite:case("relative_same_path", function(assert) + suite:case("relativeSamePath", function(assert) local from = win32.parse("C:\\Users\\username\\Documents") local to = win32.parse("C:\\Users\\username\\Documents") local result = path.win32.relative(from, to) @@ -758,7 +758,7 @@ test.suite("PathWin32RelativeSuite", function(suite) assert.eq(result.drive, nil) end) - suite:case("relative_subdirectory", function(assert) + suite:case("relativeSubdirectory", function(assert) local from = win32.parse("C:\\Users\\username") local to = win32.parse("C:\\Users\\username\\Documents\\file.txt") local result = path.win32.relative(from, to) @@ -769,7 +769,7 @@ test.suite("PathWin32RelativeSuite", function(suite) assert.eq(result.drive, nil) end) - suite:case("relative_parent_directory", function(assert) + suite:case("relativeParentDirectory", function(assert) local from = win32.parse("C:\\Users\\username\\Documents") local to = win32.parse("C:\\Users\\username\\file.txt") local result = path.win32.relative(from, to) @@ -780,7 +780,7 @@ test.suite("PathWin32RelativeSuite", function(suite) assert.eq(result.drive, nil) end) - suite:case("relative_different_kinds", function(assert) + suite:case("relativeDifferentKinds", function(assert) local from = win32.parse("C:\\Users\\username\\Documents") local to = win32.parse("Documents\\file.txt") assert.erroreq(function() @@ -788,7 +788,7 @@ test.suite("PathWin32RelativeSuite", function(suite) end, "Cannot compute relative path between different kinds of paths") end) - suite:case("relative_different_drives", function(assert) + suite:case("relativeDifferentDrives", function(assert) local from = win32.parse("C:\\Users\\username") local to = win32.parse("D:\\Documents\\file.txt") assert.erroreq(function() @@ -796,7 +796,7 @@ test.suite("PathWin32RelativeSuite", function(suite) end, "Cannot compute relative path between different drives") end) - suite:case("relative_unc_paths", function(assert) + suite:case("relativeUncPaths", function(assert) local from = win32.parse("\\\\server\\share\\folder1") local to = win32.parse("\\\\server\\share\\folder2\\file.txt") local result = path.win32.relative(from, to) @@ -807,7 +807,7 @@ test.suite("PathWin32RelativeSuite", function(suite) assert.eq(result.parts[3], "file.txt") end) - suite:case("relative_with_drive_letter_same_drive", function(assert) + suite:case("relativeWithDriveLetterSameDrive", function(assert) local from = win32.parse("C:Documents") local to = win32.parse("C:Documents\\subfolder\\file.txt") local result = path.win32.relative(from, to) @@ -817,7 +817,7 @@ test.suite("PathWin32RelativeSuite", function(suite) assert.eq(result.parts[2], "file.txt") end) - suite:case("parse_windows_absolute_path_with_forward_slashes", function(assert) + suite:case("parseWindowsAbsolutePathWithForwardSlashes", function(assert) local result: win32.path = path.win32.parse("C:/Users/username/Documents/file.txt") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index f2ade0633..3acb9d397 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -5,11 +5,11 @@ local test = require("@std/test") local windowsPath = require("@std/path/win32") test.suite("ProcessSuite", function(suite) - suite:case("process_args_is_table", function(assert) + suite:case("processArgsIsTable", function(assert) assert.eq(type(process.args), "table") end) - suite:case("homedir_and_cwd_and_execpath", function(assert) + suite:case("homedirAndCwdAndExecpath", function(assert) local hd = process.homedir() assert.neq(pathlib.format(hd), "") @@ -20,17 +20,17 @@ test.suite("ProcessSuite", function(suite) assert.neq(pathlib.format(ex), "") end) - suite:case("run_echo_array_via_run", function(assert) + suite:case("runEchoArrayViaRun", function(assert) local r = process.run({ "echo", "hello-from-lute" }) assert.eq(r.stdout, "hello-from-lute\n") end) - suite:case("run_echo_array_via_system", function(assert) + suite:case("runEchoArrayViaSystem", function(assert) local r = process.system("echo hello-from-lute") assert.eq(r.stdout, "hello-from-lute\n") end) - suite:case("run_system_and_cwd", function(check) + suite:case("runSystemAndCwd", function(check) local rootdir: string = "/" local root: pathlib.path? = nil @@ -69,7 +69,7 @@ test.suite("ProcessSuite", function(suite) }) end) - suite:case("run_nonzero_exitcode", function(assert) + suite:case("runNonzeroExitcode", function(assert) -- LUAUFIX: we shouldn't need to upcast the result of `process.run` to `{ [any]: any }` local r = process.run({ "sh", "-c", "exit 41" }) :: { [any]: any } assert.tableeq(r, { @@ -80,7 +80,7 @@ test.suite("ProcessSuite", function(suite) }) end) - suite:case("process_run_options_error_cases", function(assert) + suite:case("processRunOptionsErrorCases", function(assert) assert.erroreq(function() process.run({}) end, "process.run requires a non-empty table of arguments") @@ -115,13 +115,13 @@ test.suite("ProcessSuite", function(suite) end, "process.run requires a non-empty table of arguments") end) - suite:case("process_run_arraylike_table", function(assert) + suite:case("processRunArraylikeTable", function(assert) -- We expect this to still work and ignore the non-string keys local r = process.run({ "echo", "hello", foo = "bar" }) assert.eq(r.stdout, "hello\n") end) - suite:case("process_system_options_error_cases", function(assert) + suite:case("processSystemOptionsErrorCases", function(assert) -- All of these tests intentionally pass values of the incorrect -- type to `process.system`: this is to ensure that the error -- handling on the runtime side is correct (for example, does not diff --git a/tests/std/stringext.test.luau b/tests/std/stringext.test.luau index 1b78bbb9c..92d8717eb 100644 --- a/tests/std/stringext.test.luau +++ b/tests/std/stringext.test.luau @@ -2,7 +2,7 @@ local stringext = require("@std/stringext") local test = require("@std/test") test.suite("StringExt", function(suite) - suite:case("hasprefix_and_hassuffix", function(assert) + suite:case("hasprefixAndHassuffix", function(assert) assert.eq(stringext.hasprefix("hello", "he"), true) assert.eq(stringext.hasprefix("hi", "hello"), false) @@ -10,7 +10,7 @@ test.suite("StringExt", function(suite) assert.eq(stringext.hassuffix("foo", "foobar"), false) end) - suite:case("removeprefix_and_removesuffix", function(assert) + suite:case("removeprefixAndRemovesuffix", function(assert) assert.eq(stringext.removeprefix("hello", "he"), "llo") assert.eq(stringext.removeprefix("hello", "nope"), "hello") diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 70d1f6797..d1f5beaf9 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -521,7 +521,7 @@ test.suite("parseExpr", function(suite) end) -- table items serialized with location field - suite:case("ExprTableItem serialized with location field", function(assert) + suite:case("exprTableItemSerializedWithLocationField", function(assert) local tableExpr = parser.parseexpr([[ { 1, @@ -1246,7 +1246,7 @@ test.suite("AST_nodes_frozen", function(suite) local frozenTableMessage = "attempt to modify a readonly table" -- Expression nodes - suite:case("AstExprGroup_frozen", function(assert) + suite:case("astExprGroupFrozen", function(assert) local expr = parser.parseexpr("(x)") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "group") @@ -1255,7 +1255,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprConstantNil_frozen", function(assert) + suite:case("astExprConstantNilFrozen", function(assert) local expr = parser.parseexpr("nil") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "nil") @@ -1264,7 +1264,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprConstantBool_frozen", function(assert) + suite:case("astExprConstantBoolFrozen", function(assert) local expr = parser.parseexpr("true") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "boolean") @@ -1273,7 +1273,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprConstantNumber_frozen", function(assert) + suite:case("astExprConstantNumberFrozen", function(assert) local expr = parser.parseexpr("42") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "number") @@ -1282,7 +1282,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprConstantString_frozen", function(assert) + suite:case("astExprConstantStringFrozen", function(assert) local expr = parser.parseexpr("'hello'") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "string") @@ -1291,7 +1291,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprLocal_frozen", function(assert) + suite:case("astExprLocalFrozen", function(assert) local block = parser.parseblock("local x = 1\nreturn x") local returnStat = block.statements[2] :: syntax.AstStatReturn local expr = returnStat.expressions[1].node @@ -1302,7 +1302,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprGlobal_frozen", function(assert) + suite:case("astExprGlobalFrozen", function(assert) local expr = parser.parseexpr("_G") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "global") @@ -1311,7 +1311,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprVarargs_frozen", function(assert) + suite:case("astExprVarargsFrozen", function(assert) local expr = parser.parseexpr("...") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "vararg") @@ -1320,7 +1320,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprCall_frozen", function(assert) + suite:case("astExprCallFrozen", function(assert) local expr = parser.parseexpr("foo(1, 2)") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "call") @@ -1332,7 +1332,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprInstantiate_frozen", function(assert) + suite:case("astExprInstantiateFrozen", function(assert) local callExpr = parser.parseexpr("foo<>()") assert.eq(callExpr.tag, "call") local expr = (callExpr :: syntax.AstExprCall).func @@ -1346,7 +1346,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprIndexName_frozen", function(assert) + suite:case("astExprIndexNameFrozen", function(assert) local expr = parser.parseexpr("obj.field") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "indexname") @@ -1355,7 +1355,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprIndexExpr_frozen", function(assert) + suite:case("astExprIndexExprFrozen", function(assert) local expr = parser.parseexpr("obj[key]") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "index") @@ -1364,7 +1364,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprFunction_frozen", function(assert) + suite:case("astExprFunctionFrozen", function(assert) local expr = parser.parseexpr("@checked function(x, y) return x + y end") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "function") @@ -1385,7 +1385,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprTable_frozen", function(assert) + suite:case("astExprTableFrozen", function(assert) local expr = parser.parseexpr("{ a = 1, [2] = 3, 4 }") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "table") @@ -1406,7 +1406,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprUnary_frozen", function(assert) + suite:case("astExprUnaryFrozen", function(assert) local expr = parser.parseexpr("-x") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "unary") @@ -1415,7 +1415,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprBinary_frozen", function(assert) + suite:case("astExprBinaryFrozen", function(assert) local expr = parser.parseexpr("x + y") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "binary") @@ -1424,7 +1424,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprInterpString_frozen", function(assert) + suite:case("astExprInterpStringFrozen", function(assert) local expr = parser.parseexpr("`hello {x}`") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "interpolatedstring") @@ -1436,7 +1436,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprTypeAssertion_frozen", function(assert) + suite:case("astExprTypeAssertionFrozen", function(assert) local expr = parser.parseexpr("x :: number") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "cast") @@ -1445,7 +1445,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstExprIfElse_frozen", function(assert) + suite:case("astExprIfElseFrozen", function(assert) local expr = parser.parseexpr("if x then 1 elseif y then 2 else 3") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "conditional") @@ -1461,7 +1461,7 @@ test.suite("AST_nodes_frozen", function(suite) end) -- Statement nodes - suite:case("AstStatBlock_frozen", function(assert) + suite:case("astStatBlockFrozen", function(assert) local block = parser.parseblock("local x = 1\nlocal y = 2") assert.eq(block.kind, "stat") assert.eq(block.tag, "block") @@ -1473,7 +1473,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatDo_frozen", function(assert) + suite:case("astStatDoFrozen", function(assert) local block = parser.parseblock("do local x = 1 end") local doStat = block.statements[1] :: syntax.AstStatDo assert.eq(doStat.kind, "stat") @@ -1483,7 +1483,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatIf_frozen", function(assert) + suite:case("astStatIfFrozen", function(assert) local block = parser.parseblock("if x then local y = 1 elseif z then local w = 2 else local v = 3 end") local ifStat = block.statements[1] :: syntax.AstStatIf assert.eq(ifStat.kind, "stat") @@ -1499,7 +1499,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatWhile_frozen", function(assert) + suite:case("astStatWhileFrozen", function(assert) local block = parser.parseblock("while x do local y = 1 end") local whileStat = block.statements[1] :: syntax.AstStatWhile assert.eq(whileStat.kind, "stat") @@ -1509,7 +1509,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatRepeat_frozen", function(assert) + suite:case("astStatRepeatFrozen", function(assert) local block = parser.parseblock("repeat local x = 1 until x > 10") local repeatStat = block.statements[1] :: syntax.AstStatRepeat assert.eq(repeatStat.kind, "stat") @@ -1519,7 +1519,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatBreak_frozen", function(assert) + suite:case("astStatBreakFrozen", function(assert) local block = parser.parseblock("while true do break end") local whileStat = block.statements[1] :: syntax.AstStatWhile local breakStat = whileStat.body.statements[1] :: syntax.AstStatBreak @@ -1530,7 +1530,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatContinue_frozen", function(assert) + suite:case("astStatContinueFrozen", function(assert) local block = parser.parseblock("while true do continue end") local whileStat = block.statements[1] :: syntax.AstStatWhile local continueStat = whileStat.body.statements[1] :: syntax.AstStatContinue @@ -1541,7 +1541,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatReturn_frozen", function(assert) + suite:case("astStatReturnFrozen", function(assert) local block = parser.parseblock("return 1, 2, 3") local returnStat = block.statements[1] :: syntax.AstStatReturn assert.eq(returnStat.kind, "stat") @@ -1554,7 +1554,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatExpr_frozen", function(assert) + suite:case("astStatExprFrozen", function(assert) local block = parser.parseblock("foo()") local exprStat = block.statements[1] :: syntax.AstStatExpr assert.eq(exprStat.kind, "stat") @@ -1564,7 +1564,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatLocal_frozen", function(assert) + suite:case("astStatLocalFrozen", function(assert) local block = parser.parseblock("local x, y = 1, 2") local localStat = block.statements[1] :: syntax.AstStatLocal assert.eq(localStat.kind, "stat") @@ -1580,7 +1580,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatFor_frozen", function(assert) + suite:case("astStatForFrozen", function(assert) local block = parser.parseblock("for i = 1, 10, 2 do local x = i end") local forStat = block.statements[1] :: syntax.AstStatFor assert.eq(forStat.kind, "stat") @@ -1590,7 +1590,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatForIn_frozen", function(assert) + suite:case("astStatForInFrozen", function(assert) local block = parser.parseblock("for k, v in pairs(t) do local x = k end") local forInStat = block.statements[1] :: syntax.AstStatForIn assert.eq(forInStat.kind, "stat") @@ -1606,7 +1606,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatAssign_frozen", function(assert) + suite:case("astStatAssignFrozen", function(assert) local block = parser.parseblock("x, y = 1, 2") local assignStat = block.statements[1] :: syntax.AstStatAssign assert.eq(assignStat.kind, "stat") @@ -1622,7 +1622,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatCompoundAssign_frozen", function(assert) + suite:case("astStatCompoundAssignFrozen", function(assert) local block = parser.parseblock("x += 5") local compoundStat = block.statements[1] :: syntax.AstStatCompoundAssign assert.eq(compoundStat.kind, "stat") @@ -1632,7 +1632,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatFunction_frozen", function(assert) + suite:case("astStatFunctionFrozen", function(assert) local block = parser.parseblock("function foo() end") local funcStat = block.statements[1] :: syntax.AstStatFunction assert.eq(funcStat.kind, "stat") @@ -1642,7 +1642,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatLocalFunction_frozen", function(assert) + suite:case("astStatLocalFunctionFrozen", function(assert) local block = parser.parseblock("local function foo() end") local localFuncStat = block.statements[1] :: syntax.AstStatLocalFunction assert.eq(localFuncStat.kind, "stat") @@ -1652,7 +1652,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatTypeAlias_frozen", function(assert) + suite:case("astStatTypeAliasFrozen", function(assert) local block = parser.parseblock("type Foo = T") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.kind, "stat") @@ -1662,7 +1662,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstStatTypeFunction_frozen", function(assert) + suite:case("astStatTypeFunctionFrozen", function(assert) local block = parser.parseblock("type function foo() return function() end end") local typeFunc = block.statements[1] :: syntax.AstStatTypeFunction assert.eq(typeFunc.kind, "stat") @@ -1673,7 +1673,7 @@ test.suite("AST_nodes_frozen", function(suite) end) -- Type nodes - suite:case("AstTypeReference_frozen", function(assert) + suite:case("astTypeReferenceFrozen", function(assert) local block = parser.parseblock("type Foo = Bar") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local typeRef = typeAlias.type :: syntax.AstTypeReference @@ -1684,7 +1684,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeSingletonBool_frozen", function(assert) + suite:case("astTypeSingletonBoolFrozen", function(assert) local block = parser.parseblock("type Foo = true") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool @@ -1695,7 +1695,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeSingletonString_frozen", function(assert) + suite:case("astTypeSingletonStringFrozen", function(assert) local block = parser.parseblock("type Foo = 'hello'") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local singletonStr = typeAlias.type :: syntax.AstTypeSingletonString @@ -1706,7 +1706,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeTypeof_frozen", function(assert) + suite:case("astTypeTypeofFrozen", function(assert) local block = parser.parseblock("type Foo = typeof(x)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local typeofType = typeAlias.type :: syntax.AstTypeTypeof @@ -1717,7 +1717,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeGroup_frozen", function(assert) + suite:case("astTypeGroupFrozen", function(assert) local block = parser.parseblock("type Foo = (Bar)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local groupType = typeAlias.type :: syntax.AstTypeGroup @@ -1728,7 +1728,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeOptional_frozen", function(assert) + suite:case("astTypeOptionalFrozen", function(assert) local block = parser.parseblock("type Foo = string?") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local unionType = typeAlias.type :: syntax.AstTypeUnion @@ -1740,7 +1740,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeUnion_frozen", function(assert) + suite:case("astTypeUnionFrozen", function(assert) local block = parser.parseblock("type Foo = string | number") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local unionType = typeAlias.type :: syntax.AstTypeUnion @@ -1754,7 +1754,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeIntersection_frozen", function(assert) + suite:case("astTypeIntersectionFrozen", function(assert) local block = parser.parseblock("type Foo = A & B") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local intersectionType = typeAlias.type :: syntax.AstTypeIntersection @@ -1768,7 +1768,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeArray_frozen", function(assert) + suite:case("astTypeArrayFrozen", function(assert) local block = parser.parseblock("type Foo = {string}") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local arrayType = typeAlias.type :: syntax.AstTypeArray @@ -1779,7 +1779,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeTable_frozen", function(assert) + suite:case("astTypeTableFrozen", function(assert) local block = parser.parseblock('type Foo = { a: number, [string]: boolean, ["hello"]: "world" }') local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local tableType = typeAlias.type :: syntax.AstTypeTable @@ -1802,7 +1802,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypeFunction_frozen", function(assert) + suite:case("astTypeFunctionFrozen", function(assert) local block = parser.parseblock("type Foo = (x: number) -> string") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local funcType = typeAlias.type :: syntax.AstTypeFunction @@ -1816,7 +1816,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypePackExplicit_frozen", function(assert) + suite:case("astTypePackExplicitFrozen", function(assert) local block = parser.parseblock("type Foo = (number, string) -> (boolean, string)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local funcType = typeAlias.type :: syntax.AstTypeFunction @@ -1828,7 +1828,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstTypePackVariadic_frozen", function(assert) + suite:case("astTypePackVariadicFrozen", function(assert) local block = parser.parseblock("type Foo = () -> ...number") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local funcType = typeAlias.type :: syntax.AstTypeFunction @@ -1841,7 +1841,7 @@ test.suite("AST_nodes_frozen", function(suite) end) -- Other node types - suite:case("AstLocal_frozen", function(assert) + suite:case("astLocalFrozen", function(assert) local block = parser.parseblock("local x: number = 1") local localStat = block.statements[1] :: syntax.AstStatLocal local local_ = localStat.variables[1].node @@ -1851,7 +1851,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstGenericType_frozen", function(assert) + suite:case("astGenericTypeFrozen", function(assert) local block = parser.parseblock("type Foo = T") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local generic = (typeAlias.generics :: any)[1].node @@ -1861,7 +1861,7 @@ test.suite("AST_nodes_frozen", function(suite) end, frozenTableMessage) end) - suite:case("AstGenericTypePack_frozen", function(assert) + suite:case("astGenericTypePackFrozen", function(assert) local block = parser.parseblock("type Foo = (T, U...) -> ()") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local genericPack = (typeAlias.genericpacks :: any)[1].node @@ -1873,7 +1873,7 @@ test.suite("AST_nodes_frozen", function(suite) end) -- Token type - suite:case("Token_frozen", function(assert) + suite:case("tokenFrozen", function(assert) local block = parser.parseblock("local x = 1") local localStat = block.statements[1] :: syntax.AstStatLocal local token = localStat.localkeyword @@ -1890,7 +1890,7 @@ test.suite("AST_nodes_frozen", function(suite) end) -- Trivia types - suite:case("Trivia_frozen", function(assert) + suite:case("triviaFrozen", function(assert) local block = parser.parseblock([=[ -- This is a comment --[[This is a multiline comment @@ -1909,7 +1909,7 @@ test.suite("AST_nodes_frozen", function(suite) end) -- ParseResult type - suite:case("ParseResult_frozen", function(assert) + suite:case("parseResultFrozen", function(assert) local result = parser.parse("local x = 1") assert.erroreq(function() (result :: any).root = nil @@ -1920,7 +1920,7 @@ test.suite("AST_nodes_frozen", function(suite) end) -- Pair type (used in Punctuated) - suite:case("Pair_frozen", function(assert) + suite:case("pairFrozen", function(assert) local block = parser.parseblock("local x, y = 1, 2") local localStat = block.statements[1] :: syntax.AstStatLocal local pair = localStat.variables[1] @@ -1933,7 +1933,7 @@ test.suite("AST_nodes_frozen", function(suite) end) -- Eof type - suite:case("Eof_frozen", function(assert) + suite:case("eofFrozen", function(assert) local result = parser.parse("local x = 1") local eof = result.eof assert.eq(eof.tag, "eof") diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index a7d6f0888..5fcb0e6a8 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -21,7 +21,7 @@ local function checkReplacement( end test.suite("SyntaxPrinter", function(suite) - suite:case("string_replacement", function(assert) + suite:case("stringReplacement", function(assert) local source = [[ local name = "World" ]] @@ -35,7 +35,7 @@ test.suite("SyntaxPrinter", function(suite) end, assert) end) - suite:case("expr_replacement", function(assert) + suite:case("exprReplacement", function(assert) local source = [[ local name = "World" ]] @@ -49,7 +49,7 @@ test.suite("SyntaxPrinter", function(suite) end, assert) end) - suite:case("stat_replacement", function(assert) + suite:case("statReplacement", function(assert) local source = [[ local name = "World" ]] @@ -63,7 +63,7 @@ test.suite("SyntaxPrinter", function(suite) end, assert) end) - suite:case("type_replacement", function(assert) + suite:case("typeReplacement", function(assert) local source = [[local name: number = "World"]] local expected = [[local name: string = "World"]] @@ -82,7 +82,7 @@ test.suite("SyntaxPrinter", function(suite) end, assert) end) - suite:case("type_pack_replacement", function(assert) + suite:case("typePackReplacement", function(assert) local source = [[type t = (...number) -> ()]] local expected = [[type t = (...number) -> string, ...string)]] @@ -101,7 +101,7 @@ test.suite("SyntaxPrinter", function(suite) end, assert) end) - suite:case("expr_table_item_replacement", function(assert) + suite:case("exprTableItemReplacement", function(assert) local source = "local tbl = { a = 1, }" local expected = "local tbl = { b = 2, }" @@ -117,7 +117,7 @@ test.suite("SyntaxPrinter", function(suite) end, assert) end) - suite:case("exprgroup_trivia", function(assert) + suite:case("exprGroupTrivia", function(assert) local source = [[local x = (1 + 2) -- after]] local expected = "local x =\nreplaced -- after" @@ -129,7 +129,7 @@ test.suite("SyntaxPrinter", function(suite) end, assert) end) - suite:case("exprnil_trivia", function(assert) + suite:case("exprNilTrivia", function(assert) local source = [[local x = nil -- after]] local expected = "local x =\nreplaced -- after" @@ -141,7 +141,7 @@ nil -- after]] end, assert) end) - suite:case("exprbool_trivia", function(assert) + suite:case("exprBoolTrivia", function(assert) local source = [[local x = true -- after]] local expected = "local x =\nreplaced -- after" @@ -153,7 +153,7 @@ true -- after]] end, assert) end) - suite:case("exprnumber_trivia", function(assert) + suite:case("exprNumberTrivia", function(assert) local source = [[local x = 235 -- after]] local expected = "local x =\nreplaced -- after" @@ -165,7 +165,7 @@ true -- after]] end, assert) end) - suite:case("exprlocal_trivia", function(assert) + suite:case("exprLocalTrivia", function(assert) local source = [[local y = 3 local x = y -- after]] @@ -178,7 +178,7 @@ y -- after]] end, assert) end) - suite:case("exprglobal_trivia", function(assert) + suite:case("exprGlobalTrivia", function(assert) local source = [[local x = y -- after]] local expected = "local x =\nreplaced -- after" @@ -190,7 +190,7 @@ y -- after]] end, assert) end) - suite:case("exprvarargs_trivia", function(assert) + suite:case("exprVarargsTrivia", function(assert) local source = [[local x = ... -- after]] local expected = "local x =\nreplaced -- after" @@ -202,7 +202,7 @@ y -- after]] end, assert) end) - suite:case("exprcall_trivia", function(assert) + suite:case("exprCallTrivia", function(assert) local source = [[local x = print() -- after]] local expected = "local x =\nreplaced -- after" @@ -214,7 +214,7 @@ print() -- after]] end, assert) end) - suite:case("exprindexname_trivia", function(assert) + suite:case("exprIndexNameTrivia", function(assert) local source = [[local x = hello.world -- after]] local expected = "local x =\nreplaced -- after" @@ -226,7 +226,7 @@ hello.world -- after]] end, assert) end) - suite:case("exprindexexpr_trivia", function(assert) + suite:case("exprIndexExprTrivia", function(assert) local source = [[local x = hello['world'] -- after]] local expected = "local x =\nreplaced -- after" @@ -238,7 +238,7 @@ hello['world'] -- after]] end, assert) end) - suite:case("exprfunction_trivia", function(assert) + suite:case("exprFunctionTrivia", function(assert) local source = [[local x = function() return end -- after]] local expected = "local x =\nreplaced -- after" @@ -250,7 +250,7 @@ function() return end -- after]] end, assert) end) - suite:case("exprtable_trivia", function(assert) + suite:case("exprTableTrivia", function(assert) local source = [[local x = {} -- after]] local expected = "local x =\nreplaced -- after" @@ -262,7 +262,7 @@ function() return end -- after]] end, assert) end) - suite:case("exprunary_trivia", function(assert) + suite:case("exprUnaryTrivia", function(assert) local source = [[local x = #t -- after]] local expected = "local x =\nreplaced -- after" @@ -274,7 +274,7 @@ function() return end -- after]] end, assert) end) - suite:case("exprbinary_trivia", function(assert) + suite:case("exprBinaryTrivia", function(assert) local source = [[local x = 3 + 2 -- after]] local expected = "local x =\nreplaced -- after" @@ -286,7 +286,7 @@ function() return end -- after]] end, assert) end) - suite:case("exprinterpstring_trivia", function(assert) + suite:case("exprInterpStringTrivia", function(assert) local source = [[local x = `hello {"world"}` -- after]] local expected = "local x =\nreplaced -- after" @@ -298,7 +298,7 @@ function() return end -- after]] end, assert) end) - suite:case("exprtypeassertion_trivia", function(assert) + suite:case("exprTypeAssertionTrivia", function(assert) local source = [[local x = y :: number -- after]] local expected = "local x =\nreplaced -- after" @@ -310,7 +310,7 @@ y :: number -- after]] end, assert) end) - suite:case("exprifelse_trivia", function(assert) + suite:case("exprIfElseTrivia", function(assert) local source = [[local x = if true then 'hello' else 'world' -- after]] local expected = "local x =\nreplaced -- after" @@ -322,7 +322,7 @@ if true then 'hello' else 'world' -- after]] end, assert) end) - suite:case("expr_trivia", function(assert) + suite:case("exprTrivia", function(assert) local source = [[local x, y = if true then 'hello' else 'world', z :: string -- after]] local expected = "local x, y =\nreplaced, replaced -- after" @@ -334,7 +334,7 @@ if true then 'hello' else 'world', z :: string -- after]] end, assert) end) - suite:case("statblock_trivia", function(assert) + suite:case("statBlockTrivia", function(assert) local source = [[-- hello local y = 1 -- world]] @@ -347,7 +347,7 @@ local y = 1 end, assert) end) - suite:case("statif_trivia", function(assert) + suite:case("statIfTrivia", function(assert) local source = [[-- hello if true then return 1 @@ -364,7 +364,7 @@ end end, assert) end) - suite:case("statwhile_trivia", function(assert) + suite:case("statWhileTrivia", function(assert) local source = [[-- hello while true do print("hello") @@ -379,7 +379,7 @@ end end, assert) end) - suite:case("statrepeat_trivia", function(assert) + suite:case("statRepeatTrivia", function(assert) local source = [[-- hello repeat print("hello") @@ -394,7 +394,7 @@ until true end, assert) end) - suite:case("statbreak_trivia", function(assert) + suite:case("statBreakTrivia", function(assert) local source = [[-- hello while true do break @@ -409,7 +409,7 @@ end end, assert) end) - suite:case("statcontinue_trivia", function(assert) + suite:case("statContinueTrivia", function(assert) local source = [[-- hello while true do continue @@ -424,7 +424,7 @@ end end, assert) end) - suite:case("statreturn_trivia", function(assert) + suite:case("statReturnTrivia", function(assert) local source = [[-- hello function foo() return 1, 2 end -- world]] @@ -437,7 +437,7 @@ function foo() return 1, 2 end end, assert) end) - suite:case("statexpr_trivia", function(assert) + suite:case("statExprTrivia", function(assert) local source = [[-- hello foobar() -- world]] @@ -450,7 +450,7 @@ foobar() end, assert) end) - suite:case("statlocal_trivia", function(assert) + suite:case("statLocalTrivia", function(assert) local source = [[-- hello local x = 1 -- world]] @@ -463,7 +463,7 @@ local x = 1 end, assert) end) - suite:case("statfor_trivia", function(assert) + suite:case("statForTrivia", function(assert) local source = [[-- hello for i = 1, 10 do print(i) @@ -478,7 +478,7 @@ end end, assert) end) - suite:case("statforin_trivia", function(assert) + suite:case("statForInTrivia", function(assert) local source = [[-- hello for i in {1, 2, 3} do print(i) @@ -493,7 +493,7 @@ end end, assert) end) - suite:case("statassign_trivia", function(assert) + suite:case("statAssignTrivia", function(assert) local source = [[-- hello local y = 2 y = 1 @@ -507,7 +507,7 @@ y = 1 end, assert) end) - suite:case("statcompoundassign_trivia", function(assert) + suite:case("statCompoundAssignTrivia", function(assert) local source = [[-- hello local y = 2 y -= 1 @@ -521,7 +521,7 @@ y -= 1 end, assert) end) - suite:case("statfunction_trivia", function(assert) + suite:case("statFunctionTrivia", function(assert) local source = [[-- hello function foo() return 1 @@ -536,7 +536,7 @@ end end, assert) end) - suite:case("statlocalfunction_trivia", function(assert) + suite:case("statLocalFunctionTrivia", function(assert) local source = [[-- hello local function foo() return 1 @@ -551,7 +551,7 @@ end end, assert) end) - suite:case("stattypealias_trivia", function(assert) + suite:case("statTypeAliasTrivia", function(assert) local source = [[-- hello type foo = number -- world]] @@ -564,7 +564,7 @@ type foo = number end, assert) end) - suite:case("stattypefunction_trivia", function(assert) + suite:case("statTypeFunctionTrivia", function(assert) local source = [[-- hello type function foo() return number end -- world]] @@ -577,7 +577,7 @@ type function foo() return number end end, assert) end) - suite:case("stat_trivia", function(assert) + suite:case("statTrivia", function(assert) local source = [[-- hello local x = 1 local y = 2 @@ -591,7 +591,7 @@ local y = 2 end, assert) end) - suite:case("typereference_trivia", function(assert) + suite:case("typeReferenceTrivia", function(assert) local source = [[-- hello type foo = number -- world]] @@ -604,7 +604,7 @@ type foo = number end, assert) end) - suite:case("typesingletonbool_trivia", function(assert) + suite:case("typeSingletonBoolTrivia", function(assert) local source = [[-- hello type foo = true -- world]] @@ -617,7 +617,7 @@ type foo = true end, assert) end) - suite:case("typesingletonstring_trivia", function(assert) + suite:case("typeSingletonStringTrivia", function(assert) local source = [[-- hello local x : "hello" -- world]] @@ -630,7 +630,7 @@ local x : "hello" end, assert) end) - suite:case("typetypeof_trivia", function(assert) + suite:case("typeTypeOfTrivia", function(assert) local source = [[-- hello type foo = typeof(123) -- world]] @@ -643,7 +643,7 @@ type foo = typeof(123) end, assert) end) - suite:case("typegroup_trivia", function(assert) + suite:case("typeGroupTrivia", function(assert) local source = [[-- hello type foo = (number) -- world]] @@ -656,7 +656,7 @@ type foo = (number) end, assert) end) - suite:case("typeoptional_trivia", function(assert) + suite:case("typeOptionalTrivia", function(assert) local source = [[-- hello type foo = true? -- world]] @@ -669,7 +669,7 @@ type foo = true? end, assert) end) - suite:case("typeunion_trivia", function(assert) + suite:case("typeUnionTrivia", function(assert) local source = [[-- hello type foo = true? | false -- world]] @@ -682,7 +682,7 @@ type foo = true? | false end, assert) end) - suite:case("typeintersection_trivia", function(assert) + suite:case("typeIntersectionTrivia", function(assert) local source = [[-- hello type foo = true & false -- world]] @@ -695,7 +695,7 @@ type foo = true & false end, assert) end) - suite:case("typearray_trivia", function(assert) + suite:case("typeArrayTrivia", function(assert) local source = [[-- hello type foo = { number } -- world]] @@ -708,7 +708,7 @@ type foo = { number } end, assert) end) - suite:case("typetable_trivia", function(assert) + suite:case("typeTableTrivia", function(assert) local source = [[-- hello type foo = {} -- world]] @@ -721,7 +721,7 @@ type foo = {} end, assert) end) - suite:case("typefunction_trivia", function(assert) + suite:case("typeFunctionTrivia", function(assert) local source = [[-- hello type foo = (number) -> () -- world]] @@ -734,7 +734,7 @@ type foo = (number) -> () end, assert) end) - suite:case("type_trivia", function(assert) + suite:case("typeTrivia", function(assert) local source = [[-- hello type foo = (number) -> () -- world]] @@ -747,7 +747,7 @@ type foo = (number) -> () end, assert) end) - suite:case("typepackexplicit_trivia", function(assert) + suite:case("typePackExplicitTrivia", function(assert) local source = [[-- hello type foo = (number) -> (number, ...string) -- world]] @@ -760,7 +760,7 @@ type foo = (number) -> (number, ...string) end, assert) end) - suite:case("typepackgeneric_trivia", function(assert) + suite:case("typePackGenericTrivia", function(assert) local source = [[-- hello type foo = (number) -> (number, G...) -- world]] @@ -773,7 +773,7 @@ type foo = (number) -> (number, G...) end, assert) end) - suite:case("typepackvariadic_trivia", function(assert) + suite:case("typePackVariadicTrivia", function(assert) local source = [[-- hello type foo = (number) -> (...number) -- world]] @@ -786,7 +786,7 @@ type foo = (number) -> (...number) end, assert) end) - suite:case("typepack_trivia", function(assert) + suite:case("typePackTrivia", function(assert) local source = [[-- hello type foo = (number) -> (number, ...string) -- world]] @@ -799,7 +799,7 @@ type foo = (number) -> (number, ...string) end, assert) end) - suite:case("local_trivia", function(assert) + suite:case("localTrivia", function(assert) local source = [[-- hello local x, y = 1, 2 -- world]] @@ -812,7 +812,7 @@ local x, y = 1, 2 end, assert) end) - suite:case("attribute_trivia", function(assert) + suite:case("attributeTrivia", function(assert) local source = [[-- hello @checked function foo() return end @@ -826,7 +826,7 @@ function foo() return end end, assert) end) - suite:case("comments_in_call", function(assert) + suite:case("commentsInCall", function(assert) local source = [[foobar( foo, -- what about this bar, -- and this @@ -847,7 +847,7 @@ function foo() return end end, assert) end) - suite:case("comments_in_table", function(assert) + suite:case("commentsInTable", function(assert) local source = [[local x = { foo = -- what about this 1, diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index e1fd6a396..a7d33fb85 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -72,7 +72,7 @@ test.suite("AstQuery", function(suite) assert.eq(sum, 3) -- 0 + 1 + 2 = 3 end) - suite:case("findall_nums", function(assert) + suite:case("findallNums", function(assert) local ast = syntax.parse("print(1 + 2)") local queryResult: query.query = query.findallfromroot( ast, -- LUAUFIX: Complains that AstStatBlock doesn't have location? field @@ -95,7 +95,7 @@ test.suite("AstQuery", function(suite) assert.eq((num :: syntax.AstExprConstantNumber).value, 2) end) - suite:case("findall_utils", function(assert) + suite:case("findallUtils", function(assert) local ast = syntax.parse("print(1 + 2)") local queryResult: query.query = query.findallfromroot(ast, utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field @@ -111,7 +111,7 @@ test.suite("AstQuery", function(suite) assert.eq((num :: syntax.AstExprConstantNumber).value, 2) end) - suite:case("findall_tokens", function(assert) + suite:case("findallTokens", function(assert) local ast = syntax.parse("local x = 1 or nil") -- verify that query doesn't doubly count tokens local queryResult = query.findallfromroot(ast, utils.isToken) @@ -175,7 +175,7 @@ test.suite("AstQuery", function(suite) assert.eq(locals.nodes[2].tag, "binary") end) - suite:case("flatmap_empty", function(assert) + suite:case("flatmapEmpty", function(assert) -- Test flatmap with some nodes returning empty arrays local ast = syntax.parse("local x; local y = 1; local z") local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l) @@ -192,7 +192,7 @@ test.suite("AstQuery", function(suite) assert.eq((locals.nodes[1] :: syntax.AstExprConstantNumber).value, 1) end) - suite:case("flatmap_multiple", function(assert) + suite:case("flatmapMultiple", function(assert) -- Test flatmap with nodes that return multiple items local ast = syntax.parse("local a, b = 1, 2; local c, d = 3, 4, 5") local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l: syntax.AstStatLocal) @@ -227,7 +227,7 @@ test.suite("AstQuery", function(suite) assert.eq(doubled[5], 8) end) - suite:case("maptoarray_with_nil", function(assert) + suite:case("maptoarrayWithNil", function(assert) -- Test maptoarray filtering out nil values local ast = syntax.parse("local _ = {0, 1, 2, 3, 4}") local nums = query.findallfromroot(ast, utils.isExprConstantNumber) diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index 75951d181..e14450006 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -2,7 +2,7 @@ local tableext = require("@std/tableext") local test = require("@std/test") test.suite("TableExt", function(suite) - suite:case("map_and_filter", function(assert) + suite:case("mapAndFilter", function(assert) local t: { [string]: number } = { a = 1, b = 2, c = 3 } local mapped = tableext.map(t, function(v: number) return v * 2 diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index dc5a828c4..74f3e046b 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -26,7 +26,7 @@ local function assertErrorsWithMsg( end test.suite("LuteStdTestFramework", function(suite) - suite:case("lute_doesnt_report_xpcall_as_error_when_accessing_field_of_nil_in_suite", function(assert) + suite:case("luteDoesntReportXpcallAsErrorWhenAccessingFieldOfNilInSuite", function(assert) -- Setup local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") fs.writestringtofile( @@ -35,7 +35,7 @@ test.suite("LuteStdTestFramework", function(suite) local test = require("@std/test") test.suite("nil_field_suite", function(suite) - suite:case("access_field_of_nil", function(assert) + suite:case("accessFieldOfNil", function(assert) local t = nil local value = t.field -- This will cause an error end) @@ -62,7 +62,7 @@ test.run() fs.remove(testFilePath) end) - suite:case("lute_doesnt_report_xpcall_as_error_when_accessing_field_of_nil_in_case", function(assert) + suite:case("luteDoesntReportXpcallAsErrorWhenAccessingFieldOfNilInCase", function(assert) -- Setup local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") fs.writestringtofile( @@ -70,7 +70,7 @@ test.run() [[ local test = require("@std/test") -test.case("access_field_of_nil", function(assert) +test.case("accessFieldOfNil", function(assert) local t = nil local value = t.field -- This will cause an error end) @@ -97,13 +97,13 @@ test.run() fs.remove(testFilePath) end) - suite:case("assert.eq_error_message_in_case", function(assert) + suite:case("assert.eqErrorMessageInCase", function(assert) assertErrorsWithMsg( "assert_eq_fails", [[ local test = require("@std/test") -test.case("eq_error", function(assert) +test.case("eqError", function(assert) assert.eq(1, 2) end) @@ -114,14 +114,14 @@ test.run() ) end) - suite:case("assert.eq_error_message_in_suite", function(assert) + suite:case("assert.eqErrorMessageInSuite", function(assert) assertErrorsWithMsg( "assert_eq_fails", [[ local test = require("@std/test") test.suite("eq_failure_suite", function(suite) - suite:case("eq_error", function(assert) + suite:case("eqError", function(assert) assert.eq(1, 2) end) end) @@ -133,13 +133,13 @@ test.run() ) end) - suite:case("assert.neq_error_message_in_case", function(assert) + suite:case("assert.neqErrorMessageInCase", function(assert) assertErrorsWithMsg( "assert_neq_fails", [[ local test = require("@std/test") -test.case("neq_error", function(assert) +test.case("neqError", function(assert) assert.neq(1, 1) end) @@ -150,14 +150,14 @@ test.run() ) end) - suite:case("assert.neq_error_message_in_suite", function(assert) + suite:case("assert.neqErrorMessageInSuite", function(assert) assertErrorsWithMsg( "assert_neq_fails", [[ local test = require("@std/test") test.suite("neq_failure_suite", function(suite) - suite:case("neq_error", function(assert) + suite:case("neqError", function(assert) assert.neq(1, 1) end) end) @@ -169,13 +169,13 @@ test.run() ) end) - suite:case("assert.tableeq_error_message_in_case", function(assert) + suite:case("assert.tableeqErrorMessageInCase", function(assert) assertErrorsWithMsg( "assert_tableeq_fails", [[ local test = require("@std/test") -test.case("tableeq_error", function(assert) +test.case("tableeqError", function(assert) assert.tableeq({ a = 1 }, { a = 2 }) end) @@ -186,14 +186,14 @@ test.run() ) end) - suite:case("assert.tableeq_error_message_in_suite", function(assert) + suite:case("assert.tableeqErrorMessageInSuite", function(assert) assertErrorsWithMsg( "assert_tableeq_fails", [[ local test = require("@std/test") test.suite("tableeq_failure_suite", function(suite) - suite:case("tableeq_error", function(assert) + suite:case("tableeqError", function(assert) assert.tableeq({ a = 1 }, { a = 2 }) end) end) @@ -205,13 +205,13 @@ test.run() ) end) - suite:case("assert.buffereq_unequal_length", function(assert) + suite:case("assert.buffereqUnequalLength", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") -test.case("buffereq_error", function(assert) +test.case("buffereqError", function(assert) local buf1 = buffer.create(1) local buf2 = buffer.create(2) assert.buffereq(buf1, buf2) @@ -224,14 +224,14 @@ test.run() ) end) - suite:case("assert.buffereq_error_message_in_suite", function(assert) + suite:case("assert.buffereqErrorMessageInSuite", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") test.suite("buffereq_failure_suite", function(suite) - suite:case("buffereq_error", function(assert) + suite:case("buffereqError", function(assert) local buf1 = buffer.create(2) local buf2 = buffer.create(2) buffer.writeu8(buf1, 0, 84) @@ -247,13 +247,13 @@ test.run() ) end) - suite:case("assert.erroreq_no_error", function(assert) + suite:case("assert.erroreqNoError", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") -test.case("erroreq_error", function(assert) +test.case("erroreqError", function(assert) assert.erroreq(function() return end, "expected message") end) @@ -264,14 +264,14 @@ test.run() ) end) - suite:case("assert.erroreq_error_message", function(assert) + suite:case("assert.erroreqErrorMessage", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") test.suite("erroreq_failure_suite", function(suite) - suite:case("erroreq_error", function(assert) + suite:case("erroreqError", function(assert) assert.erroreq(function() error("wrong message") end, "expected message") end) end) @@ -283,13 +283,13 @@ test.run() ) end) - suite:case("assert.strcontains_instances_negative", function(assert) + suite:case("assert.strcontainsInstancesNegative", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") -test.case("strcontains_error", function(assert) +test.case("strcontainsError", function(assert) assert.strcontains("hello world", "goodbye", nil, -1) end) @@ -300,14 +300,14 @@ test.run() ) end) - suite:case("assert.strcontains_single_instance_fails", function(assert) + suite:case("assert.strcontainsSingleInstanceFails", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") test.suite("strcontains_failure_suite", function(suite) - suite:case("strcontains_error", function(assert) + suite:case("strcontainsError", function(assert) assert.strcontains("hello world", "goodbye") end) end) @@ -319,14 +319,14 @@ test.run() ) end) - suite:case("assert.strcontains_multiple_instance_fails", function(assert) + suite:case("assert.strcontainsMultipleInstanceFails", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") test.suite("strcontains_failure_suite", function(suite) - suite:case("strcontains_error", function(assert) + suite:case("strcontainsError", function(assert) assert.strcontains("muahahahaha", "ha", nil, 5) end) end) diff --git a/tests/std/time.test.luau b/tests/std/time.test.luau index 48f05443b..5644d2209 100644 --- a/tests/std/time.test.luau +++ b/tests/std/time.test.luau @@ -4,7 +4,7 @@ local time = require("@std/time") local duration = time.duration test.suite("TimeSuite", function(suite) - suite:case("constructors_seconds_millis_micros_nanos", function(assert) + suite:case("constructorsSecondsMillisMicroNanos", function(assert) -- seconds local d1 = duration.seconds(5) assert.eq(d1:tomilliseconds(), 5000) @@ -36,7 +36,7 @@ test.suite("TimeSuite", function(suite) assert.eq(d5:subsecnanos(), 0) end) - suite:case("constructors_minutes_hours_days_weeks", function(assert) + suite:case("constructorsMinutesHoursDaysWeeks", function(assert) local mins = duration.minutes(3) assert.eq(mins:toseconds(), 180) @@ -50,7 +50,7 @@ test.suite("TimeSuite", function(suite) assert.eq(weeks:toseconds(), 2 * 7 * 24 * 60 * 60) end) - suite:case("create_raw_values_and_normalization_via_ops", function(assert) + suite:case("createRawValuesAndNormalizationViaOps", function(assert) -- create keeps raw values without normalization local d = duration.create(1, 500_000_000) assert.eq(d:toseconds(), 1.5) @@ -68,7 +68,7 @@ test.suite("TimeSuite", function(suite) assert.eq(normalized:subsecnanos(), 500_000_000) end) - suite:case("subsecond_accessors", function(assert) + suite:case("subsecondAccessors", function(assert) local d = duration.milliseconds(987) assert.eq(d:subsecmillis(), 987) @@ -79,7 +79,7 @@ test.suite("TimeSuite", function(suite) assert.eq(d3:subsecnanos(), 123_456_789 % 1_000_000_000) end) - suite:case("conversions_to_ms_us_ns", function(assert) + suite:case("conversionsToMsUsNs", function(assert) local d = duration.seconds(2) assert.eq(d:tomilliseconds(), 2000) assert.eq(d:tomicroseconds(), 2_000_000) @@ -92,7 +92,7 @@ test.suite("TimeSuite", function(suite) assert.eq(mix:tonanoseconds(), 1_801_000_890) end) - suite:case("addition_normalizes_nanoseconds", function(assert) + suite:case("additionNormalizesNanoseconds", function(assert) local a = duration.milliseconds(800) local b = duration.milliseconds(300) local c = a + b @@ -105,7 +105,7 @@ test.suite("TimeSuite", function(suite) assert.eq(c2:subsecnanos(), 100_000_000) end) - suite:case("subtraction_borrows_nanoseconds", function(assert) + suite:case("subtractionBorrowsNanoseconds", function(assert) local a = duration.milliseconds(1100) -- 1s 100ms local b = duration.milliseconds(200) local c = a - b -- 900ms after borrow @@ -115,7 +115,7 @@ test.suite("TimeSuite", function(suite) assert.eq(d:tomilliseconds(), 0) end) - suite:case("multiplication_and_division", function(assert) + suite:case("multiplicationAndDivision", function(assert) local a = duration.milliseconds(250) local twice = a * 2 assert.eq(twice:tomilliseconds(), 500) @@ -127,7 +127,7 @@ test.suite("TimeSuite", function(suite) assert.eq(c:tomilliseconds(), 1000) end) - suite:case("comparisons_and_equality", function(assert) + suite:case("comparisonsAndEquality", function(assert) local a = duration.seconds(1) local b = duration.milliseconds(1000) local c = duration.milliseconds(999) @@ -140,7 +140,7 @@ test.suite("TimeSuite", function(suite) assert.eq(a < b, false) end) - suite:case("tostring_format", function(assert) + suite:case("tostringFormat", function(assert) local a = duration.seconds(1) assert.eq(tostring(a), "1.000000000") From 0987045d39913688f0139613e84d3af55893469d Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 11 Mar 2026 14:13:24 -0700 Subject: [PATCH 394/642] Modifies `assert.strcontains` so that specifying 1 instance asserts for exactly 1 instance (#864) Previous behavior was to check for at least one instance of the specified substring, which was a bit misleading. --- lute/std/libs/test/assert.luau | 21 +++++++++++-------- lute/std/libs/test/types.luau | 2 +- tests/cli/lint.test.luau | 12 +++++------ ...t_strcontains_instances_negative.snap.luau | 2 +- tests/std/test.test.luau | 4 ++-- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index 16f8df0b4..f8f28825c 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -138,22 +138,25 @@ local function erroreq(func: (A...) -> ...unknown, expectedErrorMessage: s return nil end -local function strcontains(haystack: string, needle: string, msg: string?, instances: number?): failure? +local function strcontains(haystack: string, needle: string, instances: number?, msg: string?): failure? + if instances == nil then + if haystack:find(needle, 1, true) == nil then + return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) + end + + return nil + end + instances = instances or 1 if instances <= 0 then return assertion("instances must be greater than 0") end - if instances == 1 then - if haystack:find(needle, 1, true) == nil then - return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) - end - else - if #haystack:split(needle) ~= instances + 1 then - return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.`) - end + if #haystack:split(needle) ~= instances + 1 then + return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}" {instances} times.`) end + return nil end diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index 06293724f..fd7478484 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -35,7 +35,7 @@ export type asserts = { tableeq: ({ [any]: any }, { [any]: any }) -> failure?, buffereq: (buffer, buffer) -> failure?, erroreq: ((A...) -> ...unknown, string, A...) -> failure?, - strcontains: (string, string, string?, number?) -> failure?, + strcontains: (string, string, number?, string?) -> failure?, strnotcontains: (string, string, string?) -> failure?, } diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index f5c556203..4347342df 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -42,7 +42,7 @@ type lintTestParams = { disableDefaultLints: true?, configContents: string?, extraOptions: { string }?, - outputExpectations: { { expectedOutput: string, count: number } }?, + outputExpectations: { { expectedOutput: string, count: number? } }?, } local function lintTestHelper(assert: testTypes.asserts, params: lintTestParams): string @@ -103,7 +103,7 @@ local function lintTestHelper(assert: testTypes.asserts, params: lintTestParams) if expectation.count == 0 then assert.strnotcontains(result.stdout, expectation.expectedOutput) else - assert.strcontains(result.stdout, expectation.expectedOutput, nil, expectation.count) + assert.strcontains(result.stdout, expectation.expectedOutput, expectation.count) end end end @@ -993,7 +993,7 @@ local y = 10 / 0 -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero, nil, 1) + assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero, 1) local expected = [[ violator1.luau:1:11-17 ── │ @@ -3028,10 +3028,10 @@ return { assert.eq(result.exitcode, 1) -- Both files should still report almost_swapped (the non-ignored rule) - assert.strcontains(result.stdout, "[almost_swapped]", nil, 2) + assert.strcontains(result.stdout, "[almost_swapped]", 2) -- Only 1 divide_by_zero: from the non-ignored file, not from ignoredDir - assert.strcontains(result.stdout, "[divide_by_zero]", nil, 1) + assert.strcontains(result.stdout, "[divide_by_zero]", 1) assert.strcontains(result.stdout, "nonIgnored.luau:1:11-16") -- the non-ignored file's violation end) @@ -3100,7 +3100,7 @@ return { expectedExitCode = 1, customRuleContents = customRuleContents, disableDefaultLints = true, - outputExpectations = { { expectedOutput = "customRule", count = 1 } }, + outputExpectations = { { expectedOutput = "customRule" } }, }) -- confirm file doesn't violate with disabled rule config diff --git a/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau b/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau index cf716e4ce..60ce657bf 100644 --- a/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau +++ b/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau @@ -1,7 +1,7 @@ local test = require("@std/test") test.case("strcontains_error", function(assert) - assert.strcontains("hello world", "goodbye", nil, -1) + assert.strcontains("hello world", "goodbye", -1) end) test.run() diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index 74f3e046b..e314b9f28 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -290,7 +290,7 @@ test.run() local test = require("@std/test") test.case("strcontainsError", function(assert) - assert.strcontains("hello world", "goodbye", nil, -1) + assert.strcontains("hello world", "goodbye", -1) end) test.run() @@ -327,7 +327,7 @@ local test = require("@std/test") test.suite("strcontains_failure_suite", function(suite) suite:case("strcontainsError", function(assert) - assert.strcontains("muahahahaha", "ha", nil, 5) + assert.strcontains("muahahahaha", "ha", 5) end) end) From 6c0514a3a4f43dafd621843577c5bc3b6805b625 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 12 Mar 2026 09:27:22 -0700 Subject: [PATCH 395/642] Renames `loadbypath` API to `loadModule` (#875) journey to shipping Lute V1: resolves [CLI-192826 ](https://roblox.atlassian.net/browse/CLI-192826) --- lute/cli/commands/lint/init.luau | 4 ++-- lute/cli/commands/test/init.luau | 2 +- lute/cli/commands/transform/init.luau | 2 +- lute/std/libs/luau.luau | 2 +- tests/cli/{loadbypath.test.luau => loadmodule.test.luau} | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename tests/cli/{loadbypath.test.luau => loadmodule.test.luau} (94%) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index b12a98b95..c93b1bda2 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -91,7 +91,7 @@ local function isLintRule(rule: unknown, path: string): boolean end local function loadLintRule(path: string): types.LintRule? - local loaded = luau.loadbypath(path) + local loaded = luau.loadModule(path) return if isLintRule(loaded, path) then loaded else nil -- Typecast isn't needed because loaded is refined to any bc of type error in isLintRule end @@ -514,7 +514,7 @@ local function main(...: string) end if fs.exists(configPath) then - local success, loadedConfig = pcall(luau.loadbypath, configPath) + local success, loadedConfig = pcall(luau.loadModule, configPath) if success then lintConfig = configUtils.extractConfig(loadedConfig) elseif VERBOSE then diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index 8e1b595c1..a5c751fdc 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -14,7 +14,7 @@ local updater = require("@self/snap/updater") local function loadtests(testfiles: { path.path }) -- Load all test files first for _, p in testfiles do - local success, err = pcall(luau.loadbypath, p, nil) + local success, err = pcall(luau.loadModule, p, nil) if not success then print(`Error loading {path.format(p)}: {err}`) diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index 369ccf582..7bfcf7f51 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -13,7 +13,7 @@ local function exhaustiveMatch(value: never): never end local function loadMigration(path: string): types.Migration - local success, loaded = pcall(luau.loadbypath, path, nil) + local success, loaded = pcall(luau.loadModule, path, nil) assert(success, `{path} failed to require: {loaded}`) assert(loaded, `{path} is missing a return`) diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 909146a02..73eaea552 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -20,7 +20,7 @@ function luau.load(bytecode: bytecode, chunkname: string?, env: { [any]: any }?) return luteLuau.load(bytecode, if chunkname ~= nil then `@{chunkname}` else "=luau.load", env) end -function luau.loadbypath(requirePath: path.pathlike, env: { [any]: any }?): any +function luau.loadModule(requirePath: path.pathlike, env: { [any]: any }?): any local requirePathStr = if typeof(requirePath) == "string" then requirePath else path.format(requirePath) local migrationHandle = fs.open(requirePathStr, "r") diff --git a/tests/cli/loadbypath.test.luau b/tests/cli/loadmodule.test.luau similarity index 94% rename from tests/cli/loadbypath.test.luau rename to tests/cli/loadmodule.test.luau index 7d52b6537..454255569 100644 --- a/tests/cli/loadbypath.test.luau +++ b/tests/cli/loadmodule.test.luau @@ -9,7 +9,7 @@ local args = { ... } assert(#args == 2, "Expected one argument: path to Luau script to require") local luau = require("@std/luau") -local result = luau.loadbypath(args[2]) +local result = luau.loadModule(args[2]) print(result) ]] @@ -28,7 +28,7 @@ test.suite("LuteCliRun", function(suite) end end) - suite:case("loadbypath", function(check) + suite:case("loadModule", function(check) -- Setup -- Create files local requirerPath = path.format(path.join(testDir, "requirer.luau")) From 6c347614b0d4755800811dd3203134706c9f2f4f Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:32:28 -0700 Subject: [PATCH 396/642] Renames `resolverequire` to `resolveModule` and return `path` instead of `string` (#876) --- definitions/luau.luau | 2 +- lute/cli/include/lute/staticrequires.h | 2 +- lute/cli/src/staticrequires.cpp | 4 ++-- lute/cli/src/tc.cpp | 4 ++-- lute/luau/CMakeLists.txt | 8 ++++---- lute/luau/include/lute/luau.h | 4 ++-- lute/luau/include/lute/resolvemodule.h | 9 +++++++++ lute/luau/include/lute/resolverequire.h | 9 --------- .../lute/{moduleresolver.h => tcmoduleresolver.h} | 4 ++-- lute/luau/src/luau.cpp | 4 ++-- .../luau/src/{resolverequire.cpp => resolvemodule.cpp} | 8 ++++---- .../src/{moduleresolver.cpp => tcmoduleresolver.cpp} | 10 +++++----- lute/std/libs/luau.luau | 4 ++-- tests/src/moduleresolver.test.cpp | 6 +++--- tests/std/luau.test.luau | 6 +++--- 15 files changed, 42 insertions(+), 42 deletions(-) create mode 100644 lute/luau/include/lute/resolvemodule.h delete mode 100644 lute/luau/include/lute/resolverequire.h rename lute/luau/include/lute/{moduleresolver.h => tcmoduleresolver.h} (83%) rename lute/luau/src/{resolverequire.cpp => resolvemodule.cpp} (93%) rename lute/luau/src/{moduleresolver.cpp => tcmoduleresolver.cpp} (65%) diff --git a/definitions/luau.luau b/definitions/luau.luau index 5ae567889..daf9e5d38 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -716,7 +716,7 @@ function luau.load(bytecode: Bytecode, chunkname: string, env: { [any]: any }?): error("not implemented") end -function luau.resolverequire(path: string, fromchunkname: string): string +function luau.resolveModule(path: string, fromchunkname: string): string error("not implemented") end diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h index d7653eac9..6bb21dbbb 100644 --- a/lute/cli/include/lute/staticrequires.h +++ b/lute/cli/include/lute/staticrequires.h @@ -51,6 +51,6 @@ class StaticRequireTracer std::vector extractRequires(const std::string& source); // Resolve a require path relative to the requiring file - std::optional resolveRequire(const std::string& requirer, const std::string& required); + std::optional resolveModule(const std::string& requirer, const std::string& required); LuteReporter& reporter; }; diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index cd7dd4031..48e687b47 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -1,7 +1,7 @@ #include "lute/staticrequires.h" #include "lute/modulepath.h" -#include "lute/resolverequire.h" +#include "lute/resolvemodule.h" #include "lute/staticrequires.h" #include "Luau/Ast.h" @@ -116,7 +116,7 @@ void StaticRequireTracer::trace(const std::string& entryPoint) if (req.find("@std/") == 0 || req.find("@lute/") == 0) continue; std::string err = ""; - std::optional resolvedPath = ::resolveRequire(req, "@" + filePath, &err); + std::optional resolvedPath = ::resolveModule(req, "@" + filePath, &err); if (resolvedPath) { diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index 002282fba..77272aff5 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -1,14 +1,14 @@ #include "lute/tc.h" #include "lute/configresolver.h" -#include "lute/moduleresolver.h" +#include "lute/tcmoduleresolver.h" #include "Luau/BuiltinDefinitions.h" #include "Luau/Error.h" #include "Luau/FileUtils.h" #include "Luau/Frontend.h" -struct LuteFileResolver : Luau::LuteModuleResolver +struct LuteFileResolver : Luau::LuteTypeCheckModuleResolver { std::optional readSource(const Luau::ModuleName& name) override { diff --git a/lute/luau/CMakeLists.txt b/lute/luau/CMakeLists.txt index ff5fc50a9..3d69a2570 100644 --- a/lute/luau/CMakeLists.txt +++ b/lute/luau/CMakeLists.txt @@ -3,14 +3,14 @@ add_library(Lute.Luau STATIC) target_sources(Lute.Luau PRIVATE include/lute/configresolver.h include/lute/luau.h - include/lute/moduleresolver.h - include/lute/resolverequire.h + include/lute/tcmoduleresolver.h + include/lute/resolvemodule.h include/lute/type.h src/configresolver.cpp src/luau.cpp - src/moduleresolver.cpp - src/resolverequire.cpp + src/tcmoduleresolver.cpp + src/resolvemodule.cpp src/type.cpp ) diff --git a/lute/luau/include/lute/luau.h b/lute/luau/include/lute/luau.h index 204f23c60..08c8f3f77 100644 --- a/lute/luau/include/lute/luau.h +++ b/lute/luau/include/lute/luau.h @@ -1,6 +1,6 @@ #pragma once -#include "lute/resolverequire.h" +#include "lute/resolvemodule.h" #include "lua.h" #include "lualib.h" @@ -32,7 +32,7 @@ static const luaL_Reg lib[] = { {"parseexpr", luau_parseexpr}, {"compile", compile_luau}, {"load", load_luau}, - {"resolverequire", resolverequire_luau}, + {"resolveModule", resolveModule_luau}, {"typeofmodule", typeofmodule_luau}, {nullptr, nullptr}, }; diff --git a/lute/luau/include/lute/resolvemodule.h b/lute/luau/include/lute/resolvemodule.h new file mode 100644 index 000000000..2292847ac --- /dev/null +++ b/lute/luau/include/lute/resolvemodule.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +struct lua_State; + +std::optional resolveModule(std::string requirePath, std::string requirerChunkname, std::string* error); +int resolveModule_luau(lua_State* L); diff --git a/lute/luau/include/lute/resolverequire.h b/lute/luau/include/lute/resolverequire.h deleted file mode 100644 index 62134700e..000000000 --- a/lute/luau/include/lute/resolverequire.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include -#include - -struct lua_State; - -std::optional resolveRequire(std::string requirePath, std::string requirerChunkname, std::string* error); -int resolverequire_luau(lua_State* L); diff --git a/lute/luau/include/lute/moduleresolver.h b/lute/luau/include/lute/tcmoduleresolver.h similarity index 83% rename from lute/luau/include/lute/moduleresolver.h rename to lute/luau/include/lute/tcmoduleresolver.h index c023346fd..3e6b3765d 100644 --- a/lute/luau/include/lute/moduleresolver.h +++ b/lute/luau/include/lute/tcmoduleresolver.h @@ -6,9 +6,9 @@ namespace Luau { // Based on CliFileResolver in Analyze.cpp. -struct LuteModuleResolver : Luau::FileResolver +struct LuteTypeCheckModuleResolver : Luau::FileResolver { - LuteModuleResolver() = default; + LuteTypeCheckModuleResolver() = default; std::optional readSource(const Luau::ModuleName& name) override; diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index fa3e26848..9bfa77ecf 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -2,7 +2,7 @@ #include "lute/common.h" #include "lute/configresolver.h" -#include "lute/moduleresolver.h" +#include "lute/tcmoduleresolver.h" #include "lute/type.h" #include "Luau/Ast.h" @@ -2972,7 +2972,7 @@ int typeofmodule_luau(lua_State* L) { std::string modulePath = luaL_checkstring(L, 1); - Luau::LuteModuleResolver moduleResolver; + Luau::LuteTypeCheckModuleResolver moduleResolver; Luau::LuteConfigResolver configResolver(Luau::Mode::NoCheck); Luau::FrontendOptions fopts; fopts.retainFullTypeGraphs = true; diff --git a/lute/luau/src/resolverequire.cpp b/lute/luau/src/resolvemodule.cpp similarity index 93% rename from lute/luau/src/resolverequire.cpp rename to lute/luau/src/resolvemodule.cpp index 1c0e2b2fc..766c6e152 100644 --- a/lute/luau/src/resolverequire.cpp +++ b/lute/luau/src/resolvemodule.cpp @@ -1,4 +1,4 @@ -#include "lute/resolverequire.h" +#include "lute/resolvemodule.h" #include "lute/filevfs.h" #include "lute/modulepath.h" @@ -133,7 +133,7 @@ void ErrorCapturer::reportError(std::string message) } // Public API -std::optional resolveRequire(std::string requirePath, std::string requirerChunkname, std::string* error) +std::optional resolveModule(std::string requirePath, std::string requirerChunkname, std::string* error) { if (requirerChunkname.empty() || requirerChunkname[0] != '@') { @@ -159,13 +159,13 @@ std::optional resolveRequire(std::string requirePath, std::string r return absolutePath; } -int resolverequire_luau(lua_State* L) +int resolveModule_luau(lua_State* L) { std::string requirePath = luaL_checkstring(L, 1); std::string requirerChunkname = luaL_checkstring(L, 2); std::string error; - std::optional absolutePath = resolveRequire(requirePath, requirerChunkname, &error); + std::optional absolutePath = resolveModule(requirePath, requirerChunkname, &error); if (!absolutePath) luaL_error(L, "%s", error.c_str()); diff --git a/lute/luau/src/moduleresolver.cpp b/lute/luau/src/tcmoduleresolver.cpp similarity index 65% rename from lute/luau/src/moduleresolver.cpp rename to lute/luau/src/tcmoduleresolver.cpp index 0f141b1e7..0ada7889c 100644 --- a/lute/luau/src/moduleresolver.cpp +++ b/lute/luau/src/tcmoduleresolver.cpp @@ -1,6 +1,6 @@ -#include "lute/moduleresolver.h" +#include "lute/tcmoduleresolver.h" -#include "lute/resolverequire.h" +#include "lute/resolvemodule.h" #include "Luau/Ast.h" #include "Luau/FileUtils.h" @@ -8,7 +8,7 @@ namespace Luau { -std::optional LuteModuleResolver::readSource(const Luau::ModuleName& name) +std::optional LuteTypeCheckModuleResolver::readSource(const Luau::ModuleName& name) { if (std::optional source = readFile(name)) return Luau::SourceCode{*source, Luau::SourceCode::Module}; @@ -16,7 +16,7 @@ std::optional LuteModuleResolver::readSource(const Luau::Modul } // We are currently resolving modules and requires only, and will add support for Roblox globals / types in a subsequent PR. -std::optional LuteModuleResolver::resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const TypeCheckLimits& limits) +std::optional LuteTypeCheckModuleResolver::resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const TypeCheckLimits& limits) { if (auto expr = node->as()) { @@ -24,7 +24,7 @@ std::optional LuteModuleResolver::resolveModule(const Luau::Mo std::string error; std::string requirerChunkname = "@" + context->name; - std::optional absolutePath = resolveRequire(requirePath, std::move(requirerChunkname), &error); + std::optional absolutePath = ::resolveModule(requirePath, std::move(requirerChunkname), &error); if (!absolutePath) { printf("Failed to resolve require: %s\n", error.c_str()); diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 73eaea552..f0628487b 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -40,9 +40,9 @@ function luau.typeofmodule(modulepath: path.pathlike): TypePack? return luteLuau.typeofmodule(modulePathStr) end -function luau.resolverequire(modulepath: string, frompath: path.pathlike): string +function luau.resolveModule(modulepath: string, frompath: path.pathlike): path.pathlike local frompathString = if typeof(frompath) == "string" then frompath else path.format(frompath) - return luteLuau.resolverequire(modulepath, `@{frompathString}`) + return luteLuau.resolveModule(modulepath, `@{frompathString}`) end return table.freeze(luau) diff --git a/tests/src/moduleresolver.test.cpp b/tests/src/moduleresolver.test.cpp index ea3f991be..5338b2af9 100644 --- a/tests/src/moduleresolver.test.cpp +++ b/tests/src/moduleresolver.test.cpp @@ -1,7 +1,7 @@ // Simplified tests for module resolver functionality using public APIs. -#include "lute/moduleresolver.h" +#include "lute/tcmoduleresolver.h" -#include "lute/resolverequire.h" +#include "lute/resolvemodule.h" #include "Luau/Ast.h" #include "Luau/FileUtils.h" @@ -15,7 +15,7 @@ TEST_CASE("moduleresolver_read_source") { std::string root = getLuteProjectRootAbsolute(); std::string file = joinPaths(root, "tests/src/resolver/mainmodule.luau"); - Luau::LuteModuleResolver resolver; + Luau::LuteTypeCheckModuleResolver resolver; auto source = resolver.readSource(file); REQUIRE(source); CHECK(source->type == Luau::SourceCode::Module); diff --git a/tests/std/luau.test.luau b/tests/std/luau.test.luau index 301ca7c1c..43b2c47a3 100644 --- a/tests/std/luau.test.luau +++ b/tests/std/luau.test.luau @@ -149,12 +149,12 @@ test.suite("LuauTypeOfModuleSuite", function(suite) assert.eq(funcType.returns.type.tag, "string") end) - suite:case("resolverequire", function(assert) - local testPath = path.join(tmpDir, "resolverequire_test.luau") + suite:case("resolveModule", function(assert) + local testPath = path.join(tmpDir, "resolvemodule_test.luau") local testFile = fs.open(testPath, "w+") fs.write(testFile, 'print("Hello, World!")') - local resolvedPath = luau.resolverequire("@self", testPath) + local resolvedPath = luau.resolveModule("@self", testPath) fs.close(testFile) local expected = path.format(testPath):gsub("\\", "/") From 6d6659e601dc6ef930c34826f79507560bf374fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:26:37 -0700 Subject: [PATCH 397/642] Update Luau to 0.712 (#879) --- extern/luau.tune | 4 ++-- lute/cli/src/luauflags.cpp | 4 ++++ lute/cli/src/tc.cpp | 3 +-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 1991d9162..2a99ba791 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.711" -revision = "004d88ff2b05758947ea1d1f4919e7bb2192b880" +branch = "0.712" +revision = "9e2984fd0334414a2ef62ceede0c06dab7574d55" diff --git a/lute/cli/src/luauflags.cpp b/lute/cli/src/luauflags.cpp index a054d3689..5b2076e2b 100644 --- a/lute/cli/src/luauflags.cpp +++ b/lute/cli/src/luauflags.cpp @@ -32,6 +32,10 @@ static void enableAllLuauFlags() void setLuauFlags() { enableAllLuauFlags(); + // This flag causes a regression in typechecking, which means that + // the canonical test we use to figure out if you're in the new solver doesn't work correctly + // We'll disable this until it gets fixed in the next release. + setLuauFlag("LuauOverloadGetsInstantiated", false); // Individual flags can be overridden here as needed, e.g.: // setLuauFlag("LuauSomeFlagThatCausedARegression", false); diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index 77272aff5..c6c6d775e 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -160,8 +160,7 @@ int typecheck(const std::vector& sourceFilesInput, LuteReporter& re LuteFileResolver fileResolver; Luau::LuteConfigResolver configResolver(mode); - Luau::Frontend frontend(&fileResolver, &configResolver, frontendOptions); - frontend.setLuauSolverMode(Luau::SolverMode::New); + Luau::Frontend frontend(Luau::SolverMode::New, &fileResolver, &configResolver, frontendOptions); Luau::registerBuiltinGlobals(frontend, frontend.globals); Luau::freeze(frontend.globals.globalTypes); From d9325d84806b502d9da26599253229dc22bda33d Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:38:00 -0700 Subject: [PATCH 398/642] Use the new `math.nan` constant instead of `0 / 0` (#883) Updates all existing uses of `0 / 0` in our codebase to `math.nan`, updates the suggested fix during linting, and adds a new lint warning for new uses of `0 / 0`. --- batteries/toml.luau | 3 +-- docs/cli/lint/divide_by_zero.md | 4 ++-- examples/badisnan.luau | 4 ---- lute/cli/commands/lint/rules/divide_by_zero.luau | 6 +++--- tests/cli/lint.test.luau | 6 +++--- 5 files changed, 9 insertions(+), 14 deletions(-) delete mode 100644 examples/badisnan.luau diff --git a/batteries/toml.luau b/batteries/toml.luau index d1a3a08f8..64988da86 100644 --- a/batteries/toml.luau +++ b/batteries/toml.luau @@ -160,8 +160,7 @@ local function deserialize(input: string): { [string]: unknown } elseif value == "-inf" then value = -math.huge elseif value == "nan" then - --lute-lint-ignore(divide_by_zero) - value = 0 / 0 + value = math.nan end currentTable[key] = value diff --git a/docs/cli/lint/divide_by_zero.md b/docs/cli/lint/divide_by_zero.md index 1613e49ce..a5466f671 100644 --- a/docs/cli/lint/divide_by_zero.md +++ b/docs/cli/lint/divide_by_zero.md @@ -10,7 +10,7 @@ This lint rule checks for instances of divison, floor division, or taking the re Division and floor division by 0 will generally give `inf` or `-inf` (unless the dividend is 0). The direct use of `math.huge` or `-math.huge` instead is encouraged. Taking remainder with respect to 0 (i.e. `3 % 0`) will give `NaN`. -We instead encourage the use of `0 / 0` until such time as a `NaN` constant is added to `math`. +We instead encourage the use of `math.nan`. ## Example violations @@ -27,5 +27,5 @@ You should instead do: ```luau local x = math.huge local y = -math.huge -local z = 0 / 0 +local z = math.nan ``` diff --git a/examples/badisnan.luau b/examples/badisnan.luau deleted file mode 100644 index afa20dd27..000000000 --- a/examples/badisnan.luau +++ /dev/null @@ -1,4 +0,0 @@ -return function(n) - --lute-lint-ignore(divide_by_zero) - return n == 0 / 0 -end diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau index bcf705c44..6d581cd0d 100644 --- a/lute/cli/commands/lint/rules/divide_by_zero.luau +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -32,10 +32,10 @@ local function lint( local suggestedfix = nil if n.operator.text == "/" or n.operator.text == "//" then if isZeroLiteral(n.lhsoperand) then - return nil + suggestedfix = table.freeze({ fix = "math.nan" }) elseif n.lhsoperand.tag == "unary" and n.lhsoperand.operator.text == "-" then if isZeroLiteral(n.lhsoperand.operand) then - suggestedfix = table.freeze({ fix = "0 / 0" }) + suggestedfix = table.freeze({ fix = "math.nan" }) else suggestedfix = table.freeze({ fix = "-math.huge" }) end @@ -43,7 +43,7 @@ local function lint( suggestedfix = table.freeze({ fix = "math.huge" }) end elseif n.operator.text == "%" then - suggestedfix = table.freeze({ fix = "0 / 0" }) + suggestedfix = table.freeze({ fix = "math.nan" }) end return { diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 4347342df..16c3e1926 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -206,9 +206,9 @@ local _negnan = -0 / 0 expectedAutofixContents = [[ local _x = math.huge local _y = -math.huge -local _z = 0 / 0 -local _nan = 0 / 0 -local _negnan = 0 / 0 +local _z = math.nan +local _nan = math.nan +local _negnan = math.nan ]], }) end) From 16bfc7e67df5a08fc223e98038fcd65ed47dcaf0 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 18 Mar 2026 09:52:54 -0700 Subject: [PATCH 399/642] Updates AstNode types so that they are never also Tokens (#872) I also added `Token` to the union which defines `AstNode`, and changed its `istoken` field to `kind = "token"`, as this plays nicer with how the type system interacts with tagged unions. Overall, I feel that this aligns our AST types to play nicer with the type system. --- definitions/luau.luau | 50 +++++-- examples/lints/almost_swapped.luau | 2 +- .../commands/lint/rules/almost_swapped.luau | 2 +- .../commands/lint/rules/duplicate_keys.luau | 4 +- .../commands/lint/rules/unused_variable.luau | 17 ++- lute/luau/src/luau.cpp | 102 ++++++++++--- lute/std/libs/syntax/printer.luau | 15 +- lute/std/libs/syntax/query.luau | 10 +- lute/std/libs/syntax/utils/init.luau | 6 +- lute/std/libs/syntax/utils/trivia.luau | 8 +- lute/std/libs/syntax/visitor.luau | 140 +++++++++--------- tests/std/syntax/parser.test.luau | 80 ++++++---- tests/std/syntax/printer.test.luau | 4 +- tests/std/syntax/query.test.luau | 6 +- tools/check-faillist.txt | 70 ++++----- 15 files changed, 300 insertions(+), 216 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index daf9e5d38..475e11c18 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -44,7 +44,7 @@ export type Token = { read location: span, read text: Kind, read trailingtrivia: { Trivia }, - read istoken: true, + read kind: "token", } export type Eof = Token<""> & { read tag: "eof" } @@ -70,28 +70,36 @@ export type AstExprGroup = { read closeparens: Token<")">, } -export type AstExprConstantNil = Token<"nil"> & { read location: span, read kind: "expr", read tag: "nil" } +export type AstExprConstantNil = { + read location: span, + read kind: "expr", + read tag: "nil", + read token: Token<"nil">, +} -export type AstExprConstantBool = Token<"true" | "false"> & { +export type AstExprConstantBool = { read location: span, read kind: "expr", read tag: "boolean", read value: boolean, + read token: Token<"true" | "false">, } -export type AstExprConstantNumber = Token & { +export type AstExprConstantNumber = { read location: span, read kind: "expr", read tag: "number", read value: number, + read token: Token, } -export type AstExprConstantString = Token & { +export type AstExprConstantString = { read location: span, read kind: "expr", read tag: "string", read quotestyle: "single" | "double" | "block" | "interp", read blockdepth: number, + read token: Token, } export type AstExprLocal = { @@ -105,7 +113,12 @@ export type AstExprLocal = { export type AstExprGlobal = { read location: span, read kind: "expr", read tag: "global", read name: Token } -export type AstExprVarargs = Token<"..."> & { read location: span, read kind: "expr", read tag: "vararg" } +export type AstExprVarargs = { + read location: span, + read kind: "expr", + read tag: "vararg", + read token: Token<"...">, +} export type AstExprCall = { read location: span, @@ -350,9 +363,14 @@ export type AstStatRepeat = { read condition: AstExpr, } -export type AstStatBreak = Token<"break"> & { read location: span, read kind: "stat", read tag: "break" } +export type AstStatBreak = { read location: span, read kind: "stat", read tag: "break", read token: Token<"break"> } -export type AstStatContinue = Token<"continue"> & { read location: span, read kind: "stat", read tag: "continue" } +export type AstStatContinue = { + read location: span, + read kind: "stat", + read tag: "continue", + read token: Token<"continue">, +} export type AstStatReturn = { read location: span, @@ -427,7 +445,11 @@ export type AstStatCompoundAssign = { read value: AstExpr, } -export type AstAttribute = Token<"@checked" | "@native" | "@deprecated"> & { read location: span, read kind: "attribute" } +export type AstAttribute = { + read location: span, + read kind: "attribute", + read token: Token<"@checked" | "@native" | "@deprecated">, +} export type AstStatFunction = { read location: span, @@ -519,18 +541,20 @@ export type AstTypeReference = { read closeparameters: Token<">">?, } -export type AstTypeSingletonBool = Token<"true" | "false"> & { +export type AstTypeSingletonBool = { read location: span, read kind: "type", read tag: "boolean", read value: boolean, + read token: Token<"true" | "false">, } -export type AstTypeSingletonString = Token & { +export type AstTypeSingletonString = { read location: span, read kind: "type", read tag: "string", read quotestyle: "single" | "double", + read token: Token, } export type AstTypeTypeof = { @@ -552,7 +576,7 @@ export type AstTypeGroup = { read closeparens: Token<")">, } -export type AstTypeOptional = Token<"?"> & { read location: span, read kind: "type", read tag: "optional" } +export type AstTypeOptional = { read location: span, read kind: "type", read tag: "optional", read token: Token<"?"> } export type AstTypeUnion = { read location: span, @@ -689,7 +713,7 @@ export type AstTypePackVariadic = { export type AstTypePack = AstTypePackExplicit | AstTypePackGeneric | AstTypePackVariadic -export type AstNode = AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute +export type AstNode = AstExpr | AstStat | AstType | AstTypePack | AstLocal | AstAttribute | Token export type ParseResult = { read root: AstStatBlock, diff --git a/examples/lints/almost_swapped.luau b/examples/lints/almost_swapped.luau index 3c6a06acf..d167fc417 100644 --- a/examples/lints/almost_swapped.luau +++ b/examples/lints/almost_swapped.luau @@ -29,7 +29,7 @@ end function compFuncs.exprIndexExprsSame(a: syntax.AstExprIndexExpr, b: syntax.AstExprIndexExpr): boolean if a.index.tag == "string" and b.index.tag == "string" then - if a.index.text ~= b.index.text then + if a.index.token.text ~= b.index.token.text then return false else return compFuncs.refExprsSame(a.expression, b.expression) diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index abef8c78b..c7eaa4745 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -30,7 +30,7 @@ end function compFuncs.exprIndexExprsSame(a: syntax.AstExprIndexExpr, b: syntax.AstExprIndexExpr): boolean if a.index.tag == "string" and b.index.tag == "string" then - if a.index.text ~= b.index.text then + if a.index.token.text ~= b.index.token.text then return false else return compFuncs.refExprsSame(a.expression, b.expression) diff --git a/lute/cli/commands/lint/rules/duplicate_keys.luau b/lute/cli/commands/lint/rules/duplicate_keys.luau index 6f875909c..f66caf9ff 100644 --- a/lute/cli/commands/lint/rules/duplicate_keys.luau +++ b/lute/cli/commands/lint/rules/duplicate_keys.luau @@ -45,10 +45,10 @@ local function lint( seenKeys[entry.key.text] = true elseif entry.kind == "general" then if entry.key.tag == "string" then - if seenKeys[entry.key.text] then + if seenKeys[entry.key.token.text] then table.insert(violations, makeViolation(entry.location)) end - seenKeys[entry.key.text] = true + seenKeys[entry.key.token.text] = true elseif entry.key.tag == "number" then if seenKeys[entry.key.value] then table.insert(violations, makeViolation(entry.location)) diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index 44fac6199..6c16e3511 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -29,10 +29,17 @@ local function getWriteOnlyArgs(func: syntax.AstExpr, writeOnlyAPIs: smallTrie): end return entry.vals - elseif - (func.tag == "indexname" and func.expression.tag == "global") - or (func.tag == "index" and func.expression.tag == "global" and func.index.tag == "string") -- LUAUFIX: Refinement bug here: func should be narrowed to AstExprIndexExpr (CLI-190889) - then + elseif func.tag == "indexname" and func.expression.tag == "global" then + local branch = writeOnlyAPIs.entries[(func.expression :: syntax.AstExprGlobal).name.text] + + if not branch or branch.tag ~= "branch" then + return nil + end + + local entry = branch.entries[func.index.text] + + return if entry then entry.vals else nil + elseif func.tag == "index" and func.expression.tag == "global" and func.index.tag == "string" then func = func :: syntax.AstExprIndexName | syntax.AstExprIndexExpr local branch = writeOnlyAPIs.entries[(func.expression :: syntax.AstExprGlobal).name.text] @@ -40,7 +47,7 @@ local function getWriteOnlyArgs(func: syntax.AstExpr, writeOnlyAPIs: smallTrie): return nil end - local entry = branch.entries[(func.index :: syntax.Token).text] + local entry = branch.entries[(func.index :: syntax.AstExprConstantString).token.text] return if entry then entry.vals else nil else diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 9bfa77ecf..ba4d94e28 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -641,10 +641,10 @@ struct AstSerialize : public Luau::AstVisitor } // For correct trivia computation, everything must end up going through serializeToken - void serializeToken(Luau::Position position, const char* text, int nrec = 0) + void serializeToken(Luau::Position position, const char* text) { lua_rawcheckstack(L, 3); - lua_createtable(L, 0, nrec + 5); + lua_createtable(L, 0, 5); const auto trivia = extractTrivia(position); if (lastTokenRef != LUA_NOREF) @@ -682,8 +682,8 @@ struct AstSerialize : public Luau::AstVisitor lua_createtable(L, 0, 0); lua_setfield(L, -2, "trailingtrivia"); - lua_pushboolean(L, 1); - lua_setfield(L, -2, "istoken"); + lua_pushstring(L, "token"); + lua_setfield(L, -2, "kind"); lastTokenRef = lua_ref(L, -1); } @@ -801,8 +801,11 @@ struct AstSerialize : public Luau::AstVisitor } void serializeAttribute(Luau::AstAttr* node) { - serializeToken(node->location.begin, ("@" + std::string(node->name.value)).c_str()); lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize); // location, kind, token + + serializeToken(node->location.begin, ("@" + std::string(node->name.value)).c_str()); + lua_setfield(L, -2, "token"); lua_pushstring(L, "attribute"); lua_setfield(L, -2, "kind"); @@ -837,34 +840,51 @@ struct AstSerialize : public Luau::AstVisitor void serialize(Luau::AstExprConstantNil* node) { - serializeToken(node->location.begin, "nil", preambleSize); + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + serializeNodePreamble(node, "nil", "expr"); + + serializeToken(node->location.begin, "nil"); + lua_setfield(L, -2, "token"); } void serialize(Luau::AstExprConstantBool* node) { - serializeToken(node->location.begin, node->value ? "true" : "false", preambleSize + 1); + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + serializeNodePreamble(node, "boolean", "expr"); lua_pushboolean(L, node->value); lua_setfield(L, -2, "value"); + + serializeToken(node->location.begin, node->value ? "true" : "false"); + lua_setfield(L, -2, "token"); } void serialize(Luau::AstExprConstantNumber* node) { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + const auto cstNode = lookupCstNode(node); - serializeToken(node->location.begin, cstNode->value.data, preambleSize + 1); serializeNodePreamble(node, "number", "expr"); lua_pushnumber(L, node->value); lua_setfield(L, -2, "value"); + + serializeToken(node->location.begin, cstNode->value.data); + lua_setfield(L, -2, "token"); } void serialize(Luau::AstExprConstantString* node) { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 3); + const auto cstNode = lookupCstNode(node); - serializeToken(node->location.begin, cstNode->sourceString.data, preambleSize + 2); serializeNodePreamble(node, "string", "expr"); switch (cstNode->quoteStyle) @@ -887,6 +907,9 @@ struct AstSerialize : public Luau::AstVisitor lua_pushnumber(L, cstNode->blockDepth); lua_setfield(L, -2, "blockdepth"); + serializeToken(node->location.begin, cstNode->sourceString.data); + lua_setfield(L, -2, "token"); + // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually LUTE_ASSERT(currentPosition <= node->location.end); @@ -923,8 +946,13 @@ struct AstSerialize : public Luau::AstVisitor void serialize(Luau::AstExprVarargs* node) { - serializeToken(node->location.begin, "...", preambleSize); + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 1); + serializeNodePreamble(node, "vararg", "expr"); + + serializeToken(node->location.begin, "..."); + lua_setfield(L, -2, "token"); } void serialize(Luau::AstExprCall* node) @@ -1524,15 +1552,23 @@ struct AstSerialize : public Luau::AstVisitor void serializeStat(Luau::AstStatBreak* node) { lua_rawcheckstack(L, 2); - serializeToken(node->location.begin, "break", preambleSize); + lua_createtable(L, 0, preambleSize + 1); + serializeNodePreamble(node, "break", "stat"); + + serializeToken(node->location.begin, "break"); + lua_setfield(L, -2, "token"); } void serializeStat(Luau::AstStatContinue* node) { lua_rawcheckstack(L, 2); - serializeToken(node->location.begin, "continue", preambleSize); + lua_createtable(L, 0, preambleSize + 1); + serializeNodePreamble(node, "continue", "stat"); + + serializeToken(node->location.begin, "continue"); + lua_setfield(L, -2, "token"); } void serializeStat(Luau::AstStatReturn* node) @@ -2017,8 +2053,17 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "indexeropen"); { - auto initialPosition = item.stringPosition; - serializeToken(item.stringPosition, item.stringInfo->sourceString.data); + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + + Luau::Position initialPosition = item.stringPosition; + Luau::Position endPosition = { + initialPosition.line, initialPosition.column + static_cast(strlen(item.stringInfo->sourceString.data)) + }; + withLocation(Luau::Location{initialPosition, endPosition}); + + lua_pushstring(L, "type"); + lua_setfield(L, -2, "kind"); lua_pushstring(L, "string"); lua_setfield(L, -2, "tag"); @@ -2036,6 +2081,9 @@ struct AstSerialize : public Luau::AstVisitor } lua_setfield(L, -2, "quotestyle"); + serializeToken(item.stringPosition, item.stringInfo->sourceString.data); + lua_setfield(L, -2, "token"); + // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually // If string part was single line, end position = current position + 2 (start and end character) @@ -2203,11 +2251,13 @@ struct AstSerialize : public Luau::AstVisitor if (node->types.data[i]->is()) { - serializeToken(node->types.data[i]->location.begin, "?", 2); - lua_pushstring(L, "type"); - lua_setfield(L, -2, "kind"); - lua_pushstring(L, "optional"); - lua_setfield(L, -2, "tag"); + lua_createtable(L, 0, preambleSize + 1); + + serializeNodePreamble(node->types.data[i], "optional", "type"); + + serializeToken(node->types.data[i]->location.begin, "?"); + lua_setfield(L, -2, "token"); + lua_setfield(L, -2, "node"); // Since this option is an optional type, the separator is always present unless it's the last type @@ -2263,17 +2313,24 @@ struct AstSerialize : public Luau::AstVisitor void serializeType(Luau::AstTypeSingletonBool* node) { - serializeToken(node->location.begin, node->value ? "true" : "false", preambleSize + 1); + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + serializeNodePreamble(node, "boolean", "type"); lua_pushboolean(L, node->value); lua_setfield(L, -2, "value"); + + serializeToken(node->location.begin, node->value ? "true" : "false"); + lua_setfield(L, -2, "token"); } void serializeType(Luau::AstTypeSingletonString* node) { + lua_rawcheckstack(L, 2); + lua_createtable(L, 0, preambleSize + 2); + const auto cstNode = lookupCstNode(node); - serializeToken(node->location.begin, cstNode->sourceString.data, preambleSize + 1); serializeNodePreamble(node, "string", "type"); switch (cstNode->quoteStyle) @@ -2289,6 +2346,9 @@ struct AstSerialize : public Luau::AstVisitor } lua_setfield(L, -2, "quotestyle"); + serializeToken(node->location.begin, cstNode->sourceString.data); + lua_setfield(L, -2, "token"); + // Unlike normal tokens, string content contains quotation marks that were not included during advancement // For simplicity, lets set the current position manually LUTE_ASSERT(currentPosition <= node->location.end); diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 30ad6804d..fbac39acb 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -54,22 +54,22 @@ local function printString(self: PrintVisitor, expr: types.AstExprConstantString return end - self:printTriviaList(expr.leadingtrivia) + self:printTriviaList(expr.token.leadingtrivia) if expr.quotestyle == "single" then - self:write(`'{expr.text}'`) + self:write(`'{expr.token.text}'`) elseif expr.quotestyle == "double" then - self:write(`"{expr.text}"`) + self:write(`"{expr.token.text}"`) elseif expr.quotestyle == "block" then local equals = string.rep("=", expr.blockdepth) - self:write(`[{equals}[{expr.text}]{equals}]`) + self:write(`[{equals}[{expr.token.text}]{equals}]`) elseif expr.quotestyle == "interp" then - self:write("`" .. expr.text .. "`") + self:write("`" .. expr.token.text .. "`") else exhaustiveMatch(expr.quotestyle) end - self:printTriviaList(expr.trailingtrivia) + self:printTriviaList(expr.token.trailingtrivia) end local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprInterpString) @@ -106,8 +106,7 @@ local function printReplacement(self: PrintVisitor, node: types.AstNode, replace if typeof(replacement) == "string" then self:write(replacement) - elseif replacement.kind ~= nil then -- LUAUFIX: type solver upset comparing string singleton union to nil - -- TODO: preserve trivia + elseif replacement.kind ~= nil then visitor.visit(replacement, self) else error("Unsupported replacement type") diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 28dfbaf90..777389b6b 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -5,7 +5,6 @@ local tableext = require("@std/tableext") local types = require("./types") local visitor = require("./visitor") -local utils = require("./utils") type node = types.AstNode @@ -65,7 +64,7 @@ function queryLib.foreach(self: query, callback: (T) -> ()): query end local function newSelectVisitor(nodes: { T }, fn: (node) -> T?): visitor.Visitor - local function visit(n: node) -- LUAUFIX: type checker doesn't like assigning visit in the visitor fields with n: node + local function visit(n: node) local selected = fn(n) if selected ~= nil then table.insert(nodes, selected) @@ -80,13 +79,6 @@ local function newSelectVisitor(nodes: { T }, fn: (node) -> T?): visitor.Visi return true end selectVisitor.visitExprEnd = function(_) end - selectVisitor.visitToken = function(n) - if utils.isBaseToken(n) then - -- only visit if it is a base token, so we don't double count - return visit(n) - end - return true - end return selectVisitor end diff --git a/lute/std/libs/syntax/utils/init.luau b/lute/std/libs/syntax/utils/init.luau index f0a2a88da..d94579f54 100644 --- a/lute/std/libs/syntax/utils/init.luau +++ b/lute/std/libs/syntax/utils/init.luau @@ -231,11 +231,7 @@ function utils.isTypePack(n: types.AstNode): types.AstTypePack? end function utils.isToken(n: types.AstNode): types.Token? - return if n.istoken then n else nil -end - -function utils.isBaseToken(n: types.AstNode): types.Token? - return if n.istoken and not n.kind then n else nil + return if n.kind == "token" then n else nil end function utils.isAttribute(n: types.AstNode): types.AstAttribute? diff --git a/lute/std/libs/syntax/utils/trivia.luau b/lute/std/libs/syntax/utils/trivia.luau index 31ad3885b..37640a79b 100644 --- a/lute/std/libs/syntax/utils/trivia.luau +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -4,8 +4,8 @@ local types = require("../types") local retrieverLib = {} function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } - local q = query.findallfromroot<>(n, function(n: types.Token) - return if n.istoken ~= nil and n.istoken then n else nil + local q = query.findallfromroot<>(n, function(n: types.AstNode) + return if n.kind == "token" then n else nil end) local leftmostToken: types.Token? = nil @@ -27,8 +27,8 @@ function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } end function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } - local q = query.findallfromroot<>(n, function(n: types.Token) - return if n.istoken ~= nil and n.istoken then n else nil + local q = query.findallfromroot<>(n, function(n: types.AstNode) + return if n.kind == "token" then n else nil end) local rightmostToken: types.Token? = nil diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 689af444d..2d78750c7 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -77,79 +77,79 @@ export type Visitor = { visitAttribute: (types.AstAttribute) -> boolean, } -local function alwaysVisit(...: any) +local function alwaysVisit(...: types.AstNode) return true end local defaultVisitor: Visitor = { - visitStatBlock = alwaysVisit :: any, - visitStatBlockEnd = alwaysVisit :: any, - visitStatDo = alwaysVisit :: any, - visitStatIf = alwaysVisit :: any, - visitStatWhile = alwaysVisit :: any, - visitStatRepeat = alwaysVisit :: any, - visitStatBreak = alwaysVisit :: any, - visitStatContinue = alwaysVisit :: any, - visitStatReturn = alwaysVisit :: any, - visitStatLocalDeclaration = alwaysVisit :: any, - visitStatLocalDeclarationEnd = alwaysVisit :: any, - visitStatFor = alwaysVisit :: any, - visitStatForEnd = alwaysVisit :: any, - visitStatForIn = alwaysVisit :: any, - visitStatForInEnd = alwaysVisit :: any, - visitStatAssign = alwaysVisit :: any, - visitStatCompoundAssign = alwaysVisit :: any, - visitStatFunction = alwaysVisit :: any, - visitStatLocalFunction = alwaysVisit :: any, - visitStatTypeAlias = alwaysVisit :: any, - visitStatTypeFunction = alwaysVisit :: any, - visitStatExpr = alwaysVisit :: any, - - visitExpr = alwaysVisit :: any, - visitExprConstantNil = alwaysVisit :: any, - visitExprConstantString = alwaysVisit :: any, - visitExprConstantBool = alwaysVisit :: any, - visitExprConstantNumber = alwaysVisit :: any, - visitExprEnd = alwaysVisit :: any, - visitExprLocal = alwaysVisit :: any, - visitExprGlobal = alwaysVisit :: any, - visitExprCall = alwaysVisit :: any, - visitExprUnary = alwaysVisit :: any, - visitExprBinary = alwaysVisit :: any, - visitExprFunction = alwaysVisit :: any, - visitExprFunctionEnd = alwaysVisit :: any, + visitStatBlock = alwaysVisit, + visitStatBlockEnd = alwaysVisit :: (types.AstNode) -> (), + visitStatDo = alwaysVisit, + visitStatIf = alwaysVisit, + visitStatWhile = alwaysVisit, + visitStatRepeat = alwaysVisit, + visitStatBreak = alwaysVisit, + visitStatContinue = alwaysVisit, + visitStatReturn = alwaysVisit, + visitStatLocalDeclaration = alwaysVisit, + visitStatLocalDeclarationEnd = alwaysVisit :: (types.AstNode) -> (), + visitStatFor = alwaysVisit, + visitStatForEnd = alwaysVisit :: (types.AstNode) -> (), + visitStatForIn = alwaysVisit, + visitStatForInEnd = alwaysVisit :: (types.AstNode) -> (), + visitStatAssign = alwaysVisit, + visitStatCompoundAssign = alwaysVisit, + visitStatFunction = alwaysVisit, + visitStatLocalFunction = alwaysVisit, + visitStatTypeAlias = alwaysVisit, + visitStatTypeFunction = alwaysVisit, + visitStatExpr = alwaysVisit, + + visitExpr = alwaysVisit, + visitExprConstantNil = alwaysVisit, + visitExprConstantString = alwaysVisit, + visitExprConstantBool = alwaysVisit, + visitExprConstantNumber = alwaysVisit, + visitExprEnd = alwaysVisit :: (types.AstNode) -> (), + visitExprLocal = alwaysVisit, + visitExprGlobal = alwaysVisit, + visitExprCall = alwaysVisit, + visitExprUnary = alwaysVisit, + visitExprBinary = alwaysVisit, + visitExprFunction = alwaysVisit, + visitExprFunctionEnd = alwaysVisit :: (types.AstNode) -> (), visitExprInstantiate = alwaysVisit, - visitTableExprItem = alwaysVisit :: any, - visitExprTable = alwaysVisit :: any, - visitExprIndexName = alwaysVisit :: any, - visitExprIndexExpr = alwaysVisit :: any, - visitExprGroup = alwaysVisit :: any, + visitTableExprItem = alwaysVisit :: (types.AstTableExprItem) -> boolean, + visitExprTable = alwaysVisit, + visitExprIndexName = alwaysVisit, + visitExprIndexExpr = alwaysVisit, + visitExprGroup = alwaysVisit, visitExprInterpString = alwaysVisit, visitExprTypeAssertion = alwaysVisit, visitExprIfElse = alwaysVisit, - visitExprVarargs = alwaysVisit :: any, - - visitTypeReference = alwaysVisit :: any, - visitTypeSingletonBool = alwaysVisit :: any, - visitTypeSingletonString = alwaysVisit :: any, - visitTypeTypeof = alwaysVisit :: any, - visitTypeGroup = alwaysVisit :: any, - visitTypeOptional = alwaysVisit :: any, - visitTypeUnion = alwaysVisit :: any, - visitTypeIntersection = alwaysVisit :: any, - visitTypeArray = alwaysVisit :: any, - visitTypeTable = alwaysVisit :: any, + visitExprVarargs = alwaysVisit, + + visitTypeReference = alwaysVisit, + visitTypeSingletonBool = alwaysVisit, + visitTypeSingletonString = alwaysVisit, + visitTypeTypeof = alwaysVisit, + visitTypeGroup = alwaysVisit, + visitTypeOptional = alwaysVisit, + visitTypeUnion = alwaysVisit, + visitTypeIntersection = alwaysVisit, + visitTypeArray = alwaysVisit, + visitTypeTable = alwaysVisit, visitTypeFunction = alwaysVisit, visitTypePackExplicit = alwaysVisit, visitTypePackGeneric = alwaysVisit, visitTypePackVariadic = alwaysVisit, - visitToken = alwaysVisit :: any, + visitToken = alwaysVisit, - visitLocal = alwaysVisit :: any, + visitLocal = alwaysVisit, - visitAttribute = alwaysVisit :: any, + visitAttribute = alwaysVisit, } local function exhaustiveMatch(value: never): never @@ -250,13 +250,13 @@ end local function visitStatBreak(node: types.AstStatBreak, visitor: Visitor) if visitor.visitStatBreak(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end local function visitStatContinue(node: types.AstStatContinue, visitor: Visitor) if visitor.visitStatContinue(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end @@ -383,25 +383,25 @@ end local function visitExprConstantString(node: types.AstExprConstantString, visitor: Visitor) if visitor.visitExprConstantString(node) then - visitor.visitToken(node) + visitor.visitToken(node.token) end end local function visitExprConstantNil(node: types.AstExprConstantNil, visitor: Visitor) if visitor.visitExprConstantNil(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end local function visitExprConstantBool(node: types.AstExprConstantBool, visitor: Visitor) if visitor.visitExprConstantBool(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end local function visitExprConstantNumber(node: types.AstExprConstantNumber, visitor: Visitor) if visitor.visitExprConstantNumber(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end @@ -419,7 +419,7 @@ end local function visitExprVarargs(node: types.AstExprVarargs, visitor: Visitor) if visitor.visitExprVarargs(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end @@ -453,7 +453,7 @@ end local function visitAttribute(node: types.AstAttribute, visitor: Visitor) if visitor.visitAttribute(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end @@ -681,13 +681,13 @@ end local function visitTypeSingletonBool(node: types.AstTypeSingletonBool, visitor: Visitor) if visitor.visitTypeSingletonBool(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end local function visitTypeSingletonString(node: types.AstTypeSingletonString, visitor: Visitor) if visitor.visitTypeSingletonString(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end @@ -710,7 +710,7 @@ end local function visitTypeOptional(node: types.AstTypeOptional, visitor: Visitor) if visitor.visitTypeOptional(node) then - visitToken(node, visitor) + visitToken(node.token, visitor) end end @@ -1068,8 +1068,8 @@ local function visit(node: types.AstNode, visitor: Visitor) visitLocal(node, visitor) elseif node.kind == "attribute" then visitAttribute(node, visitor) - elseif (node :: types.Token).istoken then - visitToken(node :: types.Token, visitor) + elseif node.kind == "token" then + visitToken(node, visitor) elseif (node :: types.AstTableExprItem).istableitem then visitTableExprItem(node :: types.AstTableExprItem, visitor) else diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index d1f5beaf9..09dbf9021 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -108,10 +108,10 @@ test.suite("Parser", function(suite) local trailingToken: syntax.AstExpr = (firstStmt :: syntax.AstStatLocal).values[1].node assert.eq(trailingToken.tag, "string") local expr = trailingToken :: syntax.AstExprConstantString - assert.eq(#expr.trailingtrivia, 3) - assert.eq(expr.trailingtrivia[1].text, " ") - assert.eq(expr.trailingtrivia[2].text, "-- comment") - assert.eq(expr.trailingtrivia[3].text, "\n") + assert.eq(#expr.token.trailingtrivia, 3) + assert.eq(expr.token.trailingtrivia[1].text, " ") + assert.eq(expr.token.trailingtrivia[2].text, "-- comment") + assert.eq(expr.token.trailingtrivia[3].text, "\n") local secondStmt = block.statements[2] assert.eq(secondStmt.tag, "local") @@ -191,7 +191,7 @@ test.suite("Parser", function(suite) assert.eq(l.tag, "localfunction") local lf = l :: syntax.AstStatLocalFunction - assert.eq(lf.func.attributes[1].text, subcase.attr) + assert.eq(lf.func.attributes[1].token.text, subcase.attr) end end) end) @@ -211,20 +211,20 @@ test.suite("parseExpr", function(suite) local expr = parser.parseexpr("nil") assert.eq(expr.tag, "nil") assert.eq(expr.kind, "expr") - assert.eq((expr :: syntax.AstExprConstantNil).text, "nil") + assert.eq((expr :: syntax.AstExprConstantNil).token.text, "nil") end) suite:case("parseExprBoolean", function(assert) local trueExpr = parser.parseexpr("true") assert.eq(trueExpr.tag, "boolean") assert.eq(trueExpr.kind, "expr") - assert.eq((trueExpr :: syntax.AstExprConstantBool).text, "true") + assert.eq((trueExpr :: syntax.AstExprConstantBool).token.text, "true") assert.eq((trueExpr :: syntax.AstExprConstantBool).value, true) local falseExpr = parser.parseexpr("false") assert.eq(falseExpr.tag, "boolean") assert.eq(falseExpr.kind, "expr") - assert.eq((falseExpr :: syntax.AstExprConstantBool).text, "false") + assert.eq((falseExpr :: syntax.AstExprConstantBool).token.text, "false") assert.eq((falseExpr :: syntax.AstExprConstantBool).value, false) end) @@ -232,13 +232,13 @@ test.suite("parseExpr", function(suite) local numberExpr = parser.parseexpr("42") assert.eq(numberExpr.tag, "number") assert.eq(numberExpr.kind, "expr") - assert.eq((numberExpr :: syntax.AstExprConstantNumber).text, "42") + assert.eq((numberExpr :: syntax.AstExprConstantNumber).token.text, "42") assert.eq((numberExpr :: syntax.AstExprConstantNumber).value, 42) local floatExpr = parser.parseexpr("3.14") assert.eq(floatExpr.tag, "number") assert.eq(floatExpr.kind, "expr") - assert.eq((floatExpr :: syntax.AstExprConstantNumber).text, "3.14") + assert.eq((floatExpr :: syntax.AstExprConstantNumber).token.text, "3.14") assert.eq((floatExpr :: syntax.AstExprConstantNumber).value, 3.14) end) @@ -246,25 +246,25 @@ test.suite("parseExpr", function(suite) local singleQuoteExpr = parser.parseexpr("'hello'") assert.eq(singleQuoteExpr.tag, "string") assert.eq(singleQuoteExpr.kind, "expr") - assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).text, "hello") + assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).token.text, "hello") assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).quotestyle, "single") local doubleQuoteExpr = parser.parseexpr('"world"') assert.eq(doubleQuoteExpr.tag, "string") assert.eq(doubleQuoteExpr.kind, "expr") - assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).text, "world") + assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).token.text, "world") assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).quotestyle, "double") local blockQuoteExpr = parser.parseexpr("[[world]]") assert.eq(blockQuoteExpr.tag, "string") assert.eq(blockQuoteExpr.kind, "expr") - assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).text, "world") + assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).token.text, "world") assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).quotestyle, "block") local interpExpr = parser.parseexpr("`foobar`") assert.eq(interpExpr.tag, "string") assert.eq(interpExpr.kind, "expr") - assert.eq((interpExpr :: syntax.AstExprConstantString).text, "foobar") + assert.eq((interpExpr :: syntax.AstExprConstantString).token.text, "foobar") assert.eq((interpExpr :: syntax.AstExprConstantString).quotestyle, "interp") end) @@ -286,7 +286,7 @@ test.suite("parseExpr", function(suite) local varargsExpr = parser.parseexpr("...") assert.eq(varargsExpr.tag, "vararg") assert.eq(varargsExpr.kind, "expr") - assert.eq((varargsExpr :: syntax.AstExprVarargs).text, "...") + assert.eq((varargsExpr :: syntax.AstExprVarargs).token.text, "...") end) suite:case("parseExprCall", function(assert) @@ -647,7 +647,7 @@ test.suite("parse", function(suite) assert.eq(breakStat.kind, "stat") breakStat = breakStat :: syntax.AstStatBreak assert.eq(breakStat.tag, "break") - assert.eq(breakStat.text, "break") + assert.eq(breakStat.token.text, "break") assert.eq(whileStat.endkeyword.text, "end") @@ -667,7 +667,7 @@ test.suite("parse", function(suite) assert.eq(continueStat.kind, "stat") continueStat = continueStat :: syntax.AstStatContinue assert.eq(continueStat.tag, "continue") - assert.eq(continueStat.text, "continue") + assert.eq(continueStat.token.text, "continue") assert.eq(whileStat.endkeyword.text, "end") end) @@ -821,9 +821,9 @@ test.suite("parse", function(suite) local funcStat = block.statements[1] :: syntax.AstStatFunction assert.eq(funcStat.tag, "function") assert.eq(#funcStat.func.attributes, 3) - assert.eq((funcStat.func.attributes[1] :: syntax.AstAttribute).text, "@checked") - assert.eq((funcStat.func.attributes[2] :: syntax.AstAttribute).text, "@native") - assert.eq((funcStat.func.attributes[3] :: syntax.AstAttribute).text, "@deprecated") + assert.eq((funcStat.func.attributes[1] :: syntax.AstAttribute).token.text, "@checked") + assert.eq((funcStat.func.attributes[2] :: syntax.AstAttribute).token.text, "@native") + assert.eq((funcStat.func.attributes[3] :: syntax.AstAttribute).token.text, "@deprecated") assert.eq(funcStat.func.functionkeyword.text, "function") assert.eq(funcStat.name.tag, "global") assert.eq((funcStat.name :: syntax.AstExprGlobal).name.text, "add") @@ -940,7 +940,7 @@ test.suite("parse types", function(suite) assert.eq(typeAlias.type.tag, "boolean") local singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool - assert.eq(singletonBool.text, "true") + assert.eq(singletonBool.token.text, "true") assert.eq(singletonBool.value, true) block = parser.parseblock("type FalseType = false") @@ -948,7 +948,7 @@ test.suite("parse types", function(suite) assert.eq(typeAlias.type.tag, "boolean") singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool - assert.eq(singletonBool.text, "false") + assert.eq(singletonBool.token.text, "false") assert.eq(singletonBool.value, false) end) @@ -958,7 +958,7 @@ test.suite("parse types", function(suite) assert.eq(typeAlias.type.tag, "string") local singletonStr = typeAlias.type :: syntax.AstTypeSingletonString - assert.eq(singletonStr.text, "hello") + assert.eq(singletonStr.token.text, "hello") assert.eq(singletonStr.quotestyle, "single") block = parser.parseblock('type WorldType = "world"') @@ -966,7 +966,7 @@ test.suite("parse types", function(suite) assert.eq(typeAlias.type.tag, "string") singletonStr = typeAlias.type :: syntax.AstTypeSingletonString - assert.eq(singletonStr.text, "world") + assert.eq(singletonStr.token.text, "world") assert.eq(singletonStr.quotestyle, "double") end) @@ -1015,7 +1015,7 @@ test.suite("parse types", function(suite) assert.eq(#unionType.types, 2) assert.eq(unionType.types[1].node.tag, "reference") assert.eq(unionType.types[2].node.tag, "optional") - assert.eq((unionType.types[2].node :: syntax.AstTypeOptional).text, "?") + assert.eq((unionType.types[2].node :: syntax.AstTypeOptional).token.text, "?") end) suite:case("testTypeUnionWithOptionalPart", function(assert) @@ -1127,8 +1127,9 @@ test.suite("parse types", function(suite) assert.eq(tableTypeItem.kind, "stringproperty") assert.eq(tableTypeItem.access, nil) assert.eq(tableTypeItem.indexeropen.text, "[") + assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).kind, "type") assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).tag, "string") - assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).text, "hello") + assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).token.text, "hello") assert.eq(tableTypeItem.indexerclose.text, "]") assert.eq(tableTypeItem.colon.text, ":") assert.eq(tableTypeItem.value.tag, "string") @@ -1260,7 +1261,10 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(expr.kind, "expr") assert.eq(expr.tag, "nil") assert.erroreq(function() - (expr :: any).value = 123 + (expr :: any).tag = "modified" + end, frozenTableMessage) + assert.erroreq(function() + (expr :: any).token.text = "blah" end, frozenTableMessage) end) @@ -1269,7 +1273,10 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(expr.kind, "expr") assert.eq(expr.tag, "boolean") assert.erroreq(function() - (expr :: any).value = false + (expr :: any).tag = "modified" + end, frozenTableMessage) + assert.erroreq(function() + (expr :: any).token.text = "blah" end, frozenTableMessage) end) @@ -1278,7 +1285,10 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(expr.kind, "expr") assert.eq(expr.tag, "number") assert.erroreq(function() - (expr :: any).value = 99 + (expr :: any).tag = "modified" + end, frozenTableMessage) + assert.erroreq(function() + (expr :: any).token.text = "blah" end, frozenTableMessage) end) @@ -1287,7 +1297,10 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(expr.kind, "expr") assert.eq(expr.tag, "string") assert.erroreq(function() - (expr :: any).quotestyle = "double" + (expr :: any).tag = "modified" + end, frozenTableMessage) + assert.erroreq(function() + (expr :: any).token.text = "blah" end, frozenTableMessage) end) @@ -1377,6 +1390,9 @@ test.suite("AST_nodes_frozen", function(suite) assert.erroreq(function() table.insert((expr :: any).attributes, nil :: any) end, frozenTableMessage) + assert.erroreq(function() + ((expr :: syntax.AstExprFunction).attributes[1] :: any).token.text = "@modified" + end, frozenTableMessage) assert.erroreq(function() table.insert((expr :: any).generics, nil :: any) end, frozenTableMessage) @@ -1877,7 +1893,7 @@ test.suite("AST_nodes_frozen", function(suite) local block = parser.parseblock("local x = 1") local localStat = block.statements[1] :: syntax.AstStatLocal local token = localStat.localkeyword - assert.eq(token.istoken, true) + assert.eq(token.kind, "token") assert.erroreq(function() (token :: any).text = "modified" end, frozenTableMessage) @@ -1937,7 +1953,7 @@ test.suite("AST_nodes_frozen", function(suite) local result = parser.parse("local x = 1") local eof = result.eof assert.eq(eof.tag, "eof") - assert.eq(eof.istoken, true) + assert.eq(eof.kind, "token") assert.erroreq(function() (eof :: any).tag = "modified" end, frozenTableMessage) diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 5fcb0e6a8..3fd6a3806 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -30,7 +30,7 @@ test.suite("SyntaxPrinter", function(suite) checkReplacement(source, expected, function(ast) return query.findallfromroot(ast, syntaxUtils.isExprConstantString):replace(function(s) - return if s.text == "World" then "1 + 2" else nil + return if s.token.text == "World" then "1 + 2" else nil end) end, assert) end) @@ -44,7 +44,7 @@ test.suite("SyntaxPrinter", function(suite) checkReplacement(source, expected, function(ast) return query.findallfromroot(ast, syntaxUtils.isExprConstantString):replace(function(s) - return if s.text == "World" then syntax.parseexpr("1 + 2") else nil + return if s.token.text == "World" then syntax.parseexpr("1 + 2") else nil end) end, assert) end) diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index a7d33fb85..33bba0bfd 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -117,14 +117,14 @@ test.suite("AstQuery", function(suite) local queryResult = query.findallfromroot(ast, utils.isToken) assert.eq(#queryResult.nodes, 6) queryResult:foreach(function(node) - assert.eq(node.istoken, true) + assert.eq(node.kind, "token") end) local constNums = query.findallfromroot(ast, utils.isExprConstantNumber) assert.eq(#constNums.nodes, 1) local constNils = query.findallfromroot(ast, utils.isExprConstantNil) assert.eq(#constNils.nodes, 1) - local baseTokens = query.findallfromroot(ast, utils.isBaseToken) - assert.eq(#baseTokens.nodes, 4) + local tokens = query.findallfromroot(ast, utils.isToken) + assert.eq(#tokens.nodes, 6) end) suite:case("findall", function(assert) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 37f79aaa3..89c0a6a06 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -32,54 +32,44 @@ lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:62:6-17 lute/cli/commands/test/filter.luau:62:6-17 -lute/std/libs/syntax/printer.luau:41:23-27 -lute/std/libs/syntax/printer.luau:41:23-27 -lute/std/libs/syntax/printer.luau:42:25-29 -lute/std/libs/syntax/printer.luau:42:25-29 -lute/std/libs/syntax/printer.luau:42:50-54 -lute/std/libs/syntax/printer.luau:42:50-54 -lute/std/libs/syntax/printer.luau:111:3-15 -lute/std/libs/syntax/printer.luau:111:3-15 -lute/std/libs/syntax/printer.luau:155:3-26 -lute/std/libs/syntax/printer.luau:155:3-26 -lute/std/libs/syntax/query.luau:41:25-25 -lute/std/libs/syntax/query.luau:41:25-25 -lute/std/libs/syntax/query.luau:41:25-25 -lute/std/libs/syntax/query.luau:41:25-25 -lute/std/libs/syntax/query.luau:83:29-100 -lute/std/libs/syntax/query.luau:83:29-100 -lute/std/libs/syntax/query.luau:136:24-31 -lute/std/libs/syntax/query.luau:136:24-31 -lute/std/libs/syntax/query.luau:136:45-52 -lute/std/libs/syntax/query.luau:136:45-52 -lute/std/libs/syntax/query.luau:136:59-61 -lute/std/libs/syntax/query.luau:136:59-61 -lute/std/libs/syntax/query.luau:136:59-61 -lute/std/libs/syntax/query.luau:136:59-61 -lute/std/libs/syntax/query.luau:142:13-28 -lute/std/libs/syntax/query.luau:142:13-28 -lute/std/libs/syntax/query.luau:142:13-28 -lute/std/libs/syntax/query.luau:142:13-28 -lute/std/libs/syntax/query.luau:143:9-20 -lute/std/libs/syntax/query.luau:143:9-20 -lute/std/libs/syntax/query.luau:145:13-28 -lute/std/libs/syntax/query.luau:145:13-28 -lute/std/libs/syntax/query.luau:147:16-34 -lute/std/libs/syntax/query.luau:147:16-34 +lute/std/libs/syntax/printer.luau:110:3-15 +lute/std/libs/syntax/printer.luau:110:3-15 +lute/std/libs/syntax/printer.luau:154:3-26 +lute/std/libs/syntax/printer.luau:154:3-26 +lute/std/libs/syntax/query.luau:40:25-25 +lute/std/libs/syntax/query.luau:40:25-25 +lute/std/libs/syntax/query.luau:40:25-25 +lute/std/libs/syntax/query.luau:40:25-25 +lute/std/libs/syntax/query.luau:128:24-31 +lute/std/libs/syntax/query.luau:128:24-31 +lute/std/libs/syntax/query.luau:128:45-52 +lute/std/libs/syntax/query.luau:128:45-52 +lute/std/libs/syntax/query.luau:128:59-61 +lute/std/libs/syntax/query.luau:128:59-61 +lute/std/libs/syntax/query.luau:128:59-61 +lute/std/libs/syntax/query.luau:128:59-61 +lute/std/libs/syntax/query.luau:134:13-28 +lute/std/libs/syntax/query.luau:134:13-28 +lute/std/libs/syntax/query.luau:134:13-28 +lute/std/libs/syntax/query.luau:134:13-28 +lute/std/libs/syntax/query.luau:135:9-20 +lute/std/libs/syntax/query.luau:135:9-20 +lute/std/libs/syntax/query.luau:137:13-28 +lute/std/libs/syntax/query.luau:137:13-28 +lute/std/libs/syntax/query.luau:139:16-34 +lute/std/libs/syntax/query.luau:139:16-34 lute/std/libs/syntax/utils/init.luau:62:12-24 lute/std/libs/syntax/utils/init.luau:62:12-24 lute/std/libs/syntax/utils/init.luau:62:31-31 lute/std/libs/syntax/utils/init.luau:62:31-31 -lute/std/libs/syntax/utils/init.luau:234:12-20 -lute/std/libs/syntax/utils/init.luau:234:12-20 -lute/std/libs/syntax/utils/init.luau:234:27-27 -lute/std/libs/syntax/utils/init.luau:234:27-27 -lute/std/libs/syntax/utils/init.luau:238:12-20 -lute/std/libs/syntax/utils/init.luau:238:12-20 lute/std/libs/syntax/utils/trivia.luau:7:12-47 lute/std/libs/syntax/utils/trivia.luau:7:12-47 lute/std/libs/syntax/utils/trivia.luau:30:12-47 lute/std/libs/syntax/utils/trivia.luau:30:12-47 +lute/std/libs/syntax/visitor.luau:141:19-29 +lute/std/libs/syntax/visitor.luau:141:19-29 +lute/std/libs/syntax/visitor.luau:141:19-29 +lute/std/libs/syntax/visitor.luau:141:19-29 lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 lute/std/libs/syntax/visitor.luau:1036:21-25 From e0faf05ee8d274be25c9e686e95c88af42d9589c Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 18 Mar 2026 09:55:44 -0700 Subject: [PATCH 400/642] Adds a hello world section to the guide/ for Lute (#885) This is an introductory bit on how to write a simple hello world program in Lute. It also walks through the directory structure required to setup a project, which is useful in subsequent chapters. --- docs/guide/hello-world.md | 42 ++++++++++++++++++++++++++++++++++++++ docs/guide/index.md | 1 + docs/guide/installation.md | 3 +++ 3 files changed, 46 insertions(+) create mode 100644 docs/guide/hello-world.md diff --git a/docs/guide/hello-world.md b/docs/guide/hello-world.md new file mode 100644 index 000000000..6e35ffe19 --- /dev/null +++ b/docs/guide/hello-world.md @@ -0,0 +1,42 @@ +--- +order: 3 +--- + +# Hello World! + +We're going to walk through the creation of a simple program using Lute. To start with, create a folder for your project with: +```bash +mkdir hello-world +cd hello-world +``` + +Create a file in this project with: +```bash +touch main.luau +``` + +Before we continue, let's ensure that we setup a good autocomplete experience for your favorite editor of choice. Run: +```bash +lute setup +``` + +This will add some files to a folder called `.lute/` in your home directory. These type definition files are then made available to `luau-lsp`, the most popular Luau Language Server, in order to provide high quality autocomplete and typechecking. + +Finally, open up the file `main.luau` and add: +```luau +print("Hello World") +``` + +You can now run this program by running: +```bash +lute run main.luau +``` + +or just: +```bash +lute main.luau +``` + +Note, that any Luau script can be run like this, not just a file named `main`. + +In the next chapter, we're going to see how we can use some of the tools Lute provides to help you write some simple programs. diff --git a/docs/guide/index.md b/docs/guide/index.md index 3ebfb3ce4..54844b2bf 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -5,3 +5,4 @@ order: 1 # Guide - [Installation](./installation) +- [Hello World!](./hello-world) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 6bc6fc3e2..46affc2a6 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -1,3 +1,6 @@ +--- +order: 2 +--- # Installation :::warning From bdcb23627e3ea44ea4c1c9a3a2824c343877e61f Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 19 Mar 2026 15:24:59 -0700 Subject: [PATCH 401/642] Fixes/silences type errors in the syntax library (#865) A lot of these were code too complex errors (most likely in Normalization based on a previous example I minimized) that I had to silence with any casts. --- lute/std/libs/syntax/printer.luau | 4 +- lute/std/libs/syntax/query.luau | 14 +- lute/std/libs/syntax/utils/init.luau | 4 +- lute/std/libs/syntax/utils/trivia.luau | 18 +- lute/std/libs/syntax/visitor.luau | 4 +- tests/std/syntax/printer.test.luau | 466 +++++++++++++++---------- tools/check-faillist.txt | 104 ------ 7 files changed, 315 insertions(+), 299 deletions(-) diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index fbac39acb..c1611eda9 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -107,7 +107,7 @@ local function printReplacement(self: PrintVisitor, node: types.AstNode, replace if typeof(replacement) == "string" then self:write(replacement) elseif replacement.kind ~= nil then - visitor.visit(replacement, self) + visitor.visit(replacement :: any, self) -- LUAUFIX: Code too complex emitted in absence of cast because AstNode is very large else error("Unsupported replacement type") end @@ -151,7 +151,7 @@ local function printVisitor(replacements: types.replacements?) if replacement == nil then return true end - printer:printReplacement(node, replacement) + printer:printReplacement(node, replacement :: any) -- remove any cast once code to complex no longer emitted return false end diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 777389b6b..20b2d40ed 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -37,7 +37,7 @@ function queryLib.replace(self: query, repl: (T) -> types.replacement?): t for _, node in self.nodes do local r = repl(node) if r ~= nil then - replacements[node] = r + replacements[node] = r :: any -- LUAUFIX: remove any cast once code too complex no longer emitted end end return replacements @@ -125,19 +125,19 @@ function queryLib.findallfromroot(ast: types.ParseResult | node, fn: (node) - local selectVisitor = newSelectVisitor(nodes, fn) - local root: node = if ast.root ~= nil then ast.root else ast + local root: node = if (ast :: any).root ~= nil then (ast :: any).root else ast :: any -- LUAUFIX: Replace any cast with cast to node once code too complex no longer emitted visitor.visit(root, selectVisitor) return { nodes = nodes, filter = queryLib.filter, - replace = queryLib.replace, - map = queryLib.map, + replace = queryLib.replace :: any, -- LUAUFIX: Remove any cast once code too complex no longer emitted + map = queryLib.map :: any, -- LUAUFIX: Remove any cast once recursion restriction is loosened foreach = queryLib.foreach, - findall = queryLib.findall, + findall = queryLib.findall :: any, -- LUAUFIX: Remove any cast once recursion restriction is loosened flatmap = queryLib.flatmap, - maptoarray = queryLib.maptoarray, - } -- LUAUFIX: queryLib.map has generics quantified at a different level than expected + maptoarray = queryLib.maptoarray :: any, -- Any cast because maptoarray has generics quantified at a different level than the query type expects + } end return table.freeze(queryLib) diff --git a/lute/std/libs/syntax/utils/init.luau b/lute/std/libs/syntax/utils/init.luau index d94579f54..ca503a71e 100644 --- a/lute/std/libs/syntax/utils/init.luau +++ b/lute/std/libs/syntax/utils/init.luau @@ -58,8 +58,8 @@ function utils.isExprTable(n: types.AstNode): types.AstExprTable? return if n.kind == "expr" and n.tag == "table" then n else nil end -function utils.isTableExprItem(n: types.AstNode): types.AstTableExprItem? - return if n.istableitem then n else nil +function utils.isTableExprItem(n: types.AstNode | types.AstTableExprItem): types.AstTableExprItem? + return if (n :: types.AstTableExprItem).istableitem then n :: types.AstTableExprItem else nil end function utils.isExprUnary(n: types.AstNode): types.AstExprUnary? diff --git a/lute/std/libs/syntax/utils/trivia.luau b/lute/std/libs/syntax/utils/trivia.luau index 37640a79b..42ac6ddab 100644 --- a/lute/std/libs/syntax/utils/trivia.luau +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -4,9 +4,12 @@ local types = require("../types") local retrieverLib = {} function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } - local q = query.findallfromroot<>(n, function(n: types.AstNode) - return if n.kind == "token" then n else nil - end) + local q = query.findallfromroot<>( + n :: any, + function(n: types.AstNode) -- LUAUFIX: Code too complex emitted because AstNode is very large + return if n.kind == "token" then n else nil + end + ) local leftmostToken: types.Token? = nil @@ -27,9 +30,12 @@ function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } end function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } - local q = query.findallfromroot<>(n, function(n: types.AstNode) - return if n.kind == "token" then n else nil - end) + local q = query.findallfromroot( + n :: any, + function(n: types.AstNode) -- LUAUFIX: Code too complex emitted because AstNode is very large + return if n.kind == "token" then n else nil + end + ) local rightmostToken: types.Token? = nil diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 2d78750c7..ec89faf75 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -138,7 +138,7 @@ local defaultVisitor: Visitor = { visitTypeUnion = alwaysVisit, visitTypeIntersection = alwaysVisit, visitTypeArray = alwaysVisit, - visitTypeTable = alwaysVisit, + visitTypeTable = alwaysVisit :: (types.AstTypeTable) -> boolean, -- LUAUFIX: Code too complex emitted in absence of cast visitTypeFunction = alwaysVisit, visitTypePackExplicit = alwaysVisit, @@ -1033,7 +1033,7 @@ local function create(visit: ((types.AstNode) -> boolean)?): Visitor visitTypeUnion = visit, visitTypeIntersection = visit, visitTypeArray = visit, - visitTypeTable = visit, + visitTypeTable = visit :: (types.AstTypeTable) -> boolean, -- LUAUFIX: Remove cast once code too complex no longer emitted visitTypeFunction = visit, visitTypePackExplicit = visit, diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 3fd6a3806..8fe3860ae 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -29,9 +29,11 @@ test.suite("SyntaxPrinter", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprConstantString):replace(function(s) - return if s.token.text == "World" then "1 + 2" else nil - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprConstantString) + :replace(function(s) -- LUAUFIX: Remove any cast once code too complex no longer emitted + return if s.token.text == "World" then "1 + 2" else nil + end) end, assert) end) @@ -43,9 +45,11 @@ test.suite("SyntaxPrinter", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprConstantString):replace(function(s) - return if s.token.text == "World" then syntax.parseexpr("1 + 2") else nil - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprConstantString) + :replace(function(s) -- LUAUFIX: Remove any cast once code too complex no longer emitted + return if s.token.text == "World" then syntax.parseexpr("1 + 2") else nil + end) end, assert) end) @@ -57,9 +61,11 @@ test.suite("SyntaxPrinter", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatLocal):replace(function(_) - return syntax.parseblock("local name = 1 + 2") - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function(_) + return syntax.parseblock("local name = 1 + 2") + end) end, assert) end) @@ -72,7 +78,7 @@ test.suite("SyntaxPrinter", function(suite) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast, syntaxUtils.isStatLocal) + .findallfromroot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :map(function(assign) return assign.variables[1].node.annotation end) @@ -91,7 +97,7 @@ test.suite("SyntaxPrinter", function(suite) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast, syntaxUtils.isStatTypeAlias) + .findallfromroot(ast :: any, syntaxUtils.isStatTypeAlias) -- LUAUFIX: Remove any cast once code too complex no longer emitted :map(function(ta) return if ta.type.tag == "function" then ta.type.returntypes else nil end) @@ -107,7 +113,7 @@ test.suite("SyntaxPrinter", function(suite) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast, syntaxUtils.isExprTable) + .findallfromroot(ast :: any, syntaxUtils.isExprTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted :map(function(node) return node.entries[1] end) @@ -123,9 +129,11 @@ test.suite("SyntaxPrinter", function(suite) local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprGroup):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprGroup) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -135,9 +143,11 @@ nil -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprConstantNil):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprConstantNil) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -147,9 +157,11 @@ true -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprConstantBool):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprConstantBool) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -159,9 +171,11 @@ true -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprConstantNumber):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprConstantNumber) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -172,9 +186,11 @@ y -- after]] local expected = "local y = 3\n\t\tlocal x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprLocal):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -184,9 +200,11 @@ y -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprGlobal):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprGlobal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -196,9 +214,11 @@ y -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprVarargs):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprVarargs) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -208,9 +228,11 @@ print() -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprCall):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprCall) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -220,9 +242,11 @@ hello.world -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprIndexName):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprIndexName) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -232,9 +256,11 @@ hello['world'] -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprIndexExpr):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprIndexExpr) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -244,9 +270,11 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprFunction):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -256,9 +284,11 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprTable):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -268,9 +298,11 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprUnary):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprUnary) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -280,9 +312,11 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprBinary):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprBinary) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -292,9 +326,11 @@ function() return end -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprInterpString):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprInterpString) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -304,9 +340,11 @@ y :: number -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprTypeAssertion):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprTypeAssertion) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -316,9 +354,11 @@ if true then 'hello' else 'world' -- after]] local expected = "local x =\nreplaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExprIfElse):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExprIfElse) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -328,9 +368,11 @@ if true then 'hello' else 'world', z :: string -- after]] local expected = "local x, y =\nreplaced, replaced -- after" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isExpr):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isExpr) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -341,9 +383,11 @@ local y = 1 local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatBlock):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatBlock) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -358,9 +402,11 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatIf):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatIf) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -373,9 +419,11 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatWhile):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatWhile) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -388,9 +436,11 @@ until true local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatRepeat):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatRepeat) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -403,9 +453,11 @@ end local expected = "-- hello\nwhile true do\n\treplaced\nend\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatBreak):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatBreak) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -418,9 +470,11 @@ end local expected = "-- hello\nwhile true do\n\treplaced\nend\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatContinue):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatContinue) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -431,9 +485,11 @@ function foo() return 1, 2 end local expected = "-- hello\nfunction foo() replaced end\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatReturn):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatReturn) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -444,9 +500,11 @@ foobar() local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatExpr):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatExpr) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -457,9 +515,11 @@ local x = 1 local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatLocal):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -472,9 +532,11 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatFor):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatFor) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -487,9 +549,11 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatForIn):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatForIn) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -501,9 +565,11 @@ y = 1 local expected = "-- hello\nlocal y = 2\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatAssign):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatAssign) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -515,9 +581,11 @@ y -= 1 local expected = "-- hello\nlocal y = 2\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatCompoundAssign):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatCompoundAssign) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -530,9 +598,11 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatFunction):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -545,9 +615,11 @@ end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatLocalFunction):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatLocalFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -558,9 +630,11 @@ type foo = number local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatTypeAlias):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatTypeAlias) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -571,9 +645,11 @@ type function foo() return number end local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStatTypeFunction):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStatTypeFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -585,9 +661,11 @@ local y = 2 local expected = "-- hello\nreplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isStat):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isStat) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -598,9 +676,11 @@ type foo = number local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeReference):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeReference) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -611,9 +691,11 @@ type foo = true local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeSingletonBool):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeSingletonBool) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -624,9 +706,11 @@ local x : "hello" local expected = "-- hello\nlocal x : replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeSingletonString):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeSingletonString) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -637,9 +721,11 @@ type foo = typeof(123) local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeTypeof):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeTypeof) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -650,9 +736,11 @@ type foo = (number) local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeGroup):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeGroup) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -663,9 +751,11 @@ type foo = true? local expected = "-- hello\ntype foo = truereplaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeOptional):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeOptional) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -676,9 +766,11 @@ type foo = true? | false local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeUnion):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeUnion) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -689,9 +781,11 @@ type foo = true & false local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeIntersection):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeIntersection) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -702,9 +796,11 @@ type foo = { number } local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeArray):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeArray) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -715,9 +811,11 @@ type foo = {} local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeTable):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -728,9 +826,11 @@ type foo = (number) -> () local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypeFunction):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypeFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -741,9 +841,11 @@ type foo = (number) -> () local expected = "-- hello\ntype foo = replaced\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isType):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isType) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -754,9 +856,11 @@ type foo = (number) -> (number, ...string) local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypePackExplicit):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypePackExplicit) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -767,9 +871,11 @@ type foo = (number) -> (number, G...) local expected = "-- hello\ntype foo = (number) -> (number, replaced)\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypePackGeneric):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypePackGeneric) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -780,9 +886,11 @@ type foo = (number) -> (...number) local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypePackVariadic):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypePackVariadic) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -793,9 +901,11 @@ type foo = (number) -> (number, ...string) local expected = "-- hello\ntype foo = (number) -> (replaced)\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isTypePack):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isTypePack) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -806,9 +916,11 @@ local x, y = 1, 2 local expected = "-- hello\nlocal replaced, replaced = 1, 2\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isLocal):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -820,9 +932,11 @@ function foo() return end local expected = "-- hello\nreplaced\nfunction foo() return end\n-- world" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast, syntaxUtils.isAttribute):replace(function() - return "replaced" - end) + return query + .findallfromroot(ast :: any, syntaxUtils.isAttribute) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :replace(function() + return "replaced" + end) end, assert) end) @@ -837,7 +951,7 @@ function foo() return end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast, syntaxUtils.isExprGlobal) + .findallfromroot(ast :: any, syntaxUtils.isExprGlobal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :filter(function(g) return g.name.text == "baz" end) @@ -868,7 +982,7 @@ function foo() return end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast, syntaxUtils.isExprTable) + .findallfromroot(ast :: any, syntaxUtils.isExprTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted :flatmap(function(t) local l = {} for _, entry in t.entries do diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 89c0a6a06..8e241619c 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -32,48 +32,6 @@ lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:34:5-16 lute/cli/commands/test/filter.luau:62:6-17 lute/cli/commands/test/filter.luau:62:6-17 -lute/std/libs/syntax/printer.luau:110:3-15 -lute/std/libs/syntax/printer.luau:110:3-15 -lute/std/libs/syntax/printer.luau:154:3-26 -lute/std/libs/syntax/printer.luau:154:3-26 -lute/std/libs/syntax/query.luau:40:25-25 -lute/std/libs/syntax/query.luau:40:25-25 -lute/std/libs/syntax/query.luau:40:25-25 -lute/std/libs/syntax/query.luau:40:25-25 -lute/std/libs/syntax/query.luau:128:24-31 -lute/std/libs/syntax/query.luau:128:24-31 -lute/std/libs/syntax/query.luau:128:45-52 -lute/std/libs/syntax/query.luau:128:45-52 -lute/std/libs/syntax/query.luau:128:59-61 -lute/std/libs/syntax/query.luau:128:59-61 -lute/std/libs/syntax/query.luau:128:59-61 -lute/std/libs/syntax/query.luau:128:59-61 -lute/std/libs/syntax/query.luau:134:13-28 -lute/std/libs/syntax/query.luau:134:13-28 -lute/std/libs/syntax/query.luau:134:13-28 -lute/std/libs/syntax/query.luau:134:13-28 -lute/std/libs/syntax/query.luau:135:9-20 -lute/std/libs/syntax/query.luau:135:9-20 -lute/std/libs/syntax/query.luau:137:13-28 -lute/std/libs/syntax/query.luau:137:13-28 -lute/std/libs/syntax/query.luau:139:16-34 -lute/std/libs/syntax/query.luau:139:16-34 -lute/std/libs/syntax/utils/init.luau:62:12-24 -lute/std/libs/syntax/utils/init.luau:62:12-24 -lute/std/libs/syntax/utils/init.luau:62:31-31 -lute/std/libs/syntax/utils/init.luau:62:31-31 -lute/std/libs/syntax/utils/trivia.luau:7:12-47 -lute/std/libs/syntax/utils/trivia.luau:7:12-47 -lute/std/libs/syntax/utils/trivia.luau:30:12-47 -lute/std/libs/syntax/utils/trivia.luau:30:12-47 -lute/std/libs/syntax/visitor.luau:141:19-29 -lute/std/libs/syntax/visitor.luau:141:19-29 -lute/std/libs/syntax/visitor.luau:141:19-29 -lute/std/libs/syntax/visitor.luau:141:19-29 -lute/std/libs/syntax/visitor.luau:1036:21-25 -lute/std/libs/syntax/visitor.luau:1036:21-25 -lute/std/libs/syntax/visitor.luau:1036:21-25 -lute/std/libs/syntax/visitor.luau:1036:21-25 tests/src/packages/package_aware_require/dep/module.luau:1:16-30 tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau:1:16-38 tests/src/require/config_tests/config_ambiguity/requirer.luau:1:8-22 @@ -83,65 +41,3 @@ tests/src/require/without_config/ambiguous_directory_requirer.luau:1:16-58 tests/src/require/without_config/ambiguous_file_requirer.luau:1:16-53 tests/src/staticrequires/circular_a.luau:2:11-33 tests/src/staticrequires/circular_b.luau:2:11-33 -tests/std/syntax/printer.test.luau:32:11-31 -tests/std/syntax/printer.test.luau:46:11-31 -tests/std/syntax/printer.test.luau:60:11-31 -tests/std/syntax/printer.test.luau:74:11-100 -tests/std/syntax/printer.test.luau:93:11-100 -tests/std/syntax/printer.test.luau:109:11-100 -tests/std/syntax/printer.test.luau:126:11-31 -tests/std/syntax/printer.test.luau:138:11-31 -tests/std/syntax/printer.test.luau:150:11-31 -tests/std/syntax/printer.test.luau:162:11-31 -tests/std/syntax/printer.test.luau:175:11-31 -tests/std/syntax/printer.test.luau:187:11-31 -tests/std/syntax/printer.test.luau:199:11-31 -tests/std/syntax/printer.test.luau:211:11-31 -tests/std/syntax/printer.test.luau:223:11-31 -tests/std/syntax/printer.test.luau:235:11-31 -tests/std/syntax/printer.test.luau:247:11-31 -tests/std/syntax/printer.test.luau:259:11-31 -tests/std/syntax/printer.test.luau:271:11-31 -tests/std/syntax/printer.test.luau:283:11-31 -tests/std/syntax/printer.test.luau:295:11-31 -tests/std/syntax/printer.test.luau:307:11-31 -tests/std/syntax/printer.test.luau:319:11-31 -tests/std/syntax/printer.test.luau:331:11-31 -tests/std/syntax/printer.test.luau:344:11-31 -tests/std/syntax/printer.test.luau:361:11-31 -tests/std/syntax/printer.test.luau:376:11-31 -tests/std/syntax/printer.test.luau:391:11-31 -tests/std/syntax/printer.test.luau:406:11-31 -tests/std/syntax/printer.test.luau:421:11-31 -tests/std/syntax/printer.test.luau:434:11-31 -tests/std/syntax/printer.test.luau:447:11-31 -tests/std/syntax/printer.test.luau:460:11-31 -tests/std/syntax/printer.test.luau:475:11-31 -tests/std/syntax/printer.test.luau:490:11-31 -tests/std/syntax/printer.test.luau:504:11-31 -tests/std/syntax/printer.test.luau:518:11-31 -tests/std/syntax/printer.test.luau:533:11-31 -tests/std/syntax/printer.test.luau:548:11-31 -tests/std/syntax/printer.test.luau:561:11-31 -tests/std/syntax/printer.test.luau:574:11-31 -tests/std/syntax/printer.test.luau:588:11-31 -tests/std/syntax/printer.test.luau:601:11-31 -tests/std/syntax/printer.test.luau:614:11-31 -tests/std/syntax/printer.test.luau:627:11-31 -tests/std/syntax/printer.test.luau:640:11-31 -tests/std/syntax/printer.test.luau:653:11-31 -tests/std/syntax/printer.test.luau:666:11-31 -tests/std/syntax/printer.test.luau:679:11-31 -tests/std/syntax/printer.test.luau:692:11-31 -tests/std/syntax/printer.test.luau:705:11-31 -tests/std/syntax/printer.test.luau:718:11-31 -tests/std/syntax/printer.test.luau:731:11-31 -tests/std/syntax/printer.test.luau:744:11-31 -tests/std/syntax/printer.test.luau:757:11-31 -tests/std/syntax/printer.test.luau:770:11-31 -tests/std/syntax/printer.test.luau:783:11-31 -tests/std/syntax/printer.test.luau:796:11-31 -tests/std/syntax/printer.test.luau:809:11-31 -tests/std/syntax/printer.test.luau:823:11-31 -tests/std/syntax/printer.test.luau:839:11-100 -tests/std/syntax/printer.test.luau:870:11-100 From 7f9b2a751e43d54a8e48824557106be9a924d30a Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 19 Mar 2026 16:36:01 -0700 Subject: [PATCH 402/642] Adds a simple guessing game tutorial to the guides/ section (#886) This tutorial walks a user through some basics of writing programs with Luau: 1. How do you require standard library functionality 2. The difference between luau provided functions and lute provided functions 3. Writing a program that talks to the outside world 4. Input validation 5. Command line arguments in Luau --- docs/guide/index.md | 1 + docs/guide/writing-a-guessing-game.md | 225 ++++++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 docs/guide/writing-a-guessing-game.md diff --git a/docs/guide/index.md b/docs/guide/index.md index 54844b2bf..3a59ba2a9 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -6,3 +6,4 @@ order: 1 - [Installation](./installation) - [Hello World!](./hello-world) +- [Writing a Guessing Game](./writing-a-guessing-game) \ No newline at end of file diff --git a/docs/guide/writing-a-guessing-game.md b/docs/guide/writing-a-guessing-game.md new file mode 100644 index 000000000..b41ff5566 --- /dev/null +++ b/docs/guide/writing-a-guessing-game.md @@ -0,0 +1,225 @@ +--- +order: 4 +--- + +# Writing a Guessing Game + +### Introduction +We're going to walk through how you might build a small guessing game using Lute. Like we did in the previous chapter, make a new folder +for this project, along with a script called `main.luau`. + +This is a program that will take a user's input and compare it to a secret number that the program will generate. The user will then be prompted to input their guess, +and the program will tell them "Too high!" or "Too low!" until they guess right. + + +### Using the Lute standard library +To start, we'll need some way of getting user input. Thankfully, Lute provides a utility for this in the [`io`](../reference/std/io) library. In `main.luau`, add: +```luau +local io = require("@std/io") +``` + +This is a require statement - the `@std` is an alias, and `io` is the name of the library. The `@std` and `@lute` aliases, have been reserved by Lute to refer to the standard library and Lute internal libraries respectively. You can `require` any set of scripts that return values, even ones you write locally. +This module returns a table with one field - `input`, which waits for the user to type something into the terminal. We can assign this table a value so we can refer to it later. + +```luau +local io = require("@std/io") + +print("Say something! ") +local input : string = io.input() +print(input) +``` + +Now, run this with `lute main.luau`. The program will pause until you input something, then print it back to you: +```bash +lute main.luau +>>> Say something! hello +hello +``` + +### Generating random numbers with Luau builtins +Let's move on to implementing this guessing game. We need to generate a random number, which Luau already provides via the `random` function on the `math` table. + +This highlights an important distinction between Luau and Lute - **Luau** provides builtin functions that operate on the primitive types, e.g. `string`, `math`, `buffer`, etc, while **Lute** provides utilities for interacting with the outside world e.g. user input, the file system, http requests. In this case, the `math.random` function is provided by Luau and will suffice for our purposes. We'll set a limit of 100, so that math.random will not generate numbers that are too small. + +```luau +local io = require("@std/io") +local randomNumber : number = math.random(100) + +print("Guess a number: ") +local input : string = io.input() +local guess = tonumber(input) +``` + +So now we have the guessed value - we should try to let the user know if they got it right or wrong. + +::: info +The output of `input()` is a string, and we want to compare it to a number. If you try to ask `x \< randomNumber`, this won't work correctly. We'll want to convert this using the [`tonumber`](https://www.lua.org/manual/5.1/manual.html#pdf-tonumber) function. +::: + + +```luau +local io = require("@std/io") +local randomNumber = math.random(100) + +print("Guess a number: ") +local input : string = io.input() + + +if guess > randomNumber then + print("Too high!") +elseif guess < randomNumber then + print("Too low!") +else + print("Got it!") +end +``` + +So far so good! Ideally though, the user would be able to guess multiple times - in this example, the user has one chance. Let's even the odds a bit - we'll need to re-prompt the user if they got it wrong and exit out if they guess right. + +### Adding a Loop + +```luau +local io = require("@std/io") +local randomNumber = math.random(100) + +local guessedRight = false +while not guessedRight do + print("Guess a number: ") + local input = io.input() + local guess = tonumber(input) + if guess > randomNumber then + print("Too high!") + elseif guess < randomNumber then + print("Too low!") + else + print("Got it!") + guessedRight = true + end +end +``` + +You can see that here, we've added a variable and a loop to check whether or not the user got the guess right. After checking at the beginning of the loop, we prompt them, they respond, and if they get it right, we update the value of guessedRight. Try running this program to see if you can guess the number! + +### Input Validation +We glanced over what happens if the input provided by the user isn't a number. In this case, `tonumber` will return nil - and we forgot to check for that. Let's add that right now! + +```luau +local io = require("@std/io") +local randomNumber = math.random(100) + +local guessedRight = false +while not guessedRight do + print("Guess a number: ") + local input = io.input() + local guess : number? = tonumber(input) + if not guess then + print(`Not a number: {input}`) + continue + end + if guess > randomNumber then + print("Too high!") + elseif guess < randomNumber then + print("Too low!") + else + print("Got it!") + guessedRight = true + end +end +``` +::: info +Notice the `continue` in the `if not guess then` check. If a user types "hello" instead of "5", tonumber returns nil. Without this check, the program would crash. Using continue allows us to skip the rest of the loop and ask the user again. +::: + +### Handling command line arguments +To finish up, let's see how we can pass arguments to programs in Lute to configure their behaviour. + +For our game, let's try to configure the maximum size of the random number being generated. In Luau, command line arguments can be unpacked at the top of the script using the syntax (`...`), which unpacks the variadic list of arguments passed to the script. For example: + +```luau +local args = {...} -- ... are the arguments passed to this script, and they're being unpacked into an array. +``` + +The first argument, stored in `args[1]` is always the script name, with the remaining arguments following after. If the user passes a `--max ` argument, we should set the maximum value of the number being generated to that value, defaulting to a 100. We can write a small helper function to validate this information: + +```luau +local io = require("@std/io") +local args = {...} +local function getArgs(args) : number + if #args < 3 then + error("Didn't pass enough arguments") + elseif #args \< 2 then + return 100 + else + if args[2] == "--max" then + local argument = tonumber(args[3]) + if argument then + return argument + end + end + end + return 100 +end +... +``` + +Here, we've written a function that will error when the user inputs not enough arguments, the number passed to the `--max` argument, or the default value of 100 in all other cases. Putting it all together gets us: + +```luau +local io = require("@std/io") +local args = { ... } + +local function getArgs(args) : number + if #args == 2 then + error("Didn't pass enough arguments") + elseif #args < 2 then + return 100 + else + if args[2] == "--max" then + local argument = tonumber(args[3]) + if argument then + return argument + end + end + end + return 100 +end + +local maxValue = getArgs(args) -- figure out what the --max argument is or use a 100 +local randomNumber = math.random(maxValue) + +local guessedRight = false +while not guessedRight do + print("Guess a number: ") + local input = io.input() + local guess : number? = tonumber(input) + if not guess then + print(`Not a number: {input}`) + continue + end + if guess > randomNumber then + print("Too high!") + elseif guess < randomNumber then + print("Too low!") + else + print("Got it!") + guessedRight = true + end +end +``` + +If you try this program out now, you should be able to run it like: +```bash +lute main.luau --max 50 +``` +to re-run the guessing game with maximum 50 guesses. + +### Conclusion +Congratulations! You've build your first Lute program. In this chapter we covered: +1. How to use `require` to use standard library functionality +2. The difference between functions and libraries provided by Lute and Luau +3. How to write a program that talks to the outside world (your terminal) +4. How to do some basic input validation +5. The structure and parsing of command line arguments in Luau programs. + +## TODOS: +- Perhaps implement a lute new \ command, that creates a main.luau file, setups a luaurc, creates a tests directory? \ No newline at end of file From 96d1afa75313ecb1639c12a2dc8a3c7149447a9b Mon Sep 17 00:00:00 2001 From: Master Oogway <125769645+ActualMasterOogway@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:38:33 +0100 Subject: [PATCH 403/642] Added path normalization in `check.luau` (#882) Normalizing all paths in `tools/check.luau`, fixing issues on Windows Fixes #881 Please add the `infra` tag as I am unable to --- tools/check.luau | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/check.luau b/tools/check.luau index 64f5cf28c..1798e42e3 100644 --- a/tools/check.luau +++ b/tools/check.luau @@ -23,12 +23,18 @@ local EXCLUDE_TYPECHECK = { "tests/std/snapshots", } +local function posixify(p: path.pathlike): string + local normalized = path.normalize(p) + local posixy, _ = string.gsub(path.format(normalized), "\\", "/") + return posixy +end + local function collectFiles(dir: string): { string } local files: { string } = {} local it = fs.walk(dir, { recursive = true }) local p = it() while p do - local pathStr = path.format(p) + local pathStr = posixify(p) local excluded = false for _, excl in EXCLUDE_TYPECHECK do if string.find(pathStr, excl, 1, true) then @@ -62,7 +68,7 @@ local function readFaillist(): string return "" end - local contents = fs.read(file) + local contents = string.gsub(fs.read(file), "\r", "") fs.close(file) return contents end @@ -116,7 +122,7 @@ local function parseErrors(output: string, cwdStr: string): { ErrorEntry } if file and lineNum then table.insert(errors, { - file = relativePath(file, cwdStr), + file = relativePath(posixify(file), cwdStr), line = tonumber(lineNum) :: number, col = tonumber(col) :: number, colEnd = tonumber(colEnd) :: number, @@ -171,7 +177,7 @@ local function main(args: { string }) print("Running typecheck...") local output = runTypecheck(lutePath, allFiles) - local cwdStr = path.format(process.cwd()) + local cwdStr = posixify(process.cwd()) local errors = parseErrors(output, cwdStr) local sortedErrors = sortErrors(errors) local formattedOutput = formatFaillist(sortedErrors) From c3c821c5dd2d50b86931753dff17bb6ba5c09030 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 20 Mar 2026 14:25:32 -0700 Subject: [PATCH 404/642] Fixes the task.spawn, task.defer, and task.resume methods (#874) Resolves #522 This PR refactors `task.spawn, defer, resume` to share most of their logic for creating the stack and handing control off to newly created threads. The cause of the bug in #522 was that the stack didn't get set up correctly in the parent stack due to `lua_xmove` popping from the top, so the value that got returned was not callable with `coroutine.status` (because it wasn't a thread). The code I've added documents the invariant that I wanted to maintain along with helpers for correctly setting up the stack. I've added tests for the following cases: {task.defer, task.spawn, task.resume } called with { thread | function (except for task.resume, which only takes a thread passed { 0 arguments to resume with, > 0 arguments to resume with }. I also introduced 'deferSelf' for the argumentless version of task.defer that says to immediately yield the current thread. --- definitions/task.luau | 12 +- examples/coroutine_error_isolation.luau | 4 +- lute/runtime/src/runtime.cpp | 21 ++- lute/std/libs/task.luau | 4 +- lute/task/CMakeLists.txt | 2 +- lute/task/include/lute/task.h | 8 +- lute/task/src/task.cpp | 186 ++++++++++++++++-------- tests/lute/task.test.luau | 127 ++++++++++++++++ 8 files changed, 290 insertions(+), 74 deletions(-) diff --git a/definitions/task.luau b/definitions/task.luau index 68b479a17..ef67d87f7 100644 --- a/definitions/task.luau +++ b/definitions/task.luau @@ -3,19 +3,23 @@ local time = require("./time") local task = {} -function task.defer() +function task.spawn(routine: ((T...) -> U...) | thread, ...: T...): thread error("unimplemented") end -function task.wait(dur: (number | time.Duration)?): number +function task.defer(routine: ((T...) -> U...) | thread, ...: T...): thread error("unimplemented") end -function task.spawn(routine: ((T...) -> U...) | thread, ...: T...): thread +function task.resume(thread: thread): thread error("unimplemented") end -function task.resume(thread: thread): thread +function task.deferSelf() + error("unimplemented") +end + +function task.wait(dur: (number | time.Duration)?): number error("unimplemented") end diff --git a/examples/coroutine_error_isolation.luau b/examples/coroutine_error_isolation.luau index a792f2288..7184270b9 100644 --- a/examples/coroutine_error_isolation.luau +++ b/examples/coroutine_error_isolation.luau @@ -2,7 +2,7 @@ local task = require("@lute/task") local function loop() while true do - task.defer() + task.deferSelf() -- It may be useful to remove this print because it gets spammed, or implement another way -- in which you can determinatively tell whether or not the thread is still active despite @@ -13,7 +13,7 @@ end local function otherloop() while true do - task.defer() + task.deferSelf() if math.random(1, 2) == 1 then error("oops") diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 2602b68e9..ac3d4684b 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -93,6 +93,25 @@ RuntimeStep Runtime::runOnce() int status = LUA_OK; + // It's possible for a spawned task to be killed by a coroutine.close() + // before it gets processed in the runningThreads queue. This leads to situations where a thread was scheduled to resume + // but has already been killed. + + // One example: + // 1) Main thread executes task.defer on a coroutine.create thread + // 2) Code is queued up on the thread + // 3) coroutine.cancel is invoked + // 4) runtime evaluates callbacks + // 5) runtime evaluates running threads <- UH OH, found a thread that was scheduled to resume but has already been killed + // 6) We can just step over it, because + // a) if it scheduled a resume, the corresponding pending token will have been cleared + // b) the corresponding ref for the lua state will be freed at the end of Runtime::runOnce() + int co_status = lua_costatus(GL, L); + if (co_status == LUA_COFIN) + { + return StepSuccess{L}; + } + if (!next.success) status = lua_resumeerror(L, nullptr); else @@ -311,10 +330,8 @@ void ResumeTokenData::complete(std::function cont) ResumeToken getResumeToken(lua_State* L) { ResumeToken token = std::make_shared(); - token->runtime = getRuntime(L); token->ref = getRefForThread(L); - token->runtime->addPendingToken(); return token; diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index b4cb1c53f..c0d6ab986 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -33,7 +33,7 @@ function tasklib.await(t: task): any end while t.success == nil do - task.defer() + task.deferSelf() end if t.success then @@ -59,7 +59,7 @@ function tasklib.awaitall(...: task): ...unknown for _, v in tasks do if v.success == nil then done = false - task.defer() + task.deferSelf() end end end diff --git a/lute/task/CMakeLists.txt b/lute/task/CMakeLists.txt index ed796ef88..01d9daa05 100644 --- a/lute/task/CMakeLists.txt +++ b/lute/task/CMakeLists.txt @@ -8,5 +8,5 @@ target_sources(Lute.Task PRIVATE target_compile_features(Lute.Task PUBLIC cxx_std_17) target_include_directories(Lute.Task PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Task PRIVATE Lute.Runtime Lute.Time Luau.VM uv_a) +target_link_libraries(Lute.Task PRIVATE Lute.Runtime Lute.Common Lute.Time Luau.VM uv_a) target_compile_options(Lute.Task PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/task/include/lute/task.h b/lute/task/include/lute/task.h index 5b2b69d22..38227b198 100644 --- a/lute/task/include/lute/task.h +++ b/lute/task/include/lute/task.h @@ -12,19 +12,19 @@ namespace task { int lua_defer(lua_State* L); +int lua_deferSelf(lua_State* L); int lua_wait(lua_State* L); int lua_spawn(lua_State* L); int lute_resume(lua_State* L); int lua_delay(lua_State* L); static const luaL_Reg lib[] = { + {"spawn", lua_spawn}, {"defer", lua_defer}, + {"resume", lute_resume}, + {"deferSelf", lua_deferSelf}, {"wait", lua_wait}, - {"spawn", lua_spawn}, {"delay", lua_delay}, - - {"resume", lute_resume}, - {nullptr, nullptr}, }; diff --git a/lute/task/src/task.cpp b/lute/task/src/task.cpp index 30e07b9f7..5f3d950c5 100644 --- a/lute/task/src/task.cpp +++ b/lute/task/src/task.cpp @@ -1,9 +1,12 @@ #include "lute/task.h" +#include "lute/common.h" #include "lute/ref.h" #include "lute/runtime.h" #include "lute/time.h" +#include "Luau/Common.h" + #include "lua.h" #include "lualib.h" @@ -88,14 +91,134 @@ static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaT namespace task { -int lua_defer(lua_State* L) + +struct LuaThread { - Runtime* runtime = getRuntime(L); + LuaThread(lua_State* L, std::string_view caller = "") + : parent(L) + , runtime(getRuntime(parent)) + { + // At this point, the lua stack should look like: + // [function, args...] + // or + // [thread, args...] + + // If passed a thread, we push the 0 or more arguments onto that threads stack and resume it. + // If passed a function, we push the function and the 0 or more arguments onto a newly created thread's stack + int toPush = lua_gettop(parent); + // We only want to resume with the actual number of arguments here, regardless of if the first argument is a thread or a function + int numResumeArgs = toPush > 1 ? toPush - 1 : 0; + if (!lua_checkstack(parent, 1)) + luaL_error(L, "Not enough stack space to create a new thread"); + if (lua_isfunction(parent, 1)) + { + child = lua_newthread(parent); + } + else if (lua_isthread(parent, 1)) + { + child = lua_tothread(parent, 1); + lua_pushvalue(parent, 1); + lua_remove(parent, 1); + toPush--; + } + else + { + luaL_error(parent, "%s: Expected a thread or function as the first argument.", caller.data()); + } + this->resumeArgs = numResumeArgs; + + // At this point the invariant here is that the caller stack should look like: + // If the first argument was a function + // [function, 0 or more args to resume, thread to return ] + // If the first argument was a thread + // [ 0 or more args to resume the thread with, thread to return ] + if (!lua_checkstack(child, toPush)) + luaL_error(L, "Not enough stack space to push arguments to child thread"); + for (int i = 1; i <= toPush; i++) + lua_xpush(parent, child, i); + + // The top of the stack must be the thread that we will return + LUTE_ASSERT(lua_isthread(parent, -1)); + } - runtime->runningThreads.push_back({true, getRefForThread(L), 0}); - return lua_yield(L, 0); + int defer() + { + runtime->runningThreads.push_back({true, getRefForThread(child), resumeArgs}); + return 1; + } + + int resume() + { + auto st = getChildThreadStatus(); + bool suspended = st.first == LUA_COSUS; + if (!suspended) + { + luaL_error(parent, "cannot resume thread with status %s", st.second); + return 1; + } + + int resumeStatus = lua_resume(child, parent, resumeArgs); + bool ok = resumeStatus == LUA_OK || resumeStatus == LUA_YIELD || resumeStatus == LUA_BREAK; + + if (!ok) + { + runtime->reportError(child); + return 1; + } + + return 1; + } + +private: + std::pair getChildThreadStatus() + { + int st = lua_costatus(parent, child); + return {st, statnames[st]}; + } + + lua_State* parent = nullptr; + Runtime* runtime = nullptr; + lua_State* child = nullptr; + int resumeArgs = 0; }; +int lua_spawn(lua_State* L) +{ + LuaThread newThread{L, "task.spawn"}; + return newThread.resume(); +} + +int lua_deferSelf(lua_State* L) +{ + if (lua_gettop(L) != 0) + { + luaL_error(L, "task.deferSelf does not take any arguments"); + return 0; + } + lua_pushthread(L); + LuaThread newThread{L, "task.deferSelf"}; + newThread.defer(); + return lua_yield(L, 0); +} + +int lua_defer(lua_State* L) +{ + + LuaThread newThread{L, "task.defer"}; + return newThread.defer(); +} + +int lute_resume(lua_State* L) +{ + if (!lua_isthread(L, 1)) + { + luaL_error(L, "Expected a thread as the first argument."); + return 0; + } + LuaThread newThread{L, "task.resume"}; + return newThread.resume(); +} + int lua_delay(lua_State* L) { @@ -157,32 +280,6 @@ int lua_delay(lua_State* L) return 1; } -int lua_spawn(lua_State* L) -{ - if (lua_isfunction(L, 1)) - { - lua_State* NL = lua_newthread(L); - - // transfer arguments from other lua_State to the called lua_State - lua_xpush(L, NL, 1); - - // remove the function arg - lua_remove(L, 1); - - // insert the thread - lua_insert(L, 1); - } - else if (!lua_isthread(L, 1)) - { - luaL_error(L, "can only pass threads or functions to task.spawn"); - } - - lute_resume(L); - - // return the thread - return 1; -} - int lua_wait(lua_State* L) { int type = lua_type(L, 1); @@ -212,35 +309,6 @@ int lua_wait(lua_State* L) return lua_yield(L, 0); } -int lute_resume(lua_State* L) -{ - Runtime* runtime = getRuntime(L); - - lua_State* thread = lua_tothread(L, 1); - luaL_argexpected(L, thread, 1, "thread"); - - int currentThreadStatus = lua_costatus(L, thread); - if (currentThreadStatus != LUA_COSUS) - { - luaL_errorL(L, "cannot resume %s coroutine", statnames[currentThreadStatus]); - - return 1; - }; - - lua_remove(L, 1); - - int args = lua_gettop(L); - lua_xmove(L, thread, args); - - int resumptionStatus = lua_resume(thread, L, args); - if (resumptionStatus != LUA_OK && resumptionStatus != LUA_YIELD && resumptionStatus != LUA_BREAK) - { - runtime->reportError(thread); - } - - return 0; -} - } // namespace task int luaopen_task(lua_State* L) diff --git a/tests/lute/task.test.luau b/tests/lute/task.test.luau index 43638ed5a..0af452e8c 100644 --- a/tests/lute/task.test.luau +++ b/tests/lute/task.test.luau @@ -22,4 +22,131 @@ test.suite("LuteTaskSuite", function(suite) assert.eq(endTime - startTime >= 0.5, true) end) + + suite:case("taskSpawn0ArgsErrors", function(assert) + assert.errors(function() + (task.spawn :: any)() + end) + end) + + suite:case("taskDefer0ArgsErrors", function(assert) + assert.errors(function() + (task.defer :: any)() + end) + end) + + suite:case("taskSpawnFunctionNoArgsCoroutineStatus", function(assert) + local thread = task.spawn(function() + task.wait(2) + end) + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) + + -- task.spawn with thread, 0 args + suite:case("taskSpawnThreadNoArgsCoroutineStatus", function(assert) + local co = coroutine.create(function() + task.wait(2) + end) + local thread = task.spawn(co) + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) + + -- task.spawn with function, >0 args + suite:case("taskSpawnFunctionWithArgsCoroutineStatus", function(assert) + local thread = task.spawn(function(_a, _b, _c) + task.wait(2) + end, "hello", "world", "boom") + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) + + -- task.spawn with thread, >0 args + suite:case("taskSpawnThreadWithArgsCoroutineStatus", function(assert) + local co = coroutine.create(function(_a, _b, _c) + task.wait(2) + end) + local thread = task.spawn(co, "hello", "world", "boom") + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) + + -- task.defer with function, 0 args + suite:case("taskDeferFunctionNoArgsCoroutineStatus", function(assert) + local thread = task.defer(function() + task.wait(2) + end) + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) + + -- task.defer with thread, 0 args + suite:case("taskDeferThreadNoArgsCoroutineStatus", function(assert) + local co = coroutine.create(function() + task.wait(2) + end) + local thread = task.defer(co) + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) + + -- task.defer with function, >0 args + suite:case("taskDeferFunctionWithArgsCoroutineStatus", function(assert) + local thread = task.defer(function(_a, _b, _c) + task.wait(2) + end, "hello", "world", "boom") + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) + + -- task.defer with thread, >0 args + suite:case("taskDeferThreadWithArgsCoroutineStatus", function(assert) + local co = coroutine.create(function(_a, _b, _c) + task.wait(2) + end) + local thread = task.defer(co, "hello", "world", "boom") + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) + + -- task.resume with thread, 0 args + suite:case("taskResumeThreadNoArgsCoroutineStatus", function(assert) + local co = coroutine.create(function() + task.wait(2) + end) + local thread = task.resume(co) + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) + + -- task.resume with thread, >0 args + suite:case("taskResumeThreadWithArgsCoroutineStatus", function(assert) + local co = coroutine.create(function(_a, _b, _c) + task.wait(2) + end) + local thread = task.resume(co, "hello", "world", "boom") + + local status = coroutine.status(thread) + assert.eq(status, "suspended") + coroutine.close(thread) + end) end) From 5d3c690d11f78781fa5be3b45a9372b69ba90425 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 20 Mar 2026 16:04:26 -0700 Subject: [PATCH 405/642] Restricts codemod replacements to just strings (#891) Consulted with @wmccrthy and existing ones already only use strings. Now that AST nodes are frozen, constructing them is a bit more of a pain, and this simplifies the codemod story. --- lute/std/libs/syntax/printer.luau | 17 ++++++----------- lute/std/libs/syntax/query.luau | 8 ++++---- lute/std/libs/syntax/types.luau | 3 +-- tests/std/syntax/printer.test.luau | 22 +++++++++------------- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index c1611eda9..9ddcca5c4 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -17,7 +17,7 @@ type PrintVisitor = visitor.Visitor & { printToken: (self: PrintVisitor, token: types.Token) -> (), printString: (self: PrintVisitor, expr: types.AstExprConstantString | types.AstTypeSingletonString) -> (), printInterpolatedString: (self: PrintVisitor, expr: types.AstExprInterpString) -> (), - printReplacement: (self: PrintVisitor, node: types.AstNode, replacement: types.replacement) -> (), + printReplacement: (self: PrintVisitor, node: types.AstNode, replacement: string) -> (), } local function exhaustiveMatch(value: never): never error(`Unknown value in exhaustive match: {value}`) @@ -98,19 +98,14 @@ local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprIn end end -local function printReplacement(self: PrintVisitor, node: types.AstNode, replacement: types.replacement) +local function printReplacement(self: PrintVisitor, node: types.AstNode, replacement: string) local leftmostTrivia = triviaUtils.leftmosttrivia(node) if leftmostTrivia ~= nil then self:printTriviaList(leftmostTrivia) end - if typeof(replacement) == "string" then - self:write(replacement) - elseif replacement.kind ~= nil then - visitor.visit(replacement :: any, self) -- LUAUFIX: Code too complex emitted in absence of cast because AstNode is very large - else - error("Unsupported replacement type") - end + assert(typeof(replacement) == "string", "Unsupported replacement type") + self:write(replacement) local rightmostTrivia = triviaUtils.rightmosttrivia(node) if rightmostTrivia ~= nil then @@ -147,11 +142,11 @@ local function printVisitor(replacements: types.replacements?) if replacements == nil then return true end - local replacement = replacements[node] + local replacement: string? = replacements[node] if replacement == nil then return true end - printer:printReplacement(node, replacement :: any) -- remove any cast once code to complex no longer emitted + printer:printReplacement(node, replacement) return false end diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 20b2d40ed..f6d95013f 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -11,7 +11,7 @@ type node = types.AstNode export type query = { nodes: { T }, filter: (self: query, pred: (T) -> boolean) -> query, - replace: (self: query, repl: (T) -> types.replacement?) -> types.replacements, + replace: (self: query, repl: (T) -> string?) -> types.replacements, map: (self: query, fn: (T) -> U?) -> query, -- recursion violation reported foreach: (self: query, callback: (T) -> ()) -> query, findall: (self: query, fn: (node) -> U?) -> query, @@ -32,12 +32,12 @@ function queryLib.filter(self: query, pred: (T) -> boolean): query return self end -function queryLib.replace(self: query, repl: (T) -> types.replacement?): types.replacements +function queryLib.replace(self: query, repl: (T) -> string?): types.replacements local replacements = {} for _, node in self.nodes do local r = repl(node) if r ~= nil then - replacements[node] = r :: any -- LUAUFIX: remove any cast once code too complex no longer emitted + replacements[node] = r end end return replacements @@ -131,7 +131,7 @@ function queryLib.findallfromroot(ast: types.ParseResult | node, fn: (node) - return { nodes = nodes, filter = queryLib.filter, - replace = queryLib.replace :: any, -- LUAUFIX: Remove any cast once code too complex no longer emitted + replace = queryLib.replace, map = queryLib.map :: any, -- LUAUFIX: Remove any cast once recursion restriction is loosened foreach = queryLib.foreach, findall = queryLib.findall :: any, -- LUAUFIX: Remove any cast once recursion restriction is loosened diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index 409246115..c26f021cb 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -159,8 +159,7 @@ export type AstNode = luau.AstNode export type ParseResult = luau.ParseResult -export type replacement = string | AstNode -export type replacements = { [AstNode]: replacement } +export type replacements = { [AstNode]: string } export type Type = luau.Type diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 8fe3860ae..47dfdc19a 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -29,11 +29,9 @@ test.suite("SyntaxPrinter", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query - .findallfromroot(ast :: any, syntaxUtils.isExprConstantString) - :replace(function(s) -- LUAUFIX: Remove any cast once code too complex no longer emitted - return if s.token.text == "World" then "1 + 2" else nil - end) + return query.findallfromroot(ast :: any, syntaxUtils.isExprConstantString):replace(function(s) + return if s.token.text == "World" then "1 + 2" else nil + end) end, assert) end) @@ -45,11 +43,9 @@ test.suite("SyntaxPrinter", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query - .findallfromroot(ast :: any, syntaxUtils.isExprConstantString) - :replace(function(s) -- LUAUFIX: Remove any cast once code too complex no longer emitted - return if s.token.text == "World" then syntax.parseexpr("1 + 2") else nil - end) + return query.findallfromroot(ast :: any, syntaxUtils.isExprConstantString):replace(function(s) + return if s.token.text == "World" then "1 + 2" else nil + end) end, assert) end) @@ -64,7 +60,7 @@ test.suite("SyntaxPrinter", function(suite) return query .findallfromroot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function(_) - return syntax.parseblock("local name = 1 + 2") + return "local name = 1 + 2" end) end, assert) end) @@ -83,7 +79,7 @@ test.suite("SyntaxPrinter", function(suite) return assign.variables[1].node.annotation end) :replace(function(_) - return ann + return printer.printnode(ann :: any) -- LUAUFIX: Remove any cast once code too complex no longer emitted end) end, assert) end) @@ -102,7 +98,7 @@ test.suite("SyntaxPrinter", function(suite) return if ta.type.tag == "function" then ta.type.returntypes else nil end) :replace(function(_) - return tp + return printer.printnode(tp) end) end, assert) end) From 6024ee4d4dab3700c7afab30d836b33a1248268d Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 20 Mar 2026 16:09:13 -0700 Subject: [PATCH 406/642] Implements a `lute new ` subcommand. (#888) I've added a subcommand to `lute` to make it easier to start new projects, similar to `zig init` or `cargo new`. This command is invoked with: ``` lute new ``` and sets up a project with the following structure: ``` project-name/ main.luau main.test.luau .config.luau ``` This command sets up the following default project configuration using the `.config.luau` format, with comments included for convenience. ```luau return { lute = { lint = { -- Array of globbed strings, representing paths to exempt from linting, ignores = {}, -- Global values passed to each linting rule, globals = {}, -- Array of strings from which to load local lint rules, rulepaths = {}, -- Per rule overrides, ruleconfigs = { -- Array of globbed strings, representing paths exempt from this rule, -- ignores: {string}?, -- Override a rule's default severity, -- severity: ("warn" | "error" | "info" | "hint")?, -- Pass custom options through to a rule, -- options: { [string]: unknown }?, -- Disable this rule, -- off: boolean?, }, }, }, luau = { -- Default language mode to use for typechecking, languagemode = "strict", -- Aliases for builtin libraries and lint rules aliases = { lute = "~/.lute/typedefs/0.1.0/lute", std = "~/.lute/typedefs/0.1.0/std", lint = "~/.lute/typedefs/0.1.0/lint", }, }, } ``` I think this is a good starting point, but I'm happy to take any suggestions for improvements to this command. I think this is a pretty sane set of defaults. I'd love to hear if folks have opinions on file names - `main.luau` isn't a common entry point, so we could do something like `projectName.luau` and `projectName.test.luau` instead of `main.luau` and `main.test.luau`. Changes I've made: 1. Extract some of the common logic between `lute setup` and `lute new` into a common file. 2. Update `lute test` to search for tests from the current directory, instead of a dedicated tests/ directory, This is informed by a discussion I had with @Nicell about Luau engineers colocating tests with source files. --- lute/cli/commands/lib/typedefs.luau | 109 ++++++++++++++++++++++++++++ lute/cli/commands/new/init.luau | 88 ++++++++++++++++++++++ lute/cli/commands/setup/init.luau | 43 +++++++---- lute/cli/commands/test/init.luau | 2 +- tests/cli/new.test.luau | 102 ++++++++++++++++++++++++++ 5 files changed, 328 insertions(+), 16 deletions(-) create mode 100644 lute/cli/commands/lib/typedefs.luau create mode 100644 lute/cli/commands/new/init.luau create mode 100644 tests/cli/new.test.luau diff --git a/lute/cli/commands/lib/typedefs.luau b/lute/cli/commands/lib/typedefs.luau new file mode 100644 index 000000000..b7c26f0bc --- /dev/null +++ b/lute/cli/commands/lib/typedefs.luau @@ -0,0 +1,109 @@ +local path = require("@std/path") +local process = require("@std/process") + +local TYPEDEFS_VERSION = "0.1.0" +local TYPEDEFS_BASE_DIR = `.lute/typedefs/{TYPEDEFS_VERSION}` +local TYPEDEFS_RELATIVE_DIR = `~/{TYPEDEFS_BASE_DIR}` +local TYPEDEFS_PATH = path.join(process.homedir(), TYPEDEFS_BASE_DIR) + +local TEMPLATE_RC_FILE: { [any]: any } = { + aliases = { + lint = `{TYPEDEFS_RELATIVE_DIR}/lint`, + lute = `{TYPEDEFS_RELATIVE_DIR}/lute`, + std = `{TYPEDEFS_RELATIVE_DIR}/std`, + }, +} + +local function indent(level: number) + return string.rep("\t", level) +end + +local function nest(src, parenIndent) + return `\{\n{src}\n{parenIndent}\}` +end + +local function kv(k, v, desc: string?) + local kvpair = `{k} = {v}` + if desc then + kvpair = kvpair .. `, -- {desc}` + end + return kvpair +end + +local function tbl(fields: { { value: string, description: string } } | { string }, level) + local buf: { string } = {} + local fieldIndent = indent(level + 1) + local parenIndent = indent(level) + for _, f in fields do + if typeof(f) == "string" then + table.insert(buf, `{fieldIndent}{f},`) + else + table.insert(buf, `{fieldIndent}-- {f.description}`) + table.insert(buf, `{fieldIndent}{f.value},`) + end + end + + return nest(table.concat(buf, "\n"), parenIndent) +end + +local function generateLuauConfig(languageMode: "strict" | "nonstrict"): string + local aliases = { + kv("lute", `"{TYPEDEFS_RELATIVE_DIR}/lute"`), + kv("std", `"{TYPEDEFS_RELATIVE_DIR}/std"`), + kv("lint", `"{TYPEDEFS_RELATIVE_DIR}/lint"`), + } + local aliasTable = tbl(aliases, 2) + local luau = tbl({ + { + value = kv("languagemode", `"{languageMode}"`), + description = "Default language mode to use for typechecking", + }, + { value = kv("aliases", aliasTable), description = "Aliases for builtin libraries" }, + }, 1) + return luau +end + +local function generateLuteConfig(): string + local ruleconfigs = tbl({ + `-- [RuleName] : \{`, + `-- Array of globbed strings, representing paths exempt from this rule`, + `-- ignores: \{string\}?`, + `-- Override a rule's default severity`, + `-- severity: ("warn" | "error" | "info" | "hint")?`, + `-- Pass custom options through to a rule`, + `-- options: \{ [string]: unknown \}?`, + `-- Disable this rule`, + `-- off: boolean?`, + `-- \}`, + }, 3) + local lint = tbl({ + { + value = kv("ignores", "{}"), + description = "Array of globbed strings, representing paths to exempt from linting", + }, + { value = kv("globals", "{}"), description = "Global values passed to each linting rule" }, + { value = kv("rulepaths", "{}"), description = "Array of strings from which to load local lint rules" }, + { + value = kv("ruleconfigs", ruleconfigs), + description = "Per rule overrides, with schema [RuleName] : {...}, where the schema is described below ", + }, + }, 2) + local lute = tbl({ kv("lint", lint) }, 1) + return lute +end + +local typedefs = {} + +typedefs.TYPEDEFS_PATH = TYPEDEFS_PATH +typedefs.TYPEDEFS_RELATIVE_DIR = TYPEDEFS_RELATIVE_DIR +typedefs.LUAURC_TEMPLATE = TEMPLATE_RC_FILE + +function typedefs.generateConfigDotLuau(languageMode: "strict" | "nonstrict"): string + local luauConfig = generateLuauConfig(languageMode) + local luteConfig = generateLuteConfig() + + local configDotLuau = "return " .. tbl({ kv("lute", luteConfig), kv("luau", luauConfig) }, 0) + return configDotLuau +end + +return table.freeze(typedefs) diff --git a/lute/cli/commands/new/init.luau b/lute/cli/commands/new/init.luau new file mode 100644 index 000000000..2bf7f87fe --- /dev/null +++ b/lute/cli/commands/new/init.luau @@ -0,0 +1,88 @@ +local cli = require("@batteries/cli") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") +local typedefs = require("./lib/typedefs") + +local USAGE = "Usage: lute new " + +local function printHelp() + print(USAGE) + print([[ +Create a new Lute project with a standard directory layout. + +ARGUMENTS: + Name of the project to create + +OPTIONS: + -h, --help Show this help message + +EXAMPLES: + lute new myproject +]]) +end + +local function main(...: string) + local args = cli.parser() + args:add("help", "flag", { help = "Show this help message", aliases = { "h" } }) + args:add("name", "positional", { help = "Name of the project to create" }) + args:parse({ ... }) + + if args:has("help") then + printHelp() + return + end + + local projectName = args:get("name") + if projectName == nil or projectName == "" then + print("Error: Missing required argument .\n\n" .. USAGE .. "\nUse --help for more information.") + process.exit(1) + end + + -- LUAUFIX - refinements aren't correctly being applied here. + -- At this point, we should know that ~ (projectName == nil or projectName == "") is true (which should imply that projectName ~= nil) + assert(projectName) + local projectPath = path.join(process.cwd(), projectName) + + if fs.exists(projectPath) then + print(`Error: Directory '{projectName}' already exists.`) + process.exit(1) + end + + if not fs.exists(typedefs.TYPEDEFS_PATH) then + print("Running lute setup first...") + local result = process.run({ path.format(process.execpath()), "setup" }, { stdio = "inherit" }) + if not result.ok then + print("Error: lute setup failed.") + process.exit(1) + end + end + + fs.createdirectory(projectPath) + + local mainScript = path.join(projectPath, "main.luau") + local testScript = path.join(projectPath, "main.test.luau") + + fs.writestringtofile( + mainScript, + [[print("Hello, world!") +]] + ) + + fs.writestringtofile( + testScript, + [[local _test = require("@std/test") +]] + ) + + fs.writestringtofile(path.join(projectPath, ".config.luau"), typedefs.generateConfigDotLuau("strict")) + + print(`Created project '{projectName}'`) + print("") + print("To get started:") + print(`\tcd {projectName}`) + print("\tlute run main.luau") + print("\tlute test") +end + +main(...) diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 9a3fc069a..8f6d70b1f 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -1,21 +1,31 @@ +local cli = require("@batteries/cli") local definitions = require("@self/generated/definitions") local fs = require("@std/fs") local path = require("@std/path") local process = require("@std/process") local json = require("@std/json") +local typedefs = require("./lib/typedefs") -local TYPEDEFS_PATH = path.parse(".lute/typedefs/0.1.0") -local BASE_PATH = path.join(process.homedir(), TYPEDEFS_PATH) -local BASE_PATH_PRETTY = `~/{TYPEDEFS_PATH}` -local TEMPLATE_RC_FILE = { - aliases = { - lint = `{BASE_PATH_PRETTY}/lint`, - lute = `{BASE_PATH_PRETTY}/lute`, - std = `{BASE_PATH_PRETTY}/std`, - }, -} +local BASE_PATH = typedefs.TYPEDEFS_PATH +local BASE_PATH_PRETTY = typedefs.TYPEDEFS_RELATIVE_DIR +local TEMPLATE = typedefs.LUAURC_TEMPLATE -local args = { ... } +local args = cli.parser() +args:add("help", "flag", { help = "Show this help message", aliases = { "h" } }) +args:add("with-luaurc", "flag", { help = "Also write a .luaurc file in the current directory" }) +args:parse({ ... }) + +if args:has("help") then + print("Usage: lute setup [options]") + print([[ +Install Lute type definitions to ~/.lute/typedefs/{version}. + +OPTIONS: + --with-luaurc Also write a .luaurc file in the current directory + -h, --help Show this help message +]]) + return +end -- TODO we're assuming the files are completely unmodified, but we really shouldn't do that. print(`Writing definitions at {BASE_PATH_PRETTY}`) @@ -31,11 +41,14 @@ for key, value in definitions :: { [string]: string } do end local typedefsLuauRcPath = path.join(BASE_PATH, ".luaurc") -fs.writestringtofile(typedefsLuauRcPath, json.serialize(TEMPLATE_RC_FILE, true)) + +-- LUAUFIX. Without an annotation on typedefs.LUAURC_TEMPLATE, we arent able to figure out the type of TEMPLATE +-- If that table was declared in this file, this typechecks, but if it's in a different one, we'll get a type error here +fs.writestringtofile(typedefsLuauRcPath, json.serialize(TEMPLATE, true)) print(`Successfully wrote type definition files to {BASE_PATH_PRETTY}`) -if not table.find(args, "--with-luaurc") then +if not args:has("with-luaurc") then return end @@ -48,11 +61,11 @@ if fs.exists(luauRcPath) then rcFile = json.deserialize(contents) :: any rcFile.aliases = rcFile.aliases or {} - for key, value in TEMPLATE_RC_FILE.aliases :: { [string]: string } do + for key, value in TEMPLATE.aliases :: { [string]: string } do rcFile.aliases[key] = value end else - rcFile = TEMPLATE_RC_FILE + rcFile = TEMPLATE end fs.writestringtofile(luauRcPath, json.serialize(rcFile, true)) diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index a5c751fdc..0f20dbd54 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -94,7 +94,7 @@ local function main(...: string) end end - local searchpath = if forwarded and #forwarded > 0 then forwarded else { "./tests" } + local searchpath = if forwarded and #forwarded > 0 then forwarded else { "./" } loadtests(finder.findtestfiles(searchpath)) if args:has("list") then diff --git a/tests/cli/new.test.luau b/tests/cli/new.test.luau new file mode 100644 index 000000000..75cdacd26 --- /dev/null +++ b/tests/cli/new.test.luau @@ -0,0 +1,102 @@ +local fs = require("@std/fs") +local pathlib = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") +local test = require("@std/test") + +local tmpdir = system.tmpdir() +local workdir = pathlib.join(tmpdir, "lute-new") +local lutePath = pathlib.format(process.execpath()) +local projectName = "myproject" +local projectDir = pathlib.join(workdir, projectName) + +test.suite("LuteNew", function(suite) + suite:beforeeach(function() + if fs.exists(workdir) then + fs.removedirectory(workdir, { recursive = true }) + end + fs.createdirectory(workdir) + end) + + suite:case("createsProjectDirectory", function(assert) + local result = process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) + + assert.eq(result.exitcode, 0) + assert.eq(true, fs.exists(pathlib.join(workdir, projectName))) + end) + + suite:case("createsMainScript", function(assert) + process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) + + local mainPath = pathlib.join(workdir, projectName, "main.luau") + assert.eq(fs.exists(mainPath), true) + local contents = fs.readfiletostring(mainPath) + assert.strcontains(contents, "Hello, world!") + end) + + suite:case("createsTestScript", function(assert) + process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) + + local testPath = pathlib.join(workdir, projectName, "main.test.luau") + assert.eq(fs.exists(testPath), true) + local contents = fs.readfiletostring(testPath) + assert.strcontains(contents, "@std/test") + end) + + suite:case("createsConfigFile", function(assert) + process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) + + local configPath = pathlib.join(workdir, projectName, ".config.luau") + assert.eq(fs.exists(configPath), true) + local contents = fs.readfiletostring(configPath) + assert.strcontains(contents, "strict") + end) + + suite:case("printSuccessMessage", function(assert) + local result = process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, "Created project 'myproject'") + end) + + suite:case("failsWhenNoProjectNameGiven", function(assert) + local result = process.run({ lutePath, "new" }, { cwd = pathlib.format(workdir) }) + + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, "Missing required argument ") + end) + + suite:case("failsWhenDirectoryAlreadyExists", function(assert) + fs.createdirectory(pathlib.join(workdir, projectName)) + + local result = process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) + + assert.eq(result.exitcode, 1) + assert.strcontains(result.stdout, "already exists") + end) + + suite:case("showsHelpWithFlag", function(assert) + local result = process.run({ lutePath, "new", "--help" }, { cwd = pathlib.format(workdir) }) + + assert.eq(result.exitcode, 0) + assert.strcontains(result.stdout, "Usage: lute new ") + end) + + suite:case("typechecksOutOfTheBox", function(assert) + local result = process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) + assert.eq(result.exitcode, 0) + + -- Now, let's try typechecking this project + local r2 = process.run({ lutePath, "check", "main.luau" }, { cwd = pathlib.format(projectDir) }) + assert.eq(r2.exitcode, 0) + end) + + suite:case("passesTestsOutOfTheBox", function(assert) + local result = process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) + assert.eq(result.exitcode, 0) + + -- Now, let's run all of the tests as well (this should pass because there are no tests in empty projects) + local r2 = process.run({ lutePath, "test" }, { cwd = pathlib.format(projectDir) }) + assert.eq(r2.exitcode, 0) + end) +end) From 991e5a148b0d6f7424693dd84bfe48301d5c8a3b Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Fri, 20 Mar 2026 17:22:42 -0700 Subject: [PATCH 407/642] Add a lint rule to warn on functions which assign to global namespace (#893) --- docs/cli/lint/global_function_in_scope.md | 46 ++++++++++ lute/cli/commands/lint/init.luau | 1 + .../lint/rules/global_function_in_scope.luau | 58 ++++++++++++ tests/cli/lint.test.luau | 91 +++++++++++++++++++ tools/check-faillist.txt | 22 ++--- 5 files changed, 207 insertions(+), 11 deletions(-) create mode 100644 docs/cli/lint/global_function_in_scope.md create mode 100644 lute/cli/commands/lint/rules/global_function_in_scope.luau diff --git a/docs/cli/lint/global_function_in_scope.md b/docs/cli/lint/global_function_in_scope.md new file mode 100644 index 000000000..5016b5f0d --- /dev/null +++ b/docs/cli/lint/global_function_in_scope.md @@ -0,0 +1,46 @@ +--- +title: global_function_in_scope +--- +# global_function_in_scope + +This lint rule warns when a non-local `function` declaration appears inside a nested scope (e.g. inside an `if`, `for`, `while`, `do`, or another function body) rather than at the top level of the file. + +## Why this is discouraged + +A `function foo() end` declaration without the `local` keyword inside a nested scope implicitly creates or assigns to a global variable. This is almost always unintentional and can lead to: + +- Accidental pollution of the global namespace, where the function leaks out of the scope in which its being defined. +- Confusing behavior, because the assignment only happens when the enclosing scope executes. +- Harder-to-debug shadowing issues when another scope references the same global name. + +If the function is only needed locally, use `local function` instead. If it genuinely needs to be a top-level declaration, move it out of the nested scope. + +## Example violations + +`global_function_in_scope` will warn on the following: + +```luau +if true then + function foo() + end +end + +local function outer() + function inner() + end +end +``` + +You should instead do: + +```luau +if true then + local function foo() + end +end + +local function outer() + local function inner() + end +end +``` diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index c93b1bda2..d2207415d 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -22,6 +22,7 @@ local DEFAULT_RULES = { "divide_by_zero", "duplicate_keys", "empty_if_block", + "global_function_in_scope", "parenthesized_conditions", "unused_variable", } diff --git a/lute/cli/commands/lint/rules/global_function_in_scope.luau b/lute/cli/commands/lint/rules/global_function_in_scope.luau new file mode 100644 index 000000000..be61c7019 --- /dev/null +++ b/lute/cli/commands/lint/rules/global_function_in_scope.luau @@ -0,0 +1,58 @@ +local lintTypes = require("../types") +local path = require("@std/path") +local syntax = require("@std/syntax") +local visitorLib = require("@std/syntax/visitor") + +local name = "global_function_in_scope" +local message = + "Non-local function declaration found inside a nested scope. Use 'local function' instead, or move this to the top level." +local target = "https://lute.luau.org/cli/lint/global_function_in_scope.html" + +local function lint( + ast: syntax.AstStatBlock, + sourcepath: path.path?, + _context: lintTypes.RuleContext +): { lintTypes.LintViolation } + local violations: { lintTypes.LintViolation } = {} + local depth = 0 + + local visitor: visitorLib.Visitor = visitorLib.create() :: any + + function visitor.visitStatBlock(_: syntax.AstStatBlock) + depth += 1 + return true + end + + function visitor.visitStatBlockEnd(_: syntax.AstStatBlock) + depth -= 1 + end + + function visitor.visitStatFunction(stat: syntax.AstStatFunction) + -- depth > 1 means we're inside a nested block (not the top-level block) + if depth > 1 and stat.name.tag ~= "indexname" then + table.insert( + violations, + { + lintname = name, + location = stat.location, + message = message, + severity = "warning", + sourcepath = sourcepath, + target = target, + } :: lintTypes.LintViolation + ) + end + return true + end + + visitorLib.visit(ast, visitor) + + return violations +end + +local rule: lintTypes.LintRule = { + name = name, + lint = lint, +} + +return table.freeze(rule) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 16c3e1926..eae666f7e 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -16,6 +16,7 @@ local defaultRulesPaths = { divide_by_zero = path.format(path.join(defaultRulesFolder, "divide_by_zero.luau")), duplicate_keys = path.format(path.join(defaultRulesFolder, "duplicate_keys.luau")), empty_if_block = path.format(path.join(defaultRulesFolder, "empty_if_block.luau")), + global_function_in_scope = path.format(path.join(defaultRulesFolder, "global_function_in_scope.luau")), parenthesized_conditions = path.format(path.join(defaultRulesFolder, "parenthesized_conditions.luau")), unused_variable = path.format(path.join(defaultRulesFolder, "unused_variable.luau")), } @@ -26,6 +27,7 @@ local LINT_RULE_MESSAGES = { divide_by_zero = "warning[divide_by_zero]: Division by zero detected.", duplicate_keys = "warning[duplicate_keys]: Duplicate keys detected in table literal.", empty_if_block = "warning[empty_if_block]: Empty if block detected. Consider removing it or adding to it.", + global_function_in_scope = "warning[global_function_in_scope]: Non-local function declaration found inside a nested scope. Use 'local function' instead, or move this to the top level.", parenthesized_conditions = "info[parenthesized_conditions]: Luau doesn't require parentheses around conditions. You can remove them for readability.", unused_variable = "warning[unused_variable]: Unused variable detected. Prefix this variable with '_' to indicate that it's intentionally unused.", } @@ -3482,4 +3484,93 @@ end }, }) end) + + suite:case("globalFunctionInScopeWarnsInsideIf", function(assert) + lintTestHelper(assert, { + content = [[ +if true then + function foo() + end +end +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.global_function_in_scope, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.global_function_in_scope, count = 1 }, + }, + }) + end) + + suite:case("globalFunctionInScopeWarnsInsideFunction", function(assert) + lintTestHelper(assert, { + content = [[ +local function outer() + function inner() + end +end +]], + expectedExitCode = 1, + rulePath = defaultRulesPaths.global_function_in_scope, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.global_function_in_scope, count = 1 }, + }, + }) + end) + + suite:case("globalFunctionInScopeAllowsTopLevel", function(assert) + lintTestHelper(assert, { + content = [[ +function foo() +end + +function bar() +end +]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.global_function_in_scope, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.global_function_in_scope, count = 0 }, + }, + }) + end) + + suite:case("globalFunctionInScopeAllowsLocalFunctionNested", function(assert) + lintTestHelper(assert, { + content = [[ +if true then + local function foo() + end +end +]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.global_function_in_scope, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.global_function_in_scope, count = 0 }, + }, + }) + end) + + suite:case("globalFunctionInScopeAllowsTableMethods", function(assert) + lintTestHelper(assert, { + content = [[ +if true then + local t = {} + function t:foo() + end + function t.bar() + end +end +]], + expectedExitCode = 0, + rulePath = defaultRulesPaths.global_function_in_scope, + disableDefaultLints = true, + outputExpectations = { + { expectedOutput = LINT_RULE_MESSAGES.global_function_in_scope, count = 0 }, + }, + }) + end) end) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 8e241619c..f1004ddc0 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -13,17 +13,17 @@ examples/process.luau:19:7-15 examples/task-resume.luau:3:38-100 lute/cli/commands/lint/configutils.luau:19:55-73 lute/cli/commands/lint/configutils.luau:19:55-73 -lute/cli/commands/lint/init.luau:83:12-20 -lute/cli/commands/lint/init.luau:311:61-67 -lute/cli/commands/lint/init.luau:516:15-24 -lute/cli/commands/lint/init.luau:517:56-65 -lute/cli/commands/lint/init.luau:517:56-65 -lute/cli/commands/lint/init.luau:517:56-65 -lute/cli/commands/lint/init.luau:517:56-65 -lute/cli/commands/lint/init.luau:517:56-65 -lute/cli/commands/lint/init.luau:517:56-65 -lute/cli/commands/lint/init.luau:530:36-45 -lute/cli/commands/lint/init.luau:531:55-64 +lute/cli/commands/lint/init.luau:84:12-20 +lute/cli/commands/lint/init.luau:312:61-67 +lute/cli/commands/lint/init.luau:517:15-24 +lute/cli/commands/lint/init.luau:518:56-65 +lute/cli/commands/lint/init.luau:518:56-65 +lute/cli/commands/lint/init.luau:518:56-65 +lute/cli/commands/lint/init.luau:518:56-65 +lute/cli/commands/lint/init.luau:518:56-65 +lute/cli/commands/lint/init.luau:518:56-65 +lute/cli/commands/lint/init.luau:531:36-45 +lute/cli/commands/lint/init.luau:532:55-64 lute/cli/commands/lint/rules/almost_swapped.luau:92:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 From 556e65d3cc461c6f326adbfdc1e9a1fb1230040f Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:31:43 -0700 Subject: [PATCH 408/642] Rename all types to be PascalCase in lute APIs (#892) in the journey to shipping a stable version of Lute, we've decided to rename all types in lute APIs to be `PascalCase` from now on. This PR updates all the usages and type definitions, so will probably be a breaking change for users when merged --- definitions/crypto.luau | 20 +-- definitions/luau.luau | 136 +++++++++--------- lute/cli/commands/lint/init.luau | 6 +- lute/cli/commands/lint/lsp/init.luau | 2 +- .../commands/lint/rules/duplicate_keys.luau | 2 +- lute/cli/commands/lint/types.luau | 4 +- lute/cli/commands/test/filter.luau | 6 +- lute/cli/commands/test/init.luau | 4 +- lute/cli/commands/test/reporter.luau | 6 +- lute/cli/commands/transform/lib/types.luau | 2 +- lute/std/libs/fs.luau | 54 +++---- lute/std/libs/json.luau | 44 +++--- lute/std/libs/luau.luau | 6 +- lute/std/libs/net.luau | 6 +- lute/std/libs/process.luau | 12 +- lute/std/libs/syntax/init.luau | 4 +- lute/std/libs/syntax/printer.luau | 10 +- lute/std/libs/syntax/query.luau | 20 +-- lute/std/libs/syntax/types.luau | 4 +- lute/std/libs/task.luau | 10 +- lute/std/libs/test/assert.luau | 28 ++-- lute/std/libs/test/failure.luau | 8 +- lute/std/libs/test/init.luau | 10 +- lute/std/libs/test/reporter.luau | 4 +- lute/std/libs/test/runner.luau | 14 +- lute/std/libs/test/types.luau | 70 ++++----- lute/std/libs/time/duration.luau | 54 +++---- lute/std/libs/time/init.luau | 2 +- tests/cli/lint.test.luau | 4 +- tests/std/syntax/parser.test.luau | 6 +- tests/std/syntax/printer.test.luau | 4 +- tests/std/test.test.luau | 2 +- tools/check.luau | 2 +- 33 files changed, 283 insertions(+), 283 deletions(-) diff --git a/definitions/crypto.luau b/definitions/crypto.luau index da336d9db..9e9415b71 100644 --- a/definitions/crypto.luau +++ b/definitions/crypto.luau @@ -1,23 +1,23 @@ -- lute-lint-global-ignore(unused_variable) local crypto = {} -export type hash = { __hash: T } +export type Hash = { __hash: T } crypto.hash = table.freeze({ - md5 = {} :: hash<"md5">, - sha1 = {} :: hash<"sha1">, - sha256 = {} :: hash<"sha256">, - sha512 = {} :: hash<"sha512">, - blake2b256 = {} :: hash<"blake2b256">, + md5 = {} :: Hash<"md5">, + sha1 = {} :: Hash<"sha1">, + sha256 = {} :: Hash<"sha256">, + sha512 = {} :: Hash<"sha512">, + blake2b256 = {} :: Hash<"blake2b256">, }) -function crypto.digest(hash: hash, message: string | buffer): buffer +function crypto.digest(hash: Hash, message: string | buffer): buffer error("not implemented") end crypto.secretbox = {} -export type secretbox = { +export type SecretBox = { read ciphertext: buffer, read nonce: buffer, read key: buffer, @@ -27,11 +27,11 @@ function crypto.secretbox.keygen(): buffer error("not implemented") end -function crypto.secretbox.seal(message: string | buffer, key: buffer?): secretbox +function crypto.secretbox.seal(message: string | buffer, key: buffer?): SecretBox error("not implemented") end -function crypto.secretbox.open(box: secretbox): buffer +function crypto.secretbox.open(box: SecretBox): buffer error("not implemented") end diff --git a/definitions/luau.luau b/definitions/luau.luau index 475e11c18..ae5901885 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -1,38 +1,38 @@ -- lute-lint-global-ignore(unused_variable) local luau = {} -type spandata = { +type SpanData = { read beginline: number, read begincolumn: number, read endline: number, read endcolumn: number, } -type spanMT = { - read __lt: (a: spandata, b: spandata) -> boolean, +type SpanMT = { + read __lt: (a: SpanData, b: SpanData) -> boolean, } -export type span = setmetatable +export type Span = setmetatable luau.span = {} -function luau.span.create(tbl: { beginline: number, begincolumn: number, endline: number, endcolumn: number }): span +function luau.span.create(tbl: { beginline: number, begincolumn: number, endline: number, endcolumn: number }): Span error("not implemented") end export type Whitespace = { read tag: "whitespace", - read location: span, + read location: Span, read text: string, } export type SingleLineComment = { read tag: "comment", - read location: span, + read location: Span, read text: string, } export type MultiLineComment = { read tag: "blockcomment", - read location: span, + read location: Span, read text: string, -- TODO: depth: number, } @@ -41,7 +41,7 @@ export type Trivia = Whitespace | SingleLineComment | MultiLineComment export type Token = { read leadingtrivia: { Trivia }, - read location: span, + read location: Span, read text: Kind, read trailingtrivia: { Trivia }, read kind: "token", @@ -53,7 +53,7 @@ export type Pair = { read node: T, read separator: Token = { Pair } export type AstLocal = { - read location: span, + read location: Span, read kind: "local", read name: Token, read colon: Token<":">?, @@ -62,7 +62,7 @@ export type AstLocal = { } export type AstExprGroup = { - read location: span, + read location: Span, read kind: "expr", read tag: "group", read openparens: Token<"(">, @@ -71,14 +71,14 @@ export type AstExprGroup = { } export type AstExprConstantNil = { - read location: span, + read location: Span, read kind: "expr", read tag: "nil", read token: Token<"nil">, } export type AstExprConstantBool = { - read location: span, + read location: Span, read kind: "expr", read tag: "boolean", read value: boolean, @@ -86,7 +86,7 @@ export type AstExprConstantBool = { } export type AstExprConstantNumber = { - read location: span, + read location: Span, read kind: "expr", read tag: "number", read value: number, @@ -94,7 +94,7 @@ export type AstExprConstantNumber = { } export type AstExprConstantString = { - read location: span, + read location: Span, read kind: "expr", read tag: "string", read quotestyle: "single" | "double" | "block" | "interp", @@ -103,7 +103,7 @@ export type AstExprConstantString = { } export type AstExprLocal = { - read location: span, + read location: Span, read kind: "expr", read tag: "local", read token: Token, @@ -111,17 +111,17 @@ export type AstExprLocal = { read upvalue: boolean, } -export type AstExprGlobal = { read location: span, read kind: "expr", read tag: "global", read name: Token } +export type AstExprGlobal = { read location: Span, read kind: "expr", read tag: "global", read name: Token } export type AstExprVarargs = { - read location: span, + read location: Span, read kind: "expr", read tag: "vararg", read token: Token<"...">, } export type AstExprCall = { - read location: span, + read location: Span, read kind: "expr", read tag: "call", read func: AstExpr, @@ -129,11 +129,11 @@ export type AstExprCall = { read arguments: Punctuated, read closeparens: Token<")">?, read self: boolean, - read argLocation: span, + read argLocation: Span, } export type AstExprInstantiate = { - read location: span, + read location: Span, read kind: "expr", read tag: "instantiate", read expr: AstExpr, @@ -145,17 +145,17 @@ export type AstExprInstantiate = { } export type AstExprIndexName = { - read location: span, + read location: Span, read kind: "expr", read tag: "indexname", read expression: AstExpr, read accessor: Token<"." | ":">, read index: Token, - read indexlocation: span, + read indexlocation: Span, } export type AstExprIndexExpr = { - read location: span, + read location: Span, read kind: "expr", read tag: "index", read expression: AstExpr, @@ -165,7 +165,7 @@ export type AstExprIndexExpr = { } export type AstExprFunction = { - read location: span, + read location: Span, read kind: "expr", read tag: "function", read attributes: { AstAttribute }, @@ -189,7 +189,7 @@ export type AstExprFunction = { -- helper types for items contained in an AstExprTable, not actually AstExprs themselves export type AstTableExprListItem = { - read location: span, + read location: Span, read kind: "list", read value: AstExpr, read separator: Token<"," | ";">?, @@ -197,7 +197,7 @@ export type AstTableExprListItem = { } export type AstTableExprRecordItem = { - read location: span, + read location: Span, read kind: "record", read key: Token, read equals: Token<"=">, @@ -207,7 +207,7 @@ export type AstTableExprRecordItem = { } export type AstTableExprGeneralItem = { - read location: span, + read location: Span, read kind: "general", read indexeropen: Token<"[">, read key: AstExpr, @@ -221,7 +221,7 @@ export type AstTableExprGeneralItem = { export type AstTableExprItem = AstTableExprListItem | AstTableExprRecordItem | AstTableExprGeneralItem export type AstExprTable = { - read location: span, + read location: Span, read kind: "expr", read tag: "table", read openbrace: Token<"{">, @@ -230,7 +230,7 @@ export type AstExprTable = { } export type AstExprUnary = { - read location: span, + read location: Span, read kind: "expr", read tag: "unary", read operator: Token<"not" | "-" | "#">, @@ -238,7 +238,7 @@ export type AstExprUnary = { } export type AstExprBinary = { - read location: span, + read location: Span, read kind: "expr", read tag: "binary", read lhsoperand: AstExpr, @@ -247,7 +247,7 @@ export type AstExprBinary = { } export type AstExprInterpString = { - read location: span, + read location: Span, read kind: "expr", read tag: "interpolatedstring", read strings: { Token }, @@ -255,7 +255,7 @@ export type AstExprInterpString = { } export type AstExprTypeAssertion = { - read location: span, + read location: Span, read kind: "expr", read tag: "cast", read operand: AstExpr, @@ -272,7 +272,7 @@ export type AstElseIfExpr = { } export type AstExprIfElse = { - read location: span, + read location: Span, read kind: "expr", read tag: "conditional", read ifkeyword: Token<"if">, @@ -306,14 +306,14 @@ export type AstExpr = | AstExprIfElse export type AstStatBlock = { - read location: span, + read location: Span, read kind: "stat", read tag: "block", read statements: { AstStat }, } export type AstStatDo = { - read location: span, + read location: Span, read kind: "stat", read tag: "do", read dokeyword: Token<"do">, @@ -329,7 +329,7 @@ export type AstElseIfStat = { } export type AstStatIf = { - read location: span, + read location: Span, read kind: "stat", read tag: "conditional", read ifkeyword: Token<"if">, @@ -343,7 +343,7 @@ export type AstStatIf = { } export type AstStatWhile = { - read location: span, + read location: Span, read kind: "stat", read tag: "while", read whilekeyword: Token<"while">, @@ -354,7 +354,7 @@ export type AstStatWhile = { } export type AstStatRepeat = { - read location: span, + read location: Span, read kind: "stat", read tag: "repeat", read repeatkeyword: Token<"repeat">, @@ -363,17 +363,17 @@ export type AstStatRepeat = { read condition: AstExpr, } -export type AstStatBreak = { read location: span, read kind: "stat", read tag: "break", read token: Token<"break"> } +export type AstStatBreak = { read location: Span, read kind: "stat", read tag: "break", read token: Token<"break"> } export type AstStatContinue = { - read location: span, + read location: Span, read kind: "stat", read tag: "continue", read token: Token<"continue">, } export type AstStatReturn = { - read location: span, + read location: Span, read kind: "stat", read tag: "return", read returnkeyword: Token<"return">, @@ -381,14 +381,14 @@ export type AstStatReturn = { } export type AstStatExpr = { - read location: span, + read location: Span, read kind: "stat", read tag: "expression", read expression: AstExpr, } export type AstStatLocal = { - read location: span, + read location: Span, read kind: "stat", read tag: "local", read localkeyword: Token<"local">, @@ -398,7 +398,7 @@ export type AstStatLocal = { } export type AstStatFor = { - read location: span, + read location: Span, read kind: "stat", read tag: "for", read forkeyword: Token<"for">, @@ -415,7 +415,7 @@ export type AstStatFor = { } export type AstStatForIn = { - read location: span, + read location: Span, read kind: "stat", read tag: "forin", read forkeyword: Token<"for">, @@ -428,7 +428,7 @@ export type AstStatForIn = { } export type AstStatAssign = { - read location: span, + read location: Span, read kind: "stat", read tag: "assign", read variables: Punctuated, @@ -437,7 +437,7 @@ export type AstStatAssign = { } export type AstStatCompoundAssign = { - read location: span, + read location: Span, read kind: "stat", read tag: "compoundassign", read variable: AstExpr, @@ -446,13 +446,13 @@ export type AstStatCompoundAssign = { } export type AstAttribute = { - read location: span, + read location: Span, read kind: "attribute", read token: Token<"@checked" | "@native" | "@deprecated">, } export type AstStatFunction = { - read location: span, + read location: Span, read kind: "stat", read tag: "function", read name: AstExpr, @@ -460,7 +460,7 @@ export type AstStatFunction = { } export type AstStatLocalFunction = { - read location: span, + read location: Span, read kind: "stat", read tag: "localfunction", read localkeyword: Token<"local">, @@ -469,7 +469,7 @@ export type AstStatLocalFunction = { } export type AstStatTypeAlias = { - read location: span, + read location: Span, read kind: "stat", read tag: "typealias", read export: Token<"export">?, @@ -485,7 +485,7 @@ export type AstStatTypeAlias = { -- A type function export type AstStatTypeFunction = { - read location: span, + read location: Span, read kind: "stat", read tag: "typefunction", read export: Token<"export">?, @@ -530,7 +530,7 @@ export type AstGenericTypePack = { } export type AstTypeReference = { - read location: span, + read location: Span, read kind: "type", read tag: "reference", read prefix: Token?, @@ -542,7 +542,7 @@ export type AstTypeReference = { } export type AstTypeSingletonBool = { - read location: span, + read location: Span, read kind: "type", read tag: "boolean", read value: boolean, @@ -550,7 +550,7 @@ export type AstTypeSingletonBool = { } export type AstTypeSingletonString = { - read location: span, + read location: Span, read kind: "type", read tag: "string", read quotestyle: "single" | "double", @@ -558,7 +558,7 @@ export type AstTypeSingletonString = { } export type AstTypeTypeof = { - read location: span, + read location: Span, read kind: "type", read tag: "typeof", read typeof: Token<"typeof">, @@ -568,7 +568,7 @@ export type AstTypeTypeof = { } export type AstTypeGroup = { - read location: span, + read location: Span, read kind: "type", read tag: "group", read openparens: Token<"(">, @@ -576,10 +576,10 @@ export type AstTypeGroup = { read closeparens: Token<")">, } -export type AstTypeOptional = { read location: span, read kind: "type", read tag: "optional", read token: Token<"?"> } +export type AstTypeOptional = { read location: Span, read kind: "type", read tag: "optional", read token: Token<"?"> } export type AstTypeUnion = { - read location: span, + read location: Span, read kind: "type", read tag: "union", read leading: Token<"|">?, @@ -588,7 +588,7 @@ export type AstTypeUnion = { } export type AstTypeIntersection = { - read location: span, + read location: Span, read kind: "type", read tag: "intersection", read leading: Token<"&">?, @@ -596,7 +596,7 @@ export type AstTypeIntersection = { } export type AstTypeArray = { - read location: span, + read location: Span, read kind: "type", read tag: "array", read openbrace: Token<"{">, @@ -639,7 +639,7 @@ export type AstTableTypeItemProperty = { export type AstTableTypeItem = AstTableTypeItemIndexer | AstTableTypeItemStringProperty | AstTableTypeItemProperty export type AstTypeTable = { - read location: span, + read location: Span, read kind: "type", read tag: "table", read openbrace: Token<"{">, @@ -648,7 +648,7 @@ export type AstTypeTable = { } export type AstFunctionTypeParameter = { - read location: span, + read location: Span, read name: Token?, read colon: Token<":">?, read type: AstType, @@ -656,7 +656,7 @@ export type AstFunctionTypeParameter = { -- A type representing a Luau function export type AstTypeFunction = { - read location: span, + read location: Span, read kind: "type", read tag: "function", read opengenerics: Token<"<">?, @@ -685,7 +685,7 @@ export type AstType = | AstTypeFunction export type AstTypePackExplicit = { - read location: span, + read location: Span, read kind: "typepack", read tag: "explicit", read openparens: Token<"(">?, @@ -695,7 +695,7 @@ export type AstTypePackExplicit = { } export type AstTypePackGeneric = { - read location: span, + read location: Span, read kind: "typepack", read tag: "generic", read name: Token, @@ -703,7 +703,7 @@ export type AstTypePackGeneric = { } export type AstTypePackVariadic = { - read location: span, + read location: Span, read kind: "typepack", read tag: "variadic", --- May be nil when present as the vararg annotation in a function body diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index d2207415d..44b8a276e 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -335,7 +335,7 @@ local function lintString( end type fix = { - location: syntax.span, + location: syntax.Span, replacement: string, } @@ -578,7 +578,7 @@ local function main(...: string) if args:has("json") then local diagnostics = tableext.map(violations, lsp.diagnostic) - print(json.serialize(diagnostics :: json.array, true)) + print(json.serialize(diagnostics :: json.Array, true)) elseif #violations > 0 then if VERBOSE then print(`Printing violations from string input\n`) @@ -604,7 +604,7 @@ local function main(...: string) lintPaths(inputFiles :: { string }, lintRules, autofixEnabled, configContextData, ignoreData) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed if args:has("json") then - print(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.object, true)) + print(json.serialize(lsp.workspaceDiagnosticReport(allViolations) :: json.Object, true)) else if VERBOSE then print(`Printing violations from {#allViolations} files\n`) diff --git a/lute/cli/commands/lint/lsp/init.luau b/lute/cli/commands/lint/lsp/init.luau index f6b4ece9c..8b9b7ec43 100644 --- a/lute/cli/commands/lint/lsp/init.luau +++ b/lute/cli/commands/lint/lsp/init.luau @@ -26,7 +26,7 @@ local function tag(t: lintTypes.tag): number end end -local function getRange(location: syntax.span): lspTypes.Range +local function getRange(location: syntax.Span): lspTypes.Range return table.freeze({ start = table.freeze({ line = location.beginline - 1, diff --git a/lute/cli/commands/lint/rules/duplicate_keys.luau b/lute/cli/commands/lint/rules/duplicate_keys.luau index f66caf9ff..b48cd7299 100644 --- a/lute/cli/commands/lint/rules/duplicate_keys.luau +++ b/lute/cli/commands/lint/rules/duplicate_keys.luau @@ -13,7 +13,7 @@ local function lint( sourcepath: path.path?, _context: lintTypes.RuleContext ): { lintTypes.LintViolation } - local function makeViolation(location: syntax.span): lintTypes.LintViolation + local function makeViolation(location: syntax.Span): lintTypes.LintViolation return { lintname = name, location = location, diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index d8d6896ba..ad279cb99 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -6,12 +6,12 @@ export type tag = "unnecessary" | "deprecated" export type LintViolation = { lintname: string, - location: syntax.span, + location: syntax.Span, message: string, severity: severity, sourcepath: path.path?, suggestedfix: { - read location: syntax.span?, -- if nil, applies to whole violation location + read location: syntax.Span?, -- if nil, applies to whole violation location read fix: string, }?, target: string?, -- Additional information on the violation (e.g link to docs) diff --git a/lute/cli/commands/test/filter.luau b/lute/cli/commands/test/filter.luau index 7d045ba9c..d56ddcd0d 100644 --- a/lute/cli/commands/test/filter.luau +++ b/lute/cli/commands/test/filter.luau @@ -1,11 +1,11 @@ local testtypes = require("@std/test/types") local function filtertests( - env: testtypes.testenvironment, + env: testtypes.TestEnvironment, suiteName: string?, caseName: string? -): testtypes.testenvironment - local filtered: testtypes.testenvironment = { +): testtypes.TestEnvironment + local filtered: testtypes.TestEnvironment = { anonymous = {}, suites = {}, suiteindex = {}, diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index 0f20dbd54..1be60ca4a 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -39,7 +39,7 @@ local function listtests() end end -local function runtests(env: testtypes.testenvironment, runner: testtypes.testrunner, reporter: testtypes.testreporter) +local function runtests(env: testtypes.TestEnvironment, runner: testtypes.TestRunner, reporter: testtypes.TestReporter) local result = runner(env) reporter(result) @@ -100,7 +100,7 @@ local function main(...: string) if args:has("list") then listtests() else - local env = test._registered() :: testtypes.testenvironment + local env = test._registered() :: testtypes.TestEnvironment local filteredEnv = filter.filtertests(env, args:get("suite"), args:get("case")) runtests(filteredEnv, runner.run, reporter.color) end diff --git a/lute/cli/commands/test/reporter.luau b/lute/cli/commands/test/reporter.luau index bfd22d8d5..9e881bfd3 100644 --- a/lute/cli/commands/test/reporter.luau +++ b/lute/cli/commands/test/reporter.luau @@ -23,7 +23,7 @@ local location = rt.cyan local dim = rt.dim local separator = dim -local function printFailure(failed: tys.failedtest) +local function printFailure(failed: tys.FailedTest) print(fail(" FAIL ") .. testname(failed.test)) if failed.failure.__tag == "assertion" then print(location(`\t{failed.failure.filename}:{failed.failure.linenumber}`)) @@ -40,7 +40,7 @@ local function printFailure(failed: tys.failedtest) end end -local function printSummary(result: tys.testrunresult) +local function printSummary(result: tys.TestRunResult) print(separator(string.rep("─", 50))) print( rt.bold("Results: ") @@ -51,7 +51,7 @@ local function printSummary(result: tys.testrunresult) ) end -local function colorReporter(result: tys.testrunresult) +local function colorReporter(result: tys.TestRunResult) if #result.failures > 0 then print("") print(fail("Failures:")) diff --git a/lute/cli/commands/transform/lib/types.luau b/lute/cli/commands/transform/lib/types.luau index ebb3f3942..d105f2804 100644 --- a/lute/cli/commands/transform/lib/types.luau +++ b/lute/cli/commands/transform/lib/types.luau @@ -8,7 +8,7 @@ export type Context = { options: Options, } -export type Transformer = (Context) -> string | syntaxTypes.replacements +export type Transformer = (Context) -> string | syntaxTypes.Replacements export type ConfigOption = { diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index 6166f2b3b..fca7f3d66 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -11,48 +11,48 @@ local win32paths = require("@std/path/win32") local fslib = {} -export type handlemode = fs.HandleMode -export type file = fs.FileHandle -export type type = fs.FileType -export type metadata = fs.FileMetadata -export type directoryentry = fs.DirectoryEntry -export type watchhandle = fs.WatchHandle -export type watchevent = fs.WatchEvent +export type HandleMode = fs.HandleMode +export type File = fs.FileHandle +export type FileType = fs.FileType +export type FileMetadata = fs.FileMetadata +export type DirectoryEntry = fs.DirectoryEntry +export type WatchHandle = fs.WatchHandle +export type WatchEvent = fs.WatchEvent type pathlike = pathlib.pathlike type path = pathlib.path type win32path = win32paths.path -export type createdirectoryoptions = { +export type CreateDirectoryOptions = { makeparents: boolean?, } -export type removedirectoryoptions = { +export type RemoveDirectoryOptions = { recursive: boolean?, } -export type walkoptions = { +export type WalkOptions = { recursive: boolean?, } -type watcher = { - next: (watcher) -> watchevent?, - close: (self: watcher) -> (), +type Watcher = { + next: (Watcher) -> WatchEvent?, + close: (self: Watcher) -> (), } -function fslib.open(path: pathlike, mode: handlemode?): file +function fslib.open(path: pathlike, mode: HandleMode?): File return fs.open(pathlib.format(path), mode) end -function fslib.read(file: file): string +function fslib.read(file: File): string return fs.read(file) end -function fslib.write(file: file, contents: string): () +function fslib.write(file: File, contents: string): () return fs.write(file, contents) end -function fslib.close(file: file): () +function fslib.close(file: File): () return fs.close(file) end @@ -60,11 +60,11 @@ function fslib.remove(path: pathlike): () return fs.remove(pathlib.format(path)) end -function fslib.metadata(path: pathlike): metadata +function fslib.metadata(path: pathlike): FileMetadata return fs.stat(pathlib.format(path)) end -function fslib.type(path: pathlike): type +function fslib.type(path: pathlike): FileType return fs.type(pathlib.format(path)) end @@ -79,18 +79,18 @@ end --- Iterator function that yields filename and event pairs when changes occur in the watched path. --- Also provides a `close` method to stop watching. --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.watch as `for _ in fs.watch(...) do` directly. A while loop can be used instead. See example/watch_directory.luau for usage. -function fslib.watch(path: pathlike): watcher +function fslib.watch(path: pathlike): Watcher -- In the future this could be written with explicit instantiation syntax -- as in: -- -- deque.new<<{ filename: path, event: watchevent }>>() - local deq = deque.new(nil :: { filename: path, event: watchevent }?) + local deq = deque.new(nil :: { filename: path, event: WatchEvent }?) local handle = fs.watch(pathlib.format(path), function(filename, event) deq:pushback({ filename = pathlib.parse(filename), event = event }) end) return { - next = function(_self: watcher): watchevent? + next = function(_self: Watcher): WatchEvent? if not deq:peekfront() then return nil end @@ -98,7 +98,7 @@ function fslib.watch(path: pathlike): watcher assert(item) return item.event end, - close = function(_self: watcher): () + close = function(_self: Watcher): () if handle then handle:close() end @@ -114,7 +114,7 @@ function fslib.copy(src: pathlike, dest: pathlike): () return fs.copy(pathlib.format(src), pathlib.format(dest)) end -function fslib.listdirectory(path: pathlike): { directoryentry } +function fslib.listdirectory(path: pathlike): { DirectoryEntry } return fs.listdir(pathlib.format(path)) end @@ -131,7 +131,7 @@ function fslib.writestringtofile(filepath: pathlike, contents: string): () fs.close(f) end -function fslib.createdirectory(path: pathlike, options: createdirectoryoptions?): () +function fslib.createdirectory(path: pathlike, options: CreateDirectoryOptions?): () if options and options.makeparents then local parsed = pathlib.parse(path) local parts: { string } = parsed.parts @@ -158,7 +158,7 @@ function fslib.createdirectory(path: pathlike, options: createdirectoryoptions?) end end -function fslib.removedirectory(path: pathlike, options: removedirectoryoptions?): () +function fslib.removedirectory(path: pathlike, options: RemoveDirectoryOptions?): () if options and options.recursive then -- Recursively delete all contents first if fslib.exists(path) and fslib.type(path) == "dir" then @@ -179,7 +179,7 @@ function fslib.removedirectory(path: pathlike, options: removedirectoryoptions?) end --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. See example/walk_directory.luau for usage. -function fslib.walk(path: pathlike, options: walkoptions?): () -> path? +function fslib.walk(path: pathlike, options: WalkOptions?): () -> path? local queue: { path } = { pathlib.parse(path) } local recursive = if options then options.recursive else false diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 7d9f73fd9..264341c13 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -7,9 +7,9 @@ local json = { null = newproxy() :: nil, } -export type object = { [string]: value } -export type array = { [number]: value } -export type value = nil | number | string | boolean | array | object +export type Object = { [string]: Value } +export type Array = { [number]: Value } +export type Value = nil | number | string | boolean | Array | Object -- a unique key to identify an object vs a table local object_key = newproxy() @@ -74,7 +74,7 @@ local function writeString(state: SerializerState, str: string): () state.cursor += #str end -local serializeAny: (SerializerState, (array | object | boolean | number | string)?) -> () +local serializeAny: (SerializerState, (Array | Object | boolean | number | string)?) -> () local ESCAPE_MAP = { [0x5C] = string.byte("\\"), -- 5C = '\' @@ -115,7 +115,7 @@ local function serializeString(state: SerializerState, str: string): () writeByte(state, string.byte('"')) end -local function serializeArray(state: SerializerState, array: array): () +local function serializeArray(state: SerializerState, array: Array): () state.depth += 1 writeByte(state, string.byte("[")) @@ -150,7 +150,7 @@ local function serializeArray(state: SerializerState, array: array): () writeByte(state, string.byte("]")) end -local function serializeTable(state: SerializerState, object: object): () +local function serializeTable(state: SerializerState, object: Object): () writeByte(state, string.byte("{")) if state.prettyPrint then @@ -195,7 +195,7 @@ local function serializeTable(state: SerializerState, object: object): () writeByte(state, string.byte("}")) end -serializeAny = function(state: SerializerState, value: value): () +serializeAny = function(state: SerializerState, value: Value): () local valueType = type(value) if value == json.null then @@ -208,9 +208,9 @@ serializeAny = function(state: SerializerState, value: value): () serializeString(state, value :: string) elseif valueType == "table" then if #(value :: {}) == 0 and next(value :: { [unknown]: unknown }) ~= nil then - serializeTable(state, value :: object) + serializeTable(state, value :: Object) else - serializeArray(state, value :: array) + serializeArray(state, value :: Array) end else error("Unknown value", 2) @@ -369,9 +369,9 @@ local function deserializeString(state: DeserializerState): string return deserializerError(state, "Unterminated string") end -local deserialize: (DeserializerState) -> (array | object | boolean | number | string)? +local deserialize: (DeserializerState) -> (Array | Object | boolean | number | string)? -local function deserializeArray(state: DeserializerState): array +local function deserializeArray(state: DeserializerState): Array if currentByte(state) ~= string.byte("[") then return deserializerError(state, "Expected array opening '['") end @@ -379,7 +379,7 @@ local function deserializeArray(state: DeserializerState): array state.cursor += 1 skipWhitespace(state) - local current: array = {} + local current: Array = {} local index = 1 -- empty array @@ -430,7 +430,7 @@ local function deserializeArray(state: DeserializerState): array return deserializerError(state, "Unterminated array") end -local function deserializeObject(state: DeserializerState): object +local function deserializeObject(state: DeserializerState): Object state.cursor += 1 local current = {} @@ -490,7 +490,7 @@ local function deserializeObject(state: DeserializerState): object return current end -deserialize = function(state: DeserializerState): value +deserialize = function(state: DeserializerState): Value skipWhitespace(state) local fourChars = string.sub(state.src, state.cursor, state.cursor + 3) @@ -522,7 +522,7 @@ end -- user-facing -function json.serialize(value: value, prettyPrint: boolean?): string +function json.serialize(value: Value, prettyPrint: boolean?): string local state: SerializerState = { buf = buffer.create(bufferSize), cursor = 0, @@ -535,7 +535,7 @@ function json.serialize(value: value, prettyPrint: boolean?): string return buffer.readstring(state.buf, 0, state.cursor) end -function json.deserialize(src: string): (array | object | boolean | number | string)? +function json.deserialize(src: string): (Array | Object | boolean | number | string)? local state = { src = src, cursor = 0, @@ -551,8 +551,8 @@ function json.deserialize(src: string): (array | object | boolean | number | str return result end -function json.object(props: { [string]: value }): object - local res: object = { [object_key] = true } +function json.object(props: { [string]: Value }): Object + local res: Object = { [object_key] = true } for key, value in props do res[key] = value @@ -561,17 +561,17 @@ function json.object(props: { [string]: value }): object return res end -function json.asobject(value: value): object? +function json.asobject(value: Value): Object? if typeof(value) == "table" and value[object_key] then - return value :: object + return value :: Object end return nil end -function json.asarray(value: value): array? +function json.asarray(value: Value): Array? if typeof(value) == "table" and not value[object_key] then - return value :: array + return value :: Array end return nil diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index f0628487b..38f860062 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -10,13 +10,13 @@ local path = require("@std/path") local luau = {} -export type bytecode = luteLuau.Bytecode +export type Bytecode = luteLuau.Bytecode -function luau.compile(source: string): bytecode +function luau.compile(source: string): Bytecode return luteLuau.compile(source) end -function luau.load(bytecode: bytecode, chunkname: string?, env: { [any]: any }?): (...any) -> ...any +function luau.load(bytecode: Bytecode, chunkname: string?, env: { [any]: any }?): (...any) -> ...any return luteLuau.load(bytecode, if chunkname ~= nil then `@{chunkname}` else "=luau.load", env) end diff --git a/lute/std/libs/net.luau b/lute/std/libs/net.luau index 0f1ea8b34..815dc0d58 100644 --- a/lute/std/libs/net.luau +++ b/lute/std/libs/net.luau @@ -6,10 +6,10 @@ local net = require("@lute/net") local netlib = {} -export type metadata = net.Metadata -export type response = net.Response +export type Metadata = net.Metadata +export type Response = net.Response -function netlib.request(url: string, metadata: metadata?): response +function netlib.request(url: string, metadata: Metadata?): Response return net.request(url, metadata) end diff --git a/lute/std/libs/process.luau b/lute/std/libs/process.luau index 443b0cee6..66b8a0750 100644 --- a/lute/std/libs/process.luau +++ b/lute/std/libs/process.luau @@ -8,10 +8,10 @@ local pathlib = require("@std/path") local processlib = {} -export type stdiokind = process.StdioKind -export type processrunoptions = process.ProcessRunOptions -export type processsystemoptions = process.ProcessSystemOptions -export type processresult = process.ProcessResult +export type StdioKind = process.StdioKind +export type ProcessRunOptions = process.ProcessRunOptions +export type ProcessSystemOptions = process.ProcessSystemOptions +export type ProcessResult = process.ProcessResult export type path = pathlib.path export type pathlike = pathlib.pathlike @@ -23,11 +23,11 @@ function processlib.cwd(): path return pathlib.parse(process.cwd()) end -function processlib.run(args: { string }, options: processrunoptions?): processresult +function processlib.run(args: { string }, options: ProcessRunOptions?): ProcessResult return process.run(args, options) end -function processlib.system(command: string, options: processsystemoptions?): processresult +function processlib.system(command: string, options: ProcessSystemOptions?): ProcessResult return process.system(command, options) end diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index a5758d94b..88b220ca3 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -3,13 +3,13 @@ local types = require("@self/types") local luau = require("@lute/luau") -export type span = types.span +export type Span = types.Span local span = { create = luau.span.create, } -function span.subsumes(haystack: span, needle: span): boolean +function span.subsumes(haystack: Span, needle: Span): boolean return ( if haystack.beginline == needle.beginline then haystack.begincolumn <= needle.begincolumn diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 9ddcca5c4..e6c6a243c 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -10,7 +10,7 @@ local printerLib = {} type PrintVisitor = visitor.Visitor & { result: buffer, cursor: number, - replacements: types.replacements, + replacements: types.Replacements, write: (self: PrintVisitor, str: string) -> (), printTrivia: (self: PrintVisitor, trivia: types.Trivia) -> (), printTriviaList: (self: PrintVisitor, trivia: { types.Trivia }) -> (), @@ -131,7 +131,7 @@ local function write(self: PrintVisitor, str: string) self.cursor = totalSize end -local function printVisitor(replacements: types.replacements?) +local function printVisitor(replacements: types.Replacements?) local result = buffer.create(1024) local cursor = 0 @@ -154,7 +154,7 @@ local function printVisitor(replacements: types.replacements?) printer.result = result printer.cursor = cursor - printer.replacements = if replacements ~= nil then replacements else {} :: types.replacements + printer.replacements = if replacements ~= nil then replacements else {} :: types.Replacements printer.write = write printer.printTrivia = printTrivia printer.printTriviaList = printTriviaList @@ -182,13 +182,13 @@ local function printVisitor(replacements: types.replacements?) return printer end -function printerLib.printnode(node: types.AstNode, replacements: types.replacements?): string +function printerLib.printnode(node: types.AstNode, replacements: types.Replacements?): string local printer = printVisitor(replacements) visitor.visit(node, printer) return buffer.readstring(printer.result, 0, printer.cursor) end -function printerLib.printfile(result: types.ParseResult, replacements: types.replacements?): string +function printerLib.printfile(result: types.ParseResult, replacements: types.Replacements?): string local printer = printVisitor(replacements) visitor.visitblock(result.root, printer) visitor.visittoken(result.eof, printer) diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index f6d95013f..50e3d071a 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -6,15 +6,15 @@ local tableext = require("@std/tableext") local types = require("./types") local visitor = require("./visitor") -type node = types.AstNode +type Node = types.AstNode -export type query = { +export type query = { nodes: { T }, filter: (self: query, pred: (T) -> boolean) -> query, - replace: (self: query, repl: (T) -> string?) -> types.replacements, + replace: (self: query, repl: (T) -> string?) -> types.Replacements, map: (self: query, fn: (T) -> U?) -> query, -- recursion violation reported foreach: (self: query, callback: (T) -> ()) -> query, - findall: (self: query, fn: (node) -> U?) -> query, + findall: (self: query, fn: (Node) -> U?) -> query, flatmap: (self: query, fn: (T) -> { U }) -> query, maptoarray: (self: query, fn: (T) -> U?) -> { U }, } @@ -32,7 +32,7 @@ function queryLib.filter(self: query, pred: (T) -> boolean): query return self end -function queryLib.replace(self: query, repl: (T) -> string?): types.replacements +function queryLib.replace(self: query, repl: (T) -> string?): types.Replacements local replacements = {} for _, node in self.nodes do local r = repl(node) @@ -63,8 +63,8 @@ function queryLib.foreach(self: query, callback: (T) -> ()): query return self end -local function newSelectVisitor(nodes: { T }, fn: (node) -> T?): visitor.Visitor - local function visit(n: node) +local function newSelectVisitor(nodes: { T }, fn: (Node) -> T?): visitor.Visitor + local function visit(n: Node) local selected = fn(n) if selected ~= nil then table.insert(nodes, selected) @@ -83,7 +83,7 @@ local function newSelectVisitor(nodes: { T }, fn: (node) -> T?): visitor.Visi return selectVisitor end -function queryLib.findall(self: query, fn: (node) -> U?): query +function queryLib.findall(self: query, fn: (Node) -> U?): query local selectedNodes: { U } = {} local selectVisitor = newSelectVisitor(selectedNodes, fn) @@ -120,12 +120,12 @@ function queryLib.maptoarray(self: query, fn: (T) -> U?): { U } return result end -function queryLib.findallfromroot(ast: types.ParseResult | node, fn: (node) -> T?): query +function queryLib.findallfromroot(ast: types.ParseResult | Node, fn: (Node) -> T?): query local nodes: { T } = {} local selectVisitor = newSelectVisitor(nodes, fn) - local root: node = if (ast :: any).root ~= nil then (ast :: any).root else ast :: any -- LUAUFIX: Replace any cast with cast to node once code too complex no longer emitted + local root: Node = if (ast :: any).root ~= nil then (ast :: any).root else ast :: any -- LUAUFIX: Replace any cast with cast to node once code too complex no longer emitted visitor.visit(root, selectVisitor) return { diff --git a/lute/std/libs/syntax/types.luau b/lute/std/libs/syntax/types.luau index c26f021cb..9ff611033 100644 --- a/lute/std/libs/syntax/types.luau +++ b/lute/std/libs/syntax/types.luau @@ -1,6 +1,6 @@ local luau = require("@lute/luau") -export type span = luau.span +export type Span = luau.Span export type Whitespace = luau.Whitespace export type SingleLineComment = luau.SingleLineComment @@ -159,7 +159,7 @@ export type AstNode = luau.AstNode export type ParseResult = luau.ParseResult -export type replacements = { [AstNode]: string } +export type Replacements = { [AstNode]: string } export type Type = luau.Type diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index c0d6ab986..278b34269 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -7,13 +7,13 @@ local task = require("@lute/task") local tasklib = {} -export type task = { +export type Task = { success: boolean?, result: any, co: thread, } -function tasklib.create(f, ...): task +function tasklib.create(f, ...): Task local data = {} data.co = coroutine.create(function(...) @@ -24,10 +24,10 @@ function tasklib.create(f, ...): task end) coroutine.resume(data.co, ...) - return data :: task + return data :: Task end -function tasklib.await(t: task): any +function tasklib.await(t: Task): any if not t.co then error(`await: argument 1 is not a task`) end @@ -43,7 +43,7 @@ function tasklib.await(t: task): any end end -function tasklib.awaitall(...: task): ...unknown +function tasklib.awaitall(...: Task): ...unknown local tasks = table.pack(...) for i, v in ipairs(tasks) do diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index f8f28825c..95d0ce071 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -7,8 +7,8 @@ local stringext = require("@std/stringext") local types = require("./types") -- types -type failure = types.failure -type asserts = types.asserts +type Failure = types.Failure +type Asserts = types.Asserts -- more utils local assertion = failure.assertion @@ -63,7 +63,7 @@ local function tablesEqual(t1: unknown, t2: unknown, path: string): (boolean, st return true, nil end -local function eq(lhs: T, rhs: T, msg: string?): failure? +local function eq(lhs: T, rhs: T, msg: string?): Failure? if lhs == rhs then return nil end @@ -71,7 +71,7 @@ local function eq(lhs: T, rhs: T, msg: string?): failure? return assertion(msg or `{lhs} ~= {rhs}`) end -local function neq(lhs: T, rhs: T, msg: string?): failure? +local function neq(lhs: T, rhs: T, msg: string?): Failure? if lhs ~= rhs then return nil end @@ -79,7 +79,7 @@ local function neq(lhs: T, rhs: T, msg: string?): failure? return assertion(msg or `{lhs} == {rhs}`) end -local function errors(callback: () -> (), msg: string?): failure? +local function errors(callback: () -> (), msg: string?): Failure? local success = pcall(callback) -- if the callback failed, then this assert passes if not success then @@ -90,7 +90,7 @@ local function errors(callback: () -> (), msg: string?): failure? end -- Table equality assertionsion with deep comparison -local function tableeq(lhs: { [any]: any }, rhs: { [any]: any }): failure? +local function tableeq(lhs: { [any]: any }, rhs: { [any]: any }): Failure? local equal, msg = tablesEqual(lhs, rhs, "") if equal then return nil @@ -98,7 +98,7 @@ local function tableeq(lhs: { [any]: any }, rhs: { [any]: any }): failure? return assertion(msg or "tables are not equal") end -local function buffereq(lhs: buffer, rhs: buffer): failure? +local function buffereq(lhs: buffer, rhs: buffer): Failure? if typeof(lhs) ~= "buffer" then return assertion(`lhs is of type {typeof(lhs)}, not buffer`) end @@ -123,7 +123,7 @@ local function buffereq(lhs: buffer, rhs: buffer): failure? return nil end -local function erroreq(func: (A...) -> ...unknown, expectedErrorMessage: string, ...: A...): failure? +local function erroreq(func: (A...) -> ...unknown, expectedErrorMessage: string, ...: A...): Failure? local success, err = pcall(func, ...) if success then return assertion("Function did not error as expected.") @@ -138,7 +138,7 @@ local function erroreq(func: (A...) -> ...unknown, expectedErrorMessage: s return nil end -local function strcontains(haystack: string, needle: string, instances: number?, msg: string?): failure? +local function strcontains(haystack: string, needle: string, instances: number?, msg: string?): Failure? if instances == nil then if haystack:find(needle, 1, true) == nil then return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) @@ -160,7 +160,7 @@ local function strcontains(haystack: string, needle: string, instances: number?, return nil end -local function strnotcontains(haystack: string, needle: string, msg: string?): failure? +local function strnotcontains(haystack: string, needle: string, msg: string?): Failure? if haystack:find(needle, 1, true) ~= nil then return assertion(if msg then msg else `Expected "{haystack}" to not contain "{needle}".`) end @@ -169,9 +169,9 @@ local function strnotcontains(haystack: string, needle: string, msg: string?): f end -- utility for taking an assertion and wrapping it as a check that must complete successfully or will return control flow to the error handler -local function reqassert(req: (T...) -> failure?): (T...) -> failure? - return function(...): failure? - local result: failure? = req(...) +local function reqassert(req: (T...) -> Failure?): (T...) -> Failure? + return function(...): Failure? + local result: Failure? = req(...) if result ~= nil then error(result) end @@ -189,4 +189,4 @@ return table.freeze({ erroreq = reqassert(erroreq), strcontains = reqassert(strcontains), strnotcontains = reqassert(strnotcontains), -}) :: asserts +}) :: Asserts diff --git a/lute/std/libs/test/failure.luau b/lute/std/libs/test/failure.luau index ae6b62185..a8a488272 100644 --- a/lute/std/libs/test/failure.luau +++ b/lute/std/libs/test/failure.luau @@ -3,11 +3,11 @@ -- utilities for working with test failures local types = require("./types") -type failure = types.failure +type Failure = types.Failure local failures = {} -- Generates the debug information needed to describe assertion failure locations -function failures.assertion(msg: string): failure +function failures.assertion(msg: string): Failure -- we need to go 5 function calls up to get to the actual assertion that invoked this local filename, linenumber = debug.info(4, "sl") local name = debug.info(2, "n") @@ -15,14 +15,14 @@ function failures.assertion(msg: string): failure end -- Generates the debug information needed to describe assertion failure locations -function failures.lifecycle(hook: types.hook, err): failure +function failures.lifecycle(hook: types.Hook, err): Failure -- we need to go 5 function calls up to get to the actual assertion that invoked this local filename, linenumber = debug.info(3, "sl") return { hook = hook, msg = tostring(err), filename = filename, linenumber = linenumber, __tag = "lifecycle" } end -- Generates the debug information needed to describe runtime error failure -function failures.runtimeerror(err): failure +function failures.runtimeerror(err): Failure -- we need to go 5 function calls up to get to the actual assertion that invoked this return { msg = tostring(err), stacktrace = debug.traceback(tostring(err)), __tag = "runtime_error" } end diff --git a/lute/std/libs/test/init.luau b/lute/std/libs/test/init.luau index eaf5ca4d4..e85919af3 100644 --- a/lute/std/libs/test/init.luau +++ b/lute/std/libs/test/init.luau @@ -8,11 +8,11 @@ local reporter = require("@self/reporter") local runner = require("@self/runner") local ps = require("@std/process") -type Test = types.test -type TestCase = types.testcase -type TestSuite = types.testsuite -type TestEnvironment = types.testenvironment -type TestReporter = types.testreporter +type Test = types.Test +type TestCase = types.TestCase +type TestSuite = types.TestSuite +type TestEnvironment = types.TestEnvironment +type TestReporter = types.TestReporter local TestSuite = {} TestSuite.__index = TestSuite diff --git a/lute/std/libs/test/reporter.luau b/lute/std/libs/test/reporter.luau index 5501d151a..affe35f38 100644 --- a/lute/std/libs/test/reporter.luau +++ b/lute/std/libs/test/reporter.luau @@ -4,8 +4,8 @@ local types = require("./types") -type FailedTest = types.failedtest -type TestRunResult = types.testrunresult +type FailedTest = types.FailedTest +type TestRunResult = types.TestRunResult local reporter = {} diff --git a/lute/std/libs/test/runner.luau b/lute/std/libs/test/runner.luau index 2d6688934..77c99d72a 100644 --- a/lute/std/libs/test/runner.luau +++ b/lute/std/libs/test/runner.luau @@ -6,13 +6,13 @@ local asserts = require("./assert") local failure = require("./failure") local types = require("./types") -type Test = types.test -type TestCase = types.testcase -type TestSuite = types.testsuite -type TestEnvironment = types.testenvironment -type TestReporter = types.testreporter -type FailedTest = types.failedtest -type TestRunResult = types.testrunresult +type Test = types.Test +type TestCase = types.TestCase +type TestSuite = types.TestSuite +type TestEnvironment = types.TestEnvironment +type TestReporter = types.TestReporter +type FailedTest = types.FailedTest +type TestRunResult = types.TestRunResult local runner = {} diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index fd7478484..3000e6aa0 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -4,8 +4,8 @@ -- Test Reporting -export type hook = "beforeeach" | "beforeall" | "aftereach" | "afterall" -export type failure = +export type Hook = "beforeeach" | "beforeall" | "aftereach" | "afterall" +export type Failure = { assertion: string, -- name of the assertion msg: string, -- failure message @@ -14,7 +14,7 @@ export type failure = __tag: "assertion", } | { - hook: hook, -- the name of the hook that broke + hook: Hook, -- the name of the hook that broke msg: string, -- failure message filename: string, -- filename linenumber: number, -- linenumber @@ -26,64 +26,64 @@ export type failure = __tag: "runtime_error", } -export type asserts = { - eq: (T, T, string?) -> failure?, - neq: (T, T, string?) -> failure?, - errors: (() -> (), string?) -> failure?, +export type Asserts = { + eq: (T, T, string?) -> Failure?, + neq: (T, T, string?) -> Failure?, + errors: (() -> (), string?) -> Failure?, -- FIXME(Luau): We would like this to be two read-only indexers, but the -- best option we have otherwise is two error suppressing indexers. - tableeq: ({ [any]: any }, { [any]: any }) -> failure?, - buffereq: (buffer, buffer) -> failure?, - erroreq: ((A...) -> ...unknown, string, A...) -> failure?, - strcontains: (string, string, number?, string?) -> failure?, - strnotcontains: (string, string, string?) -> failure?, + tableeq: ({ [any]: any }, { [any]: any }) -> Failure?, + buffereq: (buffer, buffer) -> Failure?, + erroreq: ((A...) -> ...unknown, string, A...) -> Failure?, + strcontains: (string, string, number?, string?) -> Failure?, + strnotcontains: (string, string, string?) -> Failure?, } -export type test = (asserts) -> () +export type Test = (Asserts) -> () -export type testcase = { name: string, case: test } +export type TestCase = { name: string, case: Test } -export type testsuite = { +export type TestSuite = { name: string, - cases: { testcase }, + cases: { TestCase }, _beforeeach: (() -> ())?, _beforeall: (() -> ())?, _aftereach: (() -> ())?, _afterall: (() -> ())?, - case: (self: testsuite, string, test) -> (), - beforeeach: (self: testsuite, () -> ()) -> (), - beforeall: (self: testsuite, () -> ()) -> (), - aftereach: (self: testsuite, () -> ()) -> (), - afterall: (self: testsuite, () -> ()) -> (), + case: (self: TestSuite, string, Test) -> (), + beforeeach: (self: TestSuite, () -> ()) -> (), + beforeall: (self: TestSuite, () -> ()) -> (), + aftereach: (self: TestSuite, () -> ()) -> (), + afterall: (self: TestSuite, () -> ()) -> (), } -export type failedtest = { +export type FailedTest = { test: string, -- name of the test case that failed - failure: failure, + failure: Failure, } -export type testrunresult = { - failures: { failedtest }, +export type TestRunResult = { + failures: { FailedTest }, total: number, failed: number, passed: number, } -export type testreporter = (testrunresult) -> () +export type TestReporter = (TestRunResult) -> () -- Testing Environment -export type caseindexentry = { - anonymous: { testcase }, - suites: { [string]: { testcase } }, +export type CaseIndexEntry = { + anonymous: { TestCase }, + suites: { [string]: { TestCase } }, } -export type testenvironment = { - anonymous: { testcase }, - suites: { testsuite }, - suiteindex: { [string]: testsuite }, - caseindex: { [string]: caseindexentry }, +export type TestEnvironment = { + anonymous: { TestCase }, + suites: { TestSuite }, + suiteindex: { [string]: TestSuite }, + caseindex: { [string]: CaseIndexEntry }, } -export type testrunner = (testenvironment) -> testrunresult +export type TestRunner = (TestEnvironment) -> TestRunResult return {} diff --git a/lute/std/libs/time/duration.luau b/lute/std/libs/time/duration.luau index 5740b9c93..8b9a674f2 100644 --- a/lute/std/libs/time/duration.luau +++ b/lute/std/libs/time/duration.luau @@ -11,7 +11,7 @@ local MINS_PER_HOUR = 60 local HOURS_PER_DAY = 24 local DAYS_PER_WEEK = 7 -type durationdata = { +type DurationData = { _seconds: number, _nanoseconds: number, } @@ -19,10 +19,10 @@ type durationdata = { local duration = {} duration.__index = duration -type durationinterface = typeof(duration) -export type duration = setmetatable +type DurationInterface = typeof(duration) +export type Duration = setmetatable -function duration.create(seconds: number, nanoseconds: number): duration +function duration.create(seconds: number, nanoseconds: number): Duration local self = { _seconds = seconds, _nanoseconds = nanoseconds, @@ -31,76 +31,76 @@ function duration.create(seconds: number, nanoseconds: number): duration return setmetatable(self, duration) end -function duration.seconds(seconds: number): duration +function duration.seconds(seconds: number): Duration return duration.create(seconds, 0) end -function duration.milliseconds(milliseconds: number): duration +function duration.milliseconds(milliseconds: number): Duration local seconds = math.floor(milliseconds / MILLIS_PER_SEC) local nanoseconds = (milliseconds % MILLIS_PER_SEC) * NANOS_PER_MILLI return duration.create(seconds, nanoseconds) end -function duration.microseconds(microseconds: number): duration +function duration.microseconds(microseconds: number): Duration local seconds = math.floor(microseconds / MICROS_PER_SEC) local nanoseconds = (microseconds % MICROS_PER_SEC) * NANOS_PER_MICRO return duration.create(seconds, nanoseconds) end -function duration.nanoseconds(nanoseconds: number): duration +function duration.nanoseconds(nanoseconds: number): Duration local seconds = math.floor(nanoseconds / NANOS_PER_SEC) local nanosecondsRemaining = nanoseconds % NANOS_PER_SEC return duration.create(seconds, nanosecondsRemaining) end -function duration.minutes(minutes: number): duration +function duration.minutes(minutes: number): Duration return duration.create(minutes * SECS_PER_MINUTE, 0) end -function duration.hours(hours: number): duration +function duration.hours(hours: number): Duration return duration.create(hours * MINS_PER_HOUR * SECS_PER_MINUTE, 0) end -function duration.days(days: number): duration +function duration.days(days: number): Duration return duration.create(days * HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE, 0) end -function duration.weeks(weeks: number): duration +function duration.weeks(weeks: number): Duration return duration.create(weeks * DAYS_PER_WEEK * HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE, 0) end -function duration.subsecmillis(self: duration): number +function duration.subsecmillis(self: Duration): number return math.floor(self._nanoseconds / NANOS_PER_MILLI) end -function duration.subsecmicros(self: duration): number +function duration.subsecmicros(self: Duration): number return math.floor(self._nanoseconds / NANOS_PER_MICRO) end -function duration.subsecnanos(self: duration): number +function duration.subsecnanos(self: Duration): number return self._nanoseconds end -function duration.toseconds(self: duration): number +function duration.toseconds(self: Duration): number return self._seconds + self._nanoseconds / NANOS_PER_SEC end -function duration.tomilliseconds(self: duration): number +function duration.tomilliseconds(self: Duration): number return self._seconds * MILLIS_PER_SEC + self:subsecmillis() end -function duration.tomicroseconds(self: duration): number +function duration.tomicroseconds(self: Duration): number return self._seconds * MICROS_PER_SEC + self:subsecmicros() end -function duration.tonanoseconds(self: duration): number +function duration.tonanoseconds(self: Duration): number return self._seconds * NANOS_PER_SEC + self:subsecnanos() end -function duration.__add(a: duration, b: duration): duration +function duration.__add(a: Duration, b: Duration): Duration local seconds = a._seconds + b._seconds local nanoseconds = a._nanoseconds + b._nanoseconds @@ -112,7 +112,7 @@ function duration.__add(a: duration, b: duration): duration return duration.create(seconds, nanoseconds) end -function duration.__sub(a: duration, b: duration): duration +function duration.__sub(a: Duration, b: Duration): Duration local seconds = a._seconds - b._seconds local nanoseconds = a._nanoseconds - b._nanoseconds @@ -124,7 +124,7 @@ function duration.__sub(a: duration, b: duration): duration return duration.create(seconds, nanoseconds) end -function duration.__mul(a: duration, b: number): duration +function duration.__mul(a: Duration, b: number): Duration local seconds = a._seconds * b local nanoseconds = a._nanoseconds * b @@ -134,7 +134,7 @@ function duration.__mul(a: duration, b: number): duration return duration.create(seconds, nanoseconds) end -function duration.__div(a: duration, b: number): duration +function duration.__div(a: Duration, b: number): Duration local seconds, extraSeconds = a._seconds / b, a._seconds % b local nanoseconds, extraNanos = a._nanoseconds / b, a._nanoseconds % b @@ -143,11 +143,11 @@ function duration.__div(a: duration, b: number): duration return duration.create(seconds, nanoseconds) end -function duration.__eq(a: duration, b: duration): boolean +function duration.__eq(a: Duration, b: Duration): boolean return a._seconds == b._seconds and a._nanoseconds == b._nanoseconds end -function duration.__lt(a: duration, b: duration): boolean +function duration.__lt(a: Duration, b: Duration): boolean if a._seconds == b._seconds then return a._nanoseconds < b._nanoseconds end @@ -155,7 +155,7 @@ function duration.__lt(a: duration, b: duration): boolean return a._seconds < b._seconds end -function duration.__le(a: duration, b: duration): boolean +function duration.__le(a: Duration, b: Duration): boolean if a._seconds == b._seconds then return a._nanoseconds <= b._nanoseconds end @@ -163,7 +163,7 @@ function duration.__le(a: duration, b: duration): boolean return a._seconds <= b._seconds end -function duration.__tostring(self: duration): string +function duration.__tostring(self: Duration): string return string.format("%d.%09d", self._seconds, self._nanoseconds) end diff --git a/lute/std/libs/time/init.luau b/lute/std/libs/time/init.luau index 817123243..309f650ef 100644 --- a/lute/std/libs/time/init.luau +++ b/lute/std/libs/time/init.luau @@ -5,7 +5,7 @@ local duration = require("@self/duration") local time = {} -export type duration = duration.duration +export type Duration = duration.Duration time.duration = duration diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index eae666f7e..622f36f33 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -38,7 +38,7 @@ type lintTestParams = { content: string, expectedExitCode: number, rulePath: string?, - processOptions: process.processrunoptions?, + processOptions: process.ProcessRunOptions?, customRuleContents: string?, expectedAutofixContents: string?, disableDefaultLints: true?, @@ -47,7 +47,7 @@ type lintTestParams = { outputExpectations: { { expectedOutput: string, count: number? } }?, } -local function lintTestHelper(assert: testTypes.asserts, params: lintTestParams): string +local function lintTestHelper(assert: testTypes.Asserts, params: lintTestParams): string -- Setup -- Create a file that violates the rule local violatorPath = path.format(path.join(tmpDir, "violator.luau")) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 09dbf9021..672acf041 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -8,8 +8,8 @@ local printer = require("@std/syntax/printer") local syntax = require("@std/syntax") local function assertEqualsLocation( - assert: testTypes.asserts, - actual: syntax.span, + assert: testTypes.Asserts, + actual: syntax.Span, startLine: number, startColumn: number, endLine: number, @@ -548,7 +548,7 @@ foo = bar, end) end) -local function checkIsBlock(node: any, assert: testTypes.asserts) +local function checkIsBlock(node: any, assert: testTypes.Asserts) assert.eq(node.kind, "stat") assert.eq(node.tag, "block") end diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 47dfdc19a..3b89f0cd0 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -9,8 +9,8 @@ local test = require("@std/test") local function checkReplacement( source: string, expected: string, - transformFn: (syntaxTypes.AstNode) -> syntaxTypes.replacements?, - assert: testtypes.asserts + transformFn: (syntaxTypes.AstNode) -> syntaxTypes.Replacements?, + assert: testtypes.Asserts ) local ast = syntax.parse(source) local replacements = transformFn(ast.root) diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index e314b9f28..bd5e0583e 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -12,7 +12,7 @@ local function assertErrorsWithMsg( testName: string, testContents: string, expectedErrMsg: string, - assert: testtypes.asserts + assert: testtypes.Asserts ) local testFilePath = path.join(tmpDir, `{testName}.test.luau`) fs.writestringtofile(testFilePath, testContents) diff --git a/tools/check.luau b/tools/check.luau index 1798e42e3..336961aa5 100644 --- a/tools/check.luau +++ b/tools/check.luau @@ -63,7 +63,7 @@ end local function readFaillist(): string -- LUAUFIX: the type of pcall is breaking things here, so we must cast - local ok: boolean, file: fs.file = (pcall :: any)(fs.open, FAILLIST_PATH, "r") + local ok: boolean, file: fs.File = (pcall :: any)(fs.open, FAILLIST_PATH, "r") if not ok or not file then return "" end From 87210a4537bd9f7c6199273f5b82ce99ae614b84 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:57:45 +0000 Subject: [PATCH 409/642] Update Luau to 0.713 (#894) **Luau**: Updated from `0.712` to `0.713` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.713 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: ariel --- extern/luau.tune | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 2a99ba791..df0b8427e 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.712" -revision = "9e2984fd0334414a2ef62ceede0c06dab7574d55" +branch = "0.713" +revision = "b37af212cb60366043c93c5a4acd085ab29c8d7a" From 1bf2caaee18a5cac412654239e03bd28e84362e3 Mon Sep 17 00:00:00 2001 From: Andrey Zhabsky Date: Tue, 24 Mar 2026 23:13:00 +0200 Subject: [PATCH 410/642] `luthier`: change `constexpr` to `const` (#870) The arrays generated by `luthier.luau`([line](https://github.com/luau-lang/lute/blob/93a18677dca27a989a786d88ef6b631400ff058c/tools/luthier.luau#L298)) are lookup tables for embedded source files. If I understand correctly, they are not used at compile time, so `constexpr` gives no practical benefit over `const`. Using `constexpr` forces the compiler to evaluate `strlen` at compile time for every string literal when constructing `std::string_view`. With many or large embedded files, this can exceed the compiler's limits. This has already happened in PR #863. Switching these arrays to `const` should resolve the issue. --- tools/luthier.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/luthier.luau b/tools/luthier.luau index 8ee72c583..ec43cdf37 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -295,7 +295,7 @@ local function generateEmbeddedModulesIfNeeded(config: EmbeddedModuleConfig) "", `#include "{headerFile}"`, "", - `constexpr std::pair {config.arrayName}[] = \{`, + `const std::pair {config.arrayName}[] = \{`, }) local numItems = 0 From e97dc2505a3e6f20251b52fa8fd6f3c1dfb52cfc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:22:35 +0000 Subject: [PATCH 411/642] Update Lute to 0.1.0-nightly.20260320 (#880) **Lute**: Updated from `0.1.0-nightly.20260306` to `0.1.0-nightly.20260320` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260320 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: ariel --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index c9c2b8297..7adc35dc4 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.4.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260306" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260320" } diff --git a/rokit.toml b/rokit.toml index 3091ff40b..ea9b44a6b 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.4.0" -lute = "luau-lang/lute@0.1.0-nightly.20260306" +lute = "luau-lang/lute@0.1.0-nightly.20260320" From 9d81c53ab01c93baba88934f11c1a133e366c2de Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:44:48 -0700 Subject: [PATCH 412/642] Adds `assert.that` to `@std/assert` (#896) Adds and uses `assert.that(value: unknown, msg: string?): Failure?` in lieu of places we have `assert.eq(_, true)` We considered naming this `assert.true` and `assert.truthy`, but agreed on `assert.that` since it's the [new (suggested) API for NUnit](https://docs.nunit.org/articles/nunit/writing-tests/assertions/assertion-models/constraint.html) --- lute/std/libs/test/assert.luau | 9 +++++++ lute/std/libs/test/types.luau | 1 + tests/cli/doc.test.luau | 4 +-- tests/cli/new.test.luau | 8 +++--- tests/cli/transform.test.luau | 2 +- tests/lute/task.test.luau | 4 +-- tests/lute/vm.test.luau | 4 +-- tests/std/fs.test.luau | 28 ++++++++++---------- tests/std/json.test.luau | 10 ++++---- tests/std/path/path.posix.test.luau | 40 ++++++++++++++--------------- tests/std/path/path.win32.test.luau | 8 +++--- tests/std/stringext.test.luau | 4 +-- tests/std/syntax/parser.test.luau | 4 +-- tests/std/tableext.test.luau | 26 +++++++++---------- tests/std/time.test.luau | 8 +++--- 15 files changed, 85 insertions(+), 75 deletions(-) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index 95d0ce071..a018f2f1b 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -79,6 +79,14 @@ local function neq(lhs: T, rhs: T, msg: string?): Failure? return assertion(msg or `{lhs} == {rhs}`) end +local function that(value: unknown, msg: string?): Failure? + if value then + return nil + end + + return assertion(msg or `Expected a truthy value, but got {formatValue(value)}`) +end + local function errors(callback: () -> (), msg: string?): Failure? local success = pcall(callback) -- if the callback failed, then this assert passes @@ -189,4 +197,5 @@ return table.freeze({ erroreq = reqassert(erroreq), strcontains = reqassert(strcontains), strnotcontains = reqassert(strnotcontains), + that = reqassert(that), }) :: Asserts diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index 3000e6aa0..6eee990e7 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -37,6 +37,7 @@ export type Asserts = { erroreq: ((A...) -> ...unknown, string, A...) -> Failure?, strcontains: (string, string, number?, string?) -> Failure?, strnotcontains: (string, string, string?) -> Failure?, + that: (unknown, string?) -> Failure?, } export type Test = (Asserts) -> () diff --git a/tests/cli/doc.test.luau b/tests/cli/doc.test.luau index 5a458898a..deeab0889 100644 --- a/tests/cli/doc.test.luau +++ b/tests/cli/doc.test.luau @@ -56,8 +56,8 @@ test.suite("LuteDoc", function(suite) assert.strcontains(result.stdout, "Documentation generation complete!") assert.strcontains(result.stdout, tostring(tmpOutputDir)) -- "Files written to: " - assert.eq(fs.exists(path.join(tmpOutputDir, "reference", "lute", "fs.md")), true) - assert.eq(fs.exists(path.join(tmpOutputDir, "reference", "std", "fs.md")), true) + assert.that(fs.exists(path.join(tmpOutputDir, "reference", "lute", "fs.md"))) + assert.that(fs.exists(path.join(tmpOutputDir, "reference", "std", "fs.md"))) fs.removedirectory(tmpOutputDir, { recursive = true }) end) diff --git a/tests/cli/new.test.luau b/tests/cli/new.test.luau index 75cdacd26..43e74ecc9 100644 --- a/tests/cli/new.test.luau +++ b/tests/cli/new.test.luau @@ -22,14 +22,14 @@ test.suite("LuteNew", function(suite) local result = process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) assert.eq(result.exitcode, 0) - assert.eq(true, fs.exists(pathlib.join(workdir, projectName))) + assert.that(fs.exists(pathlib.join(workdir, projectName))) end) suite:case("createsMainScript", function(assert) process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) local mainPath = pathlib.join(workdir, projectName, "main.luau") - assert.eq(fs.exists(mainPath), true) + assert.that(fs.exists(mainPath)) local contents = fs.readfiletostring(mainPath) assert.strcontains(contents, "Hello, world!") end) @@ -38,7 +38,7 @@ test.suite("LuteNew", function(suite) process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) local testPath = pathlib.join(workdir, projectName, "main.test.luau") - assert.eq(fs.exists(testPath), true) + assert.that(fs.exists(testPath)) local contents = fs.readfiletostring(testPath) assert.strcontains(contents, "@std/test") end) @@ -47,7 +47,7 @@ test.suite("LuteNew", function(suite) process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) local configPath = pathlib.join(workdir, projectName, ".config.luau") - assert.eq(fs.exists(configPath), true) + assert.that(fs.exists(configPath)) local contents = fs.readfiletostring(configPath) assert.strcontains(contents, "strict") end) diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index f6e3e9906..6c9fe6965 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -111,7 +111,7 @@ test.suite("LuteTransform", function(suite) fs.close(transformeeHandle) -- Check that the output file has been created and written to - assert.eq(fs.exists(outputPath), true) + assert.that(fs.exists(outputPath)) local h = fs.open(outputPath, "r") local outputContent = fs.read(h) assert.eq(outputContent:find("x ~= x"), nil) diff --git a/tests/lute/task.test.luau b/tests/lute/task.test.luau index 0af452e8c..00e8fc162 100644 --- a/tests/lute/task.test.luau +++ b/tests/lute/task.test.luau @@ -8,7 +8,7 @@ test.suite("LuteTaskSuite", function(suite) local endTime = os.clock() -- task.wait(0.5) actually waits ~0.48 seconds :) - assert.eq(math.ceil(endTime - startTime) >= 0.5, true) + assert.that(math.ceil(endTime - startTime) >= 0.5) end) suite:case("taskWaitInLoop", function(assert) @@ -20,7 +20,7 @@ test.suite("LuteTaskSuite", function(suite) end local endTime = os.clock() - assert.eq(endTime - startTime >= 0.5, true) + assert.that(endTime - startTime >= 0.5) end) suite:case("taskSpawn0ArgsErrors", function(assert) diff --git a/tests/lute/vm.test.luau b/tests/lute/vm.test.luau index c2ffb5b1d..2ff2035fe 100644 --- a/tests/lute/vm.test.luau +++ b/tests/lute/vm.test.luau @@ -12,7 +12,7 @@ test.suite("LuteVmSuite", function(suite) do local handle = fs.open(child_vm, "w+") - assert.eq(fs.exists(child_vm), true) + assert.that(fs.exists(child_vm)) fs.write( handle, [[ @@ -44,7 +44,7 @@ test.suite("LuteVmSuite", function(suite) do local handle = fs.open(parent_vm, "w+") - assert.eq(fs.exists(parent_vm), true) + assert.that(fs.exists(parent_vm)) fs.write( handle, [[ diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index d24cda960..41de11b6a 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -12,7 +12,7 @@ test.suite("FsSuite", function(suite) local file = path.join(tmpdir, "file.txt") local h = fs.open(file, "w+") - assert.eq(fs.exists(file), true) + assert.that(fs.exists(file)) fs.write(h, "abc") fs.close(h) @@ -40,9 +40,9 @@ test.suite("FsSuite", function(suite) local file = path.join(tmpdir, "roblox.txt") fs.writestringtofile(file, "roblox") - assert.eq(fs.exists(file), true) + assert.that(fs.exists(file)) assert.eq(fs.type(file), "file") - assert.eq(fs.exists(tmpdir), true) + assert.that(fs.exists(tmpdir)) assert.eq(fs.type(tmpdir), "dir") fs.remove(file) @@ -54,7 +54,7 @@ test.suite("FsSuite", function(suite) fs.createdirectory(dir, { makeparents = true }) end - assert.eq(fs.exists(dir), true) + assert.that(fs.exists(dir)) assert.eq(fs.type(dir), "dir") local entries = fs.listdirectory(tmpdir) @@ -64,7 +64,7 @@ test.suite("FsSuite", function(suite) found = true end end - assert.eq(found, true) + assert.that(found) fs.removedirectory(dir) end) @@ -115,7 +115,7 @@ test.suite("FsSuite", function(suite) failure.assertion("watcher event was nil") return end - assert.eq(event.change or event.rename, true) + assert.that(event.change or event.rename) watcher:close() if fs.exists(watched) then @@ -126,7 +126,7 @@ test.suite("FsSuite", function(suite) suite:case("createDirectoryMakeparentsTrue", function(assert) local nestedDir = path.join(tmpdir, "nested", "directory") fs.createdirectory(nestedDir, { makeparents = true }) - assert.eq(fs.exists(nestedDir), true) + assert.that(fs.exists(nestedDir)) fs.removedirectory(nestedDir) fs.removedirectory(path.join(tmpdir, "nested")) @@ -180,7 +180,7 @@ test.suite("FsSuite", function(suite) end local h = fs.open(file, "w") - assert.eq(fs.exists(file), true) + assert.that(fs.exists(file)) fs.write(h, "hello") fs.close(h) @@ -192,7 +192,7 @@ test.suite("FsSuite", function(suite) suite:case("removeDirectoryNonRecursive", function(assert) local dir = path.join(tmpdir, "empty_dir") fs.createdirectory(dir, { makeparents = true }) - assert.eq(fs.exists(dir), true) + assert.that(fs.exists(dir)) fs.removedirectory(dir) assert.eq(fs.exists(dir), false) @@ -215,10 +215,10 @@ test.suite("FsSuite", function(suite) fs.writestringtofile(file2, "content2") fs.writestringtofile(file3, "content3") - assert.eq(fs.exists(dir), true) - assert.eq(fs.exists(file1), true) - assert.eq(fs.exists(file2), true) - assert.eq(fs.exists(file3), true) + assert.that(fs.exists(dir)) + assert.that(fs.exists(file1)) + assert.that(fs.exists(file2)) + assert.that(fs.exists(file3)) fs.removedirectory(dir, { recursive = true }) @@ -286,7 +286,7 @@ test.suite("FsSuite", function(suite) break end end - assert.eq(found, true) + assert.that(found) end fs.removedirectory(baseDir, { recursive = true }) diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau index d900352ca..9bf9a6db9 100644 --- a/tests/std/json.test.luau +++ b/tests/std/json.test.luau @@ -26,18 +26,18 @@ test.suite("JSONParsingTestSuite", function(suite) if stringext.hasprefix(name, "y_") then -- valid JSON: must parse - assert.eq(ok, true) + assert.that(ok) -- serialize the parsed value and ensure it parses again local _ok2, _ser = pcall(function() return json.serialize(parsed) end) - assert.eq(_ok2, true) + assert.that(_ok2) local _ok3 = pcall(function() json.deserialize(_ser) end) - assert.eq(_ok3, true) + assert.that(_ok3) elseif stringext.hasprefix(name, "n_") then -- invalid JSON: must fail to parse assert.eq(ok, false) @@ -74,11 +74,11 @@ test.suite("JSONParsingTestSuite", function(suite) if obj then assert.eq(type(obj), "table") - assert.eq(obj.a, true) + assert.that(obj.a) assert.eq(type(obj.b), "table") local obj_b = json.asobject(obj.b) if obj_b then - assert.eq(obj_b.c, true) + assert.that(obj_b.c) local obj_b_d = obj_b.d if typeof(obj_b_d) ~= "table" then failure.assertion(`Expected obj.b.d to be a table, was a {typeof(obj_b_d)}`) diff --git a/tests/std/path/path.posix.test.luau b/tests/std/path/path.posix.test.luau index 8a2f5a5ea..a8f96d3ae 100644 --- a/tests/std/path/path.posix.test.luau +++ b/tests/std/path/path.posix.test.luau @@ -7,7 +7,7 @@ local posix = require("@std/path/posix") test.suite("PathPosixParseSuite", function(suite) suite:case("parsePosixAbsolutePath", function(assert) local result = path.posix.parse("/home/user/documents/file.txt") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 4) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -31,7 +31,7 @@ test.suite("PathPosixParseSuite", function(suite) suite:case("parseRootPath", function(assert) local result = path.posix.parse("/") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 0) end) @@ -246,7 +246,7 @@ end) test.suite("PathPosixIsAbsoluteSuite", function(suite) suite:case("absolutePosixAbsolutePath", function(assert) local result = path.posix.isabsolute("/home/user/documents/file.txt") - assert.eq(result, true) + assert.that(result) end) suite:case("absolutePosixRelativePath", function(assert) @@ -256,7 +256,7 @@ test.suite("PathPosixIsAbsoluteSuite", function(suite) suite:case("absoluteRootPath", function(assert) local result = path.posix.isabsolute("/") - assert.eq(result, true) + assert.that(result) end) suite:case("absoluteEmptyPath", function(assert) @@ -270,7 +270,7 @@ test.suite("PathPosixIsAbsoluteSuite", function(suite) absolute = true, }, posix.pathmt) local result = path.posix.isabsolute(pathobj) - assert.eq(result, true) + assert.that(result) end) suite:case("absolutePathobjRelative", function(assert) @@ -286,7 +286,7 @@ end) test.suite("PathPosixNormalizeSuite", function(suite) suite:case("normalizeRemovesCurrentDirectory", function(assert) local result = path.posix.normalize("/home/./user/documents") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -295,7 +295,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) suite:case("normalizeResolvesParentDirectory", function(assert) local result = path.posix.normalize("/home/user/../documents") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 2) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "documents") @@ -303,7 +303,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) suite:case("normalizeMultipleDots", function(assert) local result = path.posix.normalize("/home/user/./documents/../files/./") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -320,7 +320,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) suite:case("normalizeRemovesEmptySegments", function(assert) local result = path.posix.normalize("/home//user///documents") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -329,7 +329,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) suite:case("normalizeParentBeyondRoot", function(assert) local result = path.posix.normalize("/home/../..") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 0) end) @@ -351,7 +351,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) absolute = true, }, posix.pathmt) local result = path.posix.normalize(pathobj) - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 2) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "documents") @@ -403,7 +403,7 @@ end) test.suite("PathPosixJoinSuite", function(suite) suite:case("joinAbsoluteAndRelative", function(assert) local result = path.posix.join("/home/user", "documents", "file.txt") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 4) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -436,7 +436,7 @@ test.suite("PathPosixJoinSuite", function(suite) suite:case("joinSingleArgument", function(assert) local result = path.posix.join("/home/user") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 2) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -448,7 +448,7 @@ test.suite("PathPosixJoinSuite", function(suite) absolute = true, }, posix.pathmt) local result = path.posix.join(pathobj, "documents/file.txt") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 4) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -457,7 +457,7 @@ test.suite("PathPosixJoinSuite", function(suite) assert.eq(#pathobj.parts, 2) -- original pathobj should be unchanged assert.eq(pathobj.parts[1], "home") assert.eq(pathobj.parts[2], "user") - assert.eq(pathobj.absolute, true) + assert.that(pathobj.absolute) end) suite:case("joinErrorOnAbsoluteAddend", function(assert) @@ -471,7 +471,7 @@ end) test.suite("PathPosixResolveSuite", function(suite) suite:case("resolveAbsolutePath", function(assert) local result = path.posix.resolve("/home/user/documents/file.txt") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 4) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -493,7 +493,7 @@ test.suite("PathPosixResolveSuite", function(suite) suite:case("resolveMultiplePaths", function(assert) local result = path.posix.resolve("/home", "user", "../admin", "documents") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "admin") @@ -502,7 +502,7 @@ test.suite("PathPosixResolveSuite", function(suite) suite:case("resolveWithAbsoluteInMiddle", function(assert) local result = path.posix.resolve("relative", "/absolute/path", "more") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "absolute") assert.eq(result.parts[2], "path") @@ -519,7 +519,7 @@ test.suite("PathPosixResolveSuite", function(suite) suite:case("resolveEmptyStrings", function(assert) local result = path.posix.resolve("", "", "/home/user") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 2) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "user") @@ -527,7 +527,7 @@ test.suite("PathPosixResolveSuite", function(suite) suite:case("resolveWithDotSegments", function(assert) local result = path.posix.resolve("/home/user", "../admin/./documents") - assert.eq(result.absolute, true) + assert.that(result.absolute) assert.eq(#result.parts, 3) assert.eq(result.parts[1], "home") assert.eq(result.parts[2], "admin") diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index 60db50377..0030cc0fe 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -346,7 +346,7 @@ end) test.suite("PathWin32IsAbsoluteSuite", function(suite) suite:case("isAbsoluteWindowsAbsolutePath", function(assert) local result = path.win32.isabsolute("C:\\Users\\username\\Documents\\file.txt") - assert.eq(result, true) + assert.that(result) end) suite:case("isAbsoluteWindowsRelativePath", function(assert) @@ -361,12 +361,12 @@ test.suite("PathWin32IsAbsoluteSuite", function(suite) suite:case("isAbsoluteWindowsUncPath", function(assert) local result = path.win32.isabsolute("\\\\server\\share\\folder\\file.txt") - assert.eq(result, true) + assert.that(result) end) suite:case("isAbsoluteRootPath", function(assert) local result = path.win32.isabsolute("C:\\") - assert.eq(result, true) + assert.that(result) end) suite:case("isAbsoluteEmptyPath", function(assert) @@ -381,7 +381,7 @@ test.suite("PathWin32IsAbsoluteSuite", function(suite) drive = "C" :: string?, }, win32.pathmt) local result = path.win32.isabsolute(pathobj) - assert.eq(result, true) + assert.that(result) end) suite:case("isAbsolutePathobjRelative", function(assert) diff --git a/tests/std/stringext.test.luau b/tests/std/stringext.test.luau index 92d8717eb..e50798837 100644 --- a/tests/std/stringext.test.luau +++ b/tests/std/stringext.test.luau @@ -3,10 +3,10 @@ local test = require("@std/test") test.suite("StringExt", function(suite) suite:case("hasprefixAndHassuffix", function(assert) - assert.eq(stringext.hasprefix("hello", "he"), true) + assert.that(stringext.hasprefix("hello", "he")) assert.eq(stringext.hasprefix("hi", "hello"), false) - assert.eq(stringext.hassuffix("foobar", "bar"), true) + assert.that(stringext.hassuffix("foobar", "bar")) assert.eq(stringext.hassuffix("foo", "foobar"), false) end) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 672acf041..b78f04c97 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -219,7 +219,7 @@ test.suite("parseExpr", function(suite) assert.eq(trueExpr.tag, "boolean") assert.eq(trueExpr.kind, "expr") assert.eq((trueExpr :: syntax.AstExprConstantBool).token.text, "true") - assert.eq((trueExpr :: syntax.AstExprConstantBool).value, true) + assert.that((trueExpr :: syntax.AstExprConstantBool).value) local falseExpr = parser.parseexpr("false") assert.eq(falseExpr.tag, "boolean") @@ -941,7 +941,7 @@ test.suite("parse types", function(suite) local singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool assert.eq(singletonBool.token.text, "true") - assert.eq(singletonBool.value, true) + assert.that(singletonBool.value) block = parser.parseblock("type FalseType = false") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index e14450006..b1aafeb50 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -66,10 +66,10 @@ test.suite("TableExt", function(suite) suite:case("toset", function(assert) local a = { 1, 2, 4, 2, 1 } local setA = tableext.toset(a) - assert.eq(setA[1], true) - assert.eq(setA[2], true) + assert.that(setA[1]) + assert.that(setA[2]) assert.eq(setA[3], nil) - assert.eq(setA[4], true) + assert.that(setA[4]) assert.eq(setA[5], nil) local setSize = #tableext.keys(setA) @@ -80,19 +80,19 @@ test.suite("TableExt", function(suite) local foundKeys: { number } = {} for key, value in setA do - assert.eq(value, true) + assert.that(value) table.insert(foundKeys, key) end assert.eq(#foundKeys, setSize) - assert.eq(setA[foundKeys[1]], true) - assert.eq(setA[foundKeys[2]], true) - assert.eq(setA[foundKeys[3]], true) + assert.that(setA[foundKeys[1]]) + assert.that(setA[foundKeys[2]]) + assert.that(setA[foundKeys[3]]) local b = { "a", "b", "c", "a" } local setB = tableext.toset(b) - assert.eq(setB["a"], true) - assert.eq(setB["b"], true) - assert.eq(setB["c"], true) + assert.that(setB["a"]) + assert.that(setB["b"]) + assert.that(setB["c"]) assert.eq(setB["d"], nil) assert.eq(#tableext.keys(setB), 3) assert.neq(#tableext.keys(setB), #b) @@ -107,7 +107,7 @@ test.suite("TableExt", function(suite) local hasEven = tableext.any(a, function(v: number) return v % 2 == 0 end) - assert.eq(hasEven, true) + assert.that(hasEven) local hasGreaterThanFive = tableext.any(a, function(v: number) return v > 5 @@ -126,7 +126,7 @@ test.suite("TableExt", function(suite) local allEven = tableext.all(a, function(v: number) return v % 2 == 0 end) - assert.eq(allEven, true) + assert.that(allEven) local allLessThanEight = tableext.all(a, function(v: number) return v < 8 @@ -137,6 +137,6 @@ test.suite("TableExt", function(suite) local allInEmpty = tableext.all(a, function(_: number) return false end) - assert.eq(allInEmpty, true) + assert.that(allInEmpty) end) end) diff --git a/tests/std/time.test.luau b/tests/std/time.test.luau index 5644d2209..dde601901 100644 --- a/tests/std/time.test.luau +++ b/tests/std/time.test.luau @@ -132,11 +132,11 @@ test.suite("TimeSuite", function(suite) local b = duration.milliseconds(1000) local c = duration.milliseconds(999) - assert.eq(a == b, true) + assert.that(a == b) assert.eq(a == c, false) - assert.eq(c < a, true) - assert.eq(c <= a, true) - assert.eq(a <= b, true) + assert.that(c < a) + assert.that(c <= a) + assert.that(a <= b) assert.eq(a < b, false) end) From 5dd93451719ec61cd7a0907ae02a39c8f0b568fa Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 25 Mar 2026 09:54:28 -0700 Subject: [PATCH 413/642] Enables CLI commands to use the lute version string defined in C++ (#898) We've defined a version string in CMake, which gets turned into a header that C++ commands can consume. However, the code in lute uses a hardcoded version of this string. We need to expose this information to the cli commands so that they can use the correct version when setting up new projects or generating type definition files. This PR sets up a global table called `version` with shape: `{version: string, versionFull : string, versionSuffix : string}` for consumption _exclusively_ by the CLI commands. User code and code loaded by various lute apis in the tests cannot use this global (as those are sandboxed environments). --- lute/cli/commands/lib/typedefs.luau | 5 ++++- lute/cli/src/climain.cpp | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lute/cli/commands/lib/typedefs.luau b/lute/cli/commands/lib/typedefs.luau index b7c26f0bc..3d8f0fc91 100644 --- a/lute/cli/commands/lib/typedefs.luau +++ b/lute/cli/commands/lib/typedefs.luau @@ -1,7 +1,10 @@ local path = require("@std/path") local process = require("@std/process") -local TYPEDEFS_VERSION = "0.1.0" +type version = { version: string, versionSuffix: string, versionFull: string } +local version: version = _G["version"] + +local TYPEDEFS_VERSION = version.version local TYPEDEFS_BASE_DIR = `.lute/typedefs/{TYPEDEFS_VERSION}` local TYPEDEFS_RELATIVE_DIR = `~/{TYPEDEFS_BASE_DIR}` local TYPEDEFS_PATH = path.join(process.homedir(), TYPEDEFS_BASE_DIR) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index a147491e9..ad5592cef 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -628,10 +628,28 @@ int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& rep return 0; } +void setupVersionLibrary(lua_State* L) +{ + lua_checkstack(L, 2); + + lua_createtable(L, 0, 3); + + lua_pushstring(L, LUTE_VERSION); + lua_setfield(L, -2, "version"); + + lua_pushstring(L, LUTE_VERSION_SUFFIX); + lua_setfield(L, -2, "versionSuffix"); + + lua_pushstring(L, LUTE_VERSION_FULL); + lua_setfield(L, -2, "versionFull"); + + lua_setglobal(L, "version"); +} + int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv, LuteReporter& reporter) { Runtime runtime; - lua_State* L = setupCliCommandState(runtime); + lua_State* L = setupCliCommandState(runtime, setupVersionLibrary); std::string bytecode = Luau::compile(std::string(result.contents), copts()); return runBytecode(runtime, bytecode, "@" + result.path, L, program_argc, program_argv, reporter) ? 0 : 1; From 884a72bcc7260763fcb1179157a5acdda498261d Mon Sep 17 00:00:00 2001 From: Andrey Zhabsky Date: Wed, 25 Mar 2026 19:24:41 +0200 Subject: [PATCH 414/642] Fixes `TOML` deserialization (#877) Fix #643 `TOML` `deserialization` of quoted keys containing dots, which were incorrectly split into separate keys. Adds quote-aware path splitting for `deserialization` and key quoting for `serialization` so round-trips preserve structure. Now we can run this code: ```luau local pp = require("@batteries/pp") local toml = require("@batteries/toml") local content = [=[ [assets.package] assetId = "120786139379662" [images."icon.png"] assetId = "95124518202499" hash = "fac185770cf5e824d4c0a9b59e547df42dc10011c5ad139bfc2c7e1507eef631" ]=] local deserialized = toml.deserialize(content) local serialized = toml.serialize(deserialized) print("\nDeserialized:") print(pp(deserialized)) print("\nSerialized:") print(serialized) ``` and get: ```console Deserialized: { assets = { package = { assetId = "120786139379662", }, }, images = { ["icon.png"] = { assetId = "95124518202499", hash = "fac185770cf5e824d4c0a9b59e547df42dc10011c5ad139bfc2c7e1507eef631", }, }, } Serialized: [assets.package] assetId = "120786139379662" [images."icon.png"] hash = "fac185770cf5e824d4c0a9b59e547df42dc10011c5ad139bfc2c7e1507eef631" assetId = "95124518202499" ``` --- batteries/toml.luau | 73 +++++++++++++++++++++++++------- tests/batteries/toml.test.luau | 76 ++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 tests/batteries/toml.test.luau diff --git a/batteries/toml.luau b/batteries/toml.luau index 64988da86..3bf7accfc 100644 --- a/batteries/toml.luau +++ b/batteries/toml.luau @@ -23,6 +23,13 @@ local function serializeValue(value: string | number): string end end +local function quoteKey(key: string): string + if not string.match(key, "^[%w_-]+$") then + return '"' .. key .. '"' + end + return key +end + local function hasNestedTables(tbl: { [unknown]: unknown }): boolean for _, v in tbl do if typeof(v) == "table" and next(v) ~= nil then @@ -41,7 +48,7 @@ local function tableToToml(tbl: { [any]: any }, parent: string?): string if typeof(v) == "table" and next(v) ~= nil then if #v > 0 then for _, entry in v do - result ..= "\n[[" .. (parent and parent .. "." or "") .. k .. "]]\n" + result ..= "\n[[" .. (parent and parent .. "." or "") .. quoteKey(k) .. "]]\n" result ..= tableToToml(entry, nil) end else @@ -49,12 +56,12 @@ local function tableToToml(tbl: { [any]: any }, parent: string?): string end else hasDirectValues = true - result ..= k .. " = " .. serializeValue(v) .. "\n" + result ..= quoteKey(k) .. " = " .. serializeValue(v) .. "\n" end end for k, v in subTables do - local key = parent and (parent .. "." .. k) or k + local key = parent and (parent .. "." .. quoteKey(k)) or quoteKey(k) if hasNestedTables(v) == true then result ..= tableToToml(v, key) @@ -81,6 +88,41 @@ type DeserializerState = { cursor: number, } +local function splitTablePath(path: string): { string } + local parts = {} + local index = 1 + local pathLen = string.len(path) + + while index <= pathLen do + local symbol = string.sub(path, index, index) + if symbol == "." then + index += 1 + elseif symbol == '"' or symbol == "'" then + local closePos = string.find(path, symbol, index + 1, true) or pathLen + 1 + table.insert(parts, string.sub(path, index + 1, closePos - 1)) + index = closePos + 1 + else + local nextSpecial = string.find(path, "[.\"']", index) + local segEnd = if nextSpecial then nextSpecial - 1 else pathLen + table.insert(parts, string.sub(path, index, segEnd)) + index = segEnd + 1 + end + end + + return parts +end + +local function ensurePath(root: { [string]: unknown }, parts: { string }, depth: number?): { [string]: unknown } + local current = root + for i = 1, depth or #parts do + if not current[parts[i]] then + current[parts[i]] = {} + end + current = current[parts[i]] :: { [string]: unknown } + end + return current +end + local function skipWhitespace(state: DeserializerState): () local pos = state.cursor while pos <= string.len(state.buf) and string.match(string.sub(state.buf, pos, pos), "%s") do @@ -119,31 +161,30 @@ local function deserialize(input: string): { [string]: unknown } if string.match(line, "^%[%[(.-)%]%]$") then local tableName = string.match(line, "^%[%[(.-)%]%]$") or "" - arrayTables[tableName] = arrayTables[tableName] or {} + + if not arrayTables[tableName] then + arrayTables[tableName] = {} + local parts = splitTablePath(tableName) + local leaf = parts[#parts] + local _parent = ensurePath(result, parts, #parts - 1) + _parent[leaf] = arrayTables[tableName] + end local newEntry = {} table.insert(arrayTables[tableName], newEntry) - - result[tableName] = arrayTables[tableName] currentTable = newEntry elseif string.match(line, "^%[(.-)%]$") then local tablePath = string.match(line, "^%[(.-)%]$") - local parent = result - - for section in string.gmatch(tablePath :: string, "([^.]+)") do - if not parent[section] then - parent[section] = {} - end - parent = parent[section] - end - - currentTable = parent + currentTable = ensurePath(result, splitTablePath(tablePath :: string)) elseif string.match(line, "^(.-)%s*=%s*(.-)$") then local key, value = string.match(line, "^(.-)%s*=%s*(.-)$") assert(key and value) key = string.match(key, "^%s*(.-)%s*$") value = string.match(value, "^%s*(.-)%s*$") assert(key and value) + if string.match(key, '^"(.*)"$') or string.match(key, "^'(.*)'$") then + key = string.sub(key, 2, -2) + end if string.match(value, '^"(.*)"$') or string.match(value, "^'(.*)'$") then value = string.sub(value, 2, -2) value = string.gsub(value, "\\\\", "\\") diff --git a/tests/batteries/toml.test.luau b/tests/batteries/toml.test.luau new file mode 100644 index 000000000..35c82d3fb --- /dev/null +++ b/tests/batteries/toml.test.luau @@ -0,0 +1,76 @@ +local test = require("@std/test") +local toml = require("@batteries/toml") + +test.suite("TOML deserialization", function(suite) + suite:case("simpleKeyValue", function(assert) + local result: any = toml.deserialize('name = "hello"\ncount = 42\nenabled = true\n') + assert.eq(result.name, "hello") + assert.eq(result.count, 42) + assert.eq(result.enabled, true) + end) + + suite:case("nestedTableViaSection", function(assert) + local result: any = toml.deserialize('[server]\nhost = "localhost"\nport = 8080\n') + assert.eq(result.server.host, "localhost") + assert.eq(result.server.port, 8080) + end) + + suite:case("dottedTablePath", function(assert) + local result: any = toml.deserialize("[a.b]\nvalue = 1\n") + assert.eq(result.a.b.value, 1) + end) + + suite:case("quotedKeyWithDotInTableHeader", function(assert) + local input = '[images."icon.png"]\nassetId = "123"\n' + local result: any = toml.deserialize(input) + assert.neq(result.images, nil) + assert.neq(result.images["icon.png"], nil) + assert.eq(result.images["icon.png"].assetId, "123") + end) + + suite:case("singleQuotedKeyWithDotInTableHeader", function(assert) + local input = "[images.'icon.png']\nassetId = \"456\"\n" + local result: any = toml.deserialize(input) + assert.neq(result.images, nil) + assert.neq(result.images["icon.png"], nil) + assert.eq(result.images["icon.png"].assetId, "456") + end) + + suite:case("mixedDottedAndQuotedPath", function(assert) + local input = '[a."b.c".d]\nvalue = 1\n' + local result: any = toml.deserialize(input) + assert.neq(result.a, nil) + assert.neq(result.a["b.c"], nil) + assert.neq(result.a["b.c"].d, nil) + assert.eq(result.a["b.c"].d.value, 1) + end) + + suite:case("quotedKeyInKeyValuePair", function(assert) + local result: any = toml.deserialize('"icon.png" = "value"\n') + assert.eq(result["icon.png"], "value") + end) + + suite:case("specialValues", function(assert) + local result: any = toml.deserialize("a = inf\nb = -inf\nc = nan\n") + assert.eq(result.a, math.huge) + assert.eq(result.b, -math.huge) + assert.neq(result.c, result.c) -- NaN ~= NaN + end) +end) + +test.suite("TOML serialization", function(suite) + suite:case("keyWithDotIsQuoted", function(assert) + local input = { images = { ["icon.png"] = { assetId = "123" } } } + local output = toml.serialize(input) + assert.strcontains(output, '"icon.png"') + end) + + suite:case("roundTrip", function(assert) + local input = '[images."icon.png"]\nassetId = "123"\nhash = "abc"\n' + local first: any = toml.deserialize(input) + local serialized = toml.serialize(first) + local second: any = toml.deserialize(serialized) + assert.eq(second.images["icon.png"].assetId, "123") + assert.eq(second.images["icon.png"].hash, "abc") + end) +end) From d855c6e73c57bd21cbf84b5a8bfd27c76769f957 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:41:45 -0700 Subject: [PATCH 415/642] Rename remaining lute API types to `PascalCase` (#899) updating path types and query to PascalCase --- examples/lints/almost_swapped.luau | 2 +- examples/lints/divide_by_zero.luau | 2 +- lute/cli/commands/doc/init.luau | 18 +++--- lute/cli/commands/lib/files.luau | 6 +- lute/cli/commands/lib/ignore.luau | 4 +- lute/cli/commands/lib/parseIgnores.luau | 2 +- lute/cli/commands/lint/init.luau | 10 ++-- lute/cli/commands/lint/internaltypes.luau | 4 +- lute/cli/commands/lint/lsp/init.luau | 4 +- lute/cli/commands/lint/printer.luau | 2 +- .../commands/lint/rules/almost_swapped.luau | 2 +- .../lint/rules/constant_table_comparison.luau | 2 +- .../commands/lint/rules/divide_by_zero.luau | 2 +- .../commands/lint/rules/duplicate_keys.luau | 2 +- .../commands/lint/rules/empty_if_block.luau | 2 +- .../lint/rules/global_function_in_scope.luau | 2 +- .../lint/rules/parenthesized_conditions.luau | 2 +- .../commands/lint/rules/unused_variable.luau | 2 +- lute/cli/commands/lint/types.luau | 4 +- lute/cli/commands/test/finder.luau | 8 +-- lute/cli/commands/test/init.luau | 2 +- lute/cli/commands/test/snap/runner.luau | 4 +- lute/cli/commands/test/snap/types.luau | 6 +- lute/cli/commands/transform/init.luau | 2 +- lute/std/libs/fs.luau | 42 +++++++------- lute/std/libs/luau.luau | 6 +- lute/std/libs/path/init.luau | 56 +++++++++---------- lute/std/libs/path/pathinterface.luau | 4 +- lute/std/libs/path/posix/init.luau | 38 ++++++------- lute/std/libs/path/posix/types.luau | 6 +- lute/std/libs/path/types.luau | 4 +- lute/std/libs/path/win32/init.luau | 48 ++++++++-------- lute/std/libs/path/win32/types.luau | 10 ++-- lute/std/libs/process.luau | 10 ++-- lute/std/libs/syntax/query.luau | 32 +++++------ lute/std/libs/system/init.luau | 2 +- tests/std/path/path.posix.test.luau | 28 +++++----- tests/std/path/path.win32.test.luau | 50 ++++++++--------- tests/std/process.test.luau | 6 +- tests/std/syntax/query.test.luau | 4 +- tools/check.luau | 2 +- 41 files changed, 222 insertions(+), 222 deletions(-) diff --git a/examples/lints/almost_swapped.luau b/examples/lints/almost_swapped.luau index d167fc417..c04b45e97 100644 --- a/examples/lints/almost_swapped.luau +++ b/examples/lints/almost_swapped.luau @@ -59,7 +59,7 @@ end -- Report instances of attempted swaps like: -- a = b; b = a -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } +local function lint(ast: syntax.AstStatBlock, sourcepath: path.Path?): { lintTypes.LintViolation } local violations = {} local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes diff --git a/examples/lints/divide_by_zero.luau b/examples/lints/divide_by_zero.luau index 04794f292..f7f9ac061 100644 --- a/examples/lints/divide_by_zero.luau +++ b/examples/lints/divide_by_zero.luau @@ -9,7 +9,7 @@ local utils = require("@std/syntax/utils") local name = "divide_by_zero" local message = "Division by zero detected." -local function lint(ast: syntax.AstStatBlock, sourcepath: path.path?): { lintTypes.LintViolation } +local function lint(ast: syntax.AstStatBlock, sourcepath: path.Path?): { lintTypes.LintViolation } return query .findallfromroot(ast, utils.isExprBinary) :filter(function(bin) diff --git a/lute/cli/commands/doc/init.luau b/lute/cli/commands/doc/init.luau index 5e0afc735..8d2590043 100644 --- a/lute/cli/commands/doc/init.luau +++ b/lute/cli/commands/doc/init.luau @@ -182,8 +182,8 @@ type Definition = { name: string, signature: string, doc: string? } local function generateMarkdown( moduleName: string, definitions: { Definition }, - filePath: path.pathlike, - libraryPath: path.pathlike, + filePath: path.Pathlike, + libraryPath: path.Pathlike, requireLibraryAlias: string ): string local libPathStr = tostring(libraryPath) @@ -224,8 +224,8 @@ local function generateMarkdown( end local function parseModule( - module: path.pathlike, - filePath: path.pathlike + module: path.Pathlike, + filePath: path.Pathlike ): { { name: string, signature: string, doc: string? } } local content = fs.readfiletostring(filePath) local lines = string.split(content, "\n") @@ -256,9 +256,9 @@ local function parseModule( end local function processDirectory( - modulePath: path.pathlike, - documentationPath: path.pathlike, - libraryPath: path.pathlike, + modulePath: path.Pathlike, + documentationPath: path.Pathlike, + libraryPath: path.Pathlike, requireLibraryAlias: string ) fs.createdirectory(documentationPath, { makeparents = true }) @@ -302,7 +302,7 @@ end type Module = { name: string, isDir: boolean } -- Helper to generate index with table of children -local function generateLibraryIndex(title: string, alias: string, modulePath: path.pathlike): string +local function generateLibraryIndex(title: string, alias: string, modulePath: path.Pathlike): string local entries = fs.listdirectory(modulePath) local modules: { Module } = {} @@ -343,7 +343,7 @@ local function generateLibraryIndex(title: string, alias: string, modulePath: pa return table.concat(lines, "\n") end -local function generateAllDocs(outputDir: path.path, modulePaths: { string }): () +local function generateAllDocs(outputDir: path.Path, modulePaths: { string }): () -- Create top-level reference index page local referenceBasePath = path.join(outputDir, "reference") fs.createdirectory(referenceBasePath, { makeparents = true }) diff --git a/lute/cli/commands/lib/files.luau b/lute/cli/commands/lib/files.luau index 7000e6bf0..4e908438e 100644 --- a/lute/cli/commands/lib/files.luau +++ b/lute/cli/commands/lib/files.luau @@ -7,10 +7,10 @@ local stringext = require("@std/stringext") local tableext = require("@std/tableext") local function traverseDirectoryRecursive( - directory: path.path, + directory: path.Path, ignoreResolver: ignore.IgnoreResolver, verbose: boolean? -): { path.path } +): { path.Path } local results = {} for _, entry in fs.listdir(path.format(directory)) do @@ -44,7 +44,7 @@ local function traverseDirectoryRecursive( return results end -local function getSourceFiles(paths: { string }, verbose: boolean?): { path.path } +local function getSourceFiles(paths: { string }, verbose: boolean?): { path.Path } local files = {} local ignoreResolver = ignore.new() diff --git a/lute/cli/commands/lib/ignore.luau b/lute/cli/commands/lib/ignore.luau index 5e79cc12f..15bd0be08 100644 --- a/lute/cli/commands/lib/ignore.luau +++ b/lute/cli/commands/lib/ignore.luau @@ -27,7 +27,7 @@ function IgnoreResolver.new(): IgnoreResolver end --- Checks whether the given file matches an ignore -function IgnoreResolver.isIgnoredFile(self: IgnoreResolver, filepath: path.path) +function IgnoreResolver.isIgnoredFile(self: IgnoreResolver, filepath: path.Path) local directory = if #filepath.parts > 0 then path.dirname(filepath) else nil assert(directory ~= nil, "filepath has no directory!") @@ -35,7 +35,7 @@ function IgnoreResolver.isIgnoredFile(self: IgnoreResolver, filepath: path.path) return isIgnored(ignoreData, path.format(filepath), false) end -function IgnoreResolver.isIgnoredDirectory(self: IgnoreResolver, directorypath: path.path) +function IgnoreResolver.isIgnoredDirectory(self: IgnoreResolver, directorypath: path.Path) -- Hardcode: skip the .git directory local basename = path.basename(directorypath) if basename == ".git" then diff --git a/lute/cli/commands/lib/parseIgnores.luau b/lute/cli/commands/lib/parseIgnores.luau index 42bb076bb..5a353d97e 100644 --- a/lute/cli/commands/lib/parseIgnores.luau +++ b/lute/cli/commands/lib/parseIgnores.luau @@ -134,7 +134,7 @@ function parseIgnores.parseIgnoreContents(location: string, contents: string | { return ignoreData end -function parseIgnores.parseGitignore(filepath: path.path): GitignoreData +function parseIgnores.parseGitignore(filepath: path.Path): GitignoreData local contents = fs.readfiletostring(path.format(filepath)) local parentDirectory = if #filepath.parts > 0 then path.dirname(filepath) else nil assert(parentDirectory) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 44b8a276e..b6a23a984 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -276,7 +276,7 @@ end local function lintString( source: string, lintRules: { types.LintRule }, - inputFilePath: pathLib.path?, + inputFilePath: pathLib.Path?, configData: internalTypes.RuleConfigData ): { types.LintViolation } if VERBOSE then @@ -380,7 +380,7 @@ local function applySuggestedFixes(source: string, fixes: { fix }): string end local function lintFile( - inputFilePath: pathLib.path, + inputFilePath: pathLib.Path, lintRules: { types.LintRule }, autofixEnabled: boolean, configData: internalTypes.RuleConfigData @@ -429,13 +429,13 @@ local function lintPaths( autofixEnabled: boolean, configData: internalTypes.RuleConfigData, ignoreData: parseIgnores.GitignoreData -): { [pathLib.path]: { types.LintViolation } } +): { [pathLib.Path]: { types.LintViolation } } if VERBOSE then print("Filtering input files by gitignore") end local inputFiles = files.getSourceFiles(inputFilePaths, VERBOSE) - local allViolations: { [pathLib.path]: { types.LintViolation } } = {} + local allViolations: { [pathLib.Path]: { types.LintViolation } } = {} local hasIgnores = #ignoreData.ignores > 0 for _, inputPath in inputFiles do @@ -553,7 +553,7 @@ local function main(...: string) end -- load local rules from config rules path - local configuredRulePaths: { pathLib.pathlike } = if lintConfig.rulepaths then lintConfig.rulepaths else {} + local configuredRulePaths: { pathLib.Pathlike } = if lintConfig.rulepaths then lintConfig.rulepaths else {} for _, path in configuredRulePaths do local resolvedPath = pathLib.resolve(rootLocation, path) local loadedRules, loadedConfigs, loadedIgnoreData = diff --git a/lute/cli/commands/lint/internaltypes.luau b/lute/cli/commands/lint/internaltypes.luau index 3faf34121..71e4bf2bf 100644 --- a/lute/cli/commands/lint/internaltypes.luau +++ b/lute/cli/commands/lint/internaltypes.luau @@ -10,9 +10,9 @@ export type RuleConfig = { } export type LintConfig = { - ignores: { path.pathlike }?, -- globs/paths to ignore as we walk lintee files + ignores: { path.Pathlike }?, -- globs/paths to ignore as we walk lintee files globals: types.GlobalsConfig?, -- globals to pass down to all rules via context - rulepaths: { path.pathlike }?, -- paths to local rules to use + rulepaths: { path.Pathlike }?, -- paths to local rules to use ruleconfigs: { [string]: RuleConfig?, }?, diff --git a/lute/cli/commands/lint/lsp/init.luau b/lute/cli/commands/lint/lsp/init.luau index 8b9b7ec43..35709613a 100644 --- a/lute/cli/commands/lint/lsp/init.luau +++ b/lute/cli/commands/lint/lsp/init.luau @@ -58,7 +58,7 @@ function lsp.diagnostic(lint: lintTypes.LintViolation): lspTypes.Diagnostic end local function workspaceDocumentDiagnosticReport( - path: pathLib.path, + path: pathLib.Path, violations: { lintTypes.LintViolation } ): lspTypes.WorkspaceDocumentDiagnosticReport local report = { items = {}, uri = pathLib.format(path) } @@ -69,7 +69,7 @@ local function workspaceDocumentDiagnosticReport( end function lsp.workspaceDiagnosticReport( - violations: { [pathLib.path]: { lintTypes.LintViolation } } + violations: { [pathLib.Path]: { lintTypes.LintViolation } } ): lspTypes.WorkspaceDiagnosticReport local report = { items = {}, diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau index c6f492faf..3571a228b 100644 --- a/lute/cli/commands/lint/printer.luau +++ b/lute/cli/commands/lint/printer.luau @@ -93,7 +93,7 @@ function printerLib.printLintsWithSource(lints: { types.LintViolation }, sourceC end end -function printerLib.printLintsWithPath(lints: { types.LintViolation }, sourcePath: path.path): () +function printerLib.printLintsWithPath(lints: { types.LintViolation }, sourcePath: path.Path): () printerLib.printLintsWithSource(lints, fs.readfiletostring(sourcePath)) end diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index c7eaa4745..f27f13cf9 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -62,7 +62,7 @@ end -- a = b; b = a local function lint( ast: syntax.AstStatBlock, - sourcepath: path.path?, + sourcepath: path.Path?, _context: lintTypes.RuleContext ): { lintTypes.LintViolation } local violations = {} diff --git a/lute/cli/commands/lint/rules/constant_table_comparison.luau b/lute/cli/commands/lint/rules/constant_table_comparison.luau index 9d85519a3..a57dfca38 100644 --- a/lute/cli/commands/lint/rules/constant_table_comparison.luau +++ b/lute/cli/commands/lint/rules/constant_table_comparison.luau @@ -31,7 +31,7 @@ end local function lint( ast: syntax.AstStatBlock, - sourcepath: path.path?, + sourcepath: path.Path?, _context: lintTypes.RuleContext ): { lintTypes.LintViolation } return query diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau index 6d581cd0d..52a2d2aa7 100644 --- a/lute/cli/commands/lint/rules/divide_by_zero.luau +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -14,7 +14,7 @@ end local function lint( ast: syntax.AstStatBlock, - sourcepath: path.path?, + sourcepath: path.Path?, _context: lintTypes.RuleContext ): { lintTypes.LintViolation } return query diff --git a/lute/cli/commands/lint/rules/duplicate_keys.luau b/lute/cli/commands/lint/rules/duplicate_keys.luau index b48cd7299..a83f43ea7 100644 --- a/lute/cli/commands/lint/rules/duplicate_keys.luau +++ b/lute/cli/commands/lint/rules/duplicate_keys.luau @@ -10,7 +10,7 @@ local target = "https://lute.luau.org/cli/lint/duplicate_keys.html" local function lint( ast: syntax.AstStatBlock, - sourcepath: path.path?, + sourcepath: path.Path?, _context: lintTypes.RuleContext ): { lintTypes.LintViolation } local function makeViolation(location: syntax.Span): lintTypes.LintViolation diff --git a/lute/cli/commands/lint/rules/empty_if_block.luau b/lute/cli/commands/lint/rules/empty_if_block.luau index 2c893fd04..99d7dd2d0 100644 --- a/lute/cli/commands/lint/rules/empty_if_block.luau +++ b/lute/cli/commands/lint/rules/empty_if_block.luau @@ -43,7 +43,7 @@ end local function lint( ast: syntax.AstStatBlock, - sourcepath: path.path?, + sourcepath: path.Path?, context: lintTypes.RuleContext ): { lintTypes.LintViolation } -- Get the comments_count option from the rule configuration diff --git a/lute/cli/commands/lint/rules/global_function_in_scope.luau b/lute/cli/commands/lint/rules/global_function_in_scope.luau index be61c7019..5fc32398b 100644 --- a/lute/cli/commands/lint/rules/global_function_in_scope.luau +++ b/lute/cli/commands/lint/rules/global_function_in_scope.luau @@ -10,7 +10,7 @@ local target = "https://lute.luau.org/cli/lint/global_function_in_scope.html" local function lint( ast: syntax.AstStatBlock, - sourcepath: path.path?, + sourcepath: path.Path?, _context: lintTypes.RuleContext ): { lintTypes.LintViolation } local violations: { lintTypes.LintViolation } = {} diff --git a/lute/cli/commands/lint/rules/parenthesized_conditions.luau b/lute/cli/commands/lint/rules/parenthesized_conditions.luau index 2ea279e41..d1322b437 100644 --- a/lute/cli/commands/lint/rules/parenthesized_conditions.luau +++ b/lute/cli/commands/lint/rules/parenthesized_conditions.luau @@ -10,7 +10,7 @@ local target = "https://lute.luau.org/cli/lint/parenthesized_conditions.html" local function lint( ast: syntax.AstStatBlock, - sourcepath: path.path?, + sourcepath: path.Path?, _context: lintTypes.RuleContext ): { lintTypes.LintViolation } local violations: { lintTypes.LintViolation } = {} diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index 6c16e3511..22c9e900e 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -370,7 +370,7 @@ end local function lint( ast: syntax.AstStatBlock, - sourcepath: path.path?, + sourcepath: path.Path?, context: lintTypes.RuleContext ): { lintTypes.LintViolation } -- Validate context.options, which should be of form { ["api"] = { number } }, diff --git a/lute/cli/commands/lint/types.luau b/lute/cli/commands/lint/types.luau index ad279cb99..9da3536e0 100644 --- a/lute/cli/commands/lint/types.luau +++ b/lute/cli/commands/lint/types.luau @@ -9,7 +9,7 @@ export type LintViolation = { location: syntax.Span, message: string, severity: severity, - sourcepath: path.path?, + sourcepath: path.Path?, suggestedfix: { read location: syntax.Span?, -- if nil, applies to whole violation location read fix: string, @@ -27,7 +27,7 @@ export type RuleContext = { } export type LintRule = { - read lint: (ast: syntax.AstStatBlock, sourcepath: path.path?, context: RuleContext) -> { LintViolation }, + read lint: (ast: syntax.AstStatBlock, sourcepath: path.Path?, context: RuleContext) -> { LintViolation }, read name: string, } diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index 52a23dad4..27a594c2b 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -11,9 +11,9 @@ local function issnapfile(filename: string): boolean return stringext.hassuffix(filename, ".snap.luau") end -local function findfiles(paths: { string }, predicate: (string) -> boolean): { path.path } +local function findfiles(paths: { string }, predicate: (string) -> boolean): { path.Path } local files = {} - local function walk(p: path.pathlike) + local function walk(p: path.Pathlike) local it = fs.walk(path.normalize(p), { recursive = true }) local file = it() while file do @@ -36,11 +36,11 @@ local function findfiles(paths: { string }, predicate: (string) -> boolean): { p return files end -local function findtestfiles(paths: { string }): { path.path } +local function findtestfiles(paths: { string }): { path.Path } return findfiles(paths, istestfile) end -local function findsnapfiles(paths: { string }): { path.path } +local function findsnapfiles(paths: { string }): { path.Path } return findfiles(paths, issnapfile) end diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index 1be60ca4a..6d8ba850e 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -11,7 +11,7 @@ local test = require("@std/test") local snaprunner = require("@self/snap/runner") local updater = require("@self/snap/updater") -local function loadtests(testfiles: { path.path }) +local function loadtests(testfiles: { path.Path }) -- Load all test files first for _, p in testfiles do local success, err = pcall(luau.loadModule, p, nil) diff --git a/lute/cli/commands/test/snap/runner.luau b/lute/cli/commands/test/snap/runner.luau index c7be400e3..7ff598b99 100644 --- a/lute/cli/commands/test/snap/runner.luau +++ b/lute/cli/commands/test/snap/runner.luau @@ -7,7 +7,7 @@ local snapty = require("./types") -- Derive the .snap artifact path from a .snap.luau file path. -- e.g. /foo/bar.snap.luau -> /foo/bar.snap -local function artifactpath(filepath: path.path): string +local function artifactpath(filepath: path.Path): string local f = path.format(filepath) assert(strext.hassuffix(f, ".snap.luau"), `expected {f} to have suffix .snap.luau`) return strext.removesuffix(f, ".luau") -- strips ".luau" @@ -26,7 +26,7 @@ end local runner = {} -function runner.runsnapshots(files: { path.path }): snapty.snapresult +function runner.runsnapshots(files: { path.Path }): snapty.snapresult local result: snapty.snapresult = { passed = {}, failed = {}, diff --git a/lute/cli/commands/test/snap/types.luau b/lute/cli/commands/test/snap/types.luau index 87eec5f62..c9367d2a7 100644 --- a/lute/cli/commands/test/snap/types.luau +++ b/lute/cli/commands/test/snap/types.luau @@ -1,12 +1,12 @@ local path = require("@std/path") export type snappass = { - filepath: path.path, + filepath: path.Path, artifactpath: string, } export type snapfail = { - filepath: path.path, + filepath: path.Path, artifactpath: string, actual: string, expected: string, @@ -15,7 +15,7 @@ export type snapfail = { } export type snapnew = { - filepath: path.path, + filepath: path.Path, artifactpath: string, actual: string, } diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index 7bfcf7f51..554a2401a 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -79,7 +79,7 @@ end local function applyMigration( migration: types.Migration, - paths: { pathLib.path }, + paths: { pathLib.Path }, options: { [string]: string | boolean }, dryRun: boolean, outputPath: string? diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index fca7f3d66..ee4ccec2f 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -19,9 +19,9 @@ export type DirectoryEntry = fs.DirectoryEntry export type WatchHandle = fs.WatchHandle export type WatchEvent = fs.WatchEvent -type pathlike = pathlib.pathlike -type path = pathlib.path -type win32path = win32paths.path +type Pathlike = pathlib.Pathlike +type Path = pathlib.Path +type win32path = win32paths.Path export type CreateDirectoryOptions = { makeparents: boolean?, @@ -40,7 +40,7 @@ type Watcher = { close: (self: Watcher) -> (), } -function fslib.open(path: pathlike, mode: HandleMode?): File +function fslib.open(path: Pathlike, mode: HandleMode?): File return fs.open(pathlib.format(path), mode) end @@ -56,35 +56,35 @@ function fslib.close(file: File): () return fs.close(file) end -function fslib.remove(path: pathlike): () +function fslib.remove(path: Pathlike): () return fs.remove(pathlib.format(path)) end -function fslib.metadata(path: pathlike): FileMetadata +function fslib.metadata(path: Pathlike): FileMetadata return fs.stat(pathlib.format(path)) end -function fslib.type(path: pathlike): FileType +function fslib.type(path: Pathlike): FileType return fs.type(pathlib.format(path)) end -function fslib.link(src: pathlike, dest: pathlike): () +function fslib.link(src: Pathlike, dest: Pathlike): () return fs.link(pathlib.format(src), pathlib.format(dest)) end -function fslib.symboliclink(src: pathlike, dest: pathlike): () +function fslib.symboliclink(src: Pathlike, dest: Pathlike): () return fs.symlink(pathlib.format(src), pathlib.format(dest)) end --- Iterator function that yields filename and event pairs when changes occur in the watched path. --- Also provides a `close` method to stop watching. --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.watch as `for _ in fs.watch(...) do` directly. A while loop can be used instead. See example/watch_directory.luau for usage. -function fslib.watch(path: pathlike): Watcher +function fslib.watch(path: Pathlike): Watcher -- In the future this could be written with explicit instantiation syntax -- as in: -- -- deque.new<<{ filename: path, event: watchevent }>>() - local deq = deque.new(nil :: { filename: path, event: WatchEvent }?) + local deq = deque.new(nil :: { filename: Path, event: WatchEvent }?) local handle = fs.watch(pathlib.format(path), function(filename, event) deq:pushback({ filename = pathlib.parse(filename), event = event }) end) @@ -106,37 +106,37 @@ function fslib.watch(path: pathlike): Watcher } end -function fslib.exists(path: pathlike): boolean +function fslib.exists(path: Pathlike): boolean return fs.exists(pathlib.format(path)) end -function fslib.copy(src: pathlike, dest: pathlike): () +function fslib.copy(src: Pathlike, dest: Pathlike): () return fs.copy(pathlib.format(src), pathlib.format(dest)) end -function fslib.listdirectory(path: pathlike): { DirectoryEntry } +function fslib.listdirectory(path: Pathlike): { DirectoryEntry } return fs.listdir(pathlib.format(path)) end -function fslib.readfiletostring(filepath: pathlike): string +function fslib.readfiletostring(filepath: Pathlike): string local f = fs.open(pathlib.format(filepath), "r") local contents = fs.read(f) fs.close(f) return contents end -function fslib.writestringtofile(filepath: pathlike, contents: string): () +function fslib.writestringtofile(filepath: Pathlike, contents: string): () local f = fs.open(pathlib.format(filepath), "w+") fs.write(f, contents) fs.close(f) end -function fslib.createdirectory(path: pathlike, options: CreateDirectoryOptions?): () +function fslib.createdirectory(path: Pathlike, options: CreateDirectoryOptions?): () if options and options.makeparents then local parsed = pathlib.parse(path) local parts: { string } = parsed.parts - local subdir: pathlike = "." + local subdir: Pathlike = "." if pathlib.isabsolute(path) then if sys.win32 then -- Windows path - get the drive root like "C:\" @@ -158,7 +158,7 @@ function fslib.createdirectory(path: pathlike, options: CreateDirectoryOptions?) end end -function fslib.removedirectory(path: pathlike, options: RemoveDirectoryOptions?): () +function fslib.removedirectory(path: Pathlike, options: RemoveDirectoryOptions?): () if options and options.recursive then -- Recursively delete all contents first if fslib.exists(path) and fslib.type(path) == "dir" then @@ -179,8 +179,8 @@ function fslib.removedirectory(path: pathlike, options: RemoveDirectoryOptions?) end --- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. See example/walk_directory.luau for usage. -function fslib.walk(path: pathlike, options: WalkOptions?): () -> path? - local queue: { path } = { pathlib.parse(path) } +function fslib.walk(path: Pathlike, options: WalkOptions?): () -> Path? + local queue: { Path } = { pathlib.parse(path) } local recursive = if options then options.recursive else false return function() diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 38f860062..54690c1b6 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -20,7 +20,7 @@ function luau.load(bytecode: Bytecode, chunkname: string?, env: { [any]: any }?) return luteLuau.load(bytecode, if chunkname ~= nil then `@{chunkname}` else "=luau.load", env) end -function luau.loadModule(requirePath: path.pathlike, env: { [any]: any }?): any +function luau.loadModule(requirePath: path.Pathlike, env: { [any]: any }?): any local requirePathStr = if typeof(requirePath) == "string" then requirePath else path.format(requirePath) local migrationHandle = fs.open(requirePathStr, "r") @@ -34,13 +34,13 @@ end export type TypePack = luteLuau.TypePack -function luau.typeofmodule(modulepath: path.pathlike): TypePack? +function luau.typeofmodule(modulepath: path.Pathlike): TypePack? local modulePathStr = if typeof(modulepath) == "string" then modulepath else path.format(modulepath) return luteLuau.typeofmodule(modulePathStr) end -function luau.resolveModule(modulepath: string, frompath: path.pathlike): path.pathlike +function luau.resolveModule(modulepath: string, frompath: path.Pathlike): path.Pathlike local frompathString = if typeof(frompath) == "string" then frompath else path.format(frompath) return luteLuau.resolveModule(modulepath, `@{frompathString}`) end diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau index 78555415a..2be969c3e 100644 --- a/lute/std/libs/path/init.luau +++ b/lute/std/libs/path/init.luau @@ -7,52 +7,52 @@ local pathtypes = require("@self/types") local pathlib = {} -export type path = pathtypes.path -export type pathlike = pathtypes.pathlike +export type Path = pathtypes.Path +export type Pathlike = pathtypes.Pathlike local onWindows = platform.win32 -function pathlib.basename(path: pathlike): string? +function pathlib.basename(path: Pathlike): string? if onWindows then - return win32.basename(path :: win32.pathlike) + return win32.basename(path :: win32.Pathlike) else - return posix.basename(path :: posix.pathlike) + return posix.basename(path :: posix.Pathlike) end end -function pathlib.dirname(path: pathlike): string +function pathlib.dirname(path: Pathlike): string if onWindows then - return win32.dirname(path :: win32.pathlike) + return win32.dirname(path :: win32.Pathlike) else - return posix.dirname(path :: posix.pathlike) + return posix.dirname(path :: posix.Pathlike) end end -function pathlib.extname(path: pathlike): string +function pathlib.extname(path: Pathlike): string if onWindows then - return win32.extname(path :: win32.pathlike) + return win32.extname(path :: win32.Pathlike) else - return posix.extname(path :: posix.pathlike) + return posix.extname(path :: posix.Pathlike) end end -function pathlib.format(path: pathlike): string +function pathlib.format(path: Pathlike): string if onWindows then - return win32.format(path :: win32.pathlike) + return win32.format(path :: win32.Pathlike) else - return posix.format(path :: posix.pathlike) + return posix.format(path :: posix.Pathlike) end end -function pathlib.isabsolute(path: pathlike): boolean +function pathlib.isabsolute(path: Pathlike): boolean if onWindows then - return win32.isabsolute(path :: win32.pathlike) + return win32.isabsolute(path :: win32.Pathlike) else - return posix.isabsolute(path :: posix.pathlike) + return posix.isabsolute(path :: posix.Pathlike) end end -function pathlib.join(...: pathlike): path +function pathlib.join(...: Pathlike): Path if onWindows then return win32.join(...) else @@ -60,31 +60,31 @@ function pathlib.join(...: pathlike): path end end -function pathlib.normalize(path: pathlike): path +function pathlib.normalize(path: Pathlike): Path if onWindows then - return win32.normalize(path :: win32.pathlike) + return win32.normalize(path :: win32.Pathlike) else - return posix.normalize(path :: posix.pathlike) + return posix.normalize(path :: posix.Pathlike) end end -function pathlib.parse(path: pathlike): path +function pathlib.parse(path: Pathlike): Path if onWindows then - return win32.parse(path :: win32.pathlike) + return win32.parse(path :: win32.Pathlike) else - return posix.parse(path :: posix.pathlike) + return posix.parse(path :: posix.Pathlike) end end -function pathlib.relative(from: pathlike, to: pathlike): path +function pathlib.relative(from: Pathlike, to: Pathlike): Path if onWindows then - return win32.relative(from :: win32.pathlike, to :: win32.pathlike) + return win32.relative(from :: win32.Pathlike, to :: win32.Pathlike) else - return posix.relative(from :: posix.pathlike, to :: posix.pathlike) + return posix.relative(from :: posix.Pathlike, to :: posix.Pathlike) end end -function pathlib.resolve(...: pathlike): path +function pathlib.resolve(...: Pathlike): Path if onWindows then return win32.resolve(...) else diff --git a/lute/std/libs/path/pathinterface.luau b/lute/std/libs/path/pathinterface.luau index 430a1c71a..90401a9ad 100644 --- a/lute/std/libs/path/pathinterface.luau +++ b/lute/std/libs/path/pathinterface.luau @@ -1,5 +1,5 @@ -export type pathinterface = { - __tostring: (self: pathinterface) -> string, +export type PathInterface = { + __tostring: (self: PathInterface) -> string, } return table.freeze({}) diff --git a/lute/std/libs/path/posix/init.luau b/lute/std/libs/path/posix/init.luau index b1af1aa13..984554379 100644 --- a/lute/std/libs/path/posix/init.luau +++ b/lute/std/libs/path/posix/init.luau @@ -2,18 +2,18 @@ local process = require("@lute/process") local posixtypes = require("@self/types") local pathinterface = require("./pathinterface") -export type path = posixtypes.path -export type pathlike = posixtypes.pathlike +export type Path = posixtypes.Path +export type Pathlike = posixtypes.Pathlike local posix = {} -posix.pathmt = {} :: pathinterface.pathinterface +posix.pathmt = {} :: pathinterface.PathInterface -function posix.basename(path: pathlike): string? +function posix.basename(path: Pathlike): string? path = if typeof(path) == "string" then posix.parse(path) else path return if #path.parts > 0 then path.parts[#path.parts] else nil end -function posix.dirname(path: pathlike): string +function posix.dirname(path: Pathlike): string path = if typeof(path) == "string" then posix.parse(path) else path if #path.parts == 0 then @@ -26,7 +26,7 @@ function posix.dirname(path: pathlike): string end end -function posix.extname(path: pathlike): string +function posix.extname(path: Pathlike): string path = if typeof(path) == "string" then posix.parse(path) else path if #path.parts == 0 then @@ -47,7 +47,7 @@ function posix.extname(path: pathlike): string return filename:sub(dotIndex) end -function posix.format(path: pathlike): string +function posix.format(path: Pathlike): string if typeof(path) == "string" then return path end @@ -59,13 +59,13 @@ function posix.format(path: pathlike): string end end -function posix.isabsolute(path: pathlike): boolean +function posix.isabsolute(path: Pathlike): boolean path = if typeof(path) == "string" then posix.parse(path) else path return path.absolute end -- In place extends path with addend -local function joinHelper(path: path, addend: pathlike): () +local function joinHelper(path: Path, addend: Pathlike): () addend = if typeof(addend) == "string" then posix.parse(addend) else addend if addend.absolute then @@ -79,8 +79,8 @@ local function joinHelper(path: path, addend: pathlike): () table.move(addend.parts, 1, #addend.parts, #path.parts + 1, path.parts) end -function posix.join(...: pathlike): path - local parts: { pathlike } = { ... } +function posix.join(...: Pathlike): Path + local parts: { Pathlike } = { ... } if #parts == 0 then return setmetatable({ parts = {}, @@ -88,11 +88,11 @@ function posix.join(...: pathlike): path }, posix.pathmt) end - local path: path = if typeof(parts[1]) == "string" + local path: Path = if typeof(parts[1]) == "string" then posix.parse(parts[1]) else setmetatable({ - parts = table.clone((parts[1] :: path).parts), -- LUAUFIX: Shouldn't need cast - absolute = (parts[1] :: path).absolute, -- LUAUFIX: Shouldn't need cast + parts = table.clone((parts[1] :: Path).parts), -- LUAUFIX: Shouldn't need cast + absolute = (parts[1] :: Path).absolute, -- LUAUFIX: Shouldn't need cast }, posix.pathmt) for i = 2, #parts do @@ -102,7 +102,7 @@ function posix.join(...: pathlike): path return path end -function posix.normalize(path: pathlike): path +function posix.normalize(path: Pathlike): Path path = if typeof(path) == "string" then posix.parse(path) else path local newParts = {} @@ -132,7 +132,7 @@ function posix.normalize(path: pathlike): path }, posix.pathmt) end -function posix.parse(path: pathlike): path +function posix.parse(path: Pathlike): Path if typeof(path) == "table" then return setmetatable(path, posix.pathmt) end @@ -169,7 +169,7 @@ function posix.parse(path: pathlike): path }, posix.pathmt) end -function posix.resolve(...: pathlike): path +function posix.resolve(...: Pathlike): Path local parts = { ... } if #parts == 0 then return posix.parse(process.cwd()) @@ -201,7 +201,7 @@ function posix.resolve(...: pathlike): path return posix.normalize(path) end -function posix.relative(from: pathlike, to: pathlike): path +function posix.relative(from: Pathlike, to: Pathlike): Path from = if typeof(from) == "string" then posix.parse(from) else from to = if typeof(to) == "string" then posix.parse(to) else to @@ -239,7 +239,7 @@ function posix.relative(from: pathlike, to: pathlike): path end function posix.pathmt:__tostring(): string - return posix.format(self :: path) + return posix.format(self :: Path) end return table.freeze(posix) diff --git a/lute/std/libs/path/posix/types.luau b/lute/std/libs/path/posix/types.luau index f158038ac..8dba0868f 100644 --- a/lute/std/libs/path/posix/types.luau +++ b/lute/std/libs/path/posix/types.luau @@ -1,12 +1,12 @@ local pathinterface = require("../pathinterface") -export type pathdata = { +export type PathData = { parts: { string }, absolute: boolean, } -export type path = setmetatable +export type Path = setmetatable -export type pathlike = string | path | pathdata +export type Pathlike = string | Path | PathData return {} diff --git a/lute/std/libs/path/types.luau b/lute/std/libs/path/types.luau index 2c2bd392c..90cecfa16 100644 --- a/lute/std/libs/path/types.luau +++ b/lute/std/libs/path/types.luau @@ -1,7 +1,7 @@ local posixtypes = require("@std/path/posix/types") local win32types = require("@std/path/win32/types") -export type path = posixtypes.path | win32types.path -export type pathlike = posixtypes.pathlike | win32types.pathlike +export type Path = posixtypes.Path | win32types.Path +export type Pathlike = posixtypes.Pathlike | win32types.Pathlike return {} diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index 850cdd14d..9b368dee3 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -2,19 +2,19 @@ local process = require("@lute/process") local win32types = require("@self/types") local pathinterface = require("./pathinterface") -export type pathkind = win32types.pathkind -export type path = win32types.path -export type pathlike = win32types.pathlike +export type PathKind = win32types.PathKind +export type Path = win32types.Path +export type Pathlike = win32types.Pathlike local win32 = {} -win32.pathmt = {} :: pathinterface.pathinterface +win32.pathmt = {} :: pathinterface.PathInterface -function win32.basename(path: pathlike): string? +function win32.basename(path: Pathlike): string? path = if typeof(path) == "string" then win32.parse(path) else path return if #path.parts > 0 then path.parts[#path.parts] else nil end -function win32.dirname(path: pathlike): string +function win32.dirname(path: Pathlike): string path = if typeof(path) == "string" then win32.parse(path) else path if #path.parts == 0 then @@ -28,7 +28,7 @@ function win32.dirname(path: pathlike): string end end -function win32.extname(path: pathlike): string +function win32.extname(path: Pathlike): string path = if typeof(path) == "string" then win32.parse(path) else path if #path.parts == 0 then @@ -49,7 +49,7 @@ function win32.extname(path: pathlike): string return filename:sub(dotIndex) end -function win32.drive(path: pathlike): path +function win32.drive(path: Pathlike): Path path = if typeof(path) == "string" then win32.parse(path) else path if path.drive == nil then @@ -61,12 +61,12 @@ function win32.drive(path: pathlike): path parts = {}, kind = "absolute", drive = path.drive, - } :: win32types.pathdata, + } :: win32types.PathData, win32.pathmt ) end -function win32.format(path: pathlike): string +function win32.format(path: Pathlike): string if typeof(path) == "string" then return path end @@ -91,13 +91,13 @@ function win32.format(path: pathlike): string end end -function win32.isabsolute(path: pathlike): boolean +function win32.isabsolute(path: Pathlike): boolean path = if typeof(path) == "string" then win32.parse(path) else path return path.kind ~= "relative" end -- In place extends path with addend -local function joinHelper(path: path, addend: pathlike): () +local function joinHelper(path: Path, addend: Pathlike): () addend = if typeof(addend) == "string" then win32.parse(addend) else addend if addend.kind ~= "relative" then @@ -119,7 +119,7 @@ local function joinHelper(path: path, addend: pathlike): () end end -function win32.join(...: pathlike): path +function win32.join(...: Pathlike): Path local parts = { ... } if #parts == 0 then return setmetatable( @@ -127,17 +127,17 @@ function win32.join(...: pathlike): path parts = {}, kind = "relative", drive = nil, - } :: win32types.pathdata, -- Cast needed because of table invariance + } :: win32types.PathData, -- Cast needed because of table invariance win32.pathmt ) end - local path: path = if typeof(parts[1]) == "string" + local path: Path = if typeof(parts[1]) == "string" then win32.parse(parts[1]) else setmetatable({ - parts = table.clone((parts[1] :: path).parts), -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] - kind = (parts[1] :: path).kind, -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] - drive = (parts[1] :: path).drive, -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] + parts = table.clone((parts[1] :: Path).parts), -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] + kind = (parts[1] :: Path).kind, -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] + drive = (parts[1] :: Path).drive, -- LUAUFIX: Shouldn't need cast, refinement should remove string from possible types of parts[1] }, win32.pathmt) for i = 2, #parts do @@ -147,7 +147,7 @@ function win32.join(...: pathlike): path return path end -function win32.normalize(path: pathlike): path +function win32.normalize(path: Pathlike): Path path = if typeof(path) == "string" then win32.parse(path) else path local newParts = {} @@ -178,7 +178,7 @@ function win32.normalize(path: pathlike): path }, win32.pathmt) end -function win32.parse(path: pathlike): path +function win32.parse(path: Pathlike): Path if typeof(path) == "table" then return setmetatable(path, win32.pathmt) end @@ -186,7 +186,7 @@ function win32.parse(path: pathlike): path error("Expected string or path") end - local kind: pathkind = "relative" + local kind: PathKind = "relative" local driveLetter = nil -- Check if path is absolute and if it's a unc path on Windows @@ -234,7 +234,7 @@ function win32.parse(path: pathlike): path }, win32.pathmt) end -function win32.relative(from: pathlike, to: pathlike): path +function win32.relative(from: Pathlike, to: Pathlike): Path from = if typeof(from) == "string" then win32.parse(from) else from to = if typeof(to) == "string" then win32.parse(to) else to @@ -276,7 +276,7 @@ function win32.relative(from: pathlike, to: pathlike): path }, win32.pathmt) end -function win32.resolve(...: pathlike): path +function win32.resolve(...: Pathlike): Path local parts = { ... } if #parts == 0 then return win32.parse(process.cwd()) @@ -309,7 +309,7 @@ function win32.resolve(...: pathlike): path end function win32.pathmt:__tostring(): string - return win32.format(self :: path) + return win32.format(self :: Path) end return table.freeze(win32) diff --git a/lute/std/libs/path/win32/types.luau b/lute/std/libs/path/win32/types.luau index b07c6967b..6aa5ce845 100644 --- a/lute/std/libs/path/win32/types.luau +++ b/lute/std/libs/path/win32/types.luau @@ -1,15 +1,15 @@ local pathinterface = require("../pathinterface") -export type pathkind = "unc" | "absolute" | "relative" +export type PathKind = "unc" | "absolute" | "relative" -export type pathdata = { +export type PathData = { parts: { string }, - kind: pathkind, + kind: PathKind, drive: string?, } -export type path = setmetatable +export type Path = setmetatable -export type pathlike = string | pathdata | path +export type Pathlike = string | PathData | Path return {} diff --git a/lute/std/libs/process.luau b/lute/std/libs/process.luau index 66b8a0750..5ecc0d8e6 100644 --- a/lute/std/libs/process.luau +++ b/lute/std/libs/process.luau @@ -12,14 +12,14 @@ export type StdioKind = process.StdioKind export type ProcessRunOptions = process.ProcessRunOptions export type ProcessSystemOptions = process.ProcessSystemOptions export type ProcessResult = process.ProcessResult -export type path = pathlib.path -export type pathlike = pathlib.pathlike +export type Path = pathlib.Path +export type Pathlike = pathlib.Pathlike -function processlib.homedir(): path +function processlib.homedir(): Path return pathlib.parse(process.homedir()) end -function processlib.cwd(): path +function processlib.cwd(): Path return pathlib.parse(process.cwd()) end @@ -35,7 +35,7 @@ function processlib.exit(exitcode: number): never return process.exit(exitcode) end -function processlib.execpath(): path +function processlib.execpath(): Path return pathlib.parse(process.execpath()) end diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 50e3d071a..7967fe738 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -8,20 +8,20 @@ local visitor = require("./visitor") type Node = types.AstNode -export type query = { +export type Query = { nodes: { T }, - filter: (self: query, pred: (T) -> boolean) -> query, - replace: (self: query, repl: (T) -> string?) -> types.Replacements, - map: (self: query, fn: (T) -> U?) -> query, -- recursion violation reported - foreach: (self: query, callback: (T) -> ()) -> query, - findall: (self: query, fn: (Node) -> U?) -> query, - flatmap: (self: query, fn: (T) -> { U }) -> query, - maptoarray: (self: query, fn: (T) -> U?) -> { U }, + filter: (self: Query, pred: (T) -> boolean) -> Query, + replace: (self: Query, repl: (T) -> string?) -> types.Replacements, + map: (self: Query, fn: (T) -> U?) -> Query, -- recursion violation reported + foreach: (self: Query, callback: (T) -> ()) -> Query, + findall: (self: Query, fn: (Node) -> U?) -> Query, + flatmap: (self: Query, fn: (T) -> { U }) -> Query, + maptoarray: (self: Query, fn: (T) -> U?) -> { U }, } local queryLib = {} -function queryLib.filter(self: query, pred: (T) -> boolean): query +function queryLib.filter(self: Query, pred: (T) -> boolean): Query local newNodes = {} for _, node in self.nodes do if pred(node) then @@ -32,7 +32,7 @@ function queryLib.filter(self: query, pred: (T) -> boolean): query return self end -function queryLib.replace(self: query, repl: (T) -> string?): types.Replacements +function queryLib.replace(self: Query, repl: (T) -> string?): types.Replacements local replacements = {} for _, node in self.nodes do local r = repl(node) @@ -44,7 +44,7 @@ function queryLib.replace(self: query, repl: (T) -> string?): types.Replac end -- map will return a query where its nodes are all the non-nil values returned by fn when called on each node of the original query -function queryLib.map(self: query, fn: (T) -> U?): query +function queryLib.map(self: Query, fn: (T) -> U?): Query local newNodes = {} for _, node in self.nodes do local mapped = fn(node) @@ -56,7 +56,7 @@ function queryLib.map(self: query, fn: (T) -> U?): query return self end -function queryLib.foreach(self: query, callback: (T) -> ()): query +function queryLib.foreach(self: Query, callback: (T) -> ()): Query for _, node in self.nodes do callback(node) end @@ -83,7 +83,7 @@ local function newSelectVisitor(nodes: { T }, fn: (Node) -> T?): visitor.Visi return selectVisitor end -function queryLib.findall(self: query, fn: (Node) -> U?): query +function queryLib.findall(self: Query, fn: (Node) -> U?): Query local selectedNodes: { U } = {} local selectVisitor = newSelectVisitor(selectedNodes, fn) @@ -97,7 +97,7 @@ function queryLib.findall(self: query, fn: (Node) -> U?): query return self end -function queryLib.flatmap(self: query, fn: (T) -> { U }): query +function queryLib.flatmap(self: Query, fn: (T) -> { U }): Query local newNodes: { U } = {} for _, node in self.nodes do local mapped = fn(node) @@ -109,7 +109,7 @@ function queryLib.flatmap(self: query, fn: (T) -> { U }): query return self end -function queryLib.maptoarray(self: query, fn: (T) -> U?): { U } +function queryLib.maptoarray(self: Query, fn: (T) -> U?): { U } local result = {} for _, node in self.nodes do local mapped = fn(node) @@ -120,7 +120,7 @@ function queryLib.maptoarray(self: query, fn: (T) -> U?): { U } return result end -function queryLib.findallfromroot(ast: types.ParseResult | Node, fn: (Node) -> T?): query +function queryLib.findallfromroot(ast: types.ParseResult | Node, fn: (Node) -> T?): Query local nodes: { T } = {} local selectVisitor = newSelectVisitor(nodes, fn) diff --git a/lute/std/libs/system/init.luau b/lute/std/libs/system/init.luau index ae9e210c0..4c14c2530 100644 --- a/lute/std/libs/system/init.luau +++ b/lute/std/libs/system/init.luau @@ -9,7 +9,7 @@ local platform = require("@self/platform") local systemlib = {} -function systemlib.tmpdir(): path.path +function systemlib.tmpdir(): path.Path return path.parse(system.tmpdir()) end diff --git a/tests/std/path/path.posix.test.luau b/tests/std/path/path.posix.test.luau index a8f96d3ae..a7b14a414 100644 --- a/tests/std/path/path.posix.test.luau +++ b/tests/std/path/path.posix.test.luau @@ -43,7 +43,7 @@ test.suite("PathPosixParseSuite", function(suite) end) suite:case("parsePathobjReturnsSame", function(assert) - local originalPath: posix.path = setmetatable({ + local originalPath: posix.Path = setmetatable({ parts = { "folder", "file.txt" }, absolute = false, }, posix.pathmt) @@ -84,7 +84,7 @@ test.suite("PathPosixBasenameSuite", function(suite) end) suite:case("basenamePathobj", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, absolute = false, }, posix.pathmt) @@ -95,7 +95,7 @@ end) test.suite("PathPosixFormatSuite", function(suite) suite:case("formatPosixAbsolutePath", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = { "home", "user", "documents", "file.txt" }, absolute = true, }, posix.pathmt) @@ -104,7 +104,7 @@ test.suite("PathPosixFormatSuite", function(suite) end) suite:case("formatPosixRelativePath", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = { "documents", "file.txt" }, absolute = false, }, posix.pathmt) @@ -113,7 +113,7 @@ test.suite("PathPosixFormatSuite", function(suite) end) suite:case("formatEmptyRelativePath", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = {}, absolute = false, }, posix.pathmt) @@ -122,7 +122,7 @@ test.suite("PathPosixFormatSuite", function(suite) end) suite:case("formatRootPath", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = {}, absolute = true, }, posix.pathmt) @@ -168,7 +168,7 @@ test.suite("PathPosixDirnameSuite", function(suite) end) suite:case("dirnamePathobj", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, absolute = false, }, posix.pathmt) @@ -219,7 +219,7 @@ test.suite("PathPosixExtnameSuite", function(suite) end) suite:case("extnamePathobj", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = { "folder", "file.pdf" }, absolute = false, }, posix.pathmt) @@ -265,7 +265,7 @@ test.suite("PathPosixIsAbsoluteSuite", function(suite) end) suite:case("absolutePathobjAbsolute", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = { "home", "user", "file.txt" }, absolute = true, }, posix.pathmt) @@ -274,7 +274,7 @@ test.suite("PathPosixIsAbsoluteSuite", function(suite) end) suite:case("absolutePathobjRelative", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = { "folder", "file.txt" }, absolute = false, }, posix.pathmt) @@ -346,7 +346,7 @@ test.suite("PathPosixNormalizeSuite", function(suite) end) suite:case("normalizePathobj", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = { "home", ".", "user", "..", "documents" }, absolute = true, }, posix.pathmt) @@ -443,7 +443,7 @@ test.suite("PathPosixJoinSuite", function(suite) end) suite:case("joinPathobjAndString", function(assert) - local pathobj: posix.path = setmetatable({ + local pathobj: posix.Path = setmetatable({ parts = { "home", "user" }, absolute = true, }, posix.pathmt) @@ -538,14 +538,14 @@ end) test.suite("PathPosixToStringSuite", function(suite) suite:case("toStringPosixAbsolutePath", function(assert) local filePath = "/home/user/documents/file.txt" - local pathobj: posix.path = posix.parse(filePath) + local pathobj: posix.Path = posix.parse(filePath) local result = tostring(pathobj) assert.eq(result, filePath) end) suite:case("toStringPosixRelativePath", function(assert) local filePath = "documents/file.txt" - local pathobj: posix.path = posix.parse(filePath) + local pathobj: posix.Path = posix.parse(filePath) local result = tostring(pathobj) assert.eq(result, filePath) end) diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index 0030cc0fe..624357727 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -7,7 +7,7 @@ local win32 = require("@std/path/win32") test.suite("PathWin32ParseSuite", function(suite) suite:case("parseWindowsAbsolutePath", function(assert) - local result: win32.path = path.win32.parse("C:\\Users\\username\\Documents\\file.txt") + local result: win32.Path = path.win32.parse("C:\\Users\\username\\Documents\\file.txt") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") assert.eq(#result.parts, 4) @@ -18,7 +18,7 @@ test.suite("PathWin32ParseSuite", function(suite) end) suite:case("parseWindowsUncPath", function(assert) - local result: win32.path = path.win32.parse("\\\\server\\share\\folder\\file.txt") + local result: win32.Path = path.win32.parse("\\\\server\\share\\folder\\file.txt") assert.eq(result.kind, "unc") assert.eq(result.drive, nil) assert.eq(#result.parts, 4) @@ -29,14 +29,14 @@ test.suite("PathWin32ParseSuite", function(suite) end) suite:case("parseEmptyString", function(assert) - local result: win32.path = path.win32.parse("") + local result: win32.Path = path.win32.parse("") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) assert.eq(#result.parts, 0) end) suite:case("parseSingleFile", function(assert) - local result: win32.path = path.win32.parse("file.txt") + local result: win32.Path = path.win32.parse("file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) assert.eq(#result.parts, 1) @@ -44,7 +44,7 @@ test.suite("PathWin32ParseSuite", function(suite) end) suite:case("parsePathobjReturnsSame", function(assert) - local originalPath: win32.path = setmetatable({ + local originalPath: win32.Path = setmetatable({ parts = { "folder", "file.txt" }, kind = "relative", drive = nil :: string?, @@ -54,7 +54,7 @@ test.suite("PathWin32ParseSuite", function(suite) end) suite:case("parseMixedSeparators", function(assert) - local result: win32.path = path.win32.parse("/home/user\\documents/file.txt") + local result: win32.Path = path.win32.parse("/home/user\\documents/file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, nil) assert.eq(#result.parts, 4) @@ -65,7 +65,7 @@ test.suite("PathWin32ParseSuite", function(suite) end) suite:case("parseWindowsRelativeWithDrive", function(assert) - local result: win32.path = path.win32.parse("C:Documents\\file.txt") + local result: win32.Path = path.win32.parse("C:Documents\\file.txt") assert.eq(result.kind, "relative") assert.eq(result.drive, "C") assert.eq(#result.parts, 2) @@ -106,7 +106,7 @@ test.suite("PathWin32BasenameSuite", function(suite) end) suite:case("basenamePathobj", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, kind = "relative", drive = nil :: string?, @@ -123,7 +123,7 @@ end) test.suite("PathWin32FormatSuite", function(suite) suite:case("formatWindowsAbsolutePath", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "Users", "username", "Documents", "file.txt" }, kind = "absolute", drive = "C" :: string?, @@ -133,7 +133,7 @@ test.suite("PathWin32FormatSuite", function(suite) end) suite:case("formatWindowsRelativeWithDrive", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "Documents", "file.txt" }, kind = "relative", drive = "C" :: string?, @@ -143,7 +143,7 @@ test.suite("PathWin32FormatSuite", function(suite) end) suite:case("formatWindowsUncPath", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "server", "share", "folder", "file.txt" }, kind = "unc", drive = nil :: string?, @@ -153,7 +153,7 @@ test.suite("PathWin32FormatSuite", function(suite) end) suite:case("formatEmptyRelativePath", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = {}, kind = "relative", drive = nil :: string?, @@ -163,7 +163,7 @@ test.suite("PathWin32FormatSuite", function(suite) end) suite:case("formatEmptyRelativePathWithDrive", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = {}, kind = "relative", drive = "C" :: string?, @@ -173,7 +173,7 @@ test.suite("PathWin32FormatSuite", function(suite) end) suite:case("formatRootPathWithDrive", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = {}, kind = "absolute", drive = "C" :: string?, @@ -215,7 +215,7 @@ test.suite("PathWin32DirnameSuite", function(suite) end) suite:case("dirnamePathobj", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "folder", "subfolder", "file.txt" }, kind = "relative", drive = nil :: string?, @@ -277,7 +277,7 @@ test.suite("PathWin32ExtnameSuite", function(suite) end) suite:case("extnamePathobj", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "folder", "file.pdf" }, kind = "relative", drive = nil :: string?, @@ -318,7 +318,7 @@ test.suite("PathWin32GetDriveSuite", function(suite) end) suite:case("getdrivePathObject", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "Users", "username", "Documents" }, kind = "absolute", drive = "E" :: string?, @@ -375,7 +375,7 @@ test.suite("PathWin32IsAbsoluteSuite", function(suite) end) suite:case("isAbsolutePathobjAbsolute", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "home", "user", "file.txt" }, kind = "absolute", drive = "C" :: string?, @@ -385,7 +385,7 @@ test.suite("PathWin32IsAbsoluteSuite", function(suite) end) suite:case("isAbsolutePathobjRelative", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "folder", "file.txt" }, kind = "relative", drive = nil :: string?, @@ -507,7 +507,7 @@ test.suite("PathWin32NormalizeSuite", function(suite) end) suite:case("normalizePathobj", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "Users", ".", "username", "..", "Documents" }, kind = "absolute", drive = "C" :: string?, @@ -582,7 +582,7 @@ test.suite("PathWin32JoinSuite", function(suite) end) suite:case("joinPathobjAndString", function(assert) - local pathobj: win32.path = setmetatable({ + local pathobj: win32.Path = setmetatable({ parts = { "Users", "username" }, kind = "absolute", drive = "C" :: string?, @@ -698,7 +698,7 @@ test.suite("PathWin32ResolveSuite", function(suite) suite:case("resolveDriveRelativePath", function(assert) -- Get the drive letter from the current working directory - local cwdPath = path.win32.parse(process.cwd() :: win32.pathlike) + local cwdPath = path.win32.parse(process.cwd() :: win32.Pathlike) local drive = if system.win32 then path.win32.drive(cwdPath).drive :: string else "C" local testPath = drive .. ":Documents\\file.txt" @@ -735,14 +735,14 @@ end) test.suite("PathWin32ToStringSuite", function(suite) suite:case("toStringWindowsAbsolutePath", function(assert) local filePath = "C:\\Users\\username\\Documents\\file.txt" - local pathObj: win32.path = win32.parse(filePath) + local pathObj: win32.Path = win32.parse(filePath) local result = tostring(pathObj) assert.eq(result, filePath) end) suite:case("toStringWindowsRelativePath", function(assert) local filePath = "C:\\Users\\username\\Documents\\file.txt" - local pathObj: win32.path = path.win32.parse(filePath) + local pathObj: win32.Path = path.win32.parse(filePath) local result = tostring(pathObj) assert.eq(result, filePath) end) @@ -818,7 +818,7 @@ test.suite("PathWin32RelativeSuite", function(suite) end) suite:case("parseWindowsAbsolutePathWithForwardSlashes", function(assert) - local result: win32.path = path.win32.parse("C:/Users/username/Documents/file.txt") + local result: win32.Path = path.win32.parse("C:/Users/username/Documents/file.txt") assert.eq(result.kind, "absolute") assert.eq(result.drive, "C") assert.eq(#result.parts, 4) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 3acb9d397..976551dff 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -32,10 +32,10 @@ test.suite("ProcessSuite", function(suite) suite:case("runSystemAndCwd", function(check) local rootdir: string = "/" - local root: pathlib.path? = nil + local root: pathlib.Path? = nil if system.win32 then - root = pathlib.win32.drive(process.cwd() :: windowsPath.path) + root = pathlib.win32.drive(process.cwd() :: windowsPath.Path) rootdir = pathlib.format(root) end @@ -43,7 +43,7 @@ test.suite("ProcessSuite", function(suite) local r = process.run({ "pwd" }, { cwd = rootdir }) :: { [any]: any } if system.win32 then - assert(root ~= nil and (root :: windowsPath.path).drive ~= nil) + assert(root ~= nil and (root :: windowsPath.Path).drive ~= nil) check.tableeq(r, { exitcode = 0, stdout = `/{root.drive:lower()}\n`, diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index 33bba0bfd..bef339e36 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -74,7 +74,7 @@ test.suite("AstQuery", function(suite) suite:case("findallNums", function(assert) local ast = syntax.parse("print(1 + 2)") - local queryResult: query.query = query.findallfromroot( + local queryResult: query.Query = query.findallfromroot( ast, -- LUAUFIX: Complains that AstStatBlock doesn't have location? field function(n: syntax.AstNode): syntax.AstExprConstantNumber? if n.kind == "expr" and n.tag == "number" then @@ -97,7 +97,7 @@ test.suite("AstQuery", function(suite) suite:case("findallUtils", function(assert) local ast = syntax.parse("print(1 + 2)") - local queryResult: query.query = + local queryResult: query.Query = query.findallfromroot(ast, utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field assert.eq(#queryResult.nodes, 2) diff --git a/tools/check.luau b/tools/check.luau index 336961aa5..a02fd772c 100644 --- a/tools/check.luau +++ b/tools/check.luau @@ -23,7 +23,7 @@ local EXCLUDE_TYPECHECK = { "tests/std/snapshots", } -local function posixify(p: path.pathlike): string +local function posixify(p: path.Pathlike): string local normalized = path.normalize(p) local posixy, _ = string.gsub(path.format(normalized), "\\", "/") return posixy From 4798710fb096da97259c389a286b41f91e97b9ad Mon Sep 17 00:00:00 2001 From: Andrey Zhabsky Date: Wed, 25 Mar 2026 20:46:05 +0200 Subject: [PATCH 416/642] Adds `utils.isRequireCall` (#887) Right now, users have to write something like this to get `require` calls([example](https://github.com/luau-lang/lute/blob/e0faf05ee8d274be25c9e686e95c88af42d9589c/examples/query.luau#L7)): ```luau local requireCalls = query .findallfromroot(ast, utils.isExprCall) :filter(function(call) local calledFunc = call.func return calledFunc.tag == "global" and calledFunc.name.text == "require" end) ``` writing their own `helpers`([example](https://github.com/luau-lang/lute/blob/e0faf05ee8d274be25c9e686e95c88af42d9589c/lute/cli/commands/lint/rules/unused_variable.luau#L19)): ```luau local function isRequireCall(expr: syntax.AstExpr) return expr.tag == "call" and expr.func.tag == "global" and expr.func.name.text == "require" end ``` It would be nice if `utils` had `utils.isRequireCall`. Then it would be simplified to: ```luau local requireCalls = query.findallfromroot(ast, utils.isRequireCall) ``` Thanks to @Vighnesh-V for [highlighting](https://github.com/luau-lang/lute/pull/878/changes#r2934146238) the issue and @skberkeley for the `API` [suggestion](https://github.com/luau-lang/lute/pull/878/changes#r2934155678). --------- Co-authored-by: Vighnesh Vijay --- examples/query.luau | 14 ++++---------- .../commands/lint/rules/unused_variable.luau | 7 ++----- lute/std/libs/syntax/utils/init.luau | 6 ++++++ tests/std/syntax/utils.test.luau | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 tests/std/syntax/utils.test.luau diff --git a/examples/query.luau b/examples/query.luau index e823fc9b6..8ca193b8e 100644 --- a/examples/query.luau +++ b/examples/query.luau @@ -4,16 +4,10 @@ local utils = require("@std/syntax/utils") local ast = syntax.parseexpr("require(OldComponent)") -local p = query - .findallfromroot(ast, utils.isExprCall) - :filter(function(call) - local calledFunc = call.func - return calledFunc.tag == "global" and calledFunc.name.text == "require" - end) - :filter(function(call) - local firstArg = call.arguments[1].node - return firstArg.tag == "indexname" and firstArg.index.text == "OldComponent" - end) +local p = query.findallfromroot(ast, utils.isRequireCall):filter(function(call) + local firstArg = call.arguments[1].node + return firstArg.tag == "indexname" and firstArg.index.text == "OldComponent" +end) -- Can we improve the type solver here so we don't need to add this annotation? local argIndexNames = query.map(p, function(call: syntax.AstExprCall) diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index 22c9e900e..798cf9150 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -1,6 +1,7 @@ local lintTypes = require("../types") local path = require("@std/path") local syntax = require("@std/syntax") +local utils = require("@std/syntax/utils") local tableext = require("@std/tableext") local visitorLib = require("@std/syntax/visitor") @@ -16,10 +17,6 @@ type branch = { tag: "branch", entries: { [string]: leaf } } -- If we need more levels in the future, we can generalize, but this greatly simplifies the logic. type smallTrie = { tag: "root", entries: { [string]: branch | leaf } } -local function isRequireCall(expr: syntax.AstExpr) - return expr.tag == "call" and expr.func.tag == "global" and expr.func.name.text == "require" -end - local function getWriteOnlyArgs(func: syntax.AstExpr, writeOnlyAPIs: smallTrie): { [number]: true }? if func.tag == "global" then local entry = writeOnlyAPIs.entries[func.name.text] @@ -111,7 +108,7 @@ local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie -- For each (var, value) pair, ignore references to the var in the value for i, localVar in stat.variables do if stat.values[i] then - if isRequireCall(stat.values[i].node) then + if utils.isRequireCall(stat.values[i].node) then visitor.requiredModules[localVar.node.name.text] = localVar.node else visitor.ignoreReferences[localVar.node] = true diff --git a/lute/std/libs/syntax/utils/init.luau b/lute/std/libs/syntax/utils/init.luau index ca503a71e..3f4809751 100644 --- a/lute/std/libs/syntax/utils/init.luau +++ b/lute/std/libs/syntax/utils/init.luau @@ -42,6 +42,12 @@ function utils.isExprCall(n: types.AstNode): types.AstExprCall? return if n.kind == "expr" and n.tag == "call" then n else nil end +function utils.isRequireCall(n: types.AstNode): types.AstExprCall? + local call = utils.isExprCall(n) + local global = call and utils.isExprGlobal(call.func) + return if global and global.name.text == "require" then call else nil +end + function utils.isExprIndexName(n: types.AstNode): types.AstExprIndexName? return if n.kind == "expr" and n.tag == "indexname" then n else nil end diff --git a/tests/std/syntax/utils.test.luau b/tests/std/syntax/utils.test.luau new file mode 100644 index 000000000..c25654d92 --- /dev/null +++ b/tests/std/syntax/utils.test.luau @@ -0,0 +1,18 @@ +local test = require("@std/test") +local syntax = require("@std/syntax") +local query = require("@std/syntax/query") +local utils = require("@std/syntax/utils") + +test.suite("Utils", function(suite) + suite:case("isRequireCall", function(assert) + local ast1 = syntax.parse('local x = require("module")') + local results = query.findallfromroot(ast1, utils.isRequireCall) + assert.eq(#results.nodes, 1) + assert.eq(results.nodes[1].tag, "call") + + -- Non-require calls should not match + local ast2 = syntax.parse('local x = print("hello")') + local results2 = query.findallfromroot(ast2, utils.isRequireCall) + assert.eq(#results2.nodes, 0) + end) +end) From 0c157326e0b7a6e9537355e052b9b04c318d08e8 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:04:43 -0700 Subject: [PATCH 417/642] Finish lint config structure assertion (#858) --- lute/cli/commands/lint/configutils.luau | 42 +++++++++- tests/cli/lint.test.luau | 100 +++++++++++++++--------- tools/check-faillist.txt | 4 +- 3 files changed, 102 insertions(+), 44 deletions(-) diff --git a/lute/cli/commands/lint/configutils.luau b/lute/cli/commands/lint/configutils.luau index 9a165ed9b..065ffe911 100644 --- a/lute/cli/commands/lint/configutils.luau +++ b/lute/cli/commands/lint/configutils.luau @@ -1,18 +1,52 @@ local internalTypes = require("./internaltypes") -local function assertIsArray(arr: { any }, type: string, err: string) - for k, v in arr do - assert(typeof(k) == "number", err) - assert(typeof(v) == type, err) +local function assertTableStructure(tbl: { [unknown]: unknown }, keytype: string, valuetype: string?, err: string) + for k, v in tbl do + assert(typeof(k) == keytype, err) + if valuetype then + assert(typeof(v) == valuetype, err) + end end end +local function assertIsArray(tbl: { [unknown]: unknown }, valuetype: string, err: string) + return assertTableStructure(tbl, "number", valuetype, err) +end + -- we expect .config.luau file structured s.t top level table has: table.lute.lint field local function assertValidConfig(candidate: any) if candidate.ignores then assert(typeof(candidate.ignores) == "table", "config.lute.lint.ignores must be an array of filepath globs") assertIsArray(candidate.ignores, "string", "config.lute.lint.ignores must be an array of filepath globs") end + + if candidate.globals then + assert(typeof(candidate.globals) == "table", "config.lute.lint.globals must be a table with string keys") + assertTableStructure( + candidate.globals, + "string", + nil, + "config.lute.lint.globals must be a table with string keys" + ) + end + + if candidate.rulepaths then + assert(typeof(candidate.rulepaths) == "table", "config.lute.lint.rulepaths must be an array of filepath globs") + assertIsArray(candidate.rulepaths, "string", "config.lute.lint.rulepaths must be an array of filepath globs") + end + + if candidate.ruleconfigs then + assert( + typeof(candidate.ruleconfigs) == "table", + "config.lute.lint.ruleconfigs must be a table with string keys" + ) + assertTableStructure( + candidate.ruleconfigs, + "string", + nil, + "config.lute.lint.ruleconfigs must be a table with string keys" + ) + end end local function extractConfig(candidate: any): internalTypes.LintConfig diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 622f36f33..7f6d271b4 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -37,6 +37,7 @@ local USAGE = "Usage: lute lint [OPTIONS] [...PATHS]" type lintTestParams = { content: string, expectedExitCode: number, + returnStdErr: true?, rulePath: string?, processOptions: process.ProcessRunOptions?, customRuleContents: string?, @@ -110,7 +111,7 @@ local function lintTestHelper(assert: testTypes.Asserts, params: lintTestParams) end end - return result.stdout + return if params.returnStdErr then result.stderr else result.stdout end test.suite("lute lint", function(suite) @@ -2473,18 +2474,28 @@ return { }) end) - suite:case("throwsErrorWhenConfigHasInvalidStructure", function(assert) - -- local invalidGlobalConfig = [[ - -- return { - -- lute = { - -- lint = { - -- globals = "should be table" - -- } - -- } - -- } - -- ]] + suite:case("throwsErrorWhenConfigHasInvalidGlobals", function(assert) + local err = lintTestHelper(assert, { + content = "", + configContents = [[ + return { + lute = { + lint = { + globals = "should be table" + } + } + } + ]], + expectedExitCode = 1, + returnStdErr = true, + }) + assert.strcontains(err, "config.lute.lint.globals must be a table with string keys") + end) - local invalidIgnoresConfig = [[ + suite:case("throwsErrorWhenConfigHasInvalidIgnores", function(assert) + local err = lintTestHelper(assert, { + content = "", + configContents = [[ return { lute = { lint = { @@ -2492,34 +2503,47 @@ return { } } } - ]] + ]], + expectedExitCode = 1, + returnStdErr = true, + }) + assert.strcontains(err, "config.lute.lint.ignores must be an array of filepath globs") + end) - -- local invalidRulesConfig = [[ - -- return { - -- lute = { - -- lint = { - -- ruleConfigs "should be table" - -- } - -- } - -- } - -- ]] - - local configFile = path.join(tmpDir, ".config.luau") - - -- -- fs.writestringtofile(path.format(configFile), invalidGlobalConfig) - -- local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(configFile) }) - -- assert.eq(result.exitcode, 1) - -- assert.strcontains(result.stderr, "config.globals must be a table of [string] : unknown") - - fs.writestringtofile(path.format(configFile), invalidIgnoresConfig) - local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(configFile) }) - assert.eq(result.exitcode, 1) - assert.strcontains(result.stderr, "config.lute.lint.ignores must be an array of filepath globs") + suite:case("throwsErrorWhenConfigHasInvalidRulePaths", function(assert) + local err = lintTestHelper(assert, { + content = "", + configContents = [[ + return { + lute = { + lint = { + rulepaths = "should be table" + } + } + } + ]], + expectedExitCode = 1, + returnStdErr = true, + }) + assert.strcontains(err, "config.lute.lint.rulepaths must be an array of filepath globs") + end) - -- fs.writestringtofile(path.format(configFile), invalidRulesConfig) - -- result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(configFile) }) - -- assert.eq(result.exitcode, 1) - -- assert.strcontains(result.stderr, "config.rules must be a table") + suite:case("throwsErrorWhenConfigHasInvalidRuleConfigs", function(assert) + local err = lintTestHelper(assert, { + content = "", + configContents = [[ + return { + lute = { + lint = { + ruleconfigs = "should be table" + } + } + } + ]], + expectedExitCode = 1, + returnStdErr = true, + }) + assert.strcontains(err, "config.lute.lint.ruleconfigs must be a table with string keys") end) suite:case("luteLintAccountsForConfiguredIgnoresConfigPassedAsArg", function(assert) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index f1004ddc0..5e2135cd9 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -11,8 +11,8 @@ examples/process.luau:17:7-15 examples/process.luau:18:7-15 examples/process.luau:19:7-15 examples/task-resume.luau:3:38-100 -lute/cli/commands/lint/configutils.luau:19:55-73 -lute/cli/commands/lint/configutils.luau:19:55-73 +lute/cli/commands/lint/configutils.luau:53:55-73 +lute/cli/commands/lint/configutils.luau:53:55-73 lute/cli/commands/lint/init.luau:84:12-20 lute/cli/commands/lint/init.luau:312:61-67 lute/cli/commands/lint/init.luau:517:15-24 From 6e159beaf52181e1f1d749d526bac776509b6991 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:19:36 -0700 Subject: [PATCH 418/642] Fix `task.awaitall` (#901) If you try to run the `process` example, an error throws: ``` 0 Hello, lute! @std/task.luau:60: attempt to index number with 'success' stacktrace: @std/task.luau:60 function awaitall ./examples/process.luau:15 ``` This occurs as we iterate over `tasks`, which is equal to a `table.pack` return value. This means `tasks` has an `n` property and is not entirely array-like. We can use `ipairs` when iterating over `tasks` to avoid the bug --- lute/std/libs/task.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index 278b34269..424756136 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -56,7 +56,7 @@ function tasklib.awaitall(...: Task): ...unknown while not done do done = true - for _, v in tasks do + for _, v in ipairs(tasks) do if v.success == nil then done = false task.deferSelf() @@ -66,7 +66,7 @@ function tasklib.awaitall(...: Task): ...unknown local results = {} - for _, v in tasks do + for _, v in ipairs(tasks) do if v.success then table.insert(results, v.result) else From de58e838a38a08025bb4c0f0427e4b2f79b56ab8 Mon Sep 17 00:00:00 2001 From: ariel Date: Wed, 25 Mar 2026 16:21:39 -0700 Subject: [PATCH 419/642] Changes the standard library implementation of `@std/time` to be backed by `@lute/time` (#900) We've had a long-standing split between `@std/time` and `@lute/time` where the former provided an entirely userland implementation of duration, but the latter provided both durations and instants from C++. In the interest of getting the standard library generally into shape and avoiding these confusing duplications (with differences!) between the runtime and the standard library, this PR eliminates the `@std/time` that already exists, and replaces it with one that re-exports the runtime functionality. Making this a proper replacement required both implementing `__mul` and `__div`, as well as fixing a number of smaller bugs that were scattered throughout the runtime implementation of duration. --- definitions/time.luau | 6 ++ lute/std/libs/time/duration.luau | 156 ++++--------------------------- lute/std/libs/time/init.luau | 11 +++ lute/time/include/lute/time.h | 2 + lute/time/src/time.cpp | 101 +++++++++++++++----- tests/std/time.test.luau | 5 +- 6 files changed, 114 insertions(+), 167 deletions(-) diff --git a/definitions/time.luau b/definitions/time.luau index 41c1eac7b..cc94422ee 100644 --- a/definitions/time.luau +++ b/definitions/time.luau @@ -14,6 +14,10 @@ end time.duration = {} +function time.duration.create(seconds: number, subsecnanos: number): Duration + error("not implemented") +end + function time.duration.nanoseconds(nanoseconds: number): Duration error("not implemented") end @@ -100,6 +104,8 @@ export type Duration = setmetatable Duration, read __sub: (Duration, Duration) -> Duration, + read __mul: (Duration, number) -> Duration, + read __div: (Duration, number) -> Duration, read __eq: (Duration, Duration) -> boolean, read __lt: (Duration, Duration) -> boolean, read __le: (Duration, Duration) -> boolean, diff --git a/lute/std/libs/time/duration.luau b/lute/std/libs/time/duration.luau index 8b9a674f2..84de347c4 100644 --- a/lute/std/libs/time/duration.luau +++ b/lute/std/libs/time/duration.luau @@ -1,170 +1,46 @@ --!strict -- Submodule of @std/time that provides utility functions for time durations -local NANOS_PER_SEC = 1_000_000_000 -local NANOS_PER_MILLI = 1_000_000 -local NANOS_PER_MICRO = 1_000 -local MILLIS_PER_SEC = 1_000 -local MICROS_PER_SEC = 1_000_000 -local SECS_PER_MINUTE = 60 -local MINS_PER_HOUR = 60 -local HOURS_PER_DAY = 24 -local DAYS_PER_WEEK = 7 +local time = require("@lute/time") -type DurationData = { - _seconds: number, - _nanoseconds: number, -} +export type Duration = time.Duration local duration = {} -duration.__index = duration -type DurationInterface = typeof(duration) -export type Duration = setmetatable - -function duration.create(seconds: number, nanoseconds: number): Duration - local self = { - _seconds = seconds, - _nanoseconds = nanoseconds, - } - - return setmetatable(self, duration) +function duration.create(seconds: number, subsecnanos: number): Duration + return time.duration.create(seconds, subsecnanos) end -function duration.seconds(seconds: number): Duration - return duration.create(seconds, 0) -end - -function duration.milliseconds(milliseconds: number): Duration - local seconds = math.floor(milliseconds / MILLIS_PER_SEC) - local nanoseconds = (milliseconds % MILLIS_PER_SEC) * NANOS_PER_MILLI - - return duration.create(seconds, nanoseconds) +function duration.nanoseconds(nanoseconds: number): Duration + return time.duration.nanoseconds(nanoseconds) end function duration.microseconds(microseconds: number): Duration - local seconds = math.floor(microseconds / MICROS_PER_SEC) - local nanoseconds = (microseconds % MICROS_PER_SEC) * NANOS_PER_MICRO - - return duration.create(seconds, nanoseconds) + return time.duration.microseconds(microseconds) end -function duration.nanoseconds(nanoseconds: number): Duration - local seconds = math.floor(nanoseconds / NANOS_PER_SEC) - local nanosecondsRemaining = nanoseconds % NANOS_PER_SEC +function duration.milliseconds(milliseconds: number): Duration + return time.duration.milliseconds(milliseconds) +end - return duration.create(seconds, nanosecondsRemaining) +function duration.seconds(seconds: number): Duration + return time.duration.seconds(seconds) end function duration.minutes(minutes: number): Duration - return duration.create(minutes * SECS_PER_MINUTE, 0) + return time.duration.minutes(minutes) end function duration.hours(hours: number): Duration - return duration.create(hours * MINS_PER_HOUR * SECS_PER_MINUTE, 0) + return time.duration.hours(hours) end function duration.days(days: number): Duration - return duration.create(days * HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE, 0) + return time.duration.days(days) end function duration.weeks(weeks: number): Duration - return duration.create(weeks * DAYS_PER_WEEK * HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE, 0) -end - -function duration.subsecmillis(self: Duration): number - return math.floor(self._nanoseconds / NANOS_PER_MILLI) -end - -function duration.subsecmicros(self: Duration): number - return math.floor(self._nanoseconds / NANOS_PER_MICRO) -end - -function duration.subsecnanos(self: Duration): number - return self._nanoseconds -end - -function duration.toseconds(self: Duration): number - return self._seconds + self._nanoseconds / NANOS_PER_SEC -end - -function duration.tomilliseconds(self: Duration): number - return self._seconds * MILLIS_PER_SEC + self:subsecmillis() -end - -function duration.tomicroseconds(self: Duration): number - return self._seconds * MICROS_PER_SEC + self:subsecmicros() -end - -function duration.tonanoseconds(self: Duration): number - return self._seconds * NANOS_PER_SEC + self:subsecnanos() -end - -function duration.__add(a: Duration, b: Duration): Duration - local seconds = a._seconds + b._seconds - local nanoseconds = a._nanoseconds + b._nanoseconds - - if nanoseconds >= NANOS_PER_SEC then - seconds += 1 - nanoseconds -= NANOS_PER_SEC - end - - return duration.create(seconds, nanoseconds) -end - -function duration.__sub(a: Duration, b: Duration): Duration - local seconds = a._seconds - b._seconds - local nanoseconds = a._nanoseconds - b._nanoseconds - - if nanoseconds < 0 then - seconds -= 1 - nanoseconds += NANOS_PER_SEC - end - - return duration.create(seconds, nanoseconds) -end - -function duration.__mul(a: Duration, b: number): Duration - local seconds = a._seconds * b - local nanoseconds = a._nanoseconds * b - - seconds += math.floor(nanoseconds / NANOS_PER_SEC) - nanoseconds %= NANOS_PER_SEC - - return duration.create(seconds, nanoseconds) -end - -function duration.__div(a: Duration, b: number): Duration - local seconds, extraSeconds = a._seconds / b, a._seconds % b - local nanoseconds, extraNanos = a._nanoseconds / b, a._nanoseconds % b - - nanoseconds += (extraSeconds * NANOS_PER_SEC + extraNanos) / b - - return duration.create(seconds, nanoseconds) -end - -function duration.__eq(a: Duration, b: Duration): boolean - return a._seconds == b._seconds and a._nanoseconds == b._nanoseconds -end - -function duration.__lt(a: Duration, b: Duration): boolean - if a._seconds == b._seconds then - return a._nanoseconds < b._nanoseconds - end - - return a._seconds < b._seconds -end - -function duration.__le(a: Duration, b: Duration): boolean - if a._seconds == b._seconds then - return a._nanoseconds <= b._nanoseconds - end - - return a._seconds <= b._seconds -end - -function duration.__tostring(self: Duration): string - return string.format("%d.%09d", self._seconds, self._nanoseconds) + return time.duration.weeks(weeks) end return table.freeze(duration) diff --git a/lute/std/libs/time/init.luau b/lute/std/libs/time/init.luau index 309f650ef..c7d22e87a 100644 --- a/lute/std/libs/time/init.luau +++ b/lute/std/libs/time/init.luau @@ -2,11 +2,22 @@ -- @std/time -- Provides utility functions for time +local luteTime = require("@lute/time") local duration = require("@self/duration") + local time = {} export type Duration = duration.Duration +export type Instant = luteTime.Instant time.duration = duration +function time.now(): Instant + return luteTime.now() +end + +function time.since(instant: Instant): number + return luteTime.since(instant) +end + return table.freeze(time) diff --git a/lute/time/include/lute/time.h b/lute/time/include/lute/time.h index 92c4d2301..8a7dc34f4 100644 --- a/lute/time/include/lute/time.h +++ b/lute/time/include/lute/time.h @@ -24,6 +24,7 @@ int createDurationFromSeconds(lua_State* L, double seconds); namespace duration { +int lua_createduration(lua_State* L); int lua_nanoseconds(lua_State* L); int lua_microseconds(lua_State* L); int lua_milliseconds(lua_State* L); @@ -34,6 +35,7 @@ int lua_days(lua_State* L); int lua_weeks(lua_State* L); static const luaL_Reg lib[] = { + {"create", lua_createduration}, {"nanoseconds", lua_nanoseconds}, {"microseconds", lua_microseconds}, {"milliseconds", lua_milliseconds}, diff --git a/lute/time/src/time.cpp b/lute/time/src/time.cpp index 052610fd4..5328d4eb9 100644 --- a/lute/time/src/time.cpp +++ b/lute/time/src/time.cpp @@ -12,15 +12,15 @@ #include #include -const int64_t NANOSECONDS_PER_SECOND = 1000000000; -const int64_t MICROSECONDS_PER_SECOND = 1000000; +const int64_t NANOSECONDS_PER_SECOND = 1'000'000'000; +const int64_t MICROSECONDS_PER_SECOND = 1'000'000; const int64_t MILLISECONDS_PER_SECOND = 1000; const int64_t SECONDS_PER_MINUTE = 60; const int64_t SECONDS_PER_HOUR = 3600; -const int64_t SECONDS_PER_DAY = 86400; -const int64_t SECONDS_PER_WEEK = 604800; +const int64_t SECONDS_PER_DAY = 86'400; +const int64_t SECONDS_PER_WEEK = 604'800; const int64_t NANOSECONDS_PER_MICROSECOND = 1000; -const int64_t NANOSECONDS_PER_MILLISECOND = 1000000; +const int64_t NANOSECONDS_PER_MILLISECOND = 1'000'000; // Timespec helpers static float_t diffTimespecs(uv_timespec64_t left, uv_timespec64_t right) @@ -50,6 +50,25 @@ double getSecondsFromTimespec(uv_timespec64_t timespec) return static_cast(timespec.tv_sec) + static_cast(timespec.tv_nsec) / NANOSECONDS_PER_SECOND; } +uv_timespec64_t getTimespecFromSeconds(double seconds) +{ + int64_t sec = static_cast(seconds); + int32_t nsec = static_cast(fmod(seconds, 1) * NANOSECONDS_PER_SECOND); + return {sec, nsec}; +} + +uint64_t getNanosecondsFromTimespec(uv_timespec64_t timespec) +{ + return timespec.tv_sec * NANOSECONDS_PER_SECOND + timespec.tv_nsec; +} + +uv_timespec64_t getTimespecFromNanoseconds(int64_t nanoseconds) +{ + int64_t sec = nanoseconds / NANOSECONDS_PER_SECOND ; + int32_t nsec = static_cast(fmod(nanoseconds, NANOSECONDS_PER_SECOND)); + return {sec, nsec}; +} + // Durations // returns the address of the timespec from the duration on the stack @@ -72,7 +91,7 @@ int createDurationFromTimespec(lua_State* L, uv_timespec64_t timespec) int createDurationFromSeconds(lua_State* L, double seconds) { - return createDurationFromTimespec(L, {static_cast(seconds), static_cast(fmod(seconds, 1) * NANOSECONDS_PER_SECOND)}); + return createDurationFromTimespec(L, getTimespecFromSeconds(seconds)); } // Duration methods @@ -86,14 +105,14 @@ static int duration_tonanoseconds(lua_State* L) static int duration_tomicroseconds(lua_State* L) { uv_timespec64_t timespec = getTimespecFromDuration(L, 1); - lua_pushnumber(L, getSecondsFromTimespec(timespec) * MICROSECONDS_PER_SECOND); + lua_pushnumber(L, static_cast(timespec.tv_sec) * MICROSECONDS_PER_SECOND + timespec.tv_nsec / NANOSECONDS_PER_MICROSECOND); return 1; } static int duration_tomilliseconds(lua_State* L) { uv_timespec64_t timespec = getTimespecFromDuration(L, 1); - lua_pushnumber(L, getSecondsFromTimespec(timespec) * MILLISECONDS_PER_SECOND); + lua_pushnumber(L, static_cast(timespec.tv_sec) * MILLISECONDS_PER_SECOND + timespec.tv_nsec / NANOSECONDS_PER_MILLISECOND); return 1; } @@ -121,35 +140,35 @@ static int duration_tohours(lua_State* L) static int duration_todays(lua_State* L) { uv_timespec64_t timespec = getTimespecFromDuration(L, 1); - lua_pushnumber(L, getSecondsFromTimespec(timespec) / SECONDS_PER_DAY); + lua_pushinteger(L, getSecondsFromTimespec(timespec) / SECONDS_PER_DAY); return 1; } static int duration_toweeks(lua_State* L) { uv_timespec64_t timespec = getTimespecFromDuration(L, 1); - lua_pushnumber(L, getSecondsFromTimespec(timespec) / SECONDS_PER_WEEK); + lua_pushinteger(L, getSecondsFromTimespec(timespec) / SECONDS_PER_WEEK); return 1; } static int duration_subsecnanos(lua_State* L) { uv_timespec64_t timespec = getTimespecFromDuration(L, 1); - lua_pushnumber(L, timespec.tv_nsec); + lua_pushinteger(L, timespec.tv_nsec); return 1; } static int duration_subsecmicros(lua_State* L) { uv_timespec64_t timespec = getTimespecFromDuration(L, 1); - lua_pushnumber(L, static_cast(timespec.tv_nsec) / NANOSECONDS_PER_MICROSECOND); + lua_pushinteger(L, timespec.tv_nsec / NANOSECONDS_PER_MICROSECOND); return 1; } static int duration_subsecmillis(lua_State* L) { uv_timespec64_t timespec = getTimespecFromDuration(L, 1); - lua_pushnumber(L, static_cast(timespec.tv_nsec) / NANOSECONDS_PER_MILLISECOND); + lua_pushinteger(L, timespec.tv_nsec / NANOSECONDS_PER_MILLISECOND); return 1; } @@ -169,7 +188,7 @@ static int duration__add(lua_State* L) uv_timespec64_t right = getTimespecFromDuration(L, 2); uv_timespec64_t result = {left.tv_sec + right.tv_sec, left.tv_nsec + right.tv_nsec}; - if (result.tv_nsec > NANOSECONDS_PER_SECOND) + if (result.tv_nsec >= NANOSECONDS_PER_SECOND) { result.tv_sec += 1; result.tv_nsec -= NANOSECONDS_PER_SECOND; @@ -193,6 +212,24 @@ static int duration__sub(lua_State* L) return createDurationFromTimespec(L, {result.tv_sec >= 0 ? result.tv_sec : 0, result.tv_nsec >= 0 ? result.tv_nsec : 0}); } +static int duration__mul(lua_State* L) +{ + uv_timespec64_t left = getTimespecFromDuration(L, 1); + double factor = luaL_checknumber(L, 2); + + uv_timespec64_t result = getTimespecFromNanoseconds(getNanosecondsFromTimespec(left) * factor); + return createDurationFromTimespec(L, {result.tv_sec >= 0 ? result.tv_sec : 0, result.tv_nsec >= 0 ? result.tv_nsec : 0}); +} + +static int duration__div(lua_State* L) +{ + uv_timespec64_t left = getTimespecFromDuration(L, 1); + double factor = luaL_checknumber(L, 2); + + uv_timespec64_t result = getTimespecFromNanoseconds(getNanosecondsFromTimespec(left) / factor); + return createDurationFromTimespec(L, {result.tv_sec >= 0 ? result.tv_sec : 0, result.tv_nsec >= 0 ? result.tv_nsec : 0}); +} + static int duration__eq(lua_State* L) { uv_timespec64_t left = getTimespecFromDuration(L, 1); @@ -271,15 +308,32 @@ static int instant__le(lua_State* L) namespace duration { + +int lua_createduration(lua_State* L) +{ + int64_t seconds = luaL_checkinteger(L, 1); + int64_t nanoseconds = static_cast(luaL_checkinteger(L, 2)); + if (seconds < 0 || nanoseconds < 0) + luaL_error(L, "duration components cannot be negative"); + + if (nanoseconds >= NANOSECONDS_PER_SECOND) + { + seconds += nanoseconds / NANOSECONDS_PER_SECOND; + nanoseconds = fmod(nanoseconds, NANOSECONDS_PER_SECOND); + } + + return createDurationFromTimespec(L, {seconds, static_cast(nanoseconds)}); +} + int lua_nanoseconds(lua_State* L) { // int32_t doesn't have enough precision for nanoseconds - int64_t nanoseconds = static_cast(luaL_checknumber(L, 1)); + int64_t nanoseconds = static_cast(luaL_checkinteger(L, 1)); if (nanoseconds < 0) luaL_error(L, "duration cannot be negative"); int64_t seconds = 0; - if (nanoseconds > NANOSECONDS_PER_SECOND) + if (nanoseconds >= NANOSECONDS_PER_SECOND) { seconds = static_cast(nanoseconds / NANOSECONDS_PER_SECOND); nanoseconds = static_cast(fmod(nanoseconds, NANOSECONDS_PER_SECOND)); @@ -310,14 +364,9 @@ int lua_milliseconds(lua_State* L) if (milliseconds < 0) luaL_error(L, "duration cannot be negative"); - int64_t seconds = 0; - if (milliseconds > MILLISECONDS_PER_SECOND) - { - seconds = static_cast(milliseconds / MILLISECONDS_PER_SECOND); - milliseconds = fmod(milliseconds, MILLISECONDS_PER_SECOND); - } + uint64_t nanoseconds = milliseconds * NANOSECONDS_PER_MILLISECOND; - return createDurationFromTimespec(L, {seconds, static_cast(milliseconds * NANOSECONDS_PER_MILLISECOND)}); + return createDurationFromTimespec(L, getTimespecFromNanoseconds(nanoseconds)); } int lua_seconds(lua_State* L) @@ -432,6 +481,12 @@ static void init_duration_lib(lua_State* L) lua_pushcfunction(L, duration__sub, "Duration__sub"); lua_setfield(L, -2, "__sub"); + lua_pushcfunction(L, duration__mul, "Duration__mul"); + lua_setfield(L, -2, "__mul"); + + lua_pushcfunction(L, duration__div, "Duration__div"); + lua_setfield(L, -2, "__div"); + lua_pushcfunction(L, duration__eq, "Duration__eq"); lua_setfield(L, -2, "__eq"); diff --git a/tests/std/time.test.luau b/tests/std/time.test.luau index dde601901..c2e4ba06c 100644 --- a/tests/std/time.test.luau +++ b/tests/std/time.test.luau @@ -51,18 +51,15 @@ test.suite("TimeSuite", function(suite) end) suite:case("createRawValuesAndNormalizationViaOps", function(assert) - -- create keeps raw values without normalization local d = duration.create(1, 500_000_000) assert.eq(d:toseconds(), 1.5) assert.eq(d:subsecnanos(), 500_000_000) assert.eq(d:tomilliseconds(), 1500) - -- if nanoseconds exceed a second, create still allows it local d2 = duration.create(1, 1_500_000_000) assert.eq(d2:toseconds(), 2.5) - assert.eq(d2:subsecnanos(), 1_500_000_000) + assert.eq(d2:subsecnanos(), 500_000_000) - -- operations like addition normalize nanoseconds local normalized = d2 + duration.seconds(0) assert.eq(normalized:toseconds(), 2.5) assert.eq(normalized:subsecnanos(), 500_000_000) From e23b2cd28b6f3a42baa53fc3faf548715f97d6ad Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:20:40 -0700 Subject: [PATCH 420/642] Support non-prefixed relative paths in script configuration files (#902) Adds explicit `jumpToAlias` implementations to support FileVfs handling unprefixed relative paths in configuration files. Earlier, we were able to get away with reusing the same `resetToPath` function for both resetting to a path (such as when initializing FileVfs to point to the project entry point script) and for jumping to aliases. We now need to split these back into two different implementations (although, there is a good amount of logic reused) because the "reset" path requires us to resolve relative paths from the current working directory, but the "jumpToAlias" path requires us to resolve them relative to the .luaurc/.config.luau in which they were defined. This must be done here and not in Luau.Require, as Luau.Require relies on prefixes to determine path type. The absence of a prefix in this case means that the embedder (Lute) must be responsible for path resolution. --- lute/luau/src/resolvemodule.cpp | 2 +- lute/require/include/lute/filevfs.h | 3 + lute/require/include/lute/modulepath.h | 80 +++++++++++++++++++ lute/require/include/lute/userlandvfs.h | 1 + lute/require/src/filevfs.cpp | 56 ++++++++++--- lute/require/src/packagerequirevfs.cpp | 2 +- lute/require/src/requirevfs.cpp | 2 +- lute/require/src/userlandvfs.cpp | 14 ++++ tests/src/require.test.cpp | 2 + .../src/unprefixed_relative_alias/.luaurc | 5 ++ .../unprefixed_relative_alias/requirer.luau | 1 + .../unprefixed_dep.luau | 1 + .../unprefixed_relative_alias/.config.luau | 7 ++ .../unprefixed_relative_alias/requirer.luau | 1 + .../unprefixed_dep.luau | 1 + 15 files changed, 162 insertions(+), 16 deletions(-) create mode 100644 tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/.luaurc create mode 100644 tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/requirer.luau create mode 100644 tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/unprefixed_dep.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/.config.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/requirer.luau create mode 100644 tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/unprefixed_dep.luau diff --git a/lute/luau/src/resolvemodule.cpp b/lute/luau/src/resolvemodule.cpp index 766c6e152..6fb22cf4b 100644 --- a/lute/luau/src/resolvemodule.cpp +++ b/lute/luau/src/resolvemodule.cpp @@ -86,7 +86,7 @@ NC::NavigateResult FileVfsContext::resetToRequirer() NC::NavigateResult FileVfsContext::jumpToAlias(const std::string& path) { - return convert(vfs.resetToPath(path)); + return convert(vfs.jumpToAlias(path)); } NC::NavigateResult FileVfsContext::toParent() diff --git a/lute/require/include/lute/filevfs.h b/lute/require/include/lute/filevfs.h index ce601a53d..76ac87e4b 100644 --- a/lute/require/include/lute/filevfs.h +++ b/lute/require/include/lute/filevfs.h @@ -9,6 +9,7 @@ class FileVfs public: NavigationStatus resetToStdIn(); NavigationStatus resetToPath(const std::string& path); + NavigationStatus jumpToAlias(const std::string& path); NavigationStatus toParent(); NavigationStatus toChild(const std::string& name); @@ -23,4 +24,6 @@ class FileVfs private: std::optional modulePath; + + NavigationStatus doReset(const std::string& path, std::function handleRelativePath); }; diff --git a/lute/require/include/lute/modulepath.h b/lute/require/include/lute/modulepath.h index f671010d4..4f17bde29 100644 --- a/lute/require/include/lute/modulepath.h +++ b/lute/require/include/lute/modulepath.h @@ -1,5 +1,7 @@ #pragma once +#include "Luau/RequireNavigator.h" + #include #include #include @@ -72,3 +74,81 @@ class ModulePath std::string modulePath; std::optional relativePathToTrack; }; + +class ModulePathNavigationContext : public Luau::Require::NavigationContext +{ +public: + ModulePathNavigationContext(ModulePath modulePath) + : current(std::move(modulePath)) + , start(current) + { + } + + ModulePath getCurrentModulePath() const + { + return current; + } + + NavigateResult resetToRequirer() override + { + current = start; + return NavigateResult::Success; + } + + NavigateResult jumpToAlias(const std::string& path) override + { + return NavigateResult::NotFound; + } + + NavigateResult toParent() override + { + return convert(current.toParent()); + } + + NavigateResult toChild(const std::string& component) override + { + return convert(current.toChild(component)); + } + + ConfigStatus getConfigStatus() const override + { + return ConfigStatus::Absent; + } + + ConfigBehavior getConfigBehavior() const override + { + return ConfigBehavior::GetConfig; + } + + std::optional getAlias(const std::string& alias) const override + { + return std::nullopt; + } + + std::optional getConfig() const override + { + return std::nullopt; + } + +private: + ModulePath current; + ModulePath start; + + NavigateResult convert(NavigationStatus status) const + { + NavigateResult result = NavigateResult::NotFound; + switch (status) + { + case NavigationStatus::Success: + result = NavigateResult::Success; + break; + case NavigationStatus::Ambiguous: + result = NavigateResult::Ambiguous; + break; + case NavigationStatus::NotFound: + result = NavigateResult::NotFound; + break; + } + return result; + } +}; diff --git a/lute/require/include/lute/userlandvfs.h b/lute/require/include/lute/userlandvfs.h index 71967948c..b54c2cad9 100644 --- a/lute/require/include/lute/userlandvfs.h +++ b/lute/require/include/lute/userlandvfs.h @@ -73,6 +73,7 @@ class UserlandVfs static UserlandVfs create(std::vector directDependencies, std::vector> allDependencies); NavigationStatus resetToPath(const std::string& path); + NavigationStatus jumpToAlias(const std::string& path); NavigationStatus toAliasFallback(std::string_view aliasUnprefixed); NavigationStatus toParent(); diff --git a/lute/require/src/filevfs.cpp b/lute/require/src/filevfs.cpp index 268bb0d22..91bafaad0 100644 --- a/lute/require/src/filevfs.cpp +++ b/lute/require/src/filevfs.cpp @@ -4,16 +4,13 @@ #include "lute/modulepath.h" #include "lute/uvutils.h" -#include "Luau/Common.h" #include "Luau/Config.h" #include "Luau/FileUtils.h" #include "Luau/LuauConfig.h" #include "uv.h" -#include #include -#include NavigationStatus FileVfs::resetToStdIn() { @@ -28,7 +25,7 @@ NavigationStatus FileVfs::resetToStdIn() return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; } -NavigationStatus FileVfs::resetToPath(const std::string& path) +NavigationStatus FileVfs::doReset(const std::string& path, std::function handleRelativePath) { std::string pathToProcess = path; @@ -59,21 +56,54 @@ NavigationStatus FileVfs::resetToPath(const std::string& path) } else { - std::optional cwd = getCurrentWorkingDirectory(); - if (!cwd) + if (!handleRelativePath(normalizedPath)) return NavigationStatus::NotFound; - - std::string joinedPath = normalizePath(*cwd + "/" + normalizedPath); - - size_t firstSlash = joinedPath.find_first_of("\\/"); - LUTE_ASSERT(firstSlash != std::string::npos); - - modulePath = ModulePath::create(joinedPath.substr(0, firstSlash), joinedPath.substr(firstSlash + 1), isFile, isDirectory, normalizedPath); } return modulePath ? NavigationStatus::Success : NavigationStatus::NotFound; } +NavigationStatus FileVfs::resetToPath(const std::string& path) +{ + return doReset( + path, + [this](const std::string& normalizedPath) + { + std::optional cwd = getCurrentWorkingDirectory(); + if (!cwd) + return false; + + std::string joinedPath = normalizePath(*cwd + "/" + normalizedPath); + + size_t firstSlash = joinedPath.find_first_of("\\/"); + LUTE_ASSERT(firstSlash != std::string::npos); + + modulePath = ModulePath::create(joinedPath.substr(0, firstSlash), joinedPath.substr(firstSlash + 1), isFile, isDirectory, normalizedPath); + return true; + } + ); +} + +NavigationStatus FileVfs::jumpToAlias(const std::string& path) +{ + return doReset( + path, + [this](const std::string& normalizedPath) + { + ModulePathNavigationContext nc = ModulePathNavigationContext(*modulePath); + Luau::Require::ErrorHandler er; + Luau::Require::Navigator navigator(nc, er); + Luau::Require::Navigator::Status status = navigator.navigate("@self/" + normalizedPath); + + if (status == Luau::Require::Navigator::Status::ErrorReported) + return false; + + modulePath = nc.getCurrentModulePath(); + return true; + } + ); +} + NavigationStatus FileVfs::toParent() { LUTE_ASSERT(modulePath); diff --git a/lute/require/src/packagerequirevfs.cpp b/lute/require/src/packagerequirevfs.cpp index f4c0b11aa..d879f327b 100644 --- a/lute/require/src/packagerequirevfs.cpp +++ b/lute/require/src/packagerequirevfs.cpp @@ -53,7 +53,7 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) switch (vfsType) { case VFSType::Userland: - status = userlandVfs.resetToPath(std::string(path)); + status = userlandVfs.jumpToAlias(std::string(path)); break; case VFSType::Std: status = stdLibVfs.resetToPath(std::string(path)); diff --git a/lute/require/src/requirevfs.cpp b/lute/require/src/requirevfs.cpp index 1dfb26586..0642e9c16 100644 --- a/lute/require/src/requirevfs.cpp +++ b/lute/require/src/requirevfs.cpp @@ -75,7 +75,7 @@ NavigationStatus RequireVfs::jumpToAlias(lua_State* L, std::string_view path) switch (vfsType) { case VFSType::Disk: - status = fileVfs.resetToPath(std::string(path)); + status = fileVfs.jumpToAlias(std::string(path)); break; case VFSType::Std: status = stdLibVfs.resetToPath(std::string(path)); diff --git a/lute/require/src/userlandvfs.cpp b/lute/require/src/userlandvfs.cpp index 9fc012605..703f216fa 100644 --- a/lute/require/src/userlandvfs.cpp +++ b/lute/require/src/userlandvfs.cpp @@ -133,6 +133,20 @@ NavigationStatus UserlandVfs::resetToPath(const std::string& path) return fileVfs.resetToPath(path); } +NavigationStatus UserlandVfs::jumpToAlias(const std::string& path) +{ + for (const auto& [identifier, info] : allDependencies) + { + if (path.rfind(info.rootDirectory, 0) == 0) + return jumpToDependencySubtree(identifier); + } + + currentSubtree = std::nullopt; + vfsType = VFSType::Disk; + + return fileVfs.jumpToAlias(path); +} + NavigationStatus UserlandVfs::toAliasFallback(std::string_view aliasUnprefixed) { std::vector availableDependencies; diff --git a/tests/src/require.test.cpp b/tests/src/require.test.cpp index fc636040d..70615397e 100644 --- a/tests/src/require.test.cpp +++ b/tests/src/require.test.cpp @@ -210,6 +210,8 @@ TEST_CASE_FIXTURE(LuteFixture, "require_by_string_semantics_in_cli") joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/init.luau"), joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/submodule"), joinPaths(luteProjectRoot, "tests/src/require/without_config/nested/submodule.luau"), + joinPaths(luteProjectRoot, "tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/requirer.luau"), + joinPaths(luteProjectRoot, "tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/requirer.luau"), }; for (std::string& inputPath : inputPaths) diff --git a/tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/.luaurc b/tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/.luaurc new file mode 100644 index 000000000..11fe6bd0a --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "unprefixed_dep": "unprefixed_dep", + } +} diff --git a/tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/requirer.luau b/tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/requirer.luau new file mode 100644 index 000000000..879a5b4db --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/requirer.luau @@ -0,0 +1 @@ +return require("@unprefixed_dep") diff --git a/tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/unprefixed_dep.luau b/tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/unprefixed_dep.luau new file mode 100644 index 000000000..ec6b91418 --- /dev/null +++ b/tests/src/require/config_tests/with_config/src/unprefixed_relative_alias/unprefixed_dep.luau @@ -0,0 +1 @@ +return { "result from unprefixed relative alias" } diff --git a/tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/.config.luau b/tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/.config.luau new file mode 100644 index 000000000..f347524aa --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + unprefixed_dep = "unprefixed_dep", + } + } +} diff --git a/tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/requirer.luau b/tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/requirer.luau new file mode 100644 index 000000000..879a5b4db --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/requirer.luau @@ -0,0 +1 @@ +return require("@unprefixed_dep") diff --git a/tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/unprefixed_dep.luau b/tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/unprefixed_dep.luau new file mode 100644 index 000000000..ec6b91418 --- /dev/null +++ b/tests/src/require/config_tests/with_config_luau/src/unprefixed_relative_alias/unprefixed_dep.luau @@ -0,0 +1 @@ +return { "result from unprefixed relative alias" } From 12c0ca3956b34eb97a13832fdd76ecf7574387a7 Mon Sep 17 00:00:00 2001 From: Master Oogway <125769645+ActualMasterOogway@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:14:37 +0100 Subject: [PATCH 421/642] Use embedded sources for `@std`, `@lute`, and `@batteries` in `lute check` (#862) This PR ensures that `lute check` correctly resolves and uses embedded source code for the `@std`, `@lute`, and `@batteries` namespaces during static analysis. Previously, `lute check` used a basic file resolver that only searched the physical disk. Since Lute's standard library and runtime definitions are embedded directly into the executable, `require` calls to these namespaces would fail to resolve. This caused Luau to treat standard modules as `any`, effectively disabling typechecking for code that relied on Lute's built-in APIs. - **Refactored `resolveRequire`**: Updated the core resolution logic in `lute/luau/src/resolverequire.cpp` to support both physical disk and virtual embedded paths. - **Implemented `readSourceFromVfs`**: Added a utility to retrieve source code directly from Lute's internal memory buffers when an `@` alias is used. - **Updated Resolvers**: Integrated the new resolution and reading logic into `LuteModuleResolver` and `LuteFileResolver`. - **Build System Integration**: Updated `lute/luau/CMakeLists.txt` to link the analysis library against `Lute.Std`, `Lute.Lute`, and `Lute.Batteries`, ensuring the embedded sources are available at compile time. - **Improved Global Registry**: Ensured the `require` function is correctly recognized as a built-in global during the analysis phase. Fixes #773 --------- Co-authored-by: Master Oogway Co-authored-by: Vighnesh Vijay --- .luaurc.ci | 3 +- lute/cli/src/staticrequires.cpp | 4 +- lute/cli/src/tc.cpp | 35 +-- lute/luau/include/lute/resolvemodule.h | 8 + lute/luau/include/lute/tcmoduleresolver.h | 7 + lute/luau/src/resolvemodule.cpp | 281 +++++++++++++++++++-- lute/luau/src/tcmoduleresolver.cpp | 38 ++- lute/require/src/lutevfs.cpp | 7 +- tests/batteries/.luaurc | 6 + tests/src/moduleresolver.test.cpp | 81 +++++- tests/src/typecheck.test.cpp | 18 ++ tests/src/typecheck/.luaurc | 3 + tests/src/typecheck/all_builtins.luau | 6 + tests/src/typecheck/batteries_failure.luau | 1 + 14 files changed, 428 insertions(+), 70 deletions(-) create mode 100644 tests/batteries/.luaurc create mode 100644 tests/src/typecheck/.luaurc create mode 100644 tests/src/typecheck/all_builtins.luau create mode 100644 tests/src/typecheck/batteries_failure.luau diff --git a/.luaurc.ci b/.luaurc.ci index 99ef608b7..822341bf4 100644 --- a/.luaurc.ci +++ b/.luaurc.ci @@ -1,7 +1,6 @@ { "languageMode": "strict", "aliases": { - "batteries": "./batteries", "commands": "./lute/cli/commands" } -} +} \ No newline at end of file diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index 48e687b47..9ec8404e5 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -137,14 +137,14 @@ void StaticRequireTracer::trace(const std::string& entryPoint) // Store the resolved dependencies in the graph requireGraph[filePath] = std::move(resolvedDeps); } - + // Include .luaurc files in the lowest common root calculation std::vector allPaths = discovered; for (const auto& luaurcPath : luaurcAbsolutePaths) { allPaths.push_back(luaurcPath); } - + lowestCommonRoot = findLowestCommonRoot(allPaths); // Convert absolute .luaurc paths to LCR-relative .luaurc paths and read their content diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index c6c6d775e..a3379a9c0 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -8,39 +8,6 @@ #include "Luau/FileUtils.h" #include "Luau/Frontend.h" -struct LuteFileResolver : Luau::LuteTypeCheckModuleResolver -{ - std::optional readSource(const Luau::ModuleName& name) override - { - Luau::SourceCode::Type sourceType; - std::optional source = std::nullopt; - - // If the module name is "-", then read source from stdin - if (name == "-") - { - source = readStdin(); - sourceType = Luau::SourceCode::Script; - } - else - { - source = readFile(name); - sourceType = Luau::SourceCode::Module; - } - - if (!source) - return std::nullopt; - - return Luau::SourceCode{*source, sourceType}; - } - - std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override - { - if (name == "-") - return "stdin"; - return name; - } -}; - static void report(const char* name, const Luau::Location& loc, const char* type, const char* message, LuteReporter& reporter) { // fprintf(stderr, "%s(%d,%d): %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, type, message); @@ -158,7 +125,7 @@ int typecheck(const std::vector& sourceFilesInput, LuteReporter& re frontendOptions.retainFullTypeGraphs = annotate; frontendOptions.runLintChecks = true; - LuteFileResolver fileResolver; + Luau::LuteTypeCheckModuleResolver fileResolver; Luau::LuteConfigResolver configResolver(mode); Luau::Frontend frontend(Luau::SolverMode::New, &fileResolver, &configResolver, frontendOptions); diff --git a/lute/luau/include/lute/resolvemodule.h b/lute/luau/include/lute/resolvemodule.h index 2292847ac..3a7437b7d 100644 --- a/lute/luau/include/lute/resolvemodule.h +++ b/lute/luau/include/lute/resolvemodule.h @@ -5,5 +5,13 @@ struct lua_State; +struct ResolvedModule +{ + std::string path; + std::string source; +}; + std::optional resolveModule(std::string requirePath, std::string requirerChunkname, std::string* error); +std::optional resolveForTypeCheck(std::string requirePath, std::string requirerChunkname, std::string* error); + int resolveModule_luau(lua_State* L); diff --git a/lute/luau/include/lute/tcmoduleresolver.h b/lute/luau/include/lute/tcmoduleresolver.h index 3e6b3765d..84235b74b 100644 --- a/lute/luau/include/lute/tcmoduleresolver.h +++ b/lute/luau/include/lute/tcmoduleresolver.h @@ -1,7 +1,11 @@ #pragma once +#include "Luau/DenseHash.h" #include "Luau/FileResolver.h" +#include +#include + namespace Luau { @@ -14,6 +18,9 @@ struct LuteTypeCheckModuleResolver : Luau::FileResolver // We are currently resolving modules and requires only, and will add support for Roblox globals / types in a subsequent PR. std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const TypeCheckLimits& limits) override; + std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override; + + Luau::DenseHashMap sourceCache{""}; }; } // namespace Luau diff --git a/lute/luau/src/resolvemodule.cpp b/lute/luau/src/resolvemodule.cpp index 6fb22cf4b..f66dd520b 100644 --- a/lute/luau/src/resolvemodule.cpp +++ b/lute/luau/src/resolvemodule.cpp @@ -1,8 +1,12 @@ #include "lute/resolvemodule.h" +#include "lute/batteriesvfs.h" #include "lute/filevfs.h" +#include "lute/lutevfs.h" #include "lute/modulepath.h" +#include "lute/stdlibvfs.h" +#include "Luau/FileUtils.h" #include "Luau/RequireNavigator.h" #include "lua.h" @@ -11,28 +15,6 @@ #include #include -// FileVfsContext -class FileVfsContext : public Luau::Require::NavigationContext -{ -public: - FileVfsContext(std::string requirerChunkname); - - NavigateResult resetToRequirer() override; - NavigateResult jumpToAlias(const std::string& path) override; - - NavigateResult toParent() override; - NavigateResult toChild(const std::string& component) override; - - ConfigStatus getConfigStatus() const override; - - ConfigBehavior getConfigBehavior() const override; - std::optional getAlias(const std::string& alias) const override; - std::optional getConfig() const override; - - FileVfs vfs; - std::string requirerChunkname; -}; - using NC = Luau::Require::NavigationContext; static NC::NavigateResult convert(NavigationStatus status) @@ -74,6 +56,28 @@ static NC::ConfigStatus convert(ConfigStatus status) return result; } +// FileVfsContext +class FileVfsContext : public Luau::Require::NavigationContext +{ +public: + FileVfsContext(std::string requirerChunkname); + + NavigateResult resetToRequirer() override; + NavigateResult jumpToAlias(const std::string& path) override; + + NavigateResult toParent() override; + NavigateResult toChild(const std::string& component) override; + + ConfigStatus getConfigStatus() const override; + + ConfigBehavior getConfigBehavior() const override; + std::optional getAlias(const std::string& alias) const override; + std::optional getConfig() const override; + + FileVfs vfs; + std::string requirerChunkname; +}; + FileVfsContext::FileVfsContext(std::string requirerChunkname) : requirerChunkname(std::move(requirerChunkname)) { @@ -119,6 +123,202 @@ std::optional FileVfsContext::getConfig() const return vfs.getConfig(); } +struct LuteTypeCheckVfs +{ + FileVfs fileVfs; + StdLibVfs stdLibVfs; + LuteVfs luteVfs; + BatteriesVfs batteriesVfs; + + enum class VFSType + { + Disk, + Std, + Lute, + Batteries, + }; + VFSType vfsType = VFSType::Disk; + + NavigationStatus jumpToAlias(const std::string& path) + { + if (vfsType == VFSType::Disk) + { + return fileVfs.jumpToAlias(path); + } + return resetToPath(path); + } + + NavigationStatus resetToPath(const std::string& path) + { + if (path.rfind("@std", 0) == 0) + { + vfsType = VFSType::Std; + return stdLibVfs.resetToPath(path); + } + else if (path.rfind("@lute", 0) == 0) + { + vfsType = VFSType::Lute; + return luteVfs.resetToPath(path); + } + else if (path.rfind("@batteries", 0) == 0) + { + vfsType = VFSType::Batteries; + return batteriesVfs.resetToPath(path); + } + else + { + vfsType = VFSType::Disk; + std::string diskPath = path; + if (!diskPath.empty() && diskPath[0] == '@') + diskPath = diskPath.substr(1); + return fileVfs.resetToPath(diskPath); + } + } + + NavigationStatus toParent() + { + switch (vfsType) + { + case VFSType::Disk: + return fileVfs.toParent(); + case VFSType::Std: + return stdLibVfs.toParent(); + case VFSType::Lute: + return luteVfs.toParent(); + case VFSType::Batteries: + return batteriesVfs.toParent(); + } + return NavigationStatus::NotFound; + } + + NavigationStatus toChild(const std::string& component) + { + switch (vfsType) + { + case VFSType::Disk: + return fileVfs.toChild(component); + case VFSType::Std: + return stdLibVfs.toChild(component); + case VFSType::Lute: + return luteVfs.toChild(component); + case VFSType::Batteries: + return batteriesVfs.toChild(component); + } + return NavigationStatus::NotFound; + } + + std::string getIdentifier() const + { + switch (vfsType) + { + case VFSType::Disk: + return fileVfs.getAbsoluteFilePath(); + case VFSType::Std: + return stdLibVfs.getIdentifier(); + case VFSType::Lute: + return luteVfs.getIdentifier(); + case VFSType::Batteries: + return batteriesVfs.getIdentifier(); + } + return ""; + } + + std::optional readSource() const + { + std::string identifier = getIdentifier(); + if (identifier.empty()) + return std::nullopt; + + switch (vfsType) + { + case VFSType::Std: + return stdLibVfs.getContents(identifier); + case VFSType::Lute: + return luteVfs.getContents(identifier); + case VFSType::Batteries: + return batteriesVfs.getContents(identifier); + default: + return readFile(identifier); + } + } +}; + +class LuteTypeCheckContext : public Luau::Require::NavigationContext +{ +public: + LuteTypeCheckContext(std::string requirerChunkname) + : requirerChunkname(std::move(requirerChunkname)) + { + } + + NavigateResult resetToRequirer() override + { + return convert(vfs.resetToPath(requirerChunkname)); + } + + NavigateResult jumpToAlias(const std::string& path) override + { + return convert(vfs.jumpToAlias(path)); + } + + NavigateResult toParent() override + { + return convert(vfs.toParent()); + } + + NavigateResult toChild(const std::string& component) override + { + return convert(vfs.toChild(component)); + } + + NavigateResult toAliasOverride(const std::string& aliasUnprefixed) override + { + if (aliasUnprefixed == "std") + { + vfs.vfsType = LuteTypeCheckVfs::VFSType::Std; + return convert(vfs.stdLibVfs.resetToPath("@std")); + } + else if (aliasUnprefixed == "lute") + { + vfs.vfsType = LuteTypeCheckVfs::VFSType::Lute; + return convert(vfs.luteVfs.resetToPath("@lute")); + } + else if (aliasUnprefixed == "batteries" && vfs.vfsType != LuteTypeCheckVfs::VFSType::Disk) + { + vfs.vfsType = LuteTypeCheckVfs::VFSType::Batteries; + return convert(vfs.batteriesVfs.resetToPath("@batteries")); + } + return NC::NavigateResult::NotFound; + } + + ConfigStatus getConfigStatus() const override + { + if (vfs.vfsType == LuteTypeCheckVfs::VFSType::Disk) + return convert(vfs.fileVfs.getConfigStatus()); + return NC::ConfigStatus::Absent; + } + + ConfigBehavior getConfigBehavior() const override + { + return NC::ConfigBehavior::GetConfig; + } + + std::optional getAlias(const std::string& alias) const override + { + return std::nullopt; + } + + std::optional getConfig() const override + { + if (vfs.vfsType == LuteTypeCheckVfs::VFSType::Disk) + return vfs.fileVfs.getConfig(); + return std::nullopt; + } + + LuteTypeCheckVfs vfs; + std::string requirerChunkname; +}; + // ErrorCapturer class ErrorCapturer : public Luau::Require::ErrorHandler { @@ -159,6 +359,43 @@ std::optional resolveModule(std::string requirePath, std::string re return absolutePath; } +std::optional resolveForTypeCheck(std::string requirePath, std::string requirerChunkname, std::string* error) +{ + if (requirerChunkname.empty() || requirerChunkname[0] != '@') + { + if (error) + *error = "requirer chunkname must start with '@'"; + return std::nullopt; + } + + LuteTypeCheckContext context{requirerChunkname}; + ErrorCapturer errorCapturer{}; + + Luau::Require::Navigator navigator{context, errorCapturer}; + Luau::Require::Navigator::Status status = navigator.navigate(requirePath); + + if (status == Luau::Require::Navigator::Status::ErrorReported) + { + if (error && errorCapturer.error) + *error = *errorCapturer.error; + return std::nullopt; + } + + std::optional source = context.vfs.readSource(); + if (!source) + { + if (error) + *error = "failed to read source for '" + context.vfs.getIdentifier() + "'"; + return std::nullopt; + } + + ResolvedModule result; + result.path = context.vfs.getIdentifier(); + result.source = std::move(*source); + + return result; +} + int resolveModule_luau(lua_State* L) { std::string requirePath = luaL_checkstring(L, 1); diff --git a/lute/luau/src/tcmoduleresolver.cpp b/lute/luau/src/tcmoduleresolver.cpp index 0ada7889c..2fa10c0ec 100644 --- a/lute/luau/src/tcmoduleresolver.cpp +++ b/lute/luau/src/tcmoduleresolver.cpp @@ -10,31 +10,59 @@ namespace Luau std::optional LuteTypeCheckModuleResolver::readSource(const Luau::ModuleName& name) { + if (name == "-") + { + std::optional source = readStdin(); + if (!source) + return std::nullopt; + return Luau::SourceCode{*source, Luau::SourceCode::Script}; + } + + if (const std::string* source = sourceCache.find(name)) + return Luau::SourceCode{*source, Luau::SourceCode::Module}; + if (std::optional source = readFile(name)) return Luau::SourceCode{*source, Luau::SourceCode::Module}; + return std::nullopt; } // We are currently resolving modules and requires only, and will add support for Roblox globals / types in a subsequent PR. -std::optional LuteTypeCheckModuleResolver::resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const TypeCheckLimits& limits) +std::optional LuteTypeCheckModuleResolver::resolveModule( + const Luau::ModuleInfo* context, + Luau::AstExpr* node, + const TypeCheckLimits& limits +) { if (auto expr = node->as()) { std::string requirePath(expr->value.data, expr->value.size); std::string error; - std::string requirerChunkname = "@" + context->name; - std::optional absolutePath = ::resolveModule(requirePath, std::move(requirerChunkname), &error); - if (!absolutePath) + std::string requirerChunkname = context->name; + if (requirerChunkname.empty() || requirerChunkname[0] != '@') + requirerChunkname = "@" + requirerChunkname; + + std::optional resolved = ::resolveForTypeCheck(requirePath, std::move(requirerChunkname), &error); + if (!resolved) { printf("Failed to resolve require: %s\n", error.c_str()); return std::nullopt; } - return Luau::ModuleInfo{*absolutePath}; + sourceCache[resolved->path] = resolved->source; + + return Luau::ModuleInfo{resolved->path}; } return std::nullopt; } +std::string LuteTypeCheckModuleResolver::getHumanReadableModuleName(const Luau::ModuleName& name) const +{ + if (name == "-") + return "stdin"; + return name; +} + } // namespace Luau diff --git a/lute/require/src/lutevfs.cpp b/lute/require/src/lutevfs.cpp index d6d9e83fd..95a4280de 100644 --- a/lute/require/src/lutevfs.cpp +++ b/lute/require/src/lutevfs.cpp @@ -5,6 +5,7 @@ #include "lute/fs.h" #include "lute/io.h" #include "lute/luau.h" +#include "lute/lutemodules.h" #include "lute/modulepath.h" #include "lute/net.h" #include "lute/process.h" @@ -90,8 +91,10 @@ std::string LuteVfs::getIdentifier() const std::optional LuteVfs::getContents(const std::string& path) const { - // Lute modules have no source code. - return ""; + LuteModuleResult result = getLuteModule(path); + if (result.type == LuteModuleType::Module) + return std::string(result.contents); + return std::nullopt; } ConfigStatus LuteVfs::getConfigStatus() const diff --git a/tests/batteries/.luaurc b/tests/batteries/.luaurc new file mode 100644 index 000000000..7d30cae4d --- /dev/null +++ b/tests/batteries/.luaurc @@ -0,0 +1,6 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "../../batteries" + } +} diff --git a/tests/src/moduleresolver.test.cpp b/tests/src/moduleresolver.test.cpp index 5338b2af9..f74741bcc 100644 --- a/tests/src/moduleresolver.test.cpp +++ b/tests/src/moduleresolver.test.cpp @@ -1,7 +1,7 @@ // Simplified tests for module resolver functionality using public APIs. -#include "lute/tcmoduleresolver.h" - +#include "lute/common.h" #include "lute/resolvemodule.h" +#include "lute/tcmoduleresolver.h" #include "Luau/Ast.h" #include "Luau/FileUtils.h" @@ -22,4 +22,79 @@ TEST_CASE("moduleresolver_read_source") CHECK(source->source.find("require") != std::string::npos); } -// TODO: add tests for resolveModule +TEST_CASE("moduleresolver_resolve_for_typecheck") +{ + std::string root = getLuteProjectRootAbsolute(); + std::string mainLuau = "@" + joinPaths(root, "tests/src/resolver/mainmodule.luau"); + std::string error; + + SUBCASE("resolve_std") + { + auto resolved = resolveForTypeCheck("@std/process", mainLuau, &error); + REQUIRE(resolved); + CHECK(resolved->path == "@std/process.luau"); + } + + SUBCASE("resolve_batteries") + { + auto resolved = resolveForTypeCheck("@batteries/base64", mainLuau, &error); + if (resolved) + { + REQUIRE_FALSE_MESSAGE(false, "This shouldn't resolve successfully - you might need to delete the .luaurc and replace it with the .luaurc.ci"); + } + CHECK(!error.empty()); + CHECK(!resolved.has_value()); + } + + SUBCASE("resolve_self_in_std") + { + auto resolved = resolveForTypeCheck("@self/platform", "@std/system/init.luau", &error); + REQUIRE(resolved); + CHECK(resolved->path == "@std/system/platform.luau"); + } + + SUBCASE("resolve_lute_from_std") + { + auto resolved = resolveForTypeCheck("@lute/process", "@std/process.luau", &error); + REQUIRE(resolved); + CHECK(resolved->path == "@lute/process.luau"); + } + + SUBCASE("resolve_std_from_batteries") + { + auto resolved = resolveForTypeCheck("@std/process", "@batteries/base64.luau", &error); + REQUIRE(resolved); + CHECK(resolved->path == "@std/process.luau"); + } + + SUBCASE("resolve_batteries_from_std") + { + auto resolved = resolveForTypeCheck("@batteries/collections/deque", "@std/fs.luau", &error); + REQUIRE(resolved); + CHECK(resolved->path == "@batteries/collections/deque.luau"); + } + + SUBCASE("resolve_nonexistent_std") + { + auto resolved = resolveForTypeCheck("@std/does_not_exist", mainLuau, &error); + CHECK(!resolved); + } + + SUBCASE("resolve_self_from_userland") + { + // @self only works from within an existing library like @std or @batteries + auto resolved = resolveForTypeCheck("@self/platform", mainLuau, &error); + CHECK(!resolved); + } +} + +TEST_CASE("moduleresolver_typecheck_resolve") +{ + std::string root = getLuteProjectRootAbsolute(); + std::string mainLuau = "@" + joinPaths(root, "tests/src/resolver/mainmodule.luau"); + std::string error; + auto resolved = resolveForTypeCheck("@std/process", mainLuau, &error); + REQUIRE(resolved); + CHECK(resolved->path == "@std/process.luau"); + CHECK(resolved->source.find("processlib") != std::string::npos); +} diff --git a/tests/src/typecheck.test.cpp b/tests/src/typecheck.test.cpp index b9e0b3f47..ad08a2c2c 100644 --- a/tests/src/typecheck.test.cpp +++ b/tests/src/typecheck.test.cpp @@ -14,3 +14,21 @@ TEST_CASE_FIXTURE(LuteFixture, "typecheck_uses_new_solver") auto result = typecheck({testFilePath}, getReporter()); CHECK(result == 0); } + +TEST_CASE_FIXTURE(LuteFixture, "typecheck_all_builtins") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/typecheck/all_builtins.luau"); + + auto result = typecheck({testFilePath}, getReporter()); + CHECK(result == 0); +} + +TEST_CASE_FIXTURE(LuteFixture, "typecheck_user_code_doesnt_pass_with_batteries") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/typecheck/batteries_failure.luau"); + + auto result = typecheck({testFilePath}, getReporter()); + CHECK(result == 1); +} diff --git a/tests/src/typecheck/.luaurc b/tests/src/typecheck/.luaurc new file mode 100644 index 000000000..bb9d54789 --- /dev/null +++ b/tests/src/typecheck/.luaurc @@ -0,0 +1,3 @@ +{ + "languageMode": "strict" +} diff --git a/tests/src/typecheck/all_builtins.luau b/tests/src/typecheck/all_builtins.luau new file mode 100644 index 000000000..9eb89e8db --- /dev/null +++ b/tests/src/typecheck/all_builtins.luau @@ -0,0 +1,6 @@ +local _fs = require("@std/fs") +local _path = require("@std/path") +local _process = require("@std/process") +local _system = require("@std/system") +local _task = require("@std/task") +local _time = require("@std/time") diff --git a/tests/src/typecheck/batteries_failure.luau b/tests/src/typecheck/batteries_failure.luau new file mode 100644 index 000000000..ebcedeee7 --- /dev/null +++ b/tests/src/typecheck/batteries_failure.luau @@ -0,0 +1 @@ +local _b64 = require("@batteries/base64") From 0d36ec7189a2e7e941e7f567260767b49ccda991 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 27 Mar 2026 08:51:17 -0700 Subject: [PATCH 422/642] Adds a tutorial on how to write tests with lute (#890) This PR adds documentation that summarizes a) how to write tests, b) how to run tests, c) how to organize your tests using suites. I've also explicitly called out testing as part of the cool developer tooling we've built, and linked it in the CLI reference. The plan for the Developer Tooling section is to extend it with blurbs about `lute lint`, `lute check`, and `lute compile` (and any others that we add). --- docs/cli/test.md | 2 +- docs/guide/dev-tooling/index.md | 16 ++ docs/guide/hello-world.md | 14 +- docs/guide/index.md | 4 +- docs/guide/writing-tests/index.md | 279 ++++++++++++++++++++++++++++++ 5 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 docs/guide/dev-tooling/index.md create mode 100644 docs/guide/writing-tests/index.md diff --git a/docs/cli/test.md b/docs/cli/test.md index 0efe2c4d1..40570aad6 100755 --- a/docs/cli/test.md +++ b/docs/cli/test.md @@ -28,7 +28,7 @@ Run only test cases matching the specified name ## Arguments -Directories or files to search for tests (default: ./tests) +Directories or files to search for tests (default: ./) ## Examples diff --git a/docs/guide/dev-tooling/index.md b/docs/guide/dev-tooling/index.md new file mode 100644 index 000000000..31e33ac8b --- /dev/null +++ b/docs/guide/dev-tooling/index.md @@ -0,0 +1,16 @@ +--- +order: 6 +--- + +# Developer Tooling + +We've invested considerable effort in the developer tooling experience for users +writing Luau with Lute. This section summarizes some of the tools you may want +to consider using on a day to day basis. + +## [Testing](../../cli/test) + +`lute test` is a builtin utility for discovering and running tests. Tests are +written using the `@std/test` library in `.spec.luau` or `.test.luau` files, in +a `tests/` directory and `lute test` will handle discovering and running these +tests for you. diff --git a/docs/guide/hello-world.md b/docs/guide/hello-world.md index 6e35ffe19..8cfd43611 100644 --- a/docs/guide/hello-world.md +++ b/docs/guide/hello-world.md @@ -4,7 +4,8 @@ order: 3 # Hello World! -We're going to walk through the creation of a simple program using Lute. To start with, create a folder for your project with: +We're going to walk through the creation of a simple program using Lute. To +start with, create a folder for your project with: ```bash mkdir hello-world cd hello-world @@ -15,12 +16,16 @@ Create a file in this project with: touch main.luau ``` -Before we continue, let's ensure that we setup a good autocomplete experience for your favorite editor of choice. Run: +Before we continue, let's ensure that we setup a good autocomplete experience +for your favorite editor of choice. Run: ```bash lute setup ``` -This will add some files to a folder called `.lute/` in your home directory. These type definition files are then made available to `luau-lsp`, the most popular Luau Language Server, in order to provide high quality autocomplete and typechecking. +This will add some files to a folder called `.lute/` in your home directory. +These type definition files are then made available to `luau-lsp`, the most +popular Luau Language Server, in order to provide high quality autocomplete and +typechecking. Finally, open up the file `main.luau` and add: ```luau @@ -39,4 +44,5 @@ lute main.luau Note, that any Luau script can be run like this, not just a file named `main`. -In the next chapter, we're going to see how we can use some of the tools Lute provides to help you write some simple programs. +In the next chapter, we're going to see how we can use some of the tools Lute +provides to help you write some simple programs. diff --git a/docs/guide/index.md b/docs/guide/index.md index 3a59ba2a9..e22a4fb9d 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -6,4 +6,6 @@ order: 1 - [Installation](./installation) - [Hello World!](./hello-world) -- [Writing a Guessing Game](./writing-a-guessing-game) \ No newline at end of file +- [Writing a Guessing Game](./writing-a-guessing-game) +- [Writing Tests](./writing-tests/index) +- [Developer Tooling](./dev-tooling/index) diff --git a/docs/guide/writing-tests/index.md b/docs/guide/writing-tests/index.md new file mode 100644 index 000000000..e259c4990 --- /dev/null +++ b/docs/guide/writing-tests/index.md @@ -0,0 +1,279 @@ +--- +order: 5 +--- + +# Writing Tests + +As mentioned [here](../dev-tooling/index), `lute` features a builtin utility +for discovering and running tests. In this chapter, we'll see how you can write +tests against this framework and execute them. This can help you improve your +confidence in the correctness of your code. Specifically we'll look at the code +from the guessing game chapter, and test the code that handles argument parsing. + +### Setting up your project + +To get started, set up a project structure that has these files: +``` +project/ + utils.luau + args.test.luau +``` + +Inside of `utils.luau`, add the following code: +```luau +local function getArgs(args) : number + if #args == 2 then + error("Didn't pass enough arguments") + elseif #args \< 2 then + return 100 + else + if args[2] == "--max" then + local argument = tonumber(args[3]) + if argument then + return argument + end + end + end + return 100 +end + +return table.freeze({ getArgs = getArgs}) +``` + +This defines a module that exports a single frozen (i.e. immutable or read-only) table that has a +single function on it --- the `getArgs` function from the last chapter. + +This function (purportedly) parses a set of command line arguments and either +errors, returns a number if a `--max ` was passed, or 100. Let's try to test this! + + +### Using @std/test +First, open up `args.test.luau` in your favorite text editor. + +We will need to require `@std/test` as well as the code we are trying to test: + +```luau +local test = require("@std/test") +local utils = require("./utils") +``` + +Next up, a simple test case: +```luau +local test = require("@std/test") +local utils = require("./utils") + +test.case("maxOverridesValue", function(asserts) + -- What should happen here ? +end) +``` + +To start, let's write a simple test that asserts that when `getArgs` is invoked +with a `--max` argument, that it returns that value as a number. In order to +match the behaviour of the command line in our testing code, we'll want to +include an extra argument with the name of the script being called. + +```luau +local test = require("@std/test") +local utils = require("./utils") + +test.case("maxOverridesValue", function(asserts) + local fakeArgs = { "fakeScript.luau", "--max", "20"} + local result = utils.getArgs(fakeArgs) + + asserts.eq(20, result) +end) +``` + +:::info +Command line arguments are conventionally passed with the following +format: ` ` + +For example, when you run: +```bash +lute args.test.luau +``` +your shell will pass `lute`, `args.test.luau` as the arguments to `lute`. + +Following that same convention, `lute` will pass `args.test.luau`as well as the +remainder of the arguments as the first argument to the script being run. +::: + +### Running test cases +To run this test, from the root of your project directory run `lute test`. You +should see output like this: +```bash +────────────────────────────────────────────────── +Results: 1 passed, 0 failed of 1 +``` + +Excellent! Our first test passed, but we should write more. + +### Adding more tests + +In this next test, we'll add a test for the case where we don't pass a `--max` +argument: +```luau +local test = require("@std/test") +local utils = require("./utils") + +... + +test.case("noPassingMax", function(asserts) + local fakeArgs = { "fakeScript.luau"} + local result = utils.getArgs(fakeArgs) + + asserts.eq(100, result) +end) +``` + +Great! It looks like these tests pass too. Let's keep going! + +What happens if you pass `--max` without a corresponding number argument? If we +look at the implementation of the `getArgs` function, it looks like it raises an +exception, using the builtin Luau function `error`. Raising an exception is a +reasonable thing to do here, and we'll want to test that our program correctly +raises an exception when `--max` doesn't get passed a number. + +In order to assert this behaviour we'll need to use the `errors` assertion. + +```luau +local test = require("@std/test") +local utils = require("./utils") +... + +test.case("noArgToMax", function(asserts) + local fakeArgs = { "fakeScript.luau", "--max"} + assert.errors(function() + utils.getArgs(fakeArgs) + end) + +end) +``` +This assertion will run the code `utils.getArgs(fakeArgs)`. If it raises an +error, then the assertion will intercept it and succeed. If not, the assertion +will fail, which will be reported as a failed test case. + +### Using Test Suites to organize tests +While we're at it, we can also wrap all of these tests into a single test +suite, which will group these tests together. Test suites also allow you to use +lifecycle methods like `beforeeach`, `beforeall`, `aftereach`, and `afterall` to control setup and +tear down for tests. + +```luau +local test = require("@std/test") +local utils = require("./utils") + +test.suite("GetArgsTest", function(suite) + suite:case("maxOverridesValue", function(asserts) + local fakeArgs = { "fakeScript.luau", "--max", "20" } + local result = utils.getArgs(fakeArgs) + + asserts.eq(20, result) + end) + + suite:case("noPassingMax", function(asserts) + local fakeArgs = { "fakeScript.luau" } + local result = utils.getArgs(fakeArgs) + + asserts.eq(100, result) + end) + + suite:case("noArgToMax", function(asserts) + local fakeArgs = { "fakeScript.luau", "--max" } + asserts.errors(function() + utils.getArgs(fakeArgs) + end) + end) + +end) +``` + +Re-running the tests produces: +```bash +────────────────────────────────────────────────── +Results: 3 passed, 0 failed of 3 +``` + +:::info You can re-run individual or groups of tests using: +```bash +lute test -c caseName # run tests with the name caseName +lute test -s suiteName # run all tests with the name suiteName +lute test tests/path/to/.test.luau # run the tests in a particular file +``` +::: + +:::info +One situation where you might to use the aforementioned test suite lifecycle methods is when your tests operate on files on +files in a temporary directory. For example, testing that some code can create a +set of files. When subsequent tests execute, they may be operating in a +directory filled with files leftover from a previous test. + +In this situation, you could use the `beforeeach` method to execute some cleanup +of the temporary directory: + +```luau +local test = require("@std/test") +local fs = require("@std/fs") +local path = require("@std/path") +local system = require("@std/system") + +-- a temporary directory for tests to operate in +local testDir = path.join(system.tmpdir(), "test") + +test.suite("FileCreation", function() + test.beforeeach(function() + --Deletes the contents of the test directory before each test + fs.removedirectory(testDir, {recursive = true}) + --Recreates the directory so it exists for the next test to use it + fs.createdirectory(testDir) + end) +end) + +``` +::: + +### Fixing failed tests +So far, so good, let's take at what happens when a test case fails! Try this one out: +```luau +suite:case("unsupportedArgument", function(asserts) + local fakeArgs = { "fakeScript.luau", "--what", "foo"} + asserts.errors(function() + utils.getArgs(fakeArgs) + end) +end) +``` + +If you run this, you'll see that it doesn't throw, and `lute test` provides a +stacktrace showing what went wrong: +``` +Failures: + + FAIL getArgsTest.unsupportedArgument + .../args.test.luau:26 + errors: function: 0x000000012f859740 did not throw error. +``` + +This shows us a bug in our `getArgs` implementation - what if the user passes the +right number of arguments but supplies an unsupported option? In this case, the +fix is to error in the case that the second argument to `getArgs` isn't `--max`: +```luau +local function getArgs(args) : number + if #args == 2 then + error("Didn't pass enough arguments") + elseif #args \< 2 then + return 100 + else + if args[2] == "--max" then + local argument = tonumber(args[3]) + if argument then + return argument + end + else + error(`Expected flag --max, but got {args[2]}`) + end + end + return 100 +end +``` + +If you try to re-run the tests, they should all pass now. From 6ed3142c96d109a7ac94ec7c9a7d8dc86a56ac69 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:51:09 -0700 Subject: [PATCH 423/642] Adds `argnames` field in serialization of function Type (#907) When working on cleaning up `lute doc` with `typeofmodule` API integration, I noticed that function type serialization was missing the `argNames` fields, so we weren't preserving the argument names of the parameters in function types. This makes it hard for users (aka the `lute doc` command) to use the type serialization on function types because our output would be missing the argument names and only have the types, which isn't very useful to understand the function signature. Before this change, using the type serializer to generate docs would result in (the wip) `fs.link` serialization appearing as: ```luau fs.link (string, string) -> () ``` But after this change, we have: ```luau fs.link (src: string, dest: string) -> () ``` which better matches the actual function signature of of fs.link, which is ```luau function fs.link(src: string, dest: string): () ``` Pls see more examples of this specific change's additions (in my [WIP branch](https://github.com/luau-lang/lute/compare/annietang/lute_doc_integration_with_typeofmodule) of `lute doc`) if curious: https://github.com/luau-lang/lute/commit/94880094b613962d4aa24d566ff6953fc4318110 --- definitions/luau.luau | 1 + lute/luau/src/type.cpp | 15 +++++- lute/std/libs/luau.luau | 1 + tests/src/typeserializer.test.cpp | 79 ++++++++++++++++++++++++++++++- 4 files changed, 93 insertions(+), 3 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index ae5901885..b8a7d6c94 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -781,6 +781,7 @@ export type Type = { -- for function type parameters: TypePack, + argnames: { string? }, returns: TypePack, generics: { Type }, genericpacks: { TypePack }, diff --git a/lute/luau/src/type.cpp b/lute/luau/src/type.cpp index d6ea30bb3..d51c80429 100644 --- a/lute/luau/src/type.cpp +++ b/lute/luau/src/type.cpp @@ -287,13 +287,14 @@ struct TypeSerialize final : public Luau::TypeVisitor // Luau function type: // parameters: { head: {type}?, tail: type? }, + // argnames: { string? }, // returns: { head: {type}?, tail: type? }, // generics: {type}, // genericpacks: {typepack} void serialize(TypeId ty, const FunctionType& ftv) { checkStack(L, 3); // max 1 for table + 1 for subtable + 1 for traverse - lua_createtable(L, 0, 5); + lua_createtable(L, 0, 6); registerType(ty); pushTag("function"); @@ -302,6 +303,18 @@ struct TypeSerialize final : public Luau::TypeVisitor traverse(ftv.argTypes); lua_setfield(L, -2, "parameters"); + // ArgNames + lua_createtable(L, ftv.argNames.size(), 0); + for (size_t i = 0; i < ftv.argNames.size(); i++) + { + if (!ftv.argNames[i].has_value()) + lua_pushnil(L); + else + lua_pushstring(L, ftv.argNames[i]->name.c_str()); + lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "argnames"); + // Returns traverse(ftv.retTypes); lua_setfield(L, -2, "returns"); diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 54690c1b6..2450b43c0 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -33,6 +33,7 @@ function luau.loadModule(requirePath: path.Pathlike, env: { [any]: any }?): any end export type TypePack = luteLuau.TypePack +export type Type = luteLuau.Type function luau.typeofmodule(modulepath: path.Pathlike): TypePack? local modulePathStr = if typeof(modulepath) == "string" then modulepath else path.format(modulepath) diff --git a/tests/src/typeserializer.test.cpp b/tests/src/typeserializer.test.cpp index aefd660b3..d48492ddc 100644 --- a/tests/src/typeserializer.test.cpp +++ b/tests/src/typeserializer.test.cpp @@ -274,12 +274,15 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_function_type") TypePackId retTypes = arena.addTypePack(TypePack{{}, std::nullopt}); FunctionType ftv{{genericTy}, {}, argTypes, retTypes}; + + ftv.argNames.push_back(FunctionArgument{Name{"x"}}); + TypeId ty = arena.addType(ftv); lua_checkstack(L, 3); - // (number) -> () - // { tag: "function", parameters: { head: { { tag: "number" } }, tail: nil }, returns: { head: nil, tail: nil }, generics: { { tag: "generic", name: "T", ispack: false } }, genericpacks: {} } + // (x: number) -> () + // { tag: "function", parameters: { head: { { tag: "number" } }, tail: nil }, argnames: { "x" }, returns: { head: nil, tail: nil }, generics: { { tag: "generic", name: "T", ispack: false } }, genericpacks: {} } REQUIRE_EQ(Luau::serializeType(L, ty), 1); REQUIRE(lua_istable(L, -1)); @@ -298,6 +301,13 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_function_type") REQUIRE(lua_isnil(L, -1)); lua_pop(L, 2); // tail, parameters + lua_getfield(L, -1, "argnames"); + REQUIRE(lua_istable(L, -1)); + lua_rawgeti(L, -1, 1); + REQUIRE(lua_isstring(L, -1)); + CHECK(std::string(lua_tostring(L, -1)) == "x"); + lua_pop(L, 2); // argnames[0], argnames + lua_getfield(L, -1, "returns"); REQUIRE(lua_istable(L, -1)); lua_getfield(L, -1, "head"); @@ -320,6 +330,71 @@ TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_function_type") REQUIRE(lua_objlen(L, -1) == 0); // no generic packs } +TEST_CASE_FIXTURE(TypeSerializeFixture, "check_argnames_in_function_with_multiple_named_args") +{ + TypeId numTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + TypeId strTy = arena.addType(PrimitiveType{PrimitiveType::String}); + + TypePackId argTypes = arena.addTypePack(TypePack{{numTy, strTy}, std::nullopt}); + TypePackId retTypes = arena.addTypePack(TypePack{{}, std::nullopt}); + + FunctionType ftv{{}, {}, argTypes, retTypes}; + // (x: number, y: string) + ftv.argNames.push_back(FunctionArgument{Name{"x"}}); + ftv.argNames.push_back(FunctionArgument{Name{"y"}}); + + TypeId ty = arena.addType(ftv); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + lua_getfield(L, -1, "argnames"); + REQUIRE(lua_istable(L, -1)); + REQUIRE(lua_objlen(L, -1) == 2); + + lua_rawgeti(L, -1, 1); + CHECK(std::string(lua_tostring(L, -1)) == "x"); + lua_pop(L, 1); + + lua_rawgeti(L, -1, 2); + CHECK(std::string(lua_tostring(L, -1)) == "y"); + lua_pop(L, 2); // y, argnames +} + +TEST_CASE_FIXTURE(TypeSerializeFixture, "check_argnames_in_function_with_first_arg_unnamed_second_named") +{ + TypeId strTy = arena.addType(PrimitiveType{PrimitiveType::String}); + TypeId numTy = arena.addType(PrimitiveType{PrimitiveType::Number}); + + TypePackId argTypes = arena.addTypePack(TypePack{{strTy, numTy}, std::nullopt}); + TypePackId retTypes = arena.addTypePack(TypePack{{}, std::nullopt}); + + FunctionType ftv{{}, {}, argTypes, retTypes}; + + // First arg is unnamed, second is named "path" + ftv.argNames.push_back(std::nullopt); + ftv.argNames.push_back(FunctionArgument{Name{"path"}}); + + TypeId ty = arena.addType(ftv); + + lua_checkstack(L, 2); + REQUIRE_EQ(Luau::serializeType(L, ty), 1); + + lua_getfield(L, -1, "argnames"); + REQUIRE(lua_istable(L, -1)); + REQUIRE(lua_objlen(L, -1) == 2); // Two arguments, one named and one unnamed + + // Index 1 should be nil + lua_rawgeti(L, -1, 1); + CHECK(lua_isnil(L, -1)); + lua_pop(L, 1); + + // Index 2 should be "path" + lua_rawgeti(L, -1, 2); + CHECK(std::string(lua_tostring(L, -1)) == "path"); + lua_pop(L, 2); // path, argnames +} + TEST_CASE_FIXTURE(TypeSerializeFixture, "serialize_table_type_with_properties") { TypeId numberTy = arena.addType(PrimitiveType{PrimitiveType::Number}); From 4fef00f3c7ee2abf51fefcd966b7e5fc14ffac63 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:39:06 -0700 Subject: [PATCH 424/642] Renames `assert.errors` to `assert.throws` and `assert.erroreq` to `assert.throwsWith` (#905) following naming convention from [NUnit](https://docs.nunit.org/articles/nunit/writing-tests/assertions/classic-assertions/Assert.Throws.html) `assert.errors` renamed to `assert.throws<...T>: function, ...T` `assert.errorseq` renamed to `assert.throwsWith<...T> : (with : any, function, ...T)` --- examples/testing.luau | 4 +- lute/std/libs/test/assert.luau | 25 +- lute/std/libs/test/types.luau | 4 +- tests/batteries/base64.test.luau | 4 +- tests/batteries/collections/deque.test.luau | 8 +- tests/lute/crypto.test.luau | 22 +- tests/lute/task.test.luau | 4 +- tests/std/path/path.posix.test.luau | 4 +- tests/std/path/path.win32.test.luau | 10 +- tests/std/process.test.luau | 56 +-- .../snapshots/assert_error_eq_no_error.snap | 4 +- .../assert_error_eq_no_error.snap.luau | 6 +- .../assert_erroreq_error_message.snap | 4 +- .../assert_erroreq_error_message.snap.luau | 8 +- .../runtime_error_doesnt_report_xpcall.snap | 2 +- ...ntime_error_doesnt_report_xpcall_case.snap | 2 +- tests/std/syntax/parser.test.luau | 388 +++++++++--------- tests/std/test.test.luau | 16 +- 18 files changed, 288 insertions(+), 283 deletions(-) diff --git a/examples/testing.luau b/examples/testing.luau index 66e658cc2..9017ddeda 100644 --- a/examples/testing.luau +++ b/examples/testing.luau @@ -58,10 +58,10 @@ test.suite("MySuite", function(suite) end end - assert.errors(function() -- demonstrates success (callback throws err) + assert.throws(function() -- demonstrates success (callback throws err) foo(true) end) - assert.errors(function() -- demonstrates failure (callback doesn't throw) + assert.throws(function() -- demonstrates failure (callback doesn't throw) foo(false) end) end) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index a018f2f1b..eede1f7ff 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -87,14 +87,14 @@ local function that(value: unknown, msg: string?): Failure? return assertion(msg or `Expected a truthy value, but got {formatValue(value)}`) end -local function errors(callback: () -> (), msg: string?): Failure? - local success = pcall(callback) +local function throws(func: (T...) -> ...unknown, ...: T...): Failure? + local success = pcall(func, ...) -- if the callback failed, then this assert passes if not success then return nil end - return assertion(msg or `{callback} did not throw error.`) + return assertion(`{func} did not throw error.`) end -- Table equality assertionsion with deep comparison @@ -131,16 +131,21 @@ local function buffereq(lhs: buffer, rhs: buffer): Failure? return nil end -local function erroreq(func: (A...) -> ...unknown, expectedErrorMessage: string, ...: A...): Failure? +local function throwsWith(with: any, func: (T...) -> ...unknown, ...: T...): Failure? local success, err = pcall(func, ...) if success then - return assertion("Function did not error as expected.") + return assertion(`Expected function to throw/raise error.`) end local actualErrorMessage = tostring(err) - -- pcall appends filename and line number of the error, so we check suffix only - if not stringext.hassuffix(actualErrorMessage, expectedErrorMessage) then - return assertion(`Expected suffix of error message "{expectedErrorMessage}", but got "{actualErrorMessage}"`) + + if typeof(with) == "string" then + -- pcall appends filename and line number of the error, so we check suffix only + if not stringext.hassuffix(actualErrorMessage, with) then + return assertion(`Expected suffix of error message "{with}", but got "{actualErrorMessage}"`) + end + else + return eq(err, with, `Expected error "{with}", but got "{formatValue(err)}"`) end return nil @@ -191,10 +196,10 @@ end return table.freeze({ eq = reqassert(eq), neq = reqassert(neq), - errors = reqassert(errors), + throws = reqassert(throws), tableeq = reqassert(tableeq), buffereq = reqassert(buffereq), - erroreq = reqassert(erroreq), + throwsWith = reqassert(throwsWith), strcontains = reqassert(strcontains), strnotcontains = reqassert(strnotcontains), that = reqassert(that), diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index 6eee990e7..6b74ec4e0 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -29,12 +29,12 @@ export type Failure = export type Asserts = { eq: (T, T, string?) -> Failure?, neq: (T, T, string?) -> Failure?, - errors: (() -> (), string?) -> Failure?, + throws: ((T...) -> ...unknown, T...) -> Failure?, -- FIXME(Luau): We would like this to be two read-only indexers, but the -- best option we have otherwise is two error suppressing indexers. tableeq: ({ [any]: any }, { [any]: any }) -> Failure?, buffereq: (buffer, buffer) -> Failure?, - erroreq: ((A...) -> ...unknown, string, A...) -> Failure?, + throwsWith: (any, (T...) -> ...unknown, T...) -> Failure?, strcontains: (string, string, number?, string?) -> Failure?, strnotcontains: (string, string, string?) -> Failure?, that: (unknown, string?) -> Failure?, diff --git a/tests/batteries/base64.test.luau b/tests/batteries/base64.test.luau index 7ceefed25..652eae33a 100644 --- a/tests/batteries/base64.test.luau +++ b/tests/batteries/base64.test.luau @@ -58,7 +58,7 @@ test.suite("Base64", function(suite) for _, invalid in invalidInputs do suite:case("invalidDecode: '" .. invalid .. "'", function(assert) - assert.errors(function() + assert.throws(function() dec(invalid) end) end) @@ -66,7 +66,7 @@ test.suite("Base64", function(suite) for _, invalid in invalidLastBytes do suite:case("invalidDecodeLastBytes: '" .. invalid .. "'", function(assert) - assert.errors(function() + assert.throws(function() dec(invalid) end) end) diff --git a/tests/batteries/collections/deque.test.luau b/tests/batteries/collections/deque.test.luau index d43b0a587..d4af43917 100644 --- a/tests/batteries/collections/deque.test.luau +++ b/tests/batteries/collections/deque.test.luau @@ -64,12 +64,12 @@ test.suite("Deque", function(suite) suite:case("poppingFromEmptyDeque", function(assert) local deq = deque.new() - assert.erroreq(function() + assert.throwsWith("Popping from empty deque", function() deq:popback() - end, "Popping from empty deque") - assert.erroreq(function() + end) + assert.throwsWith("Popping from empty deque", function() deq:popfront() - end, "Popping from empty deque") + end) end) suite:case("e2e", function(assert) diff --git a/tests/lute/crypto.test.luau b/tests/lute/crypto.test.luau index 3b4f11f62..8dd4cdce9 100644 --- a/tests/lute/crypto.test.luau +++ b/tests/lute/crypto.test.luau @@ -44,7 +44,7 @@ test.suite("CryptoRuntimeTests", function(suite) local box = crypto.secretbox.seal(buf) buffer.writeu32(box.ciphertext, 2, 10_11_2025) - assert.errors(function() + assert.throws(function() crypto.secretbox.open(box) end) end) @@ -62,7 +62,7 @@ test.suite("CryptoRuntimeTests", function(suite) key = box.key, } - assert.errors(function() + assert.throws(function() -- We are testing that this fails when `ciphertext` is a string -- rather than a buffer, so we must lie to Luau. crypto.secretbox.open(other :: any) @@ -82,7 +82,7 @@ test.suite("CryptoRuntimeTests", function(suite) key = buffer.create(5), } - assert.errors(function() + assert.throws(function() crypto.secretbox.open(other) end) end) @@ -90,35 +90,35 @@ test.suite("CryptoRuntimeTests", function(suite) suite:case("secretboxOnIllTypedInputs", function(assert) local message = "I see no reason why gunpowder treason should ever be forgot." - assert.errors(function() + assert.throws(function() crypto.secretbox.seal({ message } :: any) end) - assert.errors(function() + assert.throws(function() crypto.secretbox.seal({ message, message, message } :: any) end) - assert.errors(function() + assert.throws(function() crypto.secretbox.seal({ ciphertext = message, key = message, nonce = message } :: any) end) - assert.errors(function() + assert.throws(function() crypto.secretbox.open(message :: any) end) - assert.errors(function() + assert.throws(function() crypto.secretbox.open({ ciphertext = message, key = buffer.create(0), nonce = "woof" } :: any) end) - assert.errors(function() + assert.throws(function() crypto.secretbox.open({ ciphertext = message, key = "woof", nonce = buffer.create(0) } :: any) end) - assert.errors(function() + assert.throws(function() crypto.secretbox.open({ ciphertext = message } :: any) end) - assert.errors(function() + assert.throws(function() crypto.secretbox.open(nil :: any) end) end) diff --git a/tests/lute/task.test.luau b/tests/lute/task.test.luau index 00e8fc162..20fb5b579 100644 --- a/tests/lute/task.test.luau +++ b/tests/lute/task.test.luau @@ -24,13 +24,13 @@ test.suite("LuteTaskSuite", function(suite) end) suite:case("taskSpawn0ArgsErrors", function(assert) - assert.errors(function() + assert.throws(function() (task.spawn :: any)() end) end) suite:case("taskDefer0ArgsErrors", function(assert) - assert.errors(function() + assert.throws(function() (task.defer :: any)() end) end) diff --git a/tests/std/path/path.posix.test.luau b/tests/std/path/path.posix.test.luau index a7b14a414..8053e9271 100644 --- a/tests/std/path/path.posix.test.luau +++ b/tests/std/path/path.posix.test.luau @@ -583,8 +583,8 @@ test.suite("PathPosixRelativeSuite", function(suite) suite:case("relativeDifferentAbsoluteValues", function(assert) local from = posix.parse("/home/user/documents") local to = posix.parse("home/user/file.txt") - assert.erroreq(function() + assert.throwsWith("Cannot compute relative path between absolute and relative paths", function() path.posix.relative(from, to) - end, "Cannot compute relative path between absolute and relative paths") + end) end) end) diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index 624357727..50fc2f34d 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -337,7 +337,7 @@ test.suite("PathWin32GetDriveSuite", function(suite) end) suite:case("getdriveErrorOnEmptyPath", function(assert) - assert.errors(function() + assert.throws(function() path.win32.drive("") end) end) @@ -783,17 +783,17 @@ test.suite("PathWin32RelativeSuite", function(suite) suite:case("relativeDifferentKinds", function(assert) local from = win32.parse("C:\\Users\\username\\Documents") local to = win32.parse("Documents\\file.txt") - assert.erroreq(function() + assert.throwsWith("Cannot compute relative path between different kinds of paths", function() path.win32.relative(from, to) - end, "Cannot compute relative path between different kinds of paths") + end) end) suite:case("relativeDifferentDrives", function(assert) local from = win32.parse("C:\\Users\\username") local to = win32.parse("D:\\Documents\\file.txt") - assert.erroreq(function() + assert.throwsWith("Cannot compute relative path between different drives", function() path.win32.relative(from, to) - end, "Cannot compute relative path between different drives") + end) end) suite:case("relativeUncPaths", function(assert) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 976551dff..926ab7922 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -81,38 +81,38 @@ test.suite("ProcessSuite", function(suite) end) suite:case("processRunOptionsErrorCases", function(assert) - assert.erroreq(function() + assert.throwsWith("process.run requires a non-empty table of arguments", function() process.run({}) - end, "process.run requires a non-empty table of arguments") + end) -- All of these tests intentionally pass values of the incorrect -- type to `process.run`: this is to ensure that the error -- handling on the runtime side is correct (for example, does not -- crash because we expected a table and got a string). - assert.erroreq(function() + assert.throwsWith("invalid argument #-1 to 'run' (string expected, got table)", function() process.run({ "echo", "hello" }, { cwd = {} :: any }) - end, "invalid argument #-1 to 'run' (string expected, got table)") + end) - assert.erroreq(function() + assert.throwsWith("invalid argument #-1 to 'run' (string expected, got table)", function() process.run({ "echo", "hello" }, { stdio = {} :: any }) - end, "invalid argument #-1 to 'run' (string expected, got table)") + end) - assert.erroreq(function() + assert.throwsWith("process option 'env' must be a table", function() process.run({ "echo", "hello" }, { env = "not-a-table" :: any }) - end, "process option 'env' must be a table") + end) - assert.erroreq(function() + assert.throwsWith("process options must be a table", function() process.run({ "echo", "hello" }, "invalid_option" :: any) - end, "process options must be a table") + end) - assert.erroreq(function() + assert.throwsWith("process options must be a table", function() process.run({ "echo", "hello" }, "not-a-table" :: any) - end, "process options must be a table") + end) - assert.erroreq(function() + assert.throwsWith("process.run requires a non-empty table of arguments", function() process.run({ huh = 42 }) - end, "process.run requires a non-empty table of arguments") + end) end) suite:case("processRunArraylikeTable", function(assert) @@ -127,31 +127,31 @@ test.suite("ProcessSuite", function(suite) -- handling on the runtime side is correct (for example, does not -- crash because we expected a table and got a string). - assert.erroreq(function() + assert.throwsWith("invalid argument #1 to 'system' (string expected, got nil)", function() (process.system :: any)() - end, "invalid argument #1 to 'system' (string expected, got nil)") + end) - assert.erroreq(function() + assert.throwsWith("invalid argument #1 to 'system' (string expected, got table)", function() process.system({} :: any) - end, "invalid argument #1 to 'system' (string expected, got table)") + end) - assert.erroreq(function() + assert.throwsWith("invalid argument #-1 to 'system' (string expected, got table)", function() process.system("echo hello", { system = {} :: any }) - end, "invalid argument #-1 to 'system' (string expected, got table)") + end) - assert.erroreq(function() + assert.throwsWith("invalid argument #-1 to 'system' (string expected, got table)", function() process.system("echo hello", { cwd = {} :: any }) - end, "invalid argument #-1 to 'system' (string expected, got table)") - assert.erroreq(function() + end) + assert.throwsWith("invalid argument #-1 to 'system' (string expected, got table)", function() process.system("echo hello", { stdio = {} :: any }) - end, "invalid argument #-1 to 'system' (string expected, got table)") + end) - assert.erroreq(function() + assert.throwsWith("process option 'env' must be a table", function() process.system("echo hello", { env = "not-a-table" :: any }) - end, "process option 'env' must be a table") + end) - assert.erroreq(function() + assert.throwsWith("process options must be a table", function() process.system("echo hello", "invalid_option" :: any) - end, "process options must be a table") + end) end) end) diff --git a/tests/std/snapshots/assert_error_eq_no_error.snap b/tests/std/snapshots/assert_error_eq_no_error.snap index 4f5c7019a..1344ae416 100755 --- a/tests/std/snapshots/assert_error_eq_no_error.snap +++ b/tests/std/snapshots/assert_error_eq_no_error.snap @@ -5,9 +5,9 @@ TEST RESULTS Failed Tests (1): -❌ erroreq_error +❌ throwsWithError /tests/std/snapshots/assert_error_eq_no_error.snap.luau:4 - erroreq: Function did not error as expected. + throwsWith: Function did not error as expected. -------------------------------------------------- Total: 1 diff --git a/tests/std/snapshots/assert_error_eq_no_error.snap.luau b/tests/std/snapshots/assert_error_eq_no_error.snap.luau index e823a2404..4c2439f35 100644 --- a/tests/std/snapshots/assert_error_eq_no_error.snap.luau +++ b/tests/std/snapshots/assert_error_eq_no_error.snap.luau @@ -1,9 +1,9 @@ local test = require("@std/test") -test.case("erroreq_error", function(assert) - assert.erroreq(function() +test.case("throwsWithError", function(assert) + assert.throwsWith("expected message", function() return - end, "expected message") + end) end) test.run() diff --git a/tests/std/snapshots/assert_erroreq_error_message.snap b/tests/std/snapshots/assert_erroreq_error_message.snap index 5e6d9b8ec..438b63fb1 100755 --- a/tests/std/snapshots/assert_erroreq_error_message.snap +++ b/tests/std/snapshots/assert_erroreq_error_message.snap @@ -5,9 +5,9 @@ TEST RESULTS Failed Tests (1): -❌ erroreq_failure_suite.erroreq_error +❌ throwsWith_failure_suite.throwsWith_error /tests/std/snapshots/assert_erroreq_error_message.snap.luau:5 - erroreq: Expected suffix of error message "expected message", but got "/tests/std/snapshots/assert_erroreq_error_message.snap.luau:6: wrong message" + throwsWith: Expected suffix of error message "expected message", but got "/tests/std/snapshots/assert_erroreq_error_message.snap.luau:6: wrong message" -------------------------------------------------- Total: 1 diff --git a/tests/std/snapshots/assert_erroreq_error_message.snap.luau b/tests/std/snapshots/assert_erroreq_error_message.snap.luau index f70606c46..89a148918 100644 --- a/tests/std/snapshots/assert_erroreq_error_message.snap.luau +++ b/tests/std/snapshots/assert_erroreq_error_message.snap.luau @@ -1,10 +1,10 @@ local test = require("@std/test") -test.suite("erroreq_failure_suite", function(suite) - suite:case("erroreq_error", function(assert) - assert.erroreq(function() +test.suite("throwsWith_failure_suite", function(suite) + suite:case("throwsWith_error", function(assert) + assert.throwsWith("expected message", function() error("wrong message") - end, "expected message") + end) end) end) diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap index 7609a2b89..11325881a 100755 --- a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap @@ -14,7 +14,7 @@ Stacktrace: /tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6 @std/test/runner.luau:130 function run @std/test/init.luau:103 function run -/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:10 +/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:11 -------------------------------------------------- diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap index b70a72f72..27efa663a 100755 --- a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap @@ -14,7 +14,7 @@ Stacktrace: /tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5 @std/test/runner.luau:93 function run @std/test/init.luau:103 function run -/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:8 +/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:9 -------------------------------------------------- diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index b78f04c97..d7b08d9b5 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -1251,57 +1251,57 @@ test.suite("AST_nodes_frozen", function(suite) local expr = parser.parseexpr("(x)") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "group") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).tag = "modified" - end, frozenTableMessage) + end) end) suite:case("astExprConstantNilFrozen", function(assert) local expr = parser.parseexpr("nil") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "nil") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).tag = "modified" - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (expr :: any).token.text = "blah" - end, frozenTableMessage) + end) end) suite:case("astExprConstantBoolFrozen", function(assert) local expr = parser.parseexpr("true") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "boolean") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).tag = "modified" - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (expr :: any).token.text = "blah" - end, frozenTableMessage) + end) end) suite:case("astExprConstantNumberFrozen", function(assert) local expr = parser.parseexpr("42") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "number") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).tag = "modified" - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (expr :: any).token.text = "blah" - end, frozenTableMessage) + end) end) suite:case("astExprConstantStringFrozen", function(assert) local expr = parser.parseexpr("'hello'") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "string") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).tag = "modified" - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (expr :: any).token.text = "blah" - end, frozenTableMessage) + end) end) suite:case("astExprLocalFrozen", function(assert) @@ -1310,39 +1310,39 @@ test.suite("AST_nodes_frozen", function(suite) local expr = returnStat.expressions[1].node assert.eq(expr.kind, "expr") assert.eq(expr.tag, "local") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).upvalue = true - end, frozenTableMessage) + end) end) suite:case("astExprGlobalFrozen", function(assert) local expr = parser.parseexpr("_G") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "global") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).name = nil - end, frozenTableMessage) + end) end) suite:case("astExprVarargsFrozen", function(assert) local expr = parser.parseexpr("...") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "vararg") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).tag = "modified" - end, frozenTableMessage) + end) end) suite:case("astExprCallFrozen", function(assert) local expr = parser.parseexpr("foo(1, 2)") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "call") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).func = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).arguments, nil :: any) - end, frozenTableMessage) + end) end) suite:case("astExprInstantiateFrozen", function(assert) @@ -1351,129 +1351,129 @@ test.suite("AST_nodes_frozen", function(suite) local expr = (callExpr :: syntax.AstExprCall).func assert.eq(expr.kind, "expr") assert.eq(expr.tag, "instantiate") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).expr = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).typearguments, nil :: any) - end, frozenTableMessage) + end) end) suite:case("astExprIndexNameFrozen", function(assert) local expr = parser.parseexpr("obj.field") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "indexname") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).expression = nil - end, frozenTableMessage) + end) end) suite:case("astExprIndexExprFrozen", function(assert) local expr = parser.parseexpr("obj[key]") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "index") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).expression = nil - end, frozenTableMessage) + end) end) suite:case("astExprFunctionFrozen", function(assert) local expr = parser.parseexpr("@checked function(x, y) return x + y end") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "function") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).body = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).parameters, nil :: any) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).attributes, nil :: any) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() ((expr :: syntax.AstExprFunction).attributes[1] :: any).token.text = "@modified" - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).generics, nil :: any) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).genericpacks, nil :: any) - end, frozenTableMessage) + end) end) suite:case("astExprTableFrozen", function(assert) local expr = parser.parseexpr("{ a = 1, [2] = 3, 4 }") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "table") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).openbrace = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).entries, nil :: any) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (expr :: any).entries[1].kind = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (expr :: any).entries[2].kind = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (expr :: any).entries[3].kind = nil - end, frozenTableMessage) + end) end) suite:case("astExprUnaryFrozen", function(assert) local expr = parser.parseexpr("-x") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "unary") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).operand = nil - end, frozenTableMessage) + end) end) suite:case("astExprBinaryFrozen", function(assert) local expr = parser.parseexpr("x + y") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "binary") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).lhsoperand = nil - end, frozenTableMessage) + end) end) suite:case("astExprInterpStringFrozen", function(assert) local expr = parser.parseexpr("`hello {x}`") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "interpolatedstring") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).strings, nil :: any) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).expressions, nil :: any) - end, frozenTableMessage) + end) end) suite:case("astExprTypeAssertionFrozen", function(assert) local expr = parser.parseexpr("x :: number") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "cast") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).operand = nil - end, frozenTableMessage) + end) end) suite:case("astExprIfElseFrozen", function(assert) local expr = parser.parseexpr("if x then 1 elseif y then 2 else 3") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "conditional") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (expr :: any).condition = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).elseifs, nil :: any) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (expr :: any).elseifs[1].condition = nil - end, frozenTableMessage) + end) end) -- Statement nodes @@ -1481,12 +1481,12 @@ test.suite("AST_nodes_frozen", function(suite) local block = parser.parseblock("local x = 1\nlocal y = 2") assert.eq(block.kind, "stat") assert.eq(block.tag, "block") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() table.insert((block :: any).statements, nil :: any) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (block :: any).tag = nil - end, frozenTableMessage) + end) end) suite:case("astStatDoFrozen", function(assert) @@ -1494,9 +1494,9 @@ test.suite("AST_nodes_frozen", function(suite) local doStat = block.statements[1] :: syntax.AstStatDo assert.eq(doStat.kind, "stat") assert.eq(doStat.tag, "do") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (doStat :: any).body = nil - end, frozenTableMessage) + end) end) suite:case("astStatIfFrozen", function(assert) @@ -1504,15 +1504,15 @@ test.suite("AST_nodes_frozen", function(suite) local ifStat = block.statements[1] :: syntax.AstStatIf assert.eq(ifStat.kind, "stat") assert.eq(ifStat.tag, "conditional") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (ifStat :: any).condition = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((ifStat :: any).elseifs, nil :: any) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (ifStat :: any).elseifs[1].condition = nil - end, frozenTableMessage) + end) end) suite:case("astStatWhileFrozen", function(assert) @@ -1520,9 +1520,9 @@ test.suite("AST_nodes_frozen", function(suite) local whileStat = block.statements[1] :: syntax.AstStatWhile assert.eq(whileStat.kind, "stat") assert.eq(whileStat.tag, "while") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (whileStat :: any).condition = nil - end, frozenTableMessage) + end) end) suite:case("astStatRepeatFrozen", function(assert) @@ -1530,9 +1530,9 @@ test.suite("AST_nodes_frozen", function(suite) local repeatStat = block.statements[1] :: syntax.AstStatRepeat assert.eq(repeatStat.kind, "stat") assert.eq(repeatStat.tag, "repeat") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (repeatStat :: any).body = nil - end, frozenTableMessage) + end) end) suite:case("astStatBreakFrozen", function(assert) @@ -1541,9 +1541,9 @@ test.suite("AST_nodes_frozen", function(suite) local breakStat = whileStat.body.statements[1] :: syntax.AstStatBreak assert.eq(breakStat.kind, "stat") assert.eq(breakStat.tag, "break") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (breakStat :: any).tag = "modified" - end, frozenTableMessage) + end) end) suite:case("astStatContinueFrozen", function(assert) @@ -1552,9 +1552,9 @@ test.suite("AST_nodes_frozen", function(suite) local continueStat = whileStat.body.statements[1] :: syntax.AstStatContinue assert.eq(continueStat.kind, "stat") assert.eq(continueStat.tag, "continue") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (continueStat :: any).tag = "modified" - end, frozenTableMessage) + end) end) suite:case("astStatReturnFrozen", function(assert) @@ -1562,12 +1562,12 @@ test.suite("AST_nodes_frozen", function(suite) local returnStat = block.statements[1] :: syntax.AstStatReturn assert.eq(returnStat.kind, "stat") assert.eq(returnStat.tag, "return") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (returnStat :: any).returnkeyword = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((returnStat :: any).expressions, nil) - end, frozenTableMessage) + end) end) suite:case("astStatExprFrozen", function(assert) @@ -1575,9 +1575,9 @@ test.suite("AST_nodes_frozen", function(suite) local exprStat = block.statements[1] :: syntax.AstStatExpr assert.eq(exprStat.kind, "stat") assert.eq(exprStat.tag, "expression") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (exprStat :: any).expression = nil - end, frozenTableMessage) + end) end) suite:case("astStatLocalFrozen", function(assert) @@ -1585,15 +1585,15 @@ test.suite("AST_nodes_frozen", function(suite) local localStat = block.statements[1] :: syntax.AstStatLocal assert.eq(localStat.kind, "stat") assert.eq(localStat.tag, "local") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (localStat :: any).localkeyword = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((localStat :: any).variables, nil) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((localStat :: any).values, nil) - end, frozenTableMessage) + end) end) suite:case("astStatForFrozen", function(assert) @@ -1601,9 +1601,9 @@ test.suite("AST_nodes_frozen", function(suite) local forStat = block.statements[1] :: syntax.AstStatFor assert.eq(forStat.kind, "stat") assert.eq(forStat.tag, "for") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (forStat :: any).variable = nil - end, frozenTableMessage) + end) end) suite:case("astStatForInFrozen", function(assert) @@ -1611,15 +1611,15 @@ test.suite("AST_nodes_frozen", function(suite) local forInStat = block.statements[1] :: syntax.AstStatForIn assert.eq(forInStat.kind, "stat") assert.eq(forInStat.tag, "forin") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (forInStat :: any).inkeyword = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((forInStat :: any).variables, nil) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((forInStat :: any).values, nil) - end, frozenTableMessage) + end) end) suite:case("astStatAssignFrozen", function(assert) @@ -1627,15 +1627,15 @@ test.suite("AST_nodes_frozen", function(suite) local assignStat = block.statements[1] :: syntax.AstStatAssign assert.eq(assignStat.kind, "stat") assert.eq(assignStat.tag, "assign") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (assignStat :: any).equals = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((assignStat :: any).variables, nil) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((assignStat :: any).values, nil) - end, frozenTableMessage) + end) end) suite:case("astStatCompoundAssignFrozen", function(assert) @@ -1643,9 +1643,9 @@ test.suite("AST_nodes_frozen", function(suite) local compoundStat = block.statements[1] :: syntax.AstStatCompoundAssign assert.eq(compoundStat.kind, "stat") assert.eq(compoundStat.tag, "compoundassign") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (compoundStat :: any).variable = nil - end, frozenTableMessage) + end) end) suite:case("astStatFunctionFrozen", function(assert) @@ -1653,9 +1653,9 @@ test.suite("AST_nodes_frozen", function(suite) local funcStat = block.statements[1] :: syntax.AstStatFunction assert.eq(funcStat.kind, "stat") assert.eq(funcStat.tag, "function") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (funcStat :: any).name = nil - end, frozenTableMessage) + end) end) suite:case("astStatLocalFunctionFrozen", function(assert) @@ -1663,9 +1663,9 @@ test.suite("AST_nodes_frozen", function(suite) local localFuncStat = block.statements[1] :: syntax.AstStatLocalFunction assert.eq(localFuncStat.kind, "stat") assert.eq(localFuncStat.tag, "localfunction") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (localFuncStat :: any).name = nil - end, frozenTableMessage) + end) end) suite:case("astStatTypeAliasFrozen", function(assert) @@ -1673,9 +1673,9 @@ test.suite("AST_nodes_frozen", function(suite) local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.kind, "stat") assert.eq(typeAlias.tag, "typealias") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (typeAlias :: any).name = nil - end, frozenTableMessage) + end) end) suite:case("astStatTypeFunctionFrozen", function(assert) @@ -1683,9 +1683,9 @@ test.suite("AST_nodes_frozen", function(suite) local typeFunc = block.statements[1] :: syntax.AstStatTypeFunction assert.eq(typeFunc.kind, "stat") assert.eq(typeFunc.tag, "typefunction") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (typeFunc :: any).name = nil - end, frozenTableMessage) + end) end) -- Type nodes @@ -1695,9 +1695,9 @@ test.suite("AST_nodes_frozen", function(suite) local typeRef = typeAlias.type :: syntax.AstTypeReference assert.eq(typeRef.kind, "type") assert.eq(typeRef.tag, "reference") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (typeRef :: any).name = nil - end, frozenTableMessage) + end) end) suite:case("astTypeSingletonBoolFrozen", function(assert) @@ -1706,9 +1706,9 @@ test.suite("AST_nodes_frozen", function(suite) local singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool assert.eq(singletonBool.kind, "type") assert.eq(singletonBool.tag, "boolean") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (singletonBool :: any).value = false - end, frozenTableMessage) + end) end) suite:case("astTypeSingletonStringFrozen", function(assert) @@ -1717,9 +1717,9 @@ test.suite("AST_nodes_frozen", function(suite) local singletonStr = typeAlias.type :: syntax.AstTypeSingletonString assert.eq(singletonStr.kind, "type") assert.eq(singletonStr.tag, "string") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (singletonStr :: any).quotestyle = "double" - end, frozenTableMessage) + end) end) suite:case("astTypeTypeofFrozen", function(assert) @@ -1728,9 +1728,9 @@ test.suite("AST_nodes_frozen", function(suite) local typeofType = typeAlias.type :: syntax.AstTypeTypeof assert.eq(typeofType.kind, "type") assert.eq(typeofType.tag, "typeof") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (typeofType :: any).expression = nil - end, frozenTableMessage) + end) end) suite:case("astTypeGroupFrozen", function(assert) @@ -1739,9 +1739,9 @@ test.suite("AST_nodes_frozen", function(suite) local groupType = typeAlias.type :: syntax.AstTypeGroup assert.eq(groupType.kind, "type") assert.eq(groupType.tag, "group") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (groupType :: any).type = nil - end, frozenTableMessage) + end) end) suite:case("astTypeOptionalFrozen", function(assert) @@ -1751,9 +1751,9 @@ test.suite("AST_nodes_frozen", function(suite) local optionalType = unionType.types[2].node :: syntax.AstTypeOptional assert.eq(optionalType.kind, "type") assert.eq(optionalType.tag, "optional") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (optionalType :: any).kind = "modified" - end, frozenTableMessage) + end) end) suite:case("astTypeUnionFrozen", function(assert) @@ -1762,12 +1762,12 @@ test.suite("AST_nodes_frozen", function(suite) local unionType = typeAlias.type :: syntax.AstTypeUnion assert.eq(unionType.kind, "type") assert.eq(unionType.tag, "union") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (unionType :: any).leading = "modified" - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((unionType :: any).types, nil) - end, frozenTableMessage) + end) end) suite:case("astTypeIntersectionFrozen", function(assert) @@ -1776,12 +1776,12 @@ test.suite("AST_nodes_frozen", function(suite) local intersectionType = typeAlias.type :: syntax.AstTypeIntersection assert.eq(intersectionType.kind, "type") assert.eq(intersectionType.tag, "intersection") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (intersectionType :: any).leading = "modified" - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((intersectionType :: any).types, nil) - end, frozenTableMessage) + end) end) suite:case("astTypeArrayFrozen", function(assert) @@ -1790,9 +1790,9 @@ test.suite("AST_nodes_frozen", function(suite) local arrayType = typeAlias.type :: syntax.AstTypeArray assert.eq(arrayType.kind, "type") assert.eq(arrayType.tag, "array") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (arrayType :: any).type = nil - end, frozenTableMessage) + end) end) suite:case("astTypeTableFrozen", function(assert) @@ -1801,21 +1801,21 @@ test.suite("AST_nodes_frozen", function(suite) local tableType = typeAlias.type :: syntax.AstTypeTable assert.eq(tableType.kind, "type") assert.eq(tableType.tag, "table") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (tableType :: any).openbrace = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((tableType :: any).entries, nil) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (tableType :: any).entries[1].kind = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (tableType :: any).entries[2].kind = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (tableType :: any).entries[3].kind = nil - end, frozenTableMessage) + end) end) suite:case("astTypeFunctionFrozen", function(assert) @@ -1824,12 +1824,12 @@ test.suite("AST_nodes_frozen", function(suite) local funcType = typeAlias.type :: syntax.AstTypeFunction assert.eq(funcType.kind, "type") assert.eq(funcType.tag, "function") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (funcType :: any).returnspecifier = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (funcType :: any).parameters[1].node.name = nil - end, frozenTableMessage) + end) end) suite:case("astTypePackExplicitFrozen", function(assert) @@ -1839,9 +1839,9 @@ test.suite("AST_nodes_frozen", function(suite) local typePack = funcType.returntypes :: syntax.AstTypePackExplicit assert.eq(typePack.kind, "typepack") assert.eq(typePack.tag, "explicit") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (typePack :: any).openparens = nil - end, frozenTableMessage) + end) end) suite:case("astTypePackVariadicFrozen", function(assert) @@ -1851,9 +1851,9 @@ test.suite("AST_nodes_frozen", function(suite) local typePack = funcType.returntypes :: syntax.AstTypePackVariadic assert.eq(typePack.kind, "typepack") assert.eq(typePack.tag, "variadic") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (typePack :: any).type = nil - end, frozenTableMessage) + end) end) -- Other node types @@ -1862,9 +1862,9 @@ test.suite("AST_nodes_frozen", function(suite) local localStat = block.statements[1] :: syntax.AstStatLocal local local_ = localStat.variables[1].node assert.eq(local_.kind, "local") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (local_ :: any).name = nil - end, frozenTableMessage) + end) end) suite:case("astGenericTypeFrozen", function(assert) @@ -1872,9 +1872,9 @@ test.suite("AST_nodes_frozen", function(suite) local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local generic = (typeAlias.generics :: any)[1].node assert.eq(generic.tag, "generic") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() generic.name = nil - end, frozenTableMessage) + end) end) suite:case("astGenericTypePackFrozen", function(assert) @@ -1883,9 +1883,9 @@ test.suite("AST_nodes_frozen", function(suite) local genericPack = (typeAlias.genericpacks :: any)[1].node assert.eq(genericPack.tag, "generic") assert.eq(genericPack.kind, "typepack") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() genericPack.name = nil - end, frozenTableMessage) + end) end) -- Token type @@ -1894,15 +1894,15 @@ test.suite("AST_nodes_frozen", function(suite) local localStat = block.statements[1] :: syntax.AstStatLocal local token = localStat.localkeyword assert.eq(token.kind, "token") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (token :: any).text = "modified" - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((token :: any).leadingtrivia, nil) - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((token :: any).trailingtrivia, nil) - end, frozenTableMessage) + end) end) -- Trivia types @@ -1918,21 +1918,21 @@ test.suite("AST_nodes_frozen", function(suite) local localStat = block.statements[1] :: syntax.AstStatLocal local trivias = localStat.localkeyword.leadingtrivia for _, trivia in trivias do - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (trivia :: any).text = "modified" - end, frozenTableMessage) + end) end end) -- ParseResult type suite:case("parseResultFrozen", function(assert) local result = parser.parse("local x = 1") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (result :: any).root = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() table.insert((result :: any).lineoffsets, nil) - end, frozenTableMessage) + end) end) -- Pair type (used in Punctuated) @@ -1940,12 +1940,12 @@ test.suite("AST_nodes_frozen", function(suite) local block = parser.parseblock("local x, y = 1, 2") local localStat = block.statements[1] :: syntax.AstStatLocal local pair = localStat.variables[1] - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (pair :: any).node = nil - end, frozenTableMessage) - assert.erroreq(function() + end) + assert.throwsWith(frozenTableMessage, function() (pair :: any).separator = nil - end, frozenTableMessage) + end) end) -- Eof type @@ -1954,8 +1954,8 @@ test.suite("AST_nodes_frozen", function(suite) local eof = result.eof assert.eq(eof.tag, "eof") assert.eq(eof.kind, "token") - assert.erroreq(function() + assert.throwsWith(frozenTableMessage, function() (eof :: any).tag = "modified" - end, frozenTableMessage) + end) end) end) diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index bd5e0583e..a4135f9fc 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -247,32 +247,32 @@ test.run() ) end) - suite:case("assert.erroreqNoError", function(assert) + suite:case("assert.throwsWithNoError", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") -test.case("erroreqError", function(assert) - assert.erroreq(function() return end, "expected message") +test.case("throwsWithError", function(assert) + assert.throwsWith("expected message", function() return end) end) test.run() ]], - "Function did not error as expected.", + "Expected function to throw/raise error.", assert ) end) - suite:case("assert.erroreqErrorMessage", function(assert) + suite:case("assert.throwsWithErrorMessage", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") -test.suite("erroreq_failure_suite", function(suite) - suite:case("erroreqError", function(assert) - assert.erroreq(function() error("wrong message") end, "expected message") +test.suite("throwsWith_failure_suite", function(suite) + suite:case("throwsWithError", function(assert) + assert.throwsWith("expected message", function() error("wrong message") end) end) end) From 59b2f35bf121b7521dcae910c44dd042aac4818c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:38:36 +0000 Subject: [PATCH 425/642] Update Luau to 0.714 (#909) **Luau**: Updated from `0.713` to `0.714` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.714 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Sora Kanosue --- extern/luau.tune | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index df0b8427e..9ab91f9ad 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.713" -revision = "b37af212cb60366043c93c5a4acd085ab29c8d7a" +branch = "0.714" +revision = "27718747a24448ea6418f424963c741f57263743" From 1155970d5c62e1c5833da32474a66d8145f7a254 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:39:15 +0000 Subject: [PATCH 426/642] Update Lute to 0.1.0-nightly.20260327 (#910) **Lute**: Updated from `0.1.0-nightly.20260320` to `0.1.0-nightly.20260327` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260327 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 7adc35dc4..049e7f3f6 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.4.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260320" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260327" } diff --git a/rokit.toml b/rokit.toml index ea9b44a6b..9b01eb0d8 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.4.0" -lute = "luau-lang/lute@0.1.0-nightly.20260320" +lute = "luau-lang/lute@0.1.0-nightly.20260327" From cc50860a0f63de0033cca176aceb4812643fa4f5 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 30 Mar 2026 14:38:13 -0700 Subject: [PATCH 427/642] Fixes some type errors in lute lint (#912) Achieved via a combination of refactoring, casts, and annotations. The remaining ones should be addressed/addressable when we bump Luau to 0.716. --- lute/cli/commands/lint/configutils.luau | 12 +++++++----- lute/cli/commands/lint/init.luau | 21 +++++++++++---------- tools/check-faillist.txt | 13 ------------- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/lute/cli/commands/lint/configutils.luau b/lute/cli/commands/lint/configutils.luau index 065ffe911..40e473edf 100644 --- a/lute/cli/commands/lint/configutils.luau +++ b/lute/cli/commands/lint/configutils.luau @@ -50,13 +50,15 @@ local function assertValidConfig(candidate: any) end local function extractConfig(candidate: any): internalTypes.LintConfig - local luteLintConfig = if candidate.lute ~= nil then candidate.lute.lint else nil -- LUAUFIX: I would expect candidate : any to silence this error - if luteLintConfig then - assertValidConfig(luteLintConfig) - return luteLintConfig + if candidate.lute == nil or candidate.lute.lint == nil then + return {} end - return {} + local luteLintConfig = candidate.lute.lint + + assertValidConfig(luteLintConfig) + + return luteLintConfig :: internalTypes.LintConfig end return { diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index b6a23a984..0bf8f9929 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -81,7 +81,7 @@ local function isLintRule(rule: unknown, path: string): boolean return false end - if typeof(rule.lint) ~= "function" then -- LUAUFIX: rule got refined to { read name : string } so accessing lint errors + if typeof((rule :: { lint: unknown }).lint) ~= "function" then -- LUAUFIX: rule got refined to { read name : string } so accessing prop lint errors if VERBOSE then print(`Error loading lint rule from {path}: must return a table with a 'lint' function property`) end @@ -304,7 +304,7 @@ local function lintString( end local ruleConfig = configData.ruleConfigs[rule.name] - local context = { + local context: types.RuleContext = { globals = configData.globals, options = if ruleConfig.options then ruleConfig.options else {}, } @@ -504,18 +504,19 @@ local function main(...: string) end local lintConfig: internalTypes.LintConfig = {} - local configPath = args:get("config") - if configPath == nil then - -- search in calling dir - local cwd = process.cwd() - configPath = pathLib.join(cwd, ".config.luau") - elseif fs.type(configPath) ~= "file" then - print(`Error: Configuration path must point to a file, not {fs.type(configPath)}`) + local passedConfigVal = args:get("config") + + if passedConfigVal and fs.type(passedConfigVal) ~= "file" then + print(`Error: Configuration path must point to a file, not {fs.type(passedConfigVal)}`) process.exit(1) end + local configPath: pathLib.Pathlike = if passedConfigVal + then passedConfigVal + else pathLib.join(process.cwd(), ".config.luau") + if fs.exists(configPath) then - local success, loadedConfig = pcall(luau.loadModule, configPath) + local success, loadedConfig = pcall(luau.loadModule, configPath, nil) if success then lintConfig = configUtils.extractConfig(loadedConfig) elseif VERBOSE then diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 5e2135cd9..3586fc6e1 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -11,19 +11,6 @@ examples/process.luau:17:7-15 examples/process.luau:18:7-15 examples/process.luau:19:7-15 examples/task-resume.luau:3:38-100 -lute/cli/commands/lint/configutils.luau:53:55-73 -lute/cli/commands/lint/configutils.luau:53:55-73 -lute/cli/commands/lint/init.luau:84:12-20 -lute/cli/commands/lint/init.luau:312:61-67 -lute/cli/commands/lint/init.luau:517:15-24 -lute/cli/commands/lint/init.luau:518:56-65 -lute/cli/commands/lint/init.luau:518:56-65 -lute/cli/commands/lint/init.luau:518:56-65 -lute/cli/commands/lint/init.luau:518:56-65 -lute/cli/commands/lint/init.luau:518:56-65 -lute/cli/commands/lint/init.luau:518:56-65 -lute/cli/commands/lint/init.luau:531:36-45 -lute/cli/commands/lint/init.luau:532:55-64 lute/cli/commands/lint/rules/almost_swapped.luau:92:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 From c6c74590ad8d53e19e9a93d0d434986ef7c891ce Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Mon, 30 Mar 2026 15:05:54 -0700 Subject: [PATCH 428/642] Refactors Token and Span so their properties are camelCased (#913) Part of a larger refactor of the AST types --- definitions/luau.luau | 14 +-- examples/lints/almost_swapped.luau | 8 +- lute/cli/commands/lint/init.luau | 8 +- lute/cli/commands/lint/lsp/init.luau | 8 +- lute/cli/commands/lint/printer.luau | 26 ++--- .../commands/lint/rules/almost_swapped.luau | 8 +- .../commands/lint/rules/empty_if_block.luau | 20 ++-- lute/luau/src/luau.cpp | 30 ++--- lute/std/libs/syntax/init.luau | 12 +- lute/std/libs/syntax/printer.luau | 14 +-- lute/std/libs/syntax/utils/trivia.luau | 16 +-- tests/std/syntax/parser.test.luau | 104 +++++++++--------- 12 files changed, 134 insertions(+), 134 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index b8a7d6c94..7115d4227 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -2,10 +2,10 @@ local luau = {} type SpanData = { - read beginline: number, - read begincolumn: number, - read endline: number, - read endcolumn: number, + read beginLine: number, + read beginColumn: number, + read endLine: number, + read endColumn: number, } type SpanMT = { read __lt: (a: SpanData, b: SpanData) -> boolean, @@ -14,7 +14,7 @@ export type Span = setmetatable luau.span = {} -function luau.span.create(tbl: { beginline: number, begincolumn: number, endline: number, endcolumn: number }): Span +function luau.span.create(tbl: { beginLine: number, beginColumn: number, endLine: number, endColumn: number }): Span error("not implemented") end @@ -40,10 +40,10 @@ export type MultiLineComment = { export type Trivia = Whitespace | SingleLineComment | MultiLineComment export type Token = { - read leadingtrivia: { Trivia }, + read leadingTrivia: { Trivia }, read location: Span, read text: Kind, - read trailingtrivia: { Trivia }, + read trailingTrivia: { Trivia }, read kind: "token", } diff --git a/examples/lints/almost_swapped.luau b/examples/lints/almost_swapped.luau index c04b45e97..93e572e69 100644 --- a/examples/lints/almost_swapped.luau +++ b/examples/lints/almost_swapped.luau @@ -88,10 +88,10 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.Path?): { lintTyp { lintname = name, location = syntax.span.create({ - beginline = currStat.location.beginline, - begincolumn = currStat.location.begincolumn, - endline = nextStat.location.endline, - endcolumn = nextStat.location.endcolumn, + beginLine = currStat.location.beginLine, + beginColumn = currStat.location.beginColumn, + endLine = nextStat.location.endLine, + endColumn = nextStat.location.endColumn, }), message = message, severity = "warning", diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 0bf8f9929..b0c84b9db 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -348,10 +348,10 @@ local function applySuggestedFixes(source: string, fixes: { fix }): string end) for _, fix in fixes do - local beginLine = fix.location.beginline - local endLine = fix.location.endline - local beginColumn = fix.location.begincolumn - local endColumn = fix.location.endcolumn + local beginLine = fix.location.beginLine + local endLine = fix.location.endLine + local beginColumn = fix.location.beginColumn + local endColumn = fix.location.endColumn if beginLine == endLine then -- Single-line fix diff --git a/lute/cli/commands/lint/lsp/init.luau b/lute/cli/commands/lint/lsp/init.luau index 35709613a..0d5c869ee 100644 --- a/lute/cli/commands/lint/lsp/init.luau +++ b/lute/cli/commands/lint/lsp/init.luau @@ -29,12 +29,12 @@ end local function getRange(location: syntax.Span): lspTypes.Range return table.freeze({ start = table.freeze({ - line = location.beginline - 1, - character = location.begincolumn - 1, + line = location.beginLine - 1, + character = location.beginColumn - 1, }), ["end"] = table.freeze({ - line = location.endline - 1, - character = location.endcolumn - 1, + line = location.endLine - 1, + character = location.endColumn - 1, }), }) end diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau index 3571a228b..adeafd98e 100644 --- a/lute/cli/commands/lint/printer.luau +++ b/lute/cli/commands/lint/printer.luau @@ -13,20 +13,20 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () local location = lint.location local filepath = if lint.sourcepath then path.format(lint.sourcepath) else "input" - if location.beginline == location.endline then - local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number + if location.beginLine == location.endLine then + local gutterSize = math.floor(math.log10(location.endLine)) + 3 -- + 2 for padding around line number local blankGutter = string.rep(" ", gutterSize) -- Compute the number of tabs before the violation and within the violation - local prelintTabs = stringext.count(sourceLines[location.beginline], "\t", 1, location.begincolumn - 1) + local prelintTabs = stringext.count(sourceLines[location.beginLine], "\t", 1, location.beginColumn - 1) local lintTabs = - stringext.count(sourceLines[location.beginline], "\t", location.begincolumn, location.endcolumn) + stringext.count(sourceLines[location.beginLine], "\t", location.beginColumn, location.endColumn) -- Convert tabs in the source line to spaces local sourceLine = - ` {location.beginline} │ {sourceLines[location.beginline]:gsub("\t", string.rep(" ", TAB_SIZE))}` + ` {location.beginLine} │ {sourceLines[location.beginLine]:gsub("\t", string.rep(" ", TAB_SIZE))}` local filepathLine = - `{blankGutter}┌── {filepath}:{location.beginline}:{location.begincolumn}-{location.endcolumn} ──` + `{blankGutter}┌── {filepath}:{location.beginLine}:{location.beginColumn}-{location.endColumn} ──` if #filepathLine < #sourceLine then filepathLine ..= string.rep("─", #sourceLine - #filepathLine) end @@ -35,23 +35,23 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () print(`{blankGutter}│`) print(sourceLine) print( - `{blankGutter}│ {string.rep(" ", location.begincolumn - 1 + (prelintTabs * (TAB_SIZE - 1)))}{string.rep( + `{blankGutter}│ {string.rep(" ", location.beginColumn - 1 + (prelintTabs * (TAB_SIZE - 1)))}{string.rep( "^", - location.endcolumn - location.begincolumn + (lintTabs * (TAB_SIZE - 1)) + location.endColumn - location.beginColumn + (lintTabs * (TAB_SIZE - 1)) )}` ) print(`{blankGutter}│`) print() else - local gutterSize = math.floor(math.log10(location.endline)) + 3 -- + 2 for padding around line number + local gutterSize = math.floor(math.log10(location.endLine)) + 3 -- + 2 for padding around line number local blankGutter = string.rep(" ", gutterSize) - local subbedSourceLine = sourceLines[location.beginline]:gsub("\t", string.rep(" ", TAB_SIZE)) + local subbedSourceLine = sourceLines[location.beginLine]:gsub("\t", string.rep(" ", TAB_SIZE)) local renderedSourceLines = { - ` {string.format(`%-{gutterSize - 2}d`, location.beginline)} │ ╭ {subbedSourceLine}`, + ` {string.format(`%-{gutterSize - 2}d`, location.beginLine)} │ ╭ {subbedSourceLine}`, } local maxSourceLineLength = #renderedSourceLines[1] - for lineNum = location.beginline + 1, location.endline do + for lineNum = location.beginLine + 1, location.endLine do subbedSourceLine = sourceLines[lineNum]:gsub("\t", string.rep(" ", TAB_SIZE)) table.insert( renderedSourceLines, @@ -61,7 +61,7 @@ local function printLint(lint: types.LintViolation, sourceLines: { string }): () end local filepathLine = - `{blankGutter}┌── {filepath}:{location.beginline}:{location.begincolumn}-{location.endline}:{location.endcolumn} ──` + `{blankGutter}┌── {filepath}:{location.beginLine}:{location.beginColumn}-{location.endLine}:{location.endColumn} ──` if #filepathLine < maxSourceLineLength then filepathLine ..= string.rep("─", maxSourceLineLength - #filepathLine) end diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index f27f13cf9..1ea28152f 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -92,10 +92,10 @@ local function lint( table.insert(violations, { -- LUAUFIX: inserted table isn't inferred as a LintViolation lintname = name, location = syntax.span.create({ - beginline = currStat.location.beginline, - begincolumn = currStat.location.begincolumn, - endline = nextStat.location.endline, - endcolumn = nextStat.location.endcolumn, + beginLine = currStat.location.beginLine, + beginColumn = currStat.location.beginColumn, + endLine = nextStat.location.endLine, + endColumn = nextStat.location.endColumn, }), message = message, severity = "warning" :: "warning", -- LUAUFIX: cast needed because severity isn't inferred as a singleton diff --git a/lute/cli/commands/lint/rules/empty_if_block.luau b/lute/cli/commands/lint/rules/empty_if_block.luau index 99d7dd2d0..d51362bc8 100644 --- a/lute/cli/commands/lint/rules/empty_if_block.luau +++ b/lute/cli/commands/lint/rules/empty_if_block.luau @@ -59,11 +59,11 @@ local function lint( isBlockEmpty( ifStat.thenblock, commentsCount, - ifStat.thenkeyword.trailingtrivia, + ifStat.thenkeyword.trailingTrivia, if #ifStat.elseifs > 0 -- trailingToken - then ifStat.elseifs[1].elseifkeyword.leadingtrivia - elseif ifStat.elsekeyword then ifStat.elsekeyword.leadingtrivia - else ifStat.endkeyword.leadingtrivia + then ifStat.elseifs[1].elseifkeyword.leadingTrivia + elseif ifStat.elsekeyword then ifStat.elsekeyword.leadingTrivia + else ifStat.endkeyword.leadingTrivia ) then return { @@ -82,11 +82,11 @@ local function lint( isBlockEmpty( elseIfStat.thenblock, commentsCount, - elseIfStat.thenkeyword.trailingtrivia, + elseIfStat.thenkeyword.trailingTrivia, if i < #ifStat.elseifs -- trailingToken - then ifStat.elseifs[i + 1].elseifkeyword.leadingtrivia - elseif ifStat.elsekeyword then ifStat.elsekeyword.leadingtrivia - else ifStat.endkeyword.leadingtrivia + then ifStat.elseifs[i + 1].elseifkeyword.leadingTrivia + elseif ifStat.elsekeyword then ifStat.elsekeyword.leadingTrivia + else ifStat.endkeyword.leadingTrivia ) then return { @@ -107,8 +107,8 @@ local function lint( isBlockEmpty( ifStat.elseblock, commentsCount, - ifStat.elsekeyword.trailingtrivia, - ifStat.endkeyword.leadingtrivia + ifStat.elsekeyword.trailingTrivia, + ifStat.endkeyword.leadingTrivia ) then return { diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index ba4d94e28..1e347b3bd 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -161,22 +161,22 @@ static Span checkSpan(lua_State* L, int index) if (!lua_istable(L, index)) luaL_typeerror(L, index, "span"); - if (lua_getfield(L, index, "beginline") == LUA_TNIL) + if (lua_getfield(L, index, "beginLine") == LUA_TNIL) luaL_typeerror(L, index, "span"); int beginLine = lua_tonumber(L, -1); lua_pop(L, 1); - if (lua_getfield(L, index, "begincolumn") == LUA_TNIL) + if (lua_getfield(L, index, "beginColumn") == LUA_TNIL) luaL_typeerror(L, index, "span"); int beginColumn = lua_tonumber(L, -1); lua_pop(L, 1); - if (lua_getfield(L, index, "endline") == LUA_TNIL) + if (lua_getfield(L, index, "endLine") == LUA_TNIL) luaL_typeerror(L, index, "span"); int endLine = lua_tonumber(L, -1); lua_pop(L, 1); - if (lua_getfield(L, index, "endcolumn") == LUA_TNIL) + if (lua_getfield(L, index, "endColumn") == LUA_TNIL) luaL_typeerror(L, index, "span"); int endColumn = lua_tonumber(L, -1); lua_pop(L, 1); @@ -198,16 +198,16 @@ static int createSpan(lua_State* L) lua_createtable(L, 0, 4); lua_pushinteger(L, span.beginLine); - lua_setfield(L, -2, "beginline"); + lua_setfield(L, -2, "beginLine"); lua_pushinteger(L, span.beginColumn); - lua_setfield(L, -2, "begincolumn"); + lua_setfield(L, -2, "beginColumn"); lua_pushinteger(L, span.endLine); - lua_setfield(L, -2, "endline"); + lua_setfield(L, -2, "endLine"); lua_pushinteger(L, span.endColumn); - lua_setfield(L, -2, "endcolumn"); + lua_setfield(L, -2, "endColumn"); luaL_getmetatable(L, kSpanType); lua_setmetatable(L, -2); @@ -423,16 +423,16 @@ struct AstSerialize : public Luau::AstVisitor lua_createtable(L, 0, 4); lua_pushinteger(L, location.begin.line + 1); - lua_setfield(L, -2, "beginline"); + lua_setfield(L, -2, "beginLine"); lua_pushinteger(L, location.begin.column + 1); - lua_setfield(L, -2, "begincolumn"); + lua_setfield(L, -2, "beginColumn"); lua_pushinteger(L, location.end.line + 1); - lua_setfield(L, -2, "endline"); + lua_setfield(L, -2, "endLine"); lua_pushinteger(L, location.end.column + 1); - lua_setfield(L, -2, "endcolumn"); + lua_setfield(L, -2, "endColumn"); luaL_getmetatable(L, kSpanType); lua_setmetatable(L, -2); @@ -655,7 +655,7 @@ struct AstSerialize : public Luau::AstVisitor LUTE_ASSERT(lua_istable(L, -1)); serializeTrivia(trailingTrivia); - lua_setfield(L, -2, "trailingtrivia"); + lua_setfield(L, -2, "trailingTrivia"); lua_pop(L, 1); lua_unref(L, lastTokenRef); lastTokenRef = LUA_NOREF; @@ -667,7 +667,7 @@ struct AstSerialize : public Luau::AstVisitor serializeTrivia(trivia); } LUTE_ASSERT(lua_istable(L, -2)); - lua_setfield(L, -2, "leadingtrivia"); + lua_setfield(L, -2, "leadingTrivia"); size_t textLength = strlen(text); @@ -680,7 +680,7 @@ struct AstSerialize : public Luau::AstVisitor advancePosition(text); lua_createtable(L, 0, 0); - lua_setfield(L, -2, "trailingtrivia"); + lua_setfield(L, -2, "trailingTrivia"); lua_pushstring(L, "token"); lua_setfield(L, -2, "kind"); diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index 88b220ca3..707fcc158 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -11,14 +11,14 @@ local span = { function span.subsumes(haystack: Span, needle: Span): boolean return ( - if haystack.beginline == needle.beginline - then haystack.begincolumn <= needle.begincolumn - else haystack.beginline < needle.beginline + if haystack.beginLine == needle.beginLine + then haystack.beginColumn <= needle.beginColumn + else haystack.beginLine < needle.beginLine ) and ( - if haystack.endline == needle.endline - then haystack.endcolumn >= needle.endcolumn - else haystack.endline > needle.endline + if haystack.endLine == needle.endLine + then haystack.endColumn >= needle.endColumn + else haystack.endLine > needle.endLine ) end diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index e6c6a243c..dc0eb3aeb 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -43,9 +43,9 @@ local function printToken(self: PrintVisitor, token: types.Token) return end - self:printTriviaList(token.leadingtrivia) + self:printTriviaList(token.leadingTrivia) self:write(token.text) - self:printTriviaList(token.trailingtrivia) + self:printTriviaList(token.trailingTrivia) end local function printString(self: PrintVisitor, expr: types.AstExprConstantString | types.AstTypeSingletonString) @@ -54,7 +54,7 @@ local function printString(self: PrintVisitor, expr: types.AstExprConstantString return end - self:printTriviaList(expr.token.leadingtrivia) + self:printTriviaList(expr.token.leadingTrivia) if expr.quotestyle == "single" then self:write(`'{expr.token.text}'`) @@ -69,7 +69,7 @@ local function printString(self: PrintVisitor, expr: types.AstExprConstantString exhaustiveMatch(expr.quotestyle) end - self:printTriviaList(expr.token.trailingtrivia) + self:printTriviaList(expr.token.trailingTrivia) end local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprInterpString) @@ -79,7 +79,7 @@ local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprIn end for i = 1, #expr.strings do - self:printTriviaList(expr.strings[i].leadingtrivia) + self:printTriviaList(expr.strings[i].leadingTrivia) if i == 1 then self:write("`") else @@ -89,10 +89,10 @@ local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprIn if i == #expr.strings then self:write("`") - self:printTriviaList(expr.strings[i].trailingtrivia) + self:printTriviaList(expr.strings[i].trailingTrivia) else self:write("{") - self:printTriviaList(expr.strings[i].trailingtrivia) + self:printTriviaList(expr.strings[i].trailingTrivia) visitor.visitexpression(expr.expressions[i], self) end end diff --git a/lute/std/libs/syntax/utils/trivia.luau b/lute/std/libs/syntax/utils/trivia.luau index 42ac6ddab..ff75ed41a 100644 --- a/lute/std/libs/syntax/utils/trivia.luau +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -16,17 +16,17 @@ function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } for _, token in q.nodes do if leftmostToken == nil then leftmostToken = token - elseif token.location.beginline < leftmostToken.location.beginline then + elseif token.location.beginLine < leftmostToken.location.beginLine then leftmostToken = token elseif - token.location.beginline == leftmostToken.location.beginline - and token.location.begincolumn < leftmostToken.location.begincolumn + token.location.beginLine == leftmostToken.location.beginLine + and token.location.beginColumn < leftmostToken.location.beginColumn then leftmostToken = token end end - return if leftmostToken ~= nil then leftmostToken.leadingtrivia else {} + return if leftmostToken ~= nil then leftmostToken.leadingTrivia else {} end function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } @@ -42,17 +42,17 @@ function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } for _, token in q.nodes do if rightmostToken == nil then rightmostToken = token - elseif token.location.beginline > rightmostToken.location.beginline then + elseif token.location.beginLine > rightmostToken.location.beginLine then rightmostToken = token elseif - token.location.beginline == rightmostToken.location.beginline - and token.location.begincolumn > rightmostToken.location.begincolumn + token.location.beginLine == rightmostToken.location.beginLine + and token.location.beginColumn > rightmostToken.location.beginColumn then rightmostToken = token end end - return if rightmostToken ~= nil then rightmostToken.trailingtrivia else {} + return if rightmostToken ~= nil then rightmostToken.trailingTrivia else {} end return table.freeze(retrieverLib) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index d7b08d9b5..743d58bbe 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -15,10 +15,10 @@ local function assertEqualsLocation( endLine: number, endColumn: number ) - assert.eq(actual.beginline, startLine) - assert.eq(actual.begincolumn, startColumn) - assert.eq(actual.endline, endLine) - assert.eq(actual.endcolumn, endColumn) + assert.eq(actual.beginLine, startLine) + assert.eq(actual.beginColumn, startColumn) + assert.eq(actual.endLine, endLine) + assert.eq(actual.endColumn, endColumn) end test.suite("Parser", function(suite) @@ -30,10 +30,10 @@ test.suite("Parser", function(suite) assert.eq(l.tag, "local") local token = (l :: syntax.AstStatLocal).localkeyword - assert.eq(#token.leadingtrivia, 1) - assert.eq(token.leadingtrivia[1].tag, "whitespace") - assert.eq(token.leadingtrivia[1].text, " ") - assertEqualsLocation(assert, token.leadingtrivia[1].location, 1, 1, 1, 3) + assert.eq(#token.leadingTrivia, 1) + assert.eq(token.leadingTrivia[1].tag, "whitespace") + assert.eq(token.leadingTrivia[1].text, " ") + assertEqualsLocation(assert, token.leadingTrivia[1].location, 1, 1, 1, 3) end) suite:case("tokenContainsLeadingNewline", function(assert) @@ -44,10 +44,10 @@ test.suite("Parser", function(suite) assert.eq(l.tag, "local") local token = (l :: syntax.AstStatLocal).localkeyword - assert.eq(#token.leadingtrivia, 1) - assert.eq(token.leadingtrivia[1].tag, "whitespace") - assert.eq(token.leadingtrivia[1].text, "\n") - assertEqualsLocation(assert, token.leadingtrivia[1].location, 1, 1, 2, 1) + assert.eq(#token.leadingTrivia, 1) + assert.eq(token.leadingTrivia[1].tag, "whitespace") + assert.eq(token.leadingTrivia[1].text, "\n") + assertEqualsLocation(assert, token.leadingTrivia[1].location, 1, 1, 2, 1) end) suite:case("tokenContainsLeadingSingleLineComment", function(assert) @@ -58,13 +58,13 @@ test.suite("Parser", function(suite) assert.eq(l.tag, "local") local token = (l :: syntax.AstStatLocal).localkeyword - assert.eq(#token.leadingtrivia, 2) - assert.eq(token.leadingtrivia[1].tag, "comment") - assert.eq(token.leadingtrivia[1].text, "-- comment") - assertEqualsLocation(assert, token.leadingtrivia[1].location, 1, 1, 1, 11) - assert.eq(token.leadingtrivia[2].tag, "whitespace") - assert.eq(token.leadingtrivia[2].text, "\n") - assertEqualsLocation(assert, token.leadingtrivia[2].location, 1, 11, 2, 1) + assert.eq(#token.leadingTrivia, 2) + assert.eq(token.leadingTrivia[1].tag, "comment") + assert.eq(token.leadingTrivia[1].text, "-- comment") + assertEqualsLocation(assert, token.leadingTrivia[1].location, 1, 1, 1, 11) + assert.eq(token.leadingTrivia[2].tag, "whitespace") + assert.eq(token.leadingTrivia[2].text, "\n") + assertEqualsLocation(assert, token.leadingTrivia[2].location, 1, 11, 2, 1) end) suite:case("tokenContainsLeadingBlockComment", function(assert) @@ -75,13 +75,13 @@ test.suite("Parser", function(suite) assert.eq(l.tag, "local") local token = (l :: syntax.AstStatLocal).localkeyword - assert.eq(#token.leadingtrivia, 2) - assert.eq(token.leadingtrivia[1].tag, "blockcomment") - assert.eq(token.leadingtrivia[1].text, "--[[ comment ]]") - assertEqualsLocation(assert, token.leadingtrivia[1].location, 1, 1, 1, 16) - assert.eq(token.leadingtrivia[2].tag, "whitespace") - assert.eq(token.leadingtrivia[2].text, " ") - assertEqualsLocation(assert, token.leadingtrivia[2].location, 1, 16, 1, 17) + assert.eq(#token.leadingTrivia, 2) + assert.eq(token.leadingTrivia[1].tag, "blockcomment") + assert.eq(token.leadingTrivia[1].text, "--[[ comment ]]") + assertEqualsLocation(assert, token.leadingTrivia[1].location, 1, 1, 1, 16) + assert.eq(token.leadingTrivia[2].tag, "whitespace") + assert.eq(token.leadingTrivia[2].text, " ") + assertEqualsLocation(assert, token.leadingTrivia[2].location, 1, 16, 1, 17) end) suite:case("tokenizeWhitespace", function(assert) @@ -92,10 +92,10 @@ test.suite("Parser", function(suite) assert.eq(l.tag, "local") local token = (l :: syntax.AstStatLocal).localkeyword - assert.eq(#token.leadingtrivia, 3) - assert.eq(token.leadingtrivia[1].text, " \n") - assert.eq(token.leadingtrivia[2].text, "\t\t\n") - assert.eq(token.leadingtrivia[3].text, "\n") + assert.eq(#token.leadingTrivia, 3) + assert.eq(token.leadingTrivia[1].text, " \n") + assert.eq(token.leadingTrivia[2].text, "\t\t\n") + assert.eq(token.leadingTrivia[3].text, "\n") end) suite:case("triviaSplitBetweenLeadingAndTrailing", function(assert) @@ -108,18 +108,18 @@ test.suite("Parser", function(suite) local trailingToken: syntax.AstExpr = (firstStmt :: syntax.AstStatLocal).values[1].node assert.eq(trailingToken.tag, "string") local expr = trailingToken :: syntax.AstExprConstantString - assert.eq(#expr.token.trailingtrivia, 3) - assert.eq(expr.token.trailingtrivia[1].text, " ") - assert.eq(expr.token.trailingtrivia[2].text, "-- comment") - assert.eq(expr.token.trailingtrivia[3].text, "\n") + assert.eq(#expr.token.trailingTrivia, 3) + assert.eq(expr.token.trailingTrivia[1].text, " ") + assert.eq(expr.token.trailingTrivia[2].text, "-- comment") + assert.eq(expr.token.trailingTrivia[3].text, "\n") local secondStmt = block.statements[2] assert.eq(secondStmt.tag, "local") local leadingToken = (secondStmt :: syntax.AstStatLocal).localkeyword - assert.eq(#leadingToken.leadingtrivia, 2) - assert.eq(leadingToken.leadingtrivia[1].text, "-- comment 2") - assert.eq(leadingToken.leadingtrivia[2].text, "\n") + assert.eq(#leadingToken.leadingTrivia, 2) + assert.eq(leadingToken.leadingTrivia[1].text, "-- comment 2") + assert.eq(leadingToken.leadingTrivia[2].text, "\n") end) local function visitDirectory(directory: string) @@ -533,18 +533,18 @@ foo = bar, assert.eq(tableExpr.kind, "expr") tableExpr = tableExpr :: syntax.AstExprTable assert.eq(#tableExpr.entries, 3) - assert.eq(tableExpr.entries[1].location.beginline, 2) - assert.eq(tableExpr.entries[1].location.begincolumn, 1) - assert.eq(tableExpr.entries[1].location.endline, 2) - assert.eq(tableExpr.entries[1].location.endcolumn, 2) - assert.eq(tableExpr.entries[2].location.beginline, 3) - assert.eq(tableExpr.entries[2].location.begincolumn, 1) - assert.eq(tableExpr.entries[2].location.endline, 3) - assert.eq(tableExpr.entries[2].location.endcolumn, 10) - assert.eq(tableExpr.entries[3].location.beginline, 4) - assert.eq(tableExpr.entries[3].location.begincolumn, 1) - assert.eq(tableExpr.entries[3].location.endline, 4) - assert.eq(tableExpr.entries[3].location.endcolumn, 10) + assert.eq(tableExpr.entries[1].location.beginLine, 2) + assert.eq(tableExpr.entries[1].location.beginColumn, 1) + assert.eq(tableExpr.entries[1].location.endLine, 2) + assert.eq(tableExpr.entries[1].location.endColumn, 2) + assert.eq(tableExpr.entries[2].location.beginLine, 3) + assert.eq(tableExpr.entries[2].location.beginColumn, 1) + assert.eq(tableExpr.entries[2].location.endLine, 3) + assert.eq(tableExpr.entries[2].location.endColumn, 10) + assert.eq(tableExpr.entries[3].location.beginLine, 4) + assert.eq(tableExpr.entries[3].location.beginColumn, 1) + assert.eq(tableExpr.entries[3].location.endLine, 4) + assert.eq(tableExpr.entries[3].location.endColumn, 10) end) end) @@ -1898,10 +1898,10 @@ test.suite("AST_nodes_frozen", function(suite) (token :: any).text = "modified" end) assert.throwsWith(frozenTableMessage, function() - table.insert((token :: any).leadingtrivia, nil) + table.insert((token :: any).leadingTrivia, nil) end) assert.throwsWith(frozenTableMessage, function() - table.insert((token :: any).trailingtrivia, nil) + table.insert((token :: any).trailingTrivia, nil) end) end) @@ -1916,7 +1916,7 @@ test.suite("AST_nodes_frozen", function(suite) local x = 1 ]=]) local localStat = block.statements[1] :: syntax.AstStatLocal - local trivias = localStat.localkeyword.leadingtrivia + local trivias = localStat.localkeyword.leadingTrivia for _, trivia in trivias do assert.throwsWith(frozenTableMessage, function() (trivia :: any).text = "modified" From 23498af01f5a09b820a794ba4f164ba93ffd616b Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 30 Mar 2026 16:34:50 -0700 Subject: [PATCH 429/642] Resolves typecheck errors in `lute test` (#914) The typechecker was flagging an actual error in the luau code, which is that the table we were creating to represent filtered test suites was missing properties. Although the testing framework never uses these properties (as they are index metatable methods that are used to register test cases), they are in fact missing, which can cause errors if they are accesses down the road. This PR just table.clones these testsuites and updates their test cases explicitly to the list of filtered test cases. --- lute/cli/commands/test/filter.luau | 20 ++++---------------- tools/check-faillist.txt | 4 ---- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/lute/cli/commands/test/filter.luau b/lute/cli/commands/test/filter.luau index d56ddcd0d..b9c067b91 100644 --- a/lute/cli/commands/test/filter.luau +++ b/lute/cli/commands/test/filter.luau @@ -23,14 +23,8 @@ local function filtertests( if caseEntry and caseEntry.suites[suiteName] then local suite = env.suiteindex[suiteName] if suite then - local filteredSuite = { - name = suite.name, - cases = caseEntry.suites[suiteName], - _beforeeach = suite._beforeeach, - _beforeall = suite._beforeall, - _aftereach = suite._aftereach, - _afterall = suite._afterall, - } + local filteredSuite = table.clone(suite) + filteredSuite.cases = caseEntry.suites[suiteName] table.insert(filtered.suites, filteredSuite) end end @@ -51,14 +45,8 @@ local function filtertests( for sName, cases in caseEntry.suites do local suite = env.suiteindex[sName] if suite then - local filteredSuite = { - name = suite.name, - cases = cases, - _beforeeach = suite._beforeeach, - _beforeall = suite._beforeall, - _aftereach = suite._aftereach, - _afterall = suite._afterall, - } + local filteredSuite = table.clone(suite) + filteredSuite.cases = cases table.insert(filtered.suites, filteredSuite) end end diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index 3586fc6e1..edb64664a 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -15,10 +15,6 @@ lute/cli/commands/lint/rules/almost_swapped.luau:92:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 lute/cli/commands/lint/rules/parenthesized_conditions.luau:68:4-15 -lute/cli/commands/test/filter.luau:34:5-16 -lute/cli/commands/test/filter.luau:34:5-16 -lute/cli/commands/test/filter.luau:62:6-17 -lute/cli/commands/test/filter.luau:62:6-17 tests/src/packages/package_aware_require/dep/module.luau:1:16-30 tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau:1:16-38 tests/src/require/config_tests/config_ambiguity/requirer.luau:1:8-22 From aa83e3f9ae9459c0a16a37dcc875fd445a29d5d3 Mon Sep 17 00:00:00 2001 From: Andrey Zhabsky Date: Tue, 31 Mar 2026 04:26:17 +0300 Subject: [PATCH 430/642] `Profiler`: single source for `sampling frequency` (#918) Moved the `profiler options` from the `CLI` directly into the `profiler`. Currently, the sampling `frequency` in Hz duplicates its default value in two places: [here](https://github.com/luau-lang/lute/blob/6ed3142c96d109a7ac94ec7c9a7d8dc86a56ac69/lute/cli/src/profiler.cpp#L53) and [here](https://github.com/luau-lang/lute/blob/6ed3142c96d109a7ac94ec7c9a7d8dc86a56ac69/lute/cli/include/lute/climain.h#L13). It has no impact at the moment, but having a single source will help avoid potential bugs in the future. Originally a minor change in PR #911, this has been moved to a separate PR for better clarity. Besides, in PR #911 the main logic of `runBytecode` was moved from `CLI` to `Runtime`, so we don't need the `profiler` dependency in the `CLI` at all. --- lute/cli/include/lute/climain.h | 8 ++------ lute/cli/include/lute/profiler.h | 8 ++++++++ lute/cli/src/profiler.cpp | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lute/cli/include/lute/climain.h b/lute/cli/include/lute/climain.h index 9c304962b..8ff955461 100644 --- a/lute/cli/include/lute/climain.h +++ b/lute/cli/include/lute/climain.h @@ -1,4 +1,6 @@ #pragma once +#include "lute/profiler.h" + #include #include #include @@ -7,12 +9,6 @@ struct lua_State; struct Runtime; class LuteReporter; -struct ProfileOptions -{ - std::string filename; - int frequency = 10000; -}; - int cliMain(int argc, char** argv, LuteReporter& reporter); bool runBytecode( Runtime& runtime, diff --git a/lute/cli/include/lute/profiler.h b/lute/cli/include/lute/profiler.h index 2ff881fa7..f6b149715 100644 --- a/lute/cli/include/lute/profiler.h +++ b/lute/cli/include/lute/profiler.h @@ -1,8 +1,16 @@ #pragma once #include "lute/reporter.h" +#include + struct lua_State; +struct ProfileOptions +{ + std::string filename; + int frequency = 10000; +}; + void profilerStart(lua_State* L, int frequency); void profilerStop(); void profilerDump(const char* path, LuteReporter& reporter); diff --git a/lute/cli/src/profiler.cpp b/lute/cli/src/profiler.cpp index 44d797cf9..87f087ba1 100644 --- a/lute/cli/src/profiler.cpp +++ b/lute/cli/src/profiler.cpp @@ -50,7 +50,7 @@ struct Profiler { // static state lua_Callbacks* callbacks = nullptr; - int frequency = 10000; + int frequency = ProfileOptions{}.frequency; std::thread thread; // variables for communication between loop and trigger From 795c35b4ff51e371c869f491b889d10947e9c7e2 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 31 Mar 2026 10:02:58 -0700 Subject: [PATCH 431/642] Refactor type and type pack AST types (#917) Part of a larger refactor of AST types where we want property names to be camelCased --- definitions/luau.luau | 52 ++++++------- lute/luau/src/luau.cpp | 52 ++++++------- lute/std/libs/syntax/visitor.luau | 68 ++++++++--------- tests/std/syntax/parser.test.luau | 114 ++++++++++++++--------------- tests/std/syntax/printer.test.luau | 4 +- 5 files changed, 145 insertions(+), 145 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 7115d4227..94d6ef7f5 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -534,11 +534,11 @@ export type AstTypeReference = { read kind: "type", read tag: "reference", read prefix: Token?, - read prefixpoint: Token<".">?, + read prefixPoint: Token<".">?, read name: Token, - read openparameters: Token<"<">?, + read openParameters: Token<"<">?, read parameters: Punctuated?, - read closeparameters: Token<">">?, + read closeParameters: Token<">">?, } export type AstTypeSingletonBool = { @@ -562,18 +562,18 @@ export type AstTypeTypeof = { read kind: "type", read tag: "typeof", read typeof: Token<"typeof">, - read openparens: Token<"(">, + read openParens: Token<"(">, read expression: AstExpr, - read closeparens: Token<")">, + read closeParens: Token<")">, } export type AstTypeGroup = { read location: Span, read kind: "type", read tag: "group", - read openparens: Token<"(">, + read openParens: Token<"(">, read type: AstType, - read closeparens: Token<")">, + read closeParens: Token<")">, } export type AstTypeOptional = { read location: Span, read kind: "type", read tag: "optional", read token: Token<"?"> } @@ -599,18 +599,18 @@ export type AstTypeArray = { read location: Span, read kind: "type", read tag: "array", - read openbrace: Token<"{">, + read openBrace: Token<"{">, read access: Token<"read" | "write">?, read type: AstType, - read closebrace: Token<"}">, + read closeBrace: Token<"}">, } export type AstTableTypeItemIndexer = { read kind: "indexer", read access: Token<"read" | "write">?, - read indexeropen: Token<"[">, + read indexerOpen: Token<"[">, read key: AstType, - read indexerclose: Token<"]">, + read indexerClose: Token<"]">, read colon: Token<":">, read value: AstType, read separator: Token<"," | ";">?, @@ -619,9 +619,9 @@ export type AstTableTypeItemIndexer = { export type AstTableTypeItemStringProperty = { read kind: "stringproperty", read access: Token<"read" | "write">?, - read indexeropen: Token<"[">, + read indexerOpen: Token<"[">, read key: AstTypeSingletonString, - read indexerclose: Token<"]">, + read indexerClose: Token<"]">, read colon: Token<":">, read value: AstType, read separator: Token<"," | ";">?, @@ -642,9 +642,9 @@ export type AstTypeTable = { read location: Span, read kind: "type", read tag: "table", - read openbrace: Token<"{">, + read openBrace: Token<"{">, read entries: { AstTableTypeItem }, - read closebrace: Token<"}">, + read closeBrace: Token<"}">, } export type AstFunctionTypeParameter = { @@ -659,16 +659,16 @@ export type AstTypeFunction = { read location: Span, read kind: "type", read tag: "function", - read opengenerics: Token<"<">?, + read openGenerics: Token<"<">?, read generics: Punctuated?, - read genericpacks: Punctuated?, - read closegenerics: Token<">">?, - read openparens: Token<"(">, + read genericPacks: Punctuated?, + read closeGenerics: Token<">">?, + read openParens: Token<"(">, read parameters: Punctuated, read vararg: AstTypePack?, - read closeparens: Token<")">, - read returnarrow: Token<"->">, - read returntypes: AstTypePack, + read closeParens: Token<")">, + read returnArrow: Token<"->">, + read returnTypes: AstTypePack, } export type AstType = @@ -688,10 +688,10 @@ export type AstTypePackExplicit = { read location: Span, read kind: "typepack", read tag: "explicit", - read openparens: Token<"(">?, + read openParens: Token<"(">?, read types: Punctuated, - read tailtype: AstTypePack?, - read closeparens: Token<")">?, + read tailType: AstTypePack?, + read closeParens: Token<")">?, } export type AstTypePackGeneric = { @@ -719,7 +719,7 @@ export type ParseResult = { read root: AstStatBlock, read eof: Eof, read lines: number, - read lineoffsets: { number }, + read lineOffsets: { number }, } function luau.parse(source: string): ParseResult diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 1e347b3bd..c57da5eee 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1919,7 +1919,7 @@ struct AstSerialize : public Luau::AstVisitor LUTE_ASSERT(cstNode); LUTE_ASSERT(cstNode->prefixPointPosition); serializeToken(*cstNode->prefixPointPosition, "."); - lua_setfield(L, -2, "prefixpoint"); + lua_setfield(L, -2, "prefixPoint"); } serializeToken(node->nameLocation.begin, node->name.value); @@ -1929,13 +1929,13 @@ struct AstSerialize : public Luau::AstVisitor { LUTE_ASSERT(cstNode); serializeToken(cstNode->openParametersPosition, "<"); - lua_setfield(L, -2, "openparameters"); + lua_setfield(L, -2, "openParameters"); serializePunctuated(node->parameters, cstNode->parametersCommaPositions, ","); lua_setfield(L, -2, "parameters"); serializeToken(cstNode->closeParametersPosition, ">"); - lua_setfield(L, -2, "closeparameters"); + lua_setfield(L, -2, "closeParameters"); } } @@ -1951,7 +1951,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "array", "type"); serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openbrace"); + lua_setfield(L, -2, "openBrace"); if (node->indexer->accessLocation) { @@ -1966,7 +1966,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "type"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closebrace"); + lua_setfield(L, -2, "closeBrace"); return; } @@ -1977,7 +1977,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "table", "type"); serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openbrace"); + lua_setfield(L, -2, "openBrace"); lua_createtable(L, cstNode->items.size, 0); const Luau::AstTableProp* prop = node->props.begin(); @@ -2005,13 +2005,13 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "access"); serializeToken(item.indexerOpenPosition, "["); - lua_setfield(L, -2, "indexeropen"); + lua_setfield(L, -2, "indexerOpen"); node->indexer->indexType->visit(this); lua_setfield(L, -2, "key"); serializeToken(item.indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerclose"); + lua_setfield(L, -2, "indexerClose"); serializeToken(item.colonPosition, ":"); lua_setfield(L, -2, "colon"); @@ -2050,7 +2050,7 @@ struct AstSerialize : public Luau::AstVisitor if (item.kind == Luau::CstTypeTable::Item::Kind::StringProperty) { serializeToken(item.indexerOpenPosition, "["); - lua_setfield(L, -2, "indexeropen"); + lua_setfield(L, -2, "indexerOpen"); { lua_rawcheckstack(L, 2); @@ -2096,7 +2096,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "key"); serializeToken(item.indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerclose"); + lua_setfield(L, -2, "indexerClose"); } else { @@ -2123,7 +2123,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "entries"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closebrace"); + lua_setfield(L, -2, "closeBrace"); } void serializeType(Luau::AstTypeFunction* node) @@ -2138,21 +2138,21 @@ struct AstSerialize : public Luau::AstVisitor if (node->generics.size > 0 || node->genericPacks.size > 0) { serializeToken(cstNode->openGenericsPosition, "<"); - lua_setfield(L, -2, "opengenerics"); + lua_setfield(L, -2, "openGenerics"); auto commas = cstNode->genericsCommaPositions; serializePunctuated(node->generics, commas, ","); lua_setfield(L, -2, "generics"); serializePunctuated(node->genericPacks, splitArray(commas, node->generics.size), ","); - lua_setfield(L, -2, "genericpacks"); + lua_setfield(L, -2, "genericPacks"); serializeToken(cstNode->closeGenericsPosition, ">"); - lua_setfield(L, -2, "closegenerics"); + lua_setfield(L, -2, "closeGenerics"); } serializeToken(cstNode->openArgsPosition, "("); - lua_setfield(L, -2, "openparens"); + lua_setfield(L, -2, "openParens"); lua_createtable(L, node->argTypes.types.size, 0); for (size_t i = 0; i < node->argTypes.types.size; i++) @@ -2197,13 +2197,13 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "vararg"); serializeToken(cstNode->closeArgsPosition, ")"); - lua_setfield(L, -2, "closeparens"); + lua_setfield(L, -2, "closeParens"); serializeToken(cstNode->returnArrowPosition, "->"); - lua_setfield(L, -2, "returnarrow"); + lua_setfield(L, -2, "returnArrow"); node->returnTypes->visit(this); - lua_setfield(L, -2, "returntypes"); + lua_setfield(L, -2, "returnTypes"); } void serializeType(Luau::AstTypeTypeof* node) @@ -2218,13 +2218,13 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); serializeToken(cstNode->openPosition, "("); - lua_setfield(L, -2, "openparens"); + lua_setfield(L, -2, "openParens"); node->expr->visit(this); lua_setfield(L, -2, "expression"); serializeToken(cstNode->closePosition, ")"); - lua_setfield(L, -2, "closeparens"); + lua_setfield(L, -2, "closeParens"); } void serializeType(Luau::AstTypeUnion* node) @@ -2363,13 +2363,13 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "group", "type"); serializeToken(node->location.begin, "("); - lua_setfield(L, -2, "openparens"); + lua_setfield(L, -2, "openParens"); node->type->visit(this); lua_setfield(L, -2, "type"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, ")"); - lua_setfield(L, -2, "closeparens"); + lua_setfield(L, -2, "closeParens"); } void serializeType(Luau::AstGenericType* node) @@ -2443,7 +2443,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(cstNode->openParenthesesPosition, "("); else lua_pushnil(L); - lua_setfield(L, -2, "openparens"); + lua_setfield(L, -2, "openParens"); serializePunctuated(node->typeList.types, cstNode->commaPositions, ","); lua_setfield(L, -2, "types"); @@ -2452,13 +2452,13 @@ struct AstSerialize : public Luau::AstVisitor node->typeList.tailType->visit(this); else lua_pushnil(L); - lua_setfield(L, -2, "tailtype"); + lua_setfield(L, -2, "tailType"); if (cstNode->hasParentheses) serializeToken(cstNode->closeParenthesesPosition, ")"); else lua_pushnil(L); - lua_setfield(L, -2, "closeparens"); + lua_setfield(L, -2, "closeParens"); } void serializeTypePack(Luau::AstTypePackGeneric* node) @@ -2906,7 +2906,7 @@ int luau_parse(lua_State* L) lua_pushnumber(L, serializer.lineOffsets[i] + 1); lua_settable(L, -3); } - lua_setfield(L, -2, "lineoffsets"); + lua_setfield(L, -2, "lineOffsets"); deepFreeze(L); diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index ec89faf75..9f2ab79f2 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -663,18 +663,18 @@ local function visitTypeReference(node: types.AstTypeReference, visitor: Visitor if node.prefix then visitToken(node.prefix, visitor) end - if node.prefixpoint then - visitToken(node.prefixpoint, visitor) + if node.prefixPoint then + visitToken(node.prefixPoint, visitor) end visitToken(node.name, visitor) - if node.openparameters then - visitToken(node.openparameters, visitor) + if node.openParameters then + visitToken(node.openParameters, visitor) end if node.parameters then visitPunctuated(node.parameters, visitor, visitTypeOrPack) end - if node.closeparameters then - visitToken(node.closeparameters, visitor) + if node.closeParameters then + visitToken(node.closeParameters, visitor) end end end @@ -694,17 +694,17 @@ end local function visitTypeTypeof(node: types.AstTypeTypeof, visitor: Visitor) if visitor.visitTypeTypeof(node) then visitToken(node.typeof, visitor) - visitToken(node.openparens, visitor) + visitToken(node.openParens, visitor) visitExpr(node.expression, visitor) - visitToken(node.closeparens, visitor) + visitToken(node.closeParens, visitor) end end local function visitTypeGroup(node: types.AstTypeGroup, visitor: Visitor) if visitor.visitTypeGroup(node) then - visitToken(node.openparens, visitor) + visitToken(node.openParens, visitor) visitType(node.type, visitor) - visitToken(node.closeparens, visitor) + visitToken(node.closeParens, visitor) end end @@ -734,30 +734,30 @@ end local function visitTypeArray(node: types.AstTypeArray, visitor: Visitor) if visitor.visitTypeArray(node) then - visitToken(node.openbrace, visitor) + visitToken(node.openBrace, visitor) if node.access then visitToken(node.access, visitor) end visitType(node.type, visitor) - visitToken(node.closebrace, visitor) + visitToken(node.closeBrace, visitor) end end local function visitTypeTable(node: types.AstTypeTable, visitor: Visitor) if visitor.visitTypeTable(node) then - visitToken(node.openbrace, visitor) + visitToken(node.openBrace, visitor) for _, entry in node.entries do if entry.access then visitToken(entry.access, visitor) end if entry.kind == "indexer" then - visitToken(entry.indexeropen, visitor) + visitToken(entry.indexerOpen, visitor) visitType(entry.key, visitor) - visitToken(entry.indexerclose, visitor) + visitToken(entry.indexerClose, visitor) elseif entry.kind == "stringproperty" then - visitToken(entry.indexeropen, visitor) + visitToken(entry.indexerOpen, visitor) visitTypeSingletonString(entry.key, visitor) - visitToken(entry.indexerclose, visitor) + visitToken(entry.indexerClose, visitor) else visitToken(entry.key, visitor) end @@ -767,7 +767,7 @@ local function visitTypeTable(node: types.AstTypeTable, visitor: Visitor) visitToken(entry.separator, visitor) end end - visitToken(node.closebrace, visitor) + visitToken(node.closeBrace, visitor) end end @@ -783,40 +783,40 @@ end local function visitTypeFunction(node: types.AstTypeFunction, visitor: Visitor) if visitor.visitTypeFunction(node) then - if node.opengenerics then - visitToken(node.opengenerics, visitor) + if node.openGenerics then + visitToken(node.openGenerics, visitor) end if node.generics then visitPunctuated(node.generics, visitor, visitGeneric) end - if node.genericpacks then - visitPunctuated(node.genericpacks, visitor, visitGenericPack) + if node.genericPacks then + visitPunctuated(node.genericPacks, visitor, visitGenericPack) end - if node.closegenerics then - visitToken(node.closegenerics, visitor) + if node.closeGenerics then + visitToken(node.closeGenerics, visitor) end - visitToken(node.openparens, visitor) + visitToken(node.openParens, visitor) visitPunctuated(node.parameters, visitor, visitFunctionTypeParameter) if node.vararg then visitTypePack(node.vararg, visitor) end - visitToken(node.closeparens, visitor) - visitToken(node.returnarrow, visitor) - visitTypePack(node.returntypes, visitor) + visitToken(node.closeParens, visitor) + visitToken(node.returnArrow, visitor) + visitTypePack(node.returnTypes, visitor) end end local function visitTypePackExplicit(node: types.AstTypePackExplicit, visitor: Visitor) if visitor.visitTypePackExplicit(node) then - if node.openparens then - visitToken(node.openparens, visitor) + if node.openParens then + visitToken(node.openParens, visitor) end visitPunctuated(node.types, visitor, visitType) - if node.tailtype then - visitTypePack(node.tailtype, visitor) + if node.tailType then + visitTypePack(node.tailType, visitor) end - if node.closeparens then - visitToken(node.closeparens, visitor) + if node.closeParens then + visitToken(node.closeParens, visitor) end end end diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 743d58bbe..aed6db3b1 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -148,27 +148,27 @@ test.suite("Parser", function(suite) local singleLineCode = "local x = 1" local result = parser.parse(singleLineCode) - assert.eq(type(result.lineoffsets), "table") - assert.eq(#result.lineoffsets, 1) - assert.eq(result.lineoffsets[1], 1) + assert.eq(type(result.lineOffsets), "table") + assert.eq(#result.lineOffsets, 1) + assert.eq(result.lineOffsets[1], 1) local multiLineCode = "local x = 1\nlocal y = 2\nlocal z = 3" local multiResult = parser.parse(multiLineCode) - assert.eq(type(multiResult.lineoffsets), "table") - assert.eq(#multiResult.lineoffsets, 3) - assert.eq(multiResult.lineoffsets[1], 1) - assert.eq(multiResult.lineoffsets[2], 13) - assert.eq(multiResult.lineoffsets[3], 25) + assert.eq(type(multiResult.lineOffsets), "table") + assert.eq(#multiResult.lineOffsets, 3) + assert.eq(multiResult.lineOffsets[1], 1) + assert.eq(multiResult.lineOffsets[2], 13) + assert.eq(multiResult.lineOffsets[3], 25) local emptyLineCode = "local x = 1\n\nlocal y = 2" local emptyResult = parser.parse(emptyLineCode) - assert.eq(type(emptyResult.lineoffsets), "table") - assert.eq(#emptyResult.lineoffsets, 3) - assert.eq(emptyResult.lineoffsets[1], 1) - assert.eq(emptyResult.lineoffsets[2], 13) - assert.eq(emptyResult.lineoffsets[3], 14) + assert.eq(type(emptyResult.lineOffsets), "table") + assert.eq(#emptyResult.lineOffsets, 3) + assert.eq(emptyResult.lineOffsets[1], 1) + assert.eq(emptyResult.lineOffsets[2], 13) + assert.eq(emptyResult.lineOffsets[3], 14) end) suite:case("canParseAttributes", function(assert) @@ -908,11 +908,11 @@ test.suite("parse types", function(suite) local typeRef = typeAlias.type :: syntax.AstTypeReference assert.eq(typeRef.prefix, nil) - assert.eq(typeRef.prefixpoint, nil) + assert.eq(typeRef.prefixPoint, nil) assert.eq(typeRef.name.text, "string") - assert.eq(typeRef.openparameters, nil) + assert.eq(typeRef.openParameters, nil) assert.eq(typeRef.parameters, nil) - assert.eq(typeRef.closeparameters, nil) + assert.eq(typeRef.closeParameters, nil) block = parser.parseblock("type MyTable = MyModule.Table") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias @@ -921,17 +921,17 @@ test.suite("parse types", function(suite) typeRef = typeAlias.type :: syntax.AstTypeReference assert.neq(typeRef.prefix, nil) assert.eq((typeRef.prefix :: syntax.Token).text, "MyModule") - assert.neq(typeRef.prefixpoint, nil) - assert.eq((typeRef.prefixpoint :: syntax.Token<".">).text, ".") + assert.neq(typeRef.prefixPoint, nil) + assert.eq((typeRef.prefixPoint :: syntax.Token<".">).text, ".") assert.eq(typeRef.name.text, "Table") - assert.neq(typeRef.openparameters, nil) - assert.eq((typeRef.openparameters :: syntax.Token<"<">).text, "<") + assert.neq(typeRef.openParameters, nil) + assert.eq((typeRef.openParameters :: syntax.Token<"<">).text, "<") assert.neq(typeRef.parameters, nil) assert.eq(#typeRef.parameters :: syntax.Punctuated, 2) assert.eq((typeRef.parameters :: syntax.Punctuated)[1].node.tag, "reference") assert.eq((typeRef.parameters :: syntax.Punctuated)[2].node.tag, "reference") - assert.neq(typeRef.closeparameters, nil) - assert.eq((typeRef.closeparameters :: syntax.Token<">">).text, ">") + assert.neq(typeRef.closeParameters, nil) + assert.eq((typeRef.closeParameters :: syntax.Token<">">).text, ">") end) suite:case("testTypeSingletonBoolean", function(assert) @@ -977,9 +977,9 @@ test.suite("parse types", function(suite) local typeofType = typeAlias.type :: syntax.AstTypeTypeof assert.eq(typeofType.typeof.text, "typeof") - assert.eq(typeofType.openparens.text, "(") + assert.eq(typeofType.openParens.text, "(") assert.eq(typeofType.expression.tag, "global") - assert.eq(typeofType.closeparens.text, ")") + assert.eq(typeofType.closeParens.text, ")") end) suite:case("testTypeGroup", function(assert) @@ -988,9 +988,9 @@ test.suite("parse types", function(suite) assert.eq(typeAlias.type.tag, "group") local groupType = typeAlias.type :: syntax.AstTypeGroup - assert.eq(groupType.openparens.text, "(") + assert.eq(groupType.openParens.text, "(") assert.eq(groupType.type.tag, "reference") - assert.eq(groupType.closeparens.text, ")") + assert.eq(groupType.closeParens.text, ")") end) suite:case("testTypeUnion", function(assert) @@ -1070,10 +1070,10 @@ test.suite("parse types", function(suite) assert.eq(typeAlias.type.tag, "array") local arrayType = typeAlias.type :: syntax.AstTypeArray - assert.eq(arrayType.openbrace.text, "{") + assert.eq(arrayType.openBrace.text, "{") assert.eq(arrayType.access, nil) assert.eq(arrayType.type.tag, "reference") - assert.eq(arrayType.closebrace.text, "}") + assert.eq(arrayType.closeBrace.text, "}") block = parser.parseblock("type T = { read string }") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias @@ -1098,9 +1098,9 @@ test.suite("parse types", function(suite) assert.eq(typeAlias.type.tag, "table") local tableType = typeAlias.type :: syntax.AstTypeTable - assert.eq(tableType.openbrace.text, "{") + assert.eq(tableType.openBrace.text, "{") assert.eq(#tableType.entries, 0) - assert.eq(tableType.closebrace.text, "}") + assert.eq(tableType.closeBrace.text, "}") block = parser.parseblock("type T = {[number] : string}") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias @@ -1111,9 +1111,9 @@ test.suite("parse types", function(suite) local tableTypeItem: any = tableType.entries[1] assert.eq(tableTypeItem.kind, "indexer") assert.eq(tableTypeItem.access, nil) - assert.eq(tableTypeItem.indexeropen.text, "[") + assert.eq(tableTypeItem.indexerOpen.text, "[") assert.eq(tableTypeItem.key.tag, "reference") - assert.eq(tableTypeItem.indexerclose.text, "]") + assert.eq(tableTypeItem.indexerClose.text, "]") assert.eq(tableTypeItem.colon.text, ":") assert.eq(tableTypeItem.value.tag, "reference") assert.eq(tableTypeItem.separator, nil) @@ -1126,11 +1126,11 @@ test.suite("parse types", function(suite) tableTypeItem = tableType.entries[1] assert.eq(tableTypeItem.kind, "stringproperty") assert.eq(tableTypeItem.access, nil) - assert.eq(tableTypeItem.indexeropen.text, "[") + assert.eq(tableTypeItem.indexerOpen.text, "[") assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).kind, "type") assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).tag, "string") assert.eq((tableTypeItem.key :: syntax.AstTypeSingletonString).token.text, "hello") - assert.eq(tableTypeItem.indexerclose.text, "]") + assert.eq(tableTypeItem.indexerClose.text, "]") assert.eq(tableTypeItem.colon.text, ":") assert.eq(tableTypeItem.value.tag, "string") assert.neq(tableTypeItem.separator, nil) @@ -1183,11 +1183,11 @@ test.suite("parse types", function(suite) assert.eq(typeAlias.type.tag, "function") local funcType = typeAlias.type :: syntax.AstTypeFunction - assert.eq(funcType.opengenerics, nil) + assert.eq(funcType.openGenerics, nil) assert.eq(funcType.generics, nil) - assert.eq(funcType.genericpacks, nil) - assert.eq(funcType.closegenerics, nil) - assert.eq(funcType.openparens.text, "(") + assert.eq(funcType.genericPacks, nil) + assert.eq(funcType.closeGenerics, nil) + assert.eq(funcType.openParens.text, "(") assert.eq(#funcType.parameters, 2) local param = funcType.parameters[1].node @@ -1203,29 +1203,29 @@ test.suite("parse types", function(suite) assert.eq(param.type.tag, "reference") assert.eq(funcType.vararg, nil) - assert.eq(funcType.closeparens.text, ")") - assert.eq(funcType.returnarrow.text, "->") - assert.eq(funcType.returntypes.kind, "typepack") + assert.eq(funcType.closeParens.text, ")") + assert.eq(funcType.returnArrow.text, "->") + assert.eq(funcType.returnTypes.kind, "typepack") block = parser.parseblock("type T = (item: T, ...U) -> ()") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") funcType = typeAlias.type :: syntax.AstTypeFunction - assert.neq(funcType.opengenerics, nil) - assert.eq((funcType.opengenerics :: syntax.Token<"<">).text, "<") + assert.neq(funcType.openGenerics, nil) + assert.eq((funcType.openGenerics :: syntax.Token<"<">).text, "<") assert.neq(funcType.generics, nil) assert.eq(#funcType.generics :: syntax.Punctuated, 1) - assert.neq(funcType.genericpacks, nil) - assert.eq(#funcType.genericpacks :: syntax.Punctuated, 1) - assert.eq((funcType.genericpacks :: syntax.Punctuated)[1].node.tag, "generic") + assert.neq(funcType.genericPacks, nil) + assert.eq(#funcType.genericPacks :: syntax.Punctuated, 1) + assert.eq((funcType.genericPacks :: syntax.Punctuated)[1].node.tag, "generic") - local genericTypePack = (funcType.genericpacks :: syntax.Punctuated)[1].node + local genericTypePack = (funcType.genericPacks :: syntax.Punctuated)[1].node assert.eq(genericTypePack.name.text, "U") assert.eq(genericTypePack.ellipsis.text, "...") - assert.neq(funcType.closegenerics, nil) - assert.eq((funcType.closegenerics :: syntax.Token<">">).text, ">") + assert.neq(funcType.closeGenerics, nil) + assert.eq((funcType.closeGenerics :: syntax.Token<">">).text, ">") assert.neq(funcType.vararg, nil) assert.eq((funcType.vararg :: syntax.AstTypePack).kind, "typepack") @@ -1234,9 +1234,9 @@ test.suite("parse types", function(suite) assert.eq(typeAlias.type.tag, "function") funcType = typeAlias.type :: syntax.AstTypeFunction - assert.eq(funcType.returntypes.kind, "typepack") - assert.eq(funcType.returntypes.tag, "variadic") - local variadicType: syntax.AstTypePackVariadic = funcType.returntypes :: any + assert.eq(funcType.returnTypes.kind, "typepack") + assert.eq(funcType.returnTypes.tag, "variadic") + local variadicType: syntax.AstTypePackVariadic = funcType.returnTypes :: any assert.neq(variadicType.ellipsis, nil) assert.eq((variadicType.ellipsis :: syntax.Token<"...">).text, "...") assert.eq(variadicType.type.tag, "reference") @@ -1802,7 +1802,7 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(tableType.kind, "type") assert.eq(tableType.tag, "table") assert.throwsWith(frozenTableMessage, function() - (tableType :: any).openbrace = nil + (tableType :: any).openBrace = nil end) assert.throwsWith(frozenTableMessage, function() table.insert((tableType :: any).entries, nil) @@ -1836,11 +1836,11 @@ test.suite("AST_nodes_frozen", function(suite) local block = parser.parseblock("type Foo = (number, string) -> (boolean, string)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local funcType = typeAlias.type :: syntax.AstTypeFunction - local typePack = funcType.returntypes :: syntax.AstTypePackExplicit + local typePack = funcType.returnTypes :: syntax.AstTypePackExplicit assert.eq(typePack.kind, "typepack") assert.eq(typePack.tag, "explicit") assert.throwsWith(frozenTableMessage, function() - (typePack :: any).openparens = nil + (typePack :: any).openParens = nil end) end) @@ -1848,7 +1848,7 @@ test.suite("AST_nodes_frozen", function(suite) local block = parser.parseblock("type Foo = () -> ...number") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local funcType = typeAlias.type :: syntax.AstTypeFunction - local typePack = funcType.returntypes :: syntax.AstTypePackVariadic + local typePack = funcType.returnTypes :: syntax.AstTypePackVariadic assert.eq(typePack.kind, "typepack") assert.eq(typePack.tag, "variadic") assert.throwsWith(frozenTableMessage, function() @@ -1931,7 +1931,7 @@ test.suite("AST_nodes_frozen", function(suite) (result :: any).root = nil end) assert.throwsWith(frozenTableMessage, function() - table.insert((result :: any).lineoffsets, nil) + table.insert((result :: any).lineOffsets, nil) end) end) diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 3b89f0cd0..28d23b4d1 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -89,13 +89,13 @@ test.suite("SyntaxPrinter", function(suite) local expected = [[type t = (...number) -> string, ...string)]] local stat = syntax.parseblock("type t = () -> (string, ...string)") - local tp = ((stat.statements[1] :: syntaxTypes.AstStatTypeAlias).type :: syntaxTypes.AstTypeFunction).returntypes + local tp = ((stat.statements[1] :: syntaxTypes.AstStatTypeAlias).type :: syntaxTypes.AstTypeFunction).returnTypes checkReplacement(source, expected, function(ast) return query .findallfromroot(ast :: any, syntaxUtils.isStatTypeAlias) -- LUAUFIX: Remove any cast once code too complex no longer emitted :map(function(ta) - return if ta.type.tag == "function" then ta.type.returntypes else nil + return if ta.type.tag == "function" then ta.type.returnTypes else nil end) :replace(function(_) return printer.printnode(tp) From dc745a67a1f8c24aa50ab6cc8d60ed8b025b27a1 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 31 Mar 2026 10:10:36 -0700 Subject: [PATCH 432/642] Refactor stat AST types (#916) Part of a larger refactor of AST types so that property names are camelCased --- definitions/luau.luau | 64 ++++---- .../commands/lint/rules/empty_if_block.luau | 30 ++-- lute/luau/src/luau.cpp | 70 ++++----- lute/std/libs/syntax/visitor.luau | 76 +++++----- tests/std/syntax/parser.test.luau | 142 +++++++++--------- 5 files changed, 191 insertions(+), 191 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 94d6ef7f5..5220f44a7 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -316,50 +316,50 @@ export type AstStatDo = { read location: Span, read kind: "stat", read tag: "do", - read dokeyword: Token<"do">, + read doKeyword: Token<"do">, read body: AstStatBlock, - read endkeyword: Token<"end">, + read endKeyword: Token<"end">, } export type AstElseIfStat = { - read elseifkeyword: Token<"elseif">, + read elseIfKeyword: Token<"elseif">, read condition: AstExpr, - read thenkeyword: Token<"then">, - read thenblock: AstStatBlock, + read thenKeyword: Token<"then">, + read thenBlock: AstStatBlock, } export type AstStatIf = { read location: Span, read kind: "stat", read tag: "conditional", - read ifkeyword: Token<"if">, + read ifKeyword: Token<"if">, read condition: AstExpr, - read thenkeyword: Token<"then">, - read thenblock: AstStatBlock, + read thenKeyword: Token<"then">, + read thenBlock: AstStatBlock, read elseifs: { AstElseIfStat }, - read elsekeyword: Token<"else">?, -- TODO: This could be elseif! - read elseblock: AstStatBlock?, - read endkeyword: Token<"end">, + read elseKeyword: Token<"else">?, -- TODO: This could be elseif! + read elseBlock: AstStatBlock?, + read endKeyword: Token<"end">, } export type AstStatWhile = { read location: Span, read kind: "stat", read tag: "while", - read whilekeyword: Token<"while">, + read whileKeyword: Token<"while">, read condition: AstExpr, - read dokeyword: Token<"do">, + read doKeyword: Token<"do">, read body: AstStatBlock, - read endkeyword: Token<"end">, + read endKeyword: Token<"end">, } export type AstStatRepeat = { read location: Span, read kind: "stat", read tag: "repeat", - read repeatkeyword: Token<"repeat">, + read repeatKeyword: Token<"repeat">, read body: AstStatBlock, - read untilkeyword: Token<"until">, + read untilKeyword: Token<"until">, read condition: AstExpr, } @@ -376,7 +376,7 @@ export type AstStatReturn = { read location: Span, read kind: "stat", read tag: "return", - read returnkeyword: Token<"return">, + read returnKeyword: Token<"return">, read expressions: Punctuated, } @@ -391,7 +391,7 @@ export type AstStatLocal = { read location: Span, read kind: "stat", read tag: "local", - read localkeyword: Token<"local">, + read localKeyword: Token<"local">, read variables: Punctuated, read equals: Token<"=">?, read values: Punctuated, @@ -401,30 +401,30 @@ export type AstStatFor = { read location: Span, read kind: "stat", read tag: "for", - read forkeyword: Token<"for">, + read forKeyword: Token<"for">, read variable: AstLocal, read equals: Token<"=">, read from: AstExpr, - read tocomma: Token<",">, + read toComma: Token<",">, read to: AstExpr, - read stepcomma: Token<",">?, + read stepComma: Token<",">?, read step: AstExpr?, - read dokeyword: Token<"do">, + read doKeyword: Token<"do">, read body: AstStatBlock, - read endkeyword: Token<"end">, + read endKeyword: Token<"end">, } export type AstStatForIn = { read location: Span, read kind: "stat", read tag: "forin", - read forkeyword: Token<"for">, + read forKeyword: Token<"for">, read variables: Punctuated, - read inkeyword: Token<"in">, + read inKeyword: Token<"in">, read values: Punctuated, - read dokeyword: Token<"do">, + read doKeyword: Token<"do">, read body: AstStatBlock, - read endkeyword: Token<"end">, + read endKeyword: Token<"end">, } export type AstStatAssign = { @@ -463,7 +463,7 @@ export type AstStatLocalFunction = { read location: Span, read kind: "stat", read tag: "localfunction", - read localkeyword: Token<"local">, + read localKeyword: Token<"local">, read name: AstLocal, read func: AstExprFunction, } @@ -473,12 +473,12 @@ export type AstStatTypeAlias = { read kind: "stat", read tag: "typealias", read export: Token<"export">?, - read typetoken: Token<"type">, + read typeToken: Token<"type">, read name: Token, - read opengenerics: Token<"<">?, + read openGenerics: Token<"<">?, read generics: Punctuated?, - read genericpacks: Punctuated?, - read closegenerics: Token<">">?, + read genericPacks: Punctuated?, + read closeGenerics: Token<">">?, read equals: Token<"=">, read type: AstType, } diff --git a/lute/cli/commands/lint/rules/empty_if_block.luau b/lute/cli/commands/lint/rules/empty_if_block.luau index d51362bc8..6cbe0681a 100644 --- a/lute/cli/commands/lint/rules/empty_if_block.luau +++ b/lute/cli/commands/lint/rules/empty_if_block.luau @@ -57,13 +57,13 @@ local function lint( -- Check if the then block is empty if isBlockEmpty( - ifStat.thenblock, + ifStat.thenBlock, commentsCount, - ifStat.thenkeyword.trailingTrivia, + ifStat.thenKeyword.trailingTrivia, if #ifStat.elseifs > 0 -- trailingToken - then ifStat.elseifs[1].elseifkeyword.leadingTrivia - elseif ifStat.elsekeyword then ifStat.elsekeyword.leadingTrivia - else ifStat.endkeyword.leadingTrivia + then ifStat.elseifs[1].elseIfKeyword.leadingTrivia + elseif ifStat.elseKeyword then ifStat.elseKeyword.leadingTrivia + else ifStat.endKeyword.leadingTrivia ) then return { @@ -80,13 +80,13 @@ local function lint( for i, elseIfStat in ifStat.elseifs do if isBlockEmpty( - elseIfStat.thenblock, + elseIfStat.thenBlock, commentsCount, - elseIfStat.thenkeyword.trailingTrivia, + elseIfStat.thenKeyword.trailingTrivia, if i < #ifStat.elseifs -- trailingToken - then ifStat.elseifs[i + 1].elseifkeyword.leadingTrivia - elseif ifStat.elsekeyword then ifStat.elsekeyword.leadingTrivia - else ifStat.endkeyword.leadingTrivia + then ifStat.elseifs[i + 1].elseIfKeyword.leadingTrivia + elseif ifStat.elseKeyword then ifStat.elseKeyword.leadingTrivia + else ifStat.endKeyword.leadingTrivia ) then return { @@ -101,14 +101,14 @@ local function lint( end -- Check else block if it exists - if ifStat.elseblock then - assert(ifStat.elsekeyword, "elseblock should have an elsekeyword") + if ifStat.elseBlock then + assert(ifStat.elseKeyword, "elseblock should have an elsekeyword") if isBlockEmpty( - ifStat.elseblock, + ifStat.elseBlock, commentsCount, - ifStat.elsekeyword.trailingTrivia, - ifStat.endkeyword.leadingTrivia + ifStat.elseKeyword.trailingTrivia, + ifStat.endKeyword.leadingTrivia ) then return { diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index c57da5eee..d61d0eac5 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -1391,7 +1391,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "do", "stat"); serializeToken(node->location.begin, "do"); - lua_setfield(L, -2, "dokeyword"); + lua_setfield(L, -2, "doKeyword"); // In lieu of a C++ AstStatBlock object to recurse on, manually construct a Luau AstStatBlock table lua_createtable(L, 0, preambleSize + 1); @@ -1419,7 +1419,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "body"); serializeToken(cstDo->endPosition, "end"); - lua_setfield(L, -2, "endkeyword"); + lua_setfield(L, -2, "endKeyword"); } else { @@ -1441,16 +1441,16 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "conditional", "stat"); serializeToken(node->location.begin, "if"); - lua_setfield(L, -2, "ifkeyword"); + lua_setfield(L, -2, "ifKeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); serializeToken(node->thenLocation->begin, "then"); - lua_setfield(L, -2, "thenkeyword"); + lua_setfield(L, -2, "thenKeyword"); node->thenbody->visit(this); - lua_setfield(L, -2, "thenblock"); + lua_setfield(L, -2, "thenBlock"); lua_createtable(L, 0, 0); int i = 0; @@ -1460,16 +1460,16 @@ struct AstSerialize : public Luau::AstVisitor auto elseif = node->elsebody->as(); serializeToken(elseif->location.begin, "elseif"); - lua_setfield(L, -2, "elseifkeyword"); + lua_setfield(L, -2, "elseIfKeyword"); elseif->condition->visit(this); lua_setfield(L, -2, "condition"); serializeToken(elseif->thenLocation->begin, "then"); - lua_setfield(L, -2, "thenkeyword"); + lua_setfield(L, -2, "thenKeyword"); elseif->thenbody->visit(this); - lua_setfield(L, -2, "thenblock"); + lua_setfield(L, -2, "thenBlock"); lua_rawseti(L, -2, i + 1); node = elseif; @@ -1481,24 +1481,24 @@ struct AstSerialize : public Luau::AstVisitor { LUTE_ASSERT(node->elseLocation); serializeToken(node->elseLocation->begin, "else"); - lua_setfield(L, -2, "elsekeyword"); + lua_setfield(L, -2, "elseKeyword"); node->elsebody->visit(this); - lua_setfield(L, -2, "elseblock"); + lua_setfield(L, -2, "elseBlock"); serializeToken(node->elsebody->location.end, "end"); - lua_setfield(L, -2, "endkeyword"); + lua_setfield(L, -2, "endKeyword"); } else { lua_pushnil(L); - lua_setfield(L, -2, "elsekeyword"); + lua_setfield(L, -2, "elseKeyword"); lua_pushnil(L); - lua_setfield(L, -2, "elseblock"); + lua_setfield(L, -2, "elseBlock"); serializeToken(node->thenbody->location.end, "end"); - lua_setfield(L, -2, "endkeyword"); + lua_setfield(L, -2, "endKeyword"); } } @@ -1510,7 +1510,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "while", "stat"); serializeToken(node->location.begin, "while"); - lua_setfield(L, -2, "whilekeyword"); + lua_setfield(L, -2, "whileKeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1519,13 +1519,13 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->doLocation.begin, "do"); else lua_pushnil(L); - lua_setfield(L, -2, "dokeyword"); + lua_setfield(L, -2, "doKeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endkeyword"); + lua_setfield(L, -2, "endKeyword"); } void serializeStat(Luau::AstStatRepeat* node) @@ -1536,14 +1536,14 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "repeat", "stat"); serializeToken(node->location.begin, "repeat"); - lua_setfield(L, -2, "repeatkeyword"); + lua_setfield(L, -2, "repeatKeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); auto cstNode = lookupCstNode(node); serializeToken(cstNode->untilPosition, "until"); - lua_setfield(L, -2, "untilkeyword"); + lua_setfield(L, -2, "untilKeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1579,7 +1579,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "return", "stat"); serializeToken(node->location.begin, "return"); - lua_setfield(L, -2, "returnkeyword"); + lua_setfield(L, -2, "returnKeyword"); const auto cstNode = lookupCstNode(node); serializePunctuated(node->list, cstNode->commaPositions, ","); @@ -1605,7 +1605,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "local", "stat"); serializeToken(node->location.begin, "local"); - lua_setfield(L, -2, "localkeyword"); + lua_setfield(L, -2, "localKeyword"); const auto cstNode = lookupCstNode(node); serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); @@ -1631,7 +1631,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "for", "stat"); serializeToken(node->location.begin, "for"); - lua_setfield(L, -2, "forkeyword"); + lua_setfield(L, -2, "forKeyword"); serialize(node->var, /* createToken= */ true, std::make_optional(cstNode->annotationColonPosition)); lua_setfield(L, -2, "variable"); @@ -1643,7 +1643,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "from"); serializeToken(cstNode->endCommaPosition, ","); - lua_setfield(L, -2, "tocomma"); + lua_setfield(L, -2, "toComma"); node->to->visit(this); lua_setfield(L, -2, "to"); @@ -1651,7 +1651,7 @@ struct AstSerialize : public Luau::AstVisitor if (cstNode->stepCommaPosition) { serializeToken(*cstNode->stepCommaPosition, ","); - lua_setfield(L, -2, "stepcomma"); + lua_setfield(L, -2, "stepComma"); } if (node->step) @@ -1664,13 +1664,13 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->doLocation.begin, "do"); else lua_pushnil(L); - lua_setfield(L, -2, "dokeyword"); + lua_setfield(L, -2, "doKeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endkeyword"); + lua_setfield(L, -2, "endKeyword"); } void serializeStat(Luau::AstStatForIn* node) @@ -1683,7 +1683,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "forin", "stat"); serializeToken(node->location.begin, "for"); - lua_setfield(L, -2, "forkeyword"); + lua_setfield(L, -2, "forKeyword"); serializePunctuated(node->vars, cstNode->varsCommaPositions, ",", cstNode->varsAnnotationColonPositions); lua_setfield(L, -2, "variables"); @@ -1692,7 +1692,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->inLocation.begin, "in"); else lua_pushnil(L); - lua_setfield(L, -2, "inkeyword"); + lua_setfield(L, -2, "inKeyword"); serializePunctuated(node->values, cstNode->valuesCommaPositions, ","); lua_setfield(L, -2, "values"); @@ -1701,13 +1701,13 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->doLocation.begin, "do"); else lua_pushnil(L); - lua_setfield(L, -2, "dokeyword"); + lua_setfield(L, -2, "doKeyword"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endkeyword"); + lua_setfield(L, -2, "endKeyword"); } void serializeStat(Luau::AstStatAssign* node) @@ -1781,7 +1781,7 @@ struct AstSerialize : public Luau::AstVisitor const auto cstNode = lookupCstNode(node); serializeToken(cstNode->localKeywordPosition, "local"); - lua_setfield(L, -3, "localkeyword"); + lua_setfield(L, -3, "localKeyword"); serializeToken(cstNode->functionKeywordPosition, "function"); @@ -1815,7 +1815,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "export"); serializeToken(cstNode->typeKeywordPosition, "type"); - lua_setfield(L, -2, "typetoken"); + lua_setfield(L, -2, "typeToken"); serializeToken(node->nameLocation.begin, node->name.value); lua_setfield(L, -2, "name"); @@ -1823,17 +1823,17 @@ struct AstSerialize : public Luau::AstVisitor if (node->generics.size > 0 || node->genericPacks.size > 0) { serializeToken(cstNode->genericsOpenPosition, "<"); - lua_setfield(L, -2, "opengenerics"); + lua_setfield(L, -2, "openGenerics"); auto commas = cstNode->genericsCommaPositions; serializePunctuated(node->generics, commas, ","); lua_setfield(L, -2, "generics"); serializePunctuated(node->genericPacks, splitArray(commas, node->generics.size), ","); - lua_setfield(L, -2, "genericpacks"); + lua_setfield(L, -2, "genericPacks"); serializeToken(cstNode->genericsClosePosition, ">"); - lua_setfield(L, -2, "closegenerics"); + lua_setfield(L, -2, "closeGenerics"); } serializeToken(cstNode->equalsPosition, "="); diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 9f2ab79f2..b392e45ed 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -201,49 +201,49 @@ end local function visitStatDo(node: types.AstStatDo, visitor: Visitor) if visitor.visitStatDo(node) then - visitToken(node.dokeyword, visitor) + visitToken(node.doKeyword, visitor) visitStatBlock(node.body, visitor) - visitToken(node.endkeyword, visitor) + visitToken(node.endKeyword, visitor) end end local function visitStatIf(node: types.AstStatIf, visitor: Visitor) if visitor.visitStatIf(node) then - visitToken(node.ifkeyword, visitor) + visitToken(node.ifKeyword, visitor) visitExpr(node.condition, visitor) - visitToken(node.thenkeyword, visitor) - visitStatBlock(node.thenblock, visitor) + visitToken(node.thenKeyword, visitor) + visitStatBlock(node.thenBlock, visitor) for _, elseifNode in node.elseifs do - visitToken(elseifNode.elseifkeyword, visitor) + visitToken(elseifNode.elseIfKeyword, visitor) visitExpr(elseifNode.condition, visitor) - visitToken(elseifNode.thenkeyword, visitor) - visitStatBlock(elseifNode.thenblock, visitor) + visitToken(elseifNode.thenKeyword, visitor) + visitStatBlock(elseifNode.thenBlock, visitor) end - if node.elsekeyword then - visitToken(node.elsekeyword, visitor) + if node.elseKeyword then + visitToken(node.elseKeyword, visitor) end - if node.elseblock then - visitStatBlock(node.elseblock, visitor) + if node.elseBlock then + visitStatBlock(node.elseBlock, visitor) end - visitToken(node.endkeyword, visitor) + visitToken(node.endKeyword, visitor) end end local function visitStatWhile(node: types.AstStatWhile, visitor: Visitor) if visitor.visitStatWhile(node) then - visitToken(node.whilekeyword, visitor) + visitToken(node.whileKeyword, visitor) visitExpr(node.condition, visitor) - visitToken(node.dokeyword, visitor) + visitToken(node.doKeyword, visitor) visitStatBlock(node.body, visitor) - visitToken(node.endkeyword, visitor) + visitToken(node.endKeyword, visitor) end end local function visitStatRepeat(node: types.AstStatRepeat, visitor: Visitor) if visitor.visitStatRepeat(node) then - visitToken(node.repeatkeyword, visitor) + visitToken(node.repeatKeyword, visitor) visitStatBlock(node.body, visitor) - visitToken(node.untilkeyword, visitor) + visitToken(node.untilKeyword, visitor) visitExpr(node.condition, visitor) end end @@ -262,14 +262,14 @@ end local function visitStatReturn(node: types.AstStatReturn, visitor: Visitor) if visitor.visitStatReturn(node) then - visitToken(node.returnkeyword, visitor) + visitToken(node.returnKeyword, visitor) visitPunctuated(node.expressions, visitor, visitExpr) end end local function visitLocalStatement(node: types.AstStatLocal, visitor: Visitor) if visitor.visitStatLocalDeclaration(node) then - visitToken(node.localkeyword, visitor) + visitToken(node.localKeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) if node.equals then visitToken(node.equals, visitor) @@ -282,34 +282,34 @@ end local function visitStatFor(node: types.AstStatFor, visitor: Visitor) if visitor.visitStatFor(node) then - visitToken(node.forkeyword, visitor) + visitToken(node.forKeyword, visitor) visitLocal(node.variable, visitor) visitToken(node.equals, visitor) visitExpr(node.from, visitor) - visitToken(node.tocomma, visitor) + visitToken(node.toComma, visitor) visitExpr(node.to, visitor) - if node.stepcomma then - visitToken(node.stepcomma, visitor) + if node.stepComma then + visitToken(node.stepComma, visitor) end if node.step then visitExpr(node.step, visitor) end - visitToken(node.dokeyword, visitor) + visitToken(node.doKeyword, visitor) visitStatBlock(node.body, visitor) - visitToken(node.endkeyword, visitor) + visitToken(node.endKeyword, visitor) visitor.visitStatForEnd(node) end end local function visitStatForIn(node: types.AstStatForIn, visitor: Visitor) if visitor.visitStatForIn(node) then - visitToken(node.forkeyword, visitor) + visitToken(node.forKeyword, visitor) visitPunctuated(node.variables, visitor, visitLocal) - visitToken(node.inkeyword, visitor) + visitToken(node.inKeyword, visitor) visitPunctuated(node.values, visitor, visitExpr) - visitToken(node.dokeyword, visitor) + visitToken(node.doKeyword, visitor) visitStatBlock(node.body, visitor) - visitToken(node.endkeyword, visitor) + visitToken(node.endKeyword, visitor) visitor.visitStatForInEnd(node) end end @@ -356,19 +356,19 @@ local function visitStatTypeAlias(node: types.AstStatTypeAlias, visitor: Visitor if node.export then visitToken(node.export, visitor) end - visitToken(node.typetoken, visitor) + visitToken(node.typeToken, visitor) visitToken(node.name, visitor) - if node.opengenerics then - visitToken(node.opengenerics, visitor) + if node.openGenerics then + visitToken(node.openGenerics, visitor) end if node.generics then visitPunctuated(node.generics, visitor, visitGeneric) end - if node.genericpacks then - visitPunctuated(node.genericpacks, visitor, visitGenericPack) + if node.genericPacks then + visitPunctuated(node.genericPacks, visitor, visitGenericPack) end - if node.closegenerics then - visitToken(node.closegenerics, visitor) + if node.closeGenerics then + visitToken(node.closeGenerics, visitor) end visitToken(node.equals, visitor) visitType(node.type, visitor) @@ -536,7 +536,7 @@ local function visitStatLocalFunction(node: types.AstStatLocalFunction, visitor: for _, attribute in node.func.attributes do visitAttribute(attribute, visitor) end - visitToken(node.localkeyword, visitor) + visitToken(node.localKeyword, visitor) visitToken(node.func.functionkeyword, visitor) visitLocal(node.name, visitor) visitExprFunction(node.func, visitor, false, false) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index aed6db3b1..91e1ae293 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -29,7 +29,7 @@ test.suite("Parser", function(suite) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: syntax.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localKeyword assert.eq(#token.leadingTrivia, 1) assert.eq(token.leadingTrivia[1].tag, "whitespace") assert.eq(token.leadingTrivia[1].text, " ") @@ -43,7 +43,7 @@ test.suite("Parser", function(suite) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: syntax.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localKeyword assert.eq(#token.leadingTrivia, 1) assert.eq(token.leadingTrivia[1].tag, "whitespace") assert.eq(token.leadingTrivia[1].text, "\n") @@ -57,7 +57,7 @@ test.suite("Parser", function(suite) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: syntax.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localKeyword assert.eq(#token.leadingTrivia, 2) assert.eq(token.leadingTrivia[1].tag, "comment") assert.eq(token.leadingTrivia[1].text, "-- comment") @@ -74,7 +74,7 @@ test.suite("Parser", function(suite) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: syntax.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localKeyword assert.eq(#token.leadingTrivia, 2) assert.eq(token.leadingTrivia[1].tag, "blockcomment") assert.eq(token.leadingTrivia[1].text, "--[[ comment ]]") @@ -91,7 +91,7 @@ test.suite("Parser", function(suite) local l = block.statements[1] assert.eq(l.tag, "local") - local token = (l :: syntax.AstStatLocal).localkeyword + local token = (l :: syntax.AstStatLocal).localKeyword assert.eq(#token.leadingTrivia, 3) assert.eq(token.leadingTrivia[1].text, " \n") assert.eq(token.leadingTrivia[2].text, "\t\t\n") @@ -116,7 +116,7 @@ test.suite("Parser", function(suite) local secondStmt = block.statements[2] assert.eq(secondStmt.tag, "local") - local leadingToken = (secondStmt :: syntax.AstStatLocal).localkeyword + local leadingToken = (secondStmt :: syntax.AstStatLocal).localKeyword assert.eq(#leadingToken.leadingTrivia, 2) assert.eq(leadingToken.leadingTrivia[1].text, "-- comment 2") assert.eq(leadingToken.leadingTrivia[2].text, "\n") @@ -567,10 +567,10 @@ test.suite("parse", function(suite) local doStat = block.statements[1] :: syntax.AstStatDo assert.eq(doStat.tag, "do") - assert.eq(doStat.dokeyword.text, "do") + assert.eq(doStat.doKeyword.text, "do") checkIsBlock(doStat.body, assert) assert.eq(#doStat.body.statements, 1) - assert.eq(doStat.endkeyword.text, "end") + assert.eq(doStat.endKeyword.text, "end") end) suite:case("parseIfStatement", function(assert) @@ -581,40 +581,40 @@ test.suite("parse", function(suite) local ifStat = block.statements[1] :: syntax.AstStatIf assert.eq(ifStat.tag, "conditional") - assert.eq(ifStat.ifkeyword.text, "if") + assert.eq(ifStat.ifKeyword.text, "if") assert.eq(ifStat.condition.tag, "binary") - assert.eq(ifStat.thenkeyword.text, "then") - checkIsBlock(ifStat.thenblock, assert) + assert.eq(ifStat.thenKeyword.text, "then") + checkIsBlock(ifStat.thenBlock, assert) - local thenBlock = ifStat.thenblock + local thenBlock = ifStat.thenBlock assert.eq(#thenBlock.statements, 1) assert.eq(#ifStat.elseifs, 1) local elseIf = ifStat.elseifs[1] - assert.eq(elseIf.elseifkeyword.text, "elseif") + assert.eq(elseIf.elseIfKeyword.text, "elseif") assert.eq(elseIf.condition.tag, "binary") - assert.eq(elseIf.thenkeyword.text, "then") - checkIsBlock(elseIf.thenblock, assert) + assert.eq(elseIf.thenKeyword.text, "then") + checkIsBlock(elseIf.thenBlock, assert) - assert.neq(ifStat.elsekeyword, nil) - assert.eq((ifStat.elsekeyword :: syntax.Token<"else">).text, "else") - assert.neq(ifStat.elseblock, nil) - checkIsBlock(ifStat.elseblock, assert) - assert.eq(ifStat.endkeyword.text, "end") + assert.neq(ifStat.elseKeyword, nil) + assert.eq((ifStat.elseKeyword :: syntax.Token<"else">).text, "else") + assert.neq(ifStat.elseBlock, nil) + checkIsBlock(ifStat.elseBlock, assert) + assert.eq(ifStat.endKeyword.text, "end") block = parser.parseblock("if condition then doSomething() end") assert.eq(#block.statements, 1) local simpleIfStat = block.statements[1] :: syntax.AstStatIf assert.eq(simpleIfStat.tag, "conditional") - assert.eq(simpleIfStat.ifkeyword.text, "if") + assert.eq(simpleIfStat.ifKeyword.text, "if") assert.eq(simpleIfStat.condition.tag, "global") - assert.eq(simpleIfStat.thenkeyword.text, "then") - checkIsBlock(simpleIfStat.thenblock, assert) - assert.eq(#simpleIfStat.thenblock.statements, 1) + assert.eq(simpleIfStat.thenKeyword.text, "then") + checkIsBlock(simpleIfStat.thenBlock, assert) + assert.eq(#simpleIfStat.thenBlock.statements, 1) assert.eq(#simpleIfStat.elseifs, 0) - assert.eq(simpleIfStat.elsekeyword, nil) - assert.eq(simpleIfStat.elseblock, nil) - assert.eq(simpleIfStat.endkeyword.text, "end") + assert.eq(simpleIfStat.elseKeyword, nil) + assert.eq(simpleIfStat.elseBlock, nil) + assert.eq(simpleIfStat.endKeyword.text, "end") end) suite:case("parseStatWhile", function(assert) @@ -624,12 +624,12 @@ test.suite("parse", function(suite) local whileStat = block.statements[1] :: syntax.AstStatWhile assert.eq(whileStat.tag, "while") - assert.eq(whileStat.whilekeyword.text, "while") + assert.eq(whileStat.whileKeyword.text, "while") assert.eq(whileStat.condition.tag, "binary") - assert.eq(whileStat.dokeyword.text, "do") + assert.eq(whileStat.doKeyword.text, "do") checkIsBlock(whileStat.body, assert) assert.eq(#whileStat.body.statements, 2) - assert.eq(whileStat.endkeyword.text, "end") + assert.eq(whileStat.endKeyword.text, "end") block = parser.parseblock("while true do break end") checkIsBlock(block, assert) @@ -637,9 +637,9 @@ test.suite("parse", function(suite) whileStat = block.statements[1] :: syntax.AstStatWhile assert.eq(whileStat.tag, "while") - assert.eq(whileStat.whilekeyword.text, "while") + assert.eq(whileStat.whileKeyword.text, "while") assert.eq(whileStat.condition.tag, "boolean") - assert.eq(whileStat.dokeyword.text, "do") + assert.eq(whileStat.doKeyword.text, "do") checkIsBlock(whileStat.body, assert) assert.eq(#whileStat.body.statements, 1) @@ -649,7 +649,7 @@ test.suite("parse", function(suite) assert.eq(breakStat.tag, "break") assert.eq(breakStat.token.text, "break") - assert.eq(whileStat.endkeyword.text, "end") + assert.eq(whileStat.endKeyword.text, "end") block = parser.parseblock("while true do continue end") checkIsBlock(block, assert) @@ -657,9 +657,9 @@ test.suite("parse", function(suite) whileStat = block.statements[1] :: syntax.AstStatWhile assert.eq(whileStat.tag, "while") - assert.eq(whileStat.whilekeyword.text, "while") + assert.eq(whileStat.whileKeyword.text, "while") assert.eq(whileStat.condition.tag, "boolean") - assert.eq(whileStat.dokeyword.text, "do") + assert.eq(whileStat.doKeyword.text, "do") checkIsBlock(whileStat.body, assert) assert.eq(#whileStat.body.statements, 1) @@ -669,7 +669,7 @@ test.suite("parse", function(suite) assert.eq(continueStat.tag, "continue") assert.eq(continueStat.token.text, "continue") - assert.eq(whileStat.endkeyword.text, "end") + assert.eq(whileStat.endKeyword.text, "end") end) suite:case("parseStatRepeat", function(assert) @@ -679,10 +679,10 @@ test.suite("parse", function(suite) local repeatStat = block.statements[1] :: syntax.AstStatRepeat assert.eq(repeatStat.tag, "repeat") - assert.eq(repeatStat.repeatkeyword.text, "repeat") + assert.eq(repeatStat.repeatKeyword.text, "repeat") checkIsBlock(repeatStat.body, assert) assert.eq(#repeatStat.body.statements, 2) - assert.eq(repeatStat.untilkeyword.text, "until") + assert.eq(repeatStat.untilKeyword.text, "until") assert.eq(repeatStat.condition.tag, "binary") end) @@ -694,7 +694,7 @@ test.suite("parse", function(suite) local l = block.statements[1] :: syntax.AstStatLocal assert.eq(l.tag, "local") - assert.eq(l.localkeyword.text, "local") + assert.eq(l.localKeyword.text, "local") assert.eq(#l.variables, 2) assert.eq(l.variables[1].node.name.text, "b") assert.neq(l.variables[1].separator, nil) @@ -717,7 +717,7 @@ test.suite("parse", function(suite) l = block.statements[1] :: syntax.AstStatLocal assert.eq(l.tag, "local") - assert.eq(l.localkeyword.text, "local") + assert.eq(l.localKeyword.text, "local") assert.eq(#l.variables, 1) assert.eq(l.variables[1].node.name.text, "x") assert.eq(l.variables[1].separator, nil) @@ -732,20 +732,20 @@ test.suite("parse", function(suite) local forStat = block.statements[1] :: syntax.AstStatFor assert.eq(forStat.tag, "for") - assert.eq(forStat.forkeyword.text, "for") + assert.eq(forStat.forKeyword.text, "for") assert.eq(forStat.variable.name.text, "i") assert.eq(forStat.equals.text, "=") assert.eq(forStat.from.tag, "number") - assert.eq(forStat.tocomma.text, ",") + assert.eq(forStat.toComma.text, ",") assert.eq(forStat.to.tag, "number") - assert.neq(forStat.stepcomma, nil) - assert.eq((forStat.stepcomma :: syntax.Token<",">).text, ",") + assert.neq(forStat.stepComma, nil) + assert.eq((forStat.stepComma :: syntax.Token<",">).text, ",") assert.neq(forStat.step, nil) assert.eq(((forStat.step :: any) :: syntax.AstExpr).tag, "number") - assert.eq(forStat.dokeyword.text, "do") + assert.eq(forStat.doKeyword.text, "do") checkIsBlock(forStat.body, assert) assert.eq(#forStat.body.statements, 1) - assert.eq(forStat.endkeyword.text, "end") + assert.eq(forStat.endKeyword.text, "end") block = parser.parseblock("for j = 0, 5 do print(j) end") assert.eq(block.tag, "block") @@ -753,18 +753,18 @@ test.suite("parse", function(suite) forStat = block.statements[1] :: syntax.AstStatFor assert.eq(forStat.tag, "for") - assert.eq(forStat.forkeyword.text, "for") + assert.eq(forStat.forKeyword.text, "for") assert.eq(forStat.variable.name.text, "j") assert.eq(forStat.equals.text, "=") assert.eq(forStat.from.tag, "number") - assert.eq(forStat.tocomma.text, ",") + assert.eq(forStat.toComma.text, ",") assert.eq(forStat.to.tag, "number") - assert.eq(forStat.stepcomma, nil) + assert.eq(forStat.stepComma, nil) assert.eq(forStat.step, nil) - assert.eq(forStat.dokeyword.text, "do") + assert.eq(forStat.doKeyword.text, "do") checkIsBlock(forStat.body, assert) assert.eq(#forStat.body.statements, 1) - assert.eq(forStat.endkeyword.text, "end") + assert.eq(forStat.endKeyword.text, "end") end) suite:case("parseForInLoop", function(assert) @@ -774,17 +774,17 @@ test.suite("parse", function(suite) local forInStat = block.statements[1] :: syntax.AstStatForIn assert.eq(forInStat.tag, "forin") - assert.eq(forInStat.forkeyword.text, "for") + assert.eq(forInStat.forKeyword.text, "for") assert.eq(#forInStat.variables, 2) assert.eq(forInStat.variables[1].node.name.text, "key") assert.eq(forInStat.variables[2].node.name.text, "value") - assert.eq(forInStat.inkeyword.text, "in") + assert.eq(forInStat.inKeyword.text, "in") assert.eq(#forInStat.values, 1) assert.eq(forInStat.values[1].node.tag, "global") - assert.eq(forInStat.dokeyword.text, "do") + assert.eq(forInStat.doKeyword.text, "do") checkIsBlock(forInStat.body, assert) assert.eq(#forInStat.body.statements, 1) - assert.eq(forInStat.endkeyword.text, "end") + assert.eq(forInStat.endKeyword.text, "end") end) suite:case("parseStatAssign", function(assert) @@ -838,7 +838,7 @@ test.suite("parse", function(suite) local localFuncStat = block.statements[1] :: syntax.AstStatLocalFunction assert.eq(localFuncStat.tag, "localfunction") assert.eq(#localFuncStat.func.attributes, 0) - assert.eq(localFuncStat.localkeyword.text, "local") + assert.eq(localFuncStat.localKeyword.text, "local") assert.eq(localFuncStat.func.functionkeyword.text, "function") assert.eq(localFuncStat.name.name.text, "multiply") end) @@ -851,11 +851,11 @@ test.suite("parse", function(suite) local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.tag, "typealias") assert.eq(typeAlias.export, nil) - assert.eq(typeAlias.typetoken.text, "type") + assert.eq(typeAlias.typeToken.text, "type") assert.eq(typeAlias.name.text, "Vector3") - assert.eq(typeAlias.opengenerics, nil) + assert.eq(typeAlias.openGenerics, nil) assert.eq(typeAlias.generics, nil) - assert.eq(typeAlias.closegenerics, nil) + assert.eq(typeAlias.closeGenerics, nil) assert.eq(typeAlias.equals.text, "=") assert.eq(typeAlias.type.kind, "type") assert.eq(typeAlias.type.tag, "table") @@ -867,14 +867,14 @@ test.suite("parse", function(suite) typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.neq(typeAlias.export, nil) assert.eq((typeAlias.export :: syntax.Token<"export">).text, "export") - assert.neq(typeAlias.opengenerics, nil) - assert.eq((typeAlias.opengenerics :: syntax.Token<"<">).text, "<") + assert.neq(typeAlias.openGenerics, nil) + assert.eq((typeAlias.openGenerics :: syntax.Token<"<">).text, "<") assert.neq(typeAlias.generics, nil) assert.eq(#typeAlias.generics :: syntax.Punctuated, 1) - assert.neq(typeAlias.genericpacks, nil) - assert.eq(#typeAlias.genericpacks :: syntax.Punctuated, 1) - assert.neq(typeAlias.closegenerics, nil) - assert.eq((typeAlias.closegenerics :: syntax.Token<">">).text, ">") + assert.neq(typeAlias.genericPacks, nil) + assert.eq(#typeAlias.genericPacks :: syntax.Punctuated, 1) + assert.neq(typeAlias.closeGenerics, nil) + assert.eq((typeAlias.closeGenerics :: syntax.Token<">">).text, ">") end) suite:case("parseStatTypeFunction", function(assert) @@ -1563,7 +1563,7 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(returnStat.kind, "stat") assert.eq(returnStat.tag, "return") assert.throwsWith(frozenTableMessage, function() - (returnStat :: any).returnkeyword = nil + (returnStat :: any).returnKeyword = nil end) assert.throwsWith(frozenTableMessage, function() table.insert((returnStat :: any).expressions, nil) @@ -1586,7 +1586,7 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(localStat.kind, "stat") assert.eq(localStat.tag, "local") assert.throwsWith(frozenTableMessage, function() - (localStat :: any).localkeyword = nil + (localStat :: any).localKeyword = nil end) assert.throwsWith(frozenTableMessage, function() table.insert((localStat :: any).variables, nil) @@ -1612,7 +1612,7 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(forInStat.kind, "stat") assert.eq(forInStat.tag, "forin") assert.throwsWith(frozenTableMessage, function() - (forInStat :: any).inkeyword = nil + (forInStat :: any).inKeyword = nil end) assert.throwsWith(frozenTableMessage, function() table.insert((forInStat :: any).variables, nil) @@ -1880,7 +1880,7 @@ test.suite("AST_nodes_frozen", function(suite) suite:case("astGenericTypePackFrozen", function(assert) local block = parser.parseblock("type Foo = (T, U...) -> ()") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias - local genericPack = (typeAlias.genericpacks :: any)[1].node + local genericPack = (typeAlias.genericPacks :: any)[1].node assert.eq(genericPack.tag, "generic") assert.eq(genericPack.kind, "typepack") assert.throwsWith(frozenTableMessage, function() @@ -1892,7 +1892,7 @@ test.suite("AST_nodes_frozen", function(suite) suite:case("tokenFrozen", function(assert) local block = parser.parseblock("local x = 1") local localStat = block.statements[1] :: syntax.AstStatLocal - local token = localStat.localkeyword + local token = localStat.localKeyword assert.eq(token.kind, "token") assert.throwsWith(frozenTableMessage, function() (token :: any).text = "modified" @@ -1916,7 +1916,7 @@ test.suite("AST_nodes_frozen", function(suite) local x = 1 ]=]) local localStat = block.statements[1] :: syntax.AstStatLocal - local trivias = localStat.localkeyword.leadingTrivia + local trivias = localStat.localKeyword.leadingTrivia for _, trivia in trivias do assert.throwsWith(frozenTableMessage, function() (trivia :: any).text = "modified" From 1e5ca65de24813f9d85206aaba85016e8a9acbf5 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 31 Mar 2026 11:19:14 -0700 Subject: [PATCH 433/642] Refactor AST expr and singleton string type types (#915) Part of a larger refactor of AST types so property names are camelCased --- definitions/luau.luau | 86 ++++++------ examples/lints/divide_by_zero.luau | 2 +- examples/query_transformer.luau | 8 +- .../lint/rules/constant_table_comparison.luau | 10 +- .../commands/lint/rules/divide_by_zero.luau | 8 +- lute/luau/src/luau.cpp | 96 +++++++------- lute/std/libs/syntax/printer.luau | 12 +- lute/std/libs/syntax/utils/init.luau | 2 +- lute/std/libs/syntax/visitor.luau | 98 +++++++------- tests/std/syntax/parser.test.luau | 122 +++++++++--------- 10 files changed, 222 insertions(+), 222 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 5220f44a7..272ba3d3f 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -65,9 +65,9 @@ export type AstExprGroup = { read location: Span, read kind: "expr", read tag: "group", - read openparens: Token<"(">, + read openParens: Token<"(">, read expression: AstExpr, - read closeparens: Token<")">, + read closeParens: Token<")">, } export type AstExprConstantNil = { @@ -97,8 +97,8 @@ export type AstExprConstantString = { read location: Span, read kind: "expr", read tag: "string", - read quotestyle: "single" | "double" | "block" | "interp", - read blockdepth: number, + read quoteStyle: "single" | "double" | "block" | "interp", + read blockDepth: number, read token: Token, } @@ -125,9 +125,9 @@ export type AstExprCall = { read kind: "expr", read tag: "call", read func: AstExpr, - read openparens: Token<"(">?, + read openParens: Token<"(">?, read arguments: Punctuated, - read closeparens: Token<")">?, + read closeParens: Token<")">?, read self: boolean, read argLocation: Span, } @@ -137,11 +137,11 @@ export type AstExprInstantiate = { read kind: "expr", read tag: "instantiate", read expr: AstExpr, - read leftarrow1: Token<"<">, - read leftarrow2: Token<"<">, - read typearguments: Punctuated, - read rightarrow1: Token<">">, - read rightarrow2: Token<">">, + read leftArrow1: Token<"<">, + read leftArrow2: Token<"<">, + read typeArguments: Punctuated, + read rightArrow1: Token<">">, + read rightArrow2: Token<">">, } export type AstExprIndexName = { @@ -151,7 +151,7 @@ export type AstExprIndexName = { read expression: AstExpr, read accessor: Token<"." | ":">, read index: Token, - read indexlocation: Span, + read indexLocation: Span, } export type AstExprIndexExpr = { @@ -159,9 +159,9 @@ export type AstExprIndexExpr = { read kind: "expr", read tag: "index", read expression: AstExpr, - read openbrackets: Token<"[">, + read openBrackets: Token<"[">, read index: AstExpr, - read closebrackets: Token<"]">, + read closeBrackets: Token<"]">, } export type AstExprFunction = { @@ -169,22 +169,22 @@ export type AstExprFunction = { read kind: "expr", read tag: "function", read attributes: { AstAttribute }, - read functionkeyword: Token<"function">, - read opengenerics: Token<"<">?, + read functionKeyword: Token<"function">, + read openGenerics: Token<"<">?, read generics: Punctuated?, - read genericpacks: Punctuated?, - read closegenerics: Token<">">?, - read openparens: Token<"(">, + read genericPacks: Punctuated?, + read closeGenerics: Token<">">?, + read openParens: Token<"(">, read self: AstLocal?, read parameters: Punctuated, read vararg: Token<"...">?, - read varargcolon: Token<":">?, - read varargannotation: AstTypePack?, - read closeparens: Token<")">, - read returnspecifier: Token<":">?, - read returnannotation: AstTypePack?, + read varargColon: Token<":">?, + read varargAnnotation: AstTypePack?, + read closeParens: Token<")">, + read returnSpecifier: Token<":">?, + read returnAnnotation: AstTypePack?, read body: AstStatBlock, - read endkeyword: Token<"end">, + read endKeyword: Token<"end">, } -- helper types for items contained in an AstExprTable, not actually AstExprs themselves @@ -193,7 +193,7 @@ export type AstTableExprListItem = { read kind: "list", read value: AstExpr, read separator: Token<"," | ";">?, - read istableitem: true, + read isTableItem: true, } export type AstTableExprRecordItem = { @@ -203,19 +203,19 @@ export type AstTableExprRecordItem = { read equals: Token<"=">, read value: AstExpr, read separator: Token<"," | ";">?, - read istableitem: true, + read isTableItem: true, } export type AstTableExprGeneralItem = { read location: Span, read kind: "general", - read indexeropen: Token<"[">, + read indexerOpen: Token<"[">, read key: AstExpr, - read indexerclose: Token<"]">, + read indexerClose: Token<"]">, read equals: Token<"=">, read value: AstExpr, read separator: Token<"," | ";">?, - read istableitem: true, + read isTableItem: true, } export type AstTableExprItem = AstTableExprListItem | AstTableExprRecordItem | AstTableExprGeneralItem @@ -224,9 +224,9 @@ export type AstExprTable = { read location: Span, read kind: "expr", read tag: "table", - read openbrace: Token<"{">, + read openBrace: Token<"{">, read entries: { AstTableExprItem }, - read closebrace: Token<"}">, + read closeBrace: Token<"}">, } export type AstExprUnary = { @@ -241,9 +241,9 @@ export type AstExprBinary = { read location: Span, read kind: "expr", read tag: "binary", - read lhsoperand: AstExpr, + read lhsOperand: AstExpr, read operator: Token, - read rhsoperand: AstExpr, + read rhsOperand: AstExpr, } export type AstExprInterpString = { @@ -265,23 +265,23 @@ export type AstExprTypeAssertion = { -- helper type for elseif clauses of an if-else expression, not actually an AstExpr itself export type AstElseIfExpr = { - read elseifkeyword: Token<"elseif">, + read elseIfKeyword: Token<"elseif">, read condition: AstExpr, - read thenkeyword: Token<"then">, - read thenexpr: AstExpr, + read thenKeyword: Token<"then">, + read thenExpr: AstExpr, } export type AstExprIfElse = { read location: Span, read kind: "expr", read tag: "conditional", - read ifkeyword: Token<"if">, + read ifKeyword: Token<"if">, read condition: AstExpr, - read thenkeyword: Token<"then">, - read thenexpr: AstExpr, + read thenKeyword: Token<"then">, + read thenExpr: AstExpr, read elseifs: { AstElseIfExpr }, - read elsekeyword: Token<"else">, - read elseexpr: AstExpr, + read elseKeyword: Token<"else">, + read elseExpr: AstExpr, } export type AstExpr = @@ -553,7 +553,7 @@ export type AstTypeSingletonString = { read location: Span, read kind: "type", read tag: "string", - read quotestyle: "single" | "double", + read quoteStyle: "single" | "double", read token: Token, } diff --git a/examples/lints/divide_by_zero.luau b/examples/lints/divide_by_zero.luau index f7f9ac061..3c6586701 100644 --- a/examples/lints/divide_by_zero.luau +++ b/examples/lints/divide_by_zero.luau @@ -16,7 +16,7 @@ local function lint(ast: syntax.AstStatBlock, sourcepath: path.Path?): { lintTyp return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" end) :filter(function(bin) - return bin.rhsoperand.kind == "expr" and bin.rhsoperand.tag == "number" and bin.rhsoperand.value == 0 + return bin.rhsOperand.kind == "expr" and bin.rhsOperand.tag == "number" and bin.rhsOperand.value == 0 end) :maptoarray( -- FIXME(Luau): We would need bidirectional inference of generics diff --git a/examples/query_transformer.luau b/examples/query_transformer.luau index 41f918e03..1db469571 100644 --- a/examples/query_transformer.luau +++ b/examples/query_transformer.luau @@ -7,12 +7,12 @@ local function transform(ctx) .findallfromroot(ctx.parseresult.root, syntax_utils.isExprBinary) :filter(function(bin) return bin.operator.text == "~=" - and bin.lhsoperand.tag == "local" - and bin.rhsoperand.tag == "local" - and bin.lhsoperand.token.text == bin.rhsoperand.token.text + and bin.lhsOperand.tag == "local" + and bin.rhsOperand.tag == "local" + and bin.lhsOperand.token.text == bin.rhsOperand.token.text end) :replace(function(bin) - return `math.isnan({(bin.lhsoperand :: syntax.AstExprLocal).token.text})` + return `math.isnan({(bin.lhsOperand :: syntax.AstExprLocal).token.text})` end) end diff --git a/lute/cli/commands/lint/rules/constant_table_comparison.luau b/lute/cli/commands/lint/rules/constant_table_comparison.luau index a57dfca38..9b0482c4d 100644 --- a/lute/cli/commands/lint/rules/constant_table_comparison.luau +++ b/lute/cli/commands/lint/rules/constant_table_comparison.luau @@ -40,7 +40,7 @@ local function lint( return bin.operator.text == "==" or bin.operator.text == "~=" end) :filter(function(bin) - return isTableLiteral(bin.rhsoperand) or isTableLiteral(bin.lhsoperand) + return isTableLiteral(bin.rhsOperand) or isTableLiteral(bin.lhsOperand) end) :maptoarray( function( @@ -48,12 +48,12 @@ local function lint( ): lintTypes.LintViolation -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation local suggestedFix: string? = nil - if isEmptyTableLiteral(bin.lhsoperand) then + if isEmptyTableLiteral(bin.lhsOperand) then suggestedFix = - `next({stringext.trim(syntaxPrinter.printnode(bin.rhsoperand))}) {bin.operator.text} nil` - elseif isEmptyTableLiteral(bin.rhsoperand) then + `next({stringext.trim(syntaxPrinter.printnode(bin.rhsOperand))}) {bin.operator.text} nil` + elseif isEmptyTableLiteral(bin.rhsOperand) then suggestedFix = - `next({stringext.trim(syntaxPrinter.printnode(bin.lhsoperand))}) {bin.operator.text} nil` + `next({stringext.trim(syntaxPrinter.printnode(bin.lhsOperand))}) {bin.operator.text} nil` end return { diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau index 52a2d2aa7..1fb08fdc5 100644 --- a/lute/cli/commands/lint/rules/divide_by_zero.luau +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -23,7 +23,7 @@ local function lint( return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" end) :filter(function(bin) - return isZeroLiteral(bin.rhsoperand) + return isZeroLiteral(bin.rhsOperand) end) :maptoarray( function( @@ -31,10 +31,10 @@ local function lint( ): lintTypes.LintViolation? -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation local suggestedfix = nil if n.operator.text == "/" or n.operator.text == "//" then - if isZeroLiteral(n.lhsoperand) then + if isZeroLiteral(n.lhsOperand) then suggestedfix = table.freeze({ fix = "math.nan" }) - elseif n.lhsoperand.tag == "unary" and n.lhsoperand.operator.text == "-" then - if isZeroLiteral(n.lhsoperand.operand) then + elseif n.lhsOperand.tag == "unary" and n.lhsOperand.operator.text == "-" then + if isZeroLiteral(n.lhsOperand.operand) then suggestedfix = table.freeze({ fix = "math.nan" }) else suggestedfix = table.freeze({ fix = "-math.huge" }) diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index d61d0eac5..3d090fabe 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -512,7 +512,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "kind"); lua_pushboolean(L, 1); - lua_setfield(L, -2, "istableitem"); + lua_setfield(L, -2, "isTableItem"); withLocation(Luau::Location{item.value->location.begin, item.value->location.end}); @@ -526,7 +526,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "kind"); lua_pushboolean(L, 1); - lua_setfield(L, -2, "istableitem"); + lua_setfield(L, -2, "isTableItem"); withLocation(Luau::Location{item.key->location.begin, item.value->location.end}); @@ -548,20 +548,20 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "kind"); lua_pushboolean(L, 1); - lua_setfield(L, -2, "istableitem"); + lua_setfield(L, -2, "isTableItem"); LUTE_ASSERT(cstNode->indexerOpenPosition); withLocation(Luau::Location{*cstNode->indexerOpenPosition, item.value->location.end}); serializeToken(*cstNode->indexerOpenPosition, "["); - lua_setfield(L, -2, "indexeropen"); + lua_setfield(L, -2, "indexerOpen"); visit(item.key); lua_setfield(L, -2, "key"); LUTE_ASSERT(cstNode->indexerClosePosition); serializeToken(*cstNode->indexerClosePosition, "]"); - lua_setfield(L, -2, "indexerclose"); + lua_setfield(L, -2, "indexerClose"); LUTE_ASSERT(cstNode->equalsPosition); serializeToken(*cstNode->equalsPosition, "="); @@ -829,13 +829,13 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "group", "expr"); serializeToken(node->location.begin, "("); - lua_setfield(L, -2, "openparens"); + lua_setfield(L, -2, "openParens"); node->expr->visit(this); lua_setfield(L, -2, "expression"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, ")"); - lua_setfield(L, -2, "closeparens"); + lua_setfield(L, -2, "closeParens"); } void serialize(Luau::AstExprConstantNil* node) @@ -902,10 +902,10 @@ struct AstSerialize : public Luau::AstVisitor lua_pushstring(L, "interp"); break; } - lua_setfield(L, -2, "quotestyle"); + lua_setfield(L, -2, "quoteStyle"); lua_pushnumber(L, cstNode->blockDepth); - lua_setfield(L, -2, "blockdepth"); + lua_setfield(L, -2, "blockDepth"); serializeToken(node->location.begin, cstNode->sourceString.data); lua_setfield(L, -2, "token"); @@ -971,7 +971,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(*cstNode->openParens, "("); else lua_pushnil(L); - lua_setfield(L, -2, "openparens"); + lua_setfield(L, -2, "openParens"); serializePunctuated(node->args, cstNode->commaPositions, ","); lua_setfield(L, -2, "arguments"); @@ -986,7 +986,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(*cstNode->closeParens, ")"); else lua_pushnil(L); - lua_setfield(L, -2, "closeparens"); + lua_setfield(L, -2, "closeParens"); } void serialize(Luau::AstExprInstantiate* node) @@ -1002,19 +1002,19 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "expr"); serializeToken(cstNode.leftArrow1Position, "<"); - lua_setfield(L, -2, "leftarrow1"); + lua_setfield(L, -2, "leftArrow1"); serializeToken(cstNode.leftArrow2Position, "<"); - lua_setfield(L, -2, "leftarrow2"); + lua_setfield(L, -2, "leftArrow2"); serializePunctuated(node->typeArguments, cstNode.commaPositions, ","); - lua_setfield(L, -2, "typearguments"); + lua_setfield(L, -2, "typeArguments"); serializeToken(cstNode.rightArrow1Position, ">"); - lua_setfield(L, -2, "rightarrow1"); + lua_setfield(L, -2, "rightArrow1"); serializeToken(cstNode.rightArrow2Position, ">"); - lua_setfield(L, -2, "rightarrow2"); + lua_setfield(L, -2, "rightArrow2"); } void serialize(Luau::AstExprIndexName* node) @@ -1033,7 +1033,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(node->indexLocation.begin, node->index.value); lua_setfield(L, -2, "index"); serialize(node->indexLocation); - lua_setfield(L, -2, "indexlocation"); + lua_setfield(L, -2, "indexLocation"); } void serialize(Luau::AstExprIndexExpr* node) @@ -1049,13 +1049,13 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "expression"); serializeToken(cstNode->openBracketPosition, "["); - lua_setfield(L, -2, "openbrackets"); + lua_setfield(L, -2, "openBrackets"); node->index->visit(this); lua_setfield(L, -2, "index"); serializeToken(cstNode->closeBracketPosition, "]"); - lua_setfield(L, -2, "closebrackets"); + lua_setfield(L, -2, "closeBrackets"); } enum FunctionSerializationState @@ -1081,19 +1081,19 @@ struct AstSerialize : public Luau::AstVisitor // Attributes and function keyword are on the stack, so we just need to set them in the function expr table // This is only called from serialize AstStatFunction or AstStatLocalFunction, so we assume the stack has the following structure: // -3: attributes - // -2: functionkeyword + // -2: functionKeyword // -1: function expr table (created above) lua_insert(L, -3); - lua_setfield(L, -3, "functionkeyword"); + lua_setfield(L, -3, "functionKeyword"); lua_setfield(L, -2, "attributes"); break; case FunctionKeywordSerialized: // Function keyword is on the stack, so we just need to set it in the function expr table // This is only called from serialize AstStatTypeFunction, so we assume the stack has the following structure: - // -2: functionkeyword + // -2: functionKeyword // -1: function expr table (created above) lua_insert(L, -2); - lua_setfield(L, -2, "functionkeyword"); + lua_setfield(L, -2, "functionKeyword"); lua_newtable(L); lua_setfield(L, -2, "attributes"); @@ -1103,24 +1103,24 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "attributes"); serializeToken(cstNode->functionKeywordPosition, "function"); - lua_setfield(L, -2, "functionkeyword"); + lua_setfield(L, -2, "functionKeyword"); break; } if (node->generics.size > 0 || node->genericPacks.size > 0) { serializeToken(cstNode->openGenericsPosition, "<"); - lua_setfield(L, -2, "opengenerics"); + lua_setfield(L, -2, "openGenerics"); auto commas = cstNode->genericsCommaPositions; serializePunctuated(node->generics, commas, ","); lua_setfield(L, -2, "generics"); serializePunctuated(node->genericPacks, splitArray(commas, node->generics.size), ","); - lua_setfield(L, -2, "genericpacks"); + lua_setfield(L, -2, "genericPacks"); serializeToken(cstNode->closeGenericsPosition, ">"); - lua_setfield(L, -2, "closegenerics"); + lua_setfield(L, -2, "closeGenerics"); } if (node->self) @@ -1132,7 +1132,7 @@ struct AstSerialize : public Luau::AstVisitor if (node->argLocation) { serializeToken(node->argLocation->begin, "("); - lua_setfield(L, -2, "openparens"); + lua_setfield(L, -2, "openParens"); } serializePunctuated(node->args, cstNode->argsCommaPositions, ",", cstNode->argsAnnotationColonPositions); @@ -1148,7 +1148,7 @@ struct AstSerialize : public Luau::AstVisitor serializeToken(cstNode->varargAnnotationColonPosition, ":"); else lua_pushnil(L); - lua_setfield(L, -2, "varargcolon"); + lua_setfield(L, -2, "varargColon"); if (node->varargAnnotation) { @@ -1159,31 +1159,31 @@ struct AstSerialize : public Luau::AstVisitor } else lua_pushnil(L); - lua_setfield(L, -2, "varargannotation"); + lua_setfield(L, -2, "varargAnnotation"); if (node->argLocation) { serializeToken(Luau::Position{node->argLocation->end.line, node->argLocation->end.column - 1}, ")"); - lua_setfield(L, -2, "closeparens"); + lua_setfield(L, -2, "closeParens"); } if (node->returnAnnotation) serializeToken(cstNode->returnSpecifierPosition, ":"); else lua_pushnil(L); - lua_setfield(L, -2, "returnspecifier"); + lua_setfield(L, -2, "returnSpecifier"); if (node->returnAnnotation) node->returnAnnotation->visit(this); else lua_pushnil(L); - lua_setfield(L, -2, "returnannotation"); + lua_setfield(L, -2, "returnAnnotation"); node->body->visit(this); lua_setfield(L, -2, "body"); serializeToken(node->body->location.end, "end"); - lua_setfield(L, -2, "endkeyword"); + lua_setfield(L, -2, "endKeyword"); } void serialize(Luau::AstExprTable* node) @@ -1196,7 +1196,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "table", "expr"); serializeToken(node->location.begin, "{"); - lua_setfield(L, -2, "openbrace"); + lua_setfield(L, -2, "openBrace"); lua_createtable(L, node->items.size, 0); for (size_t i = 0; i < node->items.size; i++) @@ -1207,7 +1207,7 @@ struct AstSerialize : public Luau::AstVisitor lua_setfield(L, -2, "entries"); serializeToken(Luau::Position{node->location.end.line, node->location.end.column - 1}, "}"); - lua_setfield(L, -2, "closebrace"); + lua_setfield(L, -2, "closeBrace"); } void serialize(Luau::AstExprUnary* node) @@ -1233,14 +1233,14 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "binary", "expr"); node->left->visit(this); - lua_setfield(L, -2, "lhsoperand"); + lua_setfield(L, -2, "lhsOperand"); const auto cstNode = lookupCstNode(node); serializeToken(cstNode->opPosition, Luau::toString(node->op).data()); lua_setfield(L, -2, "operator"); node->right->visit(this); - lua_setfield(L, -2, "rhsoperand"); + lua_setfield(L, -2, "rhsOperand"); } void serialize(Luau::AstExprTypeAssertion* node) @@ -1271,7 +1271,7 @@ struct AstSerialize : public Luau::AstVisitor serializeNodePreamble(node, "conditional", "expr"); serializeToken(node->location.begin, "if"); - lua_setfield(L, -2, "ifkeyword"); + lua_setfield(L, -2, "ifKeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1279,13 +1279,13 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasThen) { serializeToken(cstNode->thenPosition, "then"); - lua_setfield(L, -2, "thenkeyword"); + lua_setfield(L, -2, "thenKeyword"); node->trueExpr->visit(this); } else lua_pushnil(L); - lua_setfield(L, -2, "thenexpr"); + lua_setfield(L, -2, "thenExpr"); lua_createtable(L, 0, preambleSize + 4); int i = 0; @@ -1297,7 +1297,7 @@ struct AstSerialize : public Luau::AstVisitor cstNode = lookupCstNode(node); serializeToken(node->location.begin, "elseif"); - lua_setfield(L, -2, "elseifkeyword"); + lua_setfield(L, -2, "elseIfKeyword"); node->condition->visit(this); lua_setfield(L, -2, "condition"); @@ -1305,12 +1305,12 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasThen) { serializeToken(cstNode->thenPosition, "then"); - lua_setfield(L, -2, "thenkeyword"); + lua_setfield(L, -2, "thenKeyword"); node->trueExpr->visit(this); } else lua_pushnil(L); - lua_setfield(L, -2, "thenexpr"); + lua_setfield(L, -2, "thenExpr"); lua_rawseti(L, -2, i + 1); i++; @@ -1320,12 +1320,12 @@ struct AstSerialize : public Luau::AstVisitor if (node->hasElse) { serializeToken(cstNode->elsePosition, "else"); - lua_setfield(L, -2, "elsekeyword"); + lua_setfield(L, -2, "elseKeyword"); node->falseExpr->visit(this); } else lua_pushnil(L); - lua_setfield(L, -2, "elseexpr"); + lua_setfield(L, -2, "elseExpr"); } void serialize(Luau::AstExprInterpString* node) @@ -2079,7 +2079,7 @@ struct AstSerialize : public Luau::AstVisitor default: LUTE_ASSERT(false); } - lua_setfield(L, -2, "quotestyle"); + lua_setfield(L, -2, "quoteStyle"); serializeToken(item.stringPosition, item.stringInfo->sourceString.data); lua_setfield(L, -2, "token"); @@ -2344,7 +2344,7 @@ struct AstSerialize : public Luau::AstVisitor default: LUTE_ASSERT(false); } - lua_setfield(L, -2, "quotestyle"); + lua_setfield(L, -2, "quoteStyle"); serializeToken(node->location.begin, cstNode->sourceString.data); lua_setfield(L, -2, "token"); diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index dc0eb3aeb..9815b8e87 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -56,17 +56,17 @@ local function printString(self: PrintVisitor, expr: types.AstExprConstantString self:printTriviaList(expr.token.leadingTrivia) - if expr.quotestyle == "single" then + if expr.quoteStyle == "single" then self:write(`'{expr.token.text}'`) - elseif expr.quotestyle == "double" then + elseif expr.quoteStyle == "double" then self:write(`"{expr.token.text}"`) - elseif expr.quotestyle == "block" then - local equals = string.rep("=", expr.blockdepth) + elseif expr.quoteStyle == "block" then + local equals = string.rep("=", expr.blockDepth) self:write(`[{equals}[{expr.token.text}]{equals}]`) - elseif expr.quotestyle == "interp" then + elseif expr.quoteStyle == "interp" then self:write("`" .. expr.token.text .. "`") else - exhaustiveMatch(expr.quotestyle) + exhaustiveMatch(expr.quoteStyle) end self:printTriviaList(expr.token.trailingTrivia) diff --git a/lute/std/libs/syntax/utils/init.luau b/lute/std/libs/syntax/utils/init.luau index 3f4809751..50269775e 100644 --- a/lute/std/libs/syntax/utils/init.luau +++ b/lute/std/libs/syntax/utils/init.luau @@ -65,7 +65,7 @@ function utils.isExprTable(n: types.AstNode): types.AstExprTable? end function utils.isTableExprItem(n: types.AstNode | types.AstTableExprItem): types.AstTableExprItem? - return if (n :: types.AstTableExprItem).istableitem then n :: types.AstTableExprItem else nil + return if (n :: types.AstTableExprItem).isTableItem then n :: types.AstTableExprItem else nil end function utils.isExprUnary(n: types.AstNode): types.AstExprUnary? diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index b392e45ed..2dab1f217 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -426,12 +426,12 @@ end local function visitExprCall(node: types.AstExprCall, visitor: Visitor) if visitor.visitExprCall(node) then visitExpr(node.func, visitor) - if node.openparens then - visitToken(node.openparens, visitor) + if node.openParens then + visitToken(node.openParens, visitor) end visitPunctuated(node.arguments, visitor, visitExpr) - if node.closeparens then - visitToken(node.closeparens, visitor) + if node.closeParens then + visitToken(node.closeParens, visitor) end end end @@ -445,9 +445,9 @@ end local function visitExprBinary(node: types.AstExprBinary, visitor: Visitor) if visitor.visitExprBinary(node) then - visitExpr(node.lhsoperand, visitor) + visitExpr(node.lhsOperand, visitor) visitToken(node.operator, visitor) - visitExpr(node.rhsoperand, visitor) + visitExpr(node.rhsOperand, visitor) end end @@ -470,40 +470,40 @@ local function visitExprFunction( end end if shouldVisitFunctionKeyword ~= false then - visitToken(node.functionkeyword, visitor) + visitToken(node.functionKeyword, visitor) end - if node.opengenerics then - visitToken(node.opengenerics, visitor) + if node.openGenerics then + visitToken(node.openGenerics, visitor) end if node.generics then visitPunctuated(node.generics, visitor, visitGeneric) end - if node.genericpacks then - visitPunctuated(node.genericpacks, visitor, visitGenericPack) + if node.genericPacks then + visitPunctuated(node.genericPacks, visitor, visitGenericPack) end - if node.closegenerics then - visitToken(node.closegenerics, visitor) + if node.closeGenerics then + visitToken(node.closeGenerics, visitor) end - visitToken(node.openparens, visitor) + visitToken(node.openParens, visitor) visitPunctuated(node.parameters, visitor, visitLocal) if node.vararg then visitToken(node.vararg, visitor) end - if node.varargcolon then - visitToken(node.varargcolon, visitor) + if node.varargColon then + visitToken(node.varargColon, visitor) end - if node.varargannotation then - visitTypePack(node.varargannotation, visitor) + if node.varargAnnotation then + visitTypePack(node.varargAnnotation, visitor) end - visitToken(node.closeparens, visitor) - if node.returnspecifier then - visitToken(node.returnspecifier, visitor) + visitToken(node.closeParens, visitor) + if node.returnSpecifier then + visitToken(node.returnSpecifier, visitor) end - if node.returnannotation then - visitTypePack(node.returnannotation, visitor) + if node.returnAnnotation then + visitTypePack(node.returnAnnotation, visitor) end visitStatBlock(node.body, visitor) - visitToken(node.endkeyword, visitor) + visitToken(node.endKeyword, visitor) visitor.visitExprFunctionEnd(node) end end @@ -511,11 +511,11 @@ end local function visitExprInstantiate(node: types.AstExprInstantiate, visitor: Visitor) if visitor.visitExprInstantiate(node) then visitExpr(node.expr, visitor) - visitToken(node.leftarrow1, visitor) - visitToken(node.leftarrow2, visitor) - visitPunctuated(node.typearguments, visitor, visitTypeOrTypePack) - visitToken(node.rightarrow1, visitor) - visitToken(node.rightarrow2, visitor) + visitToken(node.leftArrow1, visitor) + visitToken(node.leftArrow2, visitor) + visitPunctuated(node.typeArguments, visitor, visitTypeOrTypePack) + visitToken(node.rightArrow1, visitor) + visitToken(node.rightArrow2, visitor) end end @@ -525,7 +525,7 @@ local function visitStatFunction(node: types.AstStatFunction, visitor: Visitor) for _, attribute in node.func.attributes do visitAttribute(attribute, visitor) end - visitToken(node.func.functionkeyword, visitor) + visitToken(node.func.functionKeyword, visitor) visitExpr(node.name, visitor) visitExprFunction(node.func, visitor, false, false) end @@ -537,7 +537,7 @@ local function visitStatLocalFunction(node: types.AstStatLocalFunction, visitor: visitAttribute(attribute, visitor) end visitToken(node.localKeyword, visitor) - visitToken(node.func.functionkeyword, visitor) + visitToken(node.func.functionKeyword, visitor) visitLocal(node.name, visitor) visitExprFunction(node.func, visitor, false, false) end @@ -549,7 +549,7 @@ local function visitStatTypeFunction(node: types.AstStatTypeFunction, visitor: V visitToken(node.export, visitor) end visitToken(node.type, visitor) - visitToken(node.body.functionkeyword, visitor) + visitToken(node.body.functionKeyword, visitor) visitToken(node.name, visitor) visitExprFunction(node.body, visitor, true, false) end @@ -564,9 +564,9 @@ local function visitTableExprItem(node: types.AstTableExprItem, visitor: Visitor visitToken(node.equals, visitor) visitExpr(node.value, visitor) elseif node.kind == "general" then - visitToken(node.indexeropen, visitor) + visitToken(node.indexerOpen, visitor) visitExpr(node.key, visitor) - visitToken(node.indexerclose, visitor) + visitToken(node.indexerClose, visitor) visitToken(node.equals, visitor) visitExpr(node.value, visitor) else @@ -581,11 +581,11 @@ end local function visitExprTable(node: types.AstExprTable, visitor: Visitor) if visitor.visitExprTable(node) then - visitToken(node.openbrace, visitor) + visitToken(node.openBrace, visitor) for _, item in node.entries do visitTableExprItem(item, visitor) end - visitToken(node.closebrace, visitor) + visitToken(node.closeBrace, visitor) end end @@ -600,17 +600,17 @@ end local function visitExprIndexExpr(node: types.AstExprIndexExpr, visitor: Visitor) if visitor.visitExprIndexExpr(node) then visitExpr(node.expression, visitor) - visitToken(node.openbrackets, visitor) + visitToken(node.openBrackets, visitor) visitExpr(node.index, visitor) - visitToken(node.closebrackets, visitor) + visitToken(node.closeBrackets, visitor) end end local function visitExprGroup(node: types.AstExprGroup, visitor: Visitor) if visitor.visitExprGroup(node) then - visitToken(node.openparens, visitor) + visitToken(node.openParens, visitor) visitExpr(node.expression, visitor) - visitToken(node.closeparens, visitor) + visitToken(node.closeParens, visitor) end end @@ -635,18 +635,18 @@ end local function visitExprIfElse(node: types.AstExprIfElse, visitor: Visitor) if visitor.visitExprIfElse(node) then - visitToken(node.ifkeyword, visitor) + visitToken(node.ifKeyword, visitor) visitExpr(node.condition, visitor) - visitToken(node.thenkeyword, visitor) - visitExpr(node.thenexpr, visitor) + visitToken(node.thenKeyword, visitor) + visitExpr(node.thenExpr, visitor) for _, elseifs in node.elseifs do - visitToken(elseifs.elseifkeyword, visitor) + visitToken(elseifs.elseIfKeyword, visitor) visitExpr(elseifs.condition, visitor) - visitToken(elseifs.thenkeyword, visitor) - visitExpr(elseifs.thenexpr, visitor) + visitToken(elseifs.thenKeyword, visitor) + visitExpr(elseifs.thenExpr, visitor) end - visitToken(node.elsekeyword, visitor) - visitExpr(node.elseexpr, visitor) + visitToken(node.elseKeyword, visitor) + visitExpr(node.elseExpr, visitor) end end @@ -1070,7 +1070,7 @@ local function visit(node: types.AstNode, visitor: Visitor) visitAttribute(node, visitor) elseif node.kind == "token" then visitToken(node, visitor) - elseif (node :: types.AstTableExprItem).istableitem then + elseif (node :: types.AstTableExprItem).isTableItem then visitTableExprItem(node :: types.AstTableExprItem, visitor) else exhaustiveMatch(node.kind) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 91e1ae293..1c32b2763 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -202,8 +202,8 @@ test.suite("parseExpr", function(suite) assert.eq(groupExpr.tag, "group") assert.eq(groupExpr.kind, "expr") local expr = groupExpr :: syntax.AstExprGroup - assert.eq(expr.openparens.text, "(") - assert.eq(expr.closeparens.text, ")") + assert.eq(expr.openParens.text, "(") + assert.eq(expr.closeParens.text, ")") assert.eq(expr.expression.tag, "global") end) @@ -247,25 +247,25 @@ test.suite("parseExpr", function(suite) assert.eq(singleQuoteExpr.tag, "string") assert.eq(singleQuoteExpr.kind, "expr") assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).token.text, "hello") - assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).quotestyle, "single") + assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).quoteStyle, "single") local doubleQuoteExpr = parser.parseexpr('"world"') assert.eq(doubleQuoteExpr.tag, "string") assert.eq(doubleQuoteExpr.kind, "expr") assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).token.text, "world") - assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).quotestyle, "double") + assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).quoteStyle, "double") local blockQuoteExpr = parser.parseexpr("[[world]]") assert.eq(blockQuoteExpr.tag, "string") assert.eq(blockQuoteExpr.kind, "expr") assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).token.text, "world") - assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).quotestyle, "block") + assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).quoteStyle, "block") local interpExpr = parser.parseexpr("`foobar`") assert.eq(interpExpr.tag, "string") assert.eq(interpExpr.kind, "expr") assert.eq((interpExpr :: syntax.AstExprConstantString).token.text, "foobar") - assert.eq((interpExpr :: syntax.AstExprConstantString).quotestyle, "interp") + assert.eq((interpExpr :: syntax.AstExprConstantString).quoteStyle, "interp") end) suite:case("parseExprLocal", function(assert) @@ -315,15 +315,15 @@ test.suite("parseExpr", function(suite) assert.eq(instExpr.tag, "instantiate") assert.eq(instExpr.kind, "expr") assert.eq((instExpr :: syntax.AstExprInstantiate).expr.tag, "global") - assert.eq((instExpr :: syntax.AstExprInstantiate).leftarrow1.text, "<") - assert.eq((instExpr :: syntax.AstExprInstantiate).leftarrow2.text, "<") - assert.eq(#(instExpr :: syntax.AstExprInstantiate).typearguments, 2) - assert.eq(((instExpr :: syntax.AstExprInstantiate).typearguments[1].node).kind, "type") - assert.eq(((instExpr :: syntax.AstExprInstantiate).typearguments[1].node).tag, "reference") - assert.eq(((instExpr :: syntax.AstExprInstantiate).typearguments[2].node).kind, "typepack") - assert.eq(((instExpr :: syntax.AstExprInstantiate).typearguments[2].node).tag, "variadic") - assert.eq((instExpr :: syntax.AstExprInstantiate).rightarrow1.text, ">") - assert.eq((instExpr :: syntax.AstExprInstantiate).rightarrow2.text, ">") + assert.eq((instExpr :: syntax.AstExprInstantiate).leftArrow1.text, "<") + assert.eq((instExpr :: syntax.AstExprInstantiate).leftArrow2.text, "<") + assert.eq(#(instExpr :: syntax.AstExprInstantiate).typeArguments, 2) + assert.eq(((instExpr :: syntax.AstExprInstantiate).typeArguments[1].node).kind, "type") + assert.eq(((instExpr :: syntax.AstExprInstantiate).typeArguments[1].node).tag, "reference") + assert.eq(((instExpr :: syntax.AstExprInstantiate).typeArguments[2].node).kind, "typepack") + assert.eq(((instExpr :: syntax.AstExprInstantiate).typeArguments[2].node).tag, "variadic") + assert.eq((instExpr :: syntax.AstExprInstantiate).rightArrow1.text, ">") + assert.eq((instExpr :: syntax.AstExprInstantiate).rightArrow2.text, ">") end) suite:case("parseExprIndexName", function(assert) @@ -341,8 +341,8 @@ test.suite("parseExpr", function(suite) assert.eq(indexExpr.kind, "expr") assert.eq((indexExpr :: syntax.AstExprIndexExpr).expression.tag, "global") assert.eq((indexExpr :: syntax.AstExprIndexExpr).index.tag, "number") - assert.eq((indexExpr :: syntax.AstExprIndexExpr).openbrackets.text, "[") - assert.eq((indexExpr :: syntax.AstExprIndexExpr).closebrackets.text, "]") + assert.eq((indexExpr :: syntax.AstExprIndexExpr).openBrackets.text, "[") + assert.eq((indexExpr :: syntax.AstExprIndexExpr).closeBrackets.text, "]") end) suite:case("parseExprFunction", function(assert) @@ -352,23 +352,23 @@ test.suite("parseExpr", function(suite) local funcExpr = anonFuncExpr :: syntax.AstExprFunction assert.eq(#funcExpr.attributes, 0) - assert.eq(funcExpr.functionkeyword.text, "function") - assert.eq(funcExpr.opengenerics, nil) + assert.eq(funcExpr.functionKeyword.text, "function") + assert.eq(funcExpr.openGenerics, nil) assert.eq(funcExpr.generics, nil) - assert.eq(funcExpr.genericpacks, nil) - assert.eq(funcExpr.closegenerics, nil) + assert.eq(funcExpr.genericPacks, nil) + assert.eq(funcExpr.closeGenerics, nil) assert.eq(funcExpr.self, nil) - assert.eq(funcExpr.openparens.text, "(") + assert.eq(funcExpr.openParens.text, "(") assert.eq(#funcExpr.parameters, 2) assert.eq(funcExpr.parameters[1].node.name.text, "a") assert.eq(funcExpr.parameters[2].node.name.text, "b") assert.eq(funcExpr.vararg, nil) - assert.eq(funcExpr.varargcolon, nil) - assert.eq(funcExpr.varargannotation, nil) - assert.eq(funcExpr.closeparens.text, ")") - assert.eq(funcExpr.returnspecifier, nil) - assert.eq(funcExpr.returnannotation, nil) - assert.eq(funcExpr.endkeyword.text, "end") + assert.eq(funcExpr.varargColon, nil) + assert.eq(funcExpr.varargAnnotation, nil) + assert.eq(funcExpr.closeParens.text, ")") + assert.eq(funcExpr.returnSpecifier, nil) + assert.eq(funcExpr.returnAnnotation, nil) + assert.eq(funcExpr.endKeyword.text, "end") anonFuncExpr = parser.parseexpr("function(a: A, ...: B...): A return a end") assert.eq(anonFuncExpr.tag, "function") @@ -376,9 +376,9 @@ test.suite("parseExpr", function(suite) funcExpr = anonFuncExpr :: syntax.AstExprFunction assert.eq(#funcExpr.attributes, 0) - assert.eq(funcExpr.functionkeyword.text, "function") + assert.eq(funcExpr.functionKeyword.text, "function") - assert.neq(funcExpr.opengenerics, nil) + assert.neq(funcExpr.openGenerics, nil) assert.neq(funcExpr.generics, nil) assert.eq(#funcExpr.generics :: syntax.Punctuated, 1) @@ -387,19 +387,19 @@ test.suite("parseExpr", function(suite) assert.eq(gen.equals, nil) assert.eq(gen.default, nil) - assert.neq(funcExpr.genericpacks, nil) - assert.eq(#funcExpr.genericpacks :: syntax.Punctuated, 1) + assert.neq(funcExpr.genericPacks, nil) + assert.eq(#funcExpr.genericPacks :: syntax.Punctuated, 1) - local genPack = (funcExpr.genericpacks :: syntax.Punctuated)[1].node + local genPack = (funcExpr.genericPacks :: syntax.Punctuated)[1].node assert.eq(genPack.name.text, "B") assert.eq(genPack.ellipsis.text, "...") assert.eq(genPack.equals, nil) assert.eq(genPack.default, nil) - assert.neq(funcExpr.closegenerics, nil) - assert.eq((funcExpr.closegenerics :: syntax.Token<">">).text, ">") + assert.neq(funcExpr.closeGenerics, nil) + assert.eq((funcExpr.closeGenerics :: syntax.Token<">">).text, ">") - assert.eq(funcExpr.openparens.text, "(") + assert.eq(funcExpr.openParens.text, "(") assert.eq(#funcExpr.parameters, 1) local param = funcExpr.parameters[1].node assert.eq(param.name.text, "a") @@ -410,16 +410,16 @@ test.suite("parseExpr", function(suite) assert.neq(funcExpr.vararg, nil) assert.eq((funcExpr.vararg :: syntax.Token<"...">).text, "...") - assert.neq(funcExpr.varargcolon, nil) - assert.eq((funcExpr.varargcolon :: syntax.Token<":">).text, ":") - assert.neq(funcExpr.varargannotation, nil) - assert.eq((funcExpr.varargannotation :: syntax.AstTypePack).tag, "generic") + assert.neq(funcExpr.varargColon, nil) + assert.eq((funcExpr.varargColon :: syntax.Token<":">).text, ":") + assert.neq(funcExpr.varargAnnotation, nil) + assert.eq((funcExpr.varargAnnotation :: syntax.AstTypePack).tag, "generic") - assert.eq(funcExpr.closeparens.text, ")") - assert.neq(funcExpr.returnspecifier, nil) - assert.eq((funcExpr.returnspecifier :: syntax.Token<":">).text, ":") - assert.neq(funcExpr.returnannotation, nil) - assert.eq((funcExpr.returnannotation :: syntax.AstTypePack).tag, "explicit") + assert.eq(funcExpr.closeParens.text, ")") + assert.neq(funcExpr.returnSpecifier, nil) + assert.eq((funcExpr.returnSpecifier :: syntax.Token<":">).text, ":") + assert.neq(funcExpr.returnAnnotation, nil) + assert.eq((funcExpr.returnAnnotation :: syntax.AstTypePack).tag, "explicit") end) suite:case("parseExprTable", function(assert) @@ -427,8 +427,8 @@ test.suite("parseExpr", function(suite) assert.eq(emptyTableExpr.tag, "table") assert.eq(emptyTableExpr.kind, "expr") local tableExpr = emptyTableExpr :: syntax.AstExprTable - assert.eq(tableExpr.openbrace.text, "{") - assert.eq(tableExpr.closebrace.text, "}") + assert.eq(tableExpr.openBrace.text, "{") + assert.eq(tableExpr.closeBrace.text, "}") assert.eq(#tableExpr.entries, 0) local mixedTable = parser.parseexpr("{1, foo = bar; [2] = baz}") @@ -453,9 +453,9 @@ test.suite("parseExpr", function(suite) local entry3: any = mixedTableExpr.entries[3] assert.eq(entry3.kind, "general") - assert.eq(entry3.indexeropen.text, "[") + assert.eq(entry3.indexerOpen.text, "[") assert.eq(entry3.key.tag, "number") - assert.eq(entry3.indexerclose.text, "]") + assert.eq(entry3.indexerClose.text, "]") assert.eq(entry3.equals.text, "=") assert.eq(entry3.value.tag, "global") assert.eq(entry3.separator, nil) @@ -489,9 +489,9 @@ test.suite("parseExpr", function(suite) assert.eq(addExpr.tag, "binary") assert.eq(addExpr.kind, "expr") local expr = addExpr :: syntax.AstExprBinary - assert.eq(expr.lhsoperand.tag, "number") + assert.eq(expr.lhsOperand.tag, "number") assert.eq(expr.operator.text, "+") - assert.eq(expr.rhsoperand.tag, "number") + assert.eq(expr.rhsOperand.tag, "number") end) suite:case("parseExprInterpString", function(assert) @@ -824,7 +824,7 @@ test.suite("parse", function(suite) assert.eq((funcStat.func.attributes[1] :: syntax.AstAttribute).token.text, "@checked") assert.eq((funcStat.func.attributes[2] :: syntax.AstAttribute).token.text, "@native") assert.eq((funcStat.func.attributes[3] :: syntax.AstAttribute).token.text, "@deprecated") - assert.eq(funcStat.func.functionkeyword.text, "function") + assert.eq(funcStat.func.functionKeyword.text, "function") assert.eq(funcStat.name.tag, "global") assert.eq((funcStat.name :: syntax.AstExprGlobal).name.text, "add") -- body parsing tested in parseExprFunction @@ -839,7 +839,7 @@ test.suite("parse", function(suite) assert.eq(localFuncStat.tag, "localfunction") assert.eq(#localFuncStat.func.attributes, 0) assert.eq(localFuncStat.localKeyword.text, "local") - assert.eq(localFuncStat.func.functionkeyword.text, "function") + assert.eq(localFuncStat.func.functionKeyword.text, "function") assert.eq(localFuncStat.name.name.text, "multiply") end) @@ -886,7 +886,7 @@ test.suite("parse", function(suite) assert.eq(typeFunc.tag, "typefunction") assert.eq(typeFunc.export, nil) assert.eq(typeFunc.type.text, "type") - assert.eq(typeFunc.body.functionkeyword.text, "function") + assert.eq(typeFunc.body.functionKeyword.text, "function") assert.eq(typeFunc.name.text, "foo") block = parser.parseblock("export type function foo() return types.number end") @@ -959,7 +959,7 @@ test.suite("parse types", function(suite) local singletonStr = typeAlias.type :: syntax.AstTypeSingletonString assert.eq(singletonStr.token.text, "hello") - assert.eq(singletonStr.quotestyle, "single") + assert.eq(singletonStr.quoteStyle, "single") block = parser.parseblock('type WorldType = "world"') typeAlias = block.statements[1] :: syntax.AstStatTypeAlias @@ -967,7 +967,7 @@ test.suite("parse types", function(suite) singletonStr = typeAlias.type :: syntax.AstTypeSingletonString assert.eq(singletonStr.token.text, "world") - assert.eq(singletonStr.quotestyle, "double") + assert.eq(singletonStr.quoteStyle, "double") end) suite:case("testTypeTypeof", function(assert) @@ -1355,7 +1355,7 @@ test.suite("AST_nodes_frozen", function(suite) (expr :: any).expr = nil end) assert.throwsWith(frozenTableMessage, function() - table.insert((expr :: any).typearguments, nil :: any) + table.insert((expr :: any).typeArguments, nil :: any) end) end) @@ -1397,7 +1397,7 @@ test.suite("AST_nodes_frozen", function(suite) table.insert((expr :: any).generics, nil :: any) end) assert.throwsWith(frozenTableMessage, function() - table.insert((expr :: any).genericpacks, nil :: any) + table.insert((expr :: any).genericPacks, nil :: any) end) end) @@ -1406,7 +1406,7 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(expr.kind, "expr") assert.eq(expr.tag, "table") assert.throwsWith(frozenTableMessage, function() - (expr :: any).openbrace = nil + (expr :: any).openBrace = nil end) assert.throwsWith(frozenTableMessage, function() table.insert((expr :: any).entries, nil :: any) @@ -1436,7 +1436,7 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(expr.kind, "expr") assert.eq(expr.tag, "binary") assert.throwsWith(frozenTableMessage, function() - (expr :: any).lhsoperand = nil + (expr :: any).lhsOperand = nil end) end) @@ -1718,7 +1718,7 @@ test.suite("AST_nodes_frozen", function(suite) assert.eq(singletonStr.kind, "type") assert.eq(singletonStr.tag, "string") assert.throwsWith(frozenTableMessage, function() - (singletonStr :: any).quotestyle = "double" + (singletonStr :: any).quoteStyle = "double" end) end) From 07ad00b92278c8f29ec271ec2ca5801118ee0cb0 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:26:47 -0700 Subject: [PATCH 434/642] Consolidates `assert` equality checks into `assert.eq`, unexposing `tableeq` and `buffereq` (#904) Updates `assert.eq` to perform structural equality, calling `assert.tableeq` and `assert.buffereq` to simplify user experience for the assertion library. We also unexpose `tableeq`, `buffereq` so users only need to use `assert.eq` to test equality Updates tests and snapshots to reflect changes --- examples/testing.luau | 4 +- lute/std/libs/test/assert.luau | 61 +++++++++++++------ lute/std/libs/test/failure.luau | 6 +- lute/std/libs/test/types.luau | 4 -- tests/lute/crypto.test.luau | 2 +- tests/std/process.test.luau | 8 +-- .../assert_buffereq_unequal_length.snap.luau | 2 +- .../snapshots/assert_error_eq_no_error.snap | 2 +- ...rt_tableeq_error_message_in_case.snap.luau | 2 +- ...t_tableeq_error_message_in_suite.snap.luau | 2 +- .../buffereq_failure_suite.snap.luau | 2 +- .../runtime_error_doesnt_report_xpcall.snap | 2 +- ...ntime_error_doesnt_report_xpcall_case.snap | 2 +- tests/std/tableext.test.luau | 6 +- tests/std/test.test.luau | 8 +-- 15 files changed, 69 insertions(+), 44 deletions(-) diff --git a/examples/testing.luau b/examples/testing.luau index 9017ddeda..ec1e9ca64 100644 --- a/examples/testing.luau +++ b/examples/testing.luau @@ -47,8 +47,8 @@ test.suite("MySuite", function(suite) suite:case("table_comparison", function(assert) local table1 = { a = 1, b = { c = 2, d = 3 } } local table2 = { a = 1, b = { c = 2, d = 3 } } - assert.tableeq(table1, table2) - assert.tableeq(table1, { a = 1 }) + assert.eq(table1, table2) + assert.eq(table1, { a = 1 }) end) suite:case("expect_error", function(assert) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index eede1f7ff..6bfa22b6d 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -63,14 +63,6 @@ local function tablesEqual(t1: unknown, t2: unknown, path: string): (boolean, st return true, nil end -local function eq(lhs: T, rhs: T, msg: string?): Failure? - if lhs == rhs then - return nil - end - - return assertion(msg or `{lhs} ~= {rhs}`) -end - local function neq(lhs: T, rhs: T, msg: string?): Failure? if lhs ~= rhs then return nil @@ -97,26 +89,32 @@ local function throws(func: (T...) -> ...unknown, ...: T...): Failure? return assertion(`{func} did not throw error.`) end +-- FIXME(Luau): We would like this to be two read-only indexers, but the +-- best option we have otherwise is two error suppressing indexers. -- Table equality assertionsion with deep comparison -local function tableeq(lhs: { [any]: any }, rhs: { [any]: any }): Failure? +local function tableeq(lhs: { [any]: any }, rhs: { [any]: any }, depth: number?): Failure? + local stackdepth = depth or 4 + local equal, msg = tablesEqual(lhs, rhs, "") if equal then return nil end - return assertion(msg or "tables are not equal") + return assertion(msg or "tables are not equal", stackdepth) end -local function buffereq(lhs: buffer, rhs: buffer): Failure? +local function buffereq(lhs: buffer, rhs: buffer, depth: number?): Failure? + local stackdepth = depth or 4 + if typeof(lhs) ~= "buffer" then - return assertion(`lhs is of type {typeof(lhs)}, not buffer`) + return assertion(`lhs is of type {typeof(lhs)}, not buffer`, stackdepth) end if typeof(rhs) ~= "buffer" then - return assertion(`rhs is of type {typeof(rhs)}, not buffer`) + return assertion(`rhs is of type {typeof(rhs)}, not buffer`, stackdepth) end if buffer.len(lhs) ~= buffer.len(rhs) then - return assertion("buffers are of unequal length") + return assertion("buffers are of unequal length", stackdepth) end local len = buffer.len(lhs) @@ -125,12 +123,43 @@ local function buffereq(lhs: buffer, rhs: buffer): Failure? local right = buffer.readu8(rhs, offset) if left ~= right then - return assertion(string.format("lhs[%d] = %02x, but rhs[%d] = %02x", offset, left, offset, right)) + return assertion( + string.format("lhs[%d] = %02x, but rhs[%d] = %02x", offset, left, offset, right), + stackdepth + ) end end return nil end +local function eq(lhs: unknown, rhs: unknown, msg: string?): Failure? + local stackdepth = 4 + + -- Simple equality check for non-tables + if lhs == rhs then + return nil + end + + -- Handle type mismatch + if typeof(lhs) ~= typeof(rhs) then + return assertion(msg or `lhs is of type {typeof(lhs)}, but rhs is of type {typeof(rhs)}`) + end + + -- Check for buffer equality + if typeof(lhs) == "buffer" then + -- increment stack depth for debug info by 1 when calling buffereq to account for additional function call (eq -> buffereq) + return buffereq(lhs :: buffer, rhs :: buffer, stackdepth + 1) + end + + -- Check for table equality + if typeof(lhs) == "table" then + -- increment stack depth for debug info by 1 when calling tableeq to account for additional function call (eq -> tableeq) + return tableeq(lhs :: { [any]: any }, rhs :: { [any]: any }, stackdepth + 1) + end + + return assertion(msg or `{lhs} ~= {rhs}`) +end + local function throwsWith(with: any, func: (T...) -> ...unknown, ...: T...): Failure? local success, err = pcall(func, ...) if success then @@ -197,8 +226,6 @@ return table.freeze({ eq = reqassert(eq), neq = reqassert(neq), throws = reqassert(throws), - tableeq = reqassert(tableeq), - buffereq = reqassert(buffereq), throwsWith = reqassert(throwsWith), strcontains = reqassert(strcontains), strnotcontains = reqassert(strnotcontains), diff --git a/lute/std/libs/test/failure.luau b/lute/std/libs/test/failure.luau index a8a488272..ec0bdec97 100644 --- a/lute/std/libs/test/failure.luau +++ b/lute/std/libs/test/failure.luau @@ -6,10 +6,12 @@ local types = require("./types") type Failure = types.Failure local failures = {} + -- Generates the debug information needed to describe assertion failure locations -function failures.assertion(msg: string): Failure +function failures.assertion(msg: string, depth: number?): Failure -- we need to go 5 function calls up to get to the actual assertion that invoked this - local filename, linenumber = debug.info(4, "sl") + local stackdepth = depth or 4 + local filename, linenumber = debug.info(stackdepth, "sl") local name = debug.info(2, "n") return { assertion = name, msg = msg, filename = filename, linenumber = linenumber, __tag = "assertion" } end diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index 6b74ec4e0..06a0db248 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -30,10 +30,6 @@ export type Asserts = { eq: (T, T, string?) -> Failure?, neq: (T, T, string?) -> Failure?, throws: ((T...) -> ...unknown, T...) -> Failure?, - -- FIXME(Luau): We would like this to be two read-only indexers, but the - -- best option we have otherwise is two error suppressing indexers. - tableeq: ({ [any]: any }, { [any]: any }) -> Failure?, - buffereq: (buffer, buffer) -> Failure?, throwsWith: (any, (T...) -> ...unknown, T...) -> Failure?, strcontains: (string, string, number?, string?) -> Failure?, strnotcontains: (string, string, string?) -> Failure?, diff --git a/tests/lute/crypto.test.luau b/tests/lute/crypto.test.luau index 8dd4cdce9..6af4f1854 100644 --- a/tests/lute/crypto.test.luau +++ b/tests/lute/crypto.test.luau @@ -21,7 +21,7 @@ test.suite("CryptoRuntimeTests", function(suite) local box = crypto.secretbox.seal(buf) local opened = crypto.secretbox.open(box) - assert.buffereq(opened, buf) + assert.eq(opened, buf) end) suite:case("secretboxSeparateKeygen", function(assert) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index 926ab7922..a0662643b 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -44,14 +44,14 @@ test.suite("ProcessSuite", function(suite) if system.win32 then assert(root ~= nil and (root :: windowsPath.Path).drive ~= nil) - check.tableeq(r, { + check.eq(r, { exitcode = 0, stdout = `/{root.drive:lower()}\n`, stderr = "", ok = true, }) else - check.tableeq(r, { + check.eq(r, { exitcode = 0, stdout = "/\n", stderr = "", @@ -61,7 +61,7 @@ test.suite("ProcessSuite", function(suite) -- LUAUFIX: we shouldn't need to upcast the result of `process.system` to `{ [any]: any }` local r2 = process.system("echo hello!") :: { [any]: any } - check.tableeq(r2, { + check.eq(r2, { exitcode = 0, stdout = "hello!\n", stderr = "", @@ -72,7 +72,7 @@ test.suite("ProcessSuite", function(suite) suite:case("runNonzeroExitcode", function(assert) -- LUAUFIX: we shouldn't need to upcast the result of `process.run` to `{ [any]: any }` local r = process.run({ "sh", "-c", "exit 41" }) :: { [any]: any } - assert.tableeq(r, { + assert.eq(r, { exitcode = 41, stdout = "", stderr = "", diff --git a/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau b/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau index 77cf73021..eb8b2a305 100644 --- a/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau +++ b/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau @@ -3,7 +3,7 @@ local test = require("@std/test") test.case("buffereq_error", function(assert) local buf1 = buffer.create(1) local buf2 = buffer.create(2) - assert.buffereq(buf1, buf2) + assert.eq(buf1, buf2) end) test.run() diff --git a/tests/std/snapshots/assert_error_eq_no_error.snap b/tests/std/snapshots/assert_error_eq_no_error.snap index 1344ae416..bd2f8ef7a 100755 --- a/tests/std/snapshots/assert_error_eq_no_error.snap +++ b/tests/std/snapshots/assert_error_eq_no_error.snap @@ -7,7 +7,7 @@ Failed Tests (1): ❌ throwsWithError /tests/std/snapshots/assert_error_eq_no_error.snap.luau:4 - throwsWith: Function did not error as expected. + throwsWith: Expected function to throw/raise error. -------------------------------------------------- Total: 1 diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau index 9d55def06..806f23c99 100644 --- a/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau +++ b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau @@ -1,7 +1,7 @@ local test = require("@std/test") test.case("tableeq_error", function(assert) - assert.tableeq({ a = 1 }, { a = 2 }) + assert.eq({ a = 1 }, { a = 2 }) end) test.run() diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau index 5de8ba608..0c7807bcf 100644 --- a/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau +++ b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau @@ -2,7 +2,7 @@ local test = require("@std/test") test.suite("tableeq_failure_suite", function(suite) suite:case("tableeq_error", function(assert) - assert.tableeq({ a = 1 }, { a = 2 }) + assert.eq({ a = 1 }, { a = 2 }) end) end) diff --git a/tests/std/snapshots/buffereq_failure_suite.snap.luau b/tests/std/snapshots/buffereq_failure_suite.snap.luau index 2062ba1f7..c2925a2c3 100644 --- a/tests/std/snapshots/buffereq_failure_suite.snap.luau +++ b/tests/std/snapshots/buffereq_failure_suite.snap.luau @@ -6,7 +6,7 @@ test.suite("buffereq_failure_suite", function(suite) local buf2 = buffer.create(2) buffer.writeu8(buf1, 0, 84) buffer.writeu8(buf2, 0, 85) - assert.buffereq(buf1, buf2) + assert.eq(buf1, buf2) end) end) diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap index 11325881a..74d9ee400 100755 --- a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap @@ -9,7 +9,7 @@ Failed Tests (1): Runtime error: /tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6: attempt to index nil with 'field' Stacktrace: /tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6: attempt to index nil with 'field' -@std/test/failure.luau:27 function runtimeerror +@std/test/failure.luau:29 function runtimeerror @std/test/runner.luau:117 function handlefailure /tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6 @std/test/runner.luau:130 function run diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap index 27efa663a..925db0e15 100755 --- a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap @@ -9,7 +9,7 @@ Failed Tests (1): Runtime error: /tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5: attempt to index nil with 'field' Stacktrace: /tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5: attempt to index nil with 'field' -@std/test/failure.luau:27 function runtimeerror +@std/test/failure.luau:29 function runtimeerror @std/test/runner.luau:87 function handlefailure /tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5 @std/test/runner.luau:93 function run diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index b1aafeb50..24fe78afe 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -54,13 +54,13 @@ test.suite("TableExt", function(suite) -- in-place local a = { 1, 2, 3 } tableext.reverse(a, true) - assert.tableeq(a, { 3, 2, 1 }) + assert.eq(a, { 3, 2, 1 }) -- return new local b = { 4, 5, 6 } local c = tableext.reverse(b) - assert.tableeq(b, { 4, 5, 6 }) - assert.tableeq(c, { 6, 5, 4 }) + assert.eq(b, { 4, 5, 6 }) + assert.eq(c, { 6, 5, 4 }) end) suite:case("toset", function(assert) diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index a4135f9fc..593ef25cf 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -176,7 +176,7 @@ test.run() local test = require("@std/test") test.case("tableeqError", function(assert) - assert.tableeq({ a = 1 }, { a = 2 }) + assert.eq({ a = 1 }, { a = 2 }) end) test.run() @@ -194,7 +194,7 @@ local test = require("@std/test") test.suite("tableeq_failure_suite", function(suite) suite:case("tableeqError", function(assert) - assert.tableeq({ a = 1 }, { a = 2 }) + assert.eq({ a = 1 }, { a = 2 }) end) end) @@ -214,7 +214,7 @@ local test = require("@std/test") test.case("buffereqError", function(assert) local buf1 = buffer.create(1) local buf2 = buffer.create(2) - assert.buffereq(buf1, buf2) + assert.eq(buf1, buf2) end) test.run() @@ -236,7 +236,7 @@ test.suite("buffereq_failure_suite", function(suite) local buf2 = buffer.create(2) buffer.writeu8(buf1, 0, 84) buffer.writeu8(buf2, 0, 85) - assert.buffereq(buf1, buf2) + assert.eq(buf1, buf2) end) end) From f62bf2a4f99f33731bbf13881fd435a4adbfff12 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Tue, 31 Mar 2026 13:52:36 -0700 Subject: [PATCH 435/642] Exposes @std/task as a wrapper around functions defined in @lute/task (#906) This PR exposes the `task` library methods in `@std` as wrappers around the task methods in `@lute`. I've left in the previous implementations of `task.await/awaitAll/create` for now, because we're still exposing the superset that users would expect of the roblox api. --- examples/net_example.luau | 2 +- examples/parallel_sort.luau | 2 +- examples/process.luau | 2 +- examples/spawn_example.luau | 4 ++-- lute/std/libs/task.luau | 28 ++++++++++++++++++++++++---- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/examples/net_example.luau b/examples/net_example.luau index 1935e695e..cf542586f 100644 --- a/examples/net_example.luau +++ b/examples/net_example.luau @@ -22,7 +22,7 @@ local together = true local start = os.clock() if together then - task.awaitall(t1, t2, t3) + task.awaitAll(t1, t2, t3) else task.await(t1) task.await(t2) diff --git a/examples/parallel_sort.luau b/examples/parallel_sort.luau index dc177b901..012725786 100644 --- a/examples/parallel_sort.luau +++ b/examples/parallel_sort.luau @@ -55,7 +55,7 @@ local function parallelMergeSort(t: { T }, comp: (T, T) -> lt): { T } table.insert(slices, task.create(threads[i].sort, getslice(t, i, threadCount))) end - local tomerge = table.pack(task.awaitall(table.unpack(slices))) + local tomerge = table.pack(task.awaitAll(table.unpack(slices))) tomerge.n = nil :: any while #tomerge > 1 do diff --git a/examples/process.luau b/examples/process.luau index a851ab2d0..9bf3859d5 100644 --- a/examples/process.luau +++ b/examples/process.luau @@ -12,7 +12,7 @@ local t1 = task.create(process.run, { "echo", "-n", "One" }) local t2 = task.create(process.run, { "echo", "-n", "Two" }) local t3 = task.create(process.run, { "echo", "-n", "Three" }) -local r1, r2, r3 = task.awaitall(t1, t2, t3) +local r1, r2, r3 = task.awaitAll(t1, t2, t3) print(r1.stdout) print(r2.stdout) diff --git a/examples/spawn_example.luau b/examples/spawn_example.luau index 32019a45a..822878c7f 100644 --- a/examples/spawn_example.luau +++ b/examples/spawn_example.luau @@ -45,7 +45,7 @@ do local s1, s2, s3 = task.create(vm1.fib, 33), task.create(vm1.fib, 33), task.create(vm1.fib, 33) - print("fib(33):", task.awaitall(s1, s2, s3)) + print("fib(33):", task.awaitAll(s1, s2, s3)) print("3 serial fibs in", os.clock() - start) end @@ -55,7 +55,7 @@ do local p1, p2, p3 = task.create(vm1.fib, 33), task.create(vm2.fib, 33), task.create(vm3.fib, 33) - print("fib(33):", task.awaitall(p1, p2, p3)) + print("fib(33):", task.awaitAll(p1, p2, p3)) print("3 parallel fibs in", os.clock() - start) end diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index 424756136..f374288ee 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -3,7 +3,7 @@ -- stdlib for `@lute/task` -- Provides utilities for creating and managing asynchronous tasks -local task = require("@lute/task") +local lutetask = require("@lute/task") local tasklib = {} @@ -13,6 +13,26 @@ export type Task = { co: thread, } +function tasklib.spawn(routine: ((T...) -> U...) | thread, ...: T...): thread + return lutetask.spawn(routine, ...) +end + +function tasklib.defer(routine: ((T...) -> U...) | thread, ...: T...): thread + return lutetask.defer(routine, ...) +end + +function tasklib.delay(dur: number, routine: ((T...) -> U...) | thread, ...: T...): thread + return lutetask.delay(dur, routine, ...) +end + +function tasklib.wait(dur: number?): number + return lutetask.wait(dur) +end + +function tasklib.cancel(thread: thread) + coroutine.close(thread) +end + function tasklib.create(f, ...): Task local data = {} @@ -33,7 +53,7 @@ function tasklib.await(t: Task): any end while t.success == nil do - task.deferSelf() + lutetask.deferSelf() end if t.success then @@ -43,7 +63,7 @@ function tasklib.await(t: Task): any end end -function tasklib.awaitall(...: Task): ...unknown +function tasklib.awaitAll(...: Task): ...unknown local tasks = table.pack(...) for i, v in ipairs(tasks) do @@ -59,7 +79,7 @@ function tasklib.awaitall(...: Task): ...unknown for _, v in ipairs(tasks) do if v.success == nil then done = false - task.deferSelf() + lutetask.deferSelf() end end end From a83c39fd3a4a95c5cf69895a6f15e5ef4c6ab5e2 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:36:44 -0700 Subject: [PATCH 436/642] Refactors std APIs `json`, `luau`, `tableext` functions to be camelCase (#920) see https://github.com/luau-lang/lute/issues/919 --- definitions/luau.luau | 2 +- examples/json.luau | 2 +- .../get_module_return_type.luau | 2 +- .../commands/lint/rules/unused_variable.luau | 6 +++--- lute/luau/include/lute/luau.h | 4 ++-- lute/luau/src/luau.cpp | 2 +- lute/std/libs/json.luau | 4 ++-- lute/std/libs/luau.luau | 4 ++-- lute/std/libs/tableext.luau | 2 +- tests/std/json.test.luau | 8 ++++---- tests/std/luau.test.luau | 20 +++++++++---------- tests/std/tableext.test.luau | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 272ba3d3f..31ee2c6a7 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -809,7 +809,7 @@ export type TypePack = { ispack: boolean, } -function luau.typeofmodule(modulepath: string): TypePack? +function luau.typeofModule(modulepath: string): TypePack? error("not implemented") end diff --git a/examples/json.luau b/examples/json.luau index c2c7e97c0..2376be8d5 100644 --- a/examples/json.luau +++ b/examples/json.luau @@ -9,5 +9,5 @@ print(serialize) local deserialized = json.deserialize([[{ "hello": "world" }]]) -deserialized = assert(json.asobject(deserialized)) +deserialized = assert(json.asObject(deserialized)) print(deserialized.hello) diff --git a/examples/module_return_type/get_module_return_type.luau b/examples/module_return_type/get_module_return_type.luau index 9154a84a3..770df4269 100644 --- a/examples/module_return_type/get_module_return_type.luau +++ b/examples/module_return_type/get_module_return_type.luau @@ -3,7 +3,7 @@ local path = require("@std/path") local process = require("@std/process") local filePath = path.join(process.cwd(), "examples", "module_return_type", "mainmodule.luau") -local returnType = luau.typeofmodule(filePath) +local returnType = luau.typeofModule(filePath) if not returnType then error("Failed to get module return type") diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index 798cf9150..dc72d6b93 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -408,12 +408,12 @@ local function lint( ) end - writeOnlyAPIs.entries[parts[1]] = { tag = "leaf", vals = tableext.toset(v :: { number }) } -- We validate v above, so the cast is ok + writeOnlyAPIs.entries[parts[1]] = { tag = "leaf", vals = tableext.toSet(v :: { number }) } -- We validate v above, so the cast is ok else if not writeOnlyAPIs.entries[parts[1]] then writeOnlyAPIs.entries[parts[1]] = { tag = "branch", - entries = { [parts[2]] = { tag = "leaf", vals = tableext.toset(v :: { number }) } }, + entries = { [parts[2]] = { tag = "leaf", vals = tableext.toSet(v :: { number }) } }, } else local branch = writeOnlyAPIs.entries[parts[1]] @@ -422,7 +422,7 @@ local function lint( `API '{parts[1]}' is already defined as a write-only API, so '{k}' cannot be added as a write-only API.` ) end - branch.entries[parts[2]] = { tag = "leaf", vals = tableext.toset(v :: { number }) } -- We validate v above, so the cast is ok + branch.entries[parts[2]] = { tag = "leaf", vals = tableext.toSet(v :: { number }) } -- We validate v above, so the cast is ok end end end diff --git a/lute/luau/include/lute/luau.h b/lute/luau/include/lute/luau.h index 08c8f3f77..8e81d7fb6 100644 --- a/lute/luau/include/lute/luau.h +++ b/lute/luau/include/lute/luau.h @@ -25,7 +25,7 @@ int compile_luau(lua_State* L); int load_luau(lua_State* L); -int typeofmodule_luau(lua_State* L); +int typeofModule_luau(lua_State* L); static const luaL_Reg lib[] = { {"parse", luau_parse}, @@ -33,7 +33,7 @@ static const luaL_Reg lib[] = { {"compile", compile_luau}, {"load", load_luau}, {"resolveModule", resolveModule_luau}, - {"typeofmodule", typeofmodule_luau}, + {"typeofModule", typeofModule_luau}, {nullptr, nullptr}, }; diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 3d090fabe..0a84e9b55 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -3028,7 +3028,7 @@ int load_luau(lua_State* L) return 1; } -int typeofmodule_luau(lua_State* L) +int typeofModule_luau(lua_State* L) { std::string modulePath = luaL_checkstring(L, 1); diff --git a/lute/std/libs/json.luau b/lute/std/libs/json.luau index 264341c13..9add17010 100644 --- a/lute/std/libs/json.luau +++ b/lute/std/libs/json.luau @@ -561,7 +561,7 @@ function json.object(props: { [string]: Value }): Object return res end -function json.asobject(value: Value): Object? +function json.asObject(value: Value): Object? if typeof(value) == "table" and value[object_key] then return value :: Object end @@ -569,7 +569,7 @@ function json.asobject(value: Value): Object? return nil end -function json.asarray(value: Value): Array? +function json.asArray(value: Value): Array? if typeof(value) == "table" and not value[object_key] then return value :: Array end diff --git a/lute/std/libs/luau.luau b/lute/std/libs/luau.luau index 2450b43c0..7ba42b471 100644 --- a/lute/std/libs/luau.luau +++ b/lute/std/libs/luau.luau @@ -35,10 +35,10 @@ end export type TypePack = luteLuau.TypePack export type Type = luteLuau.Type -function luau.typeofmodule(modulepath: path.Pathlike): TypePack? +function luau.typeofModule(modulepath: path.Pathlike): TypePack? local modulePathStr = if typeof(modulepath) == "string" then modulepath else path.format(modulepath) - return luteLuau.typeofmodule(modulePathStr) + return luteLuau.typeofModule(modulePathStr) end function luau.resolveModule(modulepath: string, frompath: path.Pathlike): path.Pathlike diff --git a/lute/std/libs/tableext.luau b/lute/std/libs/tableext.luau index 783f12cc0..70323a6b2 100644 --- a/lute/std/libs/tableext.luau +++ b/lute/std/libs/tableext.luau @@ -50,7 +50,7 @@ end -- Functions on array-like tables -function tableext.toset(tbl: { T }): { [T]: true } +function tableext.toSet(tbl: { T }): { [T]: true } local set: { [T]: true } = {} for _, v in tbl do set[v] = true diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau index 9bf9a6db9..2a7152452 100644 --- a/tests/std/json.test.luau +++ b/tests/std/json.test.luau @@ -48,7 +48,7 @@ test.suite("JSONParsingTestSuite", function(suite) end) end - suite:case("jsonObjectAndAsObject", function(assert) + suite:case("jsonObjectAndasObject", function(assert) local obj = json.object({ name = "Alice", age = 30, @@ -58,7 +58,7 @@ test.suite("JSONParsingTestSuite", function(suite) assert.eq(obj.age, 30) end) - suite:case("jsonAsObjectTest", function(assert) + suite:case("jsonasObjectTest", function(assert) local myjsonstr = [[ { "a": true, @@ -70,13 +70,13 @@ test.suite("JSONParsingTestSuite", function(suite) ]] local maybeJson = json.deserialize(myjsonstr) - local obj = json.asobject(maybeJson) + local obj = json.asObject(maybeJson) if obj then assert.eq(type(obj), "table") assert.that(obj.a) assert.eq(type(obj.b), "table") - local obj_b = json.asobject(obj.b) + local obj_b = json.asObject(obj.b) if obj_b then assert.that(obj_b.c) local obj_b_d = obj_b.d diff --git a/tests/std/luau.test.luau b/tests/std/luau.test.luau index 43b2c47a3..2ece0e425 100644 --- a/tests/std/luau.test.luau +++ b/tests/std/luau.test.luau @@ -6,9 +6,9 @@ local test = require("@std/test") local tmpDir = system.tmpdir() -test.suite("LuauTypeOfModuleSuite", function(suite) - suite:case("typeofmoduleReturnsTable", function(assert) - local testPath = path.join(tmpDir, "typeofmodule_test.luau") +test.suite("LuauTypeofModuleSuite", function(suite) + suite:case("typeofModuleReturnsTable", function(assert) + local testPath = path.join(tmpDir, "typeofModule_test.luau") local testFile = fs.open(testPath, "w+") fs.write( testFile, @@ -24,7 +24,7 @@ test.suite("LuauTypeOfModuleSuite", function(suite) ) fs.close(testFile) - local typePack = luau.typeofmodule(testPath) + local typePack = luau.typeofModule(testPath) assert.neq(typePack, nil) if not typePack then error("typePack should not be nil") @@ -76,8 +76,8 @@ test.suite("LuauTypeOfModuleSuite", function(suite) assert.eq(foundProperty, true, "Should find example_string_func property") end) - suite:case("typeofmoduleReturnsTypepackWithMultipleValues", function(assert) - local testPath = path.join(tmpDir, "typeofmodule_multiple.luau") + suite:case("typeofModuleReturnsTypepackWithMultipleValues", function(assert) + local testPath = path.join(tmpDir, "typeofModule_multiple.luau") local testFile = fs.open(testPath, "w+") fs.write( testFile, @@ -87,7 +87,7 @@ test.suite("LuauTypeOfModuleSuite", function(suite) ) fs.close(testFile) - local typePack = luau.typeofmodule(testPath) + local typePack = luau.typeofModule(testPath) assert.neq(typePack, nil) if not typePack then error("typePack should not be nil") @@ -108,8 +108,8 @@ test.suite("LuauTypeOfModuleSuite", function(suite) assert.eq(typePack.head[3].tag, "boolean") end) - suite:case("typeofmoduleReturnsTypepackWithVariadicTail", function(assert) - local testPath = path.join(tmpDir, "typeofmodule_variadic.luau") + suite:case("typeofModuleReturnsTypepackWithVariadicTail", function(assert) + local testPath = path.join(tmpDir, "typeofModule_variadic.luau") local testFile = fs.open(testPath, "w+") fs.write( testFile, @@ -122,7 +122,7 @@ test.suite("LuauTypeOfModuleSuite", function(suite) ) fs.close(testFile) - local typePack = luau.typeofmodule(testPath) + local typePack = luau.typeofModule(testPath) assert.neq(typePack, nil) if not typePack then error("typePack should not be nil") diff --git a/tests/std/tableext.test.luau b/tests/std/tableext.test.luau index 24fe78afe..1c1fe9ed4 100644 --- a/tests/std/tableext.test.luau +++ b/tests/std/tableext.test.luau @@ -63,9 +63,9 @@ test.suite("TableExt", function(suite) assert.eq(c, { 6, 5, 4 }) end) - suite:case("toset", function(assert) + suite:case("toSet", function(assert) local a = { 1, 2, 4, 2, 1 } - local setA = tableext.toset(a) + local setA = tableext.toSet(a) assert.that(setA[1]) assert.that(setA[2]) assert.eq(setA[3], nil) @@ -89,7 +89,7 @@ test.suite("TableExt", function(suite) assert.that(setA[foundKeys[3]]) local b = { "a", "b", "c", "a" } - local setB = tableext.toset(b) + local setB = tableext.toSet(b) assert.that(setB["a"]) assert.that(setB["b"]) assert.that(setB["c"]) @@ -98,7 +98,7 @@ test.suite("TableExt", function(suite) assert.neq(#tableext.keys(setB), #b) local empty = {} - local emptySet = tableext.toset(empty) + local emptySet = tableext.toSet(empty) assert.eq(#emptySet, 0) end) From eea0256f1a90495911eca547e701a8bf35385224 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 1 Apr 2026 11:37:51 -0700 Subject: [PATCH 437/642] Refactor path library to match conventions (#921) isAbsolute is now camelCased per #919 --- lute/cli/commands/doc/init.luau | 4 ++-- lute/cli/commands/lib/files.luau | 2 +- lute/cli/commands/test/finder.luau | 2 +- lute/std/libs/fs.luau | 2 +- lute/std/libs/path/init.luau | 6 +++--- lute/std/libs/path/posix/init.luau | 6 +++--- lute/std/libs/path/win32/init.luau | 6 +++--- tests/std/path/path.posix.test.luau | 14 +++++++------- tests/std/path/path.win32.test.luau | 16 ++++++++-------- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lute/cli/commands/doc/init.luau b/lute/cli/commands/doc/init.luau index 8d2590043..c01781245 100644 --- a/lute/cli/commands/doc/init.luau +++ b/lute/cli/commands/doc/init.luau @@ -361,7 +361,7 @@ API reference documentation for Lute's built-in libraries. -- Generate docs for each specified module path for _, modulePath in modulePaths do local modulePathObj = path.parse(modulePath) - local absModulePath = if path.isabsolute(modulePathObj) + local absModulePath = if path.isAbsolute(modulePathObj) then modulePathObj else path.resolve(process.cwd(), modulePathObj) @@ -412,7 +412,7 @@ local function main(...: string) end local outputDir = path.parse(args:get("output") or tostring(DEFAULT_OUTPUT_DIR)) - if not path.isabsolute(outputDir) then + if not path.isAbsolute(outputDir) then outputDir = path.resolve(process.cwd(), outputDir) end diff --git a/lute/cli/commands/lib/files.luau b/lute/cli/commands/lib/files.luau index 4e908438e..77b4d84ee 100644 --- a/lute/cli/commands/lib/files.luau +++ b/lute/cli/commands/lib/files.luau @@ -52,7 +52,7 @@ local function getSourceFiles(paths: { string }, verbose: boolean?): { path.Path for _, filepath in paths do local pathObj = path.parse(filepath) - if not path.isabsolute(pathObj) then + if not path.isAbsolute(pathObj) then pathObj = path.resolve(process.cwd(), pathObj) end diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index 27a594c2b..c1a64dd2f 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -26,7 +26,7 @@ local function findfiles(paths: { string }, predicate: (string) -> boolean): { p local cwd = ps.cwd() for _, p in paths do - if path.isabsolute(p) then + if path.isAbsolute(p) then walk(p) else walk(path.join(cwd, p)) diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index ee4ccec2f..e3d678be3 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -137,7 +137,7 @@ function fslib.createdirectory(path: Pathlike, options: CreateDirectoryOptions?) local parts: { string } = parsed.parts local subdir: Pathlike = "." - if pathlib.isabsolute(path) then + if pathlib.isAbsolute(path) then if sys.win32 then -- Windows path - get the drive root like "C:\" subdir = pathlib.win32.drive(parsed :: win32path) diff --git a/lute/std/libs/path/init.luau b/lute/std/libs/path/init.luau index 2be969c3e..be5f41e1d 100644 --- a/lute/std/libs/path/init.luau +++ b/lute/std/libs/path/init.luau @@ -44,11 +44,11 @@ function pathlib.format(path: Pathlike): string end end -function pathlib.isabsolute(path: Pathlike): boolean +function pathlib.isAbsolute(path: Pathlike): boolean if onWindows then - return win32.isabsolute(path :: win32.Pathlike) + return win32.isAbsolute(path :: win32.Pathlike) else - return posix.isabsolute(path :: posix.Pathlike) + return posix.isAbsolute(path :: posix.Pathlike) end end diff --git a/lute/std/libs/path/posix/init.luau b/lute/std/libs/path/posix/init.luau index 984554379..b58e54e61 100644 --- a/lute/std/libs/path/posix/init.luau +++ b/lute/std/libs/path/posix/init.luau @@ -59,7 +59,7 @@ function posix.format(path: Pathlike): string end end -function posix.isabsolute(path: Pathlike): boolean +function posix.isAbsolute(path: Pathlike): boolean path = if typeof(path) == "string" then posix.parse(path) else path return path.absolute end @@ -178,7 +178,7 @@ function posix.resolve(...: Pathlike): Path local path = posix.parse(parts[#parts]) for i = #parts - 1, 1, -1 do - if posix.isabsolute(path) then + if posix.isAbsolute(path) then break end @@ -192,7 +192,7 @@ function posix.resolve(...: Pathlike): Path path = segment end - if not posix.isabsolute(path) then + if not posix.isAbsolute(path) then local cwd = posix.parse(process.cwd()) joinHelper(cwd, path) path = cwd diff --git a/lute/std/libs/path/win32/init.luau b/lute/std/libs/path/win32/init.luau index 9b368dee3..9d8912f24 100644 --- a/lute/std/libs/path/win32/init.luau +++ b/lute/std/libs/path/win32/init.luau @@ -91,7 +91,7 @@ function win32.format(path: Pathlike): string end end -function win32.isabsolute(path: Pathlike): boolean +function win32.isAbsolute(path: Pathlike): boolean path = if typeof(path) == "string" then win32.parse(path) else path return path.kind ~= "relative" end @@ -285,7 +285,7 @@ function win32.resolve(...: Pathlike): Path local path = win32.parse(parts[#parts]) for i = #parts - 1, 1, -1 do - if win32.isabsolute(path) then + if win32.isAbsolute(path) then break end @@ -299,7 +299,7 @@ function win32.resolve(...: Pathlike): Path path = segment end - if not win32.isabsolute(path) then + if not win32.isAbsolute(path) then local cwd = win32.parse(process.cwd()) joinHelper(cwd, path) path = cwd diff --git a/tests/std/path/path.posix.test.luau b/tests/std/path/path.posix.test.luau index 8053e9271..c4ab5c71f 100644 --- a/tests/std/path/path.posix.test.luau +++ b/tests/std/path/path.posix.test.luau @@ -243,24 +243,24 @@ test.suite("PathPosixExtnameSuite", function(suite) end) end) -test.suite("PathPosixIsAbsoluteSuite", function(suite) +test.suite("PathPosixisAbsoluteSuite", function(suite) suite:case("absolutePosixAbsolutePath", function(assert) - local result = path.posix.isabsolute("/home/user/documents/file.txt") + local result = path.posix.isAbsolute("/home/user/documents/file.txt") assert.that(result) end) suite:case("absolutePosixRelativePath", function(assert) - local result = path.posix.isabsolute("documents/file.txt") + local result = path.posix.isAbsolute("documents/file.txt") assert.eq(result, false) end) suite:case("absoluteRootPath", function(assert) - local result = path.posix.isabsolute("/") + local result = path.posix.isAbsolute("/") assert.that(result) end) suite:case("absoluteEmptyPath", function(assert) - local result = path.posix.isabsolute("") + local result = path.posix.isAbsolute("") assert.eq(result, false) end) @@ -269,7 +269,7 @@ test.suite("PathPosixIsAbsoluteSuite", function(suite) parts = { "home", "user", "file.txt" }, absolute = true, }, posix.pathmt) - local result = path.posix.isabsolute(pathobj) + local result = path.posix.isAbsolute(pathobj) assert.that(result) end) @@ -278,7 +278,7 @@ test.suite("PathPosixIsAbsoluteSuite", function(suite) parts = { "folder", "file.txt" }, absolute = false, }, posix.pathmt) - local result = path.posix.isabsolute(pathobj) + local result = path.posix.isAbsolute(pathobj) assert.eq(result, false) end) end) diff --git a/tests/std/path/path.win32.test.luau b/tests/std/path/path.win32.test.luau index 50fc2f34d..1c73d0aa7 100644 --- a/tests/std/path/path.win32.test.luau +++ b/tests/std/path/path.win32.test.luau @@ -345,32 +345,32 @@ end) test.suite("PathWin32IsAbsoluteSuite", function(suite) suite:case("isAbsoluteWindowsAbsolutePath", function(assert) - local result = path.win32.isabsolute("C:\\Users\\username\\Documents\\file.txt") + local result = path.win32.isAbsolute("C:\\Users\\username\\Documents\\file.txt") assert.that(result) end) suite:case("isAbsoluteWindowsRelativePath", function(assert) - local result = path.win32.isabsolute("Documents\\file.txt") + local result = path.win32.isAbsolute("Documents\\file.txt") assert.eq(result, false) end) suite:case("isAbsoluteWindowsRelativeWithDrive", function(assert) - local result = path.win32.isabsolute("C:Documents\\file.txt") + local result = path.win32.isAbsolute("C:Documents\\file.txt") assert.eq(result, false) end) suite:case("isAbsoluteWindowsUncPath", function(assert) - local result = path.win32.isabsolute("\\\\server\\share\\folder\\file.txt") + local result = path.win32.isAbsolute("\\\\server\\share\\folder\\file.txt") assert.that(result) end) suite:case("isAbsoluteRootPath", function(assert) - local result = path.win32.isabsolute("C:\\") + local result = path.win32.isAbsolute("C:\\") assert.that(result) end) suite:case("isAbsoluteEmptyPath", function(assert) - local result = path.win32.isabsolute("") + local result = path.win32.isAbsolute("") assert.eq(result, false) end) @@ -380,7 +380,7 @@ test.suite("PathWin32IsAbsoluteSuite", function(suite) kind = "absolute", drive = "C" :: string?, }, win32.pathmt) - local result = path.win32.isabsolute(pathobj) + local result = path.win32.isAbsolute(pathobj) assert.that(result) end) @@ -390,7 +390,7 @@ test.suite("PathWin32IsAbsoluteSuite", function(suite) kind = "relative", drive = nil :: string?, }, win32.pathmt) - local result = path.win32.isabsolute(pathobj) + local result = path.win32.isAbsolute(pathobj) assert.eq(result, false) end) end) From 75115a5cdb36e19b5b0abbd9d6a0d1ca9189c5df Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 1 Apr 2026 13:16:06 -0700 Subject: [PATCH 438/642] Refactors stringext library to match conventions (#922) - camelCases the members of the stringext library. - `local stringext = require("@std/stringext') -> local stringExt = require("@std/stringext")` - not strictly required, but I just feel like it's better. --- lute/cli/commands/lib/files.luau | 2 +- lute/cli/commands/lib/parseIgnores.luau | 6 +-- lute/cli/commands/test/finder.luau | 4 +- lute/cli/commands/test/snap/runner.luau | 4 +- .../cli/commands/transform/lib/arguments.luau | 4 +- lute/std/libs/stringext.luau | 12 ++--- lute/std/libs/test/assert.luau | 2 +- tests/std/json.test.luau | 4 +- tests/std/stringext.test.luau | 46 +++++++++---------- tests/std/syntax/parser.test.luau | 2 +- 10 files changed, 43 insertions(+), 43 deletions(-) diff --git a/lute/cli/commands/lib/files.luau b/lute/cli/commands/lib/files.luau index 77b4d84ee..d682956d9 100644 --- a/lute/cli/commands/lib/files.luau +++ b/lute/cli/commands/lib/files.luau @@ -27,7 +27,7 @@ local function traverseDirectoryRecursive( tableext.extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver, verbose)) else -- TODO: allow customisation of the filter when traversing a directory. e.g., globs? file endings? ignore paths? - if stringext.hassuffix(entry.name, ".luau") or stringext.hassuffix(entry.name, ".lua") then + if stringext.hasSuffix(entry.name, ".luau") or stringext.hasSuffix(entry.name, ".lua") then if ignoreResolver:isIgnoredFile(fullPath) then if verbose then print("Skipping", fullPath) diff --git a/lute/cli/commands/lib/parseIgnores.luau b/lute/cli/commands/lib/parseIgnores.luau index 5a353d97e..681d043e7 100644 --- a/lute/cli/commands/lib/parseIgnores.luau +++ b/lute/cli/commands/lib/parseIgnores.luau @@ -117,12 +117,12 @@ function parseIgnores.parseIgnoreContents(location: string, contents: string | { for _, line in lines do line = stringext.trim(line) - if line == "" or stringext.hasprefix(line, "#") then + if line == "" or stringext.hasPrefix(line, "#") then continue end - if stringext.hasprefix(line, "!") then - local globPattern = stringext.removeprefix(line, "!") + if stringext.hasPrefix(line, "!") then + local globPattern = stringext.removePrefix(line, "!") local glob = parseGlob(globPattern) table.insert(ignoreData.whitelists, glob) else diff --git a/lute/cli/commands/test/finder.luau b/lute/cli/commands/test/finder.luau index c1a64dd2f..b44ff2bb3 100644 --- a/lute/cli/commands/test/finder.luau +++ b/lute/cli/commands/test/finder.luau @@ -4,11 +4,11 @@ local path = require("@std/path") local stringext = require("@std/stringext") local function istestfile(filename: string): boolean - return stringext.hassuffix(filename, ".test.luau") or stringext.hassuffix(filename, ".spec.luau") + return stringext.hasSuffix(filename, ".test.luau") or stringext.hasSuffix(filename, ".spec.luau") end local function issnapfile(filename: string): boolean - return stringext.hassuffix(filename, ".snap.luau") + return stringext.hasSuffix(filename, ".snap.luau") end local function findfiles(paths: { string }, predicate: (string) -> boolean): { path.Path } diff --git a/lute/cli/commands/test/snap/runner.luau b/lute/cli/commands/test/snap/runner.luau index 7ff598b99..9e5eaa0ca 100644 --- a/lute/cli/commands/test/snap/runner.luau +++ b/lute/cli/commands/test/snap/runner.luau @@ -9,8 +9,8 @@ local snapty = require("./types") -- e.g. /foo/bar.snap.luau -> /foo/bar.snap local function artifactpath(filepath: path.Path): string local f = path.format(filepath) - assert(strext.hassuffix(f, ".snap.luau"), `expected {f} to have suffix .snap.luau`) - return strext.removesuffix(f, ".luau") -- strips ".luau" + assert(strext.hasSuffix(f, ".snap.luau"), `expected {f} to have suffix .snap.luau`) + return strext.removeSuffix(f, ".luau") -- strips ".luau" end local function posixify(str) diff --git a/lute/cli/commands/transform/lib/arguments.luau b/lute/cli/commands/transform/lib/arguments.luau index c625ecd72..e20237218 100644 --- a/lute/cli/commands/transform/lib/arguments.luau +++ b/lute/cli/commands/transform/lib/arguments.luau @@ -34,8 +34,8 @@ local function parse(arguments: { string }): Config while i <= #arguments do local argument = arguments[i] - if stringext.hasprefix(argument, "--") then - local name = stringext.removeprefix(argument, "--") + if stringext.hasPrefix(argument, "--") then + local name = stringext.removePrefix(argument, "--") if config.migrationPath == nil then -- Options before the codemod file are parsed as options for the tool itself diff --git a/lute/std/libs/stringext.luau b/lute/std/libs/stringext.luau index ead98abb5..f217dfba9 100644 --- a/lute/std/libs/stringext.luau +++ b/lute/std/libs/stringext.luau @@ -5,7 +5,7 @@ local stringext = {} -function stringext.hasprefix(str: string, prefix: string): boolean +function stringext.hasPrefix(str: string, prefix: string): boolean if #prefix > #str then return false end @@ -13,20 +13,20 @@ function stringext.hasprefix(str: string, prefix: string): boolean return str:sub(1, #prefix) == prefix end -function stringext.removeprefix(str: string, prefix: string): string - if not stringext.hasprefix(str, prefix) then +function stringext.removePrefix(str: string, prefix: string): string + if not stringext.hasPrefix(str, prefix) then return str end return str:sub(#prefix + 1) end -function stringext.hassuffix(str: string, suffix: string): boolean +function stringext.hasSuffix(str: string, suffix: string): boolean return str:sub(-#suffix) == suffix end -function stringext.removesuffix(str: string, suffix: string): string - if not stringext.hassuffix(str, suffix) then +function stringext.removeSuffix(str: string, suffix: string): string + if not stringext.hasSuffix(str, suffix) then return str end diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index 6bfa22b6d..a07d9bef2 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -170,7 +170,7 @@ local function throwsWith(with: any, func: (T...) -> ...unknown, ...: T... if typeof(with) == "string" then -- pcall appends filename and line number of the error, so we check suffix only - if not stringext.hassuffix(actualErrorMessage, with) then + if not stringext.hasSuffix(actualErrorMessage, with) then return assertion(`Expected suffix of error message "{with}", but got "{actualErrorMessage}"`) end else diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau index 2a7152452..bf9bc1455 100644 --- a/tests/std/json.test.luau +++ b/tests/std/json.test.luau @@ -24,7 +24,7 @@ test.suite("JSONParsingTestSuite", function(suite) return json.deserialize(src) end) - if stringext.hasprefix(name, "y_") then + if stringext.hasPrefix(name, "y_") then -- valid JSON: must parse assert.that(ok) @@ -38,7 +38,7 @@ test.suite("JSONParsingTestSuite", function(suite) json.deserialize(_ser) end) assert.that(_ok3) - elseif stringext.hasprefix(name, "n_") then + elseif stringext.hasPrefix(name, "n_") then -- invalid JSON: must fail to parse assert.eq(ok, false) else diff --git a/tests/std/stringext.test.luau b/tests/std/stringext.test.luau index e50798837..e6e0ebc3f 100644 --- a/tests/std/stringext.test.luau +++ b/tests/std/stringext.test.luau @@ -1,38 +1,38 @@ -local stringext = require("@std/stringext") +local stringExt = require("@std/stringext") local test = require("@std/test") test.suite("StringExt", function(suite) - suite:case("hasprefixAndHassuffix", function(assert) - assert.that(stringext.hasprefix("hello", "he")) - assert.eq(stringext.hasprefix("hi", "hello"), false) + suite:case("hasprefixAndHasSuffix", function(assert) + assert.that(stringExt.hasPrefix("hello", "he")) + assert.eq(stringExt.hasPrefix("hi", "hello"), false) - assert.that(stringext.hassuffix("foobar", "bar")) - assert.eq(stringext.hassuffix("foo", "foobar"), false) + assert.that(stringExt.hasSuffix("foobar", "bar")) + assert.eq(stringExt.hasSuffix("foo", "foobar"), false) end) - suite:case("removeprefixAndRemovesuffix", function(assert) - assert.eq(stringext.removeprefix("hello", "he"), "llo") - assert.eq(stringext.removeprefix("hello", "nope"), "hello") + suite:case("removePrefixAndRemovesuffix", function(assert) + assert.eq(stringExt.removePrefix("hello", "he"), "llo") + assert.eq(stringExt.removePrefix("hello", "nope"), "hello") - assert.eq(stringext.removesuffix("foobar", "bar"), "foo") - assert.eq(stringext.removesuffix("foobar", "nope"), "foobar") + assert.eq(stringExt.removeSuffix("foobar", "bar"), "foo") + assert.eq(stringExt.removeSuffix("foobar", "nope"), "foobar") end) suite:case("trim", function(assert) - assert.eq(stringext.trim(" a b "), "a b") - assert.eq(stringext.trim("\n\thi\n"), "hi") - assert.eq(stringext.trim(""), "") - assert.eq(stringext.trim(" "), "") + assert.eq(stringExt.trim(" a b "), "a b") + assert.eq(stringExt.trim("\n\thi\n"), "hi") + assert.eq(stringExt.trim(""), "") + assert.eq(stringExt.trim(" "), "") end) suite:case("count", function(assert) - assert.eq(stringext.count("hello world", "o"), 2) - assert.eq(stringext.count("hello world", "l", 4), 2) -- from position 4 to end - assert.eq(stringext.count("hello world", "l", nil, 5), 2) -- from start to position 5 - assert.eq(stringext.count("hello world", "l", 4, 9), 1) -- from position 4 to position 9 - assert.eq(stringext.count("aaaaaa", "aa"), 3) -- overlapping patterns - assert.eq(stringext.count("no matches here", "z"), 0) - assert.eq(stringext.count("hello world", "%s"), 1) -- patterns supported too - assert.eq(stringext.count("hello world", "[hw]"), 2) -- count h and w + assert.eq(stringExt.count("hello world", "o"), 2) + assert.eq(stringExt.count("hello world", "l", 4), 2) -- from position 4 to end + assert.eq(stringExt.count("hello world", "l", nil, 5), 2) -- from start to position 5 + assert.eq(stringExt.count("hello world", "l", 4, 9), 1) -- from position 4 to position 9 + assert.eq(stringExt.count("aaaaaa", "aa"), 3) -- overlapping patterns + assert.eq(stringExt.count("no matches here", "z"), 0) + assert.eq(stringExt.count("hello world", "%s"), 1) -- patterns supported too + assert.eq(stringExt.count("hello world", "[hw]"), 2) -- count h and w end) end) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 1c32b2763..1fa961499 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -127,7 +127,7 @@ test.suite("Parser", function(suite) if entry.type ~= "file" then continue end - if not strext.hassuffix(entry.name, ".luau") then + if not strext.hasSuffix(entry.name, ".luau") then continue end From db897e80f2159050273b0f7b0ef786d00c3060ae Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 1 Apr 2026 13:19:13 -0700 Subject: [PATCH 439/642] Refactor std/system lib to match conventions (#924) camelCased functions for #919 --- definitions/system.luau | 8 ++++---- examples/system_lib.luau | 8 ++++---- lute/std/libs/system/init.luau | 8 ++++---- lute/system/include/lute/system.h | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/definitions/system.luau b/definitions/system.luau index c7d14f1c9..1d73bfd01 100644 --- a/definitions/system.luau +++ b/definitions/system.luau @@ -13,11 +13,11 @@ export type CpuInfo = { local system = {} -function system.threadcount(): number +function system.threadCount(): number error("unimplemented") end -function system.hostname(): string +function system.hostName(): string error("unimplemented") end @@ -25,11 +25,11 @@ function system.tmpdir(): string error("unimplemented") end -function system.totalmemory(): number +function system.totalMemory(): number error("unimplemented") end -function system.freememory(): number +function system.freeMemory(): number error("unimplemented") end diff --git a/examples/system_lib.luau b/examples/system_lib.luau index 830f071ba..7ee7b8a80 100644 --- a/examples/system_lib.luau +++ b/examples/system_lib.luau @@ -3,13 +3,13 @@ local system = require("@std/system") print( string.format( "%s (%s-%s) - %d cores, uptime for %ss | freemem: %dMB | totalmem: %dMB", - system.hostname(), + system.hostName(), system.os, system.arch, - system.threadcount(), + system.threadCount(), tostring(system.uptime()), - system.freememory(), - system.totalmemory() + system.freeMemory(), + system.totalMemory() ) ) diff --git a/lute/std/libs/system/init.luau b/lute/std/libs/system/init.luau index 4c14c2530..cc42ab308 100644 --- a/lute/std/libs/system/init.luau +++ b/lute/std/libs/system/init.luau @@ -17,10 +17,10 @@ end systemlib.os = system.os systemlib.arch = system.arch -systemlib.threadcount = system.threadcount -systemlib.hostname = system.hostname -systemlib.totalmemory = system.totalmemory -systemlib.freememory = system.freememory +systemlib.threadCount = system.threadCount +systemlib.hostName = system.hostName +systemlib.totalMemory = system.totalMemory +systemlib.freeMemory = system.freeMemory systemlib.uptime = system.uptime systemlib.cpus = system.cpus diff --git a/lute/system/include/lute/system.h b/lute/system/include/lute/system.h index 0bfcde888..4de34df32 100644 --- a/lute/system/include/lute/system.h +++ b/lute/system/include/lute/system.h @@ -26,10 +26,10 @@ int lua_tmpdir(lua_State* L); static const luaL_Reg lib[] = { {"cpus", lua_cpus}, - {"threadcount", lua_threadcount}, - {"freememory", lua_freememory}, - {"totalmemory", lua_totalmemory}, - {"hostname", lua_hostname}, + {"threadCount", lua_threadcount}, + {"freeMemory", lua_freememory}, + {"totalMemory", lua_totalmemory}, + {"hostName", lua_hostname}, {"uptime", lua_uptime}, {"tmpdir", lua_tmpdir}, From db08288e6ebcc0b62b8ee8f3b72418c68f9f20ba Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 1 Apr 2026 13:20:09 -0700 Subject: [PATCH 440/642] Refactors std/syntax library to match conventions (#923) camelCased relevant functions for #919 --- definitions/luau.luau | 2 +- examples/lints/almost_swapped.luau | 2 +- examples/lints/divide_by_zero.luau | 4 +- examples/parsing.luau | 2 +- examples/query.luau | 4 +- examples/query_transformer.luau | 2 +- lute/cli/commands/lint/init.luau | 8 +- .../commands/lint/rules/almost_swapped.luau | 6 +- .../lint/rules/constant_table_comparison.luau | 8 +- .../commands/lint/rules/divide_by_zero.luau | 4 +- .../commands/lint/rules/duplicate_keys.luau | 4 +- .../commands/lint/rules/empty_if_block.luau | 2 +- .../lint/rules/parenthesized_conditions.luau | 14 +- .../commands/lint/rules/unused_variable.luau | 44 +-- lute/cli/commands/transform/init.luau | 2 +- lute/luau/include/lute/luau.h | 2 +- lute/std/libs/syntax/init.luau | 4 +- lute/std/libs/syntax/parser.luau | 6 +- lute/std/libs/syntax/printer.luau | 14 +- lute/std/libs/syntax/query.luau | 26 +- lute/std/libs/syntax/utils/trivia.luau | 8 +- lute/std/libs/syntax/visitor.luau | 12 +- tests/std/syntax/parser.test.luau | 274 +++++++++--------- tests/std/syntax/printer.test.luau | 136 ++++----- tests/std/syntax/query.test.luau | 46 +-- tests/std/syntax/utils.test.luau | 4 +- 26 files changed, 320 insertions(+), 320 deletions(-) diff --git a/definitions/luau.luau b/definitions/luau.luau index 31ee2c6a7..40e140177 100644 --- a/definitions/luau.luau +++ b/definitions/luau.luau @@ -726,7 +726,7 @@ function luau.parse(source: string): ParseResult error("not implemented") end -function luau.parseexpr(source: string): AstExpr +function luau.parseExpr(source: string): AstExpr error("not implemented") end diff --git a/examples/lints/almost_swapped.luau b/examples/lints/almost_swapped.luau index 93e572e69..c7f67d7fe 100644 --- a/examples/lints/almost_swapped.luau +++ b/examples/lints/almost_swapped.luau @@ -62,7 +62,7 @@ end local function lint(ast: syntax.AstStatBlock, sourcepath: path.Path?): { lintTypes.LintViolation } local violations = {} - local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes + local nodes = query.findAllFromRoot(ast, utils.isStatBlock).nodes for _, block in nodes do for i = 1, #block.statements - 1 do diff --git a/examples/lints/divide_by_zero.luau b/examples/lints/divide_by_zero.luau index 3c6586701..0481e1fee 100644 --- a/examples/lints/divide_by_zero.luau +++ b/examples/lints/divide_by_zero.luau @@ -11,14 +11,14 @@ local message = "Division by zero detected." local function lint(ast: syntax.AstStatBlock, sourcepath: path.Path?): { lintTypes.LintViolation } return query - .findallfromroot(ast, utils.isExprBinary) + .findAllFromRoot(ast, utils.isExprBinary) :filter(function(bin) return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" end) :filter(function(bin) return bin.rhsOperand.kind == "expr" and bin.rhsOperand.tag == "number" and bin.rhsOperand.value == 0 end) - :maptoarray( + :mapToArray( -- FIXME(Luau): We would need bidirectional inference of generics -- and return types to remove this annotation. function(n): lintTypes.LintViolation diff --git a/examples/parsing.luau b/examples/parsing.luau index fa6fc6e3a..177a510a3 100644 --- a/examples/parsing.luau +++ b/examples/parsing.luau @@ -2,5 +2,5 @@ local syntax = require("@std/syntax") local pretty = require("@batteries/pp") -local foo = syntax.parseexpr("5") +local foo = syntax.parseExpr("5") print(pretty(foo)) diff --git a/examples/query.luau b/examples/query.luau index 8ca193b8e..06a886cb2 100644 --- a/examples/query.luau +++ b/examples/query.luau @@ -2,9 +2,9 @@ local syntax = require("@std/syntax") local query = require("@std/syntax/query") local utils = require("@std/syntax/utils") -local ast = syntax.parseexpr("require(OldComponent)") +local ast = syntax.parseExpr("require(OldComponent)") -local p = query.findallfromroot(ast, utils.isRequireCall):filter(function(call) +local p = query.findAllFromRoot(ast, utils.isRequireCall):filter(function(call) local firstArg = call.arguments[1].node return firstArg.tag == "indexname" and firstArg.index.text == "OldComponent" end) diff --git a/examples/query_transformer.luau b/examples/query_transformer.luau index 1db469571..4ccdbbf6a 100644 --- a/examples/query_transformer.luau +++ b/examples/query_transformer.luau @@ -4,7 +4,7 @@ local syntax_utils = require("@std/syntax/utils") local function transform(ctx) return query - .findallfromroot(ctx.parseresult.root, syntax_utils.isExprBinary) + .findAllFromRoot(ctx.parseresult.root, syntax_utils.isExprBinary) :filter(function(bin) return bin.operator.text == "~=" and bin.lhsOperand.tag == "local" diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index b0c84b9db..1cab9c58f 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -175,7 +175,7 @@ end local function parseDirectives(node: syntax.AstNode): { [string]: boolean } local directives: { [string]: boolean } = {} - local leadingTrivia = triviaUtils.leftmosttrivia(node) + local leadingTrivia = triviaUtils.leftmostTrivia(node) for _, trivia in leadingTrivia do if trivia.tag == "blockcomment" or trivia.tag == "comment" then for rule in trivia.text:gmatch("lute%-lint%-ignore%((.+)%)") do @@ -254,7 +254,7 @@ local function violationIsSuppressed( local suppressionVisitor = visitor.create(nodeIsSuppressed) - visitor.visitblock(ast, suppressionVisitor) + visitor.visitBlock(ast, suppressionVisitor) return violationSuppressed end @@ -282,12 +282,12 @@ local function lintString( if VERBOSE then print(`Parsing input {if inputFilePath then `file '{inputFilePath}'` else "string"}`) end - local ast = syntax.parseblock(source) + local ast = syntax.parseBlock(source) if VERBOSE then print(`Parsing global lint ignores in input {if inputFilePath then `file '{inputFilePath}'` else "string"}`) end - local globalIgnores = parseGlobalIgnores(triviaUtils.leftmosttrivia(ast)) + local globalIgnores = parseGlobalIgnores(triviaUtils.leftmostTrivia(ast)) if VERBOSE then print("Applying lint rules") diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index 1ea28152f..ec8212ae1 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -67,7 +67,7 @@ local function lint( ): { lintTypes.LintViolation } local violations = {} - local nodes = query.findallfromroot(ast, utils.isStatBlock).nodes + local nodes = query.findAllFromRoot(ast, utils.isStatBlock).nodes for _, block in nodes do for i = 1, #block.statements - 1 do @@ -85,8 +85,8 @@ local function lint( local nextVar, nextVal = nextStat.variables[1].node, nextStat.values[1].node if compFuncs.refExprsSame(currVar, nextVal) and compFuncs.refExprsSame(nextVar, currVal) then - local currVarStr = stringext.trim(printer.printnode(currVar)) - local currValStr = stringext.trim(printer.printnode(currVal)) + local currVarStr = stringext.trim(printer.printNode(currVar)) + local currValStr = stringext.trim(printer.printNode(currVal)) local suggestedFix = `{currVarStr}, {currValStr} = {currValStr}, {currVarStr}` table.insert(violations, { -- LUAUFIX: inserted table isn't inferred as a LintViolation diff --git a/lute/cli/commands/lint/rules/constant_table_comparison.luau b/lute/cli/commands/lint/rules/constant_table_comparison.luau index 9b0482c4d..9625f21f3 100644 --- a/lute/cli/commands/lint/rules/constant_table_comparison.luau +++ b/lute/cli/commands/lint/rules/constant_table_comparison.luau @@ -35,14 +35,14 @@ local function lint( _context: lintTypes.RuleContext ): { lintTypes.LintViolation } return query - .findallfromroot(ast, utils.isExprBinary) + .findAllFromRoot(ast, utils.isExprBinary) :filter(function(bin) return bin.operator.text == "==" or bin.operator.text == "~=" end) :filter(function(bin) return isTableLiteral(bin.rhsOperand) or isTableLiteral(bin.lhsOperand) end) - :maptoarray( + :mapToArray( function( bin: syntax.AstExprBinary ): lintTypes.LintViolation -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation @@ -50,10 +50,10 @@ local function lint( if isEmptyTableLiteral(bin.lhsOperand) then suggestedFix = - `next({stringext.trim(syntaxPrinter.printnode(bin.rhsOperand))}) {bin.operator.text} nil` + `next({stringext.trim(syntaxPrinter.printNode(bin.rhsOperand))}) {bin.operator.text} nil` elseif isEmptyTableLiteral(bin.rhsOperand) then suggestedFix = - `next({stringext.trim(syntaxPrinter.printnode(bin.lhsOperand))}) {bin.operator.text} nil` + `next({stringext.trim(syntaxPrinter.printNode(bin.lhsOperand))}) {bin.operator.text} nil` end return { diff --git a/lute/cli/commands/lint/rules/divide_by_zero.luau b/lute/cli/commands/lint/rules/divide_by_zero.luau index 1fb08fdc5..cbf5e1bdf 100644 --- a/lute/cli/commands/lint/rules/divide_by_zero.luau +++ b/lute/cli/commands/lint/rules/divide_by_zero.luau @@ -18,14 +18,14 @@ local function lint( _context: lintTypes.RuleContext ): { lintTypes.LintViolation } return query - .findallfromroot(ast, utils.isExprBinary) + .findAllFromRoot(ast, utils.isExprBinary) :filter(function(bin) return bin.operator.text == "/" or bin.operator.text == "//" or bin.operator.text == "%" end) :filter(function(bin) return isZeroLiteral(bin.rhsOperand) end) - :maptoarray( + :mapToArray( function( n: syntax.AstExprBinary ): lintTypes.LintViolation? -- LUAUFIX: Bidirectional inference of generics should let us not need this annotation diff --git a/lute/cli/commands/lint/rules/duplicate_keys.luau b/lute/cli/commands/lint/rules/duplicate_keys.luau index a83f43ea7..e4041fd6e 100644 --- a/lute/cli/commands/lint/rules/duplicate_keys.luau +++ b/lute/cli/commands/lint/rules/duplicate_keys.luau @@ -25,8 +25,8 @@ local function lint( end return query - .findallfromroot(ast, utils.isExprTable) - :flatmap(function(tableExpr: syntax.AstExprTable): { lintTypes.LintViolation } + .findAllFromRoot(ast, utils.isExprTable) + :flatMap(function(tableExpr: syntax.AstExprTable): { lintTypes.LintViolation } local violations = {} local seenKeys: { [string | number]: true } = {} diff --git a/lute/cli/commands/lint/rules/empty_if_block.luau b/lute/cli/commands/lint/rules/empty_if_block.luau index 6cbe0681a..fc4709587 100644 --- a/lute/cli/commands/lint/rules/empty_if_block.luau +++ b/lute/cli/commands/lint/rules/empty_if_block.luau @@ -53,7 +53,7 @@ local function lint( end -- Find all if statements - return query.findallfromroot(ast, utils.isStatIf):map(function(ifStat): lintTypes.LintViolation? + return query.findAllFromRoot(ast, utils.isStatIf):map(function(ifStat): lintTypes.LintViolation? -- Check if the then block is empty if isBlockEmpty( diff --git a/lute/cli/commands/lint/rules/parenthesized_conditions.luau b/lute/cli/commands/lint/rules/parenthesized_conditions.luau index d1322b437..acd5dbccc 100644 --- a/lute/cli/commands/lint/rules/parenthesized_conditions.luau +++ b/lute/cli/commands/lint/rules/parenthesized_conditions.luau @@ -16,13 +16,13 @@ local function lint( local violations: { lintTypes.LintViolation } = {} query - .findallfromroot(ast, function(node: syntax.AstNode): (syntax.AstExprIfElse | syntax.AstStatIf)? + .findAllFromRoot(ast, function(node: syntax.AstNode): (syntax.AstExprIfElse | syntax.AstStatIf)? if (node.kind == "expr" or node.kind == "stat") and node.tag == "conditional" then return node end return nil end) - :foreach(function(ifStat) + :forEach(function(ifStat) if ifStat.condition.tag == "group" then table.insert(violations, { lintname = name, @@ -31,7 +31,7 @@ local function lint( severity = "info", sourcepath = sourcepath, suggestedfix = { - fix = syntaxPrinter.printnode(ifStat.condition.expression), + fix = syntaxPrinter.printNode(ifStat.condition.expression), }, target = target, }) @@ -46,7 +46,7 @@ local function lint( severity = "info", sourcepath = sourcepath, suggestedfix = { - fix = syntaxPrinter.printnode(elseIfStat.condition.expression), + fix = syntaxPrinter.printNode(elseIfStat.condition.expression), }, target = target, }) @@ -55,7 +55,7 @@ local function lint( end) query - .findallfromroot(ast, function(node: syntax.AstNode): (syntax.AstStatWhile | syntax.AstStatRepeat)? + .findAllFromRoot(ast, function(node: syntax.AstNode): (syntax.AstStatWhile | syntax.AstStatRepeat)? if node.kind == "stat" and (node.tag == "while" or node.tag == "repeat") then return node end @@ -64,7 +64,7 @@ local function lint( :filter(function(n) return n.condition.tag == "group" end) - :foreach(function(n) + :forEach(function(n) table.insert(violations, { lintname = name, location = n.condition.location, @@ -72,7 +72,7 @@ local function lint( severity = "info", sourcepath = sourcepath, suggestedfix = { - fix = syntaxPrinter.printnode((n.condition :: syntax.AstExprGroup).expression), + fix = syntaxPrinter.printNode((n.condition :: syntax.AstExprGroup).expression), }, target = target, }) diff --git a/lute/cli/commands/lint/rules/unused_variable.luau b/lute/cli/commands/lint/rules/unused_variable.luau index dc72d6b93..cb590d325 100644 --- a/lute/cli/commands/lint/rules/unused_variable.luau +++ b/lute/cli/commands/lint/rules/unused_variable.luau @@ -113,9 +113,9 @@ local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie else visitor.ignoreReferences[localVar.node] = true if localVar.node.annotation then - visitorLib.visittype(localVar.node.annotation, visitor) + visitorLib.visitType(localVar.node.annotation, visitor) end - visitorLib.visitexpression(stat.values[i].node, visitor) + visitorLib.visitExpression(stat.values[i].node, visitor) visitor.ignoreReferences[localVar.node] = nil end end @@ -163,15 +163,15 @@ local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie if var.tag == "local" and not callFound then visitor.ignoreReferences[var["local"]] = true - visitorLib.visitexpression(val, visitor) + visitorLib.visitExpression(val, visitor) visitor.ignoreReferences[var["local"]] = nil else visitor.inLValueContext = true - visitorLib.visitexpression(var, visitor) + visitorLib.visitExpression(var, visitor) visitor.inLValueContext = prevInLValueContext - visitorLib.visitexpression(val, visitor) + visitorLib.visitExpression(val, visitor) end if val.tag == "call" then @@ -193,15 +193,15 @@ local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie -- x += x + 1 if stat.variable.tag == "local" then visitor.ignoreReferences[stat.variable["local"]] = true - visitorLib.visitexpression(stat.value, visitor) + visitorLib.visitExpression(stat.value, visitor) visitor.ignoreReferences[stat.variable["local"]] = nil else local prevInLValueContext = visitor.inLValueContext visitor.inLValueContext = true - visitorLib.visitexpression(stat.variable, visitor) + visitorLib.visitExpression(stat.variable, visitor) visitor.inLValueContext = prevInLValueContext - visitorLib.visitexpression(stat.value, visitor) + visitorLib.visitExpression(stat.value, visitor) end return false @@ -216,24 +216,24 @@ local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie end visitor.ignoreReferences[stat.name["local"]] = true - visitorLib.visitexpression(stat.func, visitor) + visitorLib.visitExpression(stat.func, visitor) visitor.ignoreReferences[stat.name["local"]] = nil elseif stat.name.tag == "indexname" and stat.name.accessor.text == ":" then local prevMethodContext = visitor.insideMethodContext visitor.insideMethodContext = true - visitorLib.visitexpression(stat.func, visitor) + visitorLib.visitExpression(stat.func, visitor) visitor.insideMethodContext = prevMethodContext elseif stat.name.tag == "index" then local prevInLValueContext = visitor.inLValueContext visitor.inLValueContext = true - visitorLib.visitexpression(stat.name, visitor) + visitorLib.visitExpression(stat.name, visitor) visitor.inLValueContext = prevInLValueContext - visitorLib.visitexpression(stat.func, visitor) + visitorLib.visitExpression(stat.func, visitor) else - visitorLib.visitexpression(stat.func, visitor) + visitorLib.visitExpression(stat.func, visitor) end return false @@ -243,7 +243,7 @@ local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie -- Recursive functions don't count as references to themselves visitor.ignoreReferences[stat.name] = true - visitorLib.visitexpression(stat.func, visitor) + visitorLib.visitExpression(stat.func, visitor) visitor.ignoreReferences[stat.name] = nil @@ -258,11 +258,11 @@ local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie -- Manually visit the block's states so we can keep the scope around for _, bodyStat in stat.body.statements do - visitorLib.visitstatement(bodyStat, visitor) + visitorLib.visitStatement(bodyStat, visitor) end -- Visit the condition with the same scope as the body - visitorLib.visitexpression(stat.condition, visitor) + visitorLib.visitExpression(stat.condition, visitor) popAndSaveUnusedLocals(stat) @@ -323,11 +323,11 @@ local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie end function visitor.visitExprIndexExpr(node: syntax.AstExprIndexExpr) - visitorLib.visitexpression(node.expression, visitor) + visitorLib.visitExpression(node.expression, visitor) local prevInLValueContext = visitor.inLValueContext visitor.inLValueContext = false - visitorLib.visitexpression(node.index, visitor) + visitorLib.visitExpression(node.index, visitor) visitor.inLValueContext = prevInLValueContext return false @@ -340,23 +340,23 @@ local function unusedLocals(block: syntax.AstStatBlock, writeOnlyAPIs: smallTrie return true end - visitorLib.visitexpression(node.func, visitor) + visitorLib.visitExpression(node.func, visitor) for i, arg in node.arguments do if writeOnlyIndices[i] then local prevInLValueContext = visitor.inLValueContext visitor.inLValueContext = true - visitorLib.visitexpression(arg.node, visitor) + visitorLib.visitExpression(arg.node, visitor) visitor.inLValueContext = prevInLValueContext else - visitorLib.visitexpression(arg.node, visitor) + visitorLib.visitExpression(arg.node, visitor) end end return false end - visitorLib.visitblock(block, visitor) + visitorLib.visitBlock(block, visitor) return visitor.unusedLocals end diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index 554a2401a..a15b56ef2 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -115,7 +115,7 @@ local function applyMigration( -- TODO: should we wrap in pcall? For now we don't do this to preserve stack trace local result = migration.transform(ctx) - local toWrite = if typeof(result) == "string" then result else printer.printfile(parseresult, result) + local toWrite = if typeof(result) == "string" then result else printer.printFile(parseresult, result) if toWrite == types.DELETION_MARKER then if outputFilePath then diff --git a/lute/luau/include/lute/luau.h b/lute/luau/include/lute/luau.h index 8e81d7fb6..557d0ae28 100644 --- a/lute/luau/include/lute/luau.h +++ b/lute/luau/include/lute/luau.h @@ -29,7 +29,7 @@ int typeofModule_luau(lua_State* L); static const luaL_Reg lib[] = { {"parse", luau_parse}, - {"parseexpr", luau_parseexpr}, + {"parseExpr", luau_parseexpr}, {"compile", compile_luau}, {"load", load_luau}, {"resolveModule", resolveModule_luau}, diff --git a/lute/std/libs/syntax/init.luau b/lute/std/libs/syntax/init.luau index 707fcc158..76fd2ad98 100644 --- a/lute/std/libs/syntax/init.luau +++ b/lute/std/libs/syntax/init.luau @@ -180,8 +180,8 @@ export type AstNode = types.AstNode export type ParseResult = types.ParseResult return table.freeze({ - parseblock = parser.parseblock, - parseexpr = parser.parseexpr, + parseBlock = parser.parseBlock, + parseExpr = parser.parseExpr, parse = parser.parse, span = table.freeze(span), }) diff --git a/lute/std/libs/syntax/parser.luau b/lute/std/libs/syntax/parser.luau index 713383574..4243f7610 100644 --- a/lute/std/libs/syntax/parser.luau +++ b/lute/std/libs/syntax/parser.luau @@ -8,12 +8,12 @@ local types = require("./types") local parser = {} --- Parses Luau source code into an AstStatBlock -function parser.parseblock(source: string): types.AstStatBlock +function parser.parseBlock(source: string): types.AstStatBlock return luau.parse(source).root end -function parser.parseexpr(source: string): types.AstExpr - return luau.parseexpr(source) +function parser.parseExpr(source: string): types.AstExpr + return luau.parseExpr(source) end function parser.parse(source: string): types.ParseResult diff --git a/lute/std/libs/syntax/printer.luau b/lute/std/libs/syntax/printer.luau index 9815b8e87..823c0b0ee 100644 --- a/lute/std/libs/syntax/printer.luau +++ b/lute/std/libs/syntax/printer.luau @@ -93,13 +93,13 @@ local function printInterpolatedString(self: PrintVisitor, expr: types.AstExprIn else self:write("{") self:printTriviaList(expr.strings[i].trailingTrivia) - visitor.visitexpression(expr.expressions[i], self) + visitor.visitExpression(expr.expressions[i], self) end end end local function printReplacement(self: PrintVisitor, node: types.AstNode, replacement: string) - local leftmostTrivia = triviaUtils.leftmosttrivia(node) + local leftmostTrivia = triviaUtils.leftmostTrivia(node) if leftmostTrivia ~= nil then self:printTriviaList(leftmostTrivia) end @@ -107,7 +107,7 @@ local function printReplacement(self: PrintVisitor, node: types.AstNode, replace assert(typeof(replacement) == "string", "Unsupported replacement type") self:write(replacement) - local rightmostTrivia = triviaUtils.rightmosttrivia(node) + local rightmostTrivia = triviaUtils.rightmostTrivia(node) if rightmostTrivia ~= nil then self:printTriviaList(rightmostTrivia) end @@ -182,16 +182,16 @@ local function printVisitor(replacements: types.Replacements?) return printer end -function printerLib.printnode(node: types.AstNode, replacements: types.Replacements?): string +function printerLib.printNode(node: types.AstNode, replacements: types.Replacements?): string local printer = printVisitor(replacements) visitor.visit(node, printer) return buffer.readstring(printer.result, 0, printer.cursor) end -function printerLib.printfile(result: types.ParseResult, replacements: types.Replacements?): string +function printerLib.printFile(result: types.ParseResult, replacements: types.Replacements?): string local printer = printVisitor(replacements) - visitor.visitblock(result.root, printer) - visitor.visittoken(result.eof, printer) + visitor.visitBlock(result.root, printer) + visitor.visitToken(result.eof, printer) return buffer.readstring(printer.result, 0, printer.cursor) end diff --git a/lute/std/libs/syntax/query.luau b/lute/std/libs/syntax/query.luau index 7967fe738..39cbac043 100644 --- a/lute/std/libs/syntax/query.luau +++ b/lute/std/libs/syntax/query.luau @@ -13,10 +13,10 @@ export type Query = { filter: (self: Query, pred: (T) -> boolean) -> Query, replace: (self: Query, repl: (T) -> string?) -> types.Replacements, map: (self: Query, fn: (T) -> U?) -> Query, -- recursion violation reported - foreach: (self: Query, callback: (T) -> ()) -> Query, - findall: (self: Query, fn: (Node) -> U?) -> Query, - flatmap: (self: Query, fn: (T) -> { U }) -> Query, - maptoarray: (self: Query, fn: (T) -> U?) -> { U }, + forEach: (self: Query, callback: (T) -> ()) -> Query, + findAll: (self: Query, fn: (Node) -> U?) -> Query, + flatMap: (self: Query, fn: (T) -> { U }) -> Query, + mapToArray: (self: Query, fn: (T) -> U?) -> { U }, } local queryLib = {} @@ -56,7 +56,7 @@ function queryLib.map(self: Query, fn: (T) -> U?): Query return self end -function queryLib.foreach(self: Query, callback: (T) -> ()): Query +function queryLib.forEach(self: Query, callback: (T) -> ()): Query for _, node in self.nodes do callback(node) end @@ -83,7 +83,7 @@ local function newSelectVisitor(nodes: { T }, fn: (Node) -> T?): visitor.Visi return selectVisitor end -function queryLib.findall(self: Query, fn: (Node) -> U?): Query +function queryLib.findAll(self: Query, fn: (Node) -> U?): Query local selectedNodes: { U } = {} local selectVisitor = newSelectVisitor(selectedNodes, fn) @@ -97,7 +97,7 @@ function queryLib.findall(self: Query, fn: (Node) -> U?): Query return self end -function queryLib.flatmap(self: Query, fn: (T) -> { U }): Query +function queryLib.flatMap(self: Query, fn: (T) -> { U }): Query local newNodes: { U } = {} for _, node in self.nodes do local mapped = fn(node) @@ -109,7 +109,7 @@ function queryLib.flatmap(self: Query, fn: (T) -> { U }): Query return self end -function queryLib.maptoarray(self: Query, fn: (T) -> U?): { U } +function queryLib.mapToArray(self: Query, fn: (T) -> U?): { U } local result = {} for _, node in self.nodes do local mapped = fn(node) @@ -120,7 +120,7 @@ function queryLib.maptoarray(self: Query, fn: (T) -> U?): { U } return result end -function queryLib.findallfromroot(ast: types.ParseResult | Node, fn: (Node) -> T?): Query +function queryLib.findAllFromRoot(ast: types.ParseResult | Node, fn: (Node) -> T?): Query local nodes: { T } = {} local selectVisitor = newSelectVisitor(nodes, fn) @@ -133,10 +133,10 @@ function queryLib.findallfromroot(ast: types.ParseResult | Node, fn: (Node) - filter = queryLib.filter, replace = queryLib.replace, map = queryLib.map :: any, -- LUAUFIX: Remove any cast once recursion restriction is loosened - foreach = queryLib.foreach, - findall = queryLib.findall :: any, -- LUAUFIX: Remove any cast once recursion restriction is loosened - flatmap = queryLib.flatmap, - maptoarray = queryLib.maptoarray :: any, -- Any cast because maptoarray has generics quantified at a different level than the query type expects + forEach = queryLib.forEach, + findAll = queryLib.findAll :: any, -- LUAUFIX: Remove any cast once recursion restriction is loosened + flatMap = queryLib.flatMap, + mapToArray = queryLib.mapToArray :: any, -- Any cast because mapToArray has generics quantified at a different level than the query type expects } end diff --git a/lute/std/libs/syntax/utils/trivia.luau b/lute/std/libs/syntax/utils/trivia.luau index ff75ed41a..93870f1f2 100644 --- a/lute/std/libs/syntax/utils/trivia.luau +++ b/lute/std/libs/syntax/utils/trivia.luau @@ -3,8 +3,8 @@ local types = require("../types") local retrieverLib = {} -function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } - local q = query.findallfromroot<>( +function retrieverLib.leftmostTrivia(n: types.AstNode): { types.Trivia } + local q = query.findAllFromRoot<>( n :: any, function(n: types.AstNode) -- LUAUFIX: Code too complex emitted because AstNode is very large return if n.kind == "token" then n else nil @@ -29,8 +29,8 @@ function retrieverLib.leftmosttrivia(n: types.AstNode): { types.Trivia } return if leftmostToken ~= nil then leftmostToken.leadingTrivia else {} end -function retrieverLib.rightmosttrivia(n: types.AstNode): { types.Trivia } - local q = query.findallfromroot( +function retrieverLib.rightmostTrivia(n: types.AstNode): { types.Trivia } + local q = query.findAllFromRoot( n :: any, function(n: types.AstNode) -- LUAUFIX: Code too complex emitted because AstNode is very large return if n.kind == "token" then n else nil diff --git a/lute/std/libs/syntax/visitor.luau b/lute/std/libs/syntax/visitor.luau index 2dab1f217..625c11daf 100644 --- a/lute/std/libs/syntax/visitor.luau +++ b/lute/std/libs/syntax/visitor.luau @@ -1078,12 +1078,12 @@ local function visit(node: types.AstNode, visitor: Visitor) end visitorlib.create = create -visitorlib.visitblock = visitStatBlock -visitorlib.visitstatement = visitStatement -visitorlib.visitexpression = visitExpr -visitorlib.visittype = visitType -visitorlib.visittypepack = visitTypePack -visitorlib.visittoken = visitToken +visitorlib.visitBlock = visitStatBlock +visitorlib.visitStatement = visitStatement +visitorlib.visitExpression = visitExpr +visitorlib.visitType = visitType +visitorlib.visitTypePack = visitTypePack +visitorlib.visitToken = visitToken visitorlib.visit = visit return table.freeze(visitorlib) diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 1fa961499..83973d49d 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -23,7 +23,7 @@ end test.suite("Parser", function(suite) suite:case("tokenContainsLeadingSpaces", function(assert) - local block = parser.parseblock(" local x = 1") + local block = parser.parseBlock(" local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -37,7 +37,7 @@ test.suite("Parser", function(suite) end) suite:case("tokenContainsLeadingNewline", function(assert) - local block = parser.parseblock("\n" .. "local x = 1") + local block = parser.parseBlock("\n" .. "local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -51,7 +51,7 @@ test.suite("Parser", function(suite) end) suite:case("tokenContainsLeadingSingleLineComment", function(assert) - local block = parser.parseblock("-- comment\n" .. "local x = 1") + local block = parser.parseBlock("-- comment\n" .. "local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -68,7 +68,7 @@ test.suite("Parser", function(suite) end) suite:case("tokenContainsLeadingBlockComment", function(assert) - local block = parser.parseblock("--[[ comment ]] local x = 1") + local block = parser.parseBlock("--[[ comment ]] local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -85,7 +85,7 @@ test.suite("Parser", function(suite) end) suite:case("tokenizeWhitespace", function(assert) - local block = parser.parseblock(" \n\t\t\n\n" .. "local x = 1") + local block = parser.parseBlock(" \n\t\t\n\n" .. "local x = 1") assert.eq(#block.statements, 1) local l = block.statements[1] @@ -99,7 +99,7 @@ test.suite("Parser", function(suite) end) suite:case("triviaSplitBetweenLeadingAndTrailing", function(assert) - local block = parser.parseblock("local x = 'test' -- comment\n" .. "-- comment 2\nlocal y = 'value'") + local block = parser.parseBlock("local x = 'test' -- comment\n" .. "-- comment 2\nlocal y = 'value'") assert.eq(#block.statements, 2) local firstStmt = block.statements[1] @@ -134,7 +134,7 @@ test.suite("Parser", function(suite) suite:case(`roundtrippable_Ast_{entry.name}`, function(assert) local p = path.join(directory, entry.name) local source = fs.readfiletostring(path.format(p)) - local result = printer.printfile(parser.parse(source)) + local result = printer.printFile(parser.parse(source)) assert.eq(source, result) end) @@ -184,7 +184,7 @@ test.suite("Parser", function(suite) } for _, subcase in subcases do - local block = parser.parseblock(subcase.code) + local block = parser.parseBlock(subcase.code) assert.eq(#block.statements, 1) local l = block.statements[1] @@ -198,7 +198,7 @@ end) test.suite("parseExpr", function(suite) suite:case("parseExprGroup", function(assert) - local groupExpr = parser.parseexpr("(x)") + local groupExpr = parser.parseExpr("(x)") assert.eq(groupExpr.tag, "group") assert.eq(groupExpr.kind, "expr") local expr = groupExpr :: syntax.AstExprGroup @@ -208,20 +208,20 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprNil", function(assert) - local expr = parser.parseexpr("nil") + local expr = parser.parseExpr("nil") assert.eq(expr.tag, "nil") assert.eq(expr.kind, "expr") assert.eq((expr :: syntax.AstExprConstantNil).token.text, "nil") end) suite:case("parseExprBoolean", function(assert) - local trueExpr = parser.parseexpr("true") + local trueExpr = parser.parseExpr("true") assert.eq(trueExpr.tag, "boolean") assert.eq(trueExpr.kind, "expr") assert.eq((trueExpr :: syntax.AstExprConstantBool).token.text, "true") assert.that((trueExpr :: syntax.AstExprConstantBool).value) - local falseExpr = parser.parseexpr("false") + local falseExpr = parser.parseExpr("false") assert.eq(falseExpr.tag, "boolean") assert.eq(falseExpr.kind, "expr") assert.eq((falseExpr :: syntax.AstExprConstantBool).token.text, "false") @@ -229,13 +229,13 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprNumber", function(assert) - local numberExpr = parser.parseexpr("42") + local numberExpr = parser.parseExpr("42") assert.eq(numberExpr.tag, "number") assert.eq(numberExpr.kind, "expr") assert.eq((numberExpr :: syntax.AstExprConstantNumber).token.text, "42") assert.eq((numberExpr :: syntax.AstExprConstantNumber).value, 42) - local floatExpr = parser.parseexpr("3.14") + local floatExpr = parser.parseExpr("3.14") assert.eq(floatExpr.tag, "number") assert.eq(floatExpr.kind, "expr") assert.eq((floatExpr :: syntax.AstExprConstantNumber).token.text, "3.14") @@ -243,25 +243,25 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprString", function(assert) - local singleQuoteExpr = parser.parseexpr("'hello'") + local singleQuoteExpr = parser.parseExpr("'hello'") assert.eq(singleQuoteExpr.tag, "string") assert.eq(singleQuoteExpr.kind, "expr") assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).token.text, "hello") assert.eq((singleQuoteExpr :: syntax.AstExprConstantString).quoteStyle, "single") - local doubleQuoteExpr = parser.parseexpr('"world"') + local doubleQuoteExpr = parser.parseExpr('"world"') assert.eq(doubleQuoteExpr.tag, "string") assert.eq(doubleQuoteExpr.kind, "expr") assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).token.text, "world") assert.eq((doubleQuoteExpr :: syntax.AstExprConstantString).quoteStyle, "double") - local blockQuoteExpr = parser.parseexpr("[[world]]") + local blockQuoteExpr = parser.parseExpr("[[world]]") assert.eq(blockQuoteExpr.tag, "string") assert.eq(blockQuoteExpr.kind, "expr") assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).token.text, "world") assert.eq((blockQuoteExpr :: syntax.AstExprConstantString).quoteStyle, "block") - local interpExpr = parser.parseexpr("`foobar`") + local interpExpr = parser.parseExpr("`foobar`") assert.eq(interpExpr.tag, "string") assert.eq(interpExpr.kind, "expr") assert.eq((interpExpr :: syntax.AstExprConstantString).token.text, "foobar") @@ -269,28 +269,28 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprLocal", function(assert) - local localExpr = parser.parseexpr("x") + local localExpr = parser.parseExpr("x") assert.eq(localExpr.tag, "global") assert.eq(localExpr.kind, "expr") assert.eq((localExpr :: syntax.AstExprGlobal).name.text, "x") end) suite:case("parseExprGlobal", function(assert) - local globalExpr = parser.parseexpr("_G") + local globalExpr = parser.parseExpr("_G") assert.eq(globalExpr.tag, "global") assert.eq(globalExpr.kind, "expr") assert.eq((globalExpr :: syntax.AstExprGlobal).name.text, "_G") end) suite:case("parseExprVarargs", function(assert) - local varargsExpr = parser.parseexpr("...") + local varargsExpr = parser.parseExpr("...") assert.eq(varargsExpr.tag, "vararg") assert.eq(varargsExpr.kind, "expr") assert.eq((varargsExpr :: syntax.AstExprVarargs).token.text, "...") end) suite:case("parseExprCall", function(assert) - local callExpr = parser.parseexpr("foo()") + local callExpr = parser.parseExpr("foo()") assert.eq(callExpr.tag, "call") assert.eq(callExpr.kind, "expr") assert.eq((callExpr :: syntax.AstExprCall).func.tag, "global") @@ -298,7 +298,7 @@ test.suite("parseExpr", function(suite) assert.eq((callExpr :: syntax.AstExprCall).self, false) assert.eq(#(callExpr :: syntax.AstExprCall).arguments, 0) - local callWithArgsExpr = parser.parseexpr("bar(1, 2)") + local callWithArgsExpr = parser.parseExpr("bar(1, 2)") assert.eq(callWithArgsExpr.tag, "call") assert.eq(callWithArgsExpr.kind, "expr") assert.eq(#(callWithArgsExpr :: syntax.AstExprCall).arguments, 2) @@ -307,7 +307,7 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprInstantiate", function(assert) - local callExpr = parser.parseexpr("foo<>()") + local callExpr = parser.parseExpr("foo<>()") assert.eq(callExpr.kind, "expr") assert.eq(callExpr.tag, "call") @@ -327,7 +327,7 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprIndexName", function(assert) - local indexExpr = parser.parseexpr("obj.field") + local indexExpr = parser.parseExpr("obj.field") assert.eq(indexExpr.tag, "indexname") assert.eq(indexExpr.kind, "expr") assert.eq((indexExpr :: syntax.AstExprIndexName).expression.tag, "global") @@ -336,7 +336,7 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprIndexExpr", function(assert) - local indexExpr = parser.parseexpr("arr[1]") + local indexExpr = parser.parseExpr("arr[1]") assert.eq(indexExpr.tag, "index") assert.eq(indexExpr.kind, "expr") assert.eq((indexExpr :: syntax.AstExprIndexExpr).expression.tag, "global") @@ -346,7 +346,7 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprFunction", function(assert) - local anonFuncExpr = parser.parseexpr("function(a, b) return a + b end") + local anonFuncExpr = parser.parseExpr("function(a, b) return a + b end") assert.eq(anonFuncExpr.tag, "function") assert.eq(anonFuncExpr.kind, "expr") @@ -370,7 +370,7 @@ test.suite("parseExpr", function(suite) assert.eq(funcExpr.returnAnnotation, nil) assert.eq(funcExpr.endKeyword.text, "end") - anonFuncExpr = parser.parseexpr("function(a: A, ...: B...): A return a end") + anonFuncExpr = parser.parseExpr("function(a: A, ...: B...): A return a end") assert.eq(anonFuncExpr.tag, "function") assert.eq(anonFuncExpr.kind, "expr") @@ -423,7 +423,7 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprTable", function(assert) - local emptyTableExpr = parser.parseexpr("{}") + local emptyTableExpr = parser.parseExpr("{}") assert.eq(emptyTableExpr.tag, "table") assert.eq(emptyTableExpr.kind, "expr") local tableExpr = emptyTableExpr :: syntax.AstExprTable @@ -431,7 +431,7 @@ test.suite("parseExpr", function(suite) assert.eq(tableExpr.closeBrace.text, "}") assert.eq(#tableExpr.entries, 0) - local mixedTable = parser.parseexpr("{1, foo = bar; [2] = baz}") + local mixedTable = parser.parseExpr("{1, foo = bar; [2] = baz}") assert.eq(mixedTable.tag, "table") assert.eq(mixedTable.kind, "expr") local mixedTableExpr = mixedTable :: syntax.AstExprTable @@ -462,21 +462,21 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprUnary", function(assert) - local notExpr = parser.parseexpr("not x") + local notExpr = parser.parseExpr("not x") assert.eq(notExpr.tag, "unary") assert.eq(notExpr.kind, "expr") local expr = notExpr :: syntax.AstExprUnary assert.eq(expr.operator.text, "not") assert.eq(expr.operand.tag, "global") - local negExpr = parser.parseexpr("-42") + local negExpr = parser.parseExpr("-42") assert.eq(negExpr.tag, "unary") assert.eq(negExpr.kind, "expr") expr = negExpr :: syntax.AstExprUnary assert.eq(expr.operator.text, "-") assert.eq(expr.operand.tag, "number") - local lenExpr = parser.parseexpr("#str") + local lenExpr = parser.parseExpr("#str") assert.eq(lenExpr.tag, "unary") assert.eq(lenExpr.kind, "expr") expr = lenExpr :: syntax.AstExprUnary @@ -485,7 +485,7 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprBinary", function(assert) - local addExpr = parser.parseexpr("1 + 2") + local addExpr = parser.parseExpr("1 + 2") assert.eq(addExpr.tag, "binary") assert.eq(addExpr.kind, "expr") local expr = addExpr :: syntax.AstExprBinary @@ -495,7 +495,7 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprInterpString", function(assert) - local interpExpr = parser.parseexpr("`Hello, {name}! Today is {dayOfWeek}.`") + local interpExpr = parser.parseExpr("`Hello, {name}! Today is {dayOfWeek}.`") assert.eq(interpExpr.tag, "interpolatedstring") assert.eq(interpExpr.kind, "expr") local expr = interpExpr :: syntax.AstExprInterpString @@ -511,7 +511,7 @@ test.suite("parseExpr", function(suite) end) suite:case("parseExprTypeAssertion", function(assert) - local castExpr = parser.parseexpr("x :: string") + local castExpr = parser.parseExpr("x :: string") assert.eq(castExpr.tag, "cast") assert.eq(castExpr.kind, "expr") local expr = castExpr :: syntax.AstExprTypeAssertion @@ -522,7 +522,7 @@ test.suite("parseExpr", function(suite) -- table items serialized with location field suite:case("exprTableItemSerializedWithLocationField", function(assert) - local tableExpr = parser.parseexpr([[ + local tableExpr = parser.parseExpr([[ { 1, foo = bar, @@ -555,13 +555,13 @@ end test.suite("parse", function(suite) suite:case("parseEmptyProgram", function(assert) - local block = parser.parseblock("") + local block = parser.parseBlock("") checkIsBlock(block, assert) assert.eq(#block.statements, 0) end) suite:case("parseDoStatement", function(assert) - local block = parser.parseblock("do print('Hello, World!') end") + local block = parser.parseBlock("do print('Hello, World!') end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -574,7 +574,7 @@ test.suite("parse", function(suite) end) suite:case("parseIfStatement", function(assert) - local block = parser.parseblock( + local block = parser.parseBlock( "if x > 0 then print('positive') elseif x < 0 then print('negative') else print('zero') end" ) assert.eq(#block.statements, 1) @@ -602,7 +602,7 @@ test.suite("parse", function(suite) checkIsBlock(ifStat.elseBlock, assert) assert.eq(ifStat.endKeyword.text, "end") - block = parser.parseblock("if condition then doSomething() end") + block = parser.parseBlock("if condition then doSomething() end") assert.eq(#block.statements, 1) local simpleIfStat = block.statements[1] :: syntax.AstStatIf assert.eq(simpleIfStat.tag, "conditional") @@ -618,7 +618,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatWhile", function(assert) - local block = parser.parseblock("while i <= 10 do print(i); i = i + 1 end") + local block = parser.parseBlock("while i <= 10 do print(i); i = i + 1 end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -631,7 +631,7 @@ test.suite("parse", function(suite) assert.eq(#whileStat.body.statements, 2) assert.eq(whileStat.endKeyword.text, "end") - block = parser.parseblock("while true do break end") + block = parser.parseBlock("while true do break end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -651,7 +651,7 @@ test.suite("parse", function(suite) assert.eq(whileStat.endKeyword.text, "end") - block = parser.parseblock("while true do continue end") + block = parser.parseBlock("while true do continue end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -673,7 +673,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatRepeat", function(assert) - local block = parser.parseblock("repeat i = i + 1; print(i) until i > 5") + local block = parser.parseBlock("repeat i = i + 1; print(i) until i > 5") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -687,7 +687,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatLocal", function(assert) - local block = parser.parseblock("local b, c = 1, 2") + local block = parser.parseBlock("local b, c = 1, 2") checkIsBlock(block, assert) assert.eq(#block.statements, 1) assert.eq(block.statements[1].kind, "stat") @@ -710,7 +710,7 @@ test.suite("parse", function(suite) assert.eq(l.values[2].node.tag, "number") assert.eq(l.values[2].separator, nil) - block = parser.parseblock("local x") + block = parser.parseBlock("local x") checkIsBlock(block, assert) assert.eq(#block.statements, 1) assert.eq(block.statements[1].kind, "stat") @@ -726,7 +726,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatFor", function(assert) - local block = parser.parseblock("for i = 1, 10, 2 do print(i) end") + local block = parser.parseBlock("for i = 1, 10, 2 do print(i) end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -747,7 +747,7 @@ test.suite("parse", function(suite) assert.eq(#forStat.body.statements, 1) assert.eq(forStat.endKeyword.text, "end") - block = parser.parseblock("for j = 0, 5 do print(j) end") + block = parser.parseBlock("for j = 0, 5 do print(j) end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -768,7 +768,7 @@ test.suite("parse", function(suite) end) suite:case("parseForInLoop", function(assert) - local block = parser.parseblock("for key, value in arr do print(key, value) end") + local block = parser.parseBlock("for key, value in arr do print(key, value) end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -788,7 +788,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatAssign", function(assert) - local block = parser.parseblock("x = 1") + local block = parser.parseBlock("x = 1") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -802,7 +802,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatCompoundAssign", function(assert) - local block = parser.parseblock("x += 5") + local block = parser.parseBlock("x += 5") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -814,7 +814,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatFunction", function(assert) - local block = parser.parseblock("@checked @native @deprecated function add(a, b) return a + b end") + local block = parser.parseBlock("@checked @native @deprecated function add(a, b) return a + b end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -831,7 +831,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatLocalFunction", function(assert) - local block = parser.parseblock("local function multiply(x, y) return x * y end") + local block = parser.parseBlock("local function multiply(x, y) return x * y end") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -844,7 +844,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatTypeAlias", function(assert) - local block = parser.parseblock("type Vector3 = {x: number, y: number, z: number}") + local block = parser.parseBlock("type Vector3 = {x: number, y: number, z: number}") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -860,7 +860,7 @@ test.suite("parse", function(suite) assert.eq(typeAlias.type.kind, "type") assert.eq(typeAlias.type.tag, "table") - block = parser.parseblock("export type Point = {x: T, y: T, f: (U...) -> ()}") + block = parser.parseBlock("export type Point = {x: T, y: T, f: (U...) -> ()}") checkIsBlock(block, assert) assert.eq(#block.statements, 1) @@ -878,7 +878,7 @@ test.suite("parse", function(suite) end) suite:case("parseStatTypeFunction", function(assert) - local block = parser.parseblock("type function foo() return types.number end") + local block = parser.parseBlock("type function foo() return types.number end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -889,7 +889,7 @@ test.suite("parse", function(suite) assert.eq(typeFunc.body.functionKeyword.text, "function") assert.eq(typeFunc.name.text, "foo") - block = parser.parseblock("export type function foo() return types.number end") + block = parser.parseBlock("export type function foo() return types.number end") assert.eq(block.tag, "block") assert.eq(#block.statements, 1) @@ -902,7 +902,7 @@ end) test.suite("parse types", function(suite) -- AstGenericType and AstGenericTypePack are tested in parseExprFunction suite:case("testTypeReference", function(assert) - local block = parser.parseblock("type MyString = string") + local block = parser.parseBlock("type MyString = string") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "reference") @@ -914,7 +914,7 @@ test.suite("parse types", function(suite) assert.eq(typeRef.parameters, nil) assert.eq(typeRef.closeParameters, nil) - block = parser.parseblock("type MyTable = MyModule.Table") + block = parser.parseBlock("type MyTable = MyModule.Table") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "reference") @@ -935,7 +935,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeSingletonBoolean", function(assert) - local block = parser.parseblock("type TrueType = true") + local block = parser.parseBlock("type TrueType = true") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "boolean") @@ -943,7 +943,7 @@ test.suite("parse types", function(suite) assert.eq(singletonBool.token.text, "true") assert.that(singletonBool.value) - block = parser.parseblock("type FalseType = false") + block = parser.parseBlock("type FalseType = false") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "boolean") @@ -953,7 +953,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeSingletonString", function(assert) - local block = parser.parseblock("type HelloType = 'hello'") + local block = parser.parseBlock("type HelloType = 'hello'") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "string") @@ -961,7 +961,7 @@ test.suite("parse types", function(suite) assert.eq(singletonStr.token.text, "hello") assert.eq(singletonStr.quoteStyle, "single") - block = parser.parseblock('type WorldType = "world"') + block = parser.parseBlock('type WorldType = "world"') typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "string") @@ -971,7 +971,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeTypeof", function(assert) - local block = parser.parseblock("type T = typeof(someVariable)") + local block = parser.parseBlock("type T = typeof(someVariable)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "typeof") @@ -983,7 +983,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeGroup", function(assert) - local block = parser.parseblock("type T = (string)") + local block = parser.parseBlock("type T = (string)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "group") @@ -994,7 +994,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeUnion", function(assert) - local block = parser.parseblock("type T = string | number | boolean") + local block = parser.parseBlock("type T = string | number | boolean") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "union") @@ -1005,7 +1005,7 @@ test.suite("parse types", function(suite) assert.eq(unionType.types[2].node.tag, "reference") assert.eq(unionType.types[3].node.tag, "reference") - block = parser.parseblock("type T = | number?") + block = parser.parseBlock("type T = | number?") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "union") @@ -1019,7 +1019,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeUnionWithOptionalPart", function(assert) - local block = parser.parseblock("type T = number? | string") + local block = parser.parseBlock("type T = number? | string") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "union") @@ -1042,7 +1042,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeIntersection", function(assert) - local block = parser.parseblock("type T = A & B & C") + local block = parser.parseBlock("type T = A & B & C") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "intersection") @@ -1053,7 +1053,7 @@ test.suite("parse types", function(suite) assert.eq(intersectionType.types[2].node.tag, "reference") assert.eq(intersectionType.types[3].node.tag, "reference") - block = parser.parseblock("type T = & B") + block = parser.parseBlock("type T = & B") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "intersection") @@ -1065,7 +1065,7 @@ test.suite("parse types", function(suite) end) suite:case("testTypeArray", function(assert) - local block = parser.parseblock("type T = { number }") + local block = parser.parseBlock("type T = { number }") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "array") @@ -1075,7 +1075,7 @@ test.suite("parse types", function(suite) assert.eq(arrayType.type.tag, "reference") assert.eq(arrayType.closeBrace.text, "}") - block = parser.parseblock("type T = { read string }") + block = parser.parseBlock("type T = { read string }") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "array") @@ -1083,7 +1083,7 @@ test.suite("parse types", function(suite) assert.neq(arrayType.access, nil) assert.eq((arrayType.access :: syntax.Token<"read" | "write">).text, "read") - block = parser.parseblock("type T = { write number }") + block = parser.parseBlock("type T = { write number }") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "array") @@ -1093,7 +1093,7 @@ test.suite("parse types", function(suite) end) suite:case("parseTypeTable", function(assert) - local block = parser.parseblock("type T = {}") + local block = parser.parseBlock("type T = {}") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") @@ -1102,7 +1102,7 @@ test.suite("parse types", function(suite) assert.eq(#tableType.entries, 0) assert.eq(tableType.closeBrace.text, "}") - block = parser.parseblock("type T = {[number] : string}") + block = parser.parseBlock("type T = {[number] : string}") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") @@ -1118,7 +1118,7 @@ test.suite("parse types", function(suite) assert.eq(tableTypeItem.value.tag, "reference") assert.eq(tableTypeItem.separator, nil) - block = parser.parseblock("type T = {['hello'] : 'world', read ['foo'] : number; write ['bar'] : boolean}") + block = parser.parseBlock("type T = {['hello'] : 'world', read ['foo'] : number; write ['bar'] : boolean}") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") tableType = typeAlias.type :: syntax.AstTypeTable @@ -1149,7 +1149,7 @@ test.suite("parse types", function(suite) assert.eq((tableTypeItem.access :: syntax.Token<"read" | "write">).text, "write") assert.eq(tableTypeItem.separator, nil) - block = parser.parseblock("type T = { hello: 'world'; read foo: number, write bar: boolean }") + block = parser.parseBlock("type T = { hello: 'world'; read foo: number, write bar: boolean }") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "table") tableType = typeAlias.type :: syntax.AstTypeTable @@ -1178,7 +1178,7 @@ test.suite("parse types", function(suite) end) suite:case("parseTypeFunction", function(assert) - local block = parser.parseblock("type T = (n : number, string) -> boolean") + local block = parser.parseBlock("type T = (n : number, string) -> boolean") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") @@ -1207,7 +1207,7 @@ test.suite("parse types", function(suite) assert.eq(funcType.returnArrow.text, "->") assert.eq(funcType.returnTypes.kind, "typepack") - block = parser.parseblock("type T = (item: T, ...U) -> ()") + block = parser.parseBlock("type T = (item: T, ...U) -> ()") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") @@ -1229,7 +1229,7 @@ test.suite("parse types", function(suite) assert.neq(funcType.vararg, nil) assert.eq((funcType.vararg :: syntax.AstTypePack).kind, "typepack") - block = parser.parseblock("type T = () -> ...number") + block = parser.parseBlock("type T = () -> ...number") typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.type.tag, "function") @@ -1248,7 +1248,7 @@ test.suite("AST_nodes_frozen", function(suite) -- Expression nodes suite:case("astExprGroupFrozen", function(assert) - local expr = parser.parseexpr("(x)") + local expr = parser.parseExpr("(x)") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "group") assert.throwsWith(frozenTableMessage, function() @@ -1257,7 +1257,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprConstantNilFrozen", function(assert) - local expr = parser.parseexpr("nil") + local expr = parser.parseExpr("nil") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "nil") assert.throwsWith(frozenTableMessage, function() @@ -1269,7 +1269,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprConstantBoolFrozen", function(assert) - local expr = parser.parseexpr("true") + local expr = parser.parseExpr("true") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "boolean") assert.throwsWith(frozenTableMessage, function() @@ -1281,7 +1281,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprConstantNumberFrozen", function(assert) - local expr = parser.parseexpr("42") + local expr = parser.parseExpr("42") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "number") assert.throwsWith(frozenTableMessage, function() @@ -1293,7 +1293,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprConstantStringFrozen", function(assert) - local expr = parser.parseexpr("'hello'") + local expr = parser.parseExpr("'hello'") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "string") assert.throwsWith(frozenTableMessage, function() @@ -1305,7 +1305,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprLocalFrozen", function(assert) - local block = parser.parseblock("local x = 1\nreturn x") + local block = parser.parseBlock("local x = 1\nreturn x") local returnStat = block.statements[2] :: syntax.AstStatReturn local expr = returnStat.expressions[1].node assert.eq(expr.kind, "expr") @@ -1316,7 +1316,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprGlobalFrozen", function(assert) - local expr = parser.parseexpr("_G") + local expr = parser.parseExpr("_G") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "global") assert.throwsWith(frozenTableMessage, function() @@ -1325,7 +1325,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprVarargsFrozen", function(assert) - local expr = parser.parseexpr("...") + local expr = parser.parseExpr("...") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "vararg") assert.throwsWith(frozenTableMessage, function() @@ -1334,7 +1334,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprCallFrozen", function(assert) - local expr = parser.parseexpr("foo(1, 2)") + local expr = parser.parseExpr("foo(1, 2)") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "call") assert.throwsWith(frozenTableMessage, function() @@ -1346,7 +1346,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprInstantiateFrozen", function(assert) - local callExpr = parser.parseexpr("foo<>()") + local callExpr = parser.parseExpr("foo<>()") assert.eq(callExpr.tag, "call") local expr = (callExpr :: syntax.AstExprCall).func assert.eq(expr.kind, "expr") @@ -1360,7 +1360,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprIndexNameFrozen", function(assert) - local expr = parser.parseexpr("obj.field") + local expr = parser.parseExpr("obj.field") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "indexname") assert.throwsWith(frozenTableMessage, function() @@ -1369,7 +1369,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprIndexExprFrozen", function(assert) - local expr = parser.parseexpr("obj[key]") + local expr = parser.parseExpr("obj[key]") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "index") assert.throwsWith(frozenTableMessage, function() @@ -1378,7 +1378,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprFunctionFrozen", function(assert) - local expr = parser.parseexpr("@checked function(x, y) return x + y end") + local expr = parser.parseExpr("@checked function(x, y) return x + y end") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "function") assert.throwsWith(frozenTableMessage, function() @@ -1402,7 +1402,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprTableFrozen", function(assert) - local expr = parser.parseexpr("{ a = 1, [2] = 3, 4 }") + local expr = parser.parseExpr("{ a = 1, [2] = 3, 4 }") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "table") assert.throwsWith(frozenTableMessage, function() @@ -1423,7 +1423,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprUnaryFrozen", function(assert) - local expr = parser.parseexpr("-x") + local expr = parser.parseExpr("-x") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "unary") assert.throwsWith(frozenTableMessage, function() @@ -1432,7 +1432,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprBinaryFrozen", function(assert) - local expr = parser.parseexpr("x + y") + local expr = parser.parseExpr("x + y") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "binary") assert.throwsWith(frozenTableMessage, function() @@ -1441,7 +1441,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprInterpStringFrozen", function(assert) - local expr = parser.parseexpr("`hello {x}`") + local expr = parser.parseExpr("`hello {x}`") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "interpolatedstring") assert.throwsWith(frozenTableMessage, function() @@ -1453,7 +1453,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprTypeAssertionFrozen", function(assert) - local expr = parser.parseexpr("x :: number") + local expr = parser.parseExpr("x :: number") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "cast") assert.throwsWith(frozenTableMessage, function() @@ -1462,7 +1462,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astExprIfElseFrozen", function(assert) - local expr = parser.parseexpr("if x then 1 elseif y then 2 else 3") + local expr = parser.parseExpr("if x then 1 elseif y then 2 else 3") assert.eq(expr.kind, "expr") assert.eq(expr.tag, "conditional") assert.throwsWith(frozenTableMessage, function() @@ -1478,7 +1478,7 @@ test.suite("AST_nodes_frozen", function(suite) -- Statement nodes suite:case("astStatBlockFrozen", function(assert) - local block = parser.parseblock("local x = 1\nlocal y = 2") + local block = parser.parseBlock("local x = 1\nlocal y = 2") assert.eq(block.kind, "stat") assert.eq(block.tag, "block") assert.throwsWith(frozenTableMessage, function() @@ -1490,7 +1490,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatDoFrozen", function(assert) - local block = parser.parseblock("do local x = 1 end") + local block = parser.parseBlock("do local x = 1 end") local doStat = block.statements[1] :: syntax.AstStatDo assert.eq(doStat.kind, "stat") assert.eq(doStat.tag, "do") @@ -1500,7 +1500,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatIfFrozen", function(assert) - local block = parser.parseblock("if x then local y = 1 elseif z then local w = 2 else local v = 3 end") + local block = parser.parseBlock("if x then local y = 1 elseif z then local w = 2 else local v = 3 end") local ifStat = block.statements[1] :: syntax.AstStatIf assert.eq(ifStat.kind, "stat") assert.eq(ifStat.tag, "conditional") @@ -1516,7 +1516,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatWhileFrozen", function(assert) - local block = parser.parseblock("while x do local y = 1 end") + local block = parser.parseBlock("while x do local y = 1 end") local whileStat = block.statements[1] :: syntax.AstStatWhile assert.eq(whileStat.kind, "stat") assert.eq(whileStat.tag, "while") @@ -1526,7 +1526,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatRepeatFrozen", function(assert) - local block = parser.parseblock("repeat local x = 1 until x > 10") + local block = parser.parseBlock("repeat local x = 1 until x > 10") local repeatStat = block.statements[1] :: syntax.AstStatRepeat assert.eq(repeatStat.kind, "stat") assert.eq(repeatStat.tag, "repeat") @@ -1536,7 +1536,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatBreakFrozen", function(assert) - local block = parser.parseblock("while true do break end") + local block = parser.parseBlock("while true do break end") local whileStat = block.statements[1] :: syntax.AstStatWhile local breakStat = whileStat.body.statements[1] :: syntax.AstStatBreak assert.eq(breakStat.kind, "stat") @@ -1547,7 +1547,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatContinueFrozen", function(assert) - local block = parser.parseblock("while true do continue end") + local block = parser.parseBlock("while true do continue end") local whileStat = block.statements[1] :: syntax.AstStatWhile local continueStat = whileStat.body.statements[1] :: syntax.AstStatContinue assert.eq(continueStat.kind, "stat") @@ -1558,7 +1558,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatReturnFrozen", function(assert) - local block = parser.parseblock("return 1, 2, 3") + local block = parser.parseBlock("return 1, 2, 3") local returnStat = block.statements[1] :: syntax.AstStatReturn assert.eq(returnStat.kind, "stat") assert.eq(returnStat.tag, "return") @@ -1571,7 +1571,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatExprFrozen", function(assert) - local block = parser.parseblock("foo()") + local block = parser.parseBlock("foo()") local exprStat = block.statements[1] :: syntax.AstStatExpr assert.eq(exprStat.kind, "stat") assert.eq(exprStat.tag, "expression") @@ -1581,7 +1581,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatLocalFrozen", function(assert) - local block = parser.parseblock("local x, y = 1, 2") + local block = parser.parseBlock("local x, y = 1, 2") local localStat = block.statements[1] :: syntax.AstStatLocal assert.eq(localStat.kind, "stat") assert.eq(localStat.tag, "local") @@ -1597,7 +1597,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatForFrozen", function(assert) - local block = parser.parseblock("for i = 1, 10, 2 do local x = i end") + local block = parser.parseBlock("for i = 1, 10, 2 do local x = i end") local forStat = block.statements[1] :: syntax.AstStatFor assert.eq(forStat.kind, "stat") assert.eq(forStat.tag, "for") @@ -1607,7 +1607,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatForInFrozen", function(assert) - local block = parser.parseblock("for k, v in pairs(t) do local x = k end") + local block = parser.parseBlock("for k, v in pairs(t) do local x = k end") local forInStat = block.statements[1] :: syntax.AstStatForIn assert.eq(forInStat.kind, "stat") assert.eq(forInStat.tag, "forin") @@ -1623,7 +1623,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatAssignFrozen", function(assert) - local block = parser.parseblock("x, y = 1, 2") + local block = parser.parseBlock("x, y = 1, 2") local assignStat = block.statements[1] :: syntax.AstStatAssign assert.eq(assignStat.kind, "stat") assert.eq(assignStat.tag, "assign") @@ -1639,7 +1639,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatCompoundAssignFrozen", function(assert) - local block = parser.parseblock("x += 5") + local block = parser.parseBlock("x += 5") local compoundStat = block.statements[1] :: syntax.AstStatCompoundAssign assert.eq(compoundStat.kind, "stat") assert.eq(compoundStat.tag, "compoundassign") @@ -1649,7 +1649,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatFunctionFrozen", function(assert) - local block = parser.parseblock("function foo() end") + local block = parser.parseBlock("function foo() end") local funcStat = block.statements[1] :: syntax.AstStatFunction assert.eq(funcStat.kind, "stat") assert.eq(funcStat.tag, "function") @@ -1659,7 +1659,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatLocalFunctionFrozen", function(assert) - local block = parser.parseblock("local function foo() end") + local block = parser.parseBlock("local function foo() end") local localFuncStat = block.statements[1] :: syntax.AstStatLocalFunction assert.eq(localFuncStat.kind, "stat") assert.eq(localFuncStat.tag, "localfunction") @@ -1669,7 +1669,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatTypeAliasFrozen", function(assert) - local block = parser.parseblock("type Foo = T") + local block = parser.parseBlock("type Foo = T") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias assert.eq(typeAlias.kind, "stat") assert.eq(typeAlias.tag, "typealias") @@ -1679,7 +1679,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astStatTypeFunctionFrozen", function(assert) - local block = parser.parseblock("type function foo() return function() end end") + local block = parser.parseBlock("type function foo() return function() end end") local typeFunc = block.statements[1] :: syntax.AstStatTypeFunction assert.eq(typeFunc.kind, "stat") assert.eq(typeFunc.tag, "typefunction") @@ -1690,7 +1690,7 @@ test.suite("AST_nodes_frozen", function(suite) -- Type nodes suite:case("astTypeReferenceFrozen", function(assert) - local block = parser.parseblock("type Foo = Bar") + local block = parser.parseBlock("type Foo = Bar") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local typeRef = typeAlias.type :: syntax.AstTypeReference assert.eq(typeRef.kind, "type") @@ -1701,7 +1701,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeSingletonBoolFrozen", function(assert) - local block = parser.parseblock("type Foo = true") + local block = parser.parseBlock("type Foo = true") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local singletonBool = typeAlias.type :: syntax.AstTypeSingletonBool assert.eq(singletonBool.kind, "type") @@ -1712,7 +1712,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeSingletonStringFrozen", function(assert) - local block = parser.parseblock("type Foo = 'hello'") + local block = parser.parseBlock("type Foo = 'hello'") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local singletonStr = typeAlias.type :: syntax.AstTypeSingletonString assert.eq(singletonStr.kind, "type") @@ -1723,7 +1723,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeTypeofFrozen", function(assert) - local block = parser.parseblock("type Foo = typeof(x)") + local block = parser.parseBlock("type Foo = typeof(x)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local typeofType = typeAlias.type :: syntax.AstTypeTypeof assert.eq(typeofType.kind, "type") @@ -1734,7 +1734,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeGroupFrozen", function(assert) - local block = parser.parseblock("type Foo = (Bar)") + local block = parser.parseBlock("type Foo = (Bar)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local groupType = typeAlias.type :: syntax.AstTypeGroup assert.eq(groupType.kind, "type") @@ -1745,7 +1745,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeOptionalFrozen", function(assert) - local block = parser.parseblock("type Foo = string?") + local block = parser.parseBlock("type Foo = string?") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local unionType = typeAlias.type :: syntax.AstTypeUnion local optionalType = unionType.types[2].node :: syntax.AstTypeOptional @@ -1757,7 +1757,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeUnionFrozen", function(assert) - local block = parser.parseblock("type Foo = string | number") + local block = parser.parseBlock("type Foo = string | number") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local unionType = typeAlias.type :: syntax.AstTypeUnion assert.eq(unionType.kind, "type") @@ -1771,7 +1771,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeIntersectionFrozen", function(assert) - local block = parser.parseblock("type Foo = A & B") + local block = parser.parseBlock("type Foo = A & B") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local intersectionType = typeAlias.type :: syntax.AstTypeIntersection assert.eq(intersectionType.kind, "type") @@ -1785,7 +1785,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeArrayFrozen", function(assert) - local block = parser.parseblock("type Foo = {string}") + local block = parser.parseBlock("type Foo = {string}") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local arrayType = typeAlias.type :: syntax.AstTypeArray assert.eq(arrayType.kind, "type") @@ -1796,7 +1796,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeTableFrozen", function(assert) - local block = parser.parseblock('type Foo = { a: number, [string]: boolean, ["hello"]: "world" }') + local block = parser.parseBlock('type Foo = { a: number, [string]: boolean, ["hello"]: "world" }') local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local tableType = typeAlias.type :: syntax.AstTypeTable assert.eq(tableType.kind, "type") @@ -1819,7 +1819,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypeFunctionFrozen", function(assert) - local block = parser.parseblock("type Foo = (x: number) -> string") + local block = parser.parseBlock("type Foo = (x: number) -> string") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local funcType = typeAlias.type :: syntax.AstTypeFunction assert.eq(funcType.kind, "type") @@ -1833,7 +1833,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypePackExplicitFrozen", function(assert) - local block = parser.parseblock("type Foo = (number, string) -> (boolean, string)") + local block = parser.parseBlock("type Foo = (number, string) -> (boolean, string)") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local funcType = typeAlias.type :: syntax.AstTypeFunction local typePack = funcType.returnTypes :: syntax.AstTypePackExplicit @@ -1845,7 +1845,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astTypePackVariadicFrozen", function(assert) - local block = parser.parseblock("type Foo = () -> ...number") + local block = parser.parseBlock("type Foo = () -> ...number") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local funcType = typeAlias.type :: syntax.AstTypeFunction local typePack = funcType.returnTypes :: syntax.AstTypePackVariadic @@ -1858,7 +1858,7 @@ test.suite("AST_nodes_frozen", function(suite) -- Other node types suite:case("astLocalFrozen", function(assert) - local block = parser.parseblock("local x: number = 1") + local block = parser.parseBlock("local x: number = 1") local localStat = block.statements[1] :: syntax.AstStatLocal local local_ = localStat.variables[1].node assert.eq(local_.kind, "local") @@ -1868,7 +1868,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astGenericTypeFrozen", function(assert) - local block = parser.parseblock("type Foo = T") + local block = parser.parseBlock("type Foo = T") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local generic = (typeAlias.generics :: any)[1].node assert.eq(generic.tag, "generic") @@ -1878,7 +1878,7 @@ test.suite("AST_nodes_frozen", function(suite) end) suite:case("astGenericTypePackFrozen", function(assert) - local block = parser.parseblock("type Foo = (T, U...) -> ()") + local block = parser.parseBlock("type Foo = (T, U...) -> ()") local typeAlias = block.statements[1] :: syntax.AstStatTypeAlias local genericPack = (typeAlias.genericPacks :: any)[1].node assert.eq(genericPack.tag, "generic") @@ -1890,7 +1890,7 @@ test.suite("AST_nodes_frozen", function(suite) -- Token type suite:case("tokenFrozen", function(assert) - local block = parser.parseblock("local x = 1") + local block = parser.parseBlock("local x = 1") local localStat = block.statements[1] :: syntax.AstStatLocal local token = localStat.localKeyword assert.eq(token.kind, "token") @@ -1907,7 +1907,7 @@ test.suite("AST_nodes_frozen", function(suite) -- Trivia types suite:case("triviaFrozen", function(assert) - local block = parser.parseblock([=[ + local block = parser.parseBlock([=[ -- This is a comment --[[This is a multiline comment hello @@ -1937,7 +1937,7 @@ test.suite("AST_nodes_frozen", function(suite) -- Pair type (used in Punctuated) suite:case("pairFrozen", function(assert) - local block = parser.parseblock("local x, y = 1, 2") + local block = parser.parseBlock("local x, y = 1, 2") local localStat = block.statements[1] :: syntax.AstStatLocal local pair = localStat.variables[1] assert.throwsWith(frozenTableMessage, function() diff --git a/tests/std/syntax/printer.test.luau b/tests/std/syntax/printer.test.luau index 28d23b4d1..32b55c726 100644 --- a/tests/std/syntax/printer.test.luau +++ b/tests/std/syntax/printer.test.luau @@ -15,7 +15,7 @@ local function checkReplacement( local ast = syntax.parse(source) local replacements = transformFn(ast.root) - local result = printer.printfile(ast, replacements) + local result = printer.printFile(ast, replacements) assert.eq(result, expected) end @@ -29,7 +29,7 @@ test.suite("SyntaxPrinter", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast :: any, syntaxUtils.isExprConstantString):replace(function(s) + return query.findAllFromRoot(ast :: any, syntaxUtils.isExprConstantString):replace(function(s) return if s.token.text == "World" then "1 + 2" else nil end) end, assert) @@ -43,7 +43,7 @@ test.suite("SyntaxPrinter", function(suite) local expected = " local name = 1 + 2\n" checkReplacement(source, expected, function(ast) - return query.findallfromroot(ast :: any, syntaxUtils.isExprConstantString):replace(function(s) + return query.findAllFromRoot(ast :: any, syntaxUtils.isExprConstantString):replace(function(s) return if s.token.text == "World" then "1 + 2" else nil end) end, assert) @@ -58,7 +58,7 @@ test.suite("SyntaxPrinter", function(suite) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function(_) return "local name = 1 + 2" end) @@ -69,17 +69,17 @@ test.suite("SyntaxPrinter", function(suite) local source = [[local name: number = "World"]] local expected = [[local name: string = "World"]] - local stat = syntax.parseblock("local x: string") + local stat = syntax.parseBlock("local x: string") local ann = (stat.statements[1] :: syntaxTypes.AstStatLocal).variables[1].node.annotation checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :map(function(assign) return assign.variables[1].node.annotation end) :replace(function(_) - return printer.printnode(ann :: any) -- LUAUFIX: Remove any cast once code too complex no longer emitted + return printer.printNode(ann :: any) -- LUAUFIX: Remove any cast once code too complex no longer emitted end) end, assert) end) @@ -88,17 +88,17 @@ test.suite("SyntaxPrinter", function(suite) local source = [[type t = (...number) -> ()]] local expected = [[type t = (...number) -> string, ...string)]] - local stat = syntax.parseblock("type t = () -> (string, ...string)") + local stat = syntax.parseBlock("type t = () -> (string, ...string)") local tp = ((stat.statements[1] :: syntaxTypes.AstStatTypeAlias).type :: syntaxTypes.AstTypeFunction).returnTypes checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatTypeAlias) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatTypeAlias) -- LUAUFIX: Remove any cast once code too complex no longer emitted :map(function(ta) return if ta.type.tag == "function" then ta.type.returnTypes else nil end) :replace(function(_) - return printer.printnode(tp) + return printer.printNode(tp) end) end, assert) end) @@ -109,7 +109,7 @@ test.suite("SyntaxPrinter", function(suite) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted :map(function(node) return node.entries[1] end) @@ -126,7 +126,7 @@ test.suite("SyntaxPrinter", function(suite) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprGroup) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprGroup) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -140,7 +140,7 @@ nil -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprConstantNil) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprConstantNil) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -154,7 +154,7 @@ true -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprConstantBool) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprConstantBool) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -168,7 +168,7 @@ true -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprConstantNumber) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprConstantNumber) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -183,7 +183,7 @@ y -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -197,7 +197,7 @@ y -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprGlobal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprGlobal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -211,7 +211,7 @@ y -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprVarargs) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprVarargs) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -225,7 +225,7 @@ print() -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprCall) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprCall) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -239,7 +239,7 @@ hello.world -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprIndexName) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprIndexName) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -253,7 +253,7 @@ hello['world'] -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprIndexExpr) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprIndexExpr) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -267,7 +267,7 @@ function() return end -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -281,7 +281,7 @@ function() return end -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -295,7 +295,7 @@ function() return end -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprUnary) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprUnary) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -309,7 +309,7 @@ function() return end -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprBinary) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprBinary) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -323,7 +323,7 @@ function() return end -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprInterpString) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprInterpString) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -337,7 +337,7 @@ y :: number -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprTypeAssertion) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprTypeAssertion) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -351,7 +351,7 @@ if true then 'hello' else 'world' -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprIfElse) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprIfElse) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -365,7 +365,7 @@ if true then 'hello' else 'world', z :: string -- after]] checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExpr) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExpr) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -380,7 +380,7 @@ local y = 1 checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatBlock) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatBlock) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -399,7 +399,7 @@ end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatIf) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatIf) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -416,7 +416,7 @@ end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatWhile) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatWhile) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -433,7 +433,7 @@ until true checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatRepeat) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatRepeat) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -450,7 +450,7 @@ end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatBreak) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatBreak) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -467,7 +467,7 @@ end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatContinue) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatContinue) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -482,7 +482,7 @@ function foo() return 1, 2 end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatReturn) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatReturn) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -497,7 +497,7 @@ foobar() checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatExpr) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatExpr) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -512,7 +512,7 @@ local x = 1 checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -529,7 +529,7 @@ end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatFor) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatFor) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -546,7 +546,7 @@ end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatForIn) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatForIn) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -562,7 +562,7 @@ y = 1 checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatAssign) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatAssign) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -578,7 +578,7 @@ y -= 1 checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatCompoundAssign) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatCompoundAssign) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -595,7 +595,7 @@ end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -612,7 +612,7 @@ end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatLocalFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatLocalFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -627,7 +627,7 @@ type foo = number checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatTypeAlias) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatTypeAlias) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -642,7 +642,7 @@ type function foo() return number end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStatTypeFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStatTypeFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -658,7 +658,7 @@ local y = 2 checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isStat) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isStat) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -673,7 +673,7 @@ type foo = number checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeReference) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeReference) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -688,7 +688,7 @@ type foo = true checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeSingletonBool) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeSingletonBool) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -703,7 +703,7 @@ local x : "hello" checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeSingletonString) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeSingletonString) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -718,7 +718,7 @@ type foo = typeof(123) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeTypeof) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeTypeof) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -733,7 +733,7 @@ type foo = (number) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeGroup) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeGroup) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -748,7 +748,7 @@ type foo = true? checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeOptional) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeOptional) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -763,7 +763,7 @@ type foo = true? | false checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeUnion) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeUnion) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -778,7 +778,7 @@ type foo = true & false checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeIntersection) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeIntersection) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -793,7 +793,7 @@ type foo = { number } checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeArray) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeArray) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -808,7 +808,7 @@ type foo = {} checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -823,7 +823,7 @@ type foo = (number) -> () checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypeFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypeFunction) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -838,7 +838,7 @@ type foo = (number) -> () checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isType) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isType) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -853,7 +853,7 @@ type foo = (number) -> (number, ...string) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypePackExplicit) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypePackExplicit) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -868,7 +868,7 @@ type foo = (number) -> (number, G...) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypePackGeneric) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypePackGeneric) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -883,7 +883,7 @@ type foo = (number) -> (...number) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypePackVariadic) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypePackVariadic) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -898,7 +898,7 @@ type foo = (number) -> (number, ...string) checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isTypePack) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isTypePack) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -913,7 +913,7 @@ local x, y = 1, 2 checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isLocal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -929,7 +929,7 @@ function foo() return end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isAttribute) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isAttribute) -- LUAUFIX: Remove any cast once code too complex no longer emitted :replace(function() return "replaced" end) @@ -947,7 +947,7 @@ function foo() return end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprGlobal) -- LUAUFIX: Remove any cast once code too complex no longer emitted + .findAllFromRoot(ast :: any, syntaxUtils.isExprGlobal) -- LUAUFIX: Remove any cast once code too complex no longer emitted :filter(function(g) return g.name.text == "baz" end) @@ -978,8 +978,8 @@ function foo() return end checkReplacement(source, expected, function(ast) return query - .findallfromroot(ast :: any, syntaxUtils.isExprTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted - :flatmap(function(t) + .findAllFromRoot(ast :: any, syntaxUtils.isExprTable) -- LUAUFIX: Remove any cast once code too complex no longer emitted + :flatMap(function(t) local l = {} for _, entry in t.entries do if entry.kind ~= "list" then diff --git a/tests/std/syntax/query.test.luau b/tests/std/syntax/query.test.luau index bef339e36..a4ffe412b 100644 --- a/tests/std/syntax/query.test.luau +++ b/tests/std/syntax/query.test.luau @@ -5,7 +5,7 @@ local utils = require("@std/syntax/utils") test.suite("AstQuery", function(suite) suite:case("filter", function(assert) - local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.findAllFromRoot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local filteredQuery = testQuery:filter(function(n) return n.value > 2 @@ -18,7 +18,7 @@ test.suite("AstQuery", function(suite) end) suite:case("replace", function(assert) - local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.findAllFromRoot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local replacements = testQuery:replace(function(n): string? if n.value > 0 then @@ -46,7 +46,7 @@ test.suite("AstQuery", function(suite) end) suite:case("map", function(assert) - local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.findAllFromRoot(syntax.parse("local _ = {0, 1, 2, 3, 4}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local mappedQuery = query.map(testQuery, function(n: syntax.AstExprConstantNumber): number? if n.value % 2 == 0 then @@ -62,10 +62,10 @@ test.suite("AstQuery", function(suite) end) suite:case("foreach", function(assert) - local testQuery = query.findallfromroot(syntax.parse("local _ = {0, 1, 2}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + local testQuery = query.findAllFromRoot(syntax.parse("local _ = {0, 1, 2}"), utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field local sum = 0 - testQuery:foreach(function(n: syntax.AstExprConstantNumber) + testQuery:forEach(function(n: syntax.AstExprConstantNumber) sum = sum + n.value end) @@ -74,7 +74,7 @@ test.suite("AstQuery", function(suite) suite:case("findallNums", function(assert) local ast = syntax.parse("print(1 + 2)") - local queryResult: query.Query = query.findallfromroot( + local queryResult: query.Query = query.findAllFromRoot( ast, -- LUAUFIX: Complains that AstStatBlock doesn't have location? field function(n: syntax.AstNode): syntax.AstExprConstantNumber? if n.kind == "expr" and n.tag == "number" then @@ -98,7 +98,7 @@ test.suite("AstQuery", function(suite) suite:case("findallUtils", function(assert) local ast = syntax.parse("print(1 + 2)") local queryResult: query.Query = - query.findallfromroot(ast, utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field + query.findAllFromRoot(ast, utils.isExprConstantNumber) -- LUAUFIX: Complains that AstStatBlock doesn't have location? field assert.eq(#queryResult.nodes, 2) local num: syntax.AstExpr = queryResult.nodes[1] @@ -114,23 +114,23 @@ test.suite("AstQuery", function(suite) suite:case("findallTokens", function(assert) local ast = syntax.parse("local x = 1 or nil") -- verify that query doesn't doubly count tokens - local queryResult = query.findallfromroot(ast, utils.isToken) + local queryResult = query.findAllFromRoot(ast, utils.isToken) assert.eq(#queryResult.nodes, 6) - queryResult:foreach(function(node) + queryResult:forEach(function(node) assert.eq(node.kind, "token") end) - local constNums = query.findallfromroot(ast, utils.isExprConstantNumber) + local constNums = query.findAllFromRoot(ast, utils.isExprConstantNumber) assert.eq(#constNums.nodes, 1) - local constNils = query.findallfromroot(ast, utils.isExprConstantNil) + local constNils = query.findAllFromRoot(ast, utils.isExprConstantNil) assert.eq(#constNils.nodes, 1) - local tokens = query.findallfromroot(ast, utils.isToken) + local tokens = query.findAllFromRoot(ast, utils.isToken) assert.eq(#tokens.nodes, 6) end) suite:case("findall", function(assert) -- Test selecting from a query of binary expressions to find their operands local ast = syntax.parse("local x = 1 + 2") - local nums = query.findallfromroot(ast, utils.isExprBinary):findall(utils.isExprConstantNumber) + local nums = query.findAllFromRoot(ast, utils.isExprBinary):findAll(utils.isExprConstantNumber) assert.eq(#nums.nodes, 2) assert.eq(nums.nodes[1].value, 1) @@ -140,15 +140,15 @@ test.suite("AstQuery", function(suite) suite:case("findall", function(assert) -- Test selecting through nested structures local ast = syntax.parse("local x = {a = 1 + 2, b = 3 + 4}") - local tables = query.findallfromroot(ast, utils.isExprTable) + local tables = query.findAllFromRoot(ast, utils.isExprTable) -- Select all binary expressions within the table - local binaryExprs = tables:findall(utils.isExprBinary) + local binaryExprs = tables:findAll(utils.isExprBinary) assert.eq(#binaryExprs.nodes, 2) -- Now select all numbers from those binary expressions - local numbers = binaryExprs:findall(utils.isExprConstantNumber) + local numbers = binaryExprs:findAll(utils.isExprConstantNumber) assert.eq(#numbers.nodes, 4) assert.eq(numbers.nodes[1].value, 1) @@ -160,7 +160,7 @@ test.suite("AstQuery", function(suite) suite:case("flatmap", function(assert) -- Test flatmap to collect multiple items from each node local ast = syntax.parse("local x = 1 + 2; local y = 3 + 4") - local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l) + local locals = query.findAllFromRoot(ast, utils.isStatLocal):flatMap(function(l) local values = {} for _, v in l.values do table.insert(values, v.node) @@ -178,7 +178,7 @@ test.suite("AstQuery", function(suite) suite:case("flatmapEmpty", function(assert) -- Test flatmap with some nodes returning empty arrays local ast = syntax.parse("local x; local y = 1; local z") - local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l) + local locals = query.findAllFromRoot(ast, utils.isStatLocal):flatMap(function(l) local values = {} for _, v in l.values do table.insert(values, v.node) @@ -195,7 +195,7 @@ test.suite("AstQuery", function(suite) suite:case("flatmapMultiple", function(assert) -- Test flatmap with nodes that return multiple items local ast = syntax.parse("local a, b = 1, 2; local c, d = 3, 4, 5") - local locals = query.findallfromroot(ast, utils.isStatLocal):flatmap(function(l: syntax.AstStatLocal) + local locals = query.findAllFromRoot(ast, utils.isStatLocal):flatMap(function(l: syntax.AstStatLocal) local vars = {} for _, v in l.variables do table.insert(vars, v.node) @@ -213,9 +213,9 @@ test.suite("AstQuery", function(suite) suite:case("maptoarray", function(assert) -- Test maptoarray to convert query nodes to a simple array local ast = syntax.parse("local _ = {0, 1, 2, 3, 4}") - local nums = query.findallfromroot(ast, utils.isExprConstantNumber) + local nums = query.findAllFromRoot(ast, utils.isExprConstantNumber) - local doubled = nums:maptoarray(function(n: syntax.AstExprConstantNumber): number + local doubled = nums:mapToArray(function(n: syntax.AstExprConstantNumber): number return n.value * 2 end) @@ -230,9 +230,9 @@ test.suite("AstQuery", function(suite) suite:case("maptoarrayWithNil", function(assert) -- Test maptoarray filtering out nil values local ast = syntax.parse("local _ = {0, 1, 2, 3, 4}") - local nums = query.findallfromroot(ast, utils.isExprConstantNumber) + local nums = query.findAllFromRoot(ast, utils.isExprConstantNumber) - local evenOnly = nums:maptoarray(function(n: syntax.AstExprConstantNumber): number? + local evenOnly = nums:mapToArray(function(n: syntax.AstExprConstantNumber): number? if n.value % 2 == 0 then return n.value end diff --git a/tests/std/syntax/utils.test.luau b/tests/std/syntax/utils.test.luau index c25654d92..ce461975c 100644 --- a/tests/std/syntax/utils.test.luau +++ b/tests/std/syntax/utils.test.luau @@ -6,13 +6,13 @@ local utils = require("@std/syntax/utils") test.suite("Utils", function(suite) suite:case("isRequireCall", function(assert) local ast1 = syntax.parse('local x = require("module")') - local results = query.findallfromroot(ast1, utils.isRequireCall) + local results = query.findAllFromRoot(ast1, utils.isRequireCall) assert.eq(#results.nodes, 1) assert.eq(results.nodes[1].tag, "call") -- Non-require calls should not match local ast2 = syntax.parse('local x = print("hello")') - local results2 = query.findallfromroot(ast2, utils.isRequireCall) + local results2 = query.findAllFromRoot(ast2, utils.isRequireCall) assert.eq(#results2.nodes, 0) end) end) From 458c03fe20a5ba6070d6cb7ff0113d97ca8dd2d3 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Wed, 1 Apr 2026 13:34:05 -0700 Subject: [PATCH 441/642] Refactor fs functions to be camelCased (#925) Refactor for #919 --- examples/create_directory.luau | 4 +- examples/directories.luau | 2 +- examples/walk_directory.luau | 8 +- examples/watch_directory.luau | 2 +- lute/cli/commands/doc/init.luau | 18 +-- lute/cli/commands/lib/parseIgnores.luau | 2 +- lute/cli/commands/lint/init.luau | 6 +- lute/cli/commands/lint/printer.luau | 2 +- lute/cli/commands/new/init.luau | 8 +- .../pkg/loom-core/src/utils/fsutils.luau | 6 +- lute/cli/commands/setup/init.luau | 14 +- lute/cli/commands/test/snap/runner.luau | 2 +- lute/cli/commands/test/snap/updater.luau | 2 +- lute/cli/commands/transform/init.luau | 6 +- lute/std/libs/fs.luau | 22 +-- tests/cli/doc.test.luau | 6 +- tests/cli/lib/files.test.luau | 110 +++++++------- tests/cli/lint.test.luau | 136 +++++++++--------- tests/cli/loadmodule.test.luau | 4 +- tests/cli/new.test.luau | 12 +- tests/cli/run.test.luau | 8 +- tests/std/fs.test.luau | 66 ++++----- tests/std/json.test.luau | 4 +- tests/std/syntax/parser.test.luau | 4 +- tests/std/test.test.luau | 6 +- 25 files changed, 230 insertions(+), 230 deletions(-) diff --git a/examples/create_directory.luau b/examples/create_directory.luau index e647f3a79..8c587822a 100644 --- a/examples/create_directory.luau +++ b/examples/create_directory.luau @@ -5,7 +5,7 @@ local system = require("@std/system") local tmpdir = system.tmpdir() local directory = path.join(tmpdir, "example_dir") -fs.createdirectory(directory, { makeparents = true }) +fs.createDirectory(directory, { makeParents = true }) if fs.exists(directory) and fs.type(directory) == "dir" then print("Directory successfully created") @@ -13,7 +13,7 @@ else print("Failed to create directory") end -fs.removedirectory(directory) +fs.removeDirectory(directory) if not fs.exists(directory) then print("Directory successfully removed") diff --git a/examples/directories.luau b/examples/directories.luau index 932041e7c..41afa4012 100644 --- a/examples/directories.luau +++ b/examples/directories.luau @@ -1,5 +1,5 @@ local fs = require("@std/fs") -for _, file in fs.listdirectory("./examples") do +for _, file in fs.listDirectory("./examples") do print(`Example {file.name} is a {file.type}`) end diff --git a/examples/walk_directory.luau b/examples/walk_directory.luau index fc0638ed3..a97889f46 100644 --- a/examples/walk_directory.luau +++ b/examples/walk_directory.luau @@ -7,7 +7,7 @@ local baseDir = path.join(tmpdir, "walk_directory_example") -- Setup: create a directory structure if not fslib.exists(baseDir) then - fslib.createdirectory(baseDir, { makeparents = true }) + fslib.createDirectory(baseDir, { makeParents = true }) end local subdirs = { @@ -17,7 +17,7 @@ local subdirs = { for _, subdir in subdirs do local fullPath = path.join(baseDir, subdir) - fslib.createdirectory(fullPath, { makeparents = true }) + fslib.createDirectory(fullPath, { makeParents = true }) end local files = { @@ -28,7 +28,7 @@ local files = { for _, file in files do local fullPath = path.join(baseDir, file) - fslib.writestringtofile(fullPath, "hello lute") + fslib.writeStringToFile(fullPath, "hello lute") end -- Walk the directory recursively @@ -41,4 +41,4 @@ while walker do end -- Cleanup: remove the created directory structure -fslib.removedirectory(baseDir, { recursive = true }) +fslib.removeDirectory(baseDir, { recursive = true }) diff --git a/examples/watch_directory.luau b/examples/watch_directory.luau index 7db44daea..5530e6887 100644 --- a/examples/watch_directory.luau +++ b/examples/watch_directory.luau @@ -15,7 +15,7 @@ end local watcher = fs.watch(tmpdir) -- Trigger a file change event -fs.writestringtofile(watched, "x") +fs.writeStringToFile(watched, "x") -- Poll the iterator with a timeout of 2 seconds local event diff --git a/lute/cli/commands/doc/init.luau b/lute/cli/commands/doc/init.luau index c01781245..c9047ca02 100644 --- a/lute/cli/commands/doc/init.luau +++ b/lute/cli/commands/doc/init.luau @@ -227,7 +227,7 @@ local function parseModule( module: path.Pathlike, filePath: path.Pathlike ): { { name: string, signature: string, doc: string? } } - local content = fs.readfiletostring(filePath) + local content = fs.readFileToString(filePath) local lines = string.split(content, "\n") local moduleDefinitions = {} @@ -261,16 +261,16 @@ local function processDirectory( libraryPath: path.Pathlike, requireLibraryAlias: string ) - fs.createdirectory(documentationPath, { makeparents = true }) + fs.createDirectory(documentationPath, { makeParents = true }) - local entries = fs.listdirectory(modulePath) + local entries = fs.listDirectory(modulePath) for _, entry in entries do local entryPath = path.join(modulePath, entry.name) local docPath = path.join(documentationPath, entry.name) if entry.type == "dir" then - fs.createdirectory(docPath, { makeparents = true }) + fs.createDirectory(docPath, { makeParents = true }) processDirectory(entryPath, docPath, libraryPath, requireLibraryAlias) elseif entry.type == "file" and string.find(entry.name, "%.luau$") then local moduleName = string.gsub(entry.name, "%.luau$", "") @@ -292,7 +292,7 @@ local function processDirectory( local definitions = parseModule(moduleAlias, entryPath) local markdown = generateMarkdown(moduleName, definitions, entryPath, libraryPath, requireLibraryAlias) - fs.writestringtofile(documentationFilePath, markdown) + fs.writeStringToFile(documentationFilePath, markdown) print(`Generated {tostring(documentationFilePath)}`) end @@ -303,7 +303,7 @@ type Module = { name: string, isDir: boolean } -- Helper to generate index with table of children local function generateLibraryIndex(title: string, alias: string, modulePath: path.Pathlike): string - local entries = fs.listdirectory(modulePath) + local entries = fs.listDirectory(modulePath) local modules: { Module } = {} for _, entry in entries do @@ -346,7 +346,7 @@ end local function generateAllDocs(outputDir: path.Path, modulePaths: { string }): () -- Create top-level reference index page local referenceBasePath = path.join(outputDir, "reference") - fs.createdirectory(referenceBasePath, { makeparents = true }) + fs.createDirectory(referenceBasePath, { makeParents = true }) -- Generate reference index with frontmatter for sidebar ordering local referenceIndex = [[--- @@ -378,7 +378,7 @@ API reference documentation for Lute's built-in libraries. -- Generate module index with table of children local moduleIndex = generateLibraryIndex(libraryAlias, libraryAlias, absModulePath) - fs.writestringtofile(path.join(documentationPath, "index.md"), moduleIndex) + fs.writeStringToFile(path.join(documentationPath, "index.md"), moduleIndex) -- Add to the reference index with the correct alias referenceIndex ..= "\n\n- [" .. libraryAlias .. "](./" .. libraryAlias .. "/index.md)" @@ -391,7 +391,7 @@ API reference documentation for Lute's built-in libraries. end end - fs.writestringtofile(path.join(referenceBasePath, "index.md"), referenceIndex) + fs.writeStringToFile(path.join(referenceBasePath, "index.md"), referenceIndex) end local function main(...: string) diff --git a/lute/cli/commands/lib/parseIgnores.luau b/lute/cli/commands/lib/parseIgnores.luau index 681d043e7..2dc3c79dd 100644 --- a/lute/cli/commands/lib/parseIgnores.luau +++ b/lute/cli/commands/lib/parseIgnores.luau @@ -135,7 +135,7 @@ function parseIgnores.parseIgnoreContents(location: string, contents: string | { end function parseIgnores.parseGitignore(filepath: path.Path): GitignoreData - local contents = fs.readfiletostring(path.format(filepath)) + local contents = fs.readFileToString(path.format(filepath)) local parentDirectory = if #filepath.parts > 0 then path.dirname(filepath) else nil assert(parentDirectory) return parseIgnores.parseIgnoreContents(parentDirectory, contents) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 1cab9c58f..6bc8751b6 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -141,7 +141,7 @@ local function loadLintRules( registerRule(rule, config, rootLocation, rules, ruleConfigs, ruleIgnores) end else - local entries = fs.listdirectory(currentPath) + local entries = fs.listDirectory(currentPath) local childPaths = tableext.map(entries, function(entry) return pathLib.format(pathLib.join(currentPathObj, entry.name)) end) @@ -388,7 +388,7 @@ local function lintFile( if VERBOSE then print(`Reading input file '{inputFilePath}'`) end - local fileContent = fs.readfiletostring(inputFilePath) + local fileContent = fs.readFileToString(inputFilePath) local violations = lintString(fileContent, lintRules, inputFilePath, configData) @@ -418,7 +418,7 @@ local function lintFile( local newContent = applySuggestedFixes(fileContent, fixes) - fs.writestringtofile(inputFilePath, newContent) + fs.writeStringToFile(inputFilePath, newContent) return lintString(newContent, lintRules, inputFilePath, configData) end diff --git a/lute/cli/commands/lint/printer.luau b/lute/cli/commands/lint/printer.luau index adeafd98e..a085c387f 100644 --- a/lute/cli/commands/lint/printer.luau +++ b/lute/cli/commands/lint/printer.luau @@ -94,7 +94,7 @@ function printerLib.printLintsWithSource(lints: { types.LintViolation }, sourceC end function printerLib.printLintsWithPath(lints: { types.LintViolation }, sourcePath: path.Path): () - printerLib.printLintsWithSource(lints, fs.readfiletostring(sourcePath)) + printerLib.printLintsWithSource(lints, fs.readFileToString(sourcePath)) end return table.freeze(printerLib) diff --git a/lute/cli/commands/new/init.luau b/lute/cli/commands/new/init.luau index 2bf7f87fe..01883f8ad 100644 --- a/lute/cli/commands/new/init.luau +++ b/lute/cli/commands/new/init.luau @@ -58,24 +58,24 @@ local function main(...: string) end end - fs.createdirectory(projectPath) + fs.createDirectory(projectPath) local mainScript = path.join(projectPath, "main.luau") local testScript = path.join(projectPath, "main.test.luau") - fs.writestringtofile( + fs.writeStringToFile( mainScript, [[print("Hello, world!") ]] ) - fs.writestringtofile( + fs.writeStringToFile( testScript, [[local _test = require("@std/test") ]] ) - fs.writestringtofile(path.join(projectPath, ".config.luau"), typedefs.generateConfigDotLuau("strict")) + fs.writeStringToFile(path.join(projectPath, ".config.luau"), typedefs.generateConfigDotLuau("strict")) print(`Created project '{projectName}'`) print("") diff --git a/lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau b/lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau index 9a64160ba..d625ee241 100644 --- a/lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau +++ b/lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau @@ -18,12 +18,12 @@ end local function writestringtofile(path: string, content: string): () local parentPath = getParentPath(path) - fs.createdirectory(parentPath, { makeparents = true }) - return fs.writestringtofile(path, content) + fs.createDirectory(parentPath, { makeparents = true }) + return fs.writeStringToFile(path, content) end function fsUtils.readfiletostring(path: string): string - return fs.readfiletostring(path) + return fs.readFileToString(path) end function fsUtils.writestringtofile(path: string, content: string): () diff --git a/lute/cli/commands/setup/init.luau b/lute/cli/commands/setup/init.luau index 8f6d70b1f..3550dba18 100644 --- a/lute/cli/commands/setup/init.luau +++ b/lute/cli/commands/setup/init.luau @@ -30,21 +30,21 @@ end -- TODO we're assuming the files are completely unmodified, but we really shouldn't do that. print(`Writing definitions at {BASE_PATH_PRETTY}`) if fs.exists(BASE_PATH) then - fs.removedirectory(BASE_PATH, { recursive = true }) + fs.removeDirectory(BASE_PATH, { recursive = true }) end -fs.createdirectory(BASE_PATH, { makeparents = true }) +fs.createDirectory(BASE_PATH, { makeParents = true }) for key, value in definitions :: { [string]: string } do local filePath = path.join(BASE_PATH, key) - fs.createdirectory(path.dirname(filePath), { makeparents = true }) - fs.writestringtofile(filePath, value) + fs.createDirectory(path.dirname(filePath), { makeParents = true }) + fs.writeStringToFile(filePath, value) end local typedefsLuauRcPath = path.join(BASE_PATH, ".luaurc") -- LUAUFIX. Without an annotation on typedefs.LUAURC_TEMPLATE, we arent able to figure out the type of TEMPLATE -- If that table was declared in this file, this typechecks, but if it's in a different one, we'll get a type error here -fs.writestringtofile(typedefsLuauRcPath, json.serialize(TEMPLATE, true)) +fs.writeStringToFile(typedefsLuauRcPath, json.serialize(TEMPLATE, true)) print(`Successfully wrote type definition files to {BASE_PATH_PRETTY}`) @@ -57,7 +57,7 @@ local rcFile = nil print(`Writing luaurc file at {luauRcPath}`) if fs.exists(luauRcPath) then - local contents = fs.readfiletostring(luauRcPath) + local contents = fs.readFileToString(luauRcPath) rcFile = json.deserialize(contents) :: any rcFile.aliases = rcFile.aliases or {} @@ -67,6 +67,6 @@ if fs.exists(luauRcPath) then else rcFile = TEMPLATE end -fs.writestringtofile(luauRcPath, json.serialize(rcFile, true)) +fs.writeStringToFile(luauRcPath, json.serialize(rcFile, true)) print(`Successfully wrote luaurc file to {luauRcPath}`) diff --git a/lute/cli/commands/test/snap/runner.luau b/lute/cli/commands/test/snap/runner.luau index 9e5eaa0ca..6f8623653 100644 --- a/lute/cli/commands/test/snap/runner.luau +++ b/lute/cli/commands/test/snap/runner.luau @@ -63,7 +63,7 @@ function runner.runsnapshots(files: { path.Path }): snapty.snapresult continue end - local expected = fs.readfiletostring(artifactpath) + local expected = fs.readFileToString(artifactpath) if actual == expected then table.insert(result.passed, { filepath = filepath, artifactpath = artifactpath }) diff --git a/lute/cli/commands/test/snap/updater.luau b/lute/cli/commands/test/snap/updater.luau index 7b9473010..79cd4f614 100644 --- a/lute/cli/commands/test/snap/updater.luau +++ b/lute/cli/commands/test/snap/updater.luau @@ -9,7 +9,7 @@ local strext = require("@std/stringext") -- Write content directly to a .snap artifact file. -- Used both for creating new snapshots and updating existing ones. local function writeSnapshot(snappath: string, content: string): boolean - local ok = pcall(fs.writestringtofile, snappath, content) + local ok = pcall(fs.writeStringToFile, snappath, content) return ok end diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index a15b56ef2..46bfbd45f 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -101,7 +101,7 @@ local function applyMigration( print(`Applying to {pathStr}`) - local source = fs.readfiletostring(pathStr) + local source = fs.readFileToString(pathStr) local parseresult = syntax.parse(source) @@ -127,9 +127,9 @@ local function applyMigration( elseif not dryRun then if outputFilePath then assert(outputFilePath ~= nil) - fs.writestringtofile(outputFilePath, toWrite) + fs.writeStringToFile(outputFilePath, toWrite) elseif toWrite ~= source then - fs.writestringtofile(pathStr, toWrite) + fs.writeStringToFile(pathStr, toWrite) end end end diff --git a/lute/std/libs/fs.luau b/lute/std/libs/fs.luau index e3d678be3..f1dda2040 100644 --- a/lute/std/libs/fs.luau +++ b/lute/std/libs/fs.luau @@ -24,7 +24,7 @@ type Path = pathlib.Path type win32path = win32paths.Path export type CreateDirectoryOptions = { - makeparents: boolean?, + makeParents: boolean?, } export type RemoveDirectoryOptions = { @@ -72,7 +72,7 @@ function fslib.link(src: Pathlike, dest: Pathlike): () return fs.link(pathlib.format(src), pathlib.format(dest)) end -function fslib.symboliclink(src: Pathlike, dest: Pathlike): () +function fslib.symbolicLink(src: Pathlike, dest: Pathlike): () return fs.symlink(pathlib.format(src), pathlib.format(dest)) end @@ -114,25 +114,25 @@ function fslib.copy(src: Pathlike, dest: Pathlike): () return fs.copy(pathlib.format(src), pathlib.format(dest)) end -function fslib.listdirectory(path: Pathlike): { DirectoryEntry } +function fslib.listDirectory(path: Pathlike): { DirectoryEntry } return fs.listdir(pathlib.format(path)) end -function fslib.readfiletostring(filepath: Pathlike): string +function fslib.readFileToString(filepath: Pathlike): string local f = fs.open(pathlib.format(filepath), "r") local contents = fs.read(f) fs.close(f) return contents end -function fslib.writestringtofile(filepath: Pathlike, contents: string): () +function fslib.writeStringToFile(filepath: Pathlike, contents: string): () local f = fs.open(pathlib.format(filepath), "w+") fs.write(f, contents) fs.close(f) end -function fslib.createdirectory(path: Pathlike, options: CreateDirectoryOptions?): () - if options and options.makeparents then +function fslib.createDirectory(path: Pathlike, options: CreateDirectoryOptions?): () + if options and options.makeParents then local parsed = pathlib.parse(path) local parts: { string } = parsed.parts @@ -158,15 +158,15 @@ function fslib.createdirectory(path: Pathlike, options: CreateDirectoryOptions?) end end -function fslib.removedirectory(path: Pathlike, options: RemoveDirectoryOptions?): () +function fslib.removeDirectory(path: Pathlike, options: RemoveDirectoryOptions?): () if options and options.recursive then -- Recursively delete all contents first if fslib.exists(path) and fslib.type(path) == "dir" then - local entries = fslib.listdirectory(path) + local entries = fslib.listDirectory(path) for _, entry in entries do local entryPath = pathlib.join(path, entry.name) if entry.type == "dir" then - fslib.removedirectory(entryPath, { recursive = true }) + fslib.removeDirectory(entryPath, { recursive = true }) else fslib.remove(entryPath) end @@ -191,7 +191,7 @@ function fslib.walk(path: Pathlike, options: WalkOptions?): () -> Path? end if fslib.type(current) == "dir" and recursive then - local entries = fslib.listdirectory(current) + local entries = fslib.listDirectory(current) for _, entry in entries do local fullPath = pathlib.join(current, entry.name) table.insert(queue, fullPath) diff --git a/tests/cli/doc.test.luau b/tests/cli/doc.test.luau index deeab0889..d9bb7d2a2 100644 --- a/tests/cli/doc.test.luau +++ b/tests/cli/doc.test.luau @@ -45,10 +45,10 @@ test.suite("LuteDoc", function(suite) suite:case("luteDocSpecifiedDirectory", function(assert) if fs.exists(tmpOutputDir) then - fs.removedirectory(tmpOutputDir, { recursive = true }) + fs.removeDirectory(tmpOutputDir, { recursive = true }) end - fs.createdirectory(tmpOutputDir) + fs.createDirectory(tmpOutputDir) local result = process.system(`{lutePath} doc -o {path.format(tmpOutputDir)} definitions lute/std/libs`) @@ -59,6 +59,6 @@ test.suite("LuteDoc", function(suite) assert.that(fs.exists(path.join(tmpOutputDir, "reference", "lute", "fs.md"))) assert.that(fs.exists(path.join(tmpOutputDir, "reference", "std", "fs.md"))) - fs.removedirectory(tmpOutputDir, { recursive = true }) + fs.removeDirectory(tmpOutputDir, { recursive = true }) end) end) diff --git a/tests/cli/lib/files.test.luau b/tests/cli/lib/files.test.luau index f5510a8b7..9c186a1f9 100644 --- a/tests/cli/lib/files.test.luau +++ b/tests/cli/lib/files.test.luau @@ -10,26 +10,26 @@ local testPath = path.join(tmpdir, "gitignore_test") test.suite("CliLibFiles", function(suite) suite:beforeeach(function() if fs.exists(testPath) then - fs.removedirectory(testPath, { recursive = true }) + fs.removeDirectory(testPath, { recursive = true }) end end) suite:case("parseSimpleFilePattern", function(assert) -- Setup - fs.createdirectory(testPath) + fs.createDirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") - fs.writestringtofile(gitignorePath, "*.lua\n") + fs.writeStringToFile(gitignorePath, "*.lua\n") local shouldIgnorePath = path.join(testPath, "debug.lua") - fs.writestringtofile(shouldIgnorePath, "") + fs.writeStringToFile(shouldIgnorePath, "") local shouldNotIgnorePath = path.join(testPath, "readme.luau") - fs.writestringtofile(shouldNotIgnorePath, "") + fs.writeStringToFile(shouldNotIgnorePath, "") local shouldNotIgnoreDirectory = path.join(testPath, "lua") - fs.createdirectory(shouldNotIgnoreDirectory) + fs.createDirectory(shouldNotIgnoreDirectory) local shouldNotIgnoreNestedPath = path.join(shouldNotIgnoreDirectory, "readme.luau") - fs.writestringtofile(shouldNotIgnoreNestedPath, "") + fs.writeStringToFile(shouldNotIgnoreNestedPath, "") -- Do local results = files.getSourceFiles({ path.format(testPath) }) @@ -44,27 +44,27 @@ test.suite("CliLibFiles", function(suite) assert.eq(resultPaths[path.format(shouldNotIgnoreNestedPath)], true) -- Teardown - fs.removedirectory(testPath, { recursive = true }) + fs.removeDirectory(testPath, { recursive = true }) end) suite:case("parseDirectoryPattern", function(assert) -- Setup - fs.createdirectory(testPath) + fs.createDirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") - fs.writestringtofile(gitignorePath, "node_modules/\n") + fs.writeStringToFile(gitignorePath, "node_modules/\n") local dirPath = path.join(testPath, "node_modules") - fs.createdirectory(dirPath) + fs.createDirectory(dirPath) local shouldIgnoreFile = path.join(dirPath, "package.luau") - fs.writestringtofile(shouldIgnoreFile, "") + fs.writeStringToFile(shouldIgnoreFile, "") local nestedDirPath = path.join(testPath, "a", "node_modules") - fs.createdirectory(nestedDirPath, { makeparents = true }) + fs.createDirectory(nestedDirPath, { makeParents = true }) local shouldIgnoreNestedFile = path.join(nestedDirPath, "nested_package.luau") - fs.writestringtofile(shouldIgnoreNestedFile, "") + fs.writeStringToFile(shouldIgnoreNestedFile, "") local shouldNotIgnoreFile = path.join(testPath, "node_modules.luau") - fs.writestringtofile(shouldNotIgnoreFile, "") + fs.writeStringToFile(shouldNotIgnoreFile, "") -- Do local results = files.getSourceFiles({ path.format(testPath) }) @@ -79,20 +79,20 @@ test.suite("CliLibFiles", function(suite) assert.eq(resultPaths[path.format(shouldNotIgnoreFile)], true) -- Teardown - fs.removedirectory(testPath, { recursive = true }) + fs.removeDirectory(testPath, { recursive = true }) end) suite:case("parseNegationPattern", function(assert) -- Setup - fs.createdirectory(testPath) + fs.createDirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") - fs.writestringtofile(gitignorePath, "*.lua\n!important.lua\n") + fs.writeStringToFile(gitignorePath, "*.lua\n!important.lua\n") local shouldIgnorePath = path.join(testPath, "debug.lua") - fs.writestringtofile(shouldIgnorePath, "") + fs.writeStringToFile(shouldIgnorePath, "") local shouldNotIgnorePath = path.join(testPath, "important.lua") - fs.writestringtofile(shouldNotIgnorePath, "") + fs.writeStringToFile(shouldNotIgnorePath, "") -- Do local results = files.getSourceFiles({ path.format(testPath) }) @@ -106,23 +106,23 @@ test.suite("CliLibFiles", function(suite) assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) -- Teardown - fs.removedirectory(testPath, { recursive = true }) + fs.removeDirectory(testPath, { recursive = true }) end) suite:case("parseLeadingDoubleAsteriskPattern", function(assert) -- Setup - fs.createdirectory(testPath) + fs.createDirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") - fs.writestringtofile(gitignorePath, "**/temp/foo\n") + fs.writeStringToFile(gitignorePath, "**/temp/foo\n") local subdir = path.join(testPath, "src", "temp", "foo") - fs.createdirectory(subdir, { makeparents = true }) + fs.createDirectory(subdir, { makeParents = true }) local shouldIgnorePath = path.join(subdir, "template.luau") - fs.writestringtofile(shouldIgnorePath, "") + fs.writeStringToFile(shouldIgnorePath, "") local shouldntIgnorePath = path.join(testPath, "src", "temp", "hello.luau") - fs.writestringtofile(shouldntIgnorePath, "") + fs.writeStringToFile(shouldntIgnorePath, "") -- Do local results = files.getSourceFiles({ path.format(testPath) }) @@ -136,26 +136,26 @@ test.suite("CliLibFiles", function(suite) assert.eq(resultPaths[path.format(shouldntIgnorePath)], true) -- Teardown - fs.removedirectory(testPath, { recursive = true }) + fs.removeDirectory(testPath, { recursive = true }) end) suite:case("parseTrailingDoubleAsteriskPattern", function(assert) -- Setup - fs.createdirectory(testPath) + fs.createDirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") - fs.writestringtofile(gitignorePath, "temp/foo/**\n") + fs.writeStringToFile(gitignorePath, "temp/foo/**\n") local subdir = path.join(testPath, "temp", "foo", "src") - fs.createdirectory(subdir, { makeparents = true }) + fs.createDirectory(subdir, { makeParents = true }) local shouldIgnorePath = path.join(subdir, "template.luau") - fs.writestringtofile(shouldIgnorePath, "") + fs.writeStringToFile(shouldIgnorePath, "") local shouldIgnorePath2 = path.join(testPath, "temp", "foo", "template.luau") - fs.writestringtofile(shouldIgnorePath2, "") + fs.writeStringToFile(shouldIgnorePath2, "") local shouldntIgnorePath = path.join(testPath, "temp", "hello.luau") - fs.writestringtofile(shouldntIgnorePath, "") + fs.writeStringToFile(shouldntIgnorePath, "") -- Do local results = files.getSourceFiles({ path.format(testPath) }) @@ -170,29 +170,29 @@ test.suite("CliLibFiles", function(suite) assert.eq(resultPaths[path.format(shouldntIgnorePath)], true) -- Teardown - fs.removedirectory(testPath, { recursive = true }) + fs.removeDirectory(testPath, { recursive = true }) end) suite:case("parseMiddleDoubleAsteriskPattern", function(assert) -- Setup - fs.createdirectory(testPath) + fs.createDirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") - fs.writestringtofile(gitignorePath, "temp/**/foo\n") + fs.writeStringToFile(gitignorePath, "temp/**/foo\n") local subdir = path.join(testPath, "temp", "src", "foo") - fs.createdirectory(subdir, { makeparents = true }) + fs.createDirectory(subdir, { makeParents = true }) local shouldIgnorePath = path.join(subdir, "template.luau") - fs.writestringtofile(shouldIgnorePath, "") + fs.writeStringToFile(shouldIgnorePath, "") local subdir2 = path.join(testPath, "temp", "src", "test", "foo") - fs.createdirectory(subdir2, { makeparents = true }) + fs.createDirectory(subdir2, { makeParents = true }) local shouldIgnorePath2 = path.join(subdir2, "template.luau") - fs.writestringtofile(shouldIgnorePath2, "") + fs.writeStringToFile(shouldIgnorePath2, "") local shouldntIgnorePath = path.join(testPath, "temp", "hello.luau") - fs.writestringtofile(shouldntIgnorePath, "") + fs.writeStringToFile(shouldntIgnorePath, "") -- Do local results = files.getSourceFiles({ path.format(testPath) }) @@ -207,24 +207,24 @@ test.suite("CliLibFiles", function(suite) assert.eq(resultPaths[path.format(shouldntIgnorePath)], true) -- Teardown - fs.removedirectory(testPath, { recursive = true }) + fs.removeDirectory(testPath, { recursive = true }) end) suite:case("parseRootedPattern", function(assert) -- Setup - fs.createdirectory(testPath) + fs.createDirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") - fs.writestringtofile(gitignorePath, "/build\n") + fs.writeStringToFile(gitignorePath, "/build\n") - fs.createdirectory(path.join(testPath, "build"), { makeparents = true }) + fs.createDirectory(path.join(testPath, "build"), { makeParents = true }) local shouldIgnorePath = path.join(testPath, "build", "foo.luau") - fs.writestringtofile(shouldIgnorePath, "") + fs.writeStringToFile(shouldIgnorePath, "") - fs.createdirectory(path.join(testPath, "src", "build"), { makeparents = true }) + fs.createDirectory(path.join(testPath, "src", "build"), { makeParents = true }) local shouldNotIgnorePath = path.join(testPath, "src", "build", "bar.luau") - fs.writestringtofile(shouldNotIgnorePath, "") + fs.writeStringToFile(shouldNotIgnorePath, "") -- Do local results = files.getSourceFiles({ path.format(testPath) }) @@ -238,23 +238,23 @@ test.suite("CliLibFiles", function(suite) assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) -- Teardown - fs.removedirectory(testPath, { recursive = true }) + fs.removeDirectory(testPath, { recursive = true }) end) suite:case("parseQuestionMarkPattern", function(assert) -- Setup - fs.createdirectory(testPath) + fs.createDirectory(testPath) local gitignorePath = path.join(testPath, ".gitignore") - fs.writestringtofile(gitignorePath, "file?.luau\n") + fs.writeStringToFile(gitignorePath, "file?.luau\n") local shouldIgnorePath1 = path.join(testPath, "file1.luau") - fs.writestringtofile(shouldIgnorePath1, "") + fs.writeStringToFile(shouldIgnorePath1, "") local shouldIgnorePath2 = path.join(testPath, "fileA.luau") - fs.writestringtofile(shouldIgnorePath2, "") + fs.writeStringToFile(shouldIgnorePath2, "") local shouldNotIgnorePath = path.join(testPath, "file10.luau") - fs.writestringtofile(shouldNotIgnorePath, "") + fs.writeStringToFile(shouldNotIgnorePath, "") -- Do local results = files.getSourceFiles({ path.format(testPath) }) @@ -268,6 +268,6 @@ test.suite("CliLibFiles", function(suite) assert.eq(resultPaths[path.format(shouldNotIgnorePath)], true) -- Teardown - fs.removedirectory(testPath, { recursive = true }) + fs.removeDirectory(testPath, { recursive = true }) end) end) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 7f6d271b4..6e00c6375 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -52,7 +52,7 @@ local function lintTestHelper(assert: testTypes.Asserts, params: lintTestParams) -- Setup -- Create a file that violates the rule local violatorPath = path.format(path.join(tmpDir, "violator.luau")) - fs.writestringtofile(violatorPath, params.content) + fs.writeStringToFile(violatorPath, params.content) -- Do local lintArgs = { lutePath, "lint" } @@ -64,7 +64,7 @@ local function lintTestHelper(assert: testTypes.Asserts, params: lintTestParams) table.insert(lintArgs, params.rulePath) elseif params.customRuleContents then local customRulePath = path.format(path.join(tmpDir, "custom_rule.luau")) - fs.writestringtofile(customRulePath, params.customRuleContents) + fs.writeStringToFile(customRulePath, params.customRuleContents) table.insert(lintArgs, "-r") table.insert(lintArgs, customRulePath) end @@ -80,7 +80,7 @@ local function lintTestHelper(assert: testTypes.Asserts, params: lintTestParams) -- Create a config file if specified if params.configContents then local configPath = path.format(path.join(tmpDir, ".config.luau")) - fs.writestringtofile(configPath, params.configContents) + fs.writeStringToFile(configPath, params.configContents) table.insert(lintArgs, "-c") table.insert(lintArgs, configPath) end @@ -97,7 +97,7 @@ local function lintTestHelper(assert: testTypes.Asserts, params: lintTestParams) assert.eq(result.exitcode, params.expectedExitCode) if params.expectedAutofixContents then - local fixedContents = fs.readfiletostring(violatorPath) + local fixedContents = fs.readFileToString(violatorPath) assert.eq(fixedContents, params.expectedAutofixContents) end @@ -118,12 +118,12 @@ test.suite("lute lint", function(suite) suite:beforeeach(function() -- A test that creates rulesDir which fails won't cleanup after itself if fs.exists(rulesDir) then - fs.removedirectory(rulesDir, { recursive = true }) + fs.removeDirectory(rulesDir, { recursive = true }) end local modulePath = path.join(tmpDir, "module") if fs.exists(modulePath) then - fs.removedirectory(modulePath, { recursive = true }) + fs.removeDirectory(modulePath, { recursive = true }) end end) @@ -337,15 +337,15 @@ end suite:case("luteLintCustomRulesDir", function(assert) -- Setup - fs.createdirectory(rulesDir) + fs.createDirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau -- Because the default rules require types using a relative path, we copy from examples, which uses an absolute require path fs.copy(path.join("examples", "lints", "almost_swapped.luau"), path.join(rulesDir, "almost_swapped.luau")) -- Copy divide_by_zero to rulesDir/divide_by_zero/init.luau local divideByZeroDir = path.join(rulesDir, "divide_by_zero") - fs.createdirectory(divideByZeroDir) + fs.createDirectory(divideByZeroDir) fs.copy(path.join("examples", "lints", "divide_by_zero.luau"), path.join(divideByZeroDir, "init.luau")) - fs.writestringtofile( + fs.writeStringToFile( path.join(rulesDir, "bogus.txt"), [[ not a luau file @@ -389,16 +389,16 @@ violator.luau:3:1-4:6 ── }, }) - fs.removedirectory(rulesDir, { recursive = true }) + fs.removeDirectory(rulesDir, { recursive = true }) end) suite:case("luteLintMultipleRulesButOneErrors", function(assert) -- Setup - fs.createdirectory(rulesDir) + fs.createDirectory(rulesDir) -- Copy almostSwapped.luau to rulesDir/almostSwapped.luau fs.copy(path.join("examples", "lints", "almost_swapped.luau"), path.join(rulesDir, "almost_swapped.luau")) -- Create a rule that fails - fs.writestringtofile( + fs.writeStringToFile( path.join(rulesDir, "erroring_rule.luau"), [[ return table.freeze({ @@ -441,14 +441,14 @@ b = a ) -- Teardown - fs.removedirectory(rulesDir, { recursive = true }) + fs.removeDirectory(rulesDir, { recursive = true }) end) suite:case("luteLintMultipleInputFiles", function(assert) -- Setup -- Create multiple files that violate the rule local violatorPath1 = path.format(path.join(tmpDir, "violator1.luau")) - fs.writestringtofile( + fs.writeStringToFile( violatorPath1, [[ local x = 1 / 0 @@ -459,9 +459,9 @@ b = a ) local modulePath = path.join(tmpDir, "module") - fs.createdirectory(modulePath) + fs.createDirectory(modulePath) local violatorPath2 = path.format(path.join(modulePath, "violator2.lua")) - fs.writestringtofile( + fs.writeStringToFile( violatorPath2, [[ local y = 10 / 0 @@ -472,7 +472,7 @@ x = y y = x ]] ) - fs.writestringtofile(path.format(path.join(modulePath, "not_luau.txt")), "blablabla") + fs.writeStringToFile(path.format(path.join(modulePath, "not_luau.txt")), "blablabla") -- Do -- Run the transformer on the transformee @@ -525,17 +525,17 @@ violator2.lua:5:1-6:6 ── -- Teardown fs.remove(violatorPath1) - fs.removedirectory(modulePath, { recursive = true }) + fs.removeDirectory(modulePath, { recursive = true }) end) suite:case("luteLintMultipleFilesWithErrorsRecovers", function(assert) -- Setup -- Create multiple files that violate the rule local lintee = path.format(path.join(tmpDir, "lintee.luau")) - fs.writestringtofile(lintee, "bogus") + fs.writeStringToFile(lintee, "bogus") local violatorPath = path.format(path.join(tmpDir, "violator1.luau")) - fs.writestringtofile( + fs.writeStringToFile( violatorPath, [[ local x = 1 / 0 @@ -972,22 +972,22 @@ local x = 1 / 0 -- Setup -- Create a module directory with a .gitignore that ignores one file local modulePath = path.join(tmpDir, "module") - fs.createdirectory(modulePath) + fs.createDirectory(modulePath) local violatorPath1 = path.format(path.join(modulePath, "violator1.luau")) - fs.writestringtofile( + fs.writeStringToFile( violatorPath1, [[ local x = 10 / 0 ]] ) local violatorPath2 = path.format(path.join(modulePath, "violator2.luau")) - fs.writestringtofile( + fs.writeStringToFile( violatorPath2, [[ local y = 10 / 0 ]] ) - fs.writestringtofile(path.format(path.join(modulePath, ".gitignore")), "violator2.luau\n") + fs.writeStringToFile(path.format(path.join(modulePath, ".gitignore")), "violator2.luau\n") -- Do -- Run the transformer on the transformee @@ -1016,7 +1016,7 @@ violator2.lua:1:11-17 ── assert.strnotcontains(result.stdout, expected) -- Teardown - fs.removedirectory(modulePath, { recursive = true }) + fs.removeDirectory(modulePath, { recursive = true }) end) suite:case("luteLintReportDirective", function(assert) @@ -2548,7 +2548,7 @@ return { suite:case("luteLintAccountsForConfiguredIgnoresConfigPassedAsArg", function(assert) local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + fs.createDirectory(testSubDir) local ignoredFile = path.join(testSubDir, "ignored.luau") local notIgnoredFile = path.join(testSubDir, "valid.luau") @@ -2557,8 +2557,8 @@ return { b = a c = 1 / 0 ]] - fs.writestringtofile(ignoredFile, violatingContents) - fs.writestringtofile(notIgnoredFile, violatingContents) + fs.writeStringToFile(ignoredFile, violatingContents) + fs.writeStringToFile(notIgnoredFile, violatingContents) local configFile = path.join(testSubDir, ".config.luau") local configContents = [[ @@ -2570,7 +2570,7 @@ return { } } ]] - fs.writestringtofile(configFile, configContents) + fs.writeStringToFile(configFile, configContents) local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) assert.eq(result.exitcode, 1) -- errors but ONLY from valid.luau @@ -2580,7 +2580,7 @@ return { suite:case("luteLintAccountsForConfiguredIgnoresConfigAutoDetected", function(assert) local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + fs.createDirectory(testSubDir) local ignoredFile = path.join(testSubDir, "ignored.luau") @@ -2589,7 +2589,7 @@ return { b = a c = 1 / 0 ]] - fs.writestringtofile(ignoredFile, violatingContents) + fs.writeStringToFile(ignoredFile, violatingContents) local configFile = path.join(testSubDir, ".config.luau") local configContents = [[ @@ -2601,7 +2601,7 @@ return { } } ]] - fs.writestringtofile(configFile, configContents) + fs.writeStringToFile(configFile, configContents) local result = process.run({ lutePath, "lint", path.format(testSubDir) }, { cwd = path.format(testSubDir), @@ -2613,16 +2613,16 @@ return { suite:case("luteLintConfiguredIgnoresSupplementNotOverrideExistingGitignores", function(assert) local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + fs.createDirectory(testSubDir) local ignoredFile = path.join(testSubDir, "ignored.luau") local notIgnoredFile = path.join(testSubDir, "valid.luau") local violatingContents = "c = 1 / 0" - fs.writestringtofile(ignoredFile, violatingContents) - fs.writestringtofile(notIgnoredFile, violatingContents) + fs.writeStringToFile(ignoredFile, violatingContents) + fs.writeStringToFile(notIgnoredFile, violatingContents) local gitIgnore = path.join(testSubDir, ".gitignore") -- gitignore `valid.luau` - fs.writestringtofile( + fs.writeStringToFile( gitIgnore, [[ **/valid.luau @@ -2639,7 +2639,7 @@ return { } } ]] - fs.writestringtofile(configFile, configContents) + fs.writeStringToFile(configFile, configContents) local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) assert.eq(result.exitcode, 0) -- no errors bc we ignore BOTH files in the dir @@ -2807,11 +2807,11 @@ local t = { [0] = 1 } suite:case("luteLintSupportsConfiguredDirectoryIgnores", function(assert) local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + fs.createDirectory(testSubDir) -- Create a nested directory structure with violating files local nestedDir = path.join(testSubDir, "shouldBeIgnored") - fs.createdirectory(nestedDir) + fs.createDirectory(nestedDir) local file1 = path.join(nestedDir, "file1.luau") local file2 = path.join(nestedDir, "file2.luau") @@ -2822,9 +2822,9 @@ local t = { [0] = 1 } b = a c = 1 / 0 ]] - fs.writestringtofile(file1, violatingContents) - fs.writestringtofile(file2, violatingContents) - fs.writestringtofile(file3, violatingContents) + fs.writeStringToFile(file1, violatingContents) + fs.writeStringToFile(file2, violatingContents) + fs.writeStringToFile(file3, violatingContents) local configFile = path.join(testSubDir, ".config.luau") local configContents = [[ @@ -2836,7 +2836,7 @@ local t = { [0] = 1 } } } ]] - fs.writestringtofile(configFile, configContents) + fs.writeStringToFile(configFile, configContents) local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) assert.eq(result.exitcode, 1) -- errors but ONLY from notIgnored.luau @@ -2853,7 +2853,7 @@ local t = { [0] = 1 } suite:case("luteLintHandlesIgnoresWithRelativeConfigPath", function(assert) local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + fs.createDirectory(testSubDir) local ignoredFile = path.join(testSubDir, "ignored.luau") local notIgnoredFile = path.join(testSubDir, "valid.luau") @@ -2862,8 +2862,8 @@ local t = { [0] = 1 } b = a c = 1 / 0 ]] - fs.writestringtofile(ignoredFile, violatingContents) - fs.writestringtofile(notIgnoredFile, violatingContents) + fs.writeStringToFile(ignoredFile, violatingContents) + fs.writeStringToFile(notIgnoredFile, violatingContents) local configFile = path.join(testSubDir, ".config.luau") local configContents = [[ @@ -2875,7 +2875,7 @@ local t = { [0] = 1 } } } ]] - fs.writestringtofile(configFile, configContents) + fs.writeStringToFile(configFile, configContents) -- Pass config as a relative path, setting cwd to ensure relative config path is correct local result = process.run({ lutePath, "lint", "-c", ".config.luau", "." }, { @@ -2888,7 +2888,7 @@ local t = { [0] = 1 } suite:case("luteLintHandlesIgnoresWithAbsoluteConfigPath", function(assert) local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + fs.createDirectory(testSubDir) local ignoredFile = path.join(testSubDir, "ignored.luau") local notIgnoredFile = path.join(testSubDir, "valid.luau") @@ -2897,8 +2897,8 @@ local t = { [0] = 1 } b = a c = 1 / 0 ]] - fs.writestringtofile(ignoredFile, violatingContents) - fs.writestringtofile(notIgnoredFile, violatingContents) + fs.writeStringToFile(ignoredFile, violatingContents) + fs.writeStringToFile(notIgnoredFile, violatingContents) local configFile = path.join(testSubDir, ".config.luau") local configContents = [[ @@ -2910,7 +2910,7 @@ local t = { [0] = 1 } } } ]] - fs.writestringtofile(configFile, configContents) + fs.writeStringToFile(configFile, configContents) -- Pass config as an absolute path, with cwd set to a different directory (homedir) local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) @@ -2968,10 +2968,10 @@ local t = { [0] = 1 } suite:case("luteLintAccountsForRuleSpecificIgnores", function(assert) local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + fs.createDirectory(testSubDir) local violatorFile = path.join(testSubDir, "violator.luau") - fs.writestringtofile( + fs.writeStringToFile( violatorFile, [[ local x = 1 / 0 @@ -2995,7 +2995,7 @@ return { } } ]] - fs.writestringtofile(configFile, configContents) + fs.writeStringToFile(configFile, configContents) -- assert that violations are present without config local result = process.run({ lutePath, "lint", path.format(violatorFile) }, { @@ -3017,10 +3017,10 @@ return { suite:case("luteLintAccountsForRuleSpecificDirectoryIgnores", function(assert) local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + fs.createDirectory(testSubDir) local ignoredDir = path.join(testSubDir, "ignoredDir") - fs.createdirectory(ignoredDir) + fs.createDirectory(ignoredDir) local ignoredFile = path.join(ignoredDir, "violator.luau") local notIgnoredFile = path.join(testSubDir, "nonIgnored.luau") @@ -3031,8 +3031,8 @@ local a, b a = b b = a ]] - fs.writestringtofile(ignoredFile, violatingContents) - fs.writestringtofile(notIgnoredFile, violatingContents) + fs.writeStringToFile(ignoredFile, violatingContents) + fs.writeStringToFile(notIgnoredFile, violatingContents) local configFile = path.join(testSubDir, ".config.luau") local configContents = [[ @@ -3048,7 +3048,7 @@ return { } } ]] - fs.writestringtofile(configFile, configContents) + fs.writeStringToFile(configFile, configContents) local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) assert.eq(result.exitcode, 1) @@ -3184,7 +3184,7 @@ return { ]] -- Create the config-provided rules (loaded via config.rulepaths, NOT via -r) local configuredAbsoluteRulePath = path.format(path.join(tmpDir, "configuredAbsRule.luau")) - fs.writestringtofile( + fs.writeStringToFile( configuredAbsoluteRulePath, (ruleTemplate :: any):format( "configuredAbsoluteRule", @@ -3193,7 +3193,7 @@ return { ) ) local configuredRelativePath = path.format(path.join(tmpDir, "configuredRelativeRule.luau")) - fs.writestringtofile( + fs.writeStringToFile( configuredRelativePath, (ruleTemplate :: any):format( "configuredRelativeRule", @@ -3248,7 +3248,7 @@ return { "lute lint properly loads rule-specific configuration for rules provided by config.rulepaths", function(assert) local testSubDir = path.join(tmpDir, "module") - fs.createdirectory(testSubDir) + fs.createDirectory(testSubDir) local configuredRulePath = path.join(testSubDir, "configuredRule.luau") local configuredRuleContents = [[ @@ -3267,7 +3267,7 @@ return { end } ]] - fs.writestringtofile(configuredRulePath, configuredRuleContents) + fs.writeStringToFile(configuredRulePath, configuredRuleContents) local configFile = path.join(testSubDir, ".config.luau") local configContents = ([[ @@ -3287,17 +3287,17 @@ return { } } ]]):format(path.format(configuredRulePath)) - fs.writestringtofile(configFile, configContents) + fs.writeStringToFile(configFile, configContents) local ignoredDir = path.join(testSubDir, "ignoredDir") local notIgnoredDir = path.join(testSubDir, "not_ignored") - fs.createdirectory(ignoredDir) - fs.createdirectory(notIgnoredDir) + fs.createDirectory(ignoredDir) + fs.createDirectory(notIgnoredDir) local ignoredFile = path.join(ignoredDir, "ignored.luau") local notIgnoredFile = path.join(notIgnoredDir, "valid.luau") local violatingContents = "print('hello world')" - fs.writestringtofile(ignoredFile, violatingContents) - fs.writestringtofile(notIgnoredFile, violatingContents) + fs.writeStringToFile(ignoredFile, violatingContents) + fs.writeStringToFile(notIgnoredFile, violatingContents) local result = process.run({ lutePath, diff --git a/tests/cli/loadmodule.test.luau b/tests/cli/loadmodule.test.luau index 454255569..b8e73ccc0 100644 --- a/tests/cli/loadmodule.test.luau +++ b/tests/cli/loadmodule.test.luau @@ -24,7 +24,7 @@ local testDirStr = path.format(testDir) test.suite("LuteCliRun", function(suite) suite:beforeall(function() if not fs.exists(testDirStr) then - fs.createdirectory(testDirStr) + fs.createDirectory(testDirStr) end end) @@ -52,7 +52,7 @@ test.suite("LuteCliRun", function(suite) suite:afterall(function() if fs.exists(testDirStr) then - fs.removedirectory(testDirStr) + fs.removeDirectory(testDirStr) end end) end) diff --git a/tests/cli/new.test.luau b/tests/cli/new.test.luau index 43e74ecc9..b9f5c072e 100644 --- a/tests/cli/new.test.luau +++ b/tests/cli/new.test.luau @@ -13,9 +13,9 @@ local projectDir = pathlib.join(workdir, projectName) test.suite("LuteNew", function(suite) suite:beforeeach(function() if fs.exists(workdir) then - fs.removedirectory(workdir, { recursive = true }) + fs.removeDirectory(workdir, { recursive = true }) end - fs.createdirectory(workdir) + fs.createDirectory(workdir) end) suite:case("createsProjectDirectory", function(assert) @@ -30,7 +30,7 @@ test.suite("LuteNew", function(suite) local mainPath = pathlib.join(workdir, projectName, "main.luau") assert.that(fs.exists(mainPath)) - local contents = fs.readfiletostring(mainPath) + local contents = fs.readFileToString(mainPath) assert.strcontains(contents, "Hello, world!") end) @@ -39,7 +39,7 @@ test.suite("LuteNew", function(suite) local testPath = pathlib.join(workdir, projectName, "main.test.luau") assert.that(fs.exists(testPath)) - local contents = fs.readfiletostring(testPath) + local contents = fs.readFileToString(testPath) assert.strcontains(contents, "@std/test") end) @@ -48,7 +48,7 @@ test.suite("LuteNew", function(suite) local configPath = pathlib.join(workdir, projectName, ".config.luau") assert.that(fs.exists(configPath)) - local contents = fs.readfiletostring(configPath) + local contents = fs.readFileToString(configPath) assert.strcontains(contents, "strict") end) @@ -67,7 +67,7 @@ test.suite("LuteNew", function(suite) end) suite:case("failsWhenDirectoryAlreadyExists", function(assert) - fs.createdirectory(pathlib.join(workdir, projectName)) + fs.createDirectory(pathlib.join(workdir, projectName)) local result = process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) diff --git a/tests/cli/run.test.luau b/tests/cli/run.test.luau index 1c935b76d..48515f93c 100644 --- a/tests/cli/run.test.luau +++ b/tests/cli/run.test.luau @@ -13,17 +13,17 @@ local lutePath = pathlib.format(process.execpath()) test.suite("LuteRun", function(suite) suite:beforeeach(function() if fs.exists(rundir) then - fs.removedirectory(rundir, { recursive = true }) + fs.removeDirectory(rundir, { recursive = true }) end end) suite:case("sameNamedFileAndDirectory", function(assert) -- Setup - fs.createdirectory(rundir) - fs.createdirectory(pathlib.join(rundir, "hello")) + fs.createDirectory(rundir) + fs.createDirectory(pathlib.join(rundir, "hello")) local helloFilePath = pathlib.join(rundir, "hello.luau") - fs.writestringtofile(helloFilePath, "print('Hello world')") + fs.writeStringToFile(helloFilePath, "print('Hello world')") -- Execute local result = process.run({ lutePath, pathlib.format(helloFilePath) }) diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 41de11b6a..37513298e 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -28,9 +28,9 @@ test.suite("FsSuite", function(suite) suite:case("writeStringAndReadfile", function(assert) local file = path.join(tmpdir, "hello.txt") - fs.writestringtofile(file, "hello lute") + fs.writeStringToFile(file, "hello lute") - local contents = fs.readfiletostring(file) + local contents = fs.readFileToString(file) assert.eq(contents, "hello lute") fs.remove(file) @@ -38,7 +38,7 @@ test.suite("FsSuite", function(suite) suite:case("existsAndTypeFileAndDir", function(assert) local file = path.join(tmpdir, "roblox.txt") - fs.writestringtofile(file, "roblox") + fs.writeStringToFile(file, "roblox") assert.that(fs.exists(file)) assert.eq(fs.type(file), "file") @@ -51,13 +51,13 @@ test.suite("FsSuite", function(suite) suite:case("mkdirListdirRmdir", function(assert) local dir = path.join(tmpdir, "subdir") if not fs.exists(dir) then - fs.createdirectory(dir, { makeparents = true }) + fs.createDirectory(dir, { makeParents = true }) end assert.that(fs.exists(dir)) assert.eq(fs.type(dir), "dir") - local entries = fs.listdirectory(tmpdir) + local entries = fs.listDirectory(tmpdir) local found = false for _, e in entries do if e.name == "subdir" then @@ -65,27 +65,27 @@ test.suite("FsSuite", function(suite) end end assert.that(found) - fs.removedirectory(dir) + fs.removeDirectory(dir) end) suite:case("linkAndSymlinkAndCopy", function(assert) local srcfile = "src.txt" local src = path.join(tmpdir, srcfile) - fs.writestringtofile(src, "linkcontent") + fs.writeStringToFile(src, "linkcontent") local dstlink = path.join(tmpdir, "dstlink") fs.link(src, dstlink) - assert.eq(fs.readfiletostring(dstlink), "linkcontent") + assert.eq(fs.readFileToString(dstlink), "linkcontent") fs.remove(dstlink) local dstcopy = path.join(tmpdir, "dstcopy.txt") fs.copy(src, dstcopy) - assert.eq(fs.readfiletostring(dstcopy), "linkcontent") + assert.eq(fs.readFileToString(dstcopy), "linkcontent") fs.remove(dstcopy) local dstsymlink = path.join(tmpdir, "dstsymlink") - fs.symboliclink(srcfile, dstsymlink) - assert.eq(fs.readfiletostring(dstsymlink), "linkcontent") + fs.symbolicLink(srcfile, dstsymlink) + assert.eq(fs.readFileToString(dstsymlink), "linkcontent") fs.remove(dstsymlink) fs.remove(src) @@ -97,7 +97,7 @@ test.suite("FsSuite", function(suite) local watcher = fs.watch(tmpdir) - fs.writestringtofile(watched, "hi") + fs.writeStringToFile(watched, "hi") -- Note: we had a timeout for this loop before, but it was leading to a flaky test where event = nil, so we're removing it. -- If this hangs indefinitely at, we'll see this test fail later and revisit. @@ -125,17 +125,17 @@ test.suite("FsSuite", function(suite) suite:case("createDirectoryMakeparentsTrue", function(assert) local nestedDir = path.join(tmpdir, "nested", "directory") - fs.createdirectory(nestedDir, { makeparents = true }) + fs.createDirectory(nestedDir, { makeParents = true }) assert.that(fs.exists(nestedDir)) - fs.removedirectory(nestedDir) - fs.removedirectory(path.join(tmpdir, "nested")) + fs.removeDirectory(nestedDir) + fs.removeDirectory(path.join(tmpdir, "nested")) end) - suite:case("createdirectoryMakeparentsFalse", function(assert) + suite:case("createDirectoryMakeparentsFalse", function(assert) local nestedDir = path.join(tmpdir, "this", "should", "not", "work") local success, err = pcall(function() - fs.createdirectory(nestedDir, { makeparents = false }) + fs.createDirectory(nestedDir, { makeParents = false }) -- LUAUFIX: pcall's type is wrong, and we can mitigate that by returning nil from this function return nil @@ -147,7 +147,7 @@ test.suite("FsSuite", function(suite) suite:case("createDirectoryNoOptions", function(assert) local nestedDir = path.join(tmpdir, "this", "should", "not", "work") local success, err = pcall(function() - fs.createdirectory(nestedDir) + fs.createDirectory(nestedDir) -- LUAUFIX: pcall's type is wrong, and we can mitigate that by returning nil from this function return nil @@ -184,17 +184,17 @@ test.suite("FsSuite", function(suite) fs.write(h, "hello") fs.close(h) - assert.eq(fs.readfiletostring(file), "hello") + assert.eq(fs.readFileToString(file), "hello") fs.remove(file) end) suite:case("removeDirectoryNonRecursive", function(assert) local dir = path.join(tmpdir, "empty_dir") - fs.createdirectory(dir, { makeparents = true }) + fs.createDirectory(dir, { makeParents = true }) assert.that(fs.exists(dir)) - fs.removedirectory(dir) + fs.removeDirectory(dir) assert.eq(fs.exists(dir), false) end) @@ -204,23 +204,23 @@ test.suite("FsSuite", function(suite) local subdir2 = path.join(dir, "subdir2") local nestedDir = path.join(subdir1, "nested") - fs.createdirectory(nestedDir, { makeparents = true }) - fs.createdirectory(subdir2, { makeparents = true }) + fs.createDirectory(nestedDir, { makeParents = true }) + fs.createDirectory(subdir2, { makeParents = true }) local file1 = path.join(dir, "file1.txt") local file2 = path.join(subdir1, "file2.txt") local file3 = path.join(nestedDir, "file3.txt") - fs.writestringtofile(file1, "content1") - fs.writestringtofile(file2, "content2") - fs.writestringtofile(file3, "content3") + fs.writeStringToFile(file1, "content1") + fs.writeStringToFile(file2, "content2") + fs.writeStringToFile(file3, "content3") assert.that(fs.exists(dir)) assert.that(fs.exists(file1)) assert.that(fs.exists(file2)) assert.that(fs.exists(file3)) - fs.removedirectory(dir, { recursive = true }) + fs.removeDirectory(dir, { recursive = true }) assert.eq(fs.exists(dir), false) assert.eq(fs.exists(file1), false) @@ -230,13 +230,13 @@ test.suite("FsSuite", function(suite) suite:case("removeDirectoryRecursiveFalseWithContents", function(assert) local dir = path.join(tmpdir, "non_empty_dir") - fs.createdirectory(dir, { makeparents = true }) + fs.createDirectory(dir, { makeParents = true }) local file = path.join(dir, "file.txt") - fs.writestringtofile(file, "content") + fs.writeStringToFile(file, "content") local success, err = pcall(function() - fs.removedirectory(dir, { recursive = false }) + fs.removeDirectory(dir, { recursive = false }) -- FIXME(Luau): We should add an overload to `pcall` that allows -- passing a function that returns an empty pack. return nil @@ -247,13 +247,13 @@ test.suite("FsSuite", function(suite) -- Cleanup fs.remove(file) - fs.removedirectory(dir) + fs.removeDirectory(dir) end) suite:case("walkDirectoryRecursive", function(assert) local baseDir = path.join(tmpdir, "walktest") local subDir = path.join(baseDir, "subdir") - fs.createdirectory(subDir, { makeparents = true }) + fs.createDirectory(subDir, { makeParents = true }) local file1 = path.join(baseDir, "file1.txt") local f1 = fs.open(file1, "w+") @@ -289,6 +289,6 @@ test.suite("FsSuite", function(suite) assert.that(found) end - fs.removedirectory(baseDir, { recursive = true }) + fs.removeDirectory(baseDir, { recursive = true }) end) end) diff --git a/tests/std/json.test.luau b/tests/std/json.test.luau index bf9bc1455..6d74fe2da 100644 --- a/tests/std/json.test.luau +++ b/tests/std/json.test.luau @@ -8,7 +8,7 @@ local failure = require("@std/test/failure") local suiteDir = "tests/std/JSONParsingTestSuite" test.suite("JSONParsingTestSuite", function(suite) - for _, entry in fs.listdirectory(suiteDir) do + for _, entry in fs.listDirectory(suiteDir) do if entry.type ~= "file" then continue end @@ -17,7 +17,7 @@ test.suite("JSONParsingTestSuite", function(suite) suite:case(name, function(assert) local p = path.join(suiteDir, name) - local src = fs.readfiletostring(path.format(p)) + local src = fs.readFileToString(path.format(p)) -- attempt to parse .json and print the error if it fails local ok, parsed = pcall(function() diff --git a/tests/std/syntax/parser.test.luau b/tests/std/syntax/parser.test.luau index 83973d49d..51a21bc04 100644 --- a/tests/std/syntax/parser.test.luau +++ b/tests/std/syntax/parser.test.luau @@ -123,7 +123,7 @@ test.suite("Parser", function(suite) end) local function visitDirectory(directory: string) - for _, entry in fs.listdirectory(directory) do + for _, entry in fs.listDirectory(directory) do if entry.type ~= "file" then continue end @@ -133,7 +133,7 @@ test.suite("Parser", function(suite) suite:case(`roundtrippable_Ast_{entry.name}`, function(assert) local p = path.join(directory, entry.name) - local source = fs.readfiletostring(path.format(p)) + local source = fs.readFileToString(path.format(p)) local result = printer.printFile(parser.parse(source)) assert.eq(source, result) diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index 593ef25cf..7efa50725 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -15,7 +15,7 @@ local function assertErrorsWithMsg( assert: testtypes.Asserts ) local testFilePath = path.join(tmpDir, `{testName}.test.luau`) - fs.writestringtofile(testFilePath, testContents) + fs.writeStringToFile(testFilePath, testContents) -- Run lute on the created test file local result = process.run({ lutePath, tostring(testFilePath) }) @@ -29,7 +29,7 @@ test.suite("LuteStdTestFramework", function(suite) suite:case("luteDoesntReportXpcallAsErrorWhenAccessingFieldOfNilInSuite", function(assert) -- Setup local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") - fs.writestringtofile( + fs.writeStringToFile( testFilePath, [[ local test = require("@std/test") @@ -65,7 +65,7 @@ test.run() suite:case("luteDoesntReportXpcallAsErrorWhenAccessingFieldOfNilInCase", function(assert) -- Setup local testFilePath = path.join(tmpDir, "nil_field_access_test.test.luau") - fs.writestringtofile( + fs.writeStringToFile( testFilePath, [[ local test = require("@std/test") From a1433549f7bc181e68b6bf9439929d5ef94b299d Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 1 Apr 2026 13:45:11 -0700 Subject: [PATCH 442/642] Refactors @std/process and @lute/process to be more consistent (#926) All instances of `execpath` have become `execPath`. --- definitions/process.luau | 2 +- lute/cli/commands/new/init.luau | 2 +- lute/cli/commands/test/snap/runner.luau | 2 +- lute/process/include/lute/process.h | 4 ++-- lute/process/src/process.cpp | 2 +- lute/std/libs/process.luau | 4 ++-- tests/cli/check.test.luau | 2 +- tests/cli/compile.test.luau | 2 +- tests/cli/doc.test.luau | 2 +- tests/cli/lint.test.luau | 2 +- tests/cli/loadmodule.test.luau | 2 +- tests/cli/new.test.luau | 2 +- tests/cli/run.test.luau | 2 +- tests/cli/test.test.luau | 2 +- tests/cli/transform.test.luau | 4 ++-- tests/lute/vm.test.luau | 2 +- tests/std/process.test.luau | 2 +- tests/std/test.test.luau | 2 +- 18 files changed, 21 insertions(+), 21 deletions(-) diff --git a/definitions/process.luau b/definitions/process.luau index 231f3c399..facdc2fb9 100644 --- a/definitions/process.luau +++ b/definitions/process.luau @@ -50,7 +50,7 @@ function process.exit(exitcode: number): never error("not implemented") end -function process.execpath(): string +function process.execPath(): string error("not implemented") end diff --git a/lute/cli/commands/new/init.luau b/lute/cli/commands/new/init.luau index 01883f8ad..0f3aba0a1 100644 --- a/lute/cli/commands/new/init.luau +++ b/lute/cli/commands/new/init.luau @@ -51,7 +51,7 @@ local function main(...: string) if not fs.exists(typedefs.TYPEDEFS_PATH) then print("Running lute setup first...") - local result = process.run({ path.format(process.execpath()), "setup" }, { stdio = "inherit" }) + local result = process.run({ path.format(process.execPath()), "setup" }, { stdio = "inherit" }) if not result.ok then print("Error: lute setup failed.") process.exit(1) diff --git a/lute/cli/commands/test/snap/runner.luau b/lute/cli/commands/test/snap/runner.luau index 6f8623653..7b82fd07d 100644 --- a/lute/cli/commands/test/snap/runner.luau +++ b/lute/cli/commands/test/snap/runner.luau @@ -33,7 +33,7 @@ function runner.runsnapshots(files: { path.Path }): snapty.snapresult new = {}, } - local execpath = path.format(process.execpath()) + local execpath = path.format(process.execPath()) -- Compute the normalized current working directory exactly once --[[ diff --git a/lute/process/include/lute/process.h b/lute/process/include/lute/process.h index d163b5ff1..404c9dc55 100644 --- a/lute/process/include/lute/process.h +++ b/lute/process/include/lute/process.h @@ -23,7 +23,7 @@ int cwd(lua_State* L); int exitFunc(lua_State* L); std::optional getExecPath(std::string* error); -int execpath(lua_State* L); +int execPath(lua_State* L); static const luaL_Reg lib[] = { {"run", run}, @@ -31,7 +31,7 @@ static const luaL_Reg lib[] = { {"homedir", homedir}, {"cwd", cwd}, {"exit", exitFunc}, - {"execpath", execpath}, + {"execPath", execPath}, {nullptr, nullptr} }; diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index b3d7e9ed5..b49d24708 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -516,7 +516,7 @@ std::optional getExecPath(std::string* error) return *cachedPath; } -int execpath(lua_State* L) +int execPath(lua_State* L) { std::string error; std::optional execPath = getExecPath(&error); diff --git a/lute/std/libs/process.luau b/lute/std/libs/process.luau index 5ecc0d8e6..21be22d90 100644 --- a/lute/std/libs/process.luau +++ b/lute/std/libs/process.luau @@ -35,8 +35,8 @@ function processlib.exit(exitcode: number): never return process.exit(exitcode) end -function processlib.execpath(): Path - return pathlib.parse(process.execpath()) +function processlib.execPath(): Path + return pathlib.parse(process.execPath()) end -- re-exports diff --git a/tests/cli/check.test.luau b/tests/cli/check.test.luau index 402cef6cd..235d7e63f 100644 --- a/tests/cli/check.test.luau +++ b/tests/cli/check.test.luau @@ -2,7 +2,7 @@ local path = require("@std/path") local process = require("@std/process") local test = require("@std/test") -local lutePath = path.format(process.execpath()) +local lutePath = path.format(process.execPath()) test.suite("LuteTypeCheck", function(suite) suite:case("usesNewSolver", function(assert) diff --git a/tests/cli/compile.test.luau b/tests/cli/compile.test.luau index e5574bb6e..1ce2b7e2b 100644 --- a/tests/cli/compile.test.luau +++ b/tests/cli/compile.test.luau @@ -20,7 +20,7 @@ print(`SUCCESS`) local COMPILEE_CONTENTS = [[return "Hello world"]] -local lutePath = process.execpath() +local lutePath = process.execPath() local tmpDir = system.tmpdir() test.suite("LuteCliCompile", function(suite) diff --git a/tests/cli/doc.test.luau b/tests/cli/doc.test.luau index d9bb7d2a2..c2f3932ff 100644 --- a/tests/cli/doc.test.luau +++ b/tests/cli/doc.test.luau @@ -6,7 +6,7 @@ local test = require("@std/test") local tmpDir = system.tmpdir() local tmpOutputDir = path.join(tmpDir, "lute-doc-output") -local lutePath = path.format(process.execpath()) +local lutePath = path.format(process.execPath()) local USAGE = "Usage: lute doc [OPTIONS]" diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 6e00c6375..7b87237be 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -6,7 +6,7 @@ local tableext = require("@std/tableext") local test = require("@std/test") local testTypes = require("@std/test/types") -local lutePath = path.format(process.execpath()) +local lutePath = path.format(process.execPath()) local tmpDir = system.tmpdir() local rulesDir = path.format(path.join(".", "lints")) local defaultRulesFolder = path.format(path.join("lute", "cli", "commands", "lint", "rules")) diff --git a/tests/cli/loadmodule.test.luau b/tests/cli/loadmodule.test.luau index b8e73ccc0..bc74a4d3f 100644 --- a/tests/cli/loadmodule.test.luau +++ b/tests/cli/loadmodule.test.luau @@ -15,7 +15,7 @@ print(result) local REQUIREE_CONTENTS = [[return "Success"]] -local lutePath = process.execpath() +local lutePath = process.execPath() local tmpDirStr = system.tmpdir() local testDir = path.join(tmpDirStr, "lute_require_test") diff --git a/tests/cli/new.test.luau b/tests/cli/new.test.luau index b9f5c072e..6b6ee8224 100644 --- a/tests/cli/new.test.luau +++ b/tests/cli/new.test.luau @@ -6,7 +6,7 @@ local test = require("@std/test") local tmpdir = system.tmpdir() local workdir = pathlib.join(tmpdir, "lute-new") -local lutePath = pathlib.format(process.execpath()) +local lutePath = pathlib.format(process.execPath()) local projectName = "myproject" local projectDir = pathlib.join(workdir, projectName) diff --git a/tests/cli/run.test.luau b/tests/cli/run.test.luau index 48515f93c..c3a431b37 100644 --- a/tests/cli/run.test.luau +++ b/tests/cli/run.test.luau @@ -8,7 +8,7 @@ local tmpdir = system.tmpdir() local rundir = pathlib.join(tmpdir, "lute-run") -local lutePath = pathlib.format(process.execpath()) +local lutePath = pathlib.format(process.execPath()) test.suite("LuteRun", function(suite) suite:beforeeach(function() diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index 7a0d1511d..fdb718554 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -2,7 +2,7 @@ local path = require("@std/path") local process = require("@std/process") local test = require("@std/test") -local lutePath = path.format(process.execpath()) +local lutePath = path.format(process.execPath()) local expected = [[Anonymous: passing diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index 6c9fe6965..f7a4f6e48 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -1,6 +1,6 @@ local fs = require("@std/fs") local path = require("@std/path") -local process = require("@lute/process") +local process = require("@std/process") local system = require("@std/system") local test = require("@std/test") @@ -9,7 +9,7 @@ local x = math.sqrt(-1) local b = x ~= x ]] -local lutePath = process.execpath() +local lutePath = path.format(process.execPath()) local tmpDir = system.tmpdir() local transformeePath = path.format(path.join(tmpDir, "transformee.luau")) local outputPath = path.format(path.join(tmpDir, "transformee_output.luau")) diff --git a/tests/lute/vm.test.luau b/tests/lute/vm.test.luau index 2ff2035fe..5590bf183 100644 --- a/tests/lute/vm.test.luau +++ b/tests/lute/vm.test.luau @@ -59,7 +59,7 @@ test.suite("LuteVmSuite", function(suite) fs.close(handle) end - local result = process.run({ path.format(process.execpath()), path.format(parent_vm) }) + local result = process.run({ path.format(process.execPath()), path.format(parent_vm) }) assert.eq(result.exitcode, 0) fs.remove(child_vm) diff --git a/tests/std/process.test.luau b/tests/std/process.test.luau index a0662643b..3b50fe749 100644 --- a/tests/std/process.test.luau +++ b/tests/std/process.test.luau @@ -16,7 +16,7 @@ test.suite("ProcessSuite", function(suite) local cwd = process.cwd() assert.neq(pathlib.format(cwd), "") - local ex = process.execpath() + local ex = process.execPath() assert.neq(pathlib.format(ex), "") end) diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index 7efa50725..4a382bfcd 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -5,7 +5,7 @@ local process = require("@std/process") local system = require("@std/system") local test = require("@std/test") -local lutePath = path.format(process.execpath()) +local lutePath = path.format(process.execPath()) local tmpDir = system.tmpdir() local function assertErrorsWithMsg( From 6e09c2656ba788872b8f4d1a02694f61558c4f1c Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 1 Apr 2026 16:31:02 -0700 Subject: [PATCH 443/642] Refactors the test library to conform to our style standards Pt1 (#927) This PR renames all the test lifecycle methods (beforeEach/afterEach/beforeAll/afterAll) etc + updates the documentation. --- docs/guide/writing-tests/index.md | 6 ++-- examples/testing.luau | 16 ++++----- lute/std/libs/test/init.luau | 24 ++++++------- lute/std/libs/test/runner.luau | 56 +++++++++++++++---------------- lute/std/libs/test/types.luau | 18 +++++----- tests/cli/lib/files.test.luau | 2 +- tests/cli/lint.test.luau | 2 +- tests/cli/loadmodule.test.luau | 4 +-- tests/cli/new.test.luau | 2 +- tests/cli/run.test.luau | 2 +- tests/cli/transform.test.luau | 4 +-- 11 files changed, 68 insertions(+), 68 deletions(-) diff --git a/docs/guide/writing-tests/index.md b/docs/guide/writing-tests/index.md index e259c4990..baabd3e4b 100644 --- a/docs/guide/writing-tests/index.md +++ b/docs/guide/writing-tests/index.md @@ -156,7 +156,7 @@ will fail, which will be reported as a failed test case. ### Using Test Suites to organize tests While we're at it, we can also wrap all of these tests into a single test suite, which will group these tests together. Test suites also allow you to use -lifecycle methods like `beforeeach`, `beforeall`, `aftereach`, and `afterall` to control setup and +lifecycle methods like `beforeEach`, `beforeAll`, `afterEach`, and `afterAll` to control setup and tear down for tests. ```luau @@ -208,7 +208,7 @@ files in a temporary directory. For example, testing that some code can create a set of files. When subsequent tests execute, they may be operating in a directory filled with files leftover from a previous test. -In this situation, you could use the `beforeeach` method to execute some cleanup +In this situation, you could use the `beforeEach` method to execute some cleanup of the temporary directory: ```luau @@ -221,7 +221,7 @@ local system = require("@std/system") local testDir = path.join(system.tmpdir(), "test") test.suite("FileCreation", function() - test.beforeeach(function() + test.beforeEach(function() --Deletes the contents of the test directory before each test fs.removedirectory(testDir, {recursive = true}) --Recreates the directory so it exists for the next test to use it diff --git a/examples/testing.luau b/examples/testing.luau index ec1e9ca64..d80c19325 100644 --- a/examples/testing.luau +++ b/examples/testing.luau @@ -14,23 +14,23 @@ test.case("baz", function(assert) end) test.suite("MySuite", function(suite) - -- beforeall runs once before all tests in the suite - suite:beforeall(function() + -- beforeAll runs once before all tests in the suite + suite:beforeAll(function() print("Setting up MySuite - runs once before all tests") end) - -- beforeeach runs before each individual test - suite:beforeeach(function() + -- beforeEach runs before each individual test + suite:beforeEach(function() print("Running before each test") end) - -- aftereach runs after each individual test - suite:aftereach(function() + -- afterEach runs after each individual test + suite:afterEach(function() print("Cleaning up after each test") end) - -- afterall runs once after all tests in the suite - suite:afterall(function() + -- afterAll runs once after all tests in the suite + suite:afterAll(function() print("Tearing down MySuite - runs once after all tests") end) diff --git a/lute/std/libs/test/init.luau b/lute/std/libs/test/init.luau index e85919af3..5e9b0c996 100644 --- a/lute/std/libs/test/init.luau +++ b/lute/std/libs/test/init.luau @@ -21,10 +21,10 @@ function TestSuite.new(name: string): TestSuite local self = setmetatable({ name = name, cases = {}, - _beforeeach = nil, - _beforeall = nil, - _aftereach = nil, - _afterall = nil, + _beforeEach = nil, + _beforeAll = nil, + _afterEach = nil, + _afterAll = nil, }, TestSuite) return self :: TestSuite end @@ -33,20 +33,20 @@ function TestSuite:case(name: string, testCase: Test) table.insert(self.cases, { name = name, case = testCase }) end -function TestSuite:beforeeach(hook: () -> ()) - self._beforeeach = hook +function TestSuite:beforeEach(hook: () -> ()) + self._beforeEach = hook end -function TestSuite:beforeall(hook: () -> ()) - self._beforeall = hook +function TestSuite:beforeAll(hook: () -> ()) + self._beforeAll = hook end -function TestSuite:aftereach(hook: () -> ()) - self._aftereach = hook +function TestSuite:afterEach(hook: () -> ()) + self._afterEach = hook end -function TestSuite:afterall(hook: () -> ()) - self._afterall = hook +function TestSuite:afterAll(hook: () -> ()) + self._afterAll = hook end local env: TestEnvironment = { anonymous = {}, suites = {}, suiteindex = {}, caseindex = {} } diff --git a/lute/std/libs/test/runner.luau b/lute/std/libs/test/runner.luau index 77c99d72a..b8dfba723 100644 --- a/lute/std/libs/test/runner.luau +++ b/lute/std/libs/test/runner.luau @@ -22,16 +22,16 @@ function runner.run(env: TestEnvironment): TestRunResult local failed = 0 local passed = 0 - -- Error handler for beforeall hook - local function beforeallErrorHandler(suite: TestSuite) + -- Error handler for beforeAll hook + local function beforeAllErrorHandler(suite: TestSuite) return function(err) - -- If beforeall fails, skip all tests in the suite + -- If beforeAll fails, skip all tests in the suite total += #suite.cases failed += #suite.cases for _, tc in suite.cases do local failedtest: FailedTest = { test = `{suite.name}.{tc.name}`, - failure = failure.lifecycle("beforeall", err), + failure = failure.lifecycle("beforeAll", err), } table.insert(failures, failedtest) end @@ -39,38 +39,38 @@ function runner.run(env: TestEnvironment): TestRunResult end end - -- Error handler for beforeeach hook - local function beforeeachErrorHandler(testFullName: string) + -- Error handler for beforeEach hook + local function beforeEachErrorHandler(testFullName: string) return function(err) failed += 1 local failedtest: FailedTest = { test = testFullName, - failure = failure.lifecycle("beforeeach", err), + failure = failure.lifecycle("beforeEach", err), } table.insert(failures, failedtest) return err end end - -- Error handler for aftereach hook - local function afterachErrorHandler(testFullName: string) + -- Error handler for afterEach hook + local function afterEachErrorHandler(testFullName: string) return function(err) local failedtest: FailedTest = { test = testFullName, - failure = failure.lifecycle("aftereach", err), + failure = failure.lifecycle("afterEach", err), } table.insert(failures, failedtest) return err end end - -- Error handler for afterall hook - local function afterallErrorHandler(suiteName: string) + -- Error handler for afterAll hook + local function afterAllErrorHandler(suiteName: string) return function(err) failed += 1 local failedtest: FailedTest = { test = suiteName, - failure = failure.lifecycle("afterall", err), + failure = failure.lifecycle("afterAll", err), } table.insert(failures, failedtest) return err @@ -98,11 +98,11 @@ function runner.run(env: TestEnvironment): TestRunResult -- Run tests from suites for _, suite in env.suites do - -- Run beforeall hook once per suite - if suite._beforeall then - local beforeallSuccess = xpcall(suite._beforeall, beforeallErrorHandler(suite)) - if not beforeallSuccess then - -- If beforeall fails, skip all tests in the suite + -- Run beforeAll hook once per suite + if suite._beforeAll then + local beforeAllSuccess = xpcall(suite._beforeAll, beforeAllErrorHandler(suite)) + if not beforeAllSuccess then + -- If beforeAll fails, skip all tests in the suite continue end end @@ -119,20 +119,20 @@ function runner.run(env: TestEnvironment): TestRunResult table.insert(failures, failedtest) end - -- Run beforeeach hook if it exists - if suite._beforeeach then - local beforeSuccess = xpcall(suite._beforeeach, beforeeachErrorHandler(testFullName)) + -- Run beforeEach hook if it exists + if suite._beforeEach then + local beforeSuccess = xpcall(suite._beforeEach, beforeEachErrorHandler(testFullName)) if not beforeSuccess then continue end end local success = xpcall(tc.case, handlefailure, asserts) - -- Run aftereach hook if it exists (runs even if test failed) - if suite._aftereach then - local afterSuccess = xpcall(suite._aftereach, afterachErrorHandler(testFullName)) + -- Run afterEach hook if it exists (runs even if test failed) + if suite._afterEach then + local afterSuccess = xpcall(suite._afterEach, afterEachErrorHandler(testFullName)) if not afterSuccess then - -- If test passed but aftereach failed, mark as failed + -- If test passed but afterEach failed, mark as failed success = false end end @@ -144,9 +144,9 @@ function runner.run(env: TestEnvironment): TestRunResult end end - -- Run afterall hook once per suite (runs even if tests failed) - if suite._afterall then - xpcall(suite._afterall, afterallErrorHandler(suite.name)) + -- Run afterAll hook once per suite (runs even if tests failed) + if suite._afterAll then + xpcall(suite._afterAll, afterAllErrorHandler(suite.name)) end end diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index 06a0db248..f94c147da 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -4,7 +4,7 @@ -- Test Reporting -export type Hook = "beforeeach" | "beforeall" | "aftereach" | "afterall" +export type Hook = "beforeEach" | "beforeAll" | "afterEach" | "afterAll" export type Failure = { assertion: string, -- name of the assertion @@ -43,15 +43,15 @@ export type TestCase = { name: string, case: Test } export type TestSuite = { name: string, cases: { TestCase }, - _beforeeach: (() -> ())?, - _beforeall: (() -> ())?, - _aftereach: (() -> ())?, - _afterall: (() -> ())?, + _beforeEach: (() -> ())?, + _beforeAll: (() -> ())?, + _afterEach: (() -> ())?, + _afterAll: (() -> ())?, case: (self: TestSuite, string, Test) -> (), - beforeeach: (self: TestSuite, () -> ()) -> (), - beforeall: (self: TestSuite, () -> ()) -> (), - aftereach: (self: TestSuite, () -> ()) -> (), - afterall: (self: TestSuite, () -> ()) -> (), + beforeEach: (self: TestSuite, () -> ()) -> (), + beforeAll: (self: TestSuite, () -> ()) -> (), + afterEach: (self: TestSuite, () -> ()) -> (), + afterAll: (self: TestSuite, () -> ()) -> (), } export type FailedTest = { diff --git a/tests/cli/lib/files.test.luau b/tests/cli/lib/files.test.luau index 9c186a1f9..3094010bd 100644 --- a/tests/cli/lib/files.test.luau +++ b/tests/cli/lib/files.test.luau @@ -8,7 +8,7 @@ local tmpdir = system.tmpdir() local testPath = path.join(tmpdir, "gitignore_test") test.suite("CliLibFiles", function(suite) - suite:beforeeach(function() + suite:beforeEach(function() if fs.exists(testPath) then fs.removeDirectory(testPath, { recursive = true }) end diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index 7b87237be..f4bb6cab2 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -115,7 +115,7 @@ local function lintTestHelper(assert: testTypes.Asserts, params: lintTestParams) end test.suite("lute lint", function(suite) - suite:beforeeach(function() + suite:beforeEach(function() -- A test that creates rulesDir which fails won't cleanup after itself if fs.exists(rulesDir) then fs.removeDirectory(rulesDir, { recursive = true }) diff --git a/tests/cli/loadmodule.test.luau b/tests/cli/loadmodule.test.luau index bc74a4d3f..866c23489 100644 --- a/tests/cli/loadmodule.test.luau +++ b/tests/cli/loadmodule.test.luau @@ -22,7 +22,7 @@ local testDir = path.join(tmpDirStr, "lute_require_test") local testDirStr = path.format(testDir) test.suite("LuteCliRun", function(suite) - suite:beforeall(function() + suite:beforeAll(function() if not fs.exists(testDirStr) then fs.createDirectory(testDirStr) end @@ -50,7 +50,7 @@ test.suite("LuteCliRun", function(suite) fs.remove(requireePath) end) - suite:afterall(function() + suite:afterAll(function() if fs.exists(testDirStr) then fs.removeDirectory(testDirStr) end diff --git a/tests/cli/new.test.luau b/tests/cli/new.test.luau index 6b6ee8224..aeec240d8 100644 --- a/tests/cli/new.test.luau +++ b/tests/cli/new.test.luau @@ -11,7 +11,7 @@ local projectName = "myproject" local projectDir = pathlib.join(workdir, projectName) test.suite("LuteNew", function(suite) - suite:beforeeach(function() + suite:beforeEach(function() if fs.exists(workdir) then fs.removeDirectory(workdir, { recursive = true }) end diff --git a/tests/cli/run.test.luau b/tests/cli/run.test.luau index c3a431b37..2d32572a8 100644 --- a/tests/cli/run.test.luau +++ b/tests/cli/run.test.luau @@ -11,7 +11,7 @@ local rundir = pathlib.join(tmpdir, "lute-run") local lutePath = pathlib.format(process.execPath()) test.suite("LuteRun", function(suite) - suite:beforeeach(function() + suite:beforeEach(function() if fs.exists(rundir) then fs.removeDirectory(rundir, { recursive = true }) end diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index f7a4f6e48..a0194bf08 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -15,14 +15,14 @@ local transformeePath = path.format(path.join(tmpDir, "transformee.luau")) local outputPath = path.format(path.join(tmpDir, "transformee_output.luau")) test.suite("LuteTransform", function(suite) - suite:beforeeach(function() + suite:beforeEach(function() -- Create a transformee file local transformeeHandle = fs.open(transformeePath, "w+") fs.write(transformeeHandle, TRANSFORMEE_CONTENT) fs.close(transformeeHandle) end) - suite:aftereach(function() + suite:afterEach(function() -- Remove the transformee file fs.remove(transformeePath) if fs.exists(outputPath) then From 4655472e9a0a922449c1f700a9d1eefec782c6f2 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 1 Apr 2026 16:36:43 -0700 Subject: [PATCH 444/642] Renames strcontains and strnotcontains assertions to be camelCased (#929) --- lute/std/libs/test/assert.luau | 8 +- lute/std/libs/test/types.luau | 4 +- tests/batteries/toml.test.luau | 2 +- tests/cli/doc.test.luau | 14 +-- tests/cli/lint.test.luau | 112 +++++++++--------- tests/cli/new.test.luau | 14 +-- tests/cli/run.test.luau | 2 +- tests/cli/test.test.luau | 2 +- tests/cli/transform.test.luau | 6 +- ...t_strcontains_instances_negative.snap.luau | 2 +- ...contains_multiple_instance_fails.snap.luau | 2 +- ...trcontains_single_instance_fails.snap.luau | 2 +- tests/std/test.test.luau | 30 ++--- 13 files changed, 100 insertions(+), 100 deletions(-) diff --git a/lute/std/libs/test/assert.luau b/lute/std/libs/test/assert.luau index a07d9bef2..16b8469e3 100644 --- a/lute/std/libs/test/assert.luau +++ b/lute/std/libs/test/assert.luau @@ -180,7 +180,7 @@ local function throwsWith(with: any, func: (T...) -> ...unknown, ...: T... return nil end -local function strcontains(haystack: string, needle: string, instances: number?, msg: string?): Failure? +local function strContains(haystack: string, needle: string, instances: number?, msg: string?): Failure? if instances == nil then if haystack:find(needle, 1, true) == nil then return assertion(if msg then msg else `Expected "{haystack}" to contain "{needle}".`) @@ -202,7 +202,7 @@ local function strcontains(haystack: string, needle: string, instances: number?, return nil end -local function strnotcontains(haystack: string, needle: string, msg: string?): Failure? +local function strNotContains(haystack: string, needle: string, msg: string?): Failure? if haystack:find(needle, 1, true) ~= nil then return assertion(if msg then msg else `Expected "{haystack}" to not contain "{needle}".`) end @@ -227,7 +227,7 @@ return table.freeze({ neq = reqassert(neq), throws = reqassert(throws), throwsWith = reqassert(throwsWith), - strcontains = reqassert(strcontains), - strnotcontains = reqassert(strnotcontains), + strContains = reqassert(strContains), + strNotContains = reqassert(strNotContains), that = reqassert(that), }) :: Asserts diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index f94c147da..b0ce32393 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -31,8 +31,8 @@ export type Asserts = { neq: (T, T, string?) -> Failure?, throws: ((T...) -> ...unknown, T...) -> Failure?, throwsWith: (any, (T...) -> ...unknown, T...) -> Failure?, - strcontains: (string, string, number?, string?) -> Failure?, - strnotcontains: (string, string, string?) -> Failure?, + strContains: (string, string, number?, string?) -> Failure?, + strNotContains: (string, string, string?) -> Failure?, that: (unknown, string?) -> Failure?, } diff --git a/tests/batteries/toml.test.luau b/tests/batteries/toml.test.luau index 35c82d3fb..c26019000 100644 --- a/tests/batteries/toml.test.luau +++ b/tests/batteries/toml.test.luau @@ -62,7 +62,7 @@ test.suite("TOML serialization", function(suite) suite:case("keyWithDotIsQuoted", function(assert) local input = { images = { ["icon.png"] = { assetId = "123" } } } local output = toml.serialize(input) - assert.strcontains(output, '"icon.png"') + assert.strContains(output, '"icon.png"') end) suite:case("roundTrip", function(assert) diff --git a/tests/cli/doc.test.luau b/tests/cli/doc.test.luau index c2f3932ff..a5a0eeea0 100644 --- a/tests/cli/doc.test.luau +++ b/tests/cli/doc.test.luau @@ -16,31 +16,31 @@ test.suite("LuteDoc", function(suite) local result = process.run({ lutePath, "doc", "-h" }) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, USAGE) + assert.strContains(result.stdout, USAGE) result = process.run({ lutePath, "doc", "--h" }) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, USAGE) + assert.strContains(result.stdout, USAGE) result = process.run({ lutePath, "doc", "--help" }) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, USAGE) + assert.strContains(result.stdout, USAGE) end) suite:case("luteDocNoModulesProvided", function(assert) local result = process.system(`{lutePath} doc`) assert.eq(result.exitcode, 1) - assert.strcontains(result.stderr, "Error: No module paths specified.") + assert.strContains(result.stderr, "Error: No module paths specified.") end) suite:case("luteDocDefaultDirectory", function(assert) local result = process.system(`{lutePath} doc definitions lute/std/libs`) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, "Documentation generation complete!") + assert.strContains(result.stdout, "Documentation generation complete!") end) suite:case("luteDocSpecifiedDirectory", function(assert) @@ -53,8 +53,8 @@ test.suite("LuteDoc", function(suite) local result = process.system(`{lutePath} doc -o {path.format(tmpOutputDir)} definitions lute/std/libs`) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, "Documentation generation complete!") - assert.strcontains(result.stdout, tostring(tmpOutputDir)) -- "Files written to: " + assert.strContains(result.stdout, "Documentation generation complete!") + assert.strContains(result.stdout, tostring(tmpOutputDir)) -- "Files written to: " assert.that(fs.exists(path.join(tmpOutputDir, "reference", "lute", "fs.md"))) assert.that(fs.exists(path.join(tmpOutputDir, "reference", "std", "fs.md"))) diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index f4bb6cab2..f82b5a755 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -104,9 +104,9 @@ local function lintTestHelper(assert: testTypes.Asserts, params: lintTestParams) if params.outputExpectations then for _, expectation in params.outputExpectations do if expectation.count == 0 then - assert.strnotcontains(result.stdout, expectation.expectedOutput) + assert.strNotContains(result.stdout, expectation.expectedOutput) else - assert.strcontains(result.stdout, expectation.expectedOutput, expectation.count) + assert.strContains(result.stdout, expectation.expectedOutput, expectation.count) end end end @@ -131,24 +131,24 @@ test.suite("lute lint", function(suite) local result = process.run({ lutePath, "lint", "-h" }) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, USAGE) + assert.strContains(result.stdout, USAGE) result = process.run({ lutePath, "lint", "--h" }) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, USAGE) + assert.strContains(result.stdout, USAGE) result = process.run({ lutePath, "lint", "--help" }) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, USAGE) + assert.strContains(result.stdout, USAGE) end) suite:case("luteLintNoInputFile", function(assert) local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "Error: No input files or string input specified.") + assert.strContains(result.stdout, "Error: No input files or string input specified.") end) suite:case("luteLintErrorCode0WithNoViolations", function(assert) @@ -489,7 +489,7 @@ violator1.luau:1:11-16 ── │ ^^^^^ │ ]] - assert.strcontains(result.stdout, expected) + assert.strContains(result.stdout, expected) expected = [[ violator2.lua:1:11-17 ── @@ -498,7 +498,7 @@ violator2.lua:1:11-17 ── │ ^^^^^^ │ ]] - assert.strcontains(result.stdout, expected) + assert.strContains(result.stdout, expected) assert.eq(#result.stdout:split(LINT_RULE_MESSAGES.almost_swapped), 3) expected = [[ @@ -509,7 +509,7 @@ violator1.luau:3:1-4:6 ── │ ╰─────^ │ ]] - assert.strcontains(result.stdout, expected) + assert.strContains(result.stdout, expected) expected = [[ violator2.lua:5:1-6:6 ── @@ -519,7 +519,7 @@ violator2.lua:5:1-6:6 ── │ ╰─────^ │ ]] - assert.strcontains(result.stdout, expected) + assert.strContains(result.stdout, expected) expected = "Skipping non%-Luau file '.+[/\\]module[/\\]not_luau%.txt'" assert.neq(result.stdout:find(expected, 1, false), nil) @@ -556,10 +556,10 @@ b = a lintee.luau': @std/syntax/parser.luau:12: parsing failed: (0, 0) - (0, 5): Incomplete statement: expected assignment or a function call ]] - assert.strcontains(result.stdout, expected) + assert.strContains(result.stdout, expected) - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) + assert.strContains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) + assert.strContains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) -- Teardown fs.remove(lintee) @@ -996,7 +996,7 @@ local y = 10 / 0 -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero, 1) + assert.strContains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero, 1) local expected = [[ violator1.luau:1:11-17 ── │ @@ -1004,7 +1004,7 @@ violator1.luau:1:11-17 ── │ ^^^^^^ │ ]] - assert.strcontains(result.stdout, expected) + assert.strContains(result.stdout, expected) expected = [[ violator2.lua:1:11-17 ── @@ -1013,7 +1013,7 @@ violator2.lua:1:11-17 ── │ ^^^^^^ │ ]] - assert.strnotcontains(result.stdout, expected) + assert.strNotContains(result.stdout, expected) -- Teardown fs.removeDirectory(modulePath, { recursive = true }) @@ -1099,7 +1099,7 @@ b = a -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) + assert.strContains(result.stdout, LINT_RULE_MESSAGES.almost_swapped) local expected = [[ ┌── input:1:1-2:6 ── │ @@ -1108,7 +1108,7 @@ b = a │ ╰─────^ │ ]] - assert.strcontains(result.stdout, expected) + assert.strContains(result.stdout, expected) end) suite:case("luteLintStringInputVerbose", function(assert) @@ -1121,11 +1121,11 @@ b = a -- Check assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "Parsing input string") - assert.strcontains(result.stdout, "Parsing global lint ignores in input string") - assert.strcontains(result.stdout, "Applying lint rules") - assert.strcontains(result.stdout, "Printing violations from string input") - assert.strcontains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) + assert.strContains(result.stdout, "Parsing input string") + assert.strContains(result.stdout, "Parsing global lint ignores in input string") + assert.strContains(result.stdout, "Applying lint rules") + assert.strContains(result.stdout, "Printing violations from string input") + assert.strContains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) end) suite:case("luteLintStringInputWithNoViolations", function(assert) @@ -1137,7 +1137,7 @@ b = a -- Check assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, "No lint violations found.") + assert.strContains(result.stdout, "No lint violations found.") end) suite:case("luteLintStringInputJsonOutput", function(assert) @@ -1203,7 +1203,7 @@ b = a } } ]]=] - assert.strcontains(result.stdout, expected) + assert.strContains(result.stdout, expected) end) suite:case("luteLintIgnoreTableItem", function(assert) @@ -1526,7 +1526,7 @@ local e = next(t) ~= nil } } ]]=] - assert.strcontains(result.stdout, expected) + assert.strContains(result.stdout, expected) end) suite:case("unusedVariable1", function(assert) @@ -2489,7 +2489,7 @@ return { expectedExitCode = 1, returnStdErr = true, }) - assert.strcontains(err, "config.lute.lint.globals must be a table with string keys") + assert.strContains(err, "config.lute.lint.globals must be a table with string keys") end) suite:case("throwsErrorWhenConfigHasInvalidIgnores", function(assert) @@ -2507,7 +2507,7 @@ return { expectedExitCode = 1, returnStdErr = true, }) - assert.strcontains(err, "config.lute.lint.ignores must be an array of filepath globs") + assert.strContains(err, "config.lute.lint.ignores must be an array of filepath globs") end) suite:case("throwsErrorWhenConfigHasInvalidRulePaths", function(assert) @@ -2525,7 +2525,7 @@ return { expectedExitCode = 1, returnStdErr = true, }) - assert.strcontains(err, "config.lute.lint.rulepaths must be an array of filepath globs") + assert.strContains(err, "config.lute.lint.rulepaths must be an array of filepath globs") end) suite:case("throwsErrorWhenConfigHasInvalidRuleConfigs", function(assert) @@ -2543,7 +2543,7 @@ return { expectedExitCode = 1, returnStdErr = true, }) - assert.strcontains(err, "config.lute.lint.ruleconfigs must be a table with string keys") + assert.strContains(err, "config.lute.lint.ruleconfigs must be a table with string keys") end) suite:case("luteLintAccountsForConfiguredIgnoresConfigPassedAsArg", function(assert) @@ -2574,8 +2574,8 @@ return { local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) assert.eq(result.exitcode, 1) -- errors but ONLY from valid.luau - assert.strnotcontains(result.stdout, "ignored.luau") - assert.strcontains(result.stdout, "valid.luau") + assert.strNotContains(result.stdout, "ignored.luau") + assert.strContains(result.stdout, "valid.luau") end) suite:case("luteLintAccountsForConfiguredIgnoresConfigAutoDetected", function(assert) @@ -2607,8 +2607,8 @@ return { cwd = path.format(testSubDir), }) assert.eq(result.exitcode, 0) -- no errors - assert.strnotcontains(result.stdout, "almost_swapped") - assert.strnotcontains(result.stdout, "divide_by_zero") + assert.strNotContains(result.stdout, "almost_swapped") + assert.strNotContains(result.stdout, "divide_by_zero") end) suite:case("luteLintConfiguredIgnoresSupplementNotOverrideExistingGitignores", function(assert) @@ -2643,8 +2643,8 @@ return { local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) assert.eq(result.exitcode, 0) -- no errors bc we ignore BOTH files in the dir - assert.strnotcontains(result.stdout, "almost_swapped") - assert.strnotcontains(result.stdout, "divide_by_zero") + assert.strNotContains(result.stdout, "almost_swapped") + assert.strNotContains(result.stdout, "divide_by_zero") end) suite:case("duplicateKeysListDuplicates", function(assert) @@ -2842,13 +2842,13 @@ local t = { [0] = 1 } assert.eq(result.exitcode, 1) -- errors but ONLY from notIgnored.luau -- Should NOT contain violations from the ignored directory - assert.strnotcontains(result.stdout, "file1.luau") - assert.strnotcontains(result.stdout, "file2.luau") + assert.strNotContains(result.stdout, "file1.luau") + assert.strNotContains(result.stdout, "file2.luau") -- Should contain violations from the non-ignored file - assert.strcontains(result.stdout, "notIgnored.luau") - assert.strcontains(result.stdout, "divide_by_zero") - assert.strcontains(result.stdout, "almost_swapped") + assert.strContains(result.stdout, "notIgnored.luau") + assert.strContains(result.stdout, "divide_by_zero") + assert.strContains(result.stdout, "almost_swapped") end) suite:case("luteLintHandlesIgnoresWithRelativeConfigPath", function(assert) @@ -2882,8 +2882,8 @@ local t = { [0] = 1 } cwd = path.format(testSubDir), }) assert.eq(result.exitcode, 1) -- errors but ONLY from valid.luau - assert.strnotcontains(result.stdout, "ignored.luau") - assert.strcontains(result.stdout, "valid.luau") + assert.strNotContains(result.stdout, "ignored.luau") + assert.strContains(result.stdout, "valid.luau") end) suite:case("luteLintHandlesIgnoresWithAbsoluteConfigPath", function(assert) @@ -2915,8 +2915,8 @@ local t = { [0] = 1 } -- Pass config as an absolute path, with cwd set to a different directory (homedir) local result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(testSubDir) }) assert.eq(result.exitcode, 1) -- errors but ONLY from valid.luau - assert.strnotcontains(result.stdout, "ignored.luau") - assert.strcontains(result.stdout, "valid.luau") + assert.strNotContains(result.stdout, "ignored.luau") + assert.strContains(result.stdout, "valid.luau") end) suite:case("luteLintPassesContextThroughToRules", function(assert) @@ -3002,17 +3002,17 @@ return { cwd = path.format(process.homedir()), }) assert.eq(result.exitcode, 1) -- should be violations - assert.strcontains(result.stdout, "violator.luau") - assert.strcontains(result.stdout, "[divide_by_zero]") - assert.strcontains(result.stdout, "[almost_swapped]") + assert.strContains(result.stdout, "violator.luau") + assert.strContains(result.stdout, "[divide_by_zero]") + assert.strContains(result.stdout, "[almost_swapped]") -- with config, there should be violations but NOT for divide_by_zero result = process.run({ lutePath, "lint", "-c", path.format(configFile), path.format(violatorFile) }, { cwd = path.format(process.homedir()), }) assert.eq(result.exitcode, 1) - assert.strnotcontains(result.stdout, "[divide_by_zero]") - assert.strcontains(result.stdout, "[almost_swapped]") + assert.strNotContains(result.stdout, "[divide_by_zero]") + assert.strContains(result.stdout, "[almost_swapped]") end) suite:case("luteLintAccountsForRuleSpecificDirectoryIgnores", function(assert) @@ -3054,11 +3054,11 @@ return { assert.eq(result.exitcode, 1) -- Both files should still report almost_swapped (the non-ignored rule) - assert.strcontains(result.stdout, "[almost_swapped]", 2) + assert.strContains(result.stdout, "[almost_swapped]", 2) -- Only 1 divide_by_zero: from the non-ignored file, not from ignoredDir - assert.strcontains(result.stdout, "[divide_by_zero]", 1) - assert.strcontains(result.stdout, "nonIgnored.luau:1:11-16") -- the non-ignored file's violation + assert.strContains(result.stdout, "[divide_by_zero]", 1) + assert.strContains(result.stdout, "nonIgnored.luau:1:11-16") -- the non-ignored file's violation end) suite:case("defaultRulesConfiguredWithOffDoNotRun", function(assert) @@ -3307,9 +3307,9 @@ return { path.format(testSubDir), }) assert.eq(result.exitcode, 1) -- errors - assert.strnotcontains(result.stdout, "ignored.luau") - assert.strcontains(result.stdout, "valid.luau") - assert.strcontains(result.stdout, "Configured Rule Ran with options.a = 1") -- options are correctly passed through + assert.strNotContains(result.stdout, "ignored.luau") + assert.strContains(result.stdout, "valid.luau") + assert.strContains(result.stdout, "Configured Rule Ran with options.a = 1") -- options are correctly passed through end ) diff --git a/tests/cli/new.test.luau b/tests/cli/new.test.luau index aeec240d8..9f9f2a142 100644 --- a/tests/cli/new.test.luau +++ b/tests/cli/new.test.luau @@ -31,7 +31,7 @@ test.suite("LuteNew", function(suite) local mainPath = pathlib.join(workdir, projectName, "main.luau") assert.that(fs.exists(mainPath)) local contents = fs.readFileToString(mainPath) - assert.strcontains(contents, "Hello, world!") + assert.strContains(contents, "Hello, world!") end) suite:case("createsTestScript", function(assert) @@ -40,7 +40,7 @@ test.suite("LuteNew", function(suite) local testPath = pathlib.join(workdir, projectName, "main.test.luau") assert.that(fs.exists(testPath)) local contents = fs.readFileToString(testPath) - assert.strcontains(contents, "@std/test") + assert.strContains(contents, "@std/test") end) suite:case("createsConfigFile", function(assert) @@ -49,21 +49,21 @@ test.suite("LuteNew", function(suite) local configPath = pathlib.join(workdir, projectName, ".config.luau") assert.that(fs.exists(configPath)) local contents = fs.readFileToString(configPath) - assert.strcontains(contents, "strict") + assert.strContains(contents, "strict") end) suite:case("printSuccessMessage", function(assert) local result = process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, "Created project 'myproject'") + assert.strContains(result.stdout, "Created project 'myproject'") end) suite:case("failsWhenNoProjectNameGiven", function(assert) local result = process.run({ lutePath, "new" }, { cwd = pathlib.format(workdir) }) assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "Missing required argument ") + assert.strContains(result.stdout, "Missing required argument ") end) suite:case("failsWhenDirectoryAlreadyExists", function(assert) @@ -72,14 +72,14 @@ test.suite("LuteNew", function(suite) local result = process.run({ lutePath, "new", projectName }, { cwd = pathlib.format(workdir) }) assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, "already exists") + assert.strContains(result.stdout, "already exists") end) suite:case("showsHelpWithFlag", function(assert) local result = process.run({ lutePath, "new", "--help" }, { cwd = pathlib.format(workdir) }) assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, "Usage: lute new ") + assert.strContains(result.stdout, "Usage: lute new ") end) suite:case("typechecksOutOfTheBox", function(assert) diff --git a/tests/cli/run.test.luau b/tests/cli/run.test.luau index 2d32572a8..60f210136 100644 --- a/tests/cli/run.test.luau +++ b/tests/cli/run.test.luau @@ -35,6 +35,6 @@ test.suite("LuteRun", function(suite) ), nil ) - assert.strnotcontains(result.stdout, "Hello world") + assert.strNotContains(result.stdout, "Hello world") end) end) diff --git a/tests/cli/test.test.luau b/tests/cli/test.test.luau index fdb718554..e1cedc8b4 100644 --- a/tests/cli/test.test.luau +++ b/tests/cli/test.test.luau @@ -20,7 +20,7 @@ test.suite("LuteTestCommand", function(suite) -- Check that it exits successfully and prints help assert.eq(result.exitcode, 0) - assert.strcontains(result.stdout, "Usage:") + assert.strContains(result.stdout, "Usage:") end) suite:case("luteTestRunsTestsInDiscoveryFolder", function(assert) diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index a0194bf08..1f4373ae9 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -47,7 +47,7 @@ test.suite("LuteTransform", function(suite) local transformeeHandle = fs.open(transformeePath, "r") local transformeeContent = fs.read(transformeeHandle) assert.eq(transformeeContent:find("x ~= x"), nil) - assert.strcontains(transformeeContent, "math.isnan(x)") + assert.strContains(transformeeContent, "math.isnan(x)") fs.close(transformeeHandle) -- Teardown @@ -81,7 +81,7 @@ test.suite("LuteTransform", function(suite) h = fs.open(outputPath, "r") local outputContent = fs.read(h) assert.eq(outputContent:find("x ~= x"), nil) - assert.strcontains(outputContent, "math.isnan(x)") + assert.strContains(outputContent, "math.isnan(x)") fs.close(h) -- Teardown @@ -115,7 +115,7 @@ test.suite("LuteTransform", function(suite) local h = fs.open(outputPath, "r") local outputContent = fs.read(h) assert.eq(outputContent:find("x ~= x"), nil) - assert.strcontains(outputContent, "math.isnan(x)") + assert.strContains(outputContent, "math.isnan(x)") fs.close(h) -- Teardown diff --git a/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau b/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau index 60ce657bf..9444a96b4 100644 --- a/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau +++ b/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau @@ -1,7 +1,7 @@ local test = require("@std/test") test.case("strcontains_error", function(assert) - assert.strcontains("hello world", "goodbye", -1) + assert.strContains("hello world", "goodbye", -1) end) test.run() diff --git a/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau index 9831f751d..c18d21943 100644 --- a/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau +++ b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau @@ -2,7 +2,7 @@ local test = require("@std/test") test.suite("strcontains_failure_suite", function(suite) suite:case("strcontains_error", function(assert) - assert.strcontains("hello world", "goodbye") + assert.strContains("hello world", "goodbye") end) end) diff --git a/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau index 9831f751d..c18d21943 100644 --- a/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau +++ b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau @@ -2,7 +2,7 @@ local test = require("@std/test") test.suite("strcontains_failure_suite", function(suite) suite:case("strcontains_error", function(assert) - assert.strcontains("hello world", "goodbye") + assert.strContains("hello world", "goodbye") end) end) diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index 4a382bfcd..e391387f1 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -22,7 +22,7 @@ local function assertErrorsWithMsg( -- Check that the output specifies the inequal values assert.eq(result.exitcode, 1) - assert.strcontains(result.stdout, expectedErrMsg) + assert.strContains(result.stdout, expectedErrMsg) end test.suite("LuteStdTestFramework", function(suite) @@ -51,13 +51,13 @@ test.run() assert.eq(result.exitcode, 1) assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number - assert.strcontains(result.stdout, `Runtime error`) - assert.strcontains( + assert.strContains(result.stdout, `Runtime error`) + assert.strContains( result.stdout, `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:6` ) - assert.strcontains(result.stdout, `nil_field_access_test.test.luau:6: attempt to index nil with 'field'`) - assert.strcontains(result.stdout, `nil_field_access_test.test.luau:10`) + assert.strContains(result.stdout, `nil_field_access_test.test.luau:6: attempt to index nil with 'field'`) + assert.strContains(result.stdout, `nil_field_access_test.test.luau:10`) fs.remove(testFilePath) end) @@ -86,13 +86,13 @@ test.run() assert.eq(result.exitcode, 1) assert.eq(result.stdout:find("xpcall", 1, true), nil) -- Check that the error points to filepath + correct line number - assert.strcontains(result.stdout, `Runtime error`) - assert.strcontains( + assert.strContains(result.stdout, `Runtime error`) + assert.strContains( result.stdout, `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:5` ) - assert.strcontains(result.stdout, `nil_field_access_test.test.luau:5: attempt to index nil with 'field'`) - assert.strcontains(result.stdout, `nil_field_access_test.test.luau:8`) + assert.strContains(result.stdout, `nil_field_access_test.test.luau:5: attempt to index nil with 'field'`) + assert.strContains(result.stdout, `nil_field_access_test.test.luau:8`) fs.remove(testFilePath) end) @@ -283,14 +283,14 @@ test.run() ) end) - suite:case("assert.strcontainsInstancesNegative", function(assert) + suite:case("assert.strContainsInstancesNegative", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ local test = require("@std/test") test.case("strcontainsError", function(assert) - assert.strcontains("hello world", "goodbye", -1) + assert.strContains("hello world", "goodbye", -1) end) test.run() @@ -300,7 +300,7 @@ test.run() ) end) - suite:case("assert.strcontainsSingleInstanceFails", function(assert) + suite:case("assert.strContainsSingleInstanceFails", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ @@ -308,7 +308,7 @@ local test = require("@std/test") test.suite("strcontains_failure_suite", function(suite) suite:case("strcontainsError", function(assert) - assert.strcontains("hello world", "goodbye") + assert.strContains("hello world", "goodbye") end) end) @@ -319,7 +319,7 @@ test.run() ) end) - suite:case("assert.strcontainsMultipleInstanceFails", function(assert) + suite:case("assert.strContainsMultipleInstanceFails", function(assert) assertErrorsWithMsg( "assert_buffereq_fails", [[ @@ -327,7 +327,7 @@ local test = require("@std/test") test.suite("strcontains_failure_suite", function(suite) suite:case("strcontainsError", function(assert) - assert.strcontains("muahahahaha", "ha", 5) + assert.strContains("muahahahaha", "ha", 5) end) end) From f971a7a9254bde2eed9acfece8ea163b4cc57cf1 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 2 Apr 2026 10:42:38 -0700 Subject: [PATCH 445/642] Refactors the task library to conform to style conventions (#928) I've also fixed the remaining type checker errors in the examples, either by re-writing them or adding casts or annotations where appropriate. --- examples/net_example.luau | 37 ++++++------- examples/parallel_serve.luau | 2 +- examples/parallel_sort.luau | 17 ++++-- examples/parallel_sort_helper.luau | 3 +- examples/process.luau | 9 ++-- examples/spawn_example.luau | 58 ++++++++++++++++----- examples/task-resume.luau | 3 +- lute/std/libs/task.luau | 84 +++--------------------------- tools/check-faillist.txt | 13 ----- 9 files changed, 93 insertions(+), 133 deletions(-) diff --git a/examples/net_example.luau b/examples/net_example.luau index cf542586f..13ec2f7b2 100644 --- a/examples/net_example.luau +++ b/examples/net_example.luau @@ -4,29 +4,30 @@ local task = require("@std/task") local pp = require("@batteries/pp") -local t = task.create(function() - return net.request("https://en.wikipedia.org/") -end) - -print(task.await(t).status) +print(net.request("https://en.wikipedia.org/").status) -print(pp(task.await(task.create(net.request, "https://en.wikipedia.org/")).headers)) +print(pp(net.request("https://en.wikipedia.org/").headers)) -local t1 = task.create(net.request, "https://en.wikipedia.org/") -local t2 = task.create(net.request, "https://www.google.com/") -local t3 = task.create(net.request, "https://www.bing.com/") +local start = os.clock() --- print(task.awaitAll(t1, t2, t3)) +local results = {} +local done = 0 -local together = true -local start = os.clock() +task.spawn(function() + results[1] = net.request("https://en.wikipedia.org/") + done += 1 +end) +task.spawn(function() + results[2] = net.request("https://www.google.com/") + done += 1 +end) +task.spawn(function() + results[3] = net.request("https://www.bing.com/") + done += 1 +end) -if together then - task.awaitAll(t1, t2, t3) -else - task.await(t1) - task.await(t2) - task.await(t3) +while done < 3 do + task.wait() end print(os.clock() - start) diff --git a/examples/parallel_serve.luau b/examples/parallel_serve.luau index 7aa531031..edb285de3 100644 --- a/examples/parallel_serve.luau +++ b/examples/parallel_serve.luau @@ -4,7 +4,7 @@ local task = require("@std/task") local threadCount = 8 for _ = 1, threadCount do - task.create(vm.create("./parallel_serve_helper").serve) + task.spawn(vm.create("./parallel_serve_helper").serve) end print(`Created {threadCount} server threads`) diff --git a/examples/parallel_sort.luau b/examples/parallel_sort.luau index 012725786..a609d9db4 100644 --- a/examples/parallel_sort.luau +++ b/examples/parallel_sort.luau @@ -49,14 +49,23 @@ for _ = 1, threadCount do end local function parallelMergeSort(t: { T }, comp: (T, T) -> lt): { T } - local slices = {} + local results: { { T } } = {} + local done = 0 for i = 1, threadCount do - table.insert(slices, task.create(threads[i].sort, getslice(t, i, threadCount))) + local idx = i + local slice = getslice(t, idx, threadCount) + task.spawn(function() + results[idx] = threads[idx].sort(slice) + done += 1 + end) end - local tomerge = table.pack(task.awaitAll(table.unpack(slices))) - tomerge.n = nil :: any + while done < threadCount do + task.wait() + end + + local tomerge = results while #tomerge > 1 do local next = {} diff --git a/examples/parallel_sort_helper.luau b/examples/parallel_sort_helper.luau index 95cad9793..69da3ced4 100644 --- a/examples/parallel_sort_helper.luau +++ b/examples/parallel_sort_helper.luau @@ -1,6 +1,7 @@ return { sort = function(t) - table.sort(t, function(a, b) + -- LUAUFIX - we cannot figure out the type of the array elements, even with an annotation on t. + table.sort(t, function(a: number, b: number) return a < b end) return t diff --git a/examples/process.luau b/examples/process.luau index 9bf3859d5..fcfb8ca6b 100644 --- a/examples/process.luau +++ b/examples/process.luau @@ -1,5 +1,4 @@ local process = require("@std/process") -local task = require("@std/task") process.system("echo hello", {}) @@ -8,11 +7,9 @@ local result = process.run({ "echo", "Hello, lute!" }) print(result.exitcode) print(result.stdout) -local t1 = task.create(process.run, { "echo", "-n", "One" }) -local t2 = task.create(process.run, { "echo", "-n", "Two" }) -local t3 = task.create(process.run, { "echo", "-n", "Three" }) - -local r1, r2, r3 = task.awaitAll(t1, t2, t3) +local r1 = process.run({ "echo", "-n", "One" }) +local r2 = process.run({ "echo", "-n", "Two" }) +local r3 = process.run({ "echo", "-n", "Three" }) print(r1.stdout) print(r2.stdout) diff --git a/examples/spawn_example.luau b/examples/spawn_example.luau index 822878c7f..3641819bc 100644 --- a/examples/spawn_example.luau +++ b/examples/spawn_example.luau @@ -15,11 +15,9 @@ print("done inline") do local start = os.clock() - task.await(task.create(function() - print("fib(10):", vm1.fib(10)) - print("fib(20):", vm1.fib(20)) - print("fib(33):", vm1.fib(33)) - end)) + print("fib(10):", vm1.fib(10)) + print("fib(20):", vm1.fib(20)) + print("fib(33):", vm1.fib(33)) print("multiple direct fib in", os.clock() - start) end @@ -27,7 +25,7 @@ end do local start = os.clock() - print("fib(33):", task.await(task.create(vm1.fib, 33))) + print("fib(33):", vm1.fib(33)) print("single task fib in", os.clock() - start) end @@ -35,7 +33,7 @@ end do local start = os.clock() - print("fib(33):", task.await(task.create(vm1.fibTable, { n = 33 }))) + print("fib(33):", vm1.fibTable({ n = 33 })) print("single task fibTable in", os.clock() - start) end @@ -43,9 +41,27 @@ end do local start = os.clock() - local s1, s2, s3 = task.create(vm1.fib, 33), task.create(vm1.fib, 33), task.create(vm1.fib, 33) + local results = {} + local done = 0 - print("fib(33):", task.awaitAll(s1, s2, s3)) + task.spawn(function() + results[1] = vm1.fib(33) + done += 1 + end) + task.spawn(function() + results[2] = vm1.fib(33) + done += 1 + end) + task.spawn(function() + results[3] = vm1.fib(33) + done += 1 + end) + + while done < 3 do + task.wait() + end + + print("fib(33):", results[1], results[2], results[3]) print("3 serial fibs in", os.clock() - start) end @@ -53,9 +69,27 @@ end do local start = os.clock() - local p1, p2, p3 = task.create(vm1.fib, 33), task.create(vm2.fib, 33), task.create(vm3.fib, 33) - - print("fib(33):", task.awaitAll(p1, p2, p3)) + local results = {} + local done = 0 + + task.spawn(function() + results[1] = vm1.fib(33) + done += 1 + end) + task.spawn(function() + results[2] = vm2.fib(33) + done += 1 + end) + task.spawn(function() + results[3] = vm3.fib(33) + done += 1 + end) + + while done < 3 do + task.wait() + end + + print("fib(33):", results[1], results[2], results[3]) print("3 parallel fibs in", os.clock() - start) end diff --git a/examples/task-resume.luau b/examples/task-resume.luau index 8e8b6e1fb..7cc22edf0 100644 --- a/examples/task-resume.luau +++ b/examples/task-resume.luau @@ -2,7 +2,8 @@ local task = require("@lute/task") local c = coroutine.create(function() print("hello") - print(coroutine.yield()) + -- We don't know what subsequent calls to coroutine.yield produce. + print(coroutine.yield() :: any) print("wtf") end) diff --git a/lute/std/libs/task.luau b/lute/std/libs/task.luau index f374288ee..8a793f656 100644 --- a/lute/std/libs/task.luau +++ b/lute/std/libs/task.luau @@ -5,96 +5,26 @@ local lutetask = require("@lute/task") -local tasklib = {} +local taskLib = {} -export type Task = { - success: boolean?, - result: any, - co: thread, -} - -function tasklib.spawn(routine: ((T...) -> U...) | thread, ...: T...): thread +function taskLib.spawn(routine: ((T...) -> U...) | thread, ...: T...): thread return lutetask.spawn(routine, ...) end -function tasklib.defer(routine: ((T...) -> U...) | thread, ...: T...): thread +function taskLib.defer(routine: ((T...) -> U...) | thread, ...: T...): thread return lutetask.defer(routine, ...) end -function tasklib.delay(dur: number, routine: ((T...) -> U...) | thread, ...: T...): thread +function taskLib.delay(dur: number, routine: ((T...) -> U...) | thread, ...: T...): thread return lutetask.delay(dur, routine, ...) end -function tasklib.wait(dur: number?): number +function taskLib.wait(dur: number?): number return lutetask.wait(dur) end -function tasklib.cancel(thread: thread) +function taskLib.cancel(thread: thread) coroutine.close(thread) end -function tasklib.create(f, ...): Task - local data = {} - - data.co = coroutine.create(function(...) - local success, result = pcall(f, ...) - - data.success = success - data.result = result - end) - - coroutine.resume(data.co, ...) - return data :: Task -end - -function tasklib.await(t: Task): any - if not t.co then - error(`await: argument 1 is not a task`) - end - - while t.success == nil do - lutetask.deferSelf() - end - - if t.success then - return t.result - else - error(t.result) - end -end - -function tasklib.awaitAll(...: Task): ...unknown - local tasks = table.pack(...) - - for i, v in ipairs(tasks) do - if not v.co then - error(`awaitAll: argument {i} is not a task`) - end - end - - local done = false - - while not done do - done = true - for _, v in ipairs(tasks) do - if v.success == nil then - done = false - lutetask.deferSelf() - end - end - end - - local results = {} - - for _, v in ipairs(tasks) do - if v.success then - table.insert(results, v.result) - else - error(v.result) - end - end - - return table.unpack(results) -end - -return table.freeze(tasklib) +return table.freeze(taskLib) diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt index edb64664a..391d9460d 100755 --- a/tools/check-faillist.txt +++ b/tools/check-faillist.txt @@ -1,16 +1,3 @@ -examples/net_example.luau:8:2-48 -examples/net_example.luau:13:33-43 -examples/net_example.luau:15:24-34 -examples/net_example.luau:16:24-34 -examples/net_example.luau:17:24-34 -examples/parallel_sort_helper.luau:4:11-15 -examples/process.luau:11:24-34 -examples/process.luau:12:24-34 -examples/process.luau:13:24-34 -examples/process.luau:17:7-15 -examples/process.luau:18:7-15 -examples/process.luau:19:7-15 -examples/task-resume.luau:3:38-100 lute/cli/commands/lint/rules/almost_swapped.luau:92:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 From d1df8083cdce55b299d6944c86f48aa7df9e7304 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 2 Apr 2026 10:49:40 -0700 Subject: [PATCH 446/642] Refactors time library to expose camelCased method names (#931) --- definitions/time.luau | 16 +++++----- examples/time_instants.luau | 2 +- lute/time/src/time.cpp | 32 +++++++++---------- tests/std/time.test.luau | 62 ++++++++++++++++++------------------- 4 files changed, 56 insertions(+), 56 deletions(-) diff --git a/definitions/time.luau b/definitions/time.luau index cc94422ee..38785ad12 100644 --- a/definitions/time.luau +++ b/definitions/time.luau @@ -54,35 +54,35 @@ function instant.elapsed(self: Instant): number error("not implemented") end -function duration_ops.tonanoseconds(self: Duration): number +function duration_ops.toNanoseconds(self: Duration): number error("not implemented") end -function duration_ops.tomicroseconds(self: Duration): number +function duration_ops.toMicroseconds(self: Duration): number error("not implemented") end -function duration_ops.tomilliseconds(self: Duration): number +function duration_ops.toMilliseconds(self: Duration): number error("not implemented") end -function duration_ops.toseconds(self: Duration): number +function duration_ops.toSeconds(self: Duration): number error("not implemented") end -function duration_ops.tominutes(self: Duration): number +function duration_ops.toMinutes(self: Duration): number error("not implemented") end -function duration_ops.tohours(self: Duration): number +function duration_ops.toHours(self: Duration): number error("not implemented") end -function duration_ops.todays(self: Duration): number +function duration_ops.toDays(self: Duration): number error("not implemented") end -function duration_ops.toweeks(self: Duration): number +function duration_ops.toWeeks(self: Duration): number error("not implemented") end diff --git a/examples/time_instants.luau b/examples/time_instants.luau index 00abc4535..ad71d4f45 100644 --- a/examples/time_instants.luau +++ b/examples/time_instants.luau @@ -9,4 +9,4 @@ local stop = time.now() assert(stop > start) assert(stop ~= start) assert(start == start) -print((stop - start):tomicroseconds()) +print((stop - start):toMicroseconds()) diff --git a/lute/time/src/time.cpp b/lute/time/src/time.cpp index 5328d4eb9..1498fd48a 100644 --- a/lute/time/src/time.cpp +++ b/lute/time/src/time.cpp @@ -499,29 +499,29 @@ static void init_duration_lib(lua_State* L) // __index table lua_createtable(L, 0, 11); - lua_pushcfunction(L, duration_tonanoseconds, "Duration__tonanoseconds"); - lua_setfield(L, -2, "tonanoseconds"); + lua_pushcfunction(L, duration_tonanoseconds, "Duration__toNanoseconds"); + lua_setfield(L, -2, "toNanoseconds"); - lua_pushcfunction(L, duration_tomicroseconds, "Duration__tomicroseconds"); - lua_setfield(L, -2, "tomicroseconds"); + lua_pushcfunction(L, duration_tomicroseconds, "Duration__toMicroseconds"); + lua_setfield(L, -2, "toMicroseconds"); - lua_pushcfunction(L, duration_tomilliseconds, "Duration__tomilliseconds"); - lua_setfield(L, -2, "tomilliseconds"); + lua_pushcfunction(L, duration_tomilliseconds, "Duration__toMilliseconds"); + lua_setfield(L, -2, "toMilliseconds"); - lua_pushcfunction(L, duration_toseconds, "Duration__toseconds"); - lua_setfield(L, -2, "toseconds"); + lua_pushcfunction(L, duration_toseconds, "Duration__toSeconds"); + lua_setfield(L, -2, "toSeconds"); - lua_pushcfunction(L, duration_tominutes, "Duration__tominutes"); - lua_setfield(L, -2, "tominutes"); + lua_pushcfunction(L, duration_tominutes, "Duration__toMinutes"); + lua_setfield(L, -2, "toMinutes"); - lua_pushcfunction(L, duration_tohours, "Duration__tohours"); - lua_setfield(L, -2, "tohours"); + lua_pushcfunction(L, duration_tohours, "Duration__toHours"); + lua_setfield(L, -2, "toHours"); - lua_pushcfunction(L, duration_todays, "Duration__todays"); - lua_setfield(L, -2, "todays"); + lua_pushcfunction(L, duration_todays, "Duration__toDays"); + lua_setfield(L, -2, "toDays"); - lua_pushcfunction(L, duration_toweeks, "Duration__toweeks"); - lua_setfield(L, -2, "toweeks"); + lua_pushcfunction(L, duration_toweeks, "Duration__toWeeks"); + lua_setfield(L, -2, "toWeeks"); lua_pushcfunction(L, duration_subsecnanos, "Duration__subsecnanos"); lua_setfield(L, -2, "subsecnanos"); diff --git a/tests/std/time.test.luau b/tests/std/time.test.luau index c2e4ba06c..02a22b4e4 100644 --- a/tests/std/time.test.luau +++ b/tests/std/time.test.luau @@ -7,61 +7,61 @@ test.suite("TimeSuite", function(suite) suite:case("constructorsSecondsMillisMicroNanos", function(assert) -- seconds local d1 = duration.seconds(5) - assert.eq(d1:tomilliseconds(), 5000) - assert.eq(d1:tomicroseconds(), 5_000_000) - assert.eq(d1:tonanoseconds(), 5_000_000_000) + assert.eq(d1:toMilliseconds(), 5000) + assert.eq(d1:toMicroseconds(), 5_000_000) + assert.eq(d1:toNanoseconds(), 5_000_000_000) -- milliseconds local d2 = duration.milliseconds(1500) - assert.eq(d2:tomilliseconds(), 1500) + assert.eq(d2:toMilliseconds(), 1500) assert.eq(d2:subsecmillis(), 500) -- 1.5 is exactly representable; check seconds too - assert.eq(d2:toseconds(), 1.5) + assert.eq(d2:toSeconds(), 1.5) -- microseconds local d3 = duration.microseconds(1_234_567) - assert.eq(d3:tomicroseconds(), 1_234_567) + assert.eq(d3:toMicroseconds(), 1_234_567) assert.eq(d3:subsecmicros(), 234_567) - assert.eq(d3:tomilliseconds(), 1234) -- floor down + assert.eq(d3:toMilliseconds(), 1234) -- floor down -- nanoseconds (with normalization) local d4 = duration.nanoseconds(1_234_567_890) - assert.eq(d4:tonanoseconds(), 1_234_567_890) + assert.eq(d4:toNanoseconds(), 1_234_567_890) assert.eq(d4:subsecnanos(), 234_567_890) - assert.eq(d4:tomilliseconds(), 1234) + assert.eq(d4:toMilliseconds(), 1234) -- boundary normalization: exactly 1s in nanoseconds local d5 = duration.nanoseconds(1_000_000_000) - assert.eq(d5:toseconds(), 1) + assert.eq(d5:toSeconds(), 1) assert.eq(d5:subsecnanos(), 0) end) suite:case("constructorsMinutesHoursDaysWeeks", function(assert) local mins = duration.minutes(3) - assert.eq(mins:toseconds(), 180) + assert.eq(mins:toSeconds(), 180) local hours = duration.hours(2) - assert.eq(hours:toseconds(), 2 * 60 * 60) + assert.eq(hours:toSeconds(), 2 * 60 * 60) local days = duration.days(1) - assert.eq(days:toseconds(), 24 * 60 * 60) + assert.eq(days:toSeconds(), 24 * 60 * 60) local weeks = duration.weeks(2) - assert.eq(weeks:toseconds(), 2 * 7 * 24 * 60 * 60) + assert.eq(weeks:toSeconds(), 2 * 7 * 24 * 60 * 60) end) suite:case("createRawValuesAndNormalizationViaOps", function(assert) local d = duration.create(1, 500_000_000) - assert.eq(d:toseconds(), 1.5) + assert.eq(d:toSeconds(), 1.5) assert.eq(d:subsecnanos(), 500_000_000) - assert.eq(d:tomilliseconds(), 1500) + assert.eq(d:toMilliseconds(), 1500) local d2 = duration.create(1, 1_500_000_000) - assert.eq(d2:toseconds(), 2.5) + assert.eq(d2:toSeconds(), 2.5) assert.eq(d2:subsecnanos(), 500_000_000) local normalized = d2 + duration.seconds(0) - assert.eq(normalized:toseconds(), 2.5) + assert.eq(normalized:toSeconds(), 2.5) assert.eq(normalized:subsecnanos(), 500_000_000) end) @@ -78,27 +78,27 @@ test.suite("TimeSuite", function(suite) suite:case("conversionsToMsUsNs", function(assert) local d = duration.seconds(2) - assert.eq(d:tomilliseconds(), 2000) - assert.eq(d:tomicroseconds(), 2_000_000) - assert.eq(d:tonanoseconds(), 2_000_000_000) + assert.eq(d:toMilliseconds(), 2000) + assert.eq(d:toMicroseconds(), 2_000_000) + assert.eq(d:toNanoseconds(), 2_000_000_000) local mix = duration.milliseconds(1234) + duration.microseconds(567_000) + duration.nanoseconds(890) -- 1234ms + 567ms + 0.000890ms = 1801.00089ms - assert.eq(mix:tomilliseconds(), 1801) - assert.eq(mix:tomicroseconds(), 1_801_000) - assert.eq(mix:tonanoseconds(), 1_801_000_890) + assert.eq(mix:toMilliseconds(), 1801) + assert.eq(mix:toMicroseconds(), 1_801_000) + assert.eq(mix:toNanoseconds(), 1_801_000_890) end) suite:case("additionNormalizesNanoseconds", function(assert) local a = duration.milliseconds(800) local b = duration.milliseconds(300) local c = a + b - assert.eq(c:tomilliseconds(), 1100) + assert.eq(c:toMilliseconds(), 1100) local a2 = duration.nanoseconds(800_000_000) local b2 = duration.nanoseconds(300_000_000) local c2 = a2 + b2 -- should carry +1s and leave 100_000_000ns - assert.eq(c2:toseconds(), 1.1) + assert.eq(c2:toSeconds(), 1.1) assert.eq(c2:subsecnanos(), 100_000_000) end) @@ -106,22 +106,22 @@ test.suite("TimeSuite", function(suite) local a = duration.milliseconds(1100) -- 1s 100ms local b = duration.milliseconds(200) local c = a - b -- 900ms after borrow - assert.eq(c:tomilliseconds(), 900) + assert.eq(c:toMilliseconds(), 900) local d = duration.seconds(5) - duration.seconds(5) - assert.eq(d:tomilliseconds(), 0) + assert.eq(d:toMilliseconds(), 0) end) suite:case("multiplicationAndDivision", function(assert) local a = duration.milliseconds(250) local twice = a * 2 - assert.eq(twice:tomilliseconds(), 500) + assert.eq(twice:toMilliseconds(), 500) local b = duration.milliseconds(600) * 3 - assert.eq(b:tomilliseconds(), 1800) + assert.eq(b:toMilliseconds(), 1800) local c = duration.seconds(2) / 2 - assert.eq(c:tomilliseconds(), 1000) + assert.eq(c:toMilliseconds(), 1000) end) suite:case("comparisonsAndEquality", function(assert) From bf9347facc81d2f7b660e3b0734b41813e8c1cad Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 2 Apr 2026 13:47:02 -0700 Subject: [PATCH 447/642] Moves test.run() into @std/test/runner (#930) We should still support writing tests without using `lute test`, but now the method for doing this is explicitly moved into @std/test/runner. --- examples/testing.luau | 3 +- lute/std/libs/test/env.luau | 11 ++++ lute/std/libs/test/init.luau | 18 +------ lute/std/libs/test/runner.luau | 13 +++++ lute/std/libs/test/types.luau | 2 - .../assert_buffereq_unequal_length.snap | 2 +- .../assert_buffereq_unequal_length.snap.luau | 3 +- .../assert_eq_error_msg_in_case.snap | 2 +- .../assert_eq_error_msg_in_case.snap.luau | 3 +- .../assert_eq_error_msg_in_suite.snap | 2 +- .../assert_eq_error_msg_in_suite.snap.luau | 3 +- .../snapshots/assert_error_eq_no_error.snap | 2 +- .../assert_error_eq_no_error.snap.luau | 3 +- .../assert_erroreq_error_message.snap | 4 +- .../assert_erroreq_error_message.snap.luau | 3 +- .../assert_neq_error_message_in_case.snap | 2 +- ...assert_neq_error_message_in_case.snap.luau | 3 +- ...assert_strcontains_instances_negative.snap | 4 +- ...t_strcontains_instances_negative.snap.luau | 3 +- ...t_strcontains_multiple_instance_fails.snap | 4 +- ...contains_multiple_instance_fails.snap.luau | 3 +- ...ert_strcontains_single_instance_fails.snap | 4 +- ...trcontains_single_instance_fails.snap.luau | 3 +- .../assert_tableeq_error_message_in_case.snap | 2 +- ...rt_tableeq_error_message_in_case.snap.luau | 3 +- ...assert_tableeq_error_message_in_suite.snap | 2 +- ...t_tableeq_error_message_in_suite.snap.luau | 3 +- .../assertneq_error_message_in_suite.snap | 2 +- ...assertneq_error_message_in_suite.snap.luau | 3 +- .../std/snapshots/buffereq_failure_suite.snap | 2 +- .../buffereq_failure_suite.snap.luau | 3 +- .../runtime_error_doesnt_report_xpcall.snap | 14 ++--- ...ntime_error_doesnt_report_xpcall.snap.luau | 3 +- ...ntime_error_doesnt_report_xpcall_case.snap | 14 ++--- ..._error_doesnt_report_xpcall_case.snap.luau | 3 +- tests/std/test.test.luau | 52 ++++++++++++------- 36 files changed, 121 insertions(+), 85 deletions(-) create mode 100644 lute/std/libs/test/env.luau diff --git a/examples/testing.luau b/examples/testing.luau index d80c19325..452c23fe8 100644 --- a/examples/testing.luau +++ b/examples/testing.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.case("foo", function(assert) assert.eq(3, 3) @@ -67,4 +68,4 @@ test.suite("MySuite", function(suite) end) end) -test.run() +runner.runRegisteredTests() diff --git a/lute/std/libs/test/env.luau b/lute/std/libs/test/env.luau new file mode 100644 index 000000000..3934713e8 --- /dev/null +++ b/lute/std/libs/test/env.luau @@ -0,0 +1,11 @@ +--!strict +--@std/test/types +-- Environment to store discovered tests + +local types = require("./types") +type TestEnvironment = types.TestEnvironment + +local env: TestEnvironment = { anonymous = {}, suites = {}, suiteindex = {}, caseindex = {} } + +-- not frozen, the internal test library needs to mutate this during test discovery +return env diff --git a/lute/std/libs/test/init.luau b/lute/std/libs/test/init.luau index 5e9b0c996..dcbb6bb34 100644 --- a/lute/std/libs/test/init.luau +++ b/lute/std/libs/test/init.luau @@ -3,15 +3,11 @@ -- Standard test library for Luau local types = require("@self/types") - -local reporter = require("@self/reporter") -local runner = require("@self/runner") -local ps = require("@std/process") +local env = require("@self/env") type Test = types.Test type TestCase = types.TestCase type TestSuite = types.TestSuite -type TestEnvironment = types.TestEnvironment type TestReporter = types.TestReporter local TestSuite = {} @@ -49,8 +45,6 @@ function TestSuite:afterAll(hook: () -> ()) self._afterAll = hook end -local env: TestEnvironment = { anonymous = {}, suites = {}, suiteindex = {}, caseindex = {} } - -- Test library export surface local test = {} @@ -98,14 +92,4 @@ function test._registered() }) end --- run all the tests -function test.run() - local failed = runner.run(env) - reporter.simple(failed) - if failed.failed ~= 0 then - ps.exit(1) - end - ps.exit(0) -end - return table.freeze(test) diff --git a/lute/std/libs/test/runner.luau b/lute/std/libs/test/runner.luau index b8dfba723..8b0e536cb 100644 --- a/lute/std/libs/test/runner.luau +++ b/lute/std/libs/test/runner.luau @@ -1,10 +1,13 @@ --!strict -- @std/test/runner -- Standard test runner library for Luau +local ps = require("@std/process") local asserts = require("./assert") local failure = require("./failure") +local reporter = require("./reporter") local types = require("./types") +local env = require("./env") type Test = types.Test type TestCase = types.TestCase @@ -158,4 +161,14 @@ function runner.run(env: TestEnvironment): TestRunResult } end +-- run all the tests +function runner.runRegisteredTests() + local result = runner.run(env) + reporter.simple(result) + if result.failed ~= 0 then + ps.exit(1) + end + ps.exit(0) +end + return table.freeze(runner) diff --git a/lute/std/libs/test/types.luau b/lute/std/libs/test/types.luau index b0ce32393..98e9e1903 100644 --- a/lute/std/libs/test/types.luau +++ b/lute/std/libs/test/types.luau @@ -2,8 +2,6 @@ -- @std/test/types -- Standard test types library for Luau --- Test Reporting - export type Hook = "beforeEach" | "beforeAll" | "afterEach" | "afterAll" export type Failure = { diff --git a/tests/std/snapshots/assert_buffereq_unequal_length.snap b/tests/std/snapshots/assert_buffereq_unequal_length.snap index 91e854ba3..958b435ef 100755 --- a/tests/std/snapshots/assert_buffereq_unequal_length.snap +++ b/tests/std/snapshots/assert_buffereq_unequal_length.snap @@ -6,7 +6,7 @@ TEST RESULTS Failed Tests (1): ❌ buffereq_error - /tests/std/snapshots/assert_buffereq_unequal_length.snap.luau:6 + /tests/std/snapshots/assert_buffereq_unequal_length.snap.luau:7 buffereq: buffers are of unequal length -------------------------------------------------- diff --git a/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau b/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau index eb8b2a305..4d83db28f 100644 --- a/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau +++ b/tests/std/snapshots/assert_buffereq_unequal_length.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.case("buffereq_error", function(assert) local buf1 = buffer.create(1) @@ -6,4 +7,4 @@ test.case("buffereq_error", function(assert) assert.eq(buf1, buf2) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_eq_error_msg_in_case.snap b/tests/std/snapshots/assert_eq_error_msg_in_case.snap index ec8872eb3..d90b68788 100755 --- a/tests/std/snapshots/assert_eq_error_msg_in_case.snap +++ b/tests/std/snapshots/assert_eq_error_msg_in_case.snap @@ -6,7 +6,7 @@ TEST RESULTS Failed Tests (1): ❌ eq_error - /tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau:4 + /tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau:5 eq: 1 ~= 2 -------------------------------------------------- diff --git a/tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau b/tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau index 35896d6b0..4a759a01d 100644 --- a/tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau +++ b/tests/std/snapshots/assert_eq_error_msg_in_case.snap.luau @@ -1,7 +1,8 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.case("eq_error", function(assert) assert.eq(1, 2) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_eq_error_msg_in_suite.snap b/tests/std/snapshots/assert_eq_error_msg_in_suite.snap index 7b1ea31c3..ff77fb48f 100755 --- a/tests/std/snapshots/assert_eq_error_msg_in_suite.snap +++ b/tests/std/snapshots/assert_eq_error_msg_in_suite.snap @@ -6,7 +6,7 @@ TEST RESULTS Failed Tests (1): ❌ eq_failure_suite.eq_error - /tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau:5 + /tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau:6 eq: 1 ~= 2 -------------------------------------------------- diff --git a/tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau b/tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau index 781b4bf1a..1b72f59e6 100644 --- a/tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau +++ b/tests/std/snapshots/assert_eq_error_msg_in_suite.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.suite("eq_failure_suite", function(suite) suite:case("eq_error", function(assert) @@ -6,4 +7,4 @@ test.suite("eq_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_error_eq_no_error.snap b/tests/std/snapshots/assert_error_eq_no_error.snap index bd2f8ef7a..bdba429c7 100755 --- a/tests/std/snapshots/assert_error_eq_no_error.snap +++ b/tests/std/snapshots/assert_error_eq_no_error.snap @@ -6,7 +6,7 @@ TEST RESULTS Failed Tests (1): ❌ throwsWithError - /tests/std/snapshots/assert_error_eq_no_error.snap.luau:4 + /tests/std/snapshots/assert_error_eq_no_error.snap.luau:5 throwsWith: Expected function to throw/raise error. -------------------------------------------------- diff --git a/tests/std/snapshots/assert_error_eq_no_error.snap.luau b/tests/std/snapshots/assert_error_eq_no_error.snap.luau index 4c2439f35..c5d370721 100644 --- a/tests/std/snapshots/assert_error_eq_no_error.snap.luau +++ b/tests/std/snapshots/assert_error_eq_no_error.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.case("throwsWithError", function(assert) assert.throwsWith("expected message", function() @@ -6,4 +7,4 @@ test.case("throwsWithError", function(assert) end) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_erroreq_error_message.snap b/tests/std/snapshots/assert_erroreq_error_message.snap index 438b63fb1..e47aae4af 100755 --- a/tests/std/snapshots/assert_erroreq_error_message.snap +++ b/tests/std/snapshots/assert_erroreq_error_message.snap @@ -6,8 +6,8 @@ TEST RESULTS Failed Tests (1): ❌ throwsWith_failure_suite.throwsWith_error - /tests/std/snapshots/assert_erroreq_error_message.snap.luau:5 - throwsWith: Expected suffix of error message "expected message", but got "/tests/std/snapshots/assert_erroreq_error_message.snap.luau:6: wrong message" + /tests/std/snapshots/assert_erroreq_error_message.snap.luau:6 + throwsWith: Expected suffix of error message "expected message", but got "/tests/std/snapshots/assert_erroreq_error_message.snap.luau:7: wrong message" -------------------------------------------------- Total: 1 diff --git a/tests/std/snapshots/assert_erroreq_error_message.snap.luau b/tests/std/snapshots/assert_erroreq_error_message.snap.luau index 89a148918..6bcdeff06 100644 --- a/tests/std/snapshots/assert_erroreq_error_message.snap.luau +++ b/tests/std/snapshots/assert_erroreq_error_message.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.suite("throwsWith_failure_suite", function(suite) suite:case("throwsWith_error", function(assert) @@ -8,4 +9,4 @@ test.suite("throwsWith_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_neq_error_message_in_case.snap b/tests/std/snapshots/assert_neq_error_message_in_case.snap index 87c06895f..3bb9c844d 100755 --- a/tests/std/snapshots/assert_neq_error_message_in_case.snap +++ b/tests/std/snapshots/assert_neq_error_message_in_case.snap @@ -6,7 +6,7 @@ TEST RESULTS Failed Tests (1): ❌ neq_error - /tests/std/snapshots/assert_neq_error_message_in_case.snap.luau:4 + /tests/std/snapshots/assert_neq_error_message_in_case.snap.luau:5 neq: 1 == 1 -------------------------------------------------- diff --git a/tests/std/snapshots/assert_neq_error_message_in_case.snap.luau b/tests/std/snapshots/assert_neq_error_message_in_case.snap.luau index 1cc64666d..b68de2627 100644 --- a/tests/std/snapshots/assert_neq_error_message_in_case.snap.luau +++ b/tests/std/snapshots/assert_neq_error_message_in_case.snap.luau @@ -1,7 +1,8 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.case("neq_error", function(assert) assert.neq(1, 1) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_strcontains_instances_negative.snap b/tests/std/snapshots/assert_strcontains_instances_negative.snap index bf3f6cbad..67273a84d 100755 --- a/tests/std/snapshots/assert_strcontains_instances_negative.snap +++ b/tests/std/snapshots/assert_strcontains_instances_negative.snap @@ -6,8 +6,8 @@ TEST RESULTS Failed Tests (1): ❌ strcontains_error - /tests/std/snapshots/assert_strcontains_instances_negative.snap.luau:4 - strcontains: instances must be greater than 0 + /tests/std/snapshots/assert_strcontains_instances_negative.snap.luau:5 + strContains: instances must be greater than 0 -------------------------------------------------- Total: 1 diff --git a/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau b/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau index 9444a96b4..5401d0587 100644 --- a/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau +++ b/tests/std/snapshots/assert_strcontains_instances_negative.snap.luau @@ -1,7 +1,8 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.case("strcontains_error", function(assert) assert.strContains("hello world", "goodbye", -1) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap index 1838d16d7..07db0167b 100755 --- a/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap +++ b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap @@ -6,8 +6,8 @@ TEST RESULTS Failed Tests (1): ❌ strcontains_failure_suite.strcontains_error - /tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau:5 - strcontains: Expected "hello world" to contain "goodbye". + /tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau:6 + strContains: Expected "hello world" to contain "goodbye". -------------------------------------------------- Total: 1 diff --git a/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau index c18d21943..24154c7ac 100644 --- a/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau +++ b/tests/std/snapshots/assert_strcontains_multiple_instance_fails.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.suite("strcontains_failure_suite", function(suite) suite:case("strcontains_error", function(assert) @@ -6,4 +7,4 @@ test.suite("strcontains_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_strcontains_single_instance_fails.snap b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap index 947429995..dbd6f8f09 100755 --- a/tests/std/snapshots/assert_strcontains_single_instance_fails.snap +++ b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap @@ -6,8 +6,8 @@ TEST RESULTS Failed Tests (1): ❌ strcontains_failure_suite.strcontains_error - /tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau:5 - strcontains: Expected "hello world" to contain "goodbye". + /tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau:6 + strContains: Expected "hello world" to contain "goodbye". -------------------------------------------------- Total: 1 diff --git a/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau index c18d21943..24154c7ac 100644 --- a/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau +++ b/tests/std/snapshots/assert_strcontains_single_instance_fails.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.suite("strcontains_failure_suite", function(suite) suite:case("strcontains_error", function(assert) @@ -6,4 +7,4 @@ test.suite("strcontains_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_case.snap b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap index 6b150e603..d17c1542b 100755 --- a/tests/std/snapshots/assert_tableeq_error_message_in_case.snap +++ b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap @@ -6,7 +6,7 @@ TEST RESULTS Failed Tests (1): ❌ tableeq_error - /tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau:4 + /tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau:5 tableeq: lhs[a] = 1 but rhs[a] = 2 -------------------------------------------------- diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau index 806f23c99..5a5f7e79a 100644 --- a/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau +++ b/tests/std/snapshots/assert_tableeq_error_message_in_case.snap.luau @@ -1,7 +1,8 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.case("tableeq_error", function(assert) assert.eq({ a = 1 }, { a = 2 }) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap index 82d5a0f22..f5561ec82 100755 --- a/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap +++ b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap @@ -6,7 +6,7 @@ TEST RESULTS Failed Tests (1): ❌ tableeq_failure_suite.tableeq_error - /tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau:5 + /tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau:6 tableeq: lhs[a] = 1 but rhs[a] = 2 -------------------------------------------------- diff --git a/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau index 0c7807bcf..c1d8ddb1b 100644 --- a/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau +++ b/tests/std/snapshots/assert_tableeq_error_message_in_suite.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.suite("tableeq_failure_suite", function(suite) suite:case("tableeq_error", function(assert) @@ -6,4 +7,4 @@ test.suite("tableeq_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/assertneq_error_message_in_suite.snap b/tests/std/snapshots/assertneq_error_message_in_suite.snap index 1d7712b08..e2092a984 100755 --- a/tests/std/snapshots/assertneq_error_message_in_suite.snap +++ b/tests/std/snapshots/assertneq_error_message_in_suite.snap @@ -6,7 +6,7 @@ TEST RESULTS Failed Tests (1): ❌ neq_failure_suite.neq_error - /tests/std/snapshots/assertneq_error_message_in_suite.snap.luau:5 + /tests/std/snapshots/assertneq_error_message_in_suite.snap.luau:6 neq: 1 == 1 -------------------------------------------------- diff --git a/tests/std/snapshots/assertneq_error_message_in_suite.snap.luau b/tests/std/snapshots/assertneq_error_message_in_suite.snap.luau index 3418f8570..f496ed7b5 100644 --- a/tests/std/snapshots/assertneq_error_message_in_suite.snap.luau +++ b/tests/std/snapshots/assertneq_error_message_in_suite.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.suite("neq_failure_suite", function(suite) suite:case("neq_error", function(assert) @@ -6,4 +7,4 @@ test.suite("neq_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/buffereq_failure_suite.snap b/tests/std/snapshots/buffereq_failure_suite.snap index ecb961346..8a874a8a4 100755 --- a/tests/std/snapshots/buffereq_failure_suite.snap +++ b/tests/std/snapshots/buffereq_failure_suite.snap @@ -6,7 +6,7 @@ TEST RESULTS Failed Tests (1): ❌ buffereq_failure_suite.buffereq_error - /tests/std/snapshots/buffereq_failure_suite.snap.luau:9 + /tests/std/snapshots/buffereq_failure_suite.snap.luau:10 buffereq: lhs[0] = 54, but rhs[0] = 55 -------------------------------------------------- diff --git a/tests/std/snapshots/buffereq_failure_suite.snap.luau b/tests/std/snapshots/buffereq_failure_suite.snap.luau index c2925a2c3..e01786e12 100644 --- a/tests/std/snapshots/buffereq_failure_suite.snap.luau +++ b/tests/std/snapshots/buffereq_failure_suite.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.suite("buffereq_failure_suite", function(suite) suite:case("buffereq_error", function(assert) @@ -10,4 +11,4 @@ test.suite("buffereq_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap index 74d9ee400..418d2da45 100755 --- a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap @@ -6,15 +6,15 @@ TEST RESULTS Failed Tests (1): ❌ nil_field_suite.access_field_of_nil - Runtime error: /tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6: attempt to index nil with 'field' + Runtime error: /tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:7: attempt to index nil with 'field' Stacktrace: -/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6: attempt to index nil with 'field' +/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:7: attempt to index nil with 'field' @std/test/failure.luau:29 function runtimeerror -@std/test/runner.luau:117 function handlefailure -/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:6 -@std/test/runner.luau:130 function run -@std/test/init.luau:103 function run -/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:11 +@std/test/runner.luau:119 function handlefailure +/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:7 +@std/test/runner.luau:132 function run +@std/test/runner.luau:166 function runRegisteredTests +/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau:12 -------------------------------------------------- diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau index a526eed36..0d9914eee 100644 --- a/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.suite("nil_field_suite", function(suite) suite:case("access_field_of_nil", function(assert) @@ -8,4 +9,4 @@ test.suite("nil_field_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap index 925db0e15..ffed582f6 100755 --- a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap @@ -6,15 +6,15 @@ TEST RESULTS Failed Tests (1): ❌ access_field_of_nil - Runtime error: /tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5: attempt to index nil with 'field' + Runtime error: /tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:6: attempt to index nil with 'field' Stacktrace: -/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5: attempt to index nil with 'field' +/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:6: attempt to index nil with 'field' @std/test/failure.luau:29 function runtimeerror -@std/test/runner.luau:87 function handlefailure -/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:5 -@std/test/runner.luau:93 function run -@std/test/init.luau:103 function run -/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:9 +@std/test/runner.luau:89 function handlefailure +/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:6 +@std/test/runner.luau:95 function run +@std/test/runner.luau:166 function runRegisteredTests +/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau:10 -------------------------------------------------- diff --git a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau index bd7a4c48c..88ea1afad 100644 --- a/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau +++ b/tests/std/snapshots/runtime_error_doesnt_report_xpcall_case.snap.luau @@ -1,4 +1,5 @@ local test = require("@std/test") +local runner = require("@std/test/runner") test.case("access_field_of_nil", function(assert) local t = nil :: any @@ -6,4 +7,4 @@ test.case("access_field_of_nil", function(assert) print(value) end) -test.run() +runner.runRegisteredTests() diff --git a/tests/std/test.test.luau b/tests/std/test.test.luau index e391387f1..95470bf4c 100644 --- a/tests/std/test.test.luau +++ b/tests/std/test.test.luau @@ -32,8 +32,8 @@ test.suite("LuteStdTestFramework", function(suite) fs.writeStringToFile( testFilePath, [[ +local runner = require("@std/test/runner") local test = require("@std/test") - test.suite("nil_field_suite", function(suite) suite:case("accessFieldOfNil", function(assert) local t = nil @@ -41,7 +41,7 @@ test.suite("nil_field_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() ]] ) @@ -68,6 +68,7 @@ test.run() fs.writeStringToFile( testFilePath, [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.case("accessFieldOfNil", function(assert) @@ -75,7 +76,7 @@ test.case("accessFieldOfNil", function(assert) local value = t.field -- This will cause an error end) -test.run() +runner.runRegisteredTests() ]] ) @@ -89,10 +90,10 @@ test.run() assert.strContains(result.stdout, `Runtime error`) assert.strContains( result.stdout, - `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:5` + `{if system.win32 then path.format(testFilePath):gsub("\\", "/") else path.format(testFilePath)}:6` ) - assert.strContains(result.stdout, `nil_field_access_test.test.luau:5: attempt to index nil with 'field'`) - assert.strContains(result.stdout, `nil_field_access_test.test.luau:8`) + assert.strContains(result.stdout, `nil_field_access_test.test.luau:6: attempt to index nil with 'field'`) + assert.strContains(result.stdout, `nil_field_access_test.test.luau:9`) fs.remove(testFilePath) end) @@ -101,13 +102,14 @@ test.run() assertErrorsWithMsg( "assert_eq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.case("eqError", function(assert) assert.eq(1, 2) end) -test.run() +runner.runRegisteredTests() ]], "eq: 1 ~= 2", assert @@ -118,6 +120,7 @@ test.run() assertErrorsWithMsg( "assert_eq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.suite("eq_failure_suite", function(suite) @@ -126,7 +129,7 @@ test.suite("eq_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() ]], "eq: 1 ~= 2", assert @@ -137,13 +140,14 @@ test.run() assertErrorsWithMsg( "assert_neq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.case("neqError", function(assert) assert.neq(1, 1) end) -test.run() +runner.runRegisteredTests() ]], "neq: 1 == 1", assert @@ -154,6 +158,7 @@ test.run() assertErrorsWithMsg( "assert_neq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.suite("neq_failure_suite", function(suite) @@ -162,7 +167,7 @@ test.suite("neq_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() ]], "neq: 1 == 1", assert @@ -173,13 +178,14 @@ test.run() assertErrorsWithMsg( "assert_tableeq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.case("tableeqError", function(assert) assert.eq({ a = 1 }, { a = 2 }) end) -test.run() +runner.runRegisteredTests() ]], "tableeq: lhs[a] = 1 but rhs[a] = 2", assert @@ -190,6 +196,7 @@ test.run() assertErrorsWithMsg( "assert_tableeq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.suite("tableeq_failure_suite", function(suite) @@ -198,7 +205,7 @@ test.suite("tableeq_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() ]], "tableeq: lhs[a] = 1 but rhs[a] = 2", assert @@ -209,6 +216,7 @@ test.run() assertErrorsWithMsg( "assert_buffereq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.case("buffereqError", function(assert) @@ -217,7 +225,7 @@ test.case("buffereqError", function(assert) assert.eq(buf1, buf2) end) -test.run() +runner.runRegisteredTests() ]], "buffers are of unequal length", assert @@ -228,6 +236,7 @@ test.run() assertErrorsWithMsg( "assert_buffereq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.suite("buffereq_failure_suite", function(suite) @@ -240,7 +249,7 @@ test.suite("buffereq_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() ]], "buffereq: lhs[0] = 54, but rhs[0] = 55", assert @@ -251,13 +260,14 @@ test.run() assertErrorsWithMsg( "assert_buffereq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.case("throwsWithError", function(assert) assert.throwsWith("expected message", function() return end) end) -test.run() +runner.runRegisteredTests() ]], "Expected function to throw/raise error.", assert @@ -268,6 +278,7 @@ test.run() assertErrorsWithMsg( "assert_buffereq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.suite("throwsWith_failure_suite", function(suite) @@ -276,7 +287,7 @@ test.suite("throwsWith_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() ]], 'Expected suffix of error message "expected message", but got ', -- the full message contains tmpdir path to test file assert @@ -287,13 +298,14 @@ test.run() assertErrorsWithMsg( "assert_buffereq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.case("strcontainsError", function(assert) assert.strContains("hello world", "goodbye", -1) end) -test.run() +runner.runRegisteredTests() ]], "instances must be greater than 0", assert @@ -304,6 +316,7 @@ test.run() assertErrorsWithMsg( "assert_buffereq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.suite("strcontains_failure_suite", function(suite) @@ -312,7 +325,7 @@ test.suite("strcontains_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() ]], 'Expected "hello world" to contain "goodbye".', assert @@ -323,6 +336,7 @@ test.run() assertErrorsWithMsg( "assert_buffereq_fails", [[ +local runner = require("@std/test/runner") local test = require("@std/test") test.suite("strcontains_failure_suite", function(suite) @@ -331,7 +345,7 @@ test.suite("strcontains_failure_suite", function(suite) end) end) -test.run() +runner.runRegisteredTests() ]], 'Expected "muahahahaha" to contain "ha" 5 times.', assert From e711f1fe39f12606cff09f01a280016a35885bbf Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Fri, 3 Apr 2026 12:42:07 -0700 Subject: [PATCH 448/642] Makes `lute check` a blocking check (#933) This PR makes `lute check` a blocking check, instead of a 'don't add new type errors to lute' job. For the remaining type check errors in the lint rules, I've explicitly silenced them and cut this ticket :https://github.com/luau-lang/lute/issues/932 to track un-silencing them when we bump our luau dependency to version 716. --- .github/workflows/ci.yml | 14 +++- .../commands/lint/rules/almost_swapped.luau | 37 +++++---- .../lint/rules/parenthesized_conditions.luau | 75 +++++++++++-------- tools/check-faillist.txt | 13 ---- 4 files changed, 79 insertions(+), 60 deletions(-) delete mode 100755 tools/check-faillist.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d813c23b5..93860b954 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -193,7 +193,19 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Run Lute Check - run: ${{ steps.build_lute.outputs.exe_path }} tools/check.luau ${{ steps.build_lute.outputs.exe_path }} + run: | + # @lute typechecks + ${{ steps.build_lute.outputs.exe_path }} check definitions/**/*.luau + # @std and @cli commands typechecks + ${{ steps.build_lute.outputs.exe_path }} check lute/**/*.luau + # @batteries typechecks + ${{ steps.build_lute.outputs.exe_path }} check batteries/**/*.luau + # All tests typecheck + ${{ steps.build_lute.outputs.exe_path }} check tests/**/*.test.luau + # All tools typecheck + ${{ steps.build_lute.outputs.exe_path }} check tools/*.luau + # All examples typecheck + ${{ steps.build_lute.outputs.exe_path }} check examples/*.luau aggregator: name: Gated Commits diff --git a/lute/cli/commands/lint/rules/almost_swapped.luau b/lute/cli/commands/lint/rules/almost_swapped.luau index ec8212ae1..ef1b9bee6 100644 --- a/lute/cli/commands/lint/rules/almost_swapped.luau +++ b/lute/cli/commands/lint/rules/almost_swapped.luau @@ -89,22 +89,27 @@ local function lint( local currValStr = stringext.trim(printer.printNode(currVal)) local suggestedFix = `{currVarStr}, {currValStr} = {currValStr}, {currVarStr}` - table.insert(violations, { -- LUAUFIX: inserted table isn't inferred as a LintViolation - lintname = name, - location = syntax.span.create({ - beginLine = currStat.location.beginLine, - beginColumn = currStat.location.beginColumn, - endLine = nextStat.location.endLine, - endColumn = nextStat.location.endColumn, - }), - message = message, - severity = "warning" :: "warning", -- LUAUFIX: cast needed because severity isn't inferred as a singleton - sourcepath = sourcepath, - suggestedfix = { - fix = suggestedFix, - }, - target = target, - }) + -- LUAUFIX once we bump luau version to 716 + -- https://github.com/luau-lang/lute/issues/932 + table.insert( + violations, + { -- LUAUFIX: inserted table isn't inferred as a LintViolation + lintname = name, + location = syntax.span.create({ + beginLine = currStat.location.beginLine, + beginColumn = currStat.location.beginColumn, + endLine = nextStat.location.endLine, + endColumn = nextStat.location.endColumn, + }), + message = message, + severity = "warning" :: "warning", -- LUAUFIX: cast needed because severity isn't inferred as a singleton + sourcepath = sourcepath, + suggestedfix = { + fix = suggestedFix, + }, + target = target, + } :: lintTypes.LintViolation + ) end end end diff --git a/lute/cli/commands/lint/rules/parenthesized_conditions.luau b/lute/cli/commands/lint/rules/parenthesized_conditions.luau index acd5dbccc..6bc0e37c1 100644 --- a/lute/cli/commands/lint/rules/parenthesized_conditions.luau +++ b/lute/cli/commands/lint/rules/parenthesized_conditions.luau @@ -24,32 +24,42 @@ local function lint( end) :forEach(function(ifStat) if ifStat.condition.tag == "group" then - table.insert(violations, { - lintname = name, - location = ifStat.condition.location, - message = message, - severity = "info", - sourcepath = sourcepath, - suggestedfix = { - fix = syntaxPrinter.printNode(ifStat.condition.expression), - }, - target = target, - }) - end - - for _, elseIfStat in ifStat.elseifs do - if elseIfStat.condition.tag == "group" then - table.insert(violations, { + -- LUAUFIX remove cast once we bump luau version 716 + -- https://github.com/luau-lang/lute/issues/932 + table.insert( + violations, + { lintname = name, - location = elseIfStat.condition.location, + location = ifStat.condition.location, message = message, severity = "info", sourcepath = sourcepath, suggestedfix = { - fix = syntaxPrinter.printNode(elseIfStat.condition.expression), + fix = syntaxPrinter.printNode(ifStat.condition.expression), }, target = target, - }) + } :: lintTypes.LintViolation + ) + end + + for _, elseIfStat in ifStat.elseifs do + if elseIfStat.condition.tag == "group" then + -- LUAUFIX remove cast once we bump luau version 716 + -- https://github.com/luau-lang/lute/issues/932 + table.insert( + violations, + { + lintname = name, + location = elseIfStat.condition.location, + message = message, + severity = "info", + sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printNode(elseIfStat.condition.expression), + }, + target = target, + } :: lintTypes.LintViolation + ) end end end) @@ -65,17 +75,22 @@ local function lint( return n.condition.tag == "group" end) :forEach(function(n) - table.insert(violations, { - lintname = name, - location = n.condition.location, - message = message, - severity = "info", - sourcepath = sourcepath, - suggestedfix = { - fix = syntaxPrinter.printNode((n.condition :: syntax.AstExprGroup).expression), - }, - target = target, - }) + -- LUAUFIX remove cast once we bump luau version to 716 + -- https://github.com/luau-lang/lute/issues/932 + table.insert( + violations, + { + lintname = name, + location = n.condition.location, + message = message, + severity = "info", + sourcepath = sourcepath, + suggestedfix = { + fix = syntaxPrinter.printNode((n.condition :: syntax.AstExprGroup).expression), + }, + target = target, + } :: lintTypes.LintViolation + ) end) return violations diff --git a/tools/check-faillist.txt b/tools/check-faillist.txt deleted file mode 100755 index 391d9460d..000000000 --- a/tools/check-faillist.txt +++ /dev/null @@ -1,13 +0,0 @@ -lute/cli/commands/lint/rules/almost_swapped.luau:92:5-16 -lute/cli/commands/lint/rules/parenthesized_conditions.luau:27:5-16 -lute/cli/commands/lint/rules/parenthesized_conditions.luau:42:6-17 -lute/cli/commands/lint/rules/parenthesized_conditions.luau:68:4-15 -tests/src/packages/package_aware_require/dep/module.luau:1:16-30 -tests/src/packages/pkgrun_with_lockfile/Packages/dep/src/init.luau:1:16-38 -tests/src/require/config_tests/config_ambiguity/requirer.luau:1:8-22 -tests/src/require/config_tests/config_cannot_be_required/requirer.luau:1:8-27 -tests/src/require/config_tests/tilde_config/main.luau:1:12-32 -tests/src/require/without_config/ambiguous_directory_requirer.luau:1:16-58 -tests/src/require/without_config/ambiguous_file_requirer.luau:1:16-53 -tests/src/staticrequires/circular_a.luau:2:11-33 -tests/src/staticrequires/circular_b.luau:2:11-33 From e77905c8e82d8eb19ec34e9848b729af78465cde Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 6 Apr 2026 09:42:59 -0700 Subject: [PATCH 449/642] Ensures that CI and local development workflows behave the same (#941) I discovered an issue in CI where `lute test` would fail to require the `difftext` test file. Instead of causing CI to fail, this would silently skip that test suite. Moreover, there is a divergence in how tests run in the presence of the .ci.luaurc file vs the .luaurc file. Today, we don't define a batteries alias in the .ci.luaurc because we have tests that rely on asserting that user code cannot require batteries. This also causes a divergence in how C++ and luau tests are run - luau tests pass with the regular .luaurc because some tests rely on the .luaurc to figure out where batteries are located. - luau tests fail with the .luaurc.ci because that doesn't have a batteries alias. - C++ tests pass with the .luaurc.i because that doesn't have a batteries alias - C++ tests fail with the regular .luaurc because the batteries alias is defined in the root of the repo The cause of the luau tests failing is that `loadModule` loads a test case as user code, which has no access to @batteries. It uses a .luaurc to find the local `@batteries`. The `difftext` battery itself requires the `deque` module in batteries, which causes us to look for a @batteries alias somewhere (which doesn't exist in CI). To fix this, I've added a .luaurc with the `@batteries` alias defined to the batteries/ directories. I've also: - gotten rid of the step that changes the luaurc we run with in CI - added .luaurc's to files that rely on local access to batteries, either for execution (examples/ tools/) or for editor support (std/libs). - added a ps.exit(1), so that test running failures will cause CI to fail. --- .github/workflows/ci.yml | 5 ----- .luaurc | 1 - .luaurc.ci => batteries/.luaurc | 4 ++-- examples/.luaurc | 6 ++++++ lute/cli/commands/test/init.luau | 1 + lute/std/libs/.luaurc | 6 ++++++ tools/.luaurc | 6 ++++++ 7 files changed, 21 insertions(+), 8 deletions(-) rename .luaurc.ci => batteries/.luaurc (56%) create mode 100644 examples/.luaurc create mode 100644 lute/std/libs/.luaurc create mode 100644 tools/.luaurc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93860b954..4cc87f0c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,11 +65,6 @@ jobs: options: ${{ matrix.options }} token: ${{ secrets.GITHUB_TOKEN }} use-bootstrap: ${{ matrix.use-bootstrap || 'false' }} - - - name: Use CI .luaurc file - if: ${{ steps.should_run.outputs.proceed == 'true' }} - run: rm .luaurc && mv .luaurc.ci .luaurc - - name: Run Luau Tests (POSIX) if: ${{ steps.should_run.outputs.proceed == 'true' }} run: ${{ steps.build_lute.outputs.exe_path }} test diff --git a/.luaurc b/.luaurc index 01c26f8f6..acee50a66 100644 --- a/.luaurc +++ b/.luaurc @@ -1,7 +1,6 @@ { "languageMode": "strict", "aliases": { - "batteries": "./batteries", "std": "./lute/std/libs", "lute": "./definitions", "commands": "./lute/cli/commands" diff --git a/.luaurc.ci b/batteries/.luaurc similarity index 56% rename from .luaurc.ci rename to batteries/.luaurc index 822341bf4..598234a83 100644 --- a/.luaurc.ci +++ b/batteries/.luaurc @@ -1,6 +1,6 @@ { "languageMode": "strict", "aliases": { - "commands": "./lute/cli/commands" + "batteries": "./" } -} \ No newline at end of file +} diff --git a/examples/.luaurc b/examples/.luaurc new file mode 100644 index 000000000..2b445749e --- /dev/null +++ b/examples/.luaurc @@ -0,0 +1,6 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "../batteries" + } +} diff --git a/lute/cli/commands/test/init.luau b/lute/cli/commands/test/init.luau index 6d8ba850e..5efe3b475 100644 --- a/lute/cli/commands/test/init.luau +++ b/lute/cli/commands/test/init.luau @@ -18,6 +18,7 @@ local function loadtests(testfiles: { path.Path }) if not success then print(`Error loading {path.format(p)}: {err}`) + ps.exit(1) end end end diff --git a/lute/std/libs/.luaurc b/lute/std/libs/.luaurc new file mode 100644 index 000000000..62e27d4e6 --- /dev/null +++ b/lute/std/libs/.luaurc @@ -0,0 +1,6 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "../../../batteries" + } +} diff --git a/tools/.luaurc b/tools/.luaurc new file mode 100644 index 000000000..2b445749e --- /dev/null +++ b/tools/.luaurc @@ -0,0 +1,6 @@ +{ + "languageMode": "strict", + "aliases": { + "batteries": "../batteries" + } +} From 30d6c597d6206b947af643ac331cbc5274710095 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:43:14 -0700 Subject: [PATCH 450/642] Update Luau to 0.715 (#937) **Luau**: Updated from `0.714` to `0.715` **Release Notes:** https://github.com/luau-lang/luau/releases/tag/0.715 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* --------- Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Vighnesh Vijay --- extern/luau.tune | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/luau.tune b/extern/luau.tune index 9ab91f9ad..fc6c953fa 100644 --- a/extern/luau.tune +++ b/extern/luau.tune @@ -1,5 +1,5 @@ [dependency] name = "luau" remote = "https://github.com/luau-lang/luau.git" -branch = "0.714" -revision = "27718747a24448ea6418f424963c741f57263743" +branch = "0.715" +revision = "40d4815888f63362a6cb79b3e74c4aafa0b2cbf4" From 305cccfb05384294933869fcc6ab79cd179c577d Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Mon, 6 Apr 2026 16:36:18 -0700 Subject: [PATCH 451/642] Threads a reporter through the lute `Runtime` (#944) This lets us examine stacktraces/other runtime errors being reported in tests, since we can initialize the runtime with a `TestReporter`. --- lute/cli/CMakeLists.txt | 3 +-- lute/cli/src/climain.cpp | 6 +++--- lute/common/CMakeLists.txt | 1 + lute/{cli => common}/include/lute/reporter.h | 0 lute/runtime/CMakeLists.txt | 3 ++- lute/runtime/include/lute/runtime.h | 5 ++++- lute/runtime/src/runtime.cpp | 11 ++++++----- lute/vm/src/spawn.cpp | 2 +- tests/src/cliruntimefixture.cpp | 2 +- tests/src/packagerequire.test.cpp | 2 +- 10 files changed, 20 insertions(+), 15 deletions(-) rename lute/{cli => common}/include/lute/reporter.h (100%) diff --git a/lute/cli/CMakeLists.txt b/lute/cli/CMakeLists.txt index 408e61256..14a71c43d 100644 --- a/lute/cli/CMakeLists.txt +++ b/lute/cli/CMakeLists.txt @@ -29,7 +29,6 @@ target_sources(Lute.CLI.lib PRIVATE include/lute/luauflags.h include/lute/packagerun.h include/lute/profiler.h - include/lute/reporter.h include/lute/requiresetup.h include/lute/staticrequires.h include/lute/tc.h @@ -63,5 +62,5 @@ target_sources(Lute.CLI PRIVATE set_target_properties(Lute.CLI PROPERTIES OUTPUT_NAME lute) target_compile_features(Lute.CLI PUBLIC cxx_std_17) -target_link_libraries(Lute.CLI PRIVATE Lute.CLI.lib) +target_link_libraries(Lute.CLI PRIVATE Lute.CLI.lib Lute.Common) target_compile_options(Lute.CLI PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index ad5592cef..618988e6a 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -394,7 +394,7 @@ int handleRunCommand(int argc, char** argv, int argOffset, bool packageAwareness return 1; } - Runtime runtime; + Runtime runtime{reporter}; lua_State* L; if (packageAwareness) @@ -648,7 +648,7 @@ void setupVersionLibrary(lua_State* L) int handleCliCommand(CliCommandResult result, int program_argc, char** program_argv, LuteReporter& reporter) { - Runtime runtime; + Runtime runtime{reporter}; lua_State* L = setupCliCommandState(runtime, setupVersionLibrary); std::string bytecode = Luau::compile(std::string(result.contents), copts()); @@ -665,7 +665,7 @@ int cliMain(int argc, char** argv, LuteReporter& reporter) LuteExecutable exe{argv[0], reporter}; if (auto payload = exe.extract()) { - Runtime runtime; + Runtime runtime{reporter}; lua_State* GL = setupBundleState(runtime, payload->luauConfigFiles, payload->filePathToBytecode); std::string entryPoint = payload->entryPointPath; diff --git a/lute/common/CMakeLists.txt b/lute/common/CMakeLists.txt index 30e434f0b..2df4ae155 100644 --- a/lute/common/CMakeLists.txt +++ b/lute/common/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(Lute.Common STATIC) target_sources(Lute.Common PRIVATE include/lute/common.h include/lute/fileutils.h + include/lute/reporter.h src/fileutils.cpp ) diff --git a/lute/cli/include/lute/reporter.h b/lute/common/include/lute/reporter.h similarity index 100% rename from lute/cli/include/lute/reporter.h rename to lute/common/include/lute/reporter.h diff --git a/lute/runtime/CMakeLists.txt b/lute/runtime/CMakeLists.txt index 4d4912ee6..1adf8c774 100644 --- a/lute/runtime/CMakeLists.txt +++ b/lute/runtime/CMakeLists.txt @@ -15,5 +15,6 @@ target_sources(Lute.Runtime PRIVATE target_compile_features(Lute.Runtime PUBLIC cxx_std_17) target_include_directories(Lute.Runtime PUBLIC "include" ${LIBUV_INCLUDE_DIR}) -target_link_libraries(Lute.Runtime PRIVATE Luau.Require Luau.VM Lute.Common uv_a) +target_link_libraries(Lute.Runtime PUBLIC Lute.Common) +target_link_libraries(Lute.Runtime PRIVATE Luau.Require Luau.VM uv_a) target_compile_options(Lute.Runtime PRIVATE ${LUTE_OPTIONS}) diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index 1c13b02cc..cf39ee3e4 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -1,6 +1,7 @@ #pragma once #include "lute/ref.h" +#include "lute/reporter.h" #include "Luau/Variant.h" #include "Luau/VecDeque.h" @@ -44,7 +45,7 @@ using RuntimeStep = Luau::Variant; struct Runtime { - Runtime(); + Runtime(LuteReporter& reporter); ~Runtime(); bool runToCompletion(); @@ -76,6 +77,8 @@ struct Runtime uv_loop_t* getEventLoop(); + LuteReporter& reporter; + // VM for this runtime std::unique_ptr globalState; diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index ac3d4684b..4f39e9b81 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -16,8 +16,9 @@ static void lua_close_checked(lua_State* L) lua_close(L); } -Runtime::Runtime() - : globalState(nullptr, lua_close_checked) +Runtime::Runtime(LuteReporter& reporter) + : reporter(reporter) + , globalState(nullptr, lua_close_checked) , dataCopy(nullptr, lua_close_checked) { @@ -84,7 +85,7 @@ RuntimeStep Runtime::runOnce() if (L == nullptr) { - fprintf(stderr, "Cannot resume a non-thread reference"); + reporter.reportError("Cannot resume a non-thread reference"); return StepErr{L}; } @@ -143,7 +144,7 @@ bool Runtime::runToCompletion() { if (err->L == nullptr) { - fprintf(stderr, "lua_State* L is nullptr"); + reporter.reportError("lua_State* L is nullptr"); return false; } @@ -172,7 +173,7 @@ void Runtime::reportError(lua_State* L) error += "\nstacktrace:\n"; error += lua_debugtrace(L); - fprintf(stderr, "%s", error.c_str()); + reporter.reportError(error); } void Runtime::runContinuously() diff --git a/lute/vm/src/spawn.cpp b/lute/vm/src/spawn.cpp index 29bf4f659..8e8dddeff 100644 --- a/lute/vm/src/spawn.cpp +++ b/lute/vm/src/spawn.cpp @@ -201,7 +201,7 @@ int lua_spawn(lua_State* L) { const char* file = luaL_checkstring(L, 1); - auto child = std::make_shared(); + auto child = std::make_shared(getRuntime(L)->reporter); setupState( *child, diff --git a/tests/src/cliruntimefixture.cpp b/tests/src/cliruntimefixture.cpp index 4cfec59e1..e286de94c 100644 --- a/tests/src/cliruntimefixture.cpp +++ b/tests/src/cliruntimefixture.cpp @@ -18,7 +18,7 @@ static int report(lua_State* L) } CliRuntimeFixture::CliRuntimeFixture() - : runtime(std::make_unique()) + : runtime(std::make_unique(getReporter())) { L = setupRunState( *runtime, diff --git a/tests/src/packagerequire.test.cpp b/tests/src/packagerequire.test.cpp index b70654f7f..a450d76c6 100644 --- a/tests/src/packagerequire.test.cpp +++ b/tests/src/packagerequire.test.cpp @@ -22,7 +22,7 @@ TEST_CASE_FIXTURE(LuteFixture, "package_aware_require") { - Runtime runtime; + Runtime runtime{getReporter()}; lua_State* L = setupState( runtime, From 3ebc381778acab4e87ee855b3d5a368f025b54dc Mon Sep 17 00:00:00 2001 From: jkelaty-rbx <78873527+jkelaty-rbx@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:25:09 -0700 Subject: [PATCH 452/642] Make loom's security check more robust (#940) This also fixes a bug where files containing ".." (such as: https://github.com/luau-lang/lute/blob/primary/tests/std/JSONParsingTestSuite/n_structure_angle_bracket_..json) would prevent the repo from being used as a package dependency. --- .../commands/pkg/loom-core/src/packagesource/git.luau | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau b/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau index 9bd9ccbe2..d77e703f2 100644 --- a/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau +++ b/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau @@ -24,11 +24,6 @@ local function sanitizePath(path: string): string error("Path too long (max " .. maxPathLength .. " characters)") end - -- Security checks - prevent directory traversal - if path:match("%.%.") then - error("Path contains '..' (directory traversal attempt)") - end - -- Remove/reject dangerous characters if path:match("[\0\r\n]") then error("Path contains invalid control characters") @@ -40,6 +35,11 @@ local function sanitizePath(path: string): string -- Remove leading slashes but preserve trailing slash for directories normalized = normalized:gsub("^/+", "") + -- Security check - prevent directory traversal via ".." path components + if normalized == ".." or normalized:match("^%.%./") or normalized:match("/%.%./") or normalized:match("/%.%.$") then + error("Path contains '..' (directory traversal attempt)") + end + return normalized end From 38681ac0bb71a89eba094bfe99a050f26c0b1888 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 7 Apr 2026 11:28:42 -0700 Subject: [PATCH 453/642] Adds support for a variadic argument to the CLI battery (#946) This PR adds support for a variadic argument to the CLI parsing battery. Any arguments not consumed by an option or positional argument which come before a `--` is seen will now be consumed by the variadic argument if present. If a variadic argument isn't specified, then arguments before a `--` that aren't otherwise consumed will still be considered forwarded. I also added a test suite for the CLI battery. --------- Co-authored-by: ariel --- batteries/cli.luau | 31 +++- tests/batteries/cli.test.luau | 266 ++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 tests/batteries/cli.test.luau diff --git a/batteries/cli.luau b/batteries/cli.luau index 813c201a6..a0ba11a94 100644 --- a/batteries/cli.luau +++ b/batteries/cli.luau @@ -1,7 +1,7 @@ local cli = {} cli.__index = cli -type ArgKind = "positional" | "flag" | "option" +type ArgKind = "positional" | "flag" | "option" | "variadic" type ArgOptions = { help: string?, aliases: { string }?, @@ -18,12 +18,14 @@ type ArgData = { type ParseResult = { values: { [string]: string? }, flags: { [string]: boolean }, + variadics: { string }, fwdArgs: { string }, } type ParserData = { arguments: { [number]: ArgData }, positional: { [number]: ArgData }, + variadicArg: ArgData?, parsed: ParseResult, } @@ -34,7 +36,8 @@ function cli.parser(): Parser local self = { arguments = {}, positional = {}, - parsed = { values = {}, flags = {}, fwdArgs = {} }, + variadicArg = nil :: ArgData?, + parsed = { values = {}, flags = {}, variadics = {}, fwdArgs = {} }, } return setmetatable(self, cli) @@ -50,6 +53,8 @@ function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions) table.insert(self.arguments, argument) if kind == "positional" then table.insert(self.positional, argument) + elseif kind == "variadic" then + self.variadicArg = argument end end @@ -64,7 +69,7 @@ function cli.parse(self: Parser, args: { string }): () -- if the argument is exactly "--", we pass everything along if arg == "--" then - table.move(args, i + 1, #args, 1, self.parsed.fwdArgs) + table.move(args, i + 1, #args, #self.parsed.fwdArgs + 1, self.parsed.fwdArgs) break end @@ -133,16 +138,24 @@ function cli.parse(self: Parser, args: { string }): () continue end + -- if we have a variadic argument, we can take this argument as part of it + if self.variadicArg then + table.insert(self.parsed.variadics, arg) + continue + end + -- otherwise, the argument is forwarded on table.insert(self.parsed.fwdArgs, arg) end -- check that all required arguments are present, and set any default values as needed for _, argument in self.arguments do - assert(argument) - - if argument.options.required and self.parsed.values[argument.name] == nil then - assert(self.parsed.values[argument.name], "Missing required argument: " .. argument.name) + if argument.options.required then + if argument.kind == "variadic" then + assert(#self.parsed.variadics > 0, "Missing required variadic argument: " .. argument.name) + else + assert(self.parsed.values[argument.name], "Missing required argument: " .. argument.name) + end end if self.parsed.values[argument.name] == nil and argument.options.default then @@ -159,6 +172,10 @@ function cli.has(self: Parser, name: string): boolean return self.parsed.flags[name] ~= nil end +function cli.variadics(self: Parser): { string } + return self.parsed.variadics +end + function cli.forwarded(self: Parser): { string }? return self.parsed.fwdArgs end diff --git a/tests/batteries/cli.test.luau b/tests/batteries/cli.test.luau new file mode 100644 index 000000000..7fa4d1245 --- /dev/null +++ b/tests/batteries/cli.test.luau @@ -0,0 +1,266 @@ +local test = require("@std/test") +local cli = require("@batteries/cli") + +test.suite("CLI flags", function(suite) + suite:case("longFlag", function(assert) + local p = cli.parser() + p:add("verbose", "flag", {}) + p:parse({ "--verbose" }) + assert.that(p:has("verbose")) + end) + + suite:case("shortFlag", function(assert) + local p = cli.parser() + p:add("verbose", "flag", { aliases = { "v" } }) + p:parse({ "-v" }) + assert.that(p:has("verbose")) + end) + + suite:case("absentFlagIsFalse", function(assert) + local p = cli.parser() + p:add("verbose", "flag", {}) + p:parse({}) + assert.eq(p:has("verbose"), false) + end) + + suite:case("combinedShortFlags", function(assert) + local p = cli.parser() + p:add("all", "flag", { aliases = { "a" } }) + p:add("long", "flag", { aliases = { "l" } }) + p:add("human", "flag", { aliases = { "h" } }) + p:parse({ "-alh" }) + assert.that(p:has("all")) + assert.that(p:has("long")) + assert.that(p:has("human")) + end) + + suite:case("unknownLongFlagThrows", function(assert) + local p = cli.parser() + assert.throwsWith("Unknown argument: unknown", function() + p:parse({ "--unknown" }) + end) + end) + + suite:case("unknownShortFlagThrows", function(assert) + local p = cli.parser() + assert.throwsWith("Unknown argument: z", function() + p:parse({ "-z" }) + end) + end) +end) + +test.suite("CLI options", function(suite) + suite:case("longOption", function(assert) + local p = cli.parser() + p:add("output", "option", {}) + p:parse({ "--output", "file.txt" }) + assert.eq(p:get("output"), "file.txt") + end) + + suite:case("shortOption", function(assert) + local p = cli.parser() + p:add("output", "option", { aliases = { "o" } }) + p:parse({ "-o", "file.txt" }) + assert.eq(p:get("output"), "file.txt") + end) + + suite:case("missingOptionValueThrows", function(assert) + local p = cli.parser() + p:add("output", "option", {}) + assert.throwsWith("Missing value for argument: output", function() + p:parse({ "--output" }) + end) + end) + + suite:case("defaultAppliedWhenAbsent", function(assert) + local p = cli.parser() + p:add("output", "option", { default = "out.txt" }) + p:parse({}) + assert.eq(p:get("output"), "out.txt") + end) + + suite:case("defaultNotAppliedWhenPresent", function(assert) + local p = cli.parser() + p:add("output", "option", { default = "out.txt" }) + p:parse({ "--output", "other.txt" }) + assert.eq(p:get("output"), "other.txt") + end) + + suite:case("absentOptionReturnsNil", function(assert) + local p = cli.parser() + p:add("output", "option", {}) + p:parse({}) + assert.eq(p:get("output"), nil) + end) + + suite:case("requiredOptionThrowsWhenAbsent", function(assert) + local p = cli.parser() + p:add("output", "option", { required = true }) + assert.throwsWith("Missing required argument: output", function() + p:parse({}) + end) + end) +end) + +test.suite("CLI positional arguments", function(suite) + suite:case("singlePositional", function(assert) + local p = cli.parser() + p:add("file", "positional", {}) + p:parse({ "foo.txt" }) + assert.eq(p:get("file"), "foo.txt") + end) + + suite:case("multiplePositionals", function(assert) + local p = cli.parser() + p:add("src", "positional", {}) + p:add("dst", "positional", {}) + p:parse({ "a.txt", "b.txt" }) + assert.eq(p:get("src"), "a.txt") + assert.eq(p:get("dst"), "b.txt") + end) + + suite:case("positionalInterleavedWithFlags", function(assert) + local p = cli.parser() + p:add("verbose", "flag", {}) + p:add("file", "positional", {}) + p:parse({ "--verbose", "foo.txt" }) + assert.that(p:has("verbose")) + assert.eq(p:get("file"), "foo.txt") + end) + + suite:case("requiredPositionalThrowsWhenAbsent", function(assert) + local p = cli.parser() + p:add("file", "positional", { required = true }) + assert.throwsWith("Missing required argument: file", function() + p:parse({}) + end) + end) +end) + +test.suite("CLI forwarded arguments", function(suite) + suite:case("argsAfterDashDashAreForwarded", function(assert) + local p = cli.parser() + p:parse({ "--", "foo", "bar" }) + assert.eq(p:forwarded(), { "foo", "bar" }) + end) + + suite:case("dashDashWithFlagsBefore", function(assert) + local p = cli.parser() + p:add("verbose", "flag", {}) + p:parse({ "--verbose", "--", "--not-a-flag" }) + assert.that(p:has("verbose")) + assert.eq(p:forwarded(), { "--not-a-flag" }) + end) + + suite:case("noForwardedArgsReturnsEmpty", function(assert) + local p = cli.parser() + p:parse({}) + assert.eq(p:forwarded(), {}) + end) + + suite:case("unconsumedPositionalsAreForwarded", function(assert) + local p = cli.parser() + p:parse({ "extra" }) + assert.eq(p:forwarded(), { "extra" }) + end) +end) + +test.suite("CLI variadic arguments", function(suite) + suite:case("capturesSingleArg", function(assert) + local p = cli.parser() + p:add("files", "variadic", {}) + p:parse({ "a.txt" }) + assert.eq(p:variadics("files"), { "a.txt" }) + end) + + suite:case("capturesMultipleArgs", function(assert) + local p = cli.parser() + p:add("files", "variadic", {}) + p:parse({ "a.txt", "b.txt", "c.txt" }) + assert.eq(p:variadics("files"), { "a.txt", "b.txt", "c.txt" }) + end) + + suite:case("noArgsReturnsEmpty", function(assert) + local p = cli.parser() + p:add("files", "variadic", {}) + p:parse({}) + assert.eq(p:variadics("files"), {}) + end) + + suite:case("flagsStillParsedAlongsideVariadic", function(assert) + local p = cli.parser() + p:add("verbose", "flag", {}) + p:add("files", "variadic", {}) + p:parse({ "--verbose", "a.txt", "b.txt" }) + assert.that(p:has("verbose")) + assert.eq(p:variadics("files"), { "a.txt", "b.txt" }) + end) + + suite:case("optionsStillParsedAlongsideVariadic", function(assert) + local p = cli.parser() + p:add("output", "option", {}) + p:add("files", "variadic", {}) + p:parse({ "--output", "file.txt", "a.txt", "b.txt" }) + assert.eq(p:get("output"), "file.txt") + assert.eq(p:variadics("files"), { "a.txt", "b.txt" }) + end) + + suite:case("positionalsConsumedBeforeVariadic", function(assert) + local p = cli.parser() + p:add("first", "positional", {}) + p:add("rest", "variadic", {}) + p:parse({ "one", "two", "three" }) + assert.eq(p:get("first"), "one") + assert.eq(p:variadics("rest"), { "two", "three" }) + end) + + suite:case("argsAfterDashDashGoToForwardedNotVariadic", function(assert) + local p = cli.parser() + p:add("files", "variadic", {}) + p:parse({ "a.txt", "--", "b.txt" }) + assert.eq(p:variadics("files"), { "a.txt" }) + assert.eq(p:forwarded(), { "b.txt" }) + end) + + suite:case("requiredVariadicThrowsWhenEmpty", function(assert) + local p = cli.parser() + p:add("files", "variadic", { required = true }) + assert.throwsWith("Missing required variadic argument: files", function() + p:parse({}) + end) + end) + + suite:case("requiredVariadicPassesWhenPopulated", function(assert) + local p = cli.parser() + p:add("files", "variadic", { required = true }) + p:parse({ "a.txt" }) + assert.eq(p:variadics("files"), { "a.txt" }) + end) + + suite:case("argsBeforeDashDashStillForwardedWithoutVariadic", function(assert) + local p = cli.parser() + p:parse({ "a.txt", "--", "b.txt" }) + assert.eq(p:forwarded(), { "a.txt", "b.txt" }) + end) +end) + +test.suite("CLI mixed usage", function(suite) + suite:case("flagsOptionsAndPositionals", function(assert) + local p = cli.parser() + p:add("verbose", "flag", { aliases = { "v" } }) + p:add("output", "option", { aliases = { "o" } }) + p:add("input", "positional", {}) + p:parse({ "-v", "--output", "out.txt", "in.txt" }) + assert.that(p:has("verbose")) + assert.eq(p:get("output"), "out.txt") + assert.eq(p:get("input"), "in.txt") + end) + + suite:case("positionalsThenForwardedAfterDashDash", function(assert) + local p = cli.parser() + p:add("file", "positional", {}) + p:parse({ "foo.txt", "--", "extra1", "extra2" }) + assert.eq(p:get("file"), "foo.txt") + assert.eq(p:forwarded(), { "extra1", "extra2" }) + end) +end) From 0977b1fdacc8630e22a14e0d55d09cf960fdb060 Mon Sep 17 00:00:00 2001 From: jkelaty-rbx <78873527+jkelaty-rbx@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:31:41 -0700 Subject: [PATCH 454/642] Add github.com auth token handling to loom (#936) Closes https://github.com/luau-lang/lute/issues/783 --- lute/cli/commands/pkg/init.luau | 46 ++++++++++++-- lute/cli/commands/pkg/loom-core/init.luau | 14 ----- .../authentication/authenticationstore.luau | 63 +++++++++++++++++++ .../loom-core/src/commands/authenticate.luau | 19 ++++++ .../pkg/loom-core/src/commands/install.luau | 8 +-- .../pkg/loom-core/src/packagesource/git.luau | 34 +++++++--- .../pkg/loom-core/src/utils/fsutils.luau | 2 +- 7 files changed, 153 insertions(+), 33 deletions(-) delete mode 100644 lute/cli/commands/pkg/loom-core/init.luau create mode 100644 lute/cli/commands/pkg/loom-core/src/authentication/authenticationstore.luau create mode 100644 lute/cli/commands/pkg/loom-core/src/commands/authenticate.luau diff --git a/lute/cli/commands/pkg/init.luau b/lute/cli/commands/pkg/init.luau index 978494139..a06adb881 100644 --- a/lute/cli/commands/pkg/init.luau +++ b/lute/cli/commands/pkg/init.luau @@ -1,9 +1,43 @@ -local args = { ... } +local process = require("@std/process") -local loom = require("@self/loom-core") +local install = require("@self/loom-core/src/commands/install") +local authenticate = require("@self/loom-core/src/commands/authenticate") -if args[1] == "install" then - loom:install() -else - error("Usage: `lute pkg install`") +local USAGE = "Usage: lute pkg " + +local function printHelp() + print(USAGE) + print([[ + +Loom package manager for Luau projects. + +COMMANDS: + install Install dependencies from loom.config.luau + auth --token [--domain ] Save authentication credentials + +Use --help with any command for more information. +]]) +end + +local function main(...: string) + local args = { ... } + + if #args == 0 or args[1] == "--help" or args[1] == "-h" then + printHelp() + return + end + + local command = args[1] + + if command == "install" then + install() + elseif command == "auth" then + authenticate(table.unpack(args, 2)) + else + print(`Unknown command: {command}\n`) + printHelp() + process.exit(1) + end end + +main(...) diff --git a/lute/cli/commands/pkg/loom-core/init.luau b/lute/cli/commands/pkg/loom-core/init.luau deleted file mode 100644 index d56e560cf..000000000 --- a/lute/cli/commands/pkg/loom-core/init.luau +++ /dev/null @@ -1,14 +0,0 @@ -local git = require("@self/src/packagesource/git") -local types = require("@self/src/loomtypes") -local install = require("@self/src/commands/install") - -local loom = {} :: types.Loom - -function loom:install() - local packageSource = {} :: types.PackageSource - packageSource.git = git - - install(packageSource) -end - -return loom diff --git a/lute/cli/commands/pkg/loom-core/src/authentication/authenticationstore.luau b/lute/cli/commands/pkg/loom-core/src/authentication/authenticationstore.luau new file mode 100644 index 000000000..9ace2c1e1 --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/authentication/authenticationstore.luau @@ -0,0 +1,63 @@ +local fs = require("@std/fs") +local luau = require("@std/luau") +local path = require("@std/path") +local process = require("@std/process") +local pp = require("../../extern/pp") + +export type AuthenticationEntry = { + token: string, +} + +export type AuthenticationStore = { + [string]: AuthenticationEntry, +} + +local function getAuthenticationFilePath(): string + local homedir = path.join(process.homedir(), ".loom", "auth.luau") + return path.format(homedir) +end + +local function loadAuthenticationStore(): AuthenticationStore + local authPath = getAuthenticationFilePath() + + local ok, content = pcall(fs.readFileToString, authPath) + if not ok then + return {} + end + + local compileOk, bytecode = pcall(luau.compile, content) + if not compileOk then + return {} + end + + local loadOk, func = pcall(luau.load, bytecode, authPath, nil) + if not loadOk then + return {} + end + + local execOk, result = pcall(func) + if not execOk or type(result) ~= "table" then + return {} + end + + return result +end + +local function saveAuthenticationStore(store: AuthenticationStore) + local authPath = getAuthenticationFilePath() + local authDir = path.format(path.parse(path.dirname(authPath))) + fs.createDirectory(authDir, { makeParents = true }) + fs.writeStringToFile(authPath, "return " .. pp(store) .. "\n") +end + +local function saveAuthenticationToken(domain: string, token: string) + local store = loadAuthenticationStore() + store[domain] = { token = token } + saveAuthenticationStore(store) + print(`Credentials saved for {domain}`) +end + +return { + loadAuthenticationStore = loadAuthenticationStore, + saveAuthenticationToken = saveAuthenticationToken, +} diff --git a/lute/cli/commands/pkg/loom-core/src/commands/authenticate.luau b/lute/cli/commands/pkg/loom-core/src/commands/authenticate.luau new file mode 100644 index 000000000..269900f3a --- /dev/null +++ b/lute/cli/commands/pkg/loom-core/src/commands/authenticate.luau @@ -0,0 +1,19 @@ +local cli = require("@batteries/cli") +local authStore = require("../authentication/authenticationstore") + +local function authenticate(...: string) + local args = cli.parser() + args:add("token", "option", { help = "Authentication token", required = true }) + args:add("domain", "option", { help = "Domain to authenticate with", default = "github.com" }) + args:parse({ ... }) + + local token = args:get("token") + if not token then + error("Usage: `lute pkg auth --token [--domain ]`") + end + + local domain = args:get("domain") or "github.com" + authStore.saveAuthenticationToken(domain, token) +end + +return authenticate diff --git a/lute/cli/commands/pkg/loom-core/src/commands/install.luau b/lute/cli/commands/pkg/loom-core/src/commands/install.luau index 1421b16e5..48bbe3ac2 100644 --- a/lute/cli/commands/pkg/loom-core/src/commands/install.luau +++ b/lute/cli/commands/pkg/loom-core/src/commands/install.luau @@ -1,5 +1,5 @@ +local git = require("../packagesource/git") local resolution = require("../resolver/resolution") -local loomTypes = require("../loomtypes") local projectManifest = require("../manifest/projectmanifest") local pp = require("../../extern/pp") @@ -12,7 +12,7 @@ local function executeLuauSource(source: string): any return func() end -function install(packageSource: loomTypes.PackageSource) +local function install() local manifestPath = "loom.config.luau" local manifestContent = fsUtils.readfiletostring(manifestPath) local manifestTable = executeLuauSource(manifestContent) @@ -26,8 +26,8 @@ function install(packageSource: loomTypes.PackageSource) for _, pkg in projectResolution.packages do print(`package: {pkg.name}, Rev: {pkg.rev}, Source: {pkg.sourceKind} - {pkg.source}`) if pkg.sourceKind == "github" then - local zipArchive = packageSource.git:downloadFromGitHubArchive(pkg.source) - packageSource.git:installZipArchive(pkg.name, pkg.rev, zipArchive) + local zipArchive = git:downloadFromGitHubArchive(pkg.source) + git:installZipArchive(pkg.name, pkg.rev, zipArchive) end end end diff --git a/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau b/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau index d77e703f2..801587606 100644 --- a/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau +++ b/lute/cli/commands/pkg/loom-core/src/packagesource/git.luau @@ -1,10 +1,12 @@ +local authenticationStore = require("../authentication/authenticationstore") local fsUtils = require("../utils/fsutils") local net = require("@std/net") +local process = require("@std/process") local unzip = require("../../extern/unzip") export type Git = { installZipArchive: (self: Git, name: string, version: string, content: string) -> (), - downloadFromGitHubArchive: (self: Git, url: string, token: string?) -> string, + downloadFromGitHubArchive: (self: Git, url: string) -> string, } local Git = {} :: Git @@ -43,6 +45,21 @@ local function sanitizePath(path: string): string return normalized end +local function getGithubAuthenticationToken(): string? + local githubToken = process.env.GITHUB_TOKEN + if githubToken ~= nil then + return githubToken + end + + local store = authenticationStore.loadAuthenticationStore() + local entry = store["github.com"] + if entry ~= nil then + return entry.token + end + + return nil +end + function Git:installZipArchive(name: string, _version: string, content: string) local reader = unzip.load(buffer.fromstring(content)) @@ -65,15 +82,16 @@ end function Git:downloadFromGitHubArchive(url: string): string local headers = {} - -- TODO: support token authentication. - -- local token = nil - -- if token ~= nil and token ~= "" then - -- headers["Authorization"] = `Bearer {token}` - -- end + local token = getGithubAuthenticationToken() + if token ~= nil and token ~= "" then + headers["Authorization"] = `Bearer {token}` + end local response = net.request(url, { method = "GET", headers = headers }) - local body = response.body - return body + if not response.ok then + error(`Failed to download {url}: HTTP {response.status}\n{string.sub(response.body, 1, 200)}`) + end + return response.body end return Git diff --git a/lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau b/lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau index d625ee241..0d5836d29 100644 --- a/lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau +++ b/lute/cli/commands/pkg/loom-core/src/utils/fsutils.luau @@ -18,7 +18,7 @@ end local function writestringtofile(path: string, content: string): () local parentPath = getParentPath(path) - fs.createDirectory(parentPath, { makeparents = true }) + fs.createDirectory(parentPath, { makeParents = true }) return fs.writeStringToFile(path, content) end From 5d61f576e929995d69d278eaf1035083b751c379 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:42:03 +0000 Subject: [PATCH 455/642] Update Lute to 0.1.0-nightly.20260403 (#938) **Lute**: Updated from `0.1.0-nightly.20260327` to `0.1.0-nightly.20260403` **Release Notes:** https://github.com/luau-lang/lute/releases/tag/0.1.0-nightly.20260403 --- *This PR was automatically created by the [Update Luau workflow](https://github.com/luau-lang/lute/actions/workflows/update-prs.yml)* Co-authored-by: aatxe <744293+aatxe@users.noreply.github.com> Co-authored-by: Nick Winans Co-authored-by: ariel --- foreman.toml | 2 +- rokit.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foreman.toml b/foreman.toml index 049e7f3f6..fdb24426b 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,3 +1,3 @@ [tools] stylua = { github = "JohnnyMorganz/StyLua", version = "2.4.0" } -lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260327" } +lute = { github = "luau-lang/lute", version = "=0.1.0-nightly.20260403" } diff --git a/rokit.toml b/rokit.toml index 9b01eb0d8..7958e2058 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,3 +1,3 @@ [tools] stylua = "JohnnyMorganz/StyLua@2.4.0" -lute = "luau-lang/lute@0.1.0-nightly.20260327" +lute = "luau-lang/lute@0.1.0-nightly.20260403" From cd03b4fe80d9ff31c57c15b7e3776c45b2c35ea2 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Tue, 7 Apr 2026 16:27:46 -0700 Subject: [PATCH 456/642] Update lute transform to use the CLI battery (#950) This results in a slight API change for the CLI. Previously, options forwarded on to the code transformation scripts could be passed anywhere, not just after a `--`, but now we only forward them if they come after a `--`. This is because we now use the CLI battery's shiny new variadic argument feature to parse file paths to be transformed. --- examples/options_transformer.luau | 23 +++++ lute/cli/commands/transform/init.luau | 88 ++++++++++++++++--- .../cli/commands/transform/lib/arguments.luau | 80 ----------------- .../commands/transform/{lib => }/types.luau | 0 tests/cli/transform.test.luau | 60 +++++++++++++ 5 files changed, 157 insertions(+), 94 deletions(-) create mode 100644 examples/options_transformer.luau delete mode 100644 lute/cli/commands/transform/lib/arguments.luau rename lute/cli/commands/transform/{lib => }/types.luau (100%) diff --git a/examples/options_transformer.luau b/examples/options_transformer.luau new file mode 100644 index 000000000..0f1e277cf --- /dev/null +++ b/examples/options_transformer.luau @@ -0,0 +1,23 @@ +local query = require("@std/syntax/query") +local syntax = require("@std/syntax") +local syntax_utils = require("@std/syntax/utils") + +return { + options = { + { kind = "string", name = "functionName", default = "math.isnan" }, + }, + transform = function(ctx) + local functionName = ctx.options.functionName + return query + .findAllFromRoot(ctx.parseresult.root, syntax_utils.isExprBinary) + :filter(function(bin) + return bin.operator.text == "~=" + and bin.lhsOperand.tag == "local" + and bin.rhsOperand.tag == "local" + and bin.lhsOperand.token.text == bin.rhsOperand.token.text + end) + :replace(function(bin) + return `{functionName}({(bin.lhsOperand :: syntax.AstExprLocal).token.text})` + end) + end, +} diff --git a/lute/cli/commands/transform/init.luau b/lute/cli/commands/transform/init.luau index 46bfbd45f..4298500d8 100644 --- a/lute/cli/commands/transform/init.luau +++ b/lute/cli/commands/transform/init.luau @@ -1,12 +1,13 @@ local fs = require("@std/fs") local luau = require("@std/luau") local pathLib = require("@std/path") +local process = require("@std/process") local printer = require("@std/syntax/printer") local syntax = require("@std/syntax") -local arguments = require("@self/lib/arguments") +local cli = require("@batteries/cli") local files = require("./lib/files") -local types = require("@self/lib/types") +local types = require("@self/types") local function exhaustiveMatch(value: never): never error(`Unknown value in exhaustive match: {value}`) @@ -142,33 +143,92 @@ local function applyMigration( end end +local USAGE = "Usage: lute transform [options] [files...] [-- [--option value...]]" + +local function printHelp() + print(USAGE) + print([[ +Apply the specified code transformation to the given files. + +OPTIONS: + -h, --help Show this help message + --dry-run Preview changes without writing files + -o, --output [FILE] Write output to a single file instead of in-place (only works if a single input file is specified) + +lute transform also supports forwarding options to the code transformation script by placing them after a '--' separator. For example: + + lute transform my-migration.luau files/ -- --option value + ]]) +end + local function main(...: string) - local args = arguments.parse({ ... }) + local args = cli.parser() + args:add("help", "flag", { help = "Show this help message", aliases = { "h" } }) + args:add("dry-run", "flag", { help = "Preview changes without writing any files" }) + args:add("output", "option", { help = "Write output to this file (single input only)", aliases = { "o" } }) + args:add("migration-path", "positional", { help = "Path to the migration script to run" }) + args:add( + "files", + "variadic", + { help = "Files or directories to apply the migration to (defaults to current directory if not specified)" } + ) + args:parse({ ... }) + + if args:has("help") then + printHelp() + return + end + + local migrationPath = args:get("migration-path") + if not migrationPath then + print(`Error: No code transformation script specified.\n\n{USAGE}\nUse --help for more information.`) + process.exit(1) + end + + local dryRun = args:has("dry-run") + local outputFile = args:get("output") + + -- Split forwarded args into file paths and migration options. + -- Non-flag tokens are file paths; --name value pairs are migration options. + local filePaths: { string } = args:variadics() + local migrationOptions: { [string]: string } = {} + local forwarded = args:forwarded() or {} + local i = 1 + while i <= #forwarded do + local token = forwarded[i] + local name = string.sub(token, 3) + i += 1 + assert(i <= #forwarded, `Missing value for '--{name}'`) + migrationOptions[name] = forwarded[i] + i += 1 + end + + assert(outputFile == nil or #filePaths == 1, "When specifying an output file, only one input file is allowed") - if args.dryRun then + if dryRun then print("Executing in dry run mode") end - if args.outputFile then - print(`Output path specified: '{args.outputFile}'`) + if outputFile then + print(`Output path specified: '{outputFile}'`) end - print(`Loading migration '{args.migrationPath}'`) - local migration = loadMigration(args.migrationPath) + print(`Loading migration '{migrationPath}'`) + local migration = loadMigration(migrationPath) - print("Finding source files") -- todo: might type error - local files = files.getSourceFiles(args.filePaths) - if #files == 0 then + print("Finding source files") + local sourceFiles = files.getSourceFiles(filePaths) + if #sourceFiles == 0 then error("error: no source files provided") end print("Processing migration options") - local migrationOptions = processMigrationOptions(migration, args.migrationOptions) + local processedOptions = processMigrationOptions(migration, migrationOptions) print("Applying migration") - applyMigration(migration, files, migrationOptions, args.dryRun, args.outputFile) + applyMigration(migration, sourceFiles, processedOptions, dryRun, outputFile) - print(`Processed {#files} files!`) + print(`Processed {#sourceFiles} files!`) end main(...) diff --git a/lute/cli/commands/transform/lib/arguments.luau b/lute/cli/commands/transform/lib/arguments.luau deleted file mode 100644 index e20237218..000000000 --- a/lute/cli/commands/transform/lib/arguments.luau +++ /dev/null @@ -1,80 +0,0 @@ -local stringext = require("@std/stringext") - -type Config = { - --- Prevent making changes to files - dryRun: boolean, - --- Path to the migration module to execute - migrationPath: string, - --- Configuration flags to pass to the migration - migrationOptions: { [string]: string }, - --- List of file paths to process - --- Note: no directory traversal or filtering has been applied on this list - filePaths: { string }, - --- Path to output file (only works if a single input file is specified) - outputFile: string?, -} - -local function parse(arguments: { string }): Config - -- TODO: replace with proper CLI system - -- For now, first argument is the transformer script, whilst the rest are either flags or files to apply it on - - if #arguments < 2 then - error("Usage: lute transform [options...] ") - end - - local config = { - dryRun = false, - migrationPath = nil :: string?, - migrationOptions = {}, - filePaths = {}, - outputFile = nil :: string?, - } - - local i = 1 - while i <= #arguments do - local argument = arguments[i] - - if stringext.hasPrefix(argument, "--") then - local name = stringext.removePrefix(argument, "--") - - if config.migrationPath == nil then - -- Options before the codemod file are parsed as options for the tool itself - if name == "dry-run" then - config.dryRun = true - elseif name == "output" then - i += 1 - assert(i <= #arguments, `Missing value for '--output'`) - config.outputFile = arguments[i] - else - error(`Unknown flag '--{name}'`) - end - else - i += 1 - assert(i <= #arguments, `Missing value for '{name}'`) - local value = arguments[i] - - config.migrationOptions[name] = value - end - else - if config.migrationPath == nil then - config.migrationPath = argument - else - table.insert(config.filePaths, argument) - end - end - - i += 1 - end - - assert(config.migrationPath, "ASSERTION FAILED: codemodPath ~= nil") - assert( - if config.outputFile ~= nil then #config.filePaths == 1 else true, - "ASSERTION FAILED: When specifying an output file, only one input file is allowed" - ) - - return config -end - -return { - parse = parse, -} diff --git a/lute/cli/commands/transform/lib/types.luau b/lute/cli/commands/transform/types.luau similarity index 100% rename from lute/cli/commands/transform/lib/types.luau rename to lute/cli/commands/transform/types.luau diff --git a/tests/cli/transform.test.luau b/tests/cli/transform.test.luau index 1f4373ae9..157899ad0 100644 --- a/tests/cli/transform.test.luau +++ b/tests/cli/transform.test.luau @@ -14,6 +14,8 @@ local tmpDir = system.tmpdir() local transformeePath = path.format(path.join(tmpDir, "transformee.luau")) local outputPath = path.format(path.join(tmpDir, "transformee_output.luau")) +local USAGE = "Usage: lute transform [options] [files...] [-- [--option value...]]" + test.suite("LuteTransform", function(suite) suite:beforeEach(function() -- Create a transformee file @@ -30,6 +32,33 @@ test.suite("LuteTransform", function(suite) end end) + suite:case("luteLintTransformMessage", function(assert) + local result = process.run({ lutePath, "transform", "-h" }) + + assert.eq(result.exitcode, 0) + assert.strContains(result.stdout, USAGE) + + result = process.run({ lutePath, "transform", "--h" }) + + assert.eq(result.exitcode, 0) + assert.strContains(result.stdout, USAGE) + + result = process.run({ lutePath, "transform", "--help" }) + + assert.eq(result.exitcode, 0) + assert.strContains(result.stdout, USAGE) + end) + + suite:case("luteLintMissingTransformationScript", function(assert) + local result = process.run({ lutePath, "transform" }) + + assert.eq(result.exitcode, 1) + assert.strContains( + result.stdout, + `Error: No code transformation script specified.\n\n{USAGE}\nUse --help for more information.` + ) + end) + suite:case("transformQueryStyle", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau @@ -89,6 +118,37 @@ test.suite("LuteTransform", function(suite) fs.remove(outputPath) end) + suite:case("transformForwardedOptions", function(assert) + -- Setup + local transformerExample = path.format(path.join("examples", "options_transformer.luau")) + local transformerDest = path.format(path.join(tmpDir, "options_transformer.luau")) + fs.copy(transformerExample, transformerDest) + + -- Do + -- Run the transformer with a forwarded option overriding the default functionName + local result = process.run({ + lutePath, + "transform", + transformerDest, + transformeePath, + "--", + "--functionName", + "number.isNaN", + }) + + -- Check + assert.eq(result.exitcode, 0) + + local transformeeHandle = fs.open(transformeePath, "r") + local transformeeContent = fs.read(transformeeHandle) + assert.strNotContains(transformeeContent, "x ~= x") + assert.strContains(transformeeContent, "number.isNaN(x)") + fs.close(transformeeHandle) + + -- Teardown + fs.remove(transformerDest) + end) + suite:case("transformOutputCreatesFileIfItDoesntExist", function(assert) -- Setup -- Copy examples/transformer.luau to build/transform_tests/transformer.luau From c072389cc9ab5c9404aa19db47cb7ec2cce7d101 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:55:20 -0700 Subject: [PATCH 457/642] Improves ModuleResolver's `resolveModule` by using `LuteReporter` and adds tests (#949) --- lute/cli/src/tc.cpp | 2 +- lute/luau/include/lute/tcmoduleresolver.h | 5 +- lute/luau/src/luau.cpp | 3 +- lute/luau/src/tcmoduleresolver.cpp | 7 ++- tests/CMakeLists.txt | 2 + tests/src/moduleresolver.test.cpp | 63 +++++++++++++++++++++-- tests/src/tcmoduleresolverfixture.cpp | 13 +++++ tests/src/tcmoduleresolverfixture.h | 21 ++++++++ 8 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 tests/src/tcmoduleresolverfixture.cpp create mode 100644 tests/src/tcmoduleresolverfixture.h diff --git a/lute/cli/src/tc.cpp b/lute/cli/src/tc.cpp index a3379a9c0..ec70ae281 100644 --- a/lute/cli/src/tc.cpp +++ b/lute/cli/src/tc.cpp @@ -125,7 +125,7 @@ int typecheck(const std::vector& sourceFilesInput, LuteReporter& re frontendOptions.retainFullTypeGraphs = annotate; frontendOptions.runLintChecks = true; - Luau::LuteTypeCheckModuleResolver fileResolver; + Luau::LuteTypeCheckModuleResolver fileResolver{reporter}; Luau::LuteConfigResolver configResolver(mode); Luau::Frontend frontend(Luau::SolverMode::New, &fileResolver, &configResolver, frontendOptions); diff --git a/lute/luau/include/lute/tcmoduleresolver.h b/lute/luau/include/lute/tcmoduleresolver.h index 84235b74b..c694cc876 100644 --- a/lute/luau/include/lute/tcmoduleresolver.h +++ b/lute/luau/include/lute/tcmoduleresolver.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/reporter.h" + #include "Luau/DenseHash.h" #include "Luau/FileResolver.h" @@ -12,7 +14,7 @@ namespace Luau // Based on CliFileResolver in Analyze.cpp. struct LuteTypeCheckModuleResolver : Luau::FileResolver { - LuteTypeCheckModuleResolver() = default; + LuteTypeCheckModuleResolver(LuteReporter& reporter); std::optional readSource(const Luau::ModuleName& name) override; @@ -21,6 +23,7 @@ struct LuteTypeCheckModuleResolver : Luau::FileResolver std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override; Luau::DenseHashMap sourceCache{""}; + LuteReporter& reporter; }; } // namespace Luau diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 0a84e9b55..4c693ebd8 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -2,6 +2,7 @@ #include "lute/common.h" #include "lute/configresolver.h" +#include "lute/runtime.h" #include "lute/tcmoduleresolver.h" #include "lute/type.h" @@ -3032,7 +3033,7 @@ int typeofModule_luau(lua_State* L) { std::string modulePath = luaL_checkstring(L, 1); - Luau::LuteTypeCheckModuleResolver moduleResolver; + Luau::LuteTypeCheckModuleResolver moduleResolver{getRuntime(L)->reporter}; Luau::LuteConfigResolver configResolver(Luau::Mode::NoCheck); Luau::FrontendOptions fopts; fopts.retainFullTypeGraphs = true; diff --git a/lute/luau/src/tcmoduleresolver.cpp b/lute/luau/src/tcmoduleresolver.cpp index 2fa10c0ec..4d9cd2439 100644 --- a/lute/luau/src/tcmoduleresolver.cpp +++ b/lute/luau/src/tcmoduleresolver.cpp @@ -8,6 +8,11 @@ namespace Luau { +LuteTypeCheckModuleResolver::LuteTypeCheckModuleResolver(LuteReporter& reporter) + : reporter(reporter) +{ +} + std::optional LuteTypeCheckModuleResolver::readSource(const Luau::ModuleName& name) { if (name == "-") @@ -46,7 +51,7 @@ std::optional LuteTypeCheckModuleResolver::resolveModule( std::optional resolved = ::resolveForTypeCheck(requirePath, std::move(requirerChunkname), &error); if (!resolved) { - printf("Failed to resolve require: %s\n", error.c_str()); + reporter.formatError("Failed to resolve require: %s\n", error.c_str()); return std::nullopt; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c820c42bd..58c2a7fd6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,11 +8,13 @@ target_sources(Lute.Test PRIVATE src/cliruntimefixture.h src/lutefixture.h src/luteprojectroot.h + src/tcmoduleresolverfixture.h src/testreporter.h src/cliruntimefixture.cpp src/lutefixture.cpp src/luteprojectroot.cpp + src/tcmoduleresolverfixture.cpp src/testreporter.cpp src/typeserializefixture.h diff --git a/tests/src/moduleresolver.test.cpp b/tests/src/moduleresolver.test.cpp index f74741bcc..d29ea963b 100644 --- a/tests/src/moduleresolver.test.cpp +++ b/tests/src/moduleresolver.test.cpp @@ -1,21 +1,26 @@ // Simplified tests for module resolver functionality using public APIs. #include "lute/common.h" #include "lute/resolvemodule.h" -#include "lute/tcmoduleresolver.h" -#include "Luau/Ast.h" #include "Luau/FileUtils.h" +#include #include #include "doctest.h" #include "luteprojectroot.h" +#include "tcmoduleresolverfixture.h" -TEST_CASE("moduleresolver_read_source") +static Luau::AstExprConstantString makeStringNode(const char* path) +{ + Luau::AstArray value{const_cast(path), std::strlen(path)}; + return Luau::AstExprConstantString(Luau::Location{}, value, Luau::AstExprConstantString::QuotedSimple); +} + +TEST_CASE_FIXTURE(TCModuleResolverFixture, "moduleresolver_readSource") { std::string root = getLuteProjectRootAbsolute(); std::string file = joinPaths(root, "tests/src/resolver/mainmodule.luau"); - Luau::LuteTypeCheckModuleResolver resolver; auto source = resolver.readSource(file); REQUIRE(source); CHECK(source->type == Luau::SourceCode::Module); @@ -98,3 +103,53 @@ TEST_CASE("moduleresolver_typecheck_resolve") CHECK(resolved->path == "@std/process.luau"); CHECK(resolved->source.find("processlib") != std::string::npos); } + +TEST_CASE_FIXTURE(TCModuleResolverFixture, "moduleresolver_resolveModule") +{ + SUBCASE("resolves_std_module") + { + auto strNode = makeStringNode("@std/process"); + auto result = resolver.resolveModule(&context, &strNode, limits); + + REQUIRE(result.has_value()); + CHECK(result->name == "@std/process.luau"); + CHECK(getReporter().getErrors().empty()); + } + + SUBCASE("caches_resolved_module_source") + { + auto strNode = makeStringNode("@std/process"); + resolver.resolveModule(&context, &strNode, limits); + + CHECK(resolver.sourceCache.find("@std/process.luau") != nullptr); + } + + SUBCASE("fails_for_nonexistent_module") + { + auto strNode = makeStringNode("@std/does_not_exist"); + auto result = resolver.resolveModule(&context, &strNode, limits); + + CHECK(!result.has_value()); + REQUIRE(!getReporter().getErrors().empty()); + CHECK(getReporter().getErrors()[0].find("Failed to resolve require") != std::string::npos); + } + + SUBCASE("fails_for_self_from_userland") + { + auto strNode = makeStringNode("@self/platform"); + auto result = resolver.resolveModule(&context, &strNode, limits); + + CHECK(!result.has_value()); + REQUIRE(!getReporter().getErrors().empty()); + } + + SUBCASE("returns_nullopt_silently_for_non_string_expr") + { + Luau::AstExprConstantNil nilNode(Luau::Location{}); + + auto result = resolver.resolveModule(&context, &nilNode, limits); + + CHECK(!result.has_value()); + CHECK(getReporter().getErrors().empty()); + } +} diff --git a/tests/src/tcmoduleresolverfixture.cpp b/tests/src/tcmoduleresolverfixture.cpp new file mode 100644 index 000000000..1871ca1a5 --- /dev/null +++ b/tests/src/tcmoduleresolverfixture.cpp @@ -0,0 +1,13 @@ +#include "tcmoduleresolverfixture.h" + +#include "Luau/FileUtils.h" + +#include "luteprojectroot.h" + +TCModuleResolverFixture::TCModuleResolverFixture() + : resolver(getReporter()) +{ + std::string root = getLuteProjectRootAbsolute(); + context.name = "@" + joinPaths(root, "tests/src/resolver/mainmodule.luau"); +} + diff --git a/tests/src/tcmoduleresolverfixture.h b/tests/src/tcmoduleresolverfixture.h new file mode 100644 index 000000000..5d362ae7d --- /dev/null +++ b/tests/src/tcmoduleresolverfixture.h @@ -0,0 +1,21 @@ +#pragma once + +#include "lute/tcmoduleresolver.h" + +#include "Luau/Ast.h" +#include "Luau/FileResolver.h" +#include "Luau/TypeCheckLimits.h" + +#include "lutefixture.h" + +#include + +class TCModuleResolverFixture : public LuteFixture +{ +public: + TCModuleResolverFixture(); + + Luau::LuteTypeCheckModuleResolver resolver; + Luau::ModuleInfo context; + Luau::TypeCheckLimits limits; +}; From 65cf9e1f58337e1c64c1641a3b13b1ba579d03bb Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Wed, 8 Apr 2026 09:20:44 -0700 Subject: [PATCH 458/642] Allow for forcing unbuffered stdio via env (#953) --- lute/cli/src/climain.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 618988e6a..5c1a82fdc 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -27,6 +27,7 @@ #include "lualib.h" #include +#include #include #include #include @@ -660,6 +661,12 @@ int cliMain(int argc, char** argv, LuteReporter& reporter) Luau::assertHandler() = assertionHandler; setLuauFlags(); + if (const char* unbuffered = std::getenv("LUTE_UNBUFFERED"); unbuffered && std::string_view(unbuffered) == "1") + { + setvbuf(stdout, nullptr, _IONBF, 0); + setvbuf(stderr, nullptr, _IONBF, 0); + } + std::string err = ""; LuteExecutable exe{argv[0], reporter}; From 0a9047dc6f5b32c42d3dc5c63e8e658148317a94 Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Wed, 8 Apr 2026 09:29:00 -0700 Subject: [PATCH 459/642] Fixes a bug where the runtime will print "cannot resume coroutine" on a killed task (#943) Cancelling a task should error at call time if closing a coroutine is not possible, but should not create errors in the runtime. Currently, running code like: ``` local task = require("@std/task") local t = task.spawn(function() task.wait(2) end) task.cancel(t)/coroutine.close(t) ``` will cause the runtime to print "cannot resume dead coroutine". The reason that this happens is because of how scheduling resumes works. When the user calls `coroutine.close()` on the task, we reset the thread state to 'dead'. If the function passed to `spawn` schedules a continuation in the runtime, then when this continuation is called, we **may** end up mutating the state of a cancelled thread. For example, the `WaitData` uses the resumeToken to schedule a callback that pushes one value onto the stack associated with the callback. Because this thread is dead by the time the wait elapses, we end up pushing a number onto the stack, which makes it so that callback's top and base are not the same. When this happens, the runtime check for this thread's state using `lua_costatus` will return CO_SUS instead of CO_FIN, which causes the subsequent resume in the runtime to fail. This uncovered another bug with the FS Watch handle, whereby the combination of task.wait and fs.watch caused a pending token to be registered with the runtime that never got cleared, which caused the runtime to loop forever. I refactored the watch handle code a bit to make it's initialization and tear down clearer, and removed the pending token, as the existence of a `uv_fs_event_t` handle is already tracked by the event loop to prevent premature shutdown. --- lute/fs/src/fs.cpp | 71 ++++++++++++----------------- lute/runtime/include/lute/runtime.h | 4 ++ lute/runtime/src/runtime.cpp | 27 ++++++++++- tests/CMakeLists.txt | 1 + tests/src/runtime.test.cpp | 21 +++++++++ 5 files changed, 82 insertions(+), 42 deletions(-) create mode 100644 tests/src/runtime.test.cpp diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 6d82df4ae..4ae318404 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -229,28 +229,38 @@ int symlink(lua_State* L) struct WatchHandle { - lua_State* L; + WatchHandle(lua_State* L, int callbackIdx) + : runtime(getRuntime(L)) + , callbackReference(std::make_shared(L, callbackIdx)) + , evtHandle(std::make_unique()) + { + evtHandle->data = this; + int err = uv_fs_event_init(runtime->getEventLoop(), evtHandle.get()); + if (err) + luaL_errorL(L, "%s", uv_strerror(err)); + } + + Runtime* runtime; std::shared_ptr callbackReference; bool isClosed = false; - uv_fs_event_t handle; + std::unique_ptr evtHandle; void close() { if (!isClosed) { - int err = uv_fs_event_stop(&handle); - if (err) - { - luaL_errorL(L, "Error stopping fs event: %s", uv_strerror(err)); - } - - uv_close((uv_handle_t*)&handle, nullptr); isClosed = true; - - getRuntime(L)->releasePendingToken(); - + uv_fs_event_stop(evtHandle.get()); callbackReference.reset(); + auto raw = evtHandle.release(); + uv_close( + (uv_handle_t*)raw, + [](uv_handle_t* handle) + { + delete (uv_fs_event_t*)handle; + } + ); } } @@ -280,44 +290,27 @@ int fs_watch(lua_State* L) const char* path = luaL_checkstring(L, 1); luaL_checktype(L, 2, LUA_TFUNCTION); - auto* event = new (static_cast(lua_newuserdatataggedwithmetatable(L, sizeof(WatchHandle), kWatchHandleTag))) WatchHandle{}; - - event->L = L; - event->callbackReference = std::make_shared(L, 2); - event->handle.data = event; - - int init_err = uv_fs_event_init(getRuntimeLoop(L), &event->handle); - - if (init_err) - { - luaL_errorL(L, "%s", uv_strerror(init_err)); - } + void* storage = lua_newuserdatataggedwithmetatable(L, sizeof(WatchHandle), kWatchHandleTag); + auto* event = new (storage) WatchHandle(L, 2); int event_start_err = uv_fs_event_start( - &event->handle, + event->evtHandle.get(), [](uv_fs_event_t* handle, const char* filenamePtr, int events, int status) { auto* eventHandle = static_cast(handle->data); - lua_State* newThread = lua_newthread(eventHandle->L); - std::shared_ptr ref = getRefForThread(newThread); - Runtime* runtime = getRuntime(newThread); + if (status < 0) + return; std::string filename = filenamePtr ? filenamePtr : ""; - runtime->scheduleLuauResume( - ref, - [=, filename = std::move(filename)](lua_State* L) + eventHandle->runtime->scheduleLuauCallback( + eventHandle->callbackReference, + [filename = std::move(filename), events](lua_State* L) { - // the function to the back of the stack, omit from nret - eventHandle->callbackReference->push(L); - - // filename lua_pushlstring(L, filename.c_str(), filename.size()); - // events lua_createtable(L, 0, 2); - lua_pushboolean(L, (events & UV_RENAME) != 0); lua_setfield(L, -2, "rename"); lua_pushboolean(L, (events & UV_CHANGE) != 0); @@ -326,8 +319,6 @@ int fs_watch(lua_State* L) return 2; } ); - - uv_stop(handle->loop); }, path, 0 @@ -338,8 +329,6 @@ int fs_watch(lua_State* L) luaL_errorL(L, "%s", uv_strerror(event_start_err)); } - getRuntime(L)->addPendingToken(); - return 1; // return the watch handle } diff --git a/lute/runtime/include/lute/runtime.h b/lute/runtime/include/lute/runtime.h index cf39ee3e4..bbf990e7f 100644 --- a/lute/runtime/include/lute/runtime.h +++ b/lute/runtime/include/lute/runtime.h @@ -69,6 +69,10 @@ struct Runtime // Resume thread with the results computed by the continuation void scheduleLuauResume(std::shared_ptr ref, std::function cont); + // Invoke a Luau callback function on a fresh thread. + // argPusher should push arguments onto the stack and return the count. + void scheduleLuauCallback(std::shared_ptr callbackRef, std::function argPusher); + // Run 'f' in a libuv work queue void runInWorkQueue(std::function f); diff --git a/lute/runtime/src/runtime.cpp b/lute/runtime/src/runtime.cpp index 4f39e9b81..fea2fb493 100644 --- a/lute/runtime/src/runtime.cpp +++ b/lute/runtime/src/runtime.cpp @@ -253,8 +253,33 @@ void Runtime::scheduleLuauResume(std::shared_ptr ref, std::function callbackRef, std::function argPusher) +{ + std::unique_lock lock(continuationMutex); + + continuations.push_back( + [this, callbackRef = std::move(callbackRef), argPusher = std::move(argPusher)]() mutable + { + lua_State* newThread = lua_newthread(GL); + std::shared_ptr threadRef = getRefForThread(newThread); + lua_pop(GL, 1); + + callbackRef->push(newThread); + int nargs = argPusher(newThread); + + runningThreads.push_back({true, threadRef, nargs}); } ); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 58c2a7fd6..72ac1441c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -28,6 +28,7 @@ target_sources(Lute.Test PRIVATE src/moduleresolver.test.cpp src/packagerequire.test.cpp src/require.test.cpp + src/runtime.test.cpp src/typeserializer.test.cpp src/staticrequires.test.cpp src/stdsystem.test.cpp diff --git a/tests/src/runtime.test.cpp b/tests/src/runtime.test.cpp new file mode 100644 index 000000000..b5f5f983f --- /dev/null +++ b/tests/src/runtime.test.cpp @@ -0,0 +1,21 @@ +#include "cliruntimefixture.h" +#include "doctest.h" + +TEST_CASE_FIXTURE(CliRuntimeFixture, "runtime_will_not_resume_cancelled_task_spawn") +{ + runCode(R"( + local task = require("@std/task") + local t = task.spawn(function() + task.wait(2) + end) + + task.cancel(t) + )"); + + auto reporter = getReporter(); + for (auto s : reporter.getErrors()) + { + auto idx = s.find("cannot resume dead coroutine"); + CHECK(idx == std::string::npos); + } +} From 3983c3efa1ceeafb3739d93fb816301db19e8f04 Mon Sep 17 00:00:00 2001 From: "Daniel P H Fox (Roblox)" <174147186+dphblox@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:19:31 -0700 Subject: [PATCH 460/642] Contributing glob library to batteries (#935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As part of internal explorations, we built a library that reimplements the Linux `glob` functionality in Lute. Since we anticipate this being widely useful for ecosystem tooling (particularly for defining configuration formats for common tools like package managers, unit test dispatchers, etc.), we want to propose moving our implementation into batteries so that it can be shared and improved communally, rather than fragmenting this functionality across the ecosystem. The new library has two APIs: `glob.matches()`, which returns a boolean if a path matches a glob pattern, and `glob.discover()`, which efficiently walks the filesystem with pruning to discover all paths that match a glob pattern. --- **Note:** at lute/lute/cli/commands/lib/ignore.luau there are some similar globbing facilities, but they're pretty divergent in implementation and in API surface. It's nothing we can't expand this new glob surface to standardise; I got Claude to summarise: > **What they share** — both implement the core glob metacharacters (*, ?, **) with the same semantic meaning (don't cross /, match any char, recurse into subdirectories). > > **Where they diverge:** > > - **Purpose:** batteries/glob is a filesystem glob that walks the tree and returns matching paths. parseIgnores is gitignore matching that tests whether a path matches an ignore rule. > - **Matching strategy:** batteries/glob does recursive backtracking, char-by-char, per segment. parseIgnores compiles globs into Lua patterns and uses string.match on full relative paths. > - **Filesystem:** batteries/glob walks directories via fs.listDirectory. parseIgnores has no filesystem interaction — it just tests a string. Returns: batteries/glob returns { string } (list of matching paths). parseIgnores returns boolean. > - **Character classes:** batteries/glob has full [...], [!...], [a-z] range support. parseIgnores doesn't support them — [ passes through as a literal. > - **Gitignore semantics:** batteries/glob has none. parseIgnores supports ! negation/whitelists, trailing / for directory-only matching, anchoring rules (patterns with / anchor to the .gitignore location, without / they match anywhere), and hierarchical .gitignore chain resolution. > - **Separator handling:** batteries/glob handles this implicitly — it matches one segment name at a time, so separators never appear in the match input. parseIgnores does it explicitly, baking the platform separator into compiled patterns ([^\]* vs [^/]*). I imagine we can introduce an `options` table for enabling/disabling features like character classes and gitignore patterns, and the choice between full-string vs per-segment processing, and the idea of whether to compile to regex or just directly ingest the string, is basically just a performance tradeoff. --- batteries/glob.luau | 221 +++++++++++++++++++++++++ tests/batteries/glob.test.luau | 288 +++++++++++++++++++++++++++++++++ 2 files changed, 509 insertions(+) create mode 100644 batteries/glob.luau create mode 100644 tests/batteries/glob.test.luau diff --git a/batteries/glob.luau b/batteries/glob.luau new file mode 100644 index 000000000..09383251b --- /dev/null +++ b/batteries/glob.luau @@ -0,0 +1,221 @@ +--[[ + Glob pattern matching and filesystem discovery. + + `glob.match(pattern, input)` tests whether a path matches a glob pattern. + `glob.discover(pattern)` walks the filesystem returning all matching `Path`s. + + The implementation follows the Rust glob crate. The rules are: + + - `?` matches any single character (not `/`) + - `*` matches any (possibly empty) sequence of characters (not `/`) + - `**` matches the current directory and arbitrary subdirectories; it must + form an entire path component on its own (e.g. `a/**/b` is valid, but + `a**b` is not) + - `[...]` matches any character inside the brackets; ranges like `[a-z]` + are supported, ordered by Unicode code point + - `[!...]` matches any character NOT inside the brackets + - Metacharacters can be matched literally by wrapping them in brackets, + e.g. `[?]` matches a literal `?`, `[*]` matches a literal `*` +]] +local fs = require("@std/fs") +local path = require("@std/path") + +local function matchesSegment(pattern: string, name: string, patternStart: number?, nameStart: number?): boolean + local patternIndex = patternStart or 1 + local nameIndex = nameStart or 1 + while patternIndex <= #pattern do + local patternChar = string.sub(pattern, patternIndex, patternIndex) + if patternChar == "*" then + patternIndex += 1 + for candidateIndex = nameIndex, #name + 1 do + if matchesSegment(pattern, name, patternIndex, candidateIndex) then + return true + end + end + return false + elseif patternChar == "?" then + if nameIndex > #name then + return false + end + patternIndex += 1 + nameIndex += 1 + elseif patternChar == "[" then + if nameIndex > #name then + return false + end + local nameChar = string.sub(name, nameIndex, nameIndex) + patternIndex += 1 + local negate = string.sub(pattern, patternIndex, patternIndex) == "!" + if negate then + patternIndex += 1 + end + local found = false + while patternIndex <= #pattern and string.sub(pattern, patternIndex, patternIndex) ~= "]" do + local rangeLow = string.sub(pattern, patternIndex, patternIndex) + patternIndex += 1 + if + string.sub(pattern, patternIndex, patternIndex) == "-" + and patternIndex + 1 <= #pattern + and string.sub(pattern, patternIndex + 1, patternIndex + 1) ~= "]" + then + patternIndex += 1 + local rangeHigh = string.sub(pattern, patternIndex, patternIndex) + patternIndex += 1 + if nameChar >= rangeLow and nameChar <= rangeHigh then + found = true + end + else + if nameChar == rangeLow then + found = true + end + end + end + patternIndex += 1 + if negate then + found = not found + end + if not found then + return false + end + nameIndex += 1 + else + if nameIndex > #name or string.sub(name, nameIndex, nameIndex) ~= patternChar then + return false + end + patternIndex += 1 + nameIndex += 1 + end + end + return nameIndex > #name +end + +type MatchResult = "match" | "no_match" | "partial" + +-- Matches input path segments against pattern segments, returning: +-- "match" — full match (all pattern and input segments consumed) +-- "partial" — input consumed but pattern segments remain (worth descending) +-- "no_match" — input diverges from pattern (prune) +local function matchStep( + patternSegments: { string }, + inputSegments: { string }, + patternIndex: number, + inputIndex: number +): MatchResult + while patternIndex <= #patternSegments do + local segment = patternSegments[patternIndex] + if segment == "**" then + local best: MatchResult = "no_match" + for candidateIndex = inputIndex, #inputSegments + 1 do + local result = matchStep(patternSegments, inputSegments, patternIndex + 1, candidateIndex) + if result == "match" then + return "match" + elseif result == "partial" then + best = "partial" + end + end + return best + else + if inputIndex > #inputSegments then + return "partial" + end + if not matchesSegment(segment, inputSegments[inputIndex]) then + return "no_match" + end + patternIndex += 1 + inputIndex += 1 + end + end + if inputIndex > #inputSegments then + return "match" + end + return "no_match" +end + +local function walk(directory: path.Pathlike, segments: { string }, segmentIndex: number, results: { path.Path }) + if segmentIndex > #segments then + return + end + local success, entries = pcall(fs.listDirectory, directory) + if not success then + return + end + local segment = segments[segmentIndex] + local isLast = segmentIndex == #segments + if segment == "**" then + if not isLast then + walk(directory, segments, segmentIndex + 1, results) + end + for _, entry in entries do + local entryPath = path.join(directory, entry.name) + if isLast then + table.insert(results, entryPath) + end + if entry.type == "dir" then + walk(entryPath, segments, segmentIndex, results) + end + end + else + for _, entry in entries do + if matchesSegment(segment, entry.name) then + local entryPath = path.join(directory, entry.name) + if isLast then + table.insert(results, entryPath) + elseif entry.type == "dir" then + walk(entryPath, segments, segmentIndex + 1, results) + end + end + end + end +end + +local glob = {} + +-- Returns true if the given path matches the glob pattern, without touching the filesystem. +function glob.match(pattern: path.Pathlike, input: path.Pathlike): boolean + local patternParts = path.parse(pattern).parts + local inputParts = path.parse(input).parts + return matchStep(patternParts, inputParts, 1, 1) == "match" +end + +-- Walks the filesystem and returns all paths matching the glob pattern. +function glob.discover(input: path.Pathlike): { path.Path } + local parsed = path.parse(input) + local parts: { string } = parsed.parts + + local firstGlobIndex = #parts + 1 + for index, part in parts do + if string.find(part, "[%*%?%[]") then + firstGlobIndex = index + break + end + end + + if firstGlobIndex > #parts then + if fs.exists(parsed) then + return { parsed } + end + return {} + end + + local prefixPath = table.clone(parsed) + prefixPath.parts = { table.unpack(parts, 1, firstGlobIndex - 1) } + local segments = { table.unpack(parts, firstGlobIndex) } + + local results: { path.Path } = {} + walk(prefixPath, segments, 1, results) + -- To ensure a consistent sort order across POSIX and Windows, we sort by + -- parts rather than sorting based on string representation, which ensures + -- that code implicitly dependent on the ordering (e.g. snapshot tests) do + -- not break when running on different platforms. + table.sort(results, function(a: path.Path, b: path.Path) + for i = 1, math.min(#a.parts, #b.parts) do + if a.parts[i] ~= b.parts[i] then + return a.parts[i] < b.parts[i] + end + end + return #a.parts < #b.parts + end) + return results +end + +return glob diff --git a/tests/batteries/glob.test.luau b/tests/batteries/glob.test.luau new file mode 100644 index 000000000..c2230b22c --- /dev/null +++ b/tests/batteries/glob.test.luau @@ -0,0 +1,288 @@ +local fs = require("@std/fs") +local glob = require("@batteries/glob") +local path = require("@std/path") +local system = require("@std/system") +local test = require("@std/test") + +type Folder = { [string]: Hierarchy } +type Hierarchy = "file" | Folder + +local root = path.format(path.join(system.tmpdir(), "test-glob-hierarchy")) + +local function buildTree(folder: Folder, base: string) + for name, hierarchy in folder do + local full = path.format(path.join(base, name)) + if hierarchy == "file" then + fs.writeStringToFile(full, "content") + else + fs.createDirectory(full) + buildTree(hierarchy :: Folder, full) + end + end +end + +-- Asserts that actual paths match expected paths written with forward slashes, +-- normalizing the expected values to the platform's native separator. +local function assertPaths(assert: any, actual: { string }, expected: { string }) + local normalized = {} + for _, s in expected do + table.insert(normalized, path.format(path.parse(s))) + end + assert.eq(actual, normalized) +end + +local function runGlob(folder: Folder, pattern: string): { string } + buildTree(folder, root) + + local raw = glob.discover(path.join(root, pattern)) + local stripped = {} + for _, result in raw do + local rel = path.relative(root, result) + if #rel.parts > 0 and rel.parts[1] == ".." then + error(`path {path.format(result)} is not under {root} - accidental root escape`) + end + table.insert(stripped, path.format(rel)) + end + return stripped +end + +test.suite("Glob", function(suite) + suite:beforeEach(function() + if fs.exists(root) then + fs.removeDirectory(root, { recursive = true }) + end + fs.createDirectory(root) + end) + + -- literal paths + + suite:case("literalFilePath", function(assert) + local results = runGlob({ + ["hello.txt"] = "file", + }, "hello.txt") + assertPaths(assert, results, { "hello.txt" }) + end) + + suite:case("literalPathNotFound", function(assert) + local results = runGlob({ + ["hello.txt"] = "file", + }, "missing.txt") + assertPaths(assert, results, {}) + end) + + -- star wildcard + + suite:case("starMatchesAllEntries", function(assert) + local results = runGlob({ + ["a.txt"] = "file", + ["b.txt"] = "file", + ["subdir"] = {}, + }, "*") + assertPaths(assert, results, { "a.txt", "b.txt", "subdir" }) + end) + + suite:case("starWithExtensionFilter", function(assert) + local results = runGlob({ + ["a.lua"] = "file", + ["b.txt"] = "file", + ["c.lua"] = "file", + }, "*.lua") + assertPaths(assert, results, { "a.lua", "c.lua" }) + end) + + suite:case("starDoesNotCrossDirectories", function(assert) + local results = runGlob({ + ["dir"] = { + ["nested.txt"] = "file", + }, + ["top.txt"] = "file", + }, "*.txt") + assertPaths(assert, results, { "top.txt" }) + end) + + suite:case("starInNestedDirectory", function(assert) + local results = runGlob({ + ["modules"] = { + ["ModuleA"] = { ["src"] = {} }, + ["ModuleB"] = { ["src"] = {} }, + ["ModuleC"] = { ["src"] = {} }, + }, + }, "modules/*") + assertPaths(assert, results, { "modules/ModuleA", "modules/ModuleB", "modules/ModuleC" }) + end) + + suite:case("noMatches", function(assert) + local results = runGlob({ + ["a.txt"] = "file", + }, "*.lua") + assertPaths(assert, results, {}) + end) + + -- question mark wildcard + + suite:case("questionMarkMatchesSingleChar", function(assert) + local results = runGlob({ + ["a1"] = "file", + ["a2"] = "file", + ["a12"] = "file", + ["b1"] = "file", + }, "a?") + assertPaths(assert, results, { "a1", "a2" }) + end) + + -- character classes + + suite:case("characterClass", function(assert) + local results = runGlob({ + ["a1"] = "file", + ["b1"] = "file", + ["c1"] = "file", + ["d1"] = "file", + }, "[abc]1") + assertPaths(assert, results, { "a1", "b1", "c1" }) + end) + + suite:case("negatedCharacterClass", function(assert) + local results = runGlob({ + ["a1"] = "file", + ["b1"] = "file", + ["c1"] = "file", + }, "[!a]1") + assertPaths(assert, results, { "b1", "c1" }) + end) + + suite:case("characterRange", function(assert) + local results = runGlob({ + ["a1"] = "file", + ["b1"] = "file", + ["c1"] = "file", + ["z1"] = "file", + }, "[a-c]1") + assertPaths(assert, results, { "a1", "b1", "c1" }) + end) + + -- double star + + suite:case("doubleStarRecursive", function(assert) + local results = runGlob({ + ["a.txt"] = "file", + ["sub"] = { + ["b.txt"] = "file", + ["deep"] = { + ["c.txt"] = "file", + }, + }, + }, "**/*.txt") + assertPaths(assert, results, { "a.txt", "sub/b.txt", "sub/deep/c.txt" }) + end) + + suite:case("doubleStarInMiddle", function(assert) + local results = runGlob({ + ["a"] = { + ["x.txt"] = "file", + ["b"] = { + ["x.txt"] = "file", + ["c"] = { + ["x.txt"] = "file", + }, + }, + }, + }, "a/**/x.txt") + assertPaths(assert, results, { "a/b/c/x.txt", "a/b/x.txt", "a/x.txt" }) + end) + + suite:case("doubleStarAtEnd", function(assert) + local results = runGlob({ + ["a.txt"] = "file", + ["sub"] = { + ["b.txt"] = "file", + }, + }, "**") + assertPaths(assert, results, { "a.txt", "sub", "sub/b.txt" }) + end) +end) + +test.suite("GlobMatch", function(suite) + -- star wildcard + + suite:case("starMatchesFilename", function(assert) + assert.that(glob.match("*.txt", "hello.txt")) + end) + + suite:case("starDoesNotCrossDirectories", function(assert) + assert.that(not glob.match("*.txt", "dir/hello.txt")) + end) + + suite:case("starInNestedPattern", function(assert) + assert.that(glob.match("src/*.lua", "src/main.lua")) + assert.that(not glob.match("src/*.lua", "src/lib/main.lua")) + end) + + suite:case("starMatchesEmptySequence", function(assert) + assert.that(glob.match("a*", "a")) + end) + + -- question mark + + suite:case("questionMarkMatchesSingleChar", function(assert) + assert.that(glob.match("a?", "a1")) + assert.that(not glob.match("a?", "a12")) + assert.that(not glob.match("a?", "a")) + end) + + -- character classes + + suite:case("characterClass", function(assert) + assert.that(glob.match("[abc].txt", "a.txt")) + assert.that(not glob.match("[abc].txt", "d.txt")) + end) + + suite:case("negatedCharacterClass", function(assert) + assert.that(glob.match("[!a].txt", "b.txt")) + assert.that(not glob.match("[!a].txt", "a.txt")) + end) + + suite:case("characterRange", function(assert) + assert.that(glob.match("[a-c].txt", "b.txt")) + assert.that(not glob.match("[a-c].txt", "z.txt")) + end) + + -- double star + + suite:case("doubleStarMatchesAcrossDirectories", function(assert) + assert.that(glob.match("**/*.txt", "a.txt")) + assert.that(glob.match("**/*.txt", "sub/b.txt")) + assert.that(glob.match("**/*.txt", "sub/deep/c.txt")) + end) + + suite:case("doubleStarInMiddle", function(assert) + assert.that(glob.match("a/**/x.txt", "a/x.txt")) + assert.that(glob.match("a/**/x.txt", "a/b/x.txt")) + assert.that(glob.match("a/**/x.txt", "a/b/c/x.txt")) + assert.that(not glob.match("a/**/x.txt", "b/x.txt")) + end) + + suite:case("doubleStarAtEnd", function(assert) + assert.that(glob.match("src/**", "src/a.txt")) + assert.that(glob.match("src/**", "src/sub/b.txt")) + end) + + -- literal path + + suite:case("literalMatch", function(assert) + assert.that(glob.match("hello.txt", "hello.txt")) + assert.that(not glob.match("hello.txt", "world.txt")) + end) + + suite:case("literalNestedMatch", function(assert) + assert.that(glob.match("src/main.lua", "src/main.lua")) + assert.that(not glob.match("src/main.lua", "lib/main.lua")) + end) + + -- no match + + suite:case("mismatchedDepth", function(assert) + assert.that(not glob.match("a/b", "a/b/c")) + assert.that(not glob.match("a/b/c", "a/b")) + end) +end) From 1f9971038a8c20d3625e66f482a74e070e8b3aff Mon Sep 17 00:00:00 2001 From: ariel Date: Wed, 8 Apr 2026 10:21:31 -0700 Subject: [PATCH 461/642] infra: improve luthier error behavior and help dialogues (and update `@batteries/cli` a bit in support of that) (#951) I got annoyed by luthier being kinda jank when you do incorrect invocations, so I put in some work to make the UX of the tool better. It has colorized help output now, and now will list subcommands and usage formats for the commands to the user, along side showing the full help output whenever you invoke it. Longer-term, it would probably be better if the cli library actually had a proper concept of subcommands because it could streamline _a lot_ of this logic, but this is an improvement for now. --- batteries/cli.luau | 47 +++++++++++++++++++++++++-- tools/luthier.luau | 80 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/batteries/cli.luau b/batteries/cli.luau index a0ba11a94..8324137f6 100644 --- a/batteries/cli.luau +++ b/batteries/cli.luau @@ -7,6 +7,7 @@ type ArgOptions = { aliases: { string }?, default: string?, required: boolean?, + hidden: boolean?, } type ArgData = { @@ -180,12 +181,52 @@ function cli.forwarded(self: Parser): { string }? return self.parsed.fwdArgs end -function cli.help(self: Parser): () - print("Usage:") +function cli.usage(self: Parser, name: string): string + local parts = { name } + + for _, arg in self.positional do + if not arg.options.hidden then + if arg.options.required then + table.insert(parts, `<{arg.name}>`) + else + table.insert(parts, `[{arg.name}]`) + end + end + end + + for _, arg in self.arguments do + if not arg.options.hidden and arg.kind ~= "positional" and arg.kind ~= "variadic" then + table.insert(parts, "[options]") + break + end + end + + if self.variadicArg and not self.variadicArg.options.hidden then + local v = self.variadicArg + if v.options.required then + table.insert(parts, `<{v.name}...>`) + else + table.insert(parts, `[{v.name}...]`) + end + end + + return table.concat(parts, " ") +end + +function cli.help(self: Parser, formatter: ((s: string) -> string)?): () + local fmt = formatter or function(s) + return s + end + + print(fmt("Usage:")) for _, argument in self.arguments do + if argument.options.hidden then + continue + end + local aliasStr = table.concat(argument.options.aliases or {}, ", ") local reqStr = if argument.options.required then "(required) " else "" - print(string.format(" --%s (-%s) - %s%s", argument.name, aliasStr, reqStr, argument.options.help or "")) + print(fmt(string.format(" --%s (-%s) - %s%s", argument.name, aliasStr, reqStr, argument.options.help or ""))) end end diff --git a/tools/luthier.luau b/tools/luthier.luau index ec43cdf37..ec9b181d6 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -5,6 +5,7 @@ local fs = require("@lute/fs") local process = require("@lute/process") local system = require("@lute/system") local cli = require("@batteries/cli") +local richterm = require("@batteries/richterm") local toml = require("@batteries/toml") type Tune = { @@ -66,9 +67,9 @@ local targetMap: { [string]: { exeName: string } } = { local args = cli.parser() args:add("help", "flag", { help = "show the help message" }) -args:add("luthier", "positional", { help = "the luthier script", required = true }) -- should this be handled automatically? -args:add("subcommand", "positional", { help = "command to execute", required = true }) -args:add("target", "positional", { help = "the thing to build or run", required = true }) +args:add("luthier", "positional", { help = "the luthier script", required = true, hidden = true }) -- should this be handled automatically? +args:add("subcommand", "positional", { help = "command to execute", required = true, hidden = true }) +args:add("target", "positional", { help = "the thing to build or run" }) args:add("verbose", "flag", { help = "show verbose compile output", aliases = { "v" } }) args:add("config", "option", { help = "configuration (default is debug)", default = "debug" }) args:add("clean", "flag", { help = "perform a clean build" }) @@ -533,6 +534,58 @@ local function check(exitCode: number) end end +local function printHelp() + print("luthier — a build tool for lute") + args:help(function(str) + return richterm.combine(richterm.bold, richterm.yellow)(str) + end) +end + +local function usageError(message: string, details: string?, usage: string?) + local lowercased = string.lower(string.sub(message, 1, 1)) .. string.sub(message, 2) + print(richterm.combine(richterm.bold, richterm.red)(`Error: {lowercased}`)) + print(richterm.combine(richterm.bold, richterm.yellow)(`Usage: {usage or args:usage("luthier ")}`)) + if details then + print(richterm.combine(richterm.bold, richterm.yellow)(details)) + end + print() + printHelp() + process.exit(1) +end + +local function subcommandUsage(subcommand: string): string + return args:usage(`luthier {subcommand}`) +end + +local subcommands: { { string } } = { + { "fetch" }, + { "generate" }, + { "configure", "tune" }, + { "build", "craft" }, + { "run", "play" }, +} + +local function availableSubcommands(): string + local parts = {} + for _, group in subcommands do + if #group > 1 then + table.insert(parts, `{group[1]} (or {group[2]})`) + else + table.insert(parts, group[1]) + end + end + return `Available subcommands: {table.concat(parts, ", ")}` +end + +local function availableTargets(): string + local targets = {} + for name in targetMap do + table.insert(targets, name) + end + table.sort(targets) + return `available targets: {table.concat(targets, ", ")}` +end + local function fetchDependency(dependencyInfo: Tune) local dependency = dependencyInfo.dependency @@ -663,12 +716,17 @@ local function run() return call(cmd) end -args:parse({ ... }) +local ok, err = pcall(args.parse, args, { ... }) +if not ok then + -- Strip the "file:line: " prefix that Lua prepends to error messages + local message = string.gsub(tostring(err), "^.+:%d+: ", "") + usageError(message, availableSubcommands()) +end cwd = getSourceRoot() if args:has("help") then - print(args:help()) + printHelp() return end @@ -689,6 +747,10 @@ elseif subcommand == "configure" or subcommand == "tune" then generateFilesIfNeeded() exitCode = configure() elseif subcommand == "build" or subcommand == "craft" then + if not args:get("target") then + usageError(`{subcommand} requires a target`, availableTargets(), subcommandUsage(subcommand)) + end + generateFilesIfNeeded() if not projectPathExists() or not areTuneFilesUpToDate() then @@ -697,6 +759,10 @@ elseif subcommand == "build" or subcommand == "craft" then exitCode = build() elseif subcommand == "run" or subcommand == "play" then + if not args:get("target") then + usageError(`{subcommand} requires a target`, availableTargets(), subcommandUsage(subcommand)) + end + generateFilesIfNeeded() if not projectPathExists() or not areTuneFilesUpToDate() then @@ -709,9 +775,9 @@ elseif subcommand == "run" or subcommand == "play" then exitCode = run() elseif subcommand ~= nil then - error("Unknown subcommand " .. subcommand) + usageError(`Unknown subcommand '{subcommand}'`, availableSubcommands()) else - error("No subcommand given.") + usageError("No subcommand given.", availableSubcommands()) end if exitCode ~= 0 then From 4542c9a4aa52cb31103a819736a2cc1884329d43 Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Wed, 8 Apr 2026 10:44:15 -0700 Subject: [PATCH 462/642] Split net into client and server (#945) --- definitions/net/client.luau | 21 + definitions/net/init.luau | 17 + definitions/{net.luau => net/server.luau} | 23 +- examples/net_example.luau | 2 +- examples/parallel_serve_helper.luau | 4 +- examples/serve.luau | 6 +- examples/serve_html.luau | 8 +- lute/net/CMakeLists.txt | 2 + lute/net/include/lute/net.h | 22 +- lute/net/src/client.cpp | 263 ++++++++ lute/net/src/net.cpp | 728 +--------------------- lute/net/src/server.cpp | 510 +++++++++++++++ lute/require/src/lutevfs.cpp | 19 +- lute/std/libs/net.luau | 8 +- tests/src/moduleresolver.test.cpp | 21 + tests/src/require/lute/lute.luau | 2 + tests/std/net.test.luau | 7 +- 17 files changed, 898 insertions(+), 765 deletions(-) create mode 100644 definitions/net/client.luau create mode 100644 definitions/net/init.luau rename definitions/{net.luau => net/server.luau} (64%) create mode 100644 lute/net/src/client.cpp create mode 100644 lute/net/src/server.cpp diff --git a/definitions/net/client.luau b/definitions/net/client.luau new file mode 100644 index 000000000..f2a80d28d --- /dev/null +++ b/definitions/net/client.luau @@ -0,0 +1,21 @@ +-- lute-lint-global-ignore(unused_variable) +local client = {} + +export type Metadata = { + method: string?, + body: string?, + headers: { [string]: string }?, +} + +export type Response = { + body: string, + headers: { [string]: string }, + status: number, + ok: boolean, +} + +function client.request(url: string, metadata: Metadata?): Response + error("not implemented") +end + +return client diff --git a/definitions/net/init.luau b/definitions/net/init.luau new file mode 100644 index 000000000..e41ff84e1 --- /dev/null +++ b/definitions/net/init.luau @@ -0,0 +1,17 @@ +local client = require("@self/client") +local server = require("@self/server") + +local net = {} + +export type Metadata = client.Metadata +export type Response = client.Response +export type ReceivedRequest = server.ReceivedRequest +export type ServerResponse = server.ServerResponse +export type Handler = server.Handler +export type Configuration = server.Configuration +export type Server = server.Server + +net.client = client +net.server = server + +return net diff --git a/definitions/net.luau b/definitions/net/server.luau similarity index 64% rename from definitions/net.luau rename to definitions/net/server.luau index d2c135815..af69f65f4 100644 --- a/definitions/net.luau +++ b/definitions/net/server.luau @@ -1,22 +1,5 @@ -- lute-lint-global-ignore(unused_variable) -local net = {} - -export type Metadata = { - method: string?, - body: string?, - headers: { [string]: string }?, -} - -export type Response = { - body: string, - headers: { [string]: string }, - status: number, - ok: boolean, -} - -function net.request(url: string, metadata: Metadata?): Response - error("not implemented") -end +local server = {} export type ReceivedRequest = { method: string, @@ -48,8 +31,8 @@ export type Server = { close: () -> (), } -function net.serve(config: Handler | Configuration): Server +function server.serve(config: Handler | Configuration): Server error("not implemented") end -return net +return server diff --git a/examples/net_example.luau b/examples/net_example.luau index 13ec2f7b2..8c1569527 100644 --- a/examples/net_example.luau +++ b/examples/net_example.luau @@ -1,4 +1,4 @@ -local net = require("@lute/net") +local net = require("@std/net") local task = require("@std/task") diff --git a/examples/parallel_serve_helper.luau b/examples/parallel_serve_helper.luau index efaedfc3a..6772e0615 100644 --- a/examples/parallel_serve_helper.luau +++ b/examples/parallel_serve_helper.luau @@ -1,8 +1,8 @@ -local net = require("@lute/net") +local server = require("@lute/net/server") return { serve = function() - net.serve({ + server.serve({ handler = function(_) return "Hello, lute!" end, diff --git a/examples/serve.luau b/examples/serve.luau index 6f2d69ce5..78dba3af5 100644 --- a/examples/serve.luau +++ b/examples/serve.luau @@ -1,13 +1,13 @@ -local net = require("@lute/net") +local server = require("@lute/net/server") print("Starting server...") -- Start the server (non-blocking) -local server = net.serve(function(_) +local instance = server.serve(function(_) return "Hello, lute!" end) -print(`Server listening on http://{server.hostname}:{server.port}`) +print(`Server listening on http://{instance.hostname}:{instance.port}`) print("Program ending, but server will continue running in the background") diff --git a/examples/serve_html.luau b/examples/serve_html.luau index 94d67837c..48ade17e5 100644 --- a/examples/serve_html.luau +++ b/examples/serve_html.luau @@ -1,8 +1,8 @@ -local net = require("@lute/net") +local server = require("@lute/net/server") -local server = net.serve({ +local instance = server.serve({ port = 8080, - handler = function(req: net.ReceivedRequest): net.ServerResponse + handler = function(req: server.ReceivedRequest): server.ServerResponse local headers = "" for key, value in req.headers do headers ..= `\n {key}: {value}` @@ -36,4 +36,4 @@ local server = net.serve({ end, }) -print(`Server listening on http://{server.hostname}:{server.port}`) +print(`Server listening on http://{instance.hostname}:{instance.port}`) diff --git a/lute/net/CMakeLists.txt b/lute/net/CMakeLists.txt index b82fc11d6..06d40a0d4 100644 --- a/lute/net/CMakeLists.txt +++ b/lute/net/CMakeLists.txt @@ -3,7 +3,9 @@ add_library(Lute.Net STATIC) target_sources(Lute.Net PRIVATE include/lute/net.h + src/client.cpp src/net.cpp + src/server.cpp ) target_compile_features(Lute.Net PUBLIC cxx_std_17) diff --git a/lute/net/include/lute/net.h b/lute/net/include/lute/net.h index 04a4fde17..a6e62c827 100644 --- a/lute/net/include/lute/net.h +++ b/lute/net/include/lute/net.h @@ -8,17 +8,29 @@ int luaopen_net(lua_State* L); // open the library as a table on top of the stack int luteopen_net(lua_State* L); -namespace net +int luteopen_net_client(lua_State* L); +int luteopen_net_server(lua_State* L); + +namespace net::client { int request(lua_State* L); -int lua_serve(lua_State* L); - static const luaL_Reg lib[] = { {"request", request}, - {"serve", lua_serve}, {nullptr, nullptr}, }; -} // namespace net +} // namespace net::client + +namespace net::server +{ + +int serve(lua_State* L); + +static const luaL_Reg lib[] = { + {"serve", serve}, + {nullptr, nullptr}, +}; + +} // namespace net::server diff --git a/lute/net/src/client.cpp b/lute/net/src/client.cpp new file mode 100644 index 000000000..6d0784c9f --- /dev/null +++ b/lute/net/src/client.cpp @@ -0,0 +1,263 @@ +#include "lute/net.h" + +#include "lute/common.h" +#include "lute/runtime.h" + +#include "Luau/DenseHash.h" + +#include "lua.h" +#include "lualib.h" + +#include "curl/curl.h" + +#include +#include +#include +#include + +namespace +{ + +struct CurlHolder +{ + CurlHolder() + { + curl_global_init(CURL_GLOBAL_DEFAULT); + } + + ~CurlHolder() + { + curl_global_cleanup(); + } +}; + +static CurlHolder& globalCurlInit() +{ + static CurlHolder holder; + return holder; +} + +} // namespace + +namespace net::client +{ + +static const std::string kEmptyHeaderKey = ""; + +struct CurlResponse +{ + std::string error; + std::vector body; + Luau::DenseHashMap headers; + long status = 0; + + CurlResponse() + : headers(kEmptyHeaderKey) + { + } +}; + +static size_t writeFunction(char* ptr, size_t size, size_t nmemb, void* userdata) +{ + std::vector* target = static_cast*>(userdata); + LUTE_ASSERT(target); + + size_t fullsize = size * nmemb; + target->insert(target->end(), ptr, ptr + fullsize); + return fullsize; +} + +static CurlResponse requestData( + const std::string& url, + const std::string& method, + const std::string& body, + const std::vector>& headers +) +{ + CURL* curl = curl_easy_init(); + CurlResponse resp; + if (!curl) + { + resp.error = "failed to initialize"; + return resp; + } + + std::vector data; + curl_slist* headerList = nullptr; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); + + if (method != "GET") + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str()); + + if (!body.empty()) + { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); + } + + if (!headers.empty()) + { + for (const auto& header_pair : headers) + { + std::string header_str = header_pair.first + ": " + header_pair.second; + headerList = curl_slist_append(headerList, header_str.c_str()); + } + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); + } + + CURLcode res = curl_easy_perform(curl); + + if (headerList) + curl_slist_free_all(headerList); + + if (res != CURLE_OK) + { + resp.error = curl_easy_strerror(res); + curl_easy_cleanup(curl); + return resp; + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp.status); + + resp.body = std::move(data); + + curl_header* prev = nullptr; + curl_header* h; + + while ((h = curl_easy_nextheader(curl, CURLH_HEADER, 0, prev))) + { + std::string name = h->name; + std::string value = h->value; + + if (resp.headers.contains(name)) + { + resp.headers[name] += ", " + value; + } + else + { + resp.headers[name] = value; + } + prev = h; + } + + curl_easy_cleanup(curl); + return resp; +} + +int request(lua_State* L) +{ + std::string url = luaL_checkstring(L, 1); + std::string method = "GET"; + std::string body = ""; + std::vector> headers; + + if (lua_istable(L, 2)) + { + lua_getfield(L, 2, "method"); + if (lua_isstring(L, -1)) + method = lua_tostring(L, -1); + lua_pop(L, 1); + + lua_getfield(L, 2, "body"); + if (lua_isstring(L, -1)) + { + size_t len; + const char* data = lua_tolstring(L, -1, &len); + body.assign(data, data + len); + } + lua_pop(L, 1); + + lua_getfield(L, 2, "headers"); + if (lua_istable(L, -1)) + { + lua_pushnil(L); + while (lua_next(L, -2)) + { + if (lua_isstring(L, -2) && lua_isstring(L, -1)) + { + std::string key = lua_tostring(L, -2); + std::string value = lua_tostring(L, -1); + headers.emplace_back(key, value); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + + auto token = getResumeToken(L); + + // TODO: add cancellations + token->runtime->runInWorkQueue( + [=] + { + CurlResponse resp = requestData(url, method, body, headers); + if (!resp.error.empty()) + { + token->fail("network request failed: " + resp.error); + return; + } + + token->complete( + [resp = std::move(resp)](lua_State* L) + { + lua_createtable(L, 0, 4); + + lua_pushstring(L, "body"); + lua_pushlstring(L, resp.body.data(), resp.body.size()); + lua_settable(L, -3); + + lua_pushstring(L, "headers"); + lua_createtable(L, 0, resp.headers.size()); + for (const auto& header : resp.headers) + { + lua_pushlstring(L, header.first.data(), header.first.size()); + lua_pushlstring(L, header.second.data(), header.second.size()); + lua_settable(L, -3); + } + lua_settable(L, -3); + + lua_pushstring(L, "status"); + lua_pushinteger(L, resp.status); + lua_settable(L, -3); + + lua_pushstring(L, "ok"); + lua_pushboolean(L, (resp.status >= 200 && resp.status < 300)); + lua_settable(L, -3); + + return 1; + } + ); + } + ); + + return lua_yield(L, 0); +} + +} // namespace net::client + +int luteopen_net_client(lua_State* L) +{ + globalCurlInit(); + + lua_createtable(L, 0, 1); + + for (auto& [name, func] : net::client::lib) + { + if (!name || !func) + break; + + lua_pushcfunction(L, func, name); + lua_setfield(L, -2, name); + } + + lua_setreadonly(L, -1, 1); + + return 1; +} diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index f858d963e..917523a9e 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -1,738 +1,24 @@ #include "lute/net.h" -#include "lute/common.h" -#include "lute/runtime.h" - -#include "Luau/DenseHash.h" -#include "Luau/Variant.h" - #include "lua.h" -#include "lualib.h" - -#include "curl/curl.h" -#include "uv.h" - -#include -#include -#include -#include -#include - -#include "App.h" -#include "Loop.h" - -namespace net -{ - -static const std::string kEmptyHeaderKey = ""; -struct CurlResponse -{ - std::string error; - std::vector body; - Luau::DenseHashMap headers; - long status = 0; - - CurlResponse() - : headers(kEmptyHeaderKey) - { - } -}; - -static size_t writeFunction(char* ptr, size_t size, size_t nmemb, void* userdata) -{ - std::vector* target = static_cast*>(userdata); - LUTE_ASSERT(target); - - size_t fullsize = size * nmemb; - target->insert(target->end(), ptr, ptr + fullsize); - return fullsize; -} - -static CurlResponse requestData( - const std::string& url, - const std::string& method, - const std::string& body, - const std::vector>& headers -) -{ - CURL* curl = curl_easy_init(); - CurlResponse resp; - if (!curl) - { - resp.error = "failed to initialize"; - return resp; - } - - std::vector data; - std::vector headerData; - curl_slist* headerList = nullptr; - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); - curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); - - if (method != "GET") - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str()); - - if (!body.empty()) - { - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - } - - if (!headers.empty()) - { - for (const auto& header_pair : headers) - { - std::string header_str = header_pair.first + ": " + header_pair.second; - headerList = curl_slist_append(headerList, header_str.c_str()); - } - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); - } - - CURLcode res = curl_easy_perform(curl); - - if (headerList) - curl_slist_free_all(headerList); - - if (res != CURLE_OK) - { - resp.error = curl_easy_strerror(res); - curl_easy_cleanup(curl); - return resp; - } - - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp.status); - - resp.body = std::move(data); - - curl_header* prev = nullptr; - curl_header* h; - - while ((h = curl_easy_nextheader(curl, CURLH_HEADER, 0, prev))) - { - std::string name = h->name; - std::string value = h->value; - - if (resp.headers.contains(name)) - { - resp.headers[name] += ", " + value; - } - else - { - resp.headers[name] = value; - } - prev = h; - } - - curl_easy_cleanup(curl); - return resp; -} - -int request(lua_State* L) -{ - std::string url = luaL_checkstring(L, 1); - std::string method = "GET"; - std::string body = ""; - std::vector> headers; - - if (lua_istable(L, 2)) - { - lua_getfield(L, 2, "method"); - if (lua_isstring(L, -1)) - method = lua_tostring(L, -1); - lua_pop(L, 1); - - lua_getfield(L, 2, "body"); - if (lua_isstring(L, -1)) - { - size_t len; - const char* data = lua_tolstring(L, -1, &len); - body.assign(data, data + len); - } - lua_pop(L, 1); - - lua_getfield(L, 2, "headers"); - if (lua_istable(L, -1)) - { - lua_pushnil(L); - while (lua_next(L, -2)) - { - if (lua_isstring(L, -2) && lua_isstring(L, -1)) - { - std::string key = lua_tostring(L, -2); - std::string value = lua_tostring(L, -1); - headers.emplace_back(key, value); - } - lua_pop(L, 1); - } - } - lua_pop(L, 1); - } - - auto token = getResumeToken(L); - - // TODO: add cancellations - token->runtime->runInWorkQueue( - [=] - { - CurlResponse resp = requestData(url, method, body, headers); - if (!resp.error.empty()) - { - token->fail("network request failed: " + resp.error); - return; - } - - token->complete( - [resp = std::move(resp)](lua_State* L) - { - lua_createtable(L, 0, 4); - - lua_pushstring(L, "body"); - lua_pushlstring(L, resp.body.data(), resp.body.size()); - lua_settable(L, -3); - - lua_pushstring(L, "headers"); - lua_createtable(L, 0, resp.headers.size()); - for (const auto& header : resp.headers) - { - lua_pushlstring(L, header.first.data(), header.first.size()); - lua_pushlstring(L, header.second.data(), header.second.size()); - lua_settable(L, -3); - } - lua_settable(L, -3); - - lua_pushstring(L, "status"); - lua_pushinteger(L, resp.status); - lua_settable(L, -3); - - lua_pushstring(L, "ok"); - lua_pushboolean(L, (resp.status >= 200 && resp.status < 300)); - lua_settable(L, -3); - - return 1; - } - ); - } - ); - - return lua_yield(L, 0); -} - -using uWSApp = Luau::Variant, std::unique_ptr>; - -static const int kEmptyServerKey = 0; -static Luau::DenseHashMap serverInstances(kEmptyServerKey); -static Luau::DenseHashMap> serverStates(kEmptyServerKey); -static int nextServerId = 1; - -struct ServerLoopState -{ - Luau::Variant app; - Runtime* runtime; - bool running = true; - std::function loopFunction; - std::shared_ptr handlerRef; - std::string hostname; - int port; - bool reusePort = false; -}; - -static void parseQuery(const std::string_view& query, lua_State* L) -{ - lua_createtable(L, 0, 0); - size_t start = 1; // Skip the '?' - size_t end = query.find('&'); - while (end != std::string::npos) - { - std::string_view pair = std::string_view(query.data() + start, end - start); - size_t eq = pair.find('='); - if (eq != std::string::npos) - { - std::string_view key = std::string_view(pair.data(), eq); - std::string_view value = uWS::getDecodedQueryValue(key, query); - lua_pushlstring(L, key.data(), key.size()); - lua_pushlstring(L, value.data(), value.size()); - lua_settable(L, -3); - } - start = end + 1; - end = query.find('&', start); - } - std::string_view pair = std::string_view(query.data() + start, query.size()); - size_t eq = pair.find('='); - if (eq != std::string::npos) - { - std::string_view key = std::string_view(pair.data(), eq); - std::string_view value = uWS::getDecodedQueryValue(key, query); - lua_pushlstring(L, key.data(), key.size()); - lua_pushlstring(L, value.data(), value.size()); - lua_settable(L, -3); - } -} - -static void parseHeaders(auto* req, lua_State* L) -{ - lua_createtable(L, 0, 0); - for (const auto& header : *req) - { - lua_pushlstring(L, header.first.data(), header.first.size()); - lua_pushlstring(L, header.second.data(), header.second.size()); - lua_settable(L, -3); - } -} - -static void handleResponse(auto* res, lua_State* L, int responseIndex) -{ - // Check if the response is a string or a table - if (lua_isstring(L, responseIndex)) - { - std::string body = lua_tostring(L, responseIndex); - res->writeStatus("200 OK"); - res->writeHeader("Content-Type", "text/html"); - res->end(body); - return; - } - - if (!lua_istable(L, responseIndex)) - { - res->writeStatus("500 Internal Server Error"); - res->end("Handler must return a string or a response table"); - return; - } - - - lua_getfield(L, responseIndex, "status"); - int status = lua_isnumber(L, -1) ? lua_tointeger(L, -1) : 200; - lua_pop(L, 1); - - std::string statusText; - switch (status) - { - case 200: - statusText = "200 OK"; - break; - case 201: - statusText = "201 Created"; - break; - case 204: - statusText = "204 No Content"; - break; - case 400: - statusText = "400 Bad Request"; - break; - case 401: - statusText = "401 Unauthorized"; - break; - case 403: - statusText = "403 Forbidden"; - break; - case 404: - statusText = "404 Not Found"; - break; - case 500: - statusText = "500 Internal Server Error"; - break; - default: - statusText = std::to_string(status) + " Status"; - break; - } - res->writeStatus(statusText); - - lua_getfield(L, responseIndex, "headers"); - if (lua_istable(L, -1)) - { - lua_pushnil(L); - while (lua_next(L, -2)) - { - if (lua_isstring(L, -2) && lua_isstring(L, -1)) - { - std::string headerName = lua_tostring(L, -2); - std::string headerValue = lua_tostring(L, -1); - res->writeHeader(headerName, headerValue); - } - lua_pop(L, 1); - } - } - lua_pop(L, 1); - - lua_getfield(L, responseIndex, "body"); - - std::string body = ""; - size_t bodyLength; - const char* bodyData = lua_tolstring(L, -1, &bodyLength); - body.assign(bodyData, bodyData + bodyLength); - lua_pop(L, 1); - - res->end(body); -} - -static void processRequest( - std::shared_ptr state, - auto* res, - auto* req, - const std::string& method, - const std::string_view& path, - const std::string_view& query, - const std::string_view& body -) -{ - lua_State* L = lua_newthread(state->runtime->GL); - luaL_sandboxthread(L); - std::shared_ptr threadRef = getRefForThread(L); - lua_pop(state->runtime->GL, 1); - - lua_createtable(L, 0, 5); - - lua_pushstring(L, "method"); - lua_pushstring(L, method.c_str()); - lua_settable(L, -3); - - lua_pushstring(L, "path"); - lua_pushlstring(L, path.data(), path.size()); - lua_settable(L, -3); - - lua_pushstring(L, "query"); - parseQuery(query, L); - lua_settable(L, -3); - - lua_pushstring(L, "headers"); - parseHeaders(req, L); - lua_settable(L, -3); - - lua_pushstring(L, "body"); - lua_pushlstring(L, body.data(), body.size()); - lua_settable(L, -3); - - state->handlerRef->push(L); - - lua_pushvalue(L, -2); - lua_remove(L, -3); - - int status = lua_resume(L, nullptr, 1); - if (status != LUA_OK && status != LUA_YIELD) - { - std::string error = lua_tostring(L, -1); - lua_pop(L, 1); - - res->writeStatus("500 Internal Server Error"); - res->end("Server error: " + error); - return; - } - - handleResponse(res, L, -1); - - lua_pop(L, 1); -} - -void setupAppAndListen(auto* app, std::shared_ptr state, bool& success) -{ - app->any( - "/*", - [state](auto* res, auto* req) - { - std::string method = std::string(req->getMethod()); - std::transform(method.begin(), method.end(), method.begin(), ::toupper); - std::string_view url = req->getFullUrl(); - std::string_view path = url; - - // Split URL into path and query - size_t queryPos = url.find('?'); - std::string query; - if (queryPos != std::string::npos) - { - path = std::string_view(url.data(), queryPos); - query = std::string_view(url.data() + queryPos, url.size() - queryPos); - } - - res->onAborted( - []() - { - // TODO: handle aborted requests - } - ); - - std::unique_ptr bodyBuffer; - res->onData( - [state, res, req, method, path, query, bodyBuffer = std::move(bodyBuffer)](std::string_view data, bool last) mutable - { - if (last) - { - if (bodyBuffer.get()) - { - bodyBuffer->append(data); - processRequest(state, res, req, method, path, query, *bodyBuffer); - } - else - { - processRequest(state, res, req, method, path, query, data); - } - } - else - { - if (bodyBuffer.get()) - { - bodyBuffer->append(data); - } - else - { - bodyBuffer = std::make_unique(data); - } - } - } - ); - } - ); - - int options = state->reusePort ? LIBUS_LISTEN_DEFAULT : LIBUS_LISTEN_EXCLUSIVE_PORT; - - app->listen( - state->hostname, - state->port, - options, - [&success](auto* listen_socket) - { - success = (listen_socket != nullptr); - } - ); -} - -bool closeServer(int serverId) -{ - if (!serverInstances.contains(serverId) || !serverStates.contains(serverId)) - { - return false; - } - - Luau::visit( - [](auto* appPtr) - { - if (appPtr) - appPtr->close(); - }, - serverStates[serverId]->app - ); - serverStates[serverId]->running = false; - - Luau::visit( - [](auto& ptr) - { - if (ptr) - ptr.reset(); - }, - serverInstances[serverId] - ); - serverStates[serverId] = nullptr; - - return true; -} - -int lua_serve(lua_State* L) -{ - uWS::Loop::get(getRuntimeLoop(L)); - - std::string hostname = "127.0.0.1"; - int port = 3000; - bool reusePort = false; - std::optional tlsOptions; - int handlerIndex = 1; - - // Check if first argument is a table (config) or function (handler) - if (lua_istable(L, 1)) - { - lua_getfield(L, 1, "hostname"); - if (lua_isstring(L, -1)) - { - hostname = lua_tostring(L, -1); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "port"); - if (lua_isnumber(L, -1)) - { - port = lua_tointeger(L, -1); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "reuseport"); - if (lua_isboolean(L, -1)) - { - reusePort = lua_toboolean(L, -1); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "tls"); - if (lua_istable(L, -1)) - { - tlsOptions.emplace(); - - lua_getfield(L, -1, "certfilename"); - if (!lua_isstring(L, -1)) - { - luaL_errorL(L, "tls config requires 'certfilename' (string)"); - return 0; - } - tlsOptions->cert_file_name = lua_tostring(L, -1); - lua_pop(L, 1); - - lua_getfield(L, -1, "keyfilename"); - if (!lua_isstring(L, -1)) - { - luaL_errorL(L, "tls config requires 'keyfilename' (string)"); - return 0; - } - tlsOptions->key_file_name = lua_tostring(L, -1); - lua_pop(L, 1); - - lua_getfield(L, -1, "passphrase"); - if (lua_isstring(L, -1)) - { - tlsOptions->passphrase = lua_tostring(L, -1); - } - lua_pop(L, 1); - - lua_getfield(L, -1, "cafilename"); - if (lua_isstring(L, -1)) - { - tlsOptions->ca_file_name = lua_tostring(L, -1); - } - lua_pop(L, 1); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "handler"); - if (!lua_isfunction(L, -1)) - { - lua_pop(L, 1); - luaL_errorL(L, "handler function is required in config table"); - return 0; - } - lua_insert(L, -1); - handlerIndex = lua_gettop(L); - } - else if (!lua_isfunction(L, 1)) - { - luaL_errorL(L, "serve requires a handler function or config table"); - return 0; - } - - Runtime* runtime = getRuntime(L); - - int serverId = nextServerId++; - - auto state = std::make_shared(); - state->runtime = runtime; - state->hostname = hostname; - state->port = port; - state->reusePort = reusePort; - - lua_pushvalue(L, handlerIndex); - state->handlerRef = std::make_shared(L, -1); - lua_pop(L, 1); - - uWSApp app; - bool success = false; - - if (tlsOptions) - { - auto ssl_app = std::make_unique(*tlsOptions); - state->app = ssl_app.get(); - setupAppAndListen(ssl_app.get(), state, success); - app = std::move(ssl_app); - } - else - { - auto plain_app = std::make_unique(); - state->app = plain_app.get(); - setupAppAndListen(plain_app.get(), state, success); - app = std::move(plain_app); - } - - if (!success) - { - luaL_errorL(L, "failed to listen on port %d, is it already in use? consider the reuseport option", port); - return 0; - } - - serverInstances[serverId] = std::move(app); - serverStates[serverId] = state; - - lua_createtable(L, 0, 3); - - lua_pushstring(L, "hostname"); - lua_pushstring(L, hostname.c_str()); - lua_settable(L, -3); - - lua_pushstring(L, "port"); - lua_pushinteger(L, port); - lua_settable(L, -3); - - lua_pushstring(L, "close"); - lua_pushinteger(L, serverId); - lua_pushcclosurek( - L, - [](lua_State* L) -> int - { - int serverId = lua_tointeger(L, lua_upvalueindex(1)); - - lua_pushboolean(L, closeServer(serverId)); - return 1; - }, - "server_close", - 1, - nullptr - ); - lua_settable(L, -3); - - return 1; -} - -} // namespace net - -struct CurlHolder -{ - CurlHolder() - { - curl_global_init(CURL_GLOBAL_DEFAULT); - } - - ~CurlHolder() - { - curl_global_cleanup(); - } -}; - -static CurlHolder& globalCurlInit() -{ - static CurlHolder holder; - return holder; -} int luaopen_net(lua_State* L) { - globalCurlInit(); - - luaL_register(L, "net", net::lib); + luteopen_net(L); + lua_setglobal(L, "net"); return 1; } int luteopen_net(lua_State* L) { - globalCurlInit(); - - lua_createtable(L, 0, std::size(net::lib)); + lua_createtable(L, 0, 2); - for (auto& [name, func] : net::lib) - { - if (!name || !func) - break; + luteopen_net_client(L); + lua_setfield(L, -2, "client"); - lua_pushcfunction(L, func, name); - lua_setfield(L, -2, name); - } + luteopen_net_server(L); + lua_setfield(L, -2, "server"); lua_setreadonly(L, -1, 1); diff --git a/lute/net/src/server.cpp b/lute/net/src/server.cpp new file mode 100644 index 000000000..a170008c5 --- /dev/null +++ b/lute/net/src/server.cpp @@ -0,0 +1,510 @@ +#include "lute/net.h" + +#include "lute/common.h" +#include "lute/runtime.h" + +#include "Luau/DenseHash.h" +#include "Luau/Variant.h" + +#include "lua.h" +#include "lualib.h" + +#include "uv.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "App.h" +#include "Loop.h" + +namespace net::server +{ + +using uWSApp = Luau::Variant, std::unique_ptr>; + +static const int kEmptyServerKey = 0; +static Luau::DenseHashMap serverInstances(kEmptyServerKey); +static Luau::DenseHashMap> serverStates(kEmptyServerKey); +static int nextServerId = 1; + +struct ServerLoopState +{ + Luau::Variant app; + Runtime* runtime; + bool running = true; + std::function loopFunction; + std::shared_ptr handlerRef; + std::string hostname; + int port; + bool reusePort = false; +}; + +static void parseQuery(const std::string_view& query, lua_State* L) +{ + lua_createtable(L, 0, 0); + size_t start = 1; // Skip the '?' + size_t end = query.find('&'); + while (end != std::string::npos) + { + std::string_view pair = std::string_view(query.data() + start, end - start); + size_t eq = pair.find('='); + if (eq != std::string::npos) + { + std::string_view key = std::string_view(pair.data(), eq); + std::string_view value = uWS::getDecodedQueryValue(key, query); + lua_pushlstring(L, key.data(), key.size()); + lua_pushlstring(L, value.data(), value.size()); + lua_settable(L, -3); + } + start = end + 1; + end = query.find('&', start); + } + std::string_view pair = std::string_view(query.data() + start, query.size()); + size_t eq = pair.find('='); + if (eq != std::string::npos) + { + std::string_view key = std::string_view(pair.data(), eq); + std::string_view value = uWS::getDecodedQueryValue(key, query); + lua_pushlstring(L, key.data(), key.size()); + lua_pushlstring(L, value.data(), value.size()); + lua_settable(L, -3); + } +} + +static void parseHeaders(auto* req, lua_State* L) +{ + lua_createtable(L, 0, 0); + for (const auto& header : *req) + { + lua_pushlstring(L, header.first.data(), header.first.size()); + lua_pushlstring(L, header.second.data(), header.second.size()); + lua_settable(L, -3); + } +} + +static void handleResponse(auto* res, lua_State* L, int responseIndex) +{ + if (lua_isstring(L, responseIndex)) + { + std::string body = lua_tostring(L, responseIndex); + res->writeStatus("200 OK"); + res->writeHeader("Content-Type", "text/html"); + res->end(body); + return; + } + + if (!lua_istable(L, responseIndex)) + { + res->writeStatus("500 Internal Server Error"); + res->end("Handler must return a string or a response table"); + return; + } + + lua_getfield(L, responseIndex, "status"); + int status = lua_isnumber(L, -1) ? lua_tointeger(L, -1) : 200; + lua_pop(L, 1); + + std::string statusText; + switch (status) + { + case 200: + statusText = "200 OK"; + break; + case 201: + statusText = "201 Created"; + break; + case 204: + statusText = "204 No Content"; + break; + case 400: + statusText = "400 Bad Request"; + break; + case 401: + statusText = "401 Unauthorized"; + break; + case 403: + statusText = "403 Forbidden"; + break; + case 404: + statusText = "404 Not Found"; + break; + case 500: + statusText = "500 Internal Server Error"; + break; + default: + statusText = std::to_string(status) + " Status"; + break; + } + res->writeStatus(statusText); + + lua_getfield(L, responseIndex, "headers"); + if (lua_istable(L, -1)) + { + lua_pushnil(L); + while (lua_next(L, -2)) + { + if (lua_isstring(L, -2) && lua_isstring(L, -1)) + { + std::string headerName = lua_tostring(L, -2); + std::string headerValue = lua_tostring(L, -1); + res->writeHeader(headerName, headerValue); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_getfield(L, responseIndex, "body"); + + std::string body = ""; + size_t bodyLength; + const char* bodyData = lua_tolstring(L, -1, &bodyLength); + body.assign(bodyData, bodyData + bodyLength); + lua_pop(L, 1); + + res->end(body); +} + +static void processRequest( + std::shared_ptr state, + auto* res, + auto* req, + const std::string& method, + const std::string_view& path, + const std::string_view& query, + const std::string_view& body +) +{ + lua_State* L = lua_newthread(state->runtime->GL); + luaL_sandboxthread(L); + std::shared_ptr threadRef = getRefForThread(L); + lua_pop(state->runtime->GL, 1); + + lua_createtable(L, 0, 5); + + lua_pushstring(L, "method"); + lua_pushstring(L, method.c_str()); + lua_settable(L, -3); + + lua_pushstring(L, "path"); + lua_pushlstring(L, path.data(), path.size()); + lua_settable(L, -3); + + lua_pushstring(L, "query"); + parseQuery(query, L); + lua_settable(L, -3); + + lua_pushstring(L, "headers"); + parseHeaders(req, L); + lua_settable(L, -3); + + lua_pushstring(L, "body"); + lua_pushlstring(L, body.data(), body.size()); + lua_settable(L, -3); + + state->handlerRef->push(L); + + lua_pushvalue(L, -2); + lua_remove(L, -3); + + int status = lua_resume(L, nullptr, 1); + if (status != LUA_OK && status != LUA_YIELD) + { + std::string error = lua_tostring(L, -1); + lua_pop(L, 1); + + res->writeStatus("500 Internal Server Error"); + res->end("Server error: " + error); + return; + } + + handleResponse(res, L, -1); + + lua_pop(L, 1); +} + +static void setupAppAndListen(auto* app, std::shared_ptr state, bool& success) +{ + app->any( + "/*", + [state](auto* res, auto* req) + { + std::string method = std::string(req->getMethod()); + std::transform(method.begin(), method.end(), method.begin(), ::toupper); + std::string_view url = req->getFullUrl(); + std::string_view path = url; + + size_t queryPos = url.find('?'); + std::string query; + if (queryPos != std::string::npos) + { + path = std::string_view(url.data(), queryPos); + query = std::string_view(url.data() + queryPos, url.size() - queryPos); + } + + res->onAborted( + []() + { + // TODO: handle aborted requests + } + ); + + std::unique_ptr bodyBuffer; + res->onData( + [state, res, req, method, path, query, bodyBuffer = std::move(bodyBuffer)](std::string_view data, bool last) mutable + { + if (last) + { + if (bodyBuffer.get()) + { + bodyBuffer->append(data); + processRequest(state, res, req, method, path, query, *bodyBuffer); + } + else + { + processRequest(state, res, req, method, path, query, data); + } + } + else + { + if (bodyBuffer.get()) + { + bodyBuffer->append(data); + } + else + { + bodyBuffer = std::make_unique(data); + } + } + } + ); + } + ); + + int options = state->reusePort ? LIBUS_LISTEN_DEFAULT : LIBUS_LISTEN_EXCLUSIVE_PORT; + + app->listen( + state->hostname, + state->port, + options, + [&success](auto* listen_socket) + { + success = (listen_socket != nullptr); + } + ); +} + +static bool closeServer(int serverId) +{ + if (!serverInstances.contains(serverId) || !serverStates.contains(serverId)) + { + return false; + } + + Luau::visit( + [](auto* appPtr) + { + if (appPtr) + appPtr->close(); + }, + serverStates[serverId]->app + ); + serverStates[serverId]->running = false; + + Luau::visit( + [](auto& ptr) + { + if (ptr) + ptr.reset(); + }, + serverInstances[serverId] + ); + serverStates[serverId] = nullptr; + + return true; +} + +int serve(lua_State* L) +{ + uWS::Loop::get(getRuntimeLoop(L)); + + std::string hostname = "127.0.0.1"; + int port = 3000; + bool reusePort = false; + std::optional tlsOptions; + int handlerIndex = 1; + + if (lua_istable(L, 1)) + { + lua_getfield(L, 1, "hostname"); + if (lua_isstring(L, -1)) + { + hostname = lua_tostring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, 1, "port"); + if (lua_isnumber(L, -1)) + { + port = lua_tointeger(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, 1, "reuseport"); + if (lua_isboolean(L, -1)) + { + reusePort = lua_toboolean(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, 1, "tls"); + if (lua_istable(L, -1)) + { + tlsOptions.emplace(); + + lua_getfield(L, -1, "certfilename"); + if (!lua_isstring(L, -1)) + { + luaL_errorL(L, "tls config requires 'certfilename' (string)"); + return 0; + } + tlsOptions->cert_file_name = lua_tostring(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "keyfilename"); + if (!lua_isstring(L, -1)) + { + luaL_errorL(L, "tls config requires 'keyfilename' (string)"); + return 0; + } + tlsOptions->key_file_name = lua_tostring(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "passphrase"); + if (lua_isstring(L, -1)) + { + tlsOptions->passphrase = lua_tostring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "cafilename"); + if (lua_isstring(L, -1)) + { + tlsOptions->ca_file_name = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + lua_getfield(L, 1, "handler"); + if (!lua_isfunction(L, -1)) + { + lua_pop(L, 1); + luaL_errorL(L, "handler function is required in config table"); + return 0; + } + lua_insert(L, -1); + handlerIndex = lua_gettop(L); + } + else if (!lua_isfunction(L, 1)) + { + luaL_errorL(L, "serve requires a handler function or config table"); + return 0; + } + + Runtime* runtime = getRuntime(L); + + int serverId = nextServerId++; + + auto state = std::make_shared(); + state->runtime = runtime; + state->hostname = hostname; + state->port = port; + state->reusePort = reusePort; + + lua_pushvalue(L, handlerIndex); + state->handlerRef = std::make_shared(L, -1); + lua_pop(L, 1); + + uWSApp app; + bool success = false; + + if (tlsOptions) + { + auto ssl_app = std::make_unique(*tlsOptions); + state->app = ssl_app.get(); + setupAppAndListen(ssl_app.get(), state, success); + app = std::move(ssl_app); + } + else + { + auto plain_app = std::make_unique(); + state->app = plain_app.get(); + setupAppAndListen(plain_app.get(), state, success); + app = std::move(plain_app); + } + + if (!success) + { + luaL_errorL(L, "failed to listen on port %d, is it already in use? consider the reuseport option", port); + return 0; + } + + serverInstances[serverId] = std::move(app); + serverStates[serverId] = state; + + lua_createtable(L, 0, 3); + + lua_pushstring(L, "hostname"); + lua_pushstring(L, hostname.c_str()); + lua_settable(L, -3); + + lua_pushstring(L, "port"); + lua_pushinteger(L, port); + lua_settable(L, -3); + + lua_pushstring(L, "close"); + lua_pushinteger(L, serverId); + lua_pushcclosurek( + L, + [](lua_State* L) -> int + { + int serverId = lua_tointeger(L, lua_upvalueindex(1)); + + lua_pushboolean(L, closeServer(serverId)); + return 1; + }, + "server_close", + 1, + nullptr + ); + lua_settable(L, -3); + + return 1; +} + +} // namespace net::server + +int luteopen_net_server(lua_State* L) +{ + lua_createtable(L, 0, 1); + + for (auto& [name, func] : net::server::lib) + { + if (!name || !func) + break; + + lua_pushcfunction(L, func, name); + lua_setfield(L, -2, name); + } + + lua_setreadonly(L, -1, 1); + + return 1; +} diff --git a/lute/require/src/lutevfs.cpp b/lute/require/src/lutevfs.cpp index 95a4280de..f5ae00e86 100644 --- a/lute/require/src/lutevfs.cpp +++ b/lute/require/src/lutevfs.cpp @@ -24,7 +24,9 @@ const Luau::DenseHashMap kLuteModules = []() map["@lute/crypto.luau"] = luteopen_crypto; map["@lute/fs.luau"] = luteopen_fs; map["@lute/luau.luau"] = luteopen_luau; - map["@lute/net.luau"] = luteopen_net; + map["@lute/net/init.luau"] = luteopen_net; + map["@lute/net/client.luau"] = luteopen_net_client; + map["@lute/net/server.luau"] = luteopen_net_server; map["@lute/process.luau"] = luteopen_process; map["@lute/task.luau"] = luteopen_task; map["@lute/vm.luau"] = luteopen_vm; @@ -41,7 +43,20 @@ static bool isLuteModule(const std::string& path) static bool isLuteDirectory(const std::string& path) { - return path == "@lute"; + if (path == "@lute") + return true; + + std::string prefix = path + "/"; + for (const auto& [modulePath, open] : kLuteModules) + { + if (modulePath.empty() || !open) + continue; + + if (modulePath.rfind(prefix, 0) == 0) + return true; + } + + return false; } NavigationStatus LuteVfs::resetToPath(const std::string& path) diff --git a/lute/std/libs/net.luau b/lute/std/libs/net.luau index 815dc0d58..c9ae2eefe 100644 --- a/lute/std/libs/net.luau +++ b/lute/std/libs/net.luau @@ -2,15 +2,15 @@ -- @std/net -- stdlib for `@lute/net` -local net = require("@lute/net") +local client = require("@lute/net/client") local netlib = {} -export type Metadata = net.Metadata -export type Response = net.Response +export type Metadata = client.Metadata +export type Response = client.Response function netlib.request(url: string, metadata: Metadata?): Response - return net.request(url, metadata) + return client.request(url, metadata) end return table.freeze(netlib) diff --git a/tests/src/moduleresolver.test.cpp b/tests/src/moduleresolver.test.cpp index d29ea963b..41aa417da 100644 --- a/tests/src/moduleresolver.test.cpp +++ b/tests/src/moduleresolver.test.cpp @@ -65,6 +65,27 @@ TEST_CASE("moduleresolver_resolve_for_typecheck") CHECK(resolved->path == "@lute/process.luau"); } + SUBCASE("resolve_lute_net_from_std") + { + auto resolved = resolveForTypeCheck("@lute/net", "@std/process.luau", &error); + REQUIRE(resolved); + CHECK(resolved->path == "@lute/net/init.luau"); + } + + SUBCASE("resolve_lute_net_client_from_std") + { + auto resolved = resolveForTypeCheck("@lute/net/client", "@std/process.luau", &error); + REQUIRE(resolved); + CHECK(resolved->path == "@lute/net/client.luau"); + } + + SUBCASE("resolve_lute_net_server_from_std") + { + auto resolved = resolveForTypeCheck("@lute/net/server", "@std/process.luau", &error); + REQUIRE(resolved); + CHECK(resolved->path == "@lute/net/server.luau"); + } + SUBCASE("resolve_std_from_batteries") { auto resolved = resolveForTypeCheck("@std/process", "@batteries/base64.luau", &error); diff --git a/tests/src/require/lute/lute.luau b/tests/src/require/lute/lute.luau index 84477ff01..29f7aaa80 100644 --- a/tests/src/require/lute/lute.luau +++ b/tests/src/require/lute/lute.luau @@ -12,6 +12,8 @@ assert(type(crypto) == "table") assert(type(fs) == "table") assert(type(luau) == "table") assert(type(net) == "table") +assert(type(net.client) == "table") +assert(type(net.server) == "table") assert(type(process) == "table") assert(type(task) == "table") assert(type(vm) == "table") diff --git a/tests/std/net.test.luau b/tests/std/net.test.luau index 4312d4af4..ab3b6622f 100644 --- a/tests/std/net.test.luau +++ b/tests/std/net.test.luau @@ -1,10 +1,11 @@ -local net = require("@lute/net") +local net = require("@std/net") +local server = require("@lute/net/server") local test = require("@std/test") test.suite("NetTestSuite", function(suite) suite:case("serveLocallyAndRequest", function(assert) - local server = net.serve({ - handler = function(_req: net.ReceivedRequest): net.ServerResponse + local server = server.serve({ + handler = function(_req: server.ReceivedRequest): server.ServerResponse return { status = 200, body = "Hello, World!", From d3205297948433678922fb9c0880ef59660f401c Mon Sep 17 00:00:00 2001 From: Nick Winans Date: Wed, 8 Apr 2026 11:56:21 -0700 Subject: [PATCH 463/642] Fix luthier command (#955) --- .lute/luthier.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lute/luthier.luau b/.lute/luthier.luau index 9d8e5e1f8..4b2fd0b59 100644 --- a/.lute/luthier.luau +++ b/.lute/luthier.luau @@ -5,7 +5,7 @@ local process = require("@lute/process") -- LUAUFIX: should `...` actually have the type `string...` for command-line environments local args: { [number]: string, n: number } = table.pack(...) :: any -local cmd: {string} = { process.execpath(), "tools/luthier.luau", table.unpack(args, 2, args.n) } +local cmd: {string} = { process.execPath(), "tools/luthier.luau", table.unpack(args, 2, args.n) } local result = process.run(cmd, { stdio = "inherit" }) process.exit(result.exitcode) From b8ca128593bb08df47563a7120b582bfa0b205f2 Mon Sep 17 00:00:00 2001 From: ariel Date: Wed, 8 Apr 2026 12:44:01 -0700 Subject: [PATCH 464/642] infra: fix typechecking of luthier by using `xpcall` instead of `pcall` (#956) I meant to do this in the #951 PR, but I forgot to. We should use `xpcall` instead of `pcall` so that the type system actually works correctly here. --- tools/luthier.luau | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tools/luthier.luau b/tools/luthier.luau index ec9b181d6..1dad7c660 100644 --- a/tools/luthier.luau +++ b/tools/luthier.luau @@ -716,12 +716,14 @@ local function run() return call(cmd) end -local ok, err = pcall(args.parse, args, { ... }) -if not ok then +local parameters = { ... } +xpcall(function() + args:parse(parameters) +end, function(e) -- Strip the "file:line: " prefix that Lua prepends to error messages - local message = string.gsub(tostring(err), "^.+:%d+: ", "") + local message = string.gsub(tostring(e), "^.+:%d+: ", "") usageError(message, availableSubcommands()) -end +end) cwd = getSourceRoot() From e381b5559267b7dfff29b71ff219cfa160d2f16f Mon Sep 17 00:00:00 2001 From: ariel Date: Wed, 8 Apr 2026 16:00:30 -0700 Subject: [PATCH 465/642] runtime: fix a bug where durations from `@lute/fs` would not work correctly without requiring `@lute/time` (#958) A strange bug got reported on Discord (and unfortunately they never made a ticket here) where durations that the user got from `@lute/fs` would ultimately behave incorrectly when `@lute/time` was not imported into that script. The underlying cause is that the metatable was not initialized when `@lute/time` was not imported, and therefore the code that constructs the durations in `@lute/fs` would look for a duration metatable that did not exist and just set the metatable to `nil`. We fix this by making initialization of the duration library idempotent and then invoking that initialization for `@lute/fs` as well. --- definitions/fs.luau | 8 +++++--- examples/fs_metadata.luau | 10 ++++++++++ lute/fs/src/fs.cpp | 3 +++ lute/time/include/lute/time.h | 1 + lute/time/src/time.cpp | 8 ++++++-- tests/std/fs.test.luau | 24 ++++++++++++++++++++++++ 6 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 examples/fs_metadata.luau diff --git a/definitions/fs.luau b/definitions/fs.luau index 9782efd24..23c302913 100644 --- a/definitions/fs.luau +++ b/definitions/fs.luau @@ -1,4 +1,6 @@ -- lute-lint-global-ignore(unused_variable) +local time = require("./time") + local fs = {} export type FileHandle = { @@ -13,9 +15,9 @@ export type FileMetadata = { type: FileType, permissions: { readonly: boolean }, size: number, - created: any, - accessed: any, - modified: any, + created: time.Duration, + accessed: time.Duration, + modified: time.Duration, } export type DirectoryEntry = { diff --git a/examples/fs_metadata.luau b/examples/fs_metadata.luau new file mode 100644 index 000000000..e9f939d47 --- /dev/null +++ b/examples/fs_metadata.luau @@ -0,0 +1,10 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@std/process") + +local scriptPath = path.resolve(process.cwd(), process.args[1]) + +local metadata = fs.metadata(scriptPath) +local created = metadata.created + +print(tostring(created)) diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 4ae318404..3de707743 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -1,6 +1,7 @@ #include "lute/fs.h" #include "lute/runtime.h" +#include "lute/time.h" #include "lute/userdatas.h" #include "lua.h" @@ -369,6 +370,8 @@ int listdir(lua_State* L) static void initalizeFS(lua_State* L) { + init_duration_lib(L); + luaL_newmetatable(L, "WatchHandle"); lua_pushcfunction( diff --git a/lute/time/include/lute/time.h b/lute/time/include/lute/time.h index 8a7dc34f4..6050995de 100644 --- a/lute/time/include/lute/time.h +++ b/lute/time/include/lute/time.h @@ -21,6 +21,7 @@ double getSecondsFromTimespec(uv_timespec64_t timespec); uv_timespec64_t getTimespecFromDuration(lua_State* L, int idx); int createDurationFromTimespec(lua_State* L, uv_timespec64_t timespec); int createDurationFromSeconds(lua_State* L, double seconds); +void init_duration_lib(lua_State* L); namespace duration { diff --git a/lute/time/src/time.cpp b/lute/time/src/time.cpp index 1498fd48a..39d39932c 100644 --- a/lute/time/src/time.cpp +++ b/lute/time/src/time.cpp @@ -460,9 +460,13 @@ int lua_since(lua_State* L) } } // namespace libtime -static void init_duration_lib(lua_State* L) +void init_duration_lib(lua_State* L) { - luaL_newmetatable(L, kDurationType); + if (luaL_newmetatable(L, kDurationType) == 0) + { + lua_pop(L, 1); + return; + } // Protect metatable from being changed lua_pushstring(L, "The metatable is locked"); diff --git a/tests/std/fs.test.luau b/tests/std/fs.test.luau index 37513298e..48b3bb688 100644 --- a/tests/std/fs.test.luau +++ b/tests/std/fs.test.luau @@ -228,6 +228,30 @@ test.suite("FsSuite", function(suite) assert.eq(fs.exists(file3), false) end) + suite:case("metadataDurationsHaveWorkingMetatable", function(assert) + local file = path.join(tmpdir, "duration_meta_test.txt") + fs.writeStringToFile(file, "test") + + local m = fs.metadata(file) + + -- duration methods should work without requiring @lute/time + assert.that(m.created:toSeconds() > 0) + assert.that(m.accessed:toSeconds() > 0) + assert.that(m.modified:toSeconds() > 0) + + -- __tostring should produce "seconds.nanoseconds" format, not "userdata: 0x..." + assert.that(tostring(m.created):match("^%d+%.%d+$")) + + -- arithmetic should work + local sum = m.created + m.modified + assert.that(sum:toSeconds() > 0) + + -- comparisons should work + assert.that(m.created <= m.modified or m.modified <= m.created) + + fs.remove(file) + end) + suite:case("removeDirectoryRecursiveFalseWithContents", function(assert) local dir = path.join(tmpdir, "non_empty_dir") fs.createDirectory(dir, { makeParents = true }) From 46d7fdbc0807ff85aa745bae557204da5361d5b2 Mon Sep 17 00:00:00 2001 From: Sora Kanosue Date: Thu, 9 Apr 2026 10:16:03 -0700 Subject: [PATCH 466/642] Updates lute lint to default to cwd if no lintee files specified (#961) --- lute/cli/commands/lint/init.luau | 3 +- tests/cli/lint.test.luau | 65 ++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/lute/cli/commands/lint/init.luau b/lute/cli/commands/lint/init.luau index 6bc8751b6..d036950e7 100644 --- a/lute/cli/commands/lint/init.luau +++ b/lute/cli/commands/lint/init.luau @@ -499,8 +499,7 @@ local function main(...: string) local inputFiles = args:forwarded() local stringInput = args:get("string-input") if (inputFiles == nil or #inputFiles == 0) and not stringInput then - print("Error: No input files or string input specified.\n\n" .. USAGE .. "\nUse --help for more information.") - process.exit(1) + inputFiles = { pathLib.format(process.cwd()) } end local lintConfig: internalTypes.LintConfig = {} diff --git a/tests/cli/lint.test.luau b/tests/cli/lint.test.luau index f82b5a755..389e0a5d4 100644 --- a/tests/cli/lint.test.luau +++ b/tests/cli/lint.test.luau @@ -144,13 +144,6 @@ test.suite("lute lint", function(suite) assert.strContains(result.stdout, USAGE) end) - suite:case("luteLintNoInputFile", function(assert) - local result = process.run({ lutePath, "lint", "-r", "a_rule.luau" }) - - assert.eq(result.exitcode, 1) - assert.strContains(result.stdout, "Error: No input files or string input specified.") - end) - suite:case("luteLintErrorCode0WithNoViolations", function(assert) local output = lintTestHelper(assert, { content = "", expectedExitCode = 0 }) @@ -3597,4 +3590,62 @@ end }, }) end) + + suite:case("luteLintCurrentDirFindsLuauFiles", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createDirectory(testSubDir) + + local violatorPath = path.format(path.join(testSubDir, "violator.luau")) + fs.writeStringToFile(violatorPath, "local x = 1 / 0") + + local result = process.run({ lutePath, "lint" }, { + cwd = path.format(testSubDir), + }) + + assert.eq(result.exitcode, 1) + assert.strContains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) + end) + + suite:case("luteLintCurrentDirFindsLuaFiles", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createDirectory(testSubDir) + + local violatorPath = path.format(path.join(testSubDir, "violator.lua")) + fs.writeStringToFile(violatorPath, "local x = 1 / 0") + + local result = process.run({ lutePath, "lint" }, { + cwd = path.format(testSubDir), + }) + + assert.eq(result.exitcode, 1) + assert.strContains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) + end) + + suite:case("luteLintCurrentDirRecursive", function(assert) + local testSubDir = path.join(tmpDir, "module") + local nestedDir = path.join(testSubDir, "nested") + fs.createDirectory(testSubDir) + fs.createDirectory(nestedDir) + + local violatorPath = path.format(path.join(nestedDir, "violator.luau")) + fs.writeStringToFile(violatorPath, "local x = 1 / 0") + + local result = process.run({ lutePath, "lint" }, { + cwd = path.format(testSubDir), + }) + + assert.eq(result.exitcode, 1) + assert.strContains(result.stdout, LINT_RULE_MESSAGES.divide_by_zero) + end) + + suite:case("luteLintCurrentDirNoLuauFiles", function(assert) + local testSubDir = path.join(tmpDir, "module") + fs.createDirectory(testSubDir) + + local result = process.run({ lutePath, "lint" }, { + cwd = path.format(testSubDir), + }) + + assert.eq(result.exitcode, 0) + end) end) From 1665e5a3c8da38d868b454e45cd341d471a05a6b Mon Sep 17 00:00:00 2001 From: jkelaty-rbx <78873527+jkelaty-rbx@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:00:03 -0700 Subject: [PATCH 467/642] Add support to `lute compile` for bundling .config.luau (#952) Adds support for the compile command to support bundling .config.luau for require alias resolution. --- lute/cli/include/lute/requiresetup.h | 2 +- lute/cli/include/lute/staticrequires.h | 6 +- lute/cli/src/climain.cpp | 2 +- lute/cli/src/requiresetup.cpp | 10 +-- lute/cli/src/staticrequires.cpp | 57 +++++++++---- lute/require/include/lute/bundlevfs.h | 4 +- lute/require/src/bundlevfs.cpp | 50 ++++++++---- tests/src/compile.test.cpp | 37 +++++++++ tests/src/staticrequires.test.cpp | 81 ++++++++++++------- .../staticrequires_luau_config/.config.luau | 7 ++ .../dep/nested/example.luau | 3 + .../src/staticrequires_luau_config/main.luau | 2 + 12 files changed, 189 insertions(+), 72 deletions(-) create mode 100644 tests/src/staticrequires_luau_config/.config.luau create mode 100644 tests/src/staticrequires_luau_config/dep/nested/example.luau create mode 100644 tests/src/staticrequires_luau_config/main.luau diff --git a/lute/cli/include/lute/requiresetup.h b/lute/cli/include/lute/requiresetup.h index a8e19a209..05dbd0074 100644 --- a/lute/cli/include/lute/requiresetup.h +++ b/lute/cli/include/lute/requiresetup.h @@ -22,6 +22,6 @@ lua_State* setupPkgRunState( lua_State* setupBundleState( Runtime& runtime, - Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap luauConfigFiles, Luau::DenseHashMap bundleMap ); diff --git a/lute/cli/include/lute/staticrequires.h b/lute/cli/include/lute/staticrequires.h index 6bb21dbbb..bbd668d62 100644 --- a/lute/cli/include/lute/staticrequires.h +++ b/lute/cli/include/lute/staticrequires.h @@ -31,9 +31,9 @@ class StaticRequireTracer // Get discovered .luaurc files as a map of (lcrPath -> content) // lcrPath is the absolute path with the lowest common prefix stripped - const Luau::DenseHashMap& getLuaurcFiles() const + const Luau::DenseHashMap& getLuauConfigFiles() const { - return luaurcFiles; + return luauConfigFiles; } void printRequireGraph() const; @@ -44,7 +44,7 @@ class StaticRequireTracer Luau::DenseHashSet visited{""}; std::vector discovered; // Absolute paths Luau::DenseHashMap> requireGraph{""}; // Absolute paths - Luau::DenseHashMap luaurcFiles{""}; // LCR-relative path -> content + Luau::DenseHashMap luauConfigFiles{""}; // LCR-relative path -> content std::string lowestCommonRoot; // Extract all require() paths from source code diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 5c1a82fdc..7f0707827 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -578,7 +578,7 @@ int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& rep } // Add the discovered luaurc configuration - payload.setLuauConfig(tracer.getLuaurcFiles()); + payload.setLuauConfig(tracer.getLuauConfigFiles()); // Encode the payload diff --git a/lute/cli/src/requiresetup.cpp b/lute/cli/src/requiresetup.cpp index c1c486f99..5f1f28307 100644 --- a/lute/cli/src/requiresetup.cpp +++ b/lute/cli/src/requiresetup.cpp @@ -99,7 +99,7 @@ static void* createPkgRunRequireContext( static void* createBundleRequireContext( lua_State* L, - Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap luauConfigFiles, Luau::DenseHashMap bundleMap ) { @@ -114,7 +114,7 @@ static void* createBundleRequireContext( if (!ctx) luaL_error(L, "unable to allocate RequireCtx"); - ctx = new (ctx) RequireCtx{std::make_unique(BundleVfs{std::move(luaurcFiles), std::move(bundleMap)})}; + ctx = new (ctx) RequireCtx{std::make_unique(BundleVfs{std::move(luauConfigFiles), std::move(bundleMap)})}; // Store RequireCtx in the registry to keep it alive for the lifetime of // this lua_State. Memory address is used as a key to avoid collisions. @@ -177,18 +177,18 @@ lua_State* setupPkgRunState( lua_State* setupBundleState( Runtime& runtime, - Luau::DenseHashMap luaurcFiles, + Luau::DenseHashMap luauConfigFiles, Luau::DenseHashMap bundleMap ) { return setupState( runtime, - [luaurcFiles = std::move(luaurcFiles), bundleMap = std::move(bundleMap)](lua_State* L) + [luauConfigFiles = std::move(luauConfigFiles), bundleMap = std::move(bundleMap)](lua_State* L) { if (Luau::CodeGen::isSupported()) Luau::CodeGen::create(L); - luaopen_require(L, requireConfigInit, createBundleRequireContext(L, std::move(luaurcFiles), std::move(bundleMap))); + luaopen_require(L, requireConfigInit, createBundleRequireContext(L, std::move(luauConfigFiles), std::move(bundleMap))); } ); } diff --git a/lute/cli/src/staticrequires.cpp b/lute/cli/src/staticrequires.cpp index 9ec8404e5..310a51f32 100644 --- a/lute/cli/src/staticrequires.cpp +++ b/lute/cli/src/staticrequires.cpp @@ -7,6 +7,7 @@ #include "Luau/Ast.h" #include "Luau/Config.h" #include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" #include "Luau/Parser.h" #include "Luau/VecDeque.h" @@ -45,7 +46,7 @@ void StaticRequireTracer::trace(const std::string& entryPoint) visited.clear(); discovered.clear(); requireGraph.clear(); - luaurcFiles.clear(); + luauConfigFiles.clear(); if (!isAbsolutePath(entryPoint)) { @@ -54,7 +55,7 @@ void StaticRequireTracer::trace(const std::string& entryPoint) } // Temporary set to collect absolute paths to .luaurc files - Luau::DenseHashSet luaurcAbsolutePaths{""}; + Luau::DenseHashSet configAbsolutePaths{""}; Luau::VecDeque toProcess; toProcess.push_back(entryPoint); @@ -90,11 +91,33 @@ void StaticRequireTracer::trace(const std::string& entryPoint) while (!dir.empty()) { std::string luaurcPath = dir + "/" + Luau::kConfigName; - if (isFile(luaurcPath)) + std::string luauConfigPath = dir + "/" + Luau::kLuauConfigName; + + bool luaurcExists = isFile(luaurcPath); + bool luauConfigExists = isFile(luauConfigPath); + + if (luaurcExists && luauConfigExists) { - luaurcAbsolutePaths.insert(luaurcPath); - break; + reporter.formatError( + "Warning: Both %s and %s exist in '%s', preferring %s\n", + Luau::kConfigName, + Luau::kLuauConfigName, + dir.c_str(), + Luau::kLuauConfigName + ); + configAbsolutePaths.insert(luauConfigPath); + } + else if (luauConfigExists) + { + configAbsolutePaths.insert(luauConfigPath); } + else if (luaurcExists) + { + configAbsolutePaths.insert(luaurcPath); + } + + if (luaurcExists || luauConfigExists) + break; // Move to parent directory size_t parentSlash = dir.find_last_of("/\\"); @@ -140,7 +163,7 @@ void StaticRequireTracer::trace(const std::string& entryPoint) // Include .luaurc files in the lowest common root calculation std::vector allPaths = discovered; - for (const auto& luaurcPath : luaurcAbsolutePaths) + for (const auto& luaurcPath : configAbsolutePaths) { allPaths.push_back(luaurcPath); } @@ -150,34 +173,36 @@ void StaticRequireTracer::trace(const std::string& entryPoint) // Convert absolute .luaurc paths to LCR-relative .luaurc paths and read their content size_t commonRootLen = lowestCommonRoot.empty() ? 0 : lowestCommonRoot.length() + 1; // +1 for the trailing slash - for (const auto& absolutePath : luaurcAbsolutePaths) + for (const auto& absolutePath : configAbsolutePaths) { - // Get the directory containing the .luaurc file and append .luaurc + // Get the directory and filename of the config file std::string absoluteDir = absolutePath; + std::string configFileName = absoluteDir; size_t lastSlash = absoluteDir.find_last_of("/\\"); if (lastSlash != std::string::npos) { + configFileName = absoluteDir.substr(lastSlash + 1); absoluteDir = absoluteDir.substr(0, lastSlash); } - // Convert to relative path and append .luaurc - std::string relativeLuaurc = ".luaurc"; + // Convert to relative path and append the config filename + std::string relativeConfig = configFileName; if (commonRootLen > 0 && absoluteDir.length() > commonRootLen) { std::string relativeDir = absoluteDir.substr(commonRootLen); - relativeLuaurc = relativeDir + "/.luaurc"; + relativeConfig = relativeDir + "/" + configFileName; } // Read the .luaurc file content std::optional content = readFile(absolutePath); if (content) { - // Store using the .luaurc file path (e.g., "dir/.luaurc" or just ".luaurc") - luaurcFiles[relativeLuaurc] = *content; + // Store using the config file path (e.g., "dir/.luaurc", ".luaurc", or ".config.luau") + luauConfigFiles[relativeConfig] = *content; } else { - reporter.formatError("Warning: Could not read .luaurc file '%s'\n", absolutePath.c_str()); + reporter.formatError("Warning: Could not read config file '%s'\n", absolutePath.c_str()); } } } @@ -263,10 +288,10 @@ void StaticRequireTracer::printRequireGraph() const } // Print luaurc files found - if (!luaurcFiles.empty()) + if (!luauConfigFiles.empty()) { reporter.reportOutput("\n.luaurc files found:"); - for (const auto& [configDir, content] : luaurcFiles) + for (const auto& [configDir, content] : luauConfigFiles) { reporter.formatOutput("\t%s", configDir.c_str()); } diff --git a/lute/require/include/lute/bundlevfs.h b/lute/require/include/lute/bundlevfs.h index 601c19d75..c2ee2be41 100644 --- a/lute/require/include/lute/bundlevfs.h +++ b/lute/require/include/lute/bundlevfs.h @@ -11,7 +11,7 @@ class BundleVfs { public: - BundleVfs(Luau::DenseHashMap luaurcFiles, Luau::DenseHashMap bundleMap); + BundleVfs(Luau::DenseHashMap luauConfigFiles, Luau::DenseHashMap bundleMap); NavigationStatus resetToPath(const std::string& path); @@ -27,6 +27,6 @@ class BundleVfs private: const Luau::DenseHashMap filePathToBytecode; - const Luau::DenseHashMap luaurcFiles; + const Luau::DenseHashMap luauConfigFiles; std::optional modulePath; }; diff --git a/lute/require/src/bundlevfs.cpp b/lute/require/src/bundlevfs.cpp index 10a55f104..315707e92 100644 --- a/lute/require/src/bundlevfs.cpp +++ b/lute/require/src/bundlevfs.cpp @@ -11,9 +11,21 @@ constexpr std::string_view kBundlePrefix = "@bundle"; constexpr std::string_view kBundlePrefixPath = "@bundle/"; -BundleVfs::BundleVfs(Luau::DenseHashMap luaurcFiles, Luau::DenseHashMap bundleMap) +namespace +{ + +std::string getConfigPathFromBundlePath(const std::string& path) +{ + if (path.rfind(kBundlePrefixPath, 0) == 0) + return path.substr(kBundlePrefixPath.size()); + return path; +} + +} // namespace + +BundleVfs::BundleVfs(Luau::DenseHashMap luauConfigFiles, Luau::DenseHashMap bundleMap) : filePathToBytecode(std::move(bundleMap)) - , luaurcFiles(std::move(luaurcFiles)) + , luauConfigFiles(std::move(luauConfigFiles)) { } @@ -153,15 +165,17 @@ ConfigStatus BundleVfs::getConfigStatus() const { LUTE_ASSERT(modulePath); - // Get the potential config path for the current module path - std::string configPath = modulePath->getPotentialConfigPath(".luaurc"); + const std::string luaurcPath = getConfigPathFromBundlePath(modulePath->getPotentialConfigPath(".luaurc")); + const std::string luauConfigPath = getConfigPathFromBundlePath(modulePath->getPotentialConfigPath(".config.luau")); - // Strip @bundle/ prefix if present - if (configPath.rfind(kBundlePrefixPath, 0) == 0) - configPath = configPath.substr(kBundlePrefixPath.size()); + const bool luaurcExists = luauConfigFiles.find(luaurcPath) != nullptr; + const bool luauConfigExists = luauConfigFiles.find(luauConfigPath) != nullptr; - // Check if this config file exists in our luaurc map - if (luaurcFiles.find(configPath) != nullptr) + if (luaurcExists && luauConfigExists) + return ConfigStatus::Ambiguous; + else if (luauConfigExists) + return ConfigStatus::PresentLuau; + else if (luaurcExists) return ConfigStatus::PresentJson; return ConfigStatus::Absent; @@ -171,15 +185,19 @@ std::optional BundleVfs::getConfig() const { LUTE_ASSERT(modulePath); - // Get the potential config path for the current module path - std::string configPath = modulePath->getPotentialConfigPath(".luaurc"); + const ConfigStatus status = getConfigStatus(); - // Strip @bundle/ prefix if present - if (configPath.rfind(kBundlePrefixPath, 0) == 0) - configPath = configPath.substr(kBundlePrefixPath.size()); + std::string configName; + if (status == ConfigStatus::PresentJson) + configName = ".luaurc"; + else if (status == ConfigStatus::PresentLuau) + configName = ".config.luau"; + else + return std::nullopt; + + const std::string configPath = getConfigPathFromBundlePath(modulePath->getPotentialConfigPath(configName)); - // Look up the config content in our luaurc map - const std::string* configContent = luaurcFiles.find(configPath); + const std::string* configContent = luauConfigFiles.find(configPath); if (configContent != nullptr) return *configContent; diff --git a/tests/src/compile.test.cpp b/tests/src/compile.test.cpp index 1f2011f28..dec3edb80 100644 --- a/tests/src/compile.test.cpp +++ b/tests/src/compile.test.cpp @@ -576,3 +576,40 @@ TEST_CASE_FIXTURE(LuteFixture, "compile_command_e2e_requirealias") // Clean up std::remove(outputExePath.c_str()); } + +TEST_CASE_FIXTURE(LuteFixture, "compile_command_e2e_luau_config") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + + // Use a test file that resolves aliases via .config.luau (not .luaurc) + std::string testFilePath = joinPaths(luteProjectRoot, "tests/src/staticrequires_luau_config/main.luau"); + + // Create a temporary output path for the compiled executable + std::string outputExePath = joinPaths(luteProjectRoot, "tests/temp_compiled_e2e_luau_config"); +#ifdef _WIN32 + outputExePath += ".exe"; +#endif + + char executablePlaceholder[] = "lute"; + char compileCommand[] = "compile"; + char outputFlag[] = "--output"; + + std::vector argv = {executablePlaceholder, compileCommand, testFilePath.data(), outputFlag, outputExePath.data()}; + + // Compile should succeed + int compileResult = cliMain(argv.size(), argv.data(), getReporter()); + REQUIRE(compileResult == 0); + + // Verify the output file was created + std::ifstream checkFile(outputExePath, std::ios::binary); + REQUIRE(checkFile.is_open()); + checkFile.close(); + + // Run the compiled executable — alias resolution must work at runtime + std::vector runArgv = {outputExePath.data()}; + int runResult = cliMain(runArgv.size(), runArgv.data(), getReporter()); + CHECK(runResult == 0); + + // Clean up + std::remove(outputExePath.c_str()); +} diff --git a/tests/src/staticrequires.test.cpp b/tests/src/staticrequires.test.cpp index 0d2fe1707..dc2a9fccd 100644 --- a/tests/src/staticrequires.test.cpp +++ b/tests/src/staticrequires.test.cpp @@ -38,12 +38,12 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_simple_dependencies") } // Verify .luaurc file was discovered - auto luaurcFiles = tracer.getLuaurcFiles(); - REQUIRE(luaurcFiles.size() == 2); + auto luauConfigFiles = tracer.getLuauConfigFiles(); + REQUIRE(luauConfigFiles.size() == 2); // Check that .luaurc exists in the map - CHECK(luaurcFiles.find(".luaurc") != nullptr); - CHECK(luaurcFiles.find("other/.luaurc") != nullptr); + CHECK(luauConfigFiles.find(".luaurc") != nullptr); + CHECK(luauConfigFiles.find("other/.luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_circular_dependencies") @@ -71,9 +71,9 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_circular_dependencies") CHECK(tracer.containsAbsolute(absoluteB)); // Verify .luaurc file was discovered - auto luaurcFiles = tracer.getLuaurcFiles(); - REQUIRE(luaurcFiles.size() == 1); - CHECK(luaurcFiles.find(".luaurc") != nullptr); + auto luauConfigFiles = tracer.getLuauConfigFiles(); + REQUIRE(luauConfigFiles.size() == 1); + CHECK(luauConfigFiles.find(".luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_no_dependencies") @@ -92,9 +92,9 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_no_dependencies") CHECK(pairs[0].first == "utils.luau"); // Verify .luaurc file was discovered (should find it in the same directory) - auto luaurcFiles = tracer.getLuaurcFiles(); - REQUIRE(luaurcFiles.size() == 1); - CHECK(luaurcFiles.find(".luaurc") != nullptr); + auto luauConfigFiles = tracer.getLuauConfigFiles(); + REQUIRE(luauConfigFiles.size() == 1); + CHECK(luauConfigFiles.find(".luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_relative_paths") @@ -117,9 +117,9 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_relative_paths") CHECK(tracer.containsAbsolute(absoluteShared)); // Verify .luaurc file was discovered (should find it in parent directory) - auto luaurcFiles = tracer.getLuaurcFiles(); - REQUIRE(luaurcFiles.size() == 1); - CHECK(luaurcFiles.find(".luaurc") != nullptr); + auto luauConfigFiles = tracer.getLuauConfigFiles(); + REQUIRE(luauConfigFiles.size() == 1); + CHECK(luauConfigFiles.find(".luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_requirealias") @@ -145,9 +145,9 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_requirealias") CHECK(tracer.getLowestCommonRoot() == testDir); // Verify .luaurc file was discovered (should find it in parent directory) - auto luaurcFiles = tracer.getLuaurcFiles(); - REQUIRE(luaurcFiles.size() == 1); - CHECK(luaurcFiles.find(".luaurc") != nullptr); + auto luauConfigFiles = tracer.getLuauConfigFiles(); + REQUIRE(luauConfigFiles.size() == 1); + CHECK(luauConfigFiles.find(".luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_require_graph") @@ -179,10 +179,10 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_require_graph") // Verify .luaurc file was discovered - auto luaurcFiles = tracer.getLuaurcFiles(); - REQUIRE(luaurcFiles.size() == 2); - CHECK(luaurcFiles.find(".luaurc") != nullptr); - CHECK(luaurcFiles.find("other/.luaurc") != nullptr); + auto luauConfigFiles = tracer.getLuauConfigFiles(); + REQUIRE(luauConfigFiles.size() == 2); + CHECK(luauConfigFiles.find(".luaurc") != nullptr); + CHECK(luauConfigFiles.find("other/.luaurc") != nullptr); // Verify the graph can be printed without errors (visual inspection of output) tracer.printRequireGraph(); @@ -286,10 +286,10 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_bundle_paths_and_contains") CHECK(!tracer.containsAbsolute(nonExistentAbsolute)); // Verify .luaurc file was discovered - auto luaurcFiles = tracer.getLuaurcFiles(); - REQUIRE(luaurcFiles.size() == 2); - CHECK(luaurcFiles.find(".luaurc") != nullptr); - CHECK(luaurcFiles.find("other/.luaurc") != nullptr); + auto luauConfigFiles = tracer.getLuauConfigFiles(); + REQUIRE(luauConfigFiles.size() == 2); + CHECK(luauConfigFiles.find(".luaurc") != nullptr); + CHECK(luauConfigFiles.find("other/.luaurc") != nullptr); } TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_multiple_luaurc_files") @@ -325,13 +325,38 @@ TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_multiple_luaurc_files") } // Verify both .luaurc files were discovered - auto luaurcFiles = tracer.getLuaurcFiles(); - REQUIRE(luaurcFiles.size() == 2); + auto luauConfigFiles = tracer.getLuauConfigFiles(); + REQUIRE(luauConfigFiles.size() == 2); // Check that both .luaurc files exist in the map - const std::string* rootLuaurc = luaurcFiles.find(".luaurc"); + const std::string* rootLuaurc = luauConfigFiles.find(".luaurc"); REQUIRE(rootLuaurc != nullptr); - const std::string* otherLuaurc = luaurcFiles.find("other/.luaurc"); + const std::string* otherLuaurc = luauConfigFiles.find("other/.luaurc"); REQUIRE(otherLuaurc != nullptr); } + +TEST_CASE_FIXTURE(LuteFixture, "staticrequiretracer_luau_config") +{ + std::string luteProjectRoot = getLuteProjectRootAbsolute(); + std::string testDir = joinPaths(luteProjectRoot, "tests/src/staticrequires_luau_config"); + std::string entryPoint = joinPaths(testDir, "main.luau"); + + StaticRequireTracer tracer{getReporter()}; + tracer.trace(entryPoint); + + auto pairs = tracer.getStaticRequirePairs(); + + // main.luau requires @nested/example, should resolve via .config.luau alias + REQUIRE(pairs.size() == 2); + CHECK(pairs[0].first == "main.luau"); + + std::string absoluteExample = joinPaths(tracer.getLowestCommonRoot(), "dep/nested/example.luau"); + CHECK(tracer.containsAbsolute(absoluteExample)); + + // Verify .config.luau file was discovered (not .luaurc) + auto luauConfigFiles = tracer.getLuauConfigFiles(); + REQUIRE(luauConfigFiles.size() == 1); + CHECK(luauConfigFiles.find(".config.luau") != nullptr); + CHECK(luauConfigFiles.find(".luaurc") == nullptr); +} diff --git a/tests/src/staticrequires_luau_config/.config.luau b/tests/src/staticrequires_luau_config/.config.luau new file mode 100644 index 000000000..1fa2aef6e --- /dev/null +++ b/tests/src/staticrequires_luau_config/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + nested = "./dep/nested", + }, + }, +} diff --git a/tests/src/staticrequires_luau_config/dep/nested/example.luau b/tests/src/staticrequires_luau_config/dep/nested/example.luau new file mode 100644 index 000000000..b0d5a85f6 --- /dev/null +++ b/tests/src/staticrequires_luau_config/dep/nested/example.luau @@ -0,0 +1,3 @@ +local v = { file = "example.luau" } + +return v diff --git a/tests/src/staticrequires_luau_config/main.luau b/tests/src/staticrequires_luau_config/main.luau new file mode 100644 index 000000000..eae5ac640 --- /dev/null +++ b/tests/src/staticrequires_luau_config/main.luau @@ -0,0 +1,2 @@ +local example = require("@nested/example") +return example From adbadb53a5c4ee792415b2d99ecc51aa6d867dfc Mon Sep 17 00:00:00 2001 From: "NotoriousV.I.G" Date: Thu, 9 Apr 2026 11:59:31 -0700 Subject: [PATCH 468/642] Sets up a release job for Lute (#960) The job that will be used for automating releases and the job that will be used for nightlies share a lot of common actions. This PR separates the common chunk out into a re-usable workflow, called shared-build which is parameterized on the branch name. 1) Nightlies can be manually kicked off, but will continue to run with the current cadence. These will automatically run of the 'primary' branch. 2) Releases will be kicked off on release/v{Major}.{Minor}.x branches. In both cases the job will check out the branch and derive the tag name directly from the get_version.cmake command. If the branch is primary, we'll get 'nightly.YYYMMDD' appended. --- .github/workflows/ci.yml | 2 +- .github/workflows/nightly.yml | 59 +++++++++ .github/workflows/release-common.yml | 140 ++++++++++++++++++++ .github/workflows/release.yml | 188 ++------------------------- RELEASE.md | 37 ++++++ 5 files changed, 250 insertions(+), 176 deletions(-) create mode 100644 .github/workflows/nightly.yml create mode 100644 .github/workflows/release-common.yml create mode 100644 RELEASE.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cc87f0c7..0f0c6ae77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: ["primary"] + branches: ["primary", "release/**"] pull_request: branches: ["primary"] diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..6789b80ff --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,59 @@ +name: Nightly Release + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + changes: + runs-on: ubuntu-latest + outputs: + proceed: ${{ steps.check.outputs.proceed }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for changes since last nightly tag + id: check + run: | + PROCEED="true" + + if [[ "${{ github.event_name }}" == "schedule" ]]; then + echo "Schedule trigger: Checking changes..." + + LATEST_NIGHTLY_TAG=$(git tag --sort=-creatordate --list '*-nightly.*' | head -n 1) + + if [[ -n "$LATEST_NIGHTLY_TAG" ]]; then + echo "Latest nightly tag found: $LATEST_NIGHTLY_TAG" + LATEST_NIGHTLY_COMMIT=$(git rev-list -n 1 "$LATEST_NIGHTLY_TAG" 2>/dev/null) + CURRENT_HEAD_COMMIT=$(git rev-parse HEAD) + + echo "Commit for tag $LATEST_NIGHTLY_TAG: $LATEST_NIGHTLY_COMMIT" + echo "Current HEAD commit: $CURRENT_HEAD_COMMIT" + + if [[ "$LATEST_NIGHTLY_COMMIT" == "$CURRENT_HEAD_COMMIT" ]]; then + echo "No code changes detected since the last nightly release ($LATEST_NIGHTLY_TAG)." + PROCEED="false" + else + echo "Code changes detected since $LATEST_NIGHTLY_TAG. Proceeding with build." + fi + else + echo "No previous nightly tag found. Proceeding with build." + fi + else + echo "Manual trigger. Proceeding with build." + fi + + echo "proceed=$PROCEED" >> $GITHUB_OUTPUT + + release-common: + needs: changes + if: ${{ needs.changes.outputs.proceed == 'true' }} + uses: ./.github/workflows/shared-build.yml + with: + branch: primary + secrets: inherit diff --git a/.github/workflows/release-common.yml b/.github/workflows/release-common.yml new file mode 100644 index 000000000..db89bb34c --- /dev/null +++ b/.github/workflows/release-common.yml @@ -0,0 +1,140 @@ +name: release-common + +on: + workflow_call: + inputs: + branch: + description: "Branch to use for the release. 'primary' triggers a nightly build." + required: false + default: "primary" + type: string + +env: + IS_NIGHTLY: ${{ inputs.branch == 'primary' }} + +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Validate CI status + id: check + run: | + LAST_STATUS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/luau-lang/lute/actions/workflows/ci.yml/runs?branch=${{ inputs.branch }}&status=completed&per_page=1" \ + | jq -r '.workflow_runs[0].conclusion') + if [[ "$LAST_STATUS" != "success" ]]; then + echo "The latest CI run on '${{ inputs.branch }}' branch did not succeed. Aborting release." + exit 1 + fi + + build: + strategy: + matrix: + include: + - os: ubuntu-22.04 + artifact_name: lute-linux-x86_64 + options: --c-compiler clang --cxx-compiler clang++ + - os: ubuntu-22.04-arm + artifact_name: lute-linux-aarch64 + options: --c-compiler clang --cxx-compiler clang++ + use-bootstrap: true + - os: macos-latest + artifact_name: lute-macos-aarch64 + - os: windows-latest + artifact_name: lute-windows-x86_64 + fail-fast: false + + needs: check + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: Set nightly version suffix + if: ${{ env.IS_NIGHTLY == 'true' }} + run: echo "LUTE_VERSION_SUFFIX=nightly.$(date +%Y%m%d)" >> "$GITHUB_ENV" + + - name: Setup and Build Lute + id: build_lute + uses: ./.github/actions/setup-and-build + with: + target: Lute.CLI + config: release + options: ${{ matrix.options }} + token: ${{ secrets.GITHUB_TOKEN }} + use-bootstrap: ${{ matrix.use-bootstrap || 'false' }} + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + path: ${{ steps.build_lute.outputs.exe_path }} + name: ${{ matrix.artifact_name }} + + release: + needs: [check, build] + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + sparse-checkout: CMakeBuild/get_version.cmake + + - name: Set nightly version suffix + if: ${{ env.IS_NIGHTLY == 'true' }} + run: echo "LUTE_VERSION_SUFFIX=nightly.$(date +%Y%m%d)" >> "$GITHUB_ENV" + + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + path: artifacts + + - run: | + mkdir release + for dir in ./artifacts/*; do + base_name=$(basename "$dir") + chmod 755 "$dir"/* # Artifacts don't preserve permissions, so re-add them. + zip -rj release/"${base_name}.zip" "$dir"/* + done + + - name: Get project version + id: get_version + run: | + VERSION=$(cmake -P CMakeBuild/get_version.cmake 2>&1) + echo "Resolved version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Create release tag + id: tag_release + run: | + TAG="v${{ steps.get_version.outputs.version }}" + echo "Release tag: $TAG" + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Create Draft Release + uses: luau-lang/action-gh-release@v2 + with: + tag_name: ${{ steps.tag_release.outputs.tag }} + name: ${{ steps.tag_release.outputs.tag }} + draft: true + prerelease: ${{ env.IS_NIGHTLY == 'true' }} + generate_release_notes: ${{ env.IS_NIGHTLY == 'true' }} + files: release/*.zip + + - name: Wait for Tag Creation in GitHub + run: sleep 5 + + - name: Publish Release + run: gh release edit "${{ steps.tag_release.outputs.tag }}" --draft=false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26f09ef4a..0a1a25bc2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,188 +1,26 @@ -name: Build and Release +name: Release on: - schedule: - - cron: "0 0 * * *" workflow_dispatch: - inputs: - nightly: - description: "Trigger a nightly build" - required: false - default: false - type: boolean jobs: - check: + validate: runs-on: ubuntu-latest - outputs: - proceed: ${{ steps.check.outputs.proceed }} - nightly_tag: ${{ steps.set_nightly.outputs.nightly_tag }} - permissions: - contents: read steps: - - name: Validate CI status + - name: Validate branch format run: | - LAST_STATUS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ - "https://api.github.com/repos/luau-lang/lute/actions/workflows/ci.yml/runs?branch=primary&status=completed&per_page=1" \ - | jq -r '.workflow_runs[0].conclusion') + BRANCH="${{ github.ref_name }}" - if [[ "$LAST_STATUS" != "success" ]]; then - echo "The latest CI run on 'primary' branch did not succeed. Aborting release." + if [[ ! "$BRANCH" =~ ^release/v[0-9]+\.[0-9]+\.x$ ]]; then + echo "Invalid branch format: $BRANCH. Expected release/v{Major}.{Minor}.x" + echo "Dispatch this workflow on a release branch (e.g. release/v0.1.x)" exit 1 fi - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check for changes since last nightly tag - id: check - run: | - PROCEED="true" - - if [[ "${{ github.event_name }}" == "schedule" ]]; then - echo "Schedule trigger: Checking changes..." - - LATEST_NIGHTLY_TAG=$(git tag --sort=-creatordate --list '*-nightly.*' | head -n 1) - - if [[ -n "$LATEST_NIGHTLY_TAG" ]]; then - echo "Latest nightly tag found: $LATEST_NIGHTLY_TAG" - LATEST_NIGHTLY_COMMIT=$(git rev-list -n 1 "$LATEST_NIGHTLY_TAG" 2>/dev/null) - CURRENT_HEAD_COMMIT=$(git rev-parse HEAD) - - echo "Commit for tag $LATEST_NIGHTLY_TAG: $LATEST_NIGHTLY_COMMIT" - echo "Current HEAD commit: $CURRENT_HEAD_COMMIT" - - if [[ "$LATEST_NIGHTLY_COMMIT" == "$CURRENT_HEAD_COMMIT" ]]; then - echo "No code changes detected since the last nightly release ($LATEST_NIGHTLY_TAG)." - PROCEED="false" - else - echo "Code changes detected since $LATEST_NIGHTLY_TAG. Proceeding with build." - fi - else - echo "No previous nightly tag found. Proceeding with build." - fi - else - echo "Manual trigger ('${{ github.event_name }}'). Proceeding with build." - fi - - echo "proceed=$PROCEED" >> $GITHUB_OUTPUT - - - name: Set nightly version suffix - id: set_nightly - if: github.event_name == 'schedule' || github.event.inputs.nightly - run: | - DATE=$(date +%Y%m%d) - echo "nightly_tag=nightly.$DATE" >> "$GITHUB_OUTPUT" - - build: - strategy: - matrix: - include: - - os: ubuntu-22.04 - artifact_name: lute-linux-x86_64 - options: --c-compiler clang --cxx-compiler clang++ - - os: ubuntu-22.04-arm - artifact_name: lute-linux-aarch64 - options: --c-compiler clang --cxx-compiler clang++ - use-bootstrap: true - - os: macos-latest - artifact_name: lute-macos-aarch64 - - os: windows-latest - artifact_name: lute-windows-x86_64 - fail-fast: false - - needs: check - if: ${{ needs.check.outputs.proceed == 'true' }} - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - # Used for lute --version - - name: Set nightly version suffix - if: needs.check.outputs.nightly_tag - run: echo "LUTE_VERSION_SUFFIX=${{ needs.check.outputs.nightly_tag }}" >> "$GITHUB_ENV" - - - name: Setup and Build Lute - id: build_lute - uses: ./.github/actions/setup-and-build - with: - target: Lute.CLI - config: release - options: ${{ matrix.options }} - token: ${{ secrets.GITHUB_TOKEN }} - use-bootstrap: ${{ matrix.use-bootstrap || 'false' }} - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - path: ${{ steps.build_lute.outputs.exe_path }} - name: ${{ matrix.artifact_name }} - - release: - needs: [check, build] - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - sparse-checkout: CMakeBuild/get_version.cmake - - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - path: artifacts - - - run: | - mkdir release - for dir in ./artifacts/*; do - base_name=$(basename "$dir") - chmod 755 "$dir"/* # Artifacts don't preserve permissions, so re-add them. - zip -rj release/"${base_name}.zip" "$dir"/* - done - - - name: Get project version - id: get_version - run: | - VERSION=$(cmake -P CMakeBuild/get_version.cmake 2>&1) - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Create Nightly Tag - id: tag_step - if: needs.check.outputs.nightly_tag - run: | - echo "tag=-${{ needs.check.outputs.nightly_tag }}" >> $GITHUB_OUTPUT - - - name: Create Release Tag - id: tag_release - run: | - VERSION=${{ steps.get_version.outputs.version }} - NIGHTLY_TAG=${{ steps.tag_step.outputs.tag }} - RELEASE_TAG="$VERSION$NIGHTLY_TAG" - echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT - - - name: Create Draft Release - uses: luau-lang/action-gh-release@v2 - with: - tag_name: ${{ steps.tag_release.outputs.tag }} - name: ${{ steps.tag_release.outputs.tag }} - draft: true - prerelease: ${{ github.event.inputs.nightly || github.event_name == 'schedule' }} - generate_release_notes: true - files: release/*.zip - - - name: Wait for Tag Creation in GitHub - run: sleep 5 - - - name: Publish Release - run: gh release edit "${{ steps.tag_release.outputs.tag }}" --draft=false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release-common: + needs: validate + uses: ./.github/workflows/shared-build.yml + with: + branch: ${{ github.ref_name }} + secrets: inherit diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..340f361bb --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,37 @@ +# Release Process + +Lute uses branches with the format: `release/v{Major}.{Minor}.x` to store +changes associated with a given release. We tag commits on these branches with +the format `v{Major}.{Minor}.{Patch}` to indicate that a commit should be build +as that release version. These release branches exist so that we can continue to +support older versions of lute according to some currently unknown SLA, but +continue development on the primary branch. Release notes must be written +manually. + +## Working on release branches +As much as possible, we should avoid manually making changes to release +branches, and prefer to develop directly to primary. If changes need to be +applied to release branches (security vulnerabilities or hotfixes), we should +cherry-pick those in as appropriate, only applying changes manually as a last +resort. + +## Cutting a release +Cutting a release should be mostly automatic. Navigate to +github.com/luau-lang/lute > actions > release. For this action, select a release +branch from the dropdown to cut the release from - it should start with +`release/v{Major}.{Minor}.x`. + +This job will: +1) Checkout the branch `release/v{Major}.{Minor}.x`. +2) Ensure that this branch passes all status checks +3) Derive a tag in the format `v{Major}.{Minor}.{Patch}` +4) Build `lute` for a bunch of different platforms +5) Create a draft release with the artifacts built in 4) + +You should add patch notes for this release manually. + +## Nightly releases +Nightlies can be scheduled or triggered manually. Patch notes will be +automatically generated for nightlies. + + From 86e3dc1bf76693ea7e5de6b3631b6dc3a7ea0cba Mon Sep 17 00:00:00 2001 From: ariel Date: Thu, 9 Apr 2026 12:43:36 -0700 Subject: [PATCH 469/642] runtime: introduce `LuteLibrary` shared interface for runtime modules. (#959) There's a lot of inconsistency between the different runtime library definitions, and it's bothered me for a while. To try to make things more structured, this PR introduces a shared interface `LuteLibrary` and refactors each library namespace to instead be a `struct` that implements that `LuteLibrary` interface. The goal here is to make it more difficult to author a library that isn't structured how we would expect, and to bring all of the existing libraries into that uniform structure. --- lute/cli/src/climain.cpp | 2 +- lute/common/include/lute/library.h | 52 +++++++++++++++++ lute/crypto/include/lute/crypto.h | 45 +++------------ lute/crypto/src/crypto.cpp | 53 +++++++++++++---- lute/fs/include/lute/fs.h | 90 +++-------------------------- lute/fs/src/fs.cpp | 47 ++++++++++++--- lute/io/include/lute/io.h | 17 +++--- lute/io/src/io.cpp | 31 ++++++---- lute/luau/include/lute/luau.h | 36 ++---------- lute/luau/src/luau.cpp | 36 +++++++++--- lute/net/include/lute/net.h | 36 ++++++------ lute/net/src/client.cpp | 18 +++++- lute/net/src/net.cpp | 32 ++++++---- lute/net/src/server.cpp | 18 +++++- lute/process/include/lute/process.h | 31 +++------- lute/process/src/process.cpp | 74 ++++++++++++++---------- lute/system/include/lute/system.h | 39 +++---------- lute/system/src/system.cpp | 44 ++++++++++---- lute/task/include/lute/task.h | 26 +++------ lute/task/src/task.cpp | 35 +++++++---- lute/time/include/lute/time.h | 22 +++---- lute/time/src/time.cpp | 46 ++++++++------- lute/vm/include/lute/spawn.h | 9 +-- lute/vm/include/lute/vm.h | 17 +++--- lute/vm/src/spawn.cpp | 9 +-- lute/vm/src/vm.cpp | 26 ++++++--- 26 files changed, 463 insertions(+), 428 deletions(-) create mode 100644 lute/common/include/lute/library.h diff --git a/lute/cli/src/climain.cpp b/lute/cli/src/climain.cpp index 7f0707827..0aa1fc847 100644 --- a/lute/cli/src/climain.cpp +++ b/lute/cli/src/climain.cpp @@ -610,7 +610,7 @@ int handleCompileCommand(int argc, char** argv, int argOffset, LuteReporter& rep // Get current executable path std::string errorMsg; - std::optional exePath = process::getExecPath(&errorMsg); + std::optional exePath = Process::getExecPath(&errorMsg); if (!exePath) { reporter.formatError("Error: Failed to get executable path: %s", errorMsg.c_str()); diff --git a/lute/common/include/lute/library.h b/lute/common/include/lute/library.h new file mode 100644 index 000000000..6272902c1 --- /dev/null +++ b/lute/common/include/lute/library.h @@ -0,0 +1,52 @@ +#pragma once + +#include "lua.h" +#include "lualib.h" + +#include + +// A uniform interface for Lute libraries. +// +// Each `Derived` Lute library must provide: +// static constexpr const char kName[]; -- the Luau library name (e.g. "task") +// static int pushLibrary(lua_State* L); -- push the library table onto the stack +// static const luaL_Reg lib[]; -- function registration table +// static const char* const properties[]; -- non-function table keys (empty array if none) +template +struct LuteLibrary +{ + // Verifies that the type given as `Derived` satisfies the interface contract. + // + // This must be implemented as a function (not in the class scope) because `Derived` is + // incomplete when `LuteLibrary` is instantiated (CRTP constraint). + static void verifyInterface() + { + static_assert( + std::is_array_v && + std::is_same_v, const char>, + "LuteLibrary must declare: static constexpr const char kName[]" + ); + static_assert( + std::is_same_v, + "LuteLibrary must declare: static int pushLibrary(lua_State* L)" + ); + static_assert( + std::is_array_v && + std::is_same_v, const luaL_Reg>, + "LuteLibrary must declare: static const luaL_Reg lib[]" + ); + static_assert( + std::is_array_v && + std::is_same_v, const char* const>, + "LuteLibrary must declare: static const char* const properties[]" + ); + } + + static int openAsGlobal(lua_State* L) + { + verifyInterface(); + Derived::pushLibrary(L); + lua_setglobal(L, Derived::kName); + return 1; + } +}; diff --git a/lute/crypto/include/lute/crypto.h b/lute/crypto/include/lute/crypto.h index eda8835d1..c47607152 100644 --- a/lute/crypto/include/lute/crypto.h +++ b/lute/crypto/include/lute/crypto.h @@ -1,50 +1,19 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" -#include - // open the library as a standard global luau library int luaopen_crypto(lua_State* L); // open the library as a table on top of the stack int luteopen_crypto(lua_State* L); -namespace crypto +struct Crypto : LuteLibrary { - -static const char kHashProperty[] = "hash"; -static const char kSecretboxProperty[] = "secretbox"; -static const char kPasswordProperty[] = "password"; - -static const char kDigestName[] = "digest"; -int lua_digest(lua_State* L); - -static const char kCiphertextField[] = "ciphertext"; -static const char kKeyField[] = "key"; -static const char kNonceField[] = "nonce"; - -static const char kKeygenName[] = "keygen"; -int lua_secretbox_keygen(lua_State* L); - -static const char kSealName[] = "seal"; -int lua_secretbox_seal(lua_State* L); - -static const char kOpenName[] = "open"; -int lua_secretbox_open(lua_State* L); - -static const char kPasswordHashName[] = "hash"; -int lua_pwhash(lua_State* L); - -static const char kVerifyPasswordHashName[] = "verify"; -int lua_pwhash_verify(lua_State* L); - -static const luaL_Reg lib[] = {{kDigestName, lua_digest}, {nullptr, nullptr}}; - -static const std::string properties[] = { - kHashProperty, - kSecretboxProperty, - kPasswordProperty, + static constexpr const char kName[] = "crypto"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -} // namespace crypto diff --git a/lute/crypto/src/crypto.cpp b/lute/crypto/src/crypto.cpp index 3ed774070..c82114f1c 100644 --- a/lute/crypto/src/crypto.cpp +++ b/lute/crypto/src/crypto.cpp @@ -15,6 +15,22 @@ namespace crypto { +static const char kHashProperty[] = "hash"; +static const char kSecretboxProperty[] = "secretbox"; +static const char kPasswordProperty[] = "password"; + +static const char kDigestName[] = "digest"; + +static const char kCiphertextField[] = "ciphertext"; +static const char kKeyField[] = "key"; +static const char kNonceField[] = "nonce"; + +static const char kKeygenName[] = "keygen"; +static const char kSealName[] = "seal"; +static const char kOpenName[] = "open"; +static const char kPasswordHashName[] = "hash"; +static const char kVerifyPasswordHashName[] = "verify"; + struct HashFunction { std::string name; @@ -107,7 +123,7 @@ int lua_secretbox_keygen(lua_State* L) uint8_t* key = static_cast(lua_newbuffer(L, crypto_secretbox_keybytes())); crypto_secretbox_keygen(key); - + return 1; } @@ -144,7 +160,7 @@ int lua_secretbox_seal(lua_State* L) crypto_secretbox_keygen(key); lua_setfield(L, -2, kKeyField); } - + // nonce uint8_t* nonce = static_cast(lua_newbuffer(L, crypto_secretbox_noncebytes())); randombytes_buf(nonce, crypto_secretbox_noncebytes()); @@ -268,19 +284,22 @@ int makePasswordHashLibrary(lua_State* L) } // namespace crypto -int luaopen_crypto(lua_State* L) -{ - luteopen_crypto(L); - lua_setglobal(L, "crypto"); +const char* const Crypto::properties[] = { + crypto::kHashProperty, + crypto::kSecretboxProperty, + crypto::kPasswordProperty, +}; - return 1; -} +const luaL_Reg Crypto::lib[] = { + {crypto::kDigestName, crypto::lua_digest}, + {nullptr, nullptr}, +}; -int luteopen_crypto(lua_State* L) +int Crypto::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(crypto::lib) + std::size(crypto::properties)); + lua_createtable(L, 0, std::size(Crypto::lib) + std::size(Crypto::properties)); - for (auto& [name, func] : crypto::lib) + for (auto& [name, func] : Crypto::lib) { if (!name || !func) break; @@ -298,7 +317,17 @@ int luteopen_crypto(lua_State* L) crypto::makePasswordHashLibrary(L); lua_setfield(L, -2, crypto::kPasswordProperty); - lua_setreadonly(L, -1, true); + lua_setreadonly(L, -1, 1); return 1; } + +int luaopen_crypto(lua_State* L) +{ + return Crypto::openAsGlobal(L); +} + +int luteopen_crypto(lua_State* L) +{ + return Crypto::pushLibrary(L); +} diff --git a/lute/fs/include/lute/fs.h b/lute/fs/include/lute/fs.h index b66559a10..36eba458f 100644 --- a/lute/fs/include/lute/fs.h +++ b/lute/fs/include/lute/fs.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" @@ -8,88 +10,10 @@ int luaopen_fs(lua_State* L); // open the library as a table on top of the stack int luteopen_fs(lua_State* L); -namespace fs +struct FS : LuteLibrary { - -// TODO: add the ability to open as bytes -/* Takes path: string, a mode: 'r|a|w|x|+' (defaulting to r when omitted) - Returns a table representing a handle to the file the state of a file {fd : number, error : ...} - */ -int open(lua_State* L); - -/* Reads a file into a string. Takes a file handle obtained from openfile */ -int read(lua_State* L); - -/* Writes a string to a file without closing it*/ -int write(lua_State* L); - -/* takes a file handle into a string and then closes it */ -int close(lua_State* L); - -/* reads a whole file into a string and then closes it */ -int readfiletostring(lua_State* L); -/* writes a st */ -int writestringtofile(lua_State* L); - -/* Reads a file without blocking */ -int readasync(lua_State* L); - -/* Removes a file */ -int remove(lua_State* L); - -/* Creates a folder */ -int mkdir(lua_State* L); - -/* Removes a directory */ -int rmdir(lua_State* L); - -/* Gets the metadata of a file */ -int stat(lua_State* L); - -/* Checks if a file exists */ -int exists(lua_State* L); - -/* Copies a file to another path */ -int copy(lua_State* L); - -/* Creates a link to a file */ -int link(lua_State* L); - -/* Creates a symlink to a file */ -int symlink(lua_State* L); - -/* Gets the type of a file entry */ -int type(lua_State* L); - -/* Sets up a filesystem watch event */ -int fs_watch(lua_State* L); - -/* Lists the contents of a directory */ -int listdir(lua_State* L); - -static const luaL_Reg lib[] = { - /* Manual control apis - you are responsible for calling close / open*/ - {"open", open}, - {"read", read}, - {"write", write}, - {"close", close}, - - {"remove", remove}, - - {"stat", stat}, - {"exists", exists}, - {"type", type}, - - {"watch", fs_watch}, - {"link", link}, - {"symlink", symlink}, - {"copy", copy}, - - {"mkdir", mkdir}, - {"listdir", listdir}, - {"rmdir", rmdir}, - - {NULL, NULL}, + static constexpr const char kName[] = "fs"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -} // namespace fs diff --git a/lute/fs/src/fs.cpp b/lute/fs/src/fs.cpp index 3de707743..03ead9142 100644 --- a/lute/fs/src/fs.cpp +++ b/lute/fs/src/fs.cpp @@ -408,18 +408,37 @@ static void initalizeFS(lua_State* L) lua_setuserdatametatable(L, kWatchHandleTag); } -int luaopen_fs(lua_State* L) -{ - luaL_register(L, "fs", fs::lib); - initalizeFS(L); - return 1; -} +const char* const FS::properties[] = {nullptr}; -int luteopen_fs(lua_State* L) +const luaL_Reg FS::lib[] = { + {"open", fs::open}, + {"read", fs::read}, + {"write", fs::write}, + {"close", fs::close}, + + {"remove", fs::remove}, + + {"stat", fs::stat}, + {"exists", fs::exists}, + {"type", fs::type}, + + {"watch", fs::fs_watch}, + {"link", fs::link}, + {"symlink", fs::symlink}, + {"copy", fs::copy}, + + {"mkdir", fs::mkdir}, + {"listdir", fs::listdir}, + {"rmdir", fs::rmdir}, + + {nullptr, nullptr}, +}; + +int FS::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(fs::lib)); + lua_createtable(L, 0, std::size(FS::lib)); - for (auto& [name, func] : fs::lib) + for (auto& [name, func] : FS::lib) { if (!name || !func) break; @@ -434,3 +453,13 @@ int luteopen_fs(lua_State* L) return 1; } + +int luaopen_fs(lua_State* L) +{ + return FS::openAsGlobal(L); +} + +int luteopen_fs(lua_State* L) +{ + return FS::pushLibrary(L); +} diff --git a/lute/io/include/lute/io.h b/lute/io/include/lute/io.h index 7626d9565..a7816826f 100644 --- a/lute/io/include/lute/io.h +++ b/lute/io/include/lute/io.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" @@ -8,15 +10,10 @@ int luaopen_io(lua_State* L); // open the library as a table on top of the stack int luteopen_io(lua_State* L); -namespace io +struct IO : LuteLibrary { - -int read(lua_State* L); - -static const luaL_Reg lib[] = { - {"read", read}, - - {nullptr, nullptr} + static constexpr const char kName[] = "io"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -} // namespace io diff --git a/lute/io/src/io.cpp b/lute/io/src/io.cpp index 1cc627998..4b78f6681 100644 --- a/lute/io/src/io.cpp +++ b/lute/io/src/io.cpp @@ -12,7 +12,7 @@ #include -namespace io +namespace { struct IOHandle @@ -113,19 +113,20 @@ int read(lua_State* L) return lua_yield(L, 0); } -} // namespace io +} // anonymous namespace -int luaopen_io(lua_State* L) -{ - luaL_register(L, "io", io::lib); - return 1; -} +const char* const IO::properties[] = {nullptr}; -int luteopen_io(lua_State* L) +const luaL_Reg IO::lib[] = { + {"read", read}, + {nullptr, nullptr}, +}; + +int IO::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(io::lib)); + lua_createtable(L, 0, std::size(IO::lib)); - for (auto& [name, func] : io::lib) + for (auto& [name, func] : IO::lib) { if (!name || !func) break; @@ -138,3 +139,13 @@ int luteopen_io(lua_State* L) return 1; } + +int luaopen_io(lua_State* L) +{ + return IO::openAsGlobal(L); +} + +int luteopen_io(lua_State* L) +{ + return IO::pushLibrary(L); +} diff --git a/lute/luau/include/lute/luau.h b/lute/luau/include/lute/luau.h index 557d0ae28..5582374cb 100644 --- a/lute/luau/include/lute/luau.h +++ b/lute/luau/include/lute/luau.h @@ -1,5 +1,6 @@ #pragma once +#include "lute/library.h" #include "lute/resolvemodule.h" #include "lua.h" @@ -10,35 +11,10 @@ int luaopen_luau(lua_State* L); // open the library as a table on top of the stack int luteopen_luau(lua_State* L); -namespace luau +struct LuauLib : LuteLibrary { - -static const char kSpanType[] = "span"; -static const char kCompileResultType[] = "CompileResult"; -static const char kSpanCreateName[] = "span.create"; - -int luau_parse(lua_State* L); - -int luau_parseexpr(lua_State* L); - -int compile_luau(lua_State* L); - -int load_luau(lua_State* L); - -int typeofModule_luau(lua_State* L); - -static const luaL_Reg lib[] = { - {"parse", luau_parse}, - {"parseExpr", luau_parseexpr}, - {"compile", compile_luau}, - {"load", load_luau}, - {"resolveModule", resolveModule_luau}, - {"typeofModule", typeofModule_luau}, - {nullptr, nullptr}, + static constexpr const char kName[] = "luau"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -static const std::string properties[] = { - kSpanType, -}; - -} // namespace luau diff --git a/lute/luau/src/luau.cpp b/lute/luau/src/luau.cpp index 4c693ebd8..0896b3a93 100644 --- a/lute/luau/src/luau.cpp +++ b/lute/luau/src/luau.cpp @@ -29,6 +29,10 @@ namespace luau { +static constexpr const char kSpanType[] = "span"; +static constexpr const char kCompileResultType[] = "CompileResult"; +static constexpr const char kSpanCreateName[] = "span.create"; + // Recursively freezes the table at the top of the stack and any descendant tables. // The table must be at the top of the stack when called. static void deepFreeze(lua_State* L) @@ -3087,22 +3091,26 @@ static int initLuauLibrary(lua_State* L) } // namespace luau -int luaopen_luau(lua_State* L) -{ - luaL_register(L, "luau", luau::lib); +const char* const LuauLib::properties[] = {luau::kSpanType}; - return luau::initLuauLibrary(L); -} +const luaL_Reg LuauLib::lib[] = { + {"parse", luau::luau_parse}, + {"parseExpr", luau::luau_parseexpr}, + {"compile", luau::compile_luau}, + {"load", luau::load_luau}, + {"resolveModule", resolveModule_luau}, + {"typeofModule", luau::typeofModule_luau}, + {nullptr, nullptr}, +}; -int luteopen_luau(lua_State* L) +int LuauLib::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(luau::lib) + std::size(luau::properties)); + lua_createtable(L, 0, std::size(LuauLib::lib) + std::size(LuauLib::properties)); - // span library luau::makeSpanLibrary(L); lua_setfield(L, -2, luau::kSpanType); - for (auto& [name, func] : luau::lib) + for (auto& [name, func] : LuauLib::lib) { if (!name || !func) break; @@ -3115,3 +3123,13 @@ int luteopen_luau(lua_State* L) return luau::initLuauLibrary(L); } + +int luaopen_luau(lua_State* L) +{ + return LuauLib::openAsGlobal(L); +} + +int luteopen_luau(lua_State* L) +{ + return LuauLib::pushLibrary(L); +} diff --git a/lute/net/include/lute/net.h b/lute/net/include/lute/net.h index a6e62c827..a001a83db 100644 --- a/lute/net/include/lute/net.h +++ b/lute/net/include/lute/net.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" @@ -11,26 +13,26 @@ int luteopen_net(lua_State* L); int luteopen_net_client(lua_State* L); int luteopen_net_server(lua_State* L); -namespace net::client +struct NetClient : LuteLibrary { - -int request(lua_State* L); - -static const luaL_Reg lib[] = { - {"request", request}, - {nullptr, nullptr}, + static constexpr const char kName[] = "net.client"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; -} // namespace net::client - -namespace net::server +struct NetServer : LuteLibrary { - -int serve(lua_State* L); - -static const luaL_Reg lib[] = { - {"serve", serve}, - {nullptr, nullptr}, + static constexpr const char kName[] = "net.server"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; -} // namespace net::server +struct Net : LuteLibrary +{ + static constexpr const char kName[] = "net"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; +}; diff --git a/lute/net/src/client.cpp b/lute/net/src/client.cpp index 6d0784c9f..71f9f6b5d 100644 --- a/lute/net/src/client.cpp +++ b/lute/net/src/client.cpp @@ -242,13 +242,20 @@ int request(lua_State* L) } // namespace net::client -int luteopen_net_client(lua_State* L) +const char* const NetClient::properties[] = {nullptr}; + +const luaL_Reg NetClient::lib[] = { + {"request", net::client::request}, + {nullptr, nullptr}, +}; + +int NetClient::pushLibrary(lua_State* L) { globalCurlInit(); - lua_createtable(L, 0, 1); + lua_createtable(L, 0, std::size(NetClient::lib)); - for (auto& [name, func] : net::client::lib) + for (auto& [name, func] : NetClient::lib) { if (!name || !func) break; @@ -261,3 +268,8 @@ int luteopen_net_client(lua_State* L) return 1; } + +int luteopen_net_client(lua_State* L) +{ + return NetClient::pushLibrary(L); +} diff --git a/lute/net/src/net.cpp b/lute/net/src/net.cpp index 917523a9e..0a1147b10 100644 --- a/lute/net/src/net.cpp +++ b/lute/net/src/net.cpp @@ -2,25 +2,37 @@ #include "lua.h" -int luaopen_net(lua_State* L) -{ - luteopen_net(L); - lua_setglobal(L, "net"); +#include - return 1; -} +const luaL_Reg Net::lib[] = { + {nullptr, nullptr}, +}; -int luteopen_net(lua_State* L) +const char* const Net::properties[] = {"client", "server"}; + +int Net::pushLibrary(lua_State* L) { - lua_createtable(L, 0, 2); + lua_createtable(L, 0, std::size(Net::properties)); - luteopen_net_client(L); + NetClient::verifyInterface(); + NetClient::pushLibrary(L); lua_setfield(L, -2, "client"); - luteopen_net_server(L); + NetServer::verifyInterface(); + NetServer::pushLibrary(L); lua_setfield(L, -2, "server"); lua_setreadonly(L, -1, 1); return 1; } + +int luaopen_net(lua_State* L) +{ + return Net::openAsGlobal(L); +} + +int luteopen_net(lua_State* L) +{ + return Net::pushLibrary(L); +} diff --git a/lute/net/src/server.cpp b/lute/net/src/server.cpp index a170008c5..f07daaf1b 100644 --- a/lute/net/src/server.cpp +++ b/lute/net/src/server.cpp @@ -491,11 +491,18 @@ int serve(lua_State* L) } // namespace net::server -int luteopen_net_server(lua_State* L) +const char* const NetServer::properties[] = {nullptr}; + +const luaL_Reg NetServer::lib[] = { + {"serve", net::server::serve}, + {nullptr, nullptr}, +}; + +int NetServer::pushLibrary(lua_State* L) { - lua_createtable(L, 0, 1); + lua_createtable(L, 0, std::size(NetServer::lib)); - for (auto& [name, func] : net::server::lib) + for (auto& [name, func] : NetServer::lib) { if (!name || !func) break; @@ -508,3 +515,8 @@ int luteopen_net_server(lua_State* L) return 1; } + +int luteopen_net_server(lua_State* L) +{ + return NetServer::pushLibrary(L); +} diff --git a/lute/process/include/lute/process.h b/lute/process/include/lute/process.h index 404c9dc55..96413c2f5 100644 --- a/lute/process/include/lute/process.h +++ b/lute/process/include/lute/process.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" @@ -11,29 +13,12 @@ int luaopen_process(lua_State* L); // open the library as a table on top of the stack int luteopen_process(lua_State* L); -namespace process +struct Process : LuteLibrary { + static constexpr const char kName[] = "process"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; -int run(lua_State* L); -int system(lua_State* L); - -int homedir(lua_State* L); -int cwd(lua_State* L); - -int exitFunc(lua_State* L); - -std::optional getExecPath(std::string* error); -int execPath(lua_State* L); - -static const luaL_Reg lib[] = { - {"run", run}, - {"system", system}, - {"homedir", homedir}, - {"cwd", cwd}, - {"exit", exitFunc}, - {"execPath", execPath}, - - {nullptr, nullptr} + static std::optional getExecPath(std::string* error); }; - -} // namespace process diff --git a/lute/process/src/process.cpp b/lute/process/src/process.cpp index b49d24708..30bf9d806 100644 --- a/lute/process/src/process.cpp +++ b/lute/process/src/process.cpp @@ -494,32 +494,10 @@ int cwd(lua_State* L) return 1; }; -std::optional getExecPath(std::string* error) -{ - // Executable path is not expected to change during process lifetime, so we - // can safely cache it after the first retrieval. - static std::optional cachedPath = std::nullopt; - if (cachedPath) - return *cachedPath; - - char buf[LUTE_PATH_MAX]; - size_t len = sizeof(buf); - - if (int status = uv_exepath(buf, &len); status < 0) - { - if (error) - *error = uv_strerror(status); - return std::nullopt; - } - - cachedPath = std::string(buf, len); - return *cachedPath; -} - int execPath(lua_State* L) { std::string error; - std::optional execPath = getExecPath(&error); + std::optional execPath = Process::getExecPath(&error); if (!execPath) luaL_error(L, "Failed to get executable path: %s", error.c_str()); @@ -653,17 +631,45 @@ static int envIter(lua_State* L) static const luaL_Reg processEnvMeta[] = {{"__index", process::envIndex}, {"__newindex", process::envNewindex}, {"__iter", process::envIter}, {nullptr, nullptr}}; -int luaopen_process(lua_State* L) +std::optional Process::getExecPath(std::string* error) { - luaL_register(L, "process", process::lib); - return 1; + // Executable path is not expected to change during process lifetime, so we + // can safely cache it after the first retrieval. + static std::optional cachedPath = std::nullopt; + if (cachedPath) + return *cachedPath; + + char buf[LUTE_PATH_MAX]; + size_t len = sizeof(buf); + + if (int status = uv_exepath(buf, &len); status < 0) + { + if (error) + *error = uv_strerror(status); + return std::nullopt; + } + + cachedPath = std::string(buf, len); + return *cachedPath; } -int luteopen_process(lua_State* L) +const char* const Process::properties[] = {"env", "args"}; + +const luaL_Reg Process::lib[] = { + {"run", process::run}, + {"system", process::system}, + {"homedir", process::homedir}, + {"cwd", process::cwd}, + {"exit", process::exitFunc}, + {"execPath", process::execPath}, + {nullptr, nullptr}, +}; + +int Process::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(process::lib)); + lua_createtable(L, 0, std::size(Process::lib) + std::size(Process::properties)); - for (auto& [name, func] : process::lib) + for (auto& [name, func] : Process::lib) { if (!name || !func) break; @@ -700,3 +706,13 @@ int luteopen_process(lua_State* L) return 1; } + +int luaopen_process(lua_State* L) +{ + return Process::openAsGlobal(L); +} + +int luteopen_process(lua_State* L) +{ + return Process::pushLibrary(L); +} diff --git a/lute/system/include/lute/system.h b/lute/system/include/lute/system.h index 4de34df32..b022b30f8 100644 --- a/lute/system/include/lute/system.h +++ b/lute/system/include/lute/system.h @@ -1,44 +1,19 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" -#include - // open the library as a standard global luau library int luaopen_system(lua_State* L); // open the library as a table on top of the stack int luteopen_system(lua_State* L); -namespace libsystem +struct System : LuteLibrary { - -static const char kArchitectureProperty[] = "arch"; -static const char kOperatingSystemProperty[] = "os"; - -int lua_cpus(lua_State* L); -int lua_threadcount(lua_State* L); -int lua_freememory(lua_State* L); -int lua_totalmemory(lua_State* L); -int lua_hostname(lua_State* L); -int lua_uptime(lua_State* L); -int lua_tmpdir(lua_State* L); - -static const luaL_Reg lib[] = { - {"cpus", lua_cpus}, - {"threadCount", lua_threadcount}, - {"freeMemory", lua_freememory}, - {"totalMemory", lua_totalmemory}, - {"hostName", lua_hostname}, - {"uptime", lua_uptime}, - {"tmpdir", lua_tmpdir}, - - {nullptr, nullptr} -}; - -static const std::string properties[] = { - kArchitectureProperty, - kOperatingSystemProperty, + static constexpr const char kName[] = "system"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -} // namespace libsystem diff --git a/lute/system/src/system.cpp b/lute/system/src/system.cpp index 5d6277ae5..6581fe61d 100644 --- a/lute/system/src/system.cpp +++ b/lute/system/src/system.cpp @@ -15,6 +15,10 @@ namespace libsystem { + +static const char kArchitectureProperty[] = "arch"; +static const char kOperatingSystemProperty[] = "os"; + int lua_cpus(lua_State* L) { int count = uv_available_parallelism(); @@ -128,19 +132,27 @@ int lua_tmpdir(lua_State* L) } } // namespace libsystem -int luaopen_system(lua_State* L) -{ - luteopen_system(L); - lua_setglobal(L, "system"); - - return 1; -} - -int luteopen_system(lua_State* L) +const char* const System::properties[] = { + libsystem::kArchitectureProperty, + libsystem::kOperatingSystemProperty, +}; + +const luaL_Reg System::lib[] = { + {"cpus", libsystem::lua_cpus}, + {"threadCount", libsystem::lua_threadcount}, + {"freeMemory", libsystem::lua_freememory}, + {"totalMemory", libsystem::lua_totalmemory}, + {"hostName", libsystem::lua_hostname}, + {"uptime", libsystem::lua_uptime}, + {"tmpdir", libsystem::lua_tmpdir}, + {nullptr, nullptr}, +}; + +int System::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(libsystem::lib) + std::size(libsystem::properties)); + lua_createtable(L, 0, std::size(System::lib) + std::size(System::properties)); - for (auto& [name, func] : libsystem::lib) + for (auto& [name, func] : System::lib) { if (!name || !func) break; @@ -163,3 +175,13 @@ int luteopen_system(lua_State* L) return 1; } + +int luaopen_system(lua_State* L) +{ + return System::openAsGlobal(L); +} + +int luteopen_system(lua_State* L) +{ + return System::pushLibrary(L); +} diff --git a/lute/task/include/lute/task.h b/lute/task/include/lute/task.h index 38227b198..65646d46a 100644 --- a/lute/task/include/lute/task.h +++ b/lute/task/include/lute/task.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" @@ -8,24 +10,10 @@ int luaopen_task(lua_State* L); // open the library as a table on top of the stack int luteopen_task(lua_State* L); -namespace task +struct Task : LuteLibrary { - -int lua_defer(lua_State* L); -int lua_deferSelf(lua_State* L); -int lua_wait(lua_State* L); -int lua_spawn(lua_State* L); -int lute_resume(lua_State* L); -int lua_delay(lua_State* L); - -static const luaL_Reg lib[] = { - {"spawn", lua_spawn}, - {"defer", lua_defer}, - {"resume", lute_resume}, - {"deferSelf", lua_deferSelf}, - {"wait", lua_wait}, - {"delay", lua_delay}, - {nullptr, nullptr}, + static constexpr const char kName[] = "task"; + static int pushLibrary(lua_State* L); + static const luaL_Reg lib[]; + static const char* const properties[]; }; - -} // namespace task diff --git a/lute/task/src/task.cpp b/lute/task/src/task.cpp index 5f3d950c5..e1c5afe8b 100644 --- a/lute/task/src/task.cpp +++ b/lute/task/src/task.cpp @@ -89,7 +89,7 @@ static void yieldLuaStateFor(lua_State* L, uint64_t milliseconds, bool putDeltaT ); } -namespace task +namespace { struct LuaThread @@ -309,20 +309,25 @@ int lua_wait(lua_State* L) return lua_yield(L, 0); } -} // namespace task +} // anonymous namespace -int luaopen_task(lua_State* L) -{ - luaL_register(L, "task", task::lib); +const char* const Task::properties[] = {nullptr}; - return 1; -} +const luaL_Reg Task::lib[] = { + {"spawn", lua_spawn}, + {"defer", lua_defer}, + {"resume", lute_resume}, + {"deferSelf", lua_deferSelf}, + {"wait", lua_wait}, + {"delay", lua_delay}, + {nullptr, nullptr}, +}; -int luteopen_task(lua_State* L) +int Task::pushLibrary(lua_State* L) { - lua_createtable(L, 0, std::size(task::lib)); + lua_createtable(L, 0, std::size(Task::lib)); - for (auto& [name, func] : task::lib) + for (auto& [name, func] : Task::lib) { if (!name || !func) break; @@ -335,3 +340,13 @@ int luteopen_task(lua_State* L) return 1; } + +int luaopen_task(lua_State* L) +{ + return Task::openAsGlobal(L); +} + +int luteopen_task(lua_State* L) +{ + return Task::pushLibrary(L); +} diff --git a/lute/time/include/lute/time.h b/lute/time/include/lute/time.h index 6050995de..499175862 100644 --- a/lute/time/include/lute/time.h +++ b/lute/time/include/lute/time.h @@ -1,5 +1,7 @@ #pragma once +#include "lute/library.h" + #include "lua.h" #include "lualib.h" @@ -51,20 +53,10 @@ static const luaL_Reg lib[] = { } // namespace duration -namespace libtime +struct Time : LuteLibrary