// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypeFamily.h" #include "Luau/ConstraintSolver.h" #include "Luau/NotNull.h" #include "Luau/TxnLog.h" #include "Luau/Type.h" #include "ClassFixture.h" #include "Fixture.h" #include "doctest.h" using namespace Luau; LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) struct FamilyFixture : Fixture { TypeFamily swapFamily; FamilyFixture() : Fixture(true, false) { swapFamily = TypeFamily{/* name */ "Swap", /* reducer */ [](TypeId instance, std::vector tys, std::vector tps, NotNull ctx) -> TypeFamilyReductionResult { LUAU_ASSERT(tys.size() == 1); TypeId param = follow(tys.at(0)); if (isString(param)) { return TypeFamilyReductionResult{ctx->builtins->numberType, false, {}, {}}; } else if (isNumber(param)) { return TypeFamilyReductionResult{ctx->builtins->stringType, false, {}, {}}; } else if (is(param) || is(param) || is(param) || (ctx->solver && ctx->solver->hasUnresolvedConstraints(param))) { return TypeFamilyReductionResult{std::nullopt, false, {param}, {}}; } else { return TypeFamilyReductionResult{std::nullopt, true, {}, {}}; } }}; unfreeze(frontend.globals.globalTypes); TypeId t = frontend.globals.globalTypes.addType(GenericType{"T"}); GenericTypeDefinition genericT{t}; ScopePtr globalScope = frontend.globals.globalScope; globalScope->exportedTypeBindings["Swap"] = TypeFun{{genericT}, frontend.globals.globalTypes.addType(TypeFamilyInstanceType{NotNull{&swapFamily}, {t}, {}})}; freeze(frontend.globals.globalTypes); } }; TEST_SUITE_BEGIN("TypeFamilyTests"); TEST_CASE_FIXTURE(FamilyFixture, "basic_type_family") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type A = Swap type B = Swap type C = Swap local x = 123 local y: Swap = "foo" )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK("string" == toString(requireTypeAlias("A"))); CHECK("number" == toString(requireTypeAlias("B"))); CHECK("Swap" == toString(requireTypeAlias("C"))); CHECK("string" == toString(requireType("y"))); CHECK("Type family instance Swap is uninhabited" == toString(result.errors[0])); }; TEST_CASE_FIXTURE(FamilyFixture, "family_as_fn_ret") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local swapper: (T) -> Swap local a = swapper(123) local b = swapper("foo") local c = swapper(false) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK("string" == toString(requireType("a"))); CHECK("number" == toString(requireType("b"))); CHECK("Swap" == toString(requireType("c"))); CHECK("Type family instance Swap is uninhabited" == toString(result.errors[0])); } TEST_CASE_FIXTURE(FamilyFixture, "family_as_fn_arg") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local swapper: (Swap) -> T local a = swapper(123) local b = swapper(false) )"); LUAU_REQUIRE_ERROR_COUNT(2, result); // FIXME: Can we constrain these to `never` or `unknown`? CHECK("a" == toString(requireType("a"))); CHECK("a" == toString(requireType("b"))); CHECK("Type family instance Swap is uninhabited" == toString(result.errors[0])); CHECK("Type family instance Swap is uninhabited" == toString(result.errors[1])); } TEST_CASE_FIXTURE(FamilyFixture, "resolve_deep_families") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local x: Swap>> )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK("number" == toString(requireType("x"))); } TEST_CASE_FIXTURE(FamilyFixture, "unsolvable_family") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local impossible: (Swap) -> Swap> local a = impossible(123) local b = impossible(true) )"); LUAU_REQUIRE_ERROR_COUNT(2, result); for (size_t i = 0; i < 2; ++i) { CHECK(toString(result.errors[i]) == "Type family instance Swap is uninhabited"); } } TEST_CASE_FIXTURE(FamilyFixture, "table_internal_families") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local t: ({T}) -> {Swap} local a = t({1, 2, 3}) local b = t({"a", "b", "c"}) local c = t({true, false, true}) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK(toString(requireType("a")) == "{string}"); CHECK(toString(requireType("b")) == "{number}"); CHECK(toString(requireType("c")) == "{Swap}"); CHECK(toString(result.errors[0]) == "Type family instance Swap is uninhabited"); } TEST_CASE_FIXTURE(FamilyFixture, "function_internal_families") { // This test is broken right now, but it's not because of type families. See // CLI-71143. if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local f0: (T) -> (() -> T) local f: (T) -> (() -> Swap) local a = f(1) local b = f("a") local c = f(true) local d = f0(1) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK(toString(requireType("a")) == "() -> string"); CHECK(toString(requireType("b")) == "() -> number"); CHECK(toString(requireType("c")) == "() -> Swap"); CHECK(toString(result.errors[0]) == "Type family instance Swap is uninhabited"); } TEST_CASE_FIXTURE(Fixture, "add_family_at_work") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local function add(a, b) return a + b end local a = add(1, 2) local b = add(1, "foo") local c = add("foo", 1) )"); LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK(toString(requireType("a")) == "number"); CHECK(toString(requireType("b")) == "Add"); CHECK(toString(requireType("c")) == "Add"); CHECK(toString(result.errors[0]) == "Type family instance Add is uninhabited"); CHECK(toString(result.errors[1]) == "Type family instance Add is uninhabited"); } TEST_CASE_FIXTURE(Fixture, "internal_families_raise_errors") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local function innerSum(a, b) local _ = a + b end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK(toString(result.errors[0]) == "Type family instance Add depends on generic function parameters but does not appear in the function " "signature; this construct cannot be type-checked at this time"); } TEST_CASE_FIXTURE(BuiltinsFixture, "type_families_can_be_shadowed") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type add = string -- shadow add -- this should be ok function hi(f: add) return string.format("hi %s", f) end -- this should still work totally fine (and use the real type family) function plus(a, b) return a + b end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK(toString(requireType("hi")) == "(string) -> string"); CHECK(toString(requireType("plus")) == "(a, b) -> add"); } TEST_CASE_FIXTURE(BuiltinsFixture, "type_families_inhabited_with_normalization") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local useGridConfig : any local columns = useGridConfig("columns", {}) or 1 local gutter = useGridConfig('gutter', {}) or 0 local margin = useGridConfig('margin', {}) or 0 return function(frameAbsoluteWidth: number) local cellAbsoluteWidth = (frameAbsoluteWidth - 2 * margin + gutter) / columns - gutter end )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_works") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type MyObject = { x: number, y: number, z: number } type KeysOfMyObject = keyof local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end local function err(idx: KeysOfMyObject): "x" | "y" return idx end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypePackMismatch* tpm = get(result.errors[0]); REQUIRE(tpm); CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp)); CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp)); } TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_works_with_metatables") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local metatable = { __index = {w = 1} } local obj = setmetatable({x = 1, y = 2, z = 3}, metatable) type MyObject = typeof(obj) type KeysOfMyObject = keyof local function ok(idx: KeysOfMyObject): "w" | "x" | "y" | "z" return idx end local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypePackMismatch* tpm = get(result.errors[0]); REQUIRE(tpm); CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->wantedTp)); CHECK_EQ("\"w\" | \"x\" | \"y\" | \"z\"", toString(tpm->givenTp)); } TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_errors_if_it_has_nontable_part") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type MyObject = { x: number, y: number, z: number } type KeysOfMyObject = keyof local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end )"); // FIXME(CLI-95289): we should actually only report the type family being uninhabited error at its first use, I think? LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK(toString(result.errors[0]) == "Type family instance keyof is uninhabited"); CHECK(toString(result.errors[1]) == "Type family instance keyof is uninhabited"); } TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_string_indexer") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type MyObject = { x: number, y: number, z: number } type MyOtherObject = { [string]: number } type KeysOfMyOtherObject = keyof type KeysOfMyObjects = keyof local function ok(idx: KeysOfMyOtherObject): "z" return idx end local function err(idx: KeysOfMyObjects): "z" return idx end )"); LUAU_REQUIRE_ERROR_COUNT(2, result); TypePackMismatch* tpm = get(result.errors[0]); REQUIRE(tpm); CHECK_EQ("\"z\"", toString(tpm->wantedTp)); CHECK_EQ("string", toString(tpm->givenTp)); tpm = get(result.errors[1]); REQUIRE(tpm); CHECK_EQ("\"z\"", toString(tpm->wantedTp)); CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp)); } TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_common_subset_if_union_of_differing_tables") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type MyObject = { x: number, y: number, z: number } type MyOtherObject = { w: number, y: number, z: number } type KeysOfMyObject = keyof local function err(idx: KeysOfMyObject): "z" return idx end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypePackMismatch* tpm = get(result.errors[0]); REQUIRE(tpm); CHECK_EQ("\"z\"", toString(tpm->wantedTp)); CHECK_EQ("\"y\" | \"z\"", toString(tpm->givenTp)); } TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_never_for_empty_table") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type KeyofEmpty = keyof<{}> local foo = ((nil :: any) :: KeyofEmpty) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK(toString(requireType("foo")) == "never"); } TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_works") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type MyObject = { x: number, y: number, z: number } type KeysOfMyObject = rawkeyof local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end local function err(idx: KeysOfMyObject): "x" | "y" return idx end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypePackMismatch* tpm = get(result.errors[0]); REQUIRE(tpm); CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp)); CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp)); } TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_ignores_metatables") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local metatable = { __index = {w = 1} } local obj = setmetatable({x = 1, y = 2, z = 3}, metatable) type MyObject = typeof(obj) type KeysOfMyObject = rawkeyof local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end local function err(idx: KeysOfMyObject): "x" | "y" return idx end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypePackMismatch* tpm = get(result.errors[0]); REQUIRE(tpm); CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp)); CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp)); } TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_errors_if_it_has_nontable_part") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type MyObject = { x: number, y: number, z: number } type KeysOfMyObject = rawkeyof local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end )"); // FIXME(CLI-95289): we should actually only report the type family being uninhabited error at its first use, I think? LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK(toString(result.errors[0]) == "Type family instance rawkeyof is uninhabited"); CHECK(toString(result.errors[1]) == "Type family instance rawkeyof is uninhabited"); } TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_common_subset_if_union_of_differing_tables") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type MyObject = { x: number, y: number, z: number } type MyOtherObject = { w: number, y: number, z: number } type KeysOfMyObject = rawkeyof local function err(idx: KeysOfMyObject): "z" return idx end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypePackMismatch* tpm = get(result.errors[0]); REQUIRE(tpm); CHECK_EQ("\"z\"", toString(tpm->wantedTp)); CHECK_EQ("\"y\" | \"z\"", toString(tpm->givenTp)); } TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_never_for_empty_table") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type RawkeyofEmpty = rawkeyof<{}> local foo = ((nil :: any) :: RawkeyofEmpty) )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK(toString(requireType("foo")) == "never"); } TEST_CASE_FIXTURE(ClassFixture, "keyof_type_family_works_on_classes") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type KeysOfMyObject = keyof local function ok(idx: KeysOfMyObject): "BaseMethod" | "BaseField" | "Touched" return idx end local function err(idx: KeysOfMyObject): "BaseMethod" return idx end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypePackMismatch* tpm = get(result.errors[0]); REQUIRE(tpm); CHECK_EQ("\"BaseMethod\"", toString(tpm->wantedTp)); CHECK_EQ("\"BaseField\" | \"BaseMethod\" | \"Touched\"", toString(tpm->givenTp)); } TEST_CASE_FIXTURE(ClassFixture, "keyof_type_family_errors_if_it_has_nonclass_part") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type KeysOfMyObject = keyof local function err(idx: KeysOfMyObject): "BaseMethod" | "BaseField" return idx end )"); // FIXME(CLI-95289): we should actually only report the type family being uninhabited error at its first use, I think? LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK(toString(result.errors[0]) == "Type family instance keyof is uninhabited"); CHECK(toString(result.errors[1]) == "Type family instance keyof is uninhabited"); } TEST_CASE_FIXTURE(ClassFixture, "keyof_type_family_common_subset_if_union_of_differing_classes") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type KeysOfMyObject = keyof local function ok(idx: KeysOfMyObject): never return idx end )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(ClassFixture, "binary_type_family_works_with_default_argument") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( type result = mul local function thunk(): result return 5 * 4 end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK("() -> number" == toString(requireType("thunk"))); } TEST_CASE_FIXTURE(ClassFixture, "vector2_multiply_is_overloaded") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local v = Vector2.New(1, 2) local v2 = v * 1.5 local v3 = v * v local v4 = v * "Hello" -- line 5 )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK(5 == result.errors[0].location.begin.line); CHECK(5 == result.errors[0].location.end.line); CHECK("Vector2" == toString(requireType("v2"))); CHECK("Vector2" == toString(requireType("v3"))); CHECK("mul" == toString(requireType("v4"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_rfc_example") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local animals = { cat = { speak = function() print "meow" end }, dog = { speak = function() print "woof woof" end }, monkey = { speak = function() print "oo oo" end }, fox = { speak = function() print "gekk gekk" end } } type AnimalType = keyof function speakByType(animal: AnimalType) animals[animal].speak() end speakByType("dog") -- ok speakByType("cactus") -- errors )"); LUAU_REQUIRE_ERROR_COUNT(1, result); TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); CHECK_EQ("\"cat\" | \"dog\" | \"fox\" | \"monkey\"", toString(tm->wantedType)); CHECK_EQ("\"cactus\"", toString(tm->givenType)); } TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_oss_crash_gh1161") { if (!FFlag::DebugLuauDeferredConstraintResolution) return; CheckResult result = check(R"( local EnumVariants = { ["a"] = 1, ["b"] = 2, ["c"] = 3 } type EnumKey = keyof function fnA(i: T): keyof end function fnB(i: EnumKey) end local result = fnA(EnumVariants) fnB(result) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK(get(result.errors[0])); } TEST_SUITE_END();