// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypeInfer.h" #include "Luau/BuiltinDefinitions.h" #include "Luau/Common.h" #include "Fixture.h" #include "doctest.h" using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) LUAU_FASTFLAG(LuauTypestateBuiltins) LUAU_FASTFLAG(LuauStringFormatArityFix) TEST_SUITE_BEGIN("BuiltinTests"); TEST_CASE_FIXTURE(BuiltinsFixture, "math_things_are_defined") { CheckResult result = check(R"( local a00 = math.frexp local a01 = math.ldexp local a02 = math.fmod local a03 = math.modf local a04 = math.pow local a05 = math.exp local a06 = math.floor local a07 = math.abs local a08 = math.sqrt local a09 = math.log local a10 = math.log10 local a11 = math.rad local a12 = math.deg local a13 = math.sin local a14 = math.cos local a15 = math.tan local a16 = math.sinh local a17 = math.cosh local a18 = math.tanh local a19 = math.atan local a20 = math.acos local a21 = math.asin local a22 = math.atan2 local a23 = math.ceil local a24 = math.min local a25 = math.max local a26 = math.pi local a29 = math.huge local a30 = math.randomseed local a31 = math.random )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "next_iterator_should_infer_types_and_type_check") { CheckResult result = check(R"( local a: string, b: number = next({ 1 }) local s = "foo" local t = { [s] = 1 } local c: string?, d: number = next(t) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); } TEST_CASE_FIXTURE(BuiltinsFixture, "pairs_iterator_should_infer_types_and_type_check") { CheckResult result = check(R"( type Map = { [K]: V } local map: Map = { ["foo"] = 1, ["bar"] = 2, ["baz"] = 3 } local it: (Map, string | nil) -> (string?, number), t: Map, i: nil = pairs(map) )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "ipairs_iterator_should_infer_types_and_type_check") { CheckResult result = check(R"( type Map = { [K]: V } local array: Map = { "foo", "bar", "baz" } local it: (Map, number) -> (number?, string), t: Map, i: number = ipairs(array) )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_dot_remove_optionally_returns_generic") { CheckResult result = check(R"( local t = { 1 } local n = table.remove(t, 7) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("n")), "number?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_concat_returns_string") { CheckResult result = check(R"( local r = table.concat({1,2,3,4}, ",", 2); )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(*builtinTypes->stringType, *requireType("r")); } TEST_CASE_FIXTURE(BuiltinsFixture, "sort") { CheckResult result = check(R"( local t = {1, 2, 3}; table.sort(t) )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_predicate") { CheckResult result = check(R"( --!strict local t = {1, 2, 3} local function p(a: number, b: number) return a < b end table.sort(t, p) )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate") { ScopedFastFlag sff{FFlag::LuauSolverV2, false}; CheckResult result = check(R"( --!strict local t = {'one', 'two', 'three'} local function p(a: number, b: number) return a < b end table.sort(t, p) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type '(number, number) -> boolean' could not be converted into '((string, string) -> boolean)?' caused by: None of the union options are compatible. For example: Type '(number, number) -> boolean' could not be converted into '(string, string) -> boolean' caused by: Argument #1 type is not compatible. Type 'string' could not be converted into 'number')"; CHECK_EQ(expected, toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "strings_have_methods") { CheckResult result = check(R"LUA( local s = ("RoactHostChangeEvent(%s)"):format("hello") )LUA"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(*builtinTypes->stringType, *requireType("s")); } TEST_CASE_FIXTURE(BuiltinsFixture, "math_max_variatic") { CheckResult result = check(R"( local n = math.max(1,2,3,4,5,6,7,8,9,0) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(*builtinTypes->numberType, *requireType("n")); } TEST_CASE_FIXTURE(BuiltinsFixture, "math_max_checks_for_numbers") { CheckResult result = check(R"( local n = math.max(1,2,"3") )"); LUAU_REQUIRE_ERRORS(result); CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_tables_sealed") { CheckResult result = check(R"LUA( local b = bit32 )LUA"); TypeId bit32 = requireType("b"); REQUIRE(bit32 != nullptr); const TableType* bit32t = get(bit32); REQUIRE(bit32t != nullptr); CHECK_EQ(bit32t->state, TableState::Sealed); } TEST_CASE_FIXTURE(BuiltinsFixture, "lua_51_exported_globals_all_exist") { // Extracted from lua5.1 CheckResult result = check(R"( local v__G = _G local v_string_sub = string.sub local v_string_upper = string.upper local v_string_len = string.len local v_string_rep = string.rep local v_string_find = string.find local v_string_match = string.match local v_string_char = string.char local v_string_gmatch = string.gmatch local v_string_reverse = string.reverse local v_string_byte = string.byte local v_string_format = string.format local v_string_gsub = string.gsub local v_string_lower = string.lower local v_xpcall = xpcall --local v_package_loadlib = package.loadlib --local v_package_loaders_1_ = package.loaders[1] --local v_package_loaders_2_ = package.loaders[2] --local v_package_loaders_3_ = package.loaders[3] --local v_package_loaders_4_ = package.loaders[4] local v_tostring = tostring local v_print = print --local v_os_exit = os.exit --local v_os_setlocale = os.setlocale local v_os_date = os.date --local v_os_getenv = os.getenv local v_os_difftime = os.difftime --local v_os_remove = os.remove local v_os_time = os.time --local v_os_clock = os.clock --local v_os_tmpname = os.tmpname --local v_os_rename = os.rename --local v_os_execute = os.execute local v_unpack = unpack local v_require = require local v_getfenv = getfenv local v_setmetatable = setmetatable local v_next = next local v_assert = assert local v_tonumber = tonumber --local v_io_lines = io.lines --local v_io_write = io.write --local v_io_close = io.close --local v_io_flush = io.flush --local v_io_open = io.open --local v_io_output = io.output --local v_io_type = io.type --local v_io_read = io.read --local v_io_stderr = io.stderr --local v_io_stdin = io.stdin --local v_io_input = io.input --local v_io_stdout = io.stdout --local v_io_popen = io.popen --local v_io_tmpfile = io.tmpfile local v_rawequal = rawequal --local v_collectgarbage = collectgarbage local v_getmetatable = getmetatable local v_rawset = rawset local v_math_log = math.log local v_math_max = math.max local v_math_acos = math.acos local v_math_huge = math.huge local v_math_ldexp = math.ldexp local v_math_pi = math.pi local v_math_cos = math.cos local v_math_tanh = math.tanh local v_math_pow = math.pow local v_math_deg = math.deg local v_math_tan = math.tan local v_math_cosh = math.cosh local v_math_sinh = math.sinh local v_math_random = math.random local v_math_randomseed = math.randomseed local v_math_frexp = math.frexp local v_math_ceil = math.ceil local v_math_floor = math.floor local v_math_rad = math.rad local v_math_abs = math.abs local v_math_sqrt = math.sqrt local v_math_modf = math.modf local v_math_asin = math.asin local v_math_min = math.min --local v_math_mod = math.mod local v_math_fmod = math.fmod local v_math_log10 = math.log10 local v_math_atan2 = math.atan2 local v_math_exp = math.exp local v_math_sin = math.sin local v_math_atan = math.atan --local v_debug_getupvalue = debug.getupvalue --local v_debug_debug = debug.debug --local v_debug_sethook = debug.sethook --local v_debug_getmetatable = debug.getmetatable --local v_debug_gethook = debug.gethook --local v_debug_setmetatable = debug.setmetatable --local v_debug_setlocal = debug.setlocal --local v_debug_traceback = debug.traceback --local v_debug_setfenv = debug.setfenv --local v_debug_getinfo = debug.getinfo --local v_debug_setupvalue = debug.setupvalue --local v_debug_getlocal = debug.getlocal --local v_debug_getregistry = debug.getregistry --local v_debug_getfenv = debug.getfenv local v_pcall = pcall --local v_table_setn = table.setn local v_table_insert = table.insert --local v_table_getn = table.getn --local v_table_foreachi = table.foreachi local v_table_maxn = table.maxn --local v_table_foreach = table.foreach local v_table_concat = table.concat local v_table_sort = table.sort local v_table_remove = table.remove local v_newproxy = newproxy local v_type = type local v_coroutine_resume = coroutine.resume local v_coroutine_yield = coroutine.yield local v_coroutine_status = coroutine.status local v_coroutine_wrap = coroutine.wrap local v_coroutine_create = coroutine.create local v_coroutine_running = coroutine.running local v_select = select local v_gcinfo = gcinfo local v_pairs = pairs local v_rawget = rawget local v_loadstring = loadstring local v_ipairs = ipairs local v__VERSION = _VERSION --local v_dofile = dofile local v_setfenv = setfenv --local v_load = load local v_error = error --local v_loadfile = loadfile )"); dumpErrors(result); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_unpacks_arg_types_correctly") { CheckResult result = check(R"( setmetatable({}, setmetatable({}, {})) )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_on_union_of_tables") { CheckResult result = check(R"( type A = {tag: "A", x: number} type B = {tag: "B", y: string} type T = A | B type X = typeof( setmetatable({} :: T, {}) ) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK("{ @metatable { }, A } | { @metatable { }, B }" == toString(requireTypeAlias("X"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_correctly_infers_type_of_array_2_args_overload") { CheckResult result = check(R"( local t = {} table.insert(t, "foo") local s = t[1] )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(builtinTypes->stringType, requireType("s")); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_correctly_infers_type_of_array_3_args_overload") { CheckResult result = check(R"( local t = {} table.insert(t, 1, "foo") local s = t[1] )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("string", toString(requireType("s"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack") { CheckResult result = check(R"( local t = table.pack(1, "foo", true) )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) CHECK_EQ("{ [number]: boolean | number | string, n: number }", toString(requireType("t"))); else CHECK_EQ("{| [number]: boolean | number | string, n: number |}", toString(requireType("t"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_variadic") { CheckResult result = check(R"( --!strict function f(): (string, ...number) return "str", 2, 3, 4 end local t = table.pack(f()) )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) CHECK_EQ("{ [number]: number | string, n: number }", toString(requireType("t"))); else CHECK_EQ("{| [number]: number | string, n: number |}", toString(requireType("t"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_reduce") { CheckResult result = check(R"( local t = table.pack(1, 2, true) )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) CHECK_EQ("{ [number]: boolean | number, n: number }", toString(requireType("t"))); else CHECK_EQ("{| [number]: boolean | number, n: number |}", toString(requireType("t"))); result = check(R"( local t = table.pack("a", "b", "c") )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) CHECK_EQ("{ [number]: string, n: number }", toString(requireType("t"))); else CHECK_EQ("{| [number]: string, n: number |}", toString(requireType("t"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "gcinfo") { CheckResult result = check(R"( local n = gcinfo() )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(*builtinTypes->numberType, *requireType("n")); } TEST_CASE_FIXTURE(BuiltinsFixture, "getfenv") { LUAU_REQUIRE_NO_ERRORS(check("getfenv(1)")); } TEST_CASE_FIXTURE(BuiltinsFixture, "os_time_takes_optional_date_table") { CheckResult result = check(R"( local n1 = os.time() local n2 = os.time({ year = 2020, month = 4, day = 20 }) local n3 = os.time({ year = 2020, month = 4, day = 20, hour = 0, min = 0, sec = 0, isdst = true }) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(*builtinTypes->numberType, *requireType("n1")); CHECK_EQ(*builtinTypes->numberType, *requireType("n2")); CHECK_EQ(*builtinTypes->numberType, *requireType("n3")); } TEST_CASE_FIXTURE(BuiltinsFixture, "thread_is_a_type") { CheckResult result = check(R"( local co = coroutine.create(function() end) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK("thread" == toString(requireType("co"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "buffer_is_a_type") { CheckResult result = check(R"( local b = buffer.create(10) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK("buffer" == toString(requireType("b"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "coroutine_resume_anything_goes") { ScopedFastFlag sff{FFlag::LuauSolverV2, false}; CheckResult result = check(R"( local function nifty(x, y) print(x, y) local z = coroutine.yield(1, 2) print(z) return 42 end local co = coroutine.create(nifty) local x, y = coroutine.resume(co, 1, 2) local answer = coroutine.resume(co, 3) )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "coroutine_wrap_anything_goes") { CheckResult result = check(R"( --!nonstrict local function nifty(x, y) print(x, y) local z = coroutine.yield(1, 2) print(z) return 42 end local f = coroutine.wrap(nifty) local x, y = f(1, 2) local answer = f(3) )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_should_not_mutate_persisted_types") { if (FFlag::LuauSolverV2) return; CheckResult result = check(R"( local string = string setmetatable(string, {}) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); auto stringType = requireType("string"); auto ttv = get(stringType); REQUIRE(ttv); } TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_arg_types_inference") { CheckResult result = check(R"( --!strict function f(a, b, c) return string.format("%f %d %s", a, b, c) end )"); CHECK_EQ(0, result.errors.size()); CHECK_EQ("(number, number, string) -> string", toString(requireType("f"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_arg_count_mismatch") { CheckResult result = check(R"( --!strict string.format("%f %d %s") string.format("%s", "hi", 42) string.format("%s", "hi", 42, ...) string.format("%s", "hi", ...) )"); LUAU_REQUIRE_ERROR_COUNT(3, result); CHECK_EQ(result.errors[0].location.begin.line, 2); CHECK_EQ(result.errors[1].location.begin.line, 3); CHECK_EQ(result.errors[2].location.begin.line, 4); } TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_correctly_ordered_types") { // CLI-115690 if (FFlag::LuauSolverV2) return; CheckResult result = check(R"( --!strict string.format("%s", 123) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); CHECK_EQ(tm->wantedType, builtinTypes->stringType); CHECK_EQ(tm->givenType, builtinTypes->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"( --!strict local a, b, c = xpcall( function() return 5, true end, function(e) return 0, false end ) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("boolean", toString(requireType("a"))); CHECK_EQ("number", toString(requireType("b"))); CHECK_EQ("boolean", toString(requireType("c"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "trivial_select") { CheckResult result = check(R"( local a:number = select(1, 42) )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "see_thru_select") { CheckResult result = check(R"( local a:number, b:boolean = select(2,"hi", 10, true) )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "see_thru_select_count") { CheckResult result = check(R"( local a = select("#","hi", 10, true) )"); dumpErrors(result); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "select_with_decimal_argument_is_rounded_down") { CheckResult result = check(R"( local a: number, b: boolean = select(2.9, "foo", 1, true) )"); LUAU_REQUIRE_NO_ERRORS(result); } // Could be flaky if the fix has regressed. TEST_CASE_FIXTURE(BuiltinsFixture, "bad_select_should_not_crash") { CheckResult result = check(R"( do end local _ = function(l0,...) end local _ = function() _(_); _ += select(_()) end )"); if (FFlag::LuauSolverV2) { // Counterintuitively, the parameter l0 is unconstrained and therefore it is valid to pass nil. // The new solver therefore considers that parameter to be optional. LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK("Argument count mismatch. Function expects at least 1 argument, but none are specified" == toString(result.errors[0])); } else { LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK_EQ("Argument count mismatch. Function '_' expects at least 1 argument, but none are specified", toString(result.errors[0])); CHECK_EQ("Argument count mismatch. Function 'select' expects 1 argument, but none are specified", toString(result.errors[1])); } } TEST_CASE_FIXTURE(BuiltinsFixture, "select_way_out_of_range") { // CLI-115720 if (FFlag::LuauSolverV2) return; CheckResult result = check(R"( select(5432598430953240958) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); REQUIRE(get(result.errors[0])); CHECK_EQ("bad argument #1 to select (index out of range)", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "select_slightly_out_of_range") { // CLI-115720 if (FFlag::LuauSolverV2) return; CheckResult result = check(R"( select(3, "a", 1) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); REQUIRE(get(result.errors[0])); CHECK_EQ("bad argument #1 to select (index out of range)", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "select_with_variadic_typepack_tail") { CheckResult result = check(R"( --!nonstrict local function f(...) return ... end local foo, bar, baz, quux = select(1, f("foo", true)) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("any", toString(requireType("foo"))); CHECK_EQ("any", toString(requireType("bar"))); CHECK_EQ("any", toString(requireType("baz"))); CHECK_EQ("any", toString(requireType("quux"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "select_with_variadic_typepack_tail_and_string_head") { // CLI-115720 if (FFlag::LuauSolverV2) return; CheckResult result = check(R"( --!nonstrict local function f(...) return ... end local foo, bar, baz, quux = select(1, "foo", f("bar", true)) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("any", toString(requireType("foo"))); CHECK_EQ("any", toString(requireType("bar"))); CHECK_EQ("any", toString(requireType("baz"))); CHECK_EQ("any", toString(requireType("quux"))); } TEST_CASE_FIXTURE(Fixture, "string_format_as_method") { CheckResult result = check("local _ = ('%s'):format(5)"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); CHECK_EQ(tm->wantedType, builtinTypes->stringType); CHECK_EQ(tm->givenType, builtinTypes->numberType); } TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_trivial_arity") { ScopedFastFlag sff{FFlag::LuauStringFormatArityFix, true}; CheckResult result = check(R"( string.format() )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ("Argument count mismatch. Function 'string.format' expects at least 1 argument, but none are specified", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "string_format_use_correct_argument") { CheckResult result = check(R"( local _ = ("%s"):format("%d", "hello") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ("Argument count mismatch. Function expects 2 arguments, but 3 are specified", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "string_format_use_correct_argument2") { CheckResult result = check(R"( local _ = ("%s %d").format("%d %s", "A type error", 2) )"); LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[1])); } TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_use_correct_argument3") { CheckResult result = check(R"( local s1 = string.format("%d") local s2 = string.format("%d", 1) local s3 = string.format("%d", 1, 2) )"); LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK_EQ("Argument count mismatch. Function expects 2 arguments, but only 1 is specified", toString(result.errors[0])); CHECK_EQ("Argument count mismatch. Function expects 2 arguments, but 3 are specified", toString(result.errors[1])); } TEST_CASE_FIXTURE(BuiltinsFixture, "debug_traceback_is_crazy") { CheckResult result = check(R"( function f(co: thread) -- debug.traceback takes thread?, message?, level? - yes, all optional! debug.traceback() debug.traceback(nil, 1) debug.traceback("msg") debug.traceback("msg", 1) debug.traceback(co) debug.traceback(co, "msg") debug.traceback(co, "msg", 1) end )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "debug_info_is_crazy") { CheckResult result = check(R"( function f(co: thread, f: () -> ()) -- debug.info takes thread?, level, options or function, options debug.info(1, "n") debug.info(co, 1, "n") debug.info(f, "n") end )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "aliased_string_format") { CheckResult result = check(R"( local fmt = string.format local s = fmt("%d", "oops") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "string_lib_self_noself") { CheckResult result = check(R"( --!nonstrict local a1 = string.byte("abcdef", 2) local a2 = string.find("abcdef", "def") local a3 = string.gmatch("ab ab", "%a+") local a4 = string.gsub("abab", "ab", "cd") local a5 = string.len("abc") local a6 = string.match("12 ab", "%d+ %a+") local a7 = string.rep("a", 10) local a8 = string.sub("abcd", 1, 2) local a9 = string.split("a,b,c", ",") local a0 = string.packsize("ff") )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_definition") { CheckResult result = check(R"_( local a, b, c = ("hey"):gmatch("(.)(.)(.)")() for c in ("hey"):gmatch("(.)") do print(c:upper()) end )_"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "select_on_variadic") { CheckResult result = check(R"( local function f(): (number, ...(boolean | number)) return 100, true, 1 end local a, b, c = select(f()) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("any", toString(requireType("a"))); CHECK_EQ("any", toString(requireType("b"))); CHECK_EQ("any", toString(requireType("c"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_report_all_type_errors_at_correct_positions") { CheckResult result = check(R"( ("%s%d%s"):format(1, "hello", true) string.format("%s%d%s", 1, "hello", true) )"); TypeId stringType = builtinTypes->stringType; TypeId numberType = builtinTypes->numberType; TypeId booleanType = builtinTypes->booleanType; LUAU_REQUIRE_ERROR_COUNT(6, result); CHECK_EQ(Location(Position{1, 26}, Position{1, 27}), result.errors[0].location); CHECK_EQ(TypeErrorData(TypeMismatch{stringType, numberType}), result.errors[0].data); CHECK_EQ(Location(Position{1, 29}, Position{1, 36}), result.errors[1].location); CHECK_EQ(TypeErrorData(TypeMismatch{numberType, stringType}), result.errors[1].data); CHECK_EQ(Location(Position{1, 38}, Position{1, 42}), result.errors[2].location); CHECK_EQ(TypeErrorData(TypeMismatch{stringType, booleanType}), result.errors[2].data); CHECK_EQ(Location(Position{2, 32}, Position{2, 33}), result.errors[3].location); CHECK_EQ(TypeErrorData(TypeMismatch{stringType, numberType}), result.errors[3].data); CHECK_EQ(Location(Position{2, 35}, Position{2, 42}), result.errors[4].location); CHECK_EQ(TypeErrorData(TypeMismatch{numberType, stringType}), result.errors[4].data); CHECK_EQ(Location(Position{2, 44}, Position{2, 48}), result.errors[5].location); CHECK_EQ(TypeErrorData(TypeMismatch{stringType, booleanType}), result.errors[5].data); } TEST_CASE_FIXTURE(BuiltinsFixture, "tonumber_returns_optional_number_type") { CheckResult result = check(R"( --!strict local b: number = tonumber('asdf') )"); LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) CHECK_EQ( "Type 'number?' could not be converted into 'number'; type number?[1] (nil) is not a subtype of number (number)", toString(result.errors[0]) ); else CHECK_EQ("Type 'number?' could not be converted into 'number'", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "tonumber_returns_optional_number_type2") { CheckResult result = check(R"( --!strict local b: number = tonumber('asdf') or 1 )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "dont_add_definitions_to_persistent_types") { // This test makes no sense with type states and I think it generally makes no sense under the new solver. // TODO: clip. if (FFlag::LuauSolverV2) return; CheckResult result = check(R"( local f = math.sin local function g(x) return math.sin(x) end f = g )"); LUAU_REQUIRE_NO_ERRORS(result); TypeId fType = requireType("f"); const FunctionType* ftv = get(fType); REQUIRE(fType); REQUIRE(fType->persistent); REQUIRE(!ftv->definition); TypeId gType = requireType("g"); const FunctionType* gtv = get(gType); REQUIRE(gType); REQUIRE(!gType->persistent); REQUIRE(gtv->definition); } TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types") { CheckResult result = check(R"( local function f(x: (number | boolean)?) return assert(x) end )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f"))); else CHECK_EQ("((boolean | number)?) -> boolean | number", toString(requireType("f"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types2") { CheckResult result = check(R"( local function f(x: (number | boolean)?): number | true return assert(x) end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types3") { CheckResult result = check(R"( local function f(x: (number | boolean)?) assert(x) return x end )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f"))); else // without the annotation, the old solver doesn't infer the best return type here CHECK_EQ("((boolean | number)?) -> boolean | number", toString(requireType("f"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type") { if (FFlag::LuauSolverV2) return; CheckResult result = check(R"( local function f(...: number?) return assert(...) end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("(...number?) -> (number, ...number?)", toString(requireType("f"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy") { if (FFlag::LuauSolverV2) { // CLI-114134 - egraph simplification return; } CheckResult result = check(R"( local function f(x: nil) return assert(x, "hmm") end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("(nil) -> (never, ...never)", toString(requireType("f"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_is_generic") { CheckResult result = check(R"( local t1: {a: number} = {a = 42} local t2: {b: string} = {b = "hello"} local t3: {boolean} = {false, true} local tf1 = table.freeze(t1) local tf2 = table.freeze(t2) local tf3 = table.freeze(t3) local a = tf1.a local b = tf2.b local c = tf3[2] local d = tf1.b local a2 = t1.a local b2 = t2.b local c2 = t3[2] )"); LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2 && FFlag::LuauTypestateBuiltins) CHECK("Key 'b' not found in table '{ read a: number }'" == toString(result.errors[0])); else if (FFlag::LuauSolverV2) CHECK("Key 'b' not found in table '{ a: number }'" == toString(result.errors[0])); else CHECK_EQ("Key 'b' not found in table '{| a: number |}'", toString(result.errors[0])); CHECK(Location({13, 18}, {13, 23}) == result.errors[0].location); if (FFlag::LuauSolverV2 && FFlag::LuauTypestateBuiltins) { CHECK_EQ("{ read a: number }", toString(requireTypeAtPosition({15, 19}))); CHECK_EQ("{ read b: string }", toString(requireTypeAtPosition({16, 19}))); CHECK_EQ("{boolean}", toString(requireTypeAtPosition({17, 19}))); } CHECK_EQ("number", toString(requireType("a"))); CHECK_EQ("string", toString(requireType("b"))); CHECK_EQ("boolean", toString(requireType("c"))); if (FFlag::LuauSolverV2) CHECK_EQ("any", toString(requireType("d"))); else CHECK_EQ("*error-type*", toString(requireType("d"))); CHECK_EQ("number", toString(requireType("a2"))); CHECK_EQ("string", toString(requireType("b2"))); CHECK_EQ("boolean", toString(requireType("c2"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_does_not_retroactively_block_mutation") { CheckResult result = check(R"( local t1 = {a = 42} t1.q = ":3" local tf1 = table.freeze(t1) local a = tf1.a local b = t1.a )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauTypestateBuiltins) { CHECK_EQ("t1 | { read a: number, read q: string }", toString(requireType("t1"))); // before the assignment, it's `t1` CHECK_EQ("t1", toString(requireTypeAtPosition({3, 8}))); // after the assignment, it's read-only. CHECK_EQ("{ read a: number, read q: string }", toString(requireTypeAtPosition({8, 18}))); } CHECK_EQ("number", toString(requireType("a"))); CHECK_EQ("number", toString(requireType("b"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_no_generic_table") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( --!strict type k = { read k: string, } function _(): k return table.freeze({ k = "", }) end )"); if (FFlag::LuauTypestateBuiltins) LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_errors_on_non_tables") { CheckResult result = check(R"( --!strict table.freeze(42) )"); // this does not error in the new solver without the typestate builtins functionality. if (FFlag::LuauSolverV2 && !FFlag::LuauTypestateBuiltins) { LUAU_REQUIRE_NO_ERRORS(result); return; } LUAU_REQUIRE_ERROR_COUNT(1, result); TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); if (FFlag::LuauSolverV2 && FFlag::LuauTypestateBuiltins) CHECK_EQ(toString(tm->wantedType), "table"); else CHECK_EQ(toString(tm->wantedType), "{- -}"); CHECK_EQ(toString(tm->givenType), "number"); } TEST_CASE_FIXTURE(BuiltinsFixture, "set_metatable_needs_arguments") { // In the new solver, nil can certainly be used where a generic is required, so all generic parameters are optional. ScopedFastFlag sff{FFlag::LuauSolverV2, false}; CheckResult result = check(R"( local a = {b=setmetatable} a.b() a:b() a:b({}) )"); LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK_EQ(toString(result.errors[0]), "Argument count mismatch. Function 'a.b' expects 2 arguments, but none are specified"); CHECK_EQ(toString(result.errors[1]), "Argument count mismatch. Function 'a.b' expects 2 arguments, but only 1 is specified"); } TEST_CASE_FIXTURE(Fixture, "typeof_unresolved_function") { CheckResult result = check(R"( local function f(a: typeof(f)) end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ("Unknown global 'f'", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "no_persistent_typelevel_change") { TypeId mathTy = requireType(frontend.globals.globalScope, "math"); REQUIRE(mathTy); TableType* ttv = getMutable(mathTy); REQUIRE(ttv); const FunctionType* ftv = get(ttv->props["frexp"].type()); REQUIRE(ftv); auto original = ftv->level; CheckResult result = check("local a = math.frexp"); LUAU_REQUIRE_NO_ERRORS(result); CHECK(ftv->level.level == original.level); CHECK(ftv->level.subLevel == original.subLevel); } TEST_CASE_FIXTURE(BuiltinsFixture, "global_singleton_types_are_sealed") { CheckResult result = check(R"( local function f(x: string) local p = x:split('a') p = table.pack(table.unpack(p, 1, #p - 1)) return p end )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(Fixture, "string_match") { CheckResult result = check(R"( local s: string = "hello" local p = s:match("foo") )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("p")), "string?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types") { CheckResult result = check(R"END( local a, b, c = string.gmatch("This is a string", "(.()(%a+))")() )END"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("a")), "string?"); CHECK_EQ(toString(requireType("b")), "number?"); CHECK_EQ(toString(requireType("c")), "string?"); } TEST_CASE_FIXTURE(Fixture, "gmatch_capture_types2") { CheckResult result = check(R"END( local a, b, c = ("This is a string"):gmatch("(.()(%a+))")() )END"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("a")), "string?"); CHECK_EQ(toString(requireType("b")), "number?"); CHECK_EQ(toString(requireType("c")), "string?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_default_capture") { CheckResult result = check(R"END( local a, b, c, d = string.gmatch("T(his)() is a string", ".")() )END"); LUAU_REQUIRE_ERROR_COUNT(1, result); CountMismatch* acm = get(result.errors[0]); REQUIRE(acm); CHECK_EQ(acm->context, CountMismatch::FunctionResult); CHECK_EQ(acm->expected, 1); CHECK_EQ(acm->actual, 4); CHECK_EQ(toString(requireType("a")), "string?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_balanced_escaped_parens") { CheckResult result = check(R"END( local a, b, c, d = string.gmatch("T(his) is a string", "((.)%b()())")() )END"); LUAU_REQUIRE_ERROR_COUNT(1, result); CountMismatch* acm = get(result.errors[0]); REQUIRE(acm); CHECK_EQ(acm->context, CountMismatch::FunctionResult); CHECK_EQ(acm->expected, 3); CHECK_EQ(acm->actual, 4); CHECK_EQ(toString(requireType("a")), "string?"); CHECK_EQ(toString(requireType("b")), "string?"); CHECK_EQ(toString(requireType("c")), "number?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_parens_in_sets_are_ignored") { CheckResult result = check(R"END( local a, b, c = string.gmatch("T(his)() is a string", "(T[()])()")() )END"); LUAU_REQUIRE_ERROR_COUNT(1, result); CountMismatch* acm = get(result.errors[0]); REQUIRE(acm); CHECK_EQ(acm->context, CountMismatch::FunctionResult); CHECK_EQ(acm->expected, 2); CHECK_EQ(acm->actual, 3); CHECK_EQ(toString(requireType("a")), "string?"); CHECK_EQ(toString(requireType("b")), "number?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_set_containing_lbracket") { CheckResult result = check(R"END( local a, b = string.gmatch("[[[", "()([[])")() )END"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("a")), "number?"); CHECK_EQ(toString(requireType("b")), "string?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_leading_end_bracket_is_part_of_set") { CheckResult result = check(R"END( -- An immediate right-bracket following a left-bracket is included within the set; -- thus, '[]]'' is the set containing ']', and '[]' is an invalid set missing an enclosing -- right-bracket. We detect an invalid set in this case and fall back to to default gmatch -- typing. local foo = string.gmatch("T[hi%]s]]]() is a string", "([]s)") )END"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("foo")), "() -> (...string)"); } TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_invalid_pattern_fallback_to_builtin") { CheckResult result = check(R"END( local foo = string.gmatch("T(his)() is a string", ")") )END"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("foo")), "() -> (...string)"); } TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_invalid_pattern_fallback_to_builtin2") { CheckResult result = check(R"END( local foo = string.gmatch("T(his)() is a string", "[") )END"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("foo")), "() -> (...string)"); } TEST_CASE_FIXTURE(BuiltinsFixture, "match_capture_types") { CheckResult result = check(R"END( local a, b, c = string.match("This is a string", "(.()(%a+))") )END"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("a")), "string?"); CHECK_EQ(toString(requireType("b")), "number?"); CHECK_EQ(toString(requireType("c")), "string?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "match_capture_types2") { CheckResult result = check(R"END( local a, b, c = string.match("This is a string", "(.()(%a+))", "this should be a number") )END"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); CHECK_EQ(toString(tm->wantedType), "number?"); CHECK_EQ(toString(tm->givenType), "string"); CHECK_EQ(toString(requireType("a")), "string?"); CHECK_EQ(toString(requireType("b")), "number?"); CHECK_EQ(toString(requireType("c")), "string?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "find_capture_types") { CheckResult result = check(R"END( local d, e, a, b, c = string.find("This is a string", "(.()(%a+))") )END"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireType("a")), "string?"); CHECK_EQ(toString(requireType("b")), "number?"); CHECK_EQ(toString(requireType("c")), "string?"); CHECK_EQ(toString(requireType("d")), "number?"); CHECK_EQ(toString(requireType("e")), "number?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "find_capture_types2") { CheckResult result = check(R"END( local d, e, a, b, c = string.find("This is a string", "(.()(%a+))", "this should be a number") )END"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); CHECK_EQ(toString(tm->wantedType), "number?"); CHECK_EQ(toString(tm->givenType), "string"); CHECK_EQ(toString(requireType("a")), "string?"); CHECK_EQ(toString(requireType("b")), "number?"); CHECK_EQ(toString(requireType("c")), "string?"); CHECK_EQ(toString(requireType("d")), "number?"); CHECK_EQ(toString(requireType("e")), "number?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "find_capture_types3") { CheckResult result = check(R"END( local d, e, a, b, c = string.find("This is a string", "(.()(%a+))", 1, "this should be a bool") )END"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); CHECK_EQ(toString(tm->wantedType), "boolean?"); CHECK_EQ(toString(tm->givenType), "string"); CHECK_EQ(toString(requireType("a")), "string?"); CHECK_EQ(toString(requireType("b")), "number?"); CHECK_EQ(toString(requireType("c")), "string?"); CHECK_EQ(toString(requireType("d")), "number?"); CHECK_EQ(toString(requireType("e")), "number?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "find_capture_types3") { CheckResult result = check(R"END( local d, e, a, b = string.find("This is a string", "(.()(%a+))", 1, true) )END"); LUAU_REQUIRE_ERROR_COUNT(1, result); CountMismatch* acm = get(result.errors[0]); REQUIRE(acm); CHECK_EQ(acm->context, CountMismatch::FunctionResult); CHECK_EQ(acm->expected, 2); CHECK_EQ(acm->actual, 4); CHECK_EQ(toString(requireType("d")), "number?"); CHECK_EQ(toString(requireType("e")), "number?"); } TEST_CASE_FIXTURE(BuiltinsFixture, "string_find_should_not_crash") { ScopedFastFlag _{FFlag::LuauSolverV2, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( local function StringSplit(input, separator) string.find(input, separator) if not separator then separator = "%s+" end end )")); } TEST_SUITE_END();