diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index fb952f5..6e9e57f 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -1433,7 +1433,7 @@ private: const char* checkStringFormat(const char* data, size_t size) { const char* flags = "-+ #0"; - const char* options = "cdiouxXeEfgGqs"; + const char* options = "cdiouxXeEfgGqs*"; for (size_t i = 0; i < size; ++i) { diff --git a/Analysis/src/TypeVar.cpp b/Analysis/src/TypeVar.cpp index ebaf590..51517db 100644 --- a/Analysis/src/TypeVar.cpp +++ b/Analysis/src/TypeVar.cpp @@ -1058,7 +1058,7 @@ ConstrainedTypeVarIterator end(const ConstrainedTypeVar* ctv) static std::vector parseFormatString(TypeChecker& typechecker, const char* data, size_t size) { - const char* options = "cdiouxXeEfgGqs"; + const char* options = "cdiouxXeEfgGqs*"; std::vector result; @@ -1072,7 +1072,7 @@ static std::vector parseFormatString(TypeChecker& typechecker, const cha continue; // we just ignore all characters (including flags/precision) up until first alphabetic character - while (i < size && !(data[i] > 0 && isalpha(data[i]))) + while (i < size && !(data[i] > 0 && (isalpha(data[i]) || data[i] == '*'))) i++; if (i == size) @@ -1080,6 +1080,8 @@ static std::vector parseFormatString(TypeChecker& typechecker, const cha if (data[i] == 'q' || data[i] == 's') result.push_back(typechecker.stringType); + else if (data[i] == '*') + result.push_back(typechecker.unknownType); else if (strchr(options, data[i])) result.push_back(typechecker.numberType); else diff --git a/CLI/Repl.cpp b/CLI/Repl.cpp index 4136073..13033bb 100644 --- a/CLI/Repl.cpp +++ b/CLI/Repl.cpp @@ -20,6 +20,9 @@ #ifdef _WIN32 #include #include + +#define WIN32_LEAN_AND_MEAN +#include #endif #ifdef CALLGRIND @@ -27,6 +30,7 @@ #endif #include +#include LUAU_FASTFLAG(DebugLuauTimeTracing) @@ -47,6 +51,35 @@ enum class CompileFormat constexpr int MaxTraversalLimit = 50; +// Ctrl-C handling +static void sigintCallback(lua_State* L, int gc) +{ + if (gc >= 0) + return; + + lua_callbacks(L)->interrupt = NULL; + + lua_rawcheckstack(L, 1); // reserve space for error string + luaL_error(L, "Execution interrupted"); +} + +static lua_State* replState = NULL; + +#ifdef _WIN32 +BOOL WINAPI sigintHandler(DWORD signal) +{ + if (signal == CTRL_C_EVENT && replState) + lua_callbacks(replState)->interrupt = &sigintCallback; + return TRUE; +} +#else +static void sigintHandler(int signum) +{ + if (signum == SIGINT && replState) + lua_callbacks(replState)->interrupt = &sigintCallback; +} +#endif + struct GlobalOptions { int optimizationLevel = 1; @@ -535,6 +568,15 @@ static void runRepl() lua_State* L = globalState.get(); setupState(L); + + // setup Ctrl+C handling + replState = L; +#ifdef _WIN32 + SetConsoleCtrlHandler(sigintHandler, TRUE); +#else + signal(SIGINT, sigintHandler); +#endif + luaL_sandboxthread(L); runReplImpl(L); } diff --git a/VM/src/lstrlib.cpp b/VM/src/lstrlib.cpp index b0cd4dc..98bbcd4 100644 --- a/VM/src/lstrlib.cpp +++ b/VM/src/lstrlib.cpp @@ -8,6 +8,8 @@ #include #include +LUAU_FASTFLAGVARIABLE(LuauTostringFormatSpecifier, false); + /* macro to `unsign' a character */ #define uchar(c) ((unsigned char)(c)) @@ -1032,6 +1034,26 @@ static int str_format(lua_State* L) break; } } + case '*': + { + if (!FFlag::LuauTostringFormatSpecifier) + { + luaL_error(L, "invalid option '%%*' to 'format'"); + break; + } + + if (formatItemSize != 1) + { + luaL_error(L, "'%%*' does not take a form"); + } + + size_t length; + const char* string = luaL_tolstring(L, arg, &length); + + luaL_addlstring(&b, string, length); + + continue; /* skip the `addsize' at the end */ + } default: { /* also treat cases `pnLlh' */ luaL_error(L, "invalid option '%%%c' to 'format'", *(strfrmt - 1)); diff --git a/rfcs/STATUS.md b/rfcs/STATUS.md index 23a1be8..d2fe86f 100644 --- a/rfcs/STATUS.md +++ b/rfcs/STATUS.md @@ -26,15 +26,3 @@ This document tracks unimplemented RFCs. [RFC: Lower bounds calculation](https://github.com/Roblox/luau/blob/master/rfcs/lower-bounds-calculation.md) **Status**: Implemented but not fully rolled out yet. - -## never and unknown types - -[RFC: never and unknown types](https://github.com/Roblox/luau/blob/master/rfcs/never-and-unknown-types.md) - -**Status**: Needs implementation - -## __len metamethod for tables and rawlen function - -[RFC: Support __len metamethod for tables and rawlen function](https://github.com/Roblox/luau/blob/master/rfcs/len-metamethod-rawlen.md) - -**Status**: Needs implementation diff --git a/rfcs/len-metamethod-rawlen.md b/rfcs/len-metamethod-rawlen.md index 45284b7..60278dd 100644 --- a/rfcs/len-metamethod-rawlen.md +++ b/rfcs/len-metamethod-rawlen.md @@ -1,5 +1,7 @@ # Support `__len` metamethod for tables and `rawlen` function +**Status**: Implemented + ## Summary `__len` metamethod will be called by `#` operator on tables, matching Lua 5.2 diff --git a/rfcs/never-and-unknown-types.md b/rfcs/never-and-unknown-types.md index d996afc..5ad216e 100644 --- a/rfcs/never-and-unknown-types.md +++ b/rfcs/never-and-unknown-types.md @@ -1,5 +1,7 @@ # never and unknown types +**Status**: Implemented + ## Summary Add `unknown` and `never` types that are inhabited by everything and nothing respectively. diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 1339fa0..5758f86 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -291,6 +291,8 @@ TEST_CASE("Clear") TEST_CASE("Strings") { + ScopedFastFlag sff{"LuauTostringFormatSpecifier", true}; + runConformance("strings.lua"); } diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 0c87802..10da0ef 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -556,6 +556,29 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_correctly_ordered_types") CHECK_EQ(tm->givenType, typeChecker.numberType); } +TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_tostring_specifier") +{ + CheckResult result = check(R"( + --!strict + string.format("%* %* %* %*", "string", 1, true, function() end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_tostring_specifier_type_constraint") +{ + CheckResult result = check(R"( + local function f(x): string + local _ = string.format("%*", x) + return x + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("(string) -> string", toString(requireType("f"))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "xpcall") { CheckResult result = check(R"( diff --git a/tests/conformance/strings.lua b/tests/conformance/strings.lua index 98a5721..c87cf15 100644 --- a/tests/conformance/strings.lua +++ b/tests/conformance/strings.lua @@ -130,6 +130,26 @@ assert(string.format('"-%20s.20s"', string.rep("%", 2000)) == -- longest number that can be formated assert(string.len(string.format('%99.99f', -1e308)) >= 100) +local function return_one_thing() + return "hi" +end +local function return_two_nils() + return nil, nil +end + +assert(string.format("%*", return_one_thing()) == "hi") +assert(string.format("%* %*", return_two_nils()) == "nil nil") +assert(pcall(function() + string.format("%* %* %*", return_two_nils()) +end) == false) + +assert(string.format("%*", "a\0b\0c") == "a\0b\0c") +assert(string.format("%*", string.rep("doge", 3000)) == string.rep("doge", 3000)) + +assert(pcall(function() + string.format("%#*", "bad form") +end) == false) + assert(loadstring("return 1\n--comentário sem EOL no final")() == 1)