diff --git a/Analysis/include/Luau/BuiltinTypeFunctions.h b/Analysis/include/Luau/BuiltinTypeFunctions.h new file mode 100644 index 00000000..7e75f53d --- /dev/null +++ b/Analysis/include/Luau/BuiltinTypeFunctions.h @@ -0,0 +1,56 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/TypeFunction.h" + +namespace Luau +{ + +struct BuiltinTypeFunctions +{ + BuiltinTypeFunctions(); + + TypeFunction userFunc; + + TypeFunction notFunc; + TypeFunction lenFunc; + TypeFunction unmFunc; + + TypeFunction addFunc; + TypeFunction subFunc; + TypeFunction mulFunc; + TypeFunction divFunc; + TypeFunction idivFunc; + TypeFunction powFunc; + TypeFunction modFunc; + + TypeFunction concatFunc; + + TypeFunction andFunc; + TypeFunction orFunc; + + TypeFunction ltFunc; + TypeFunction leFunc; + TypeFunction eqFunc; + + TypeFunction refineFunc; + TypeFunction singletonFunc; + TypeFunction unionFunc; + TypeFunction intersectFunc; + + TypeFunction keyofFunc; + TypeFunction rawkeyofFunc; + TypeFunction indexFunc; + TypeFunction rawgetFunc; + + TypeFunction setmetatableFunc; + TypeFunction getmetatableFunc; + + TypeFunction weakoptionalFunc; + + void addToScope(NotNull arena, NotNull scope) const; +}; + +const BuiltinTypeFunctions& builtinTypeFunctions(); + +} diff --git a/Analysis/include/Luau/TypeFunction.h b/Analysis/include/Luau/TypeFunction.h index 10cf30e2..a96b800b 100644 --- a/Analysis/include/Luau/TypeFunction.h +++ b/Analysis/include/Luau/TypeFunction.h @@ -25,43 +25,7 @@ struct TypeFunctionRuntimeBuilderState; struct TypeFunctionContext; class Normalizer; -using StateRef = std::unique_ptr; - -struct TypeFunctionRuntime -{ - TypeFunctionRuntime(NotNull ice, NotNull limits); - ~TypeFunctionRuntime(); - - // Return value is an error message if registration failed - std::optional registerFunction(AstStatTypeFunction* function); - - // For user-defined type functions, we store all generated types and packs for the duration of the typecheck - TypedAllocator typeArena; - TypedAllocator typePackArena; - - NotNull ice; - NotNull limits; - - StateRef state; - - // Set of functions which have their environment table initialized - DenseHashSet initialized{nullptr}; - - // Evaluation of type functions should only be performed in the absence of parse errors in the source module - bool allowEvaluation = true; - - // Root scope in which the type function operates in, set up by ConstraintGenerator - ScopePtr rootScope; - - // Output created by 'print' function - std::vector messages; - - // Type builder, valid for the duration of a single evaluation - TypeFunctionRuntimeBuilderState* runtimeBuilder = nullptr; - -private: - void prepareState(); -}; +struct TypeFunctionRuntime; struct TypeFunctionContext { @@ -203,7 +167,7 @@ struct FunctionGraphReductionResult * @param normalizer the normalizer to use when normalizing types * @param ice the internal error reporter to use for ICEs */ -FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, TypeFunctionContext, bool force = false); +FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, NotNull ctx, bool force = false); /** * Attempt to reduce all instances of any type or type pack functions in the type @@ -217,53 +181,13 @@ FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location loc * @param normalizer the normalizer to use when normalizing types * @param ice the internal error reporter to use for ICEs */ -FunctionGraphReductionResult reduceTypeFunctions(TypePackId entrypoint, Location location, TypeFunctionContext, bool force = false); +FunctionGraphReductionResult reduceTypeFunctions(TypePackId entrypoint, Location location, NotNull ctx, bool force = false); -struct BuiltinTypeFunctions -{ - BuiltinTypeFunctions(); - - TypeFunction userFunc; - - TypeFunction notFunc; - TypeFunction lenFunc; - TypeFunction unmFunc; - - TypeFunction addFunc; - TypeFunction subFunc; - TypeFunction mulFunc; - TypeFunction divFunc; - TypeFunction idivFunc; - TypeFunction powFunc; - TypeFunction modFunc; - - TypeFunction concatFunc; - - TypeFunction andFunc; - TypeFunction orFunc; - - TypeFunction ltFunc; - TypeFunction leFunc; - TypeFunction eqFunc; - - TypeFunction refineFunc; - TypeFunction singletonFunc; - TypeFunction unionFunc; - TypeFunction intersectFunc; - - TypeFunction keyofFunc; - TypeFunction rawkeyofFunc; - TypeFunction indexFunc; - TypeFunction rawgetFunc; - - TypeFunction setmetatableFunc; - TypeFunction getmetatableFunc; - - TypeFunction weakoptionalFunc; - - void addToScope(NotNull arena, NotNull scope) const; -}; - -const BuiltinTypeFunctions& builtinTypeFunctions(); +/* Returns true if the type provided should block a type function from reducing. + * + * Most type functions cannot dispatch if one of their operands is a + * BlockedType, a PendingExpansionType, or an unsolved TypeFunctionInstanceType. + */ +bool isPending(TypeId ty, ConstraintSolver* solver); } // namespace Luau diff --git a/Analysis/include/Luau/TypeFunctionRuntime.h b/Analysis/include/Luau/TypeFunctionRuntime.h index 26d216c4..6b198c72 100644 --- a/Analysis/include/Luau/TypeFunctionRuntime.h +++ b/Analysis/include/Luau/TypeFunctionRuntime.h @@ -2,8 +2,10 @@ #pragma once #include "Luau/Common.h" +#include "Luau/Scope.h" +#include "Luau/TypeFunctionRuntimeBuilder.h" +#include "Luau/Type.h" #include "Luau/Variant.h" -#include "Luau/TypeFwd.h" #include #include @@ -15,17 +17,22 @@ using lua_State = struct lua_State; namespace Luau { -struct TypeFunctionRuntime; +struct InternalErrorReporter; +struct TypeCheckLimits; +struct TypeFunctionRuntimeBuilderState; + +struct LuauTempThreadPopper +{ + explicit LuauTempThreadPopper(lua_State* L); + ~LuauTempThreadPopper(); + + lua_State* L = nullptr; +}; + +using StateRef = std::unique_ptr; void* typeFunctionAlloc(void* ud, void* ptr, size_t osize, size_t nsize); -// Replica of types from Type.h -struct TypeFunctionType; -using TypeFunctionTypeId = const TypeFunctionType*; - -struct TypeFunctionTypePackVar; -using TypeFunctionTypePackId = const TypeFunctionTypePackVar*; - struct TypeFunctionPrimitiveType { enum Type @@ -274,6 +281,42 @@ T* getMutable(TypeFunctionTypeId tv) return tv ? Luau::get_if(&const_cast(tv)->type) : nullptr; } +struct TypeFunctionRuntime +{ + TypeFunctionRuntime(NotNull ice, NotNull limits); + ~TypeFunctionRuntime(); + + // Return value is an error message if registration failed + std::optional registerFunction(AstStatTypeFunction* function); + + // For user-defined type functions, we store all generated types and packs for the duration of the typecheck + TypedAllocator typeArena; + TypedAllocator typePackArena; + + NotNull ice; + NotNull limits; + + StateRef state; + + // Set of functions which have their environment table initialized + DenseHashSet initialized{nullptr}; + + // Evaluation of type functions should only be performed in the absence of parse errors in the source module + bool allowEvaluation = true; + + // Root scope in which the type function operates in, set up by ConstraintGenerator + ScopePtr rootScope; + + // Output created by 'print' function + std::vector messages; + + // Type builder, valid for the duration of a single evaluation + TypeFunctionRuntimeBuilderState* runtimeBuilder = nullptr; + +private: + void prepareState(); +}; + std::optional checkResultForError(lua_State* L, const char* typeFunctionName, int luaResult); TypeFunctionRuntime* getTypeFunctionRuntime(lua_State* L); diff --git a/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h b/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h index 191bcf18..4dc57c69 100644 --- a/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h +++ b/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h @@ -1,20 +1,12 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include "Luau/Type.h" #include "Luau/TypeFunction.h" -#include "Luau/TypeFunctionRuntime.h" namespace Luau { -using Kind = Variant; - -template -const T* get(const Kind& kind) -{ - return get_if(&kind); -} +struct TypeFunctionContext; using TypeFunctionKind = Variant; diff --git a/Analysis/include/Luau/TypeFwd.h b/Analysis/include/Luau/TypeFwd.h index 895feaf0..07b3e1e5 100644 --- a/Analysis/include/Luau/TypeFwd.h +++ b/Analysis/include/Luau/TypeFwd.h @@ -56,4 +56,10 @@ struct BuiltinTypes; using TypeOrPack = Variant; +struct TypeFunctionType; +using TypeFunctionTypeId = const TypeFunctionType*; + +struct TypeFunctionTypePackVar; +using TypeFunctionTypePackId = const TypeFunctionTypePackVar*; + } // namespace Luau diff --git a/Analysis/include/Luau/UserDefinedTypeFunction.h b/Analysis/include/Luau/UserDefinedTypeFunction.h new file mode 100644 index 00000000..91e9cac2 --- /dev/null +++ b/Analysis/include/Luau/UserDefinedTypeFunction.h @@ -0,0 +1,17 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/TypeFunction.h" +#include "Luau/TypeFwd.h" + +namespace Luau +{ + +TypeFunctionReductionResult userDefinedTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +); + +} \ No newline at end of file diff --git a/Analysis/src/AutocompleteCore.cpp b/Analysis/src/AutocompleteCore.cpp index 3ac8915e..49a22fa2 100644 --- a/Analysis/src/AutocompleteCore.cpp +++ b/Analysis/src/AutocompleteCore.cpp @@ -26,7 +26,7 @@ LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAGVARIABLE(DebugLuauMagicVariableNames) LUAU_FASTFLAGVARIABLE(LuauAutocompleteMissingFollows) -LUAU_FASTFLAG(LuauImplicitTableIndexerKeys2) +LUAU_FASTFLAG(LuauImplicitTableIndexerKeys3) static const std::unordered_set kStatementStartingKeywords = {"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; @@ -578,16 +578,40 @@ static void autocompleteStringSingleton(TypeId ty, bool addQuotes, AstNode* node ty = follow(ty); - if (auto ss = get(get(ty))) + if (FFlag::LuauImplicitTableIndexerKeys3) { - result[formatKey(ss->value)] = AutocompleteEntry{AutocompleteEntryKind::String, ty, false, false, TypeCorrectKind::Correct}; - } - else if (auto uty = get(ty)) - { - for (auto el : uty) + if (auto ss = get(get(ty))) { - if (auto ss = get(get(el))) - result[formatKey(ss->value)] = AutocompleteEntry{AutocompleteEntryKind::String, ty, false, false, TypeCorrectKind::Correct}; + // This is purposefully `try_emplace` as we don't want to override any existing entries. + result.try_emplace(formatKey(ss->value), AutocompleteEntry{AutocompleteEntryKind::String, ty, false, false, TypeCorrectKind::Correct}); + } + else if (auto uty = get(ty)) + { + for (auto el : uty) + { + if (auto ss = get(get(el))) + { + // This is purposefully `try_emplace` as we don't want to override any existing entries. + result.try_emplace( + formatKey(ss->value), AutocompleteEntry{AutocompleteEntryKind::String, ty, false, false, TypeCorrectKind::Correct} + ); + } + } + } + } + else + { + if (auto ss = get(get(ty))) + { + result[formatKey(ss->value)] = AutocompleteEntry{AutocompleteEntryKind::String, ty, false, false, TypeCorrectKind::Correct}; + } + else if (auto uty = get(ty)) + { + for (auto el : uty) + { + if (auto ss = get(get(el))) + result[formatKey(ss->value)] = AutocompleteEntry{AutocompleteEntryKind::String, ty, false, false, TypeCorrectKind::Correct}; + } } } }; @@ -2030,7 +2054,7 @@ AutocompleteResult autocomplete_( { AutocompleteEntryMap result; - if (!FFlag::LuauImplicitTableIndexerKeys2) + if (!FFlag::LuauImplicitTableIndexerKeys3) { if (auto it = module->astExpectedTypes.find(node->asExpr())) autocompleteStringSingleton(*it, false, node, position, result); @@ -2053,7 +2077,7 @@ AutocompleteResult autocomplete_( } } - if (FFlag::LuauImplicitTableIndexerKeys2) + if (FFlag::LuauImplicitTableIndexerKeys3) { if (auto it = module->astExpectedTypes.find(node->asExpr())) autocompleteStringSingleton(*it, false, node, position, result); diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 81f1c738..f336c482 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -2,6 +2,7 @@ #include "Luau/BuiltinDefinitions.h" #include "Luau/Ast.h" +#include "Luau/BuiltinTypeFunctions.h" #include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/ConstraintGenerator.h" @@ -35,7 +36,6 @@ LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauRemoveTypeCallsForReadWriteProps) LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType3) LUAU_FASTFLAGVARIABLE(LuauStringFormatImprovements) -LUAU_FASTFLAGVARIABLE(LuauMagicFreezeCheckBlocked2) LUAU_FASTFLAGVARIABLE(LuauUpdateSetMetatableTypeSignature) LUAU_FASTFLAGVARIABLE(LuauUpdateGetMetatableTypeSignature) LUAU_FASTFLAG(LuauUseWorkspacePropToChooseSolver) @@ -1781,15 +1781,12 @@ bool MagicFreeze::infer(const MagicFunctionCallContext& context) std::optional resultDef = dfg->getDefOptional(targetExpr); std::optional resultTy = resultDef ? scope->lookup(*resultDef) : std::nullopt; - if (FFlag::LuauMagicFreezeCheckBlocked2) + if (resultTy && !get(follow(resultTy))) { - if (resultTy && !get(follow(resultTy))) - { - // If there's an existing result type, but it's _not_ blocked, then - // we aren't type stating this builtin and should fall back to - // regular inference. - return false; - } + // If there's an existing result type, but it's _not_ blocked, then + // we aren't type stating this builtin and should fall back to + // regular inference. + return false; } std::optional frozenType = freezeTable(inputType, context); diff --git a/Analysis/src/BuiltinTypeFunctions.cpp b/Analysis/src/BuiltinTypeFunctions.cpp new file mode 100644 index 00000000..d4a2bf2f --- /dev/null +++ b/Analysis/src/BuiltinTypeFunctions.cpp @@ -0,0 +1,2828 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/BuiltinTypeFunctions.h" + +#include "Luau/Common.h" +#include "Luau/ConstraintSolver.h" +#include "Luau/Instantiation.h" +#include "Luau/OverloadResolution.h" +#include "Luau/Scope.h" +#include "Luau/Simplify.h" +#include "Luau/Subtyping.h" +#include "Luau/Type.h" +#include "Luau/TypeArena.h" +#include "Luau/TypeFunctionRuntimeBuilder.h" +#include "Luau/TypeUtils.h" +#include "Luau/Unifier2.h" +#include "Luau/UserDefinedTypeFunction.h" +#include "Luau/VisitType.h" + +LUAU_FASTFLAG(LuauNotAllBinaryTypeFunsHaveDefaults) +LUAU_FASTFLAG(LuauEmptyStringInKeyOf) +LUAU_FASTFLAG(LuauRemoveTypeCallsForReadWriteProps) +LUAU_FASTFLAG(LuauUpdateGetMetatableTypeSignature) +LUAU_FASTFLAG(LuauUserTypeFunctionAliases) +LUAU_FASTFLAG(LuauRefineTablesWithReadType) +LUAU_FASTFLAG(LuauAvoidExcessiveTypeCopying) +LUAU_FASTFLAG(LuauOccursCheckForRefinement) +LUAU_FASTFLAG(LuauEagerGeneralization4) +LUAU_FASTFLAG(DebugLuauEqSatSimplification) +LUAU_FASTFLAG(LuauStuckTypeFunctionsStillDispatch) +LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) + +LUAU_FASTFLAGVARIABLE(LuauRefineNoRefineAlways) + +namespace Luau +{ + +namespace +{ + +template +std::optional> tryDistributeTypeFunctionApp( + F f, + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx, + Args&&... args +) +{ + // op (a | b) (c | d) ~ (op a (c | d)) | (op b (c | d)) ~ (op a c) | (op a d) | (op b c) | (op b d) + Reduction reductionStatus = Reduction::MaybeOk; + std::vector blockedTypes; + std::vector results; + size_t cartesianProductSize = 1; + + const UnionType* firstUnion = nullptr; + size_t unionIndex = 0; + + std::vector arguments = typeParams; + for (size_t i = 0; i < arguments.size(); ++i) + { + const UnionType* ut = get(follow(arguments[i])); + if (!ut) + continue; + + // We want to find the first union type in the set of arguments to distribute that one and only that one union. + // The function `f` we have is recursive, so `arguments[unionIndex]` will be updated in-place for each option in + // the union we've found in this context, so that index will no longer be a union type. Any other arguments at + // index + 1 or after will instead be distributed, if those are a union, which will be subjected to the same rules. + if (!firstUnion && ut) + { + firstUnion = ut; + unionIndex = i; + } + + cartesianProductSize *= std::distance(begin(ut), end(ut)); + + // TODO: We'd like to report that the type function application is too complex here. + if (size_t(DFInt::LuauTypeFamilyApplicationCartesianProductLimit) <= cartesianProductSize) + return {{std::nullopt, Reduction::Erroneous, {}, {}}}; + } + + if (!firstUnion) + { + // If we couldn't find any union type argument, we're not distributing. + return std::nullopt; + } + + for (TypeId option : firstUnion) + { + arguments[unionIndex] = option; + + TypeFunctionReductionResult result = f(instance, arguments, packParams, ctx, args...); // NOLINT + blockedTypes.insert(blockedTypes.end(), result.blockedTypes.begin(), result.blockedTypes.end()); + if (result.reductionStatus != Reduction::MaybeOk) + reductionStatus = result.reductionStatus; + + if (reductionStatus != Reduction::MaybeOk || !result.result) + break; + else + results.push_back(*result.result); + } + + if (reductionStatus != Reduction::MaybeOk || !blockedTypes.empty()) + return {{std::nullopt, reductionStatus, std::move(blockedTypes), {}}}; + + if (!results.empty()) + { + if (results.size() == 1) + return {{results[0], Reduction::MaybeOk, {}, {}}}; + + TypeId resultTy = ctx->arena->addType(TypeFunctionInstanceType{ + NotNull{&builtinTypeFunctions().unionFunc}, + std::move(results), + {}, + }); + + if (ctx->solver) + ctx->pushConstraint(ReduceConstraint{resultTy}); + + return {{resultTy, Reduction::MaybeOk, {}, {}}}; + } + + return std::nullopt; +} + +} + +TypeFunctionReductionResult notTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("not type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId ty = follow(typeParams.at(0)); + + if (ty == instance) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + if (isPending(ty, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {ty}, {}}; + + if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx)) + return *result; + + // `not` operates on anything and returns a `boolean` always. + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; +} + +TypeFunctionReductionResult lenTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("len type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId operandTy = follow(typeParams.at(0)); + + if (operandTy == instance) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + // check to see if the operand type is resolved enough, and wait to reduce if not + // the use of `typeFromNormal` later necessitates blocking on local types. + if (isPending(operandTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; + + std::shared_ptr normTy = ctx->normalizer->normalize(operandTy); + NormalizationResult inhabited = ctx->normalizer->isInhabited(normTy.get()); + + // if the type failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normTy || inhabited == NormalizationResult::HitLimits) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // if the operand type is error suppressing, we can immediately reduce to `number`. + if (normTy->shouldSuppressErrors()) + return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; + + // # always returns a number, even if its operand is never. + // if we're checking the length of a string, that works! + if (inhabited == NormalizationResult::False || normTy->isSubtypeOfString()) + return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; + + // we use the normalized operand here in case there was an intersection or union. + TypeId normalizedOperand = follow(ctx->normalizer->typeFromNormal(*normTy)); + if (normTy->hasTopTable() || get(normalizedOperand)) + return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; + + if (auto result = tryDistributeTypeFunctionApp(lenTypeFunction, instance, typeParams, packParams, ctx)) + return *result; + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, operandTy, "__len", Location{}); + if (!mmType) + { + // If we have a metatable type with no __len, this means we still have a table with default length function + if (get(normalizedOperand)) + return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; + + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + + mmType = follow(*mmType); + if (isPending(*mmType, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; + + const FunctionType* mmFtv = get(*mmType); + if (!mmFtv) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); + if (!instantiatedMmType) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); + if (!instantiatedMmFtv) + return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; + + TypePackId inferredArgPack = ctx->arena->addTypePack({operandTy}); + Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; + if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) + return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed + + Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + // `len` must return a `number`. + return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; +} + +TypeFunctionReductionResult unmTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("unm type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId operandTy = follow(typeParams.at(0)); + + if (operandTy == instance) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + // check to see if the operand type is resolved enough, and wait to reduce if not + if (isPending(operandTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; + + if (FFlag::LuauEagerGeneralization4) + operandTy = follow(operandTy); + + std::shared_ptr normTy = ctx->normalizer->normalize(operandTy); + + // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normTy) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // if the operand is error suppressing, we can just go ahead and reduce. + if (normTy->shouldSuppressErrors()) + return {operandTy, Reduction::MaybeOk, {}, {}}; + + // if we have a `never`, we can never observe that the operation didn't work. + if (is(operandTy)) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + // If the type is exactly `number`, we can reduce now. + if (normTy->isExactlyNumber()) + return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; + + if (auto result = tryDistributeTypeFunctionApp(unmTypeFunction, instance, typeParams, packParams, ctx)) + return *result; + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, operandTy, "__unm", Location{}); + if (!mmType) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + mmType = follow(*mmType); + if (isPending(*mmType, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; + + const FunctionType* mmFtv = get(*mmType); + if (!mmFtv) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); + if (!instantiatedMmType) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); + if (!instantiatedMmFtv) + return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; + + TypePackId inferredArgPack = ctx->arena->addTypePack({operandTy}); + Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; + if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) + return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed + + Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + if (std::optional ret = first(instantiatedMmFtv->retTypes)) + return {ret, Reduction::MaybeOk, {}, {}}; + else + return {std::nullopt, Reduction::Erroneous, {}, {}}; +} + +TypeFunctionContext::TypeFunctionContext(NotNull cs, NotNull scope, NotNull constraint) + : arena(cs->arena) + , builtins(cs->builtinTypes) + , scope(scope) + , simplifier(cs->simplifier) + , normalizer(cs->normalizer) + , typeFunctionRuntime(cs->typeFunctionRuntime) + , ice(NotNull{&cs->iceReporter}) + , limits(NotNull{&cs->limits}) + , solver(cs.get()) + , constraint(constraint.get()) +{ +} + +NotNull TypeFunctionContext::pushConstraint(ConstraintV&& c) const +{ + LUAU_ASSERT(solver); + NotNull newConstraint = solver->pushConstraint(scope, constraint ? constraint->location : Location{}, std::move(c)); + + // Every constraint that is blocked on the current constraint must also be + // blocked on this new one. + if (constraint) + solver->inheritBlocks(NotNull{constraint}, newConstraint); + + return newConstraint; +} + +TypeFunctionReductionResult numericBinopTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx, + const std::string metamethod +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId lhsTy = follow(typeParams.at(0)); + TypeId rhsTy = follow(typeParams.at(1)); + + // isPending of `lhsTy` or `rhsTy` would return true, even if it cycles. We want a different answer for that. + if (lhsTy == instance || rhsTy == instance) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + // if we have a `never`, we can never observe that the math operator is unreachable. + if (is(lhsTy) || is(rhsTy)) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + const Location location = ctx->constraint ? ctx->constraint->location : Location{}; + + // check to see if both operand types are resolved enough, and wait to reduce if not + if (isPending(lhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; + else if (isPending(rhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; + + // TODO: Normalization needs to remove cyclic type functions from a `NormalizedType`. + std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); + std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); + + // if either failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normLhsTy || !normRhsTy) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // if one of the types is error suppressing, we can reduce to `any` since we should suppress errors in the result of the usage. + if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) + return {ctx->builtins->anyType, Reduction::MaybeOk, {}, {}}; + + // if we're adding two `number` types, the result is `number`. + if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber()) + return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; + + if (auto result = tryDistributeTypeFunctionApp(numericBinopTypeFunction, instance, typeParams, packParams, ctx, metamethod)) + return *result; + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, metamethod, location); + bool reversed = false; + if (!mmType) + { + mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, metamethod, location); + reversed = true; + } + + if (!mmType) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + mmType = follow(*mmType); + if (isPending(*mmType, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; + + TypePackId argPack = ctx->arena->addTypePack({lhsTy, rhsTy}); + SolveResult solveResult; + + if (!reversed) + solveResult = solveFunctionCall( + ctx->arena, + ctx->builtins, + ctx->simplifier, + ctx->normalizer, + ctx->typeFunctionRuntime, + ctx->ice, + ctx->limits, + ctx->scope, + location, + *mmType, + argPack + ); + else + { + TypePack* p = getMutable(argPack); + std::swap(p->head.front(), p->head.back()); + solveResult = solveFunctionCall( + ctx->arena, + ctx->builtins, + ctx->simplifier, + ctx->normalizer, + ctx->typeFunctionRuntime, + ctx->ice, + ctx->limits, + ctx->scope, + location, + *mmType, + argPack + ); + } + + if (!solveResult.typePackId.has_value()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); + if (extracted.head.empty()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + return {extracted.head.front(), Reduction::MaybeOk, {}, {}}; +} + +TypeFunctionReductionResult addTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("add type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__add"); +} + +TypeFunctionReductionResult subTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("sub type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__sub"); +} + +TypeFunctionReductionResult mulTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("mul type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__mul"); +} + +TypeFunctionReductionResult divTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("div type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__div"); +} + +TypeFunctionReductionResult idivTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("integer div type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__idiv"); +} + +TypeFunctionReductionResult powTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("pow type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__pow"); +} + +TypeFunctionReductionResult modTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("modulo type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__mod"); +} + +TypeFunctionReductionResult concatTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("concat type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId lhsTy = follow(typeParams.at(0)); + TypeId rhsTy = follow(typeParams.at(1)); + + // isPending of `lhsTy` or `rhsTy` would return true, even if it cycles. We want a different answer for that. + if (lhsTy == instance || rhsTy == instance) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + // check to see if both operand types are resolved enough, and wait to reduce if not + if (isPending(lhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; + else if (isPending(rhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; + + std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); + std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); + + // if either failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normLhsTy || !normRhsTy) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // if one of the types is error suppressing, we can reduce to `any` since we should suppress errors in the result of the usage. + if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) + return {ctx->builtins->anyType, Reduction::MaybeOk, {}, {}}; + + // if we have a `never`, we can never observe that the operator didn't work. + if (is(lhsTy) || is(rhsTy)) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + // if we're concatenating two elements that are either strings or numbers, the result is `string`. + if ((normLhsTy->isSubtypeOfString() || normLhsTy->isExactlyNumber()) && (normRhsTy->isSubtypeOfString() || normRhsTy->isExactlyNumber())) + return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; + + if (auto result = tryDistributeTypeFunctionApp(concatTypeFunction, instance, typeParams, packParams, ctx)) + return *result; + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, "__concat", Location{}); + bool reversed = false; + if (!mmType) + { + mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, "__concat", Location{}); + reversed = true; + } + + if (!mmType) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + mmType = follow(*mmType); + if (isPending(*mmType, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; + + const FunctionType* mmFtv = get(*mmType); + if (!mmFtv) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); + if (!instantiatedMmType) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); + if (!instantiatedMmFtv) + return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; + + std::vector inferredArgs; + if (!reversed) + inferredArgs = {lhsTy, rhsTy}; + else + inferredArgs = {rhsTy, lhsTy}; + + TypePackId inferredArgPack = ctx->arena->addTypePack(std::move(inferredArgs)); + Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; + if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) + return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed + + Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; +} + +TypeFunctionReductionResult andTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("and type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId lhsTy = follow(typeParams.at(0)); + TypeId rhsTy = follow(typeParams.at(1)); + + // t1 = and ~> lhs + if (follow(rhsTy) == instance && lhsTy != rhsTy) + return {lhsTy, Reduction::MaybeOk, {}, {}}; + // t1 = and ~> rhs + if (follow(lhsTy) == instance && lhsTy != rhsTy) + return {rhsTy, Reduction::MaybeOk, {}, {}}; + + // check to see if both operand types are resolved enough, and wait to reduce if not + if (isPending(lhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; + else if (isPending(rhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; + + // And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is truthy. + SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->falsyType); + SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); + std::vector blockedTypes{}; + for (auto ty : filteredLhs.blockedTypes) + blockedTypes.push_back(ty); + for (auto ty : overallResult.blockedTypes) + blockedTypes.push_back(ty); + return {overallResult.result, Reduction::MaybeOk, std::move(blockedTypes), {}}; +} + +TypeFunctionReductionResult orTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("or type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId lhsTy = follow(typeParams.at(0)); + TypeId rhsTy = follow(typeParams.at(1)); + + // t1 = or ~> lhs + if (follow(rhsTy) == instance && lhsTy != rhsTy) + return {lhsTy, Reduction::MaybeOk, {}, {}}; + // t1 = or ~> rhs + if (follow(lhsTy) == instance && lhsTy != rhsTy) + return {rhsTy, Reduction::MaybeOk, {}, {}}; + + // check to see if both operand types are resolved enough, and wait to reduce if not + if (FFlag::LuauEagerGeneralization4) + { + if (is(lhsTy)) + return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; + else if (is(rhsTy)) + return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; + } + else + { + if (isPending(lhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; + else if (isPending(rhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; + } + + // Or evalutes to the LHS type if the LHS is truthy, and the RHS type if LHS is falsy. + SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->truthyType); + SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); + std::vector blockedTypes{}; + for (auto ty : filteredLhs.blockedTypes) + blockedTypes.push_back(ty); + for (auto ty : overallResult.blockedTypes) + blockedTypes.push_back(ty); + return {overallResult.result, Reduction::MaybeOk, std::move(blockedTypes), {}}; +} + +static TypeFunctionReductionResult comparisonTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx, + const std::string metamethod +) +{ + + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId lhsTy = follow(typeParams.at(0)); + TypeId rhsTy = follow(typeParams.at(1)); + + if (lhsTy == instance || rhsTy == instance) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + if (FFlag::LuauEagerGeneralization4) + { + if (is(lhsTy)) + return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; + else if (is(rhsTy)) + return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; + } + else + { + if (isPending(lhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; + else if (isPending(rhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; + } + + // Algebra Reduction Rules for comparison type functions + // Note that comparing to never tells you nothing about the other operand + // lt< 'a , never> -> continue + // lt< never, 'a> -> continue + // lt< 'a, t> -> 'a is t - we'll solve the constraint, return and solve lt -> bool + // lt< t, 'a> -> same as above + bool canSubmitConstraint = ctx->solver && ctx->constraint; + bool lhsFree = get(lhsTy) != nullptr; + bool rhsFree = get(rhsTy) != nullptr; + if (canSubmitConstraint) + { + // Implement injective type functions for comparison type functions + // lt implies t is number + // lt implies t is number + if (lhsFree && isNumber(rhsTy)) + emplaceType(asMutable(lhsTy), ctx->builtins->numberType); + else if (rhsFree && isNumber(lhsTy)) + emplaceType(asMutable(rhsTy), ctx->builtins->numberType); + } + + // The above might have caused the operand types to be rebound, we need to follow them again + lhsTy = follow(lhsTy); + rhsTy = follow(rhsTy); + + // check to see if both operand types are resolved enough, and wait to reduce if not + + std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); + std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); + NormalizationResult lhsInhabited = ctx->normalizer->isInhabited(normLhsTy.get()); + NormalizationResult rhsInhabited = ctx->normalizer->isInhabited(normRhsTy.get()); + + // if either failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normLhsTy || !normRhsTy || lhsInhabited == NormalizationResult::HitLimits || rhsInhabited == NormalizationResult::HitLimits) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // if one of the types is error suppressing, we can just go ahead and reduce. + if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; + + // if we have an uninhabited type (e.g. `never`), we can never observe that the comparison didn't work. + if (lhsInhabited == NormalizationResult::False || rhsInhabited == NormalizationResult::False) + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; + + // If both types are some strict subset of `string`, we can reduce now. + if (normLhsTy->isSubtypeOfString() && normRhsTy->isSubtypeOfString()) + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; + + // If both types are exactly `number`, we can reduce now. + if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber()) + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; + + if (auto result = tryDistributeTypeFunctionApp(comparisonTypeFunction, instance, typeParams, packParams, ctx, metamethod)) + return *result; + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, metamethod, Location{}); + if (!mmType) + mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, metamethod, Location{}); + + if (!mmType) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + mmType = follow(*mmType); + if (isPending(*mmType, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; + + const FunctionType* mmFtv = get(*mmType); + if (!mmFtv) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); + if (!instantiatedMmType) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); + if (!instantiatedMmFtv) + return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; + + TypePackId inferredArgPack = ctx->arena->addTypePack({lhsTy, rhsTy}); + Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; + if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) + return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed + + Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; +} + +TypeFunctionReductionResult ltTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("lt type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return comparisonTypeFunction(instance, typeParams, packParams, ctx, "__lt"); +} + +TypeFunctionReductionResult leTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("le type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return comparisonTypeFunction(instance, typeParams, packParams, ctx, "__le"); +} + +TypeFunctionReductionResult eqTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("eq type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId lhsTy = follow(typeParams.at(0)); + TypeId rhsTy = follow(typeParams.at(1)); + + // check to see if both operand types are resolved enough, and wait to reduce if not + if (isPending(lhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; + else if (isPending(rhsTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; + + std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); + std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); + NormalizationResult lhsInhabited = ctx->normalizer->isInhabited(normLhsTy.get()); + NormalizationResult rhsInhabited = ctx->normalizer->isInhabited(normRhsTy.get()); + + // if either failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normLhsTy || !normRhsTy || lhsInhabited == NormalizationResult::HitLimits || rhsInhabited == NormalizationResult::HitLimits) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // if one of the types is error suppressing, we can just go ahead and reduce. + if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; + + // if we have a `never`, we can never observe that the comparison didn't work. + if (lhsInhabited == NormalizationResult::False || rhsInhabited == NormalizationResult::False) + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, "__eq", Location{}); + if (!mmType) + mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, "__eq", Location{}); + + // if neither type has a metatable entry for `__eq`, then we'll check for inhabitance of the intersection! + NormalizationResult intersectInhabited = ctx->normalizer->isIntersectionInhabited(lhsTy, rhsTy); + if (!mmType) + { + if (intersectInhabited == NormalizationResult::True) + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; // if it's inhabited, everything is okay! + + // we might be in a case where we still want to accept the comparison... + if (intersectInhabited == NormalizationResult::False) + { + // if they're both subtypes of `string` but have no common intersection, the comparison is allowed but always `false`. + if (normLhsTy->isSubtypeOfString() && normRhsTy->isSubtypeOfString()) + return {ctx->builtins->falseType, Reduction::MaybeOk, {}, {}}; + + // if they're both subtypes of `boolean` but have no common intersection, the comparison is allowed but always `false`. + if (normLhsTy->isSubtypeOfBooleans() && normRhsTy->isSubtypeOfBooleans()) + return {ctx->builtins->falseType, Reduction::MaybeOk, {}, {}}; + } + + return {std::nullopt, Reduction::Erroneous, {}, {}}; // if it's not, then this type function is irreducible! + } + + mmType = follow(*mmType); + if (isPending(*mmType, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; + + const FunctionType* mmFtv = get(*mmType); + if (!mmFtv) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); + if (!instantiatedMmType) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); + if (!instantiatedMmFtv) + return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; + + TypePackId inferredArgPack = ctx->arena->addTypePack({lhsTy, rhsTy}); + Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; + if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) + return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed + + Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; +} + +// Collect types that prevent us from reducing a particular refinement. +struct FindRefinementBlockers : TypeOnceVisitor +{ + DenseHashSet found{nullptr}; + bool visit(TypeId ty, const BlockedType&) override + { + found.insert(ty); + return false; + } + + bool visit(TypeId ty, const PendingExpansionType&) override + { + found.insert(ty); + return false; + } + + bool visit(TypeId ty, const ExternType&) override + { + return false; + } +}; + +struct ContainsRefinableType : TypeOnceVisitor +{ + bool found = false; + ContainsRefinableType() + : TypeOnceVisitor(/* skipBoundTypes */ true) + { + } + + + bool visit(TypeId ty) override + { + // Default case: if we find *some* type that's worth refining against, + // then we can claim that this type contains a refineable type. + found = true; + return false; + } + + bool visit(TypeId Ty, const NoRefineType&) override + { + // No refine types aren't interesting + return false; + } + + bool visit(TypeId ty, const TableType&) override + { + return !found; + } + bool visit(TypeId ty, const MetatableType&) override + { + return !found; + } + bool visit(TypeId ty, const FunctionType&) override + { + return !found; + } + bool visit(TypeId ty, const UnionType&) override + { + return !found; + } + bool visit(TypeId ty, const IntersectionType&) override + { + return !found; + } + bool visit(TypeId ty, const NegationType&) override + { + return !found; + } +}; + +namespace +{ + +bool isTruthyOrFalsyType(TypeId ty) +{ + ty = follow(ty); + return isApproximatelyTruthyType(ty) || isApproximatelyFalsyType(ty); +} + +struct RefineTypeScrubber : public Substitution +{ + NotNull ctx; + TypeId needle; + + explicit RefineTypeScrubber(NotNull ctx, TypeId needle) + : Substitution(ctx->arena) + , ctx{ctx} + , needle{needle} + { + } + + bool isDirty(TypePackId tp) override + { + return false; + } + + bool ignoreChildren(TypePackId tp) override + { + return false; + } + + TypePackId clean(TypePackId tp) override + { + return tp; + } + + bool isDirty(TypeId ty) override + { + if (auto ut = get(ty)) + { + for (auto option : ut) + { + if (option == needle) + return true; + } + } + else if (auto it = get(ty)) + { + for (auto part : it) + { + if (part == needle) + return true; + } + } + return false; + } + + bool ignoreChildren(TypeId ty) override + { + return !is(ty); + } + + TypeId clean(TypeId ty) override + { + // NOTE: this feels pretty similar to other places where we try to + // filter over a set type, may be worth combining those in the future. + if (auto ut = get(ty)) + { + TypeIds newOptions; + for (auto option : ut) + { + if (option != needle && !is(option)) + newOptions.insert(option); + } + if (newOptions.empty()) + return ctx->builtins->neverType; + else if (newOptions.size() == 1) + return *newOptions.begin(); + else + return ctx->arena->addType(UnionType{newOptions.take()}); + } + else if (auto it = get(ty)) + { + TypeIds newParts; + for (auto part : it) + { + if (part != needle && !is(part)) + newParts.insert(part); + } + if (newParts.empty()) + return ctx->builtins->unknownType; + else if (newParts.size() == 1) + return *newParts.begin(); + else + return ctx->arena->addType(IntersectionType{newParts.take()}); + } + return ty; + } + +}; + +bool occurs(TypeId haystack, TypeId needle, DenseHashSet& seen) +{ + if (needle == haystack) + return true; + + if (seen.contains(haystack)) + return false; + + seen.insert(haystack); + + if (auto ut = get(haystack)) + { + for (auto option : ut) + if (occurs(option, needle, seen)) + return true; + } + + if (auto it = get(haystack)) + { + for (auto part : it) + if (occurs(part, needle, seen)) + return true; + } + + return false; +} + +bool occurs(TypeId haystack, TypeId needle) +{ + DenseHashSet seen{nullptr}; + return occurs(haystack, needle, seen); +} + +} // namespace + +TypeFunctionReductionResult refineTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() < 2 || !packParams.empty()) + { + ctx->ice->ice("refine type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId targetTy = follow(typeParams.at(0)); + + if (FFlag::LuauOccursCheckForRefinement) + { + // If we end up minting a refine type like: + // + // t1 where t1 = refine + // + // This can create a degenerate set type such as: + // + // t1 where t1 = (T | t1) & Y + // + // Instead, we can clip the recursive part: + // + // t1 where t1 = refine => refine + if (!FFlag::LuauAvoidExcessiveTypeCopying || occurs(targetTy, instance)) + { + RefineTypeScrubber rts{ctx, instance}; + if (auto result = rts.substitute(targetTy)) + targetTy = *result; + } + } + + std::vector discriminantTypes; + for (size_t i = 1; i < typeParams.size(); i++) + discriminantTypes.push_back(follow(typeParams.at(i))); + + if (FFlag::LuauRefineNoRefineAlways) + { + bool hasAnyRealRefinements = false; + for (auto discriminant : discriminantTypes) + { + // If the discriminant type is only: + // - The `*no-refine*` type or, + // - tables, metatables, unions, intersections, functions, or negations _containing_ `*no-refine*`. + // There's no point in refining against it. + ContainsRefinableType crt; + crt.traverse(discriminant); + + hasAnyRealRefinements = hasAnyRealRefinements || crt.found; + } + + // if we don't have any real refinements, i.e. they're all `*no-refine*`, then we can reduce immediately. + if (!hasAnyRealRefinements) + return {targetTy, {}}; + } + + const bool targetIsPending = FFlag::LuauEagerGeneralization4 ? is(targetTy) + : isPending(targetTy, ctx->solver); + + // check to see if both operand types are resolved enough, and wait to reduce if not + if (targetIsPending) + return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; + else + { + for (auto t : discriminantTypes) + { + if (isPending(t, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {t}, {}}; + } + } + + // If we have a blocked type in the target, we *could* potentially + // refine it, but more likely we end up with some type explosion in + // normalization. + FindRefinementBlockers frb; + frb.traverse(targetTy); + if (!frb.found.empty()) + return {std::nullopt, Reduction::MaybeOk, {frb.found.begin(), frb.found.end()}, {}}; + + // Refine a target type and a discriminant one at a time. + // Returns result : TypeId, toBlockOn : vector + auto stepRefine = [&ctx](TypeId target, TypeId discriminant) -> std::pair> + { + std::vector toBlock; + // we need a more complex check for blocking on the discriminant in particular + FindRefinementBlockers frb; + frb.traverse(discriminant); + + if (!frb.found.empty()) + return {nullptr, {frb.found.begin(), frb.found.end()}}; + + if (FFlag::DebugLuauEqSatSimplification) + { + auto simplifyResult = eqSatSimplify(ctx->simplifier, ctx->arena->addType(IntersectionType{{target, discriminant}})); + if (simplifyResult) + { + if (ctx->solver) + { + for (TypeId newTf : simplifyResult->newTypeFunctions) + ctx->pushConstraint(ReduceConstraint{newTf}); + } + + return {simplifyResult->result, {}}; + } + else + return {nullptr, {}}; + } + else + { + // FFlag::LuauRefineNoRefineAlways moves this check upwards so that it runs even if the thing being refined is pending. + if (!FFlag::LuauRefineNoRefineAlways) + { + // If the discriminant type is only: + // - The `*no-refine*` type or, + // - tables, metatables, unions, intersections, functions, or negations _containing_ `*no-refine*`. + // There's no point in refining against it. + ContainsRefinableType crt; + crt.traverse(discriminant); + if (!crt.found) + return {target, {}}; + } + + if (FFlag::LuauRefineTablesWithReadType) + { + if (auto ty = intersectWithSimpleDiscriminant(ctx->builtins, ctx->arena, target, discriminant)) + return {*ty, {}}; + } + + // NOTE: This block causes us to refine too early in some cases. + if (auto negation = get(discriminant)) + { + if (auto primitive = get(follow(negation->ty)); primitive && primitive->type == PrimitiveType::NilType) + { + SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, target, discriminant); + return {result.result, {}}; + } + } + + // If the target type is a table, then simplification already implements the logic to deal with refinements properly since the + // type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type. + // We also fire for simple discriminants such as false? and ~(false?): the falsy and truthy types respectively. + if (is(target) || isTruthyOrFalsyType(discriminant)) + { + SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, target, discriminant); + if (FFlag::LuauEagerGeneralization4) + { + // Simplification considers free and generic types to be + // 'blocking', but that's not suitable for refine<>. + // + // If we are only blocked on those types, we consider + // the simplification a success and reduce. + if (std::all_of( + begin(result.blockedTypes), + end(result.blockedTypes), + [](auto&& v) + { + return is(follow(v)); + } + )) + { + return {result.result, {}}; + } + else + return {nullptr, {result.blockedTypes.begin(), result.blockedTypes.end()}}; + } + else + { + if (!result.blockedTypes.empty()) + return {nullptr, {result.blockedTypes.begin(), result.blockedTypes.end()}}; + } + return {result.result, {}}; + } + + + // In the general case, we'll still use normalization though. + TypeId intersection = ctx->arena->addType(IntersectionType{{target, discriminant}}); + std::shared_ptr normIntersection = ctx->normalizer->normalize(intersection); + std::shared_ptr normType = ctx->normalizer->normalize(target); + + // if the intersection failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normIntersection || !normType) + return {nullptr, {}}; + + TypeId resultTy = ctx->normalizer->typeFromNormal(*normIntersection); + // include the error type if the target type is error-suppressing and the intersection we computed is not + if (normType->shouldSuppressErrors() && !normIntersection->shouldSuppressErrors()) + resultTy = ctx->arena->addType(UnionType{{resultTy, ctx->builtins->errorType}}); + + return {resultTy, {}}; + } + + }; + + // refine target with each discriminant type in sequence (reverse of insertion order) + // If we cannot proceed, block. If all discriminant types refine successfully, return + // the result + TypeId target = targetTy; + while (!discriminantTypes.empty()) + { + TypeId discriminant = discriminantTypes.back(); + auto [refined, blocked] = stepRefine(target, discriminant); + + if (blocked.empty() && refined == nullptr) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + if (!blocked.empty()) + return {std::nullopt, Reduction::MaybeOk, blocked, {}}; + + target = refined; + discriminantTypes.pop_back(); + } + return {target, Reduction::MaybeOk, {}, {}}; +} + +TypeFunctionReductionResult singletonTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("singleton type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId type = follow(typeParams.at(0)); + + // check to see if both operand types are resolved enough, and wait to reduce if not + if (isPending(type, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {type}, {}}; + + TypeId followed = type; + // we want to follow through a negation here as well. + if (auto negation = get(followed)) + followed = follow(negation->ty); + + // if we have a singleton type or `nil`, which is its own singleton type... + if (get(followed) || isNil(followed)) + return {type, Reduction::MaybeOk, {}, {}}; + + // otherwise, we'll return the top type, `unknown`. + return {ctx->builtins->unknownType, Reduction::MaybeOk, {}, {}}; +} + +struct CollectUnionTypeOptions : TypeOnceVisitor +{ + NotNull ctx; + DenseHashSet options{nullptr}; + DenseHashSet blockingTypes{nullptr}; + + explicit CollectUnionTypeOptions(NotNull ctx) + : TypeOnceVisitor(/* skipBoundTypes */ true) + , ctx(ctx) + { + } + + bool visit(TypeId ty) override + { + options.insert(ty); + if (isPending(ty, ctx->solver)) + blockingTypes.insert(ty); + return false; + } + + bool visit(TypePackId tp) override + { + return false; + } + + bool visit(TypeId ty, const UnionType& ut) override + { + // If we have something like: + // + // union + // + // We probably just want to consider this to be the same as + // + // union + return true; + } + + bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override + { + if (tfit.function->name != builtinTypeFunctions().unionFunc.name) + { + options.insert(ty); + blockingTypes.insert(ty); + return false; + } + return true; + } +}; + +TypeFunctionReductionResult unionTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (!packParams.empty()) + { + ctx->ice->ice("union type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + // if we only have one parameter, there's nothing to do. + if (typeParams.size() == 1) + return {follow(typeParams[0]), Reduction::MaybeOk, {}, {}}; + + + CollectUnionTypeOptions collector{ctx}; + collector.traverse(instance); + + if (!collector.blockingTypes.empty()) + { + std::vector blockingTypes{collector.blockingTypes.begin(), collector.blockingTypes.end()}; + return {std::nullopt, Reduction::MaybeOk, std::move(blockingTypes), {}}; + } + + TypeId resultTy = ctx->builtins->neverType; + for (auto ty : collector.options) + { + SimplifyResult result = simplifyUnion(ctx->builtins, ctx->arena, resultTy, ty); + // This condition might fire if one of the arguments to this type + // function is a free type somewhere deep in a nested union or + // intersection type, even though we ran a pass above to capture + // some blocked types. + if (!result.blockedTypes.empty()) + return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}}; + + resultTy = result.result; + } + + return {resultTy, Reduction::MaybeOk, {}, {}}; +} + + +TypeFunctionReductionResult intersectTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (!packParams.empty()) + { + ctx->ice->ice("intersect type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + // if we only have one parameter, there's nothing to do. + if (typeParams.size() == 1) + return {follow(typeParams[0]), Reduction::MaybeOk, {}, {}}; + + // we need to follow all of the type parameters. + std::vector types; + types.reserve(typeParams.size()); + for (auto ty : typeParams) + types.emplace_back(follow(ty)); + + // if we only have two parameters and one is `*no-refine*`, we're all done. + if (types.size() == 2 && get(types[1])) + return {types[0], Reduction::MaybeOk, {}, {}}; + else if (types.size() == 2 && get(types[0])) + return {types[1], Reduction::MaybeOk, {}, {}}; + + // check to see if the operand types are resolved enough, and wait to reduce if not + // if any of them are `never`, the intersection will always be `never`, so we can reduce directly. + for (auto ty : types) + { + if (isPending(ty, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {ty}, {}}; + else if (get(ty)) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + } + + // fold over the types with `simplifyIntersection` + TypeId resultTy = ctx->builtins->unknownType; + // collect types which caused intersection to return never + DenseHashSet unintersectableTypes{nullptr}; + for (auto ty : types) + { + // skip any `*no-refine*` types. + if (get(ty)) + continue; + + if (FFlag::LuauRefineTablesWithReadType) + { + if (auto simpleResult = intersectWithSimpleDiscriminant(ctx->builtins, ctx->arena, resultTy, ty)) + { + if (get(*simpleResult)) + unintersectableTypes.insert(follow(ty)); + else + resultTy = *simpleResult; + continue; + } + } + + SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, resultTy, ty); + + // If simplifying the intersection returned never, note the type we tried to intersect it with, and continue trying to intersect with the + // rest + if (get(result.result)) + { + unintersectableTypes.insert(follow(ty)); + continue; + } + for (TypeId blockedType : result.blockedTypes) + { + if (!get(blockedType)) + return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}}; + } + + resultTy = result.result; + } + + if (!unintersectableTypes.empty()) + { + unintersectableTypes.insert(resultTy); + if (unintersectableTypes.size() > 1) + { + TypeId intersection = + ctx->arena->addType(IntersectionType{std::vector(unintersectableTypes.begin(), unintersectableTypes.end())}); + return {intersection, Reduction::MaybeOk, {}, {}}; + } + else + { + return {*unintersectableTypes.begin(), Reduction::MaybeOk, {}, {}}; + } + } + // if the intersection simplifies to `never`, this gives us bad autocomplete. + // we'll just produce the intersection plainly instead, but this might be revisitable + // if we ever give `never` some kind of "explanation" trail. + if (get(resultTy)) + { + TypeId intersection = ctx->arena->addType(IntersectionType{typeParams}); + return {intersection, Reduction::MaybeOk, {}, {}}; + } + + return {resultTy, Reduction::MaybeOk, {}, {}}; +} + +// computes the keys of `ty` into `result` +// `isRaw` parameter indicates whether or not we should follow __index metamethods +// returns `false` if `result` should be ignored because the answer is "all strings" +bool computeKeysOf_DEPRECATED(TypeId ty, Set& result, DenseHashSet& seen, bool isRaw, NotNull ctx) +{ + // if the type is the top table type, the answer is just "all strings" + if (get(ty)) + return false; + + // if we've already seen this type, we can do nothing + if (seen.contains(ty)) + return true; + seen.insert(ty); + + // if we have a particular table type, we can insert the keys + if (auto tableTy = get(ty)) + { + if (tableTy->indexer) + { + // if we have a string indexer, the answer is, again, "all strings" + if (isString(tableTy->indexer->indexType)) + return false; + } + + for (auto [key, _] : tableTy->props) + result.insert(key); + return true; + } + + // otherwise, we have a metatable to deal with + if (auto metatableTy = get(ty)) + { + bool res = true; + + if (!isRaw) + { + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, ty, "__index", Location{}); + if (mmType) + res = res && computeKeysOf_DEPRECATED(*mmType, result, seen, isRaw, ctx); + } + + res = res && computeKeysOf_DEPRECATED(metatableTy->table, result, seen, isRaw, ctx); + + return res; + } + + if (auto classTy = get(ty)) + { + for (auto [key, _] : classTy->props) // NOLINT(performance-for-range-copy) + result.insert(key); + + bool res = true; + if (classTy->metatable && !isRaw) + { + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, ty, "__index", Location{}); + if (mmType) + res = res && computeKeysOf_DEPRECATED(*mmType, result, seen, isRaw, ctx); + } + + if (classTy->parent) + res = res && computeKeysOf_DEPRECATED(follow(*classTy->parent), result, seen, isRaw, ctx); + + return res; + } + + // this should not be reachable since the type should be a valid tables or extern types part from normalization. + LUAU_ASSERT(false); + return false; +} + +namespace { + +/** + * Computes the keys of `ty` into `result` + * `isRaw` parameter indicates whether or not we should follow __index metamethods + * returns `false` if `result` should be ignored because the answer is "all strings" + */ +bool computeKeysOf(TypeId ty, Set>& result, DenseHashSet& seen, bool isRaw, NotNull ctx) +{ + + // if the type is the top table type, the answer is just "all strings" + if (get(ty)) + return false; + + // if we've already seen this type, we can do nothing + if (seen.contains(ty)) + return true; + seen.insert(ty); + + // if we have a particular table type, we can insert the keys + if (auto tableTy = get(ty)) + { + if (tableTy->indexer) + { + // if we have a string indexer, the answer is, again, "all strings" + if (isString(tableTy->indexer->indexType)) + return false; + } + + for (const auto& [key, _] : tableTy->props) + result.insert(key); + return true; + } + + // otherwise, we have a metatable to deal with + if (auto metatableTy = get(ty)) + { + bool res = true; + + if (!isRaw) + { + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, ty, "__index", Location{}); + if (mmType) + res = res && computeKeysOf(*mmType, result, seen, isRaw, ctx); + } + + res = res && computeKeysOf(metatableTy->table, result, seen, isRaw, ctx); + + return res; + } + + if (auto classTy = get(ty)) + { + for (const auto& [key, _] : classTy->props) + result.insert(key); + + bool res = true; + if (classTy->metatable && !isRaw) + { + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, ty, "__index", Location{}); + if (mmType) + res = res && computeKeysOf(*mmType, result, seen, isRaw, ctx); + } + + if (classTy->parent) + res = res && computeKeysOf(follow(*classTy->parent), result, seen, isRaw, ctx); + + return res; + } + + // this should not be reachable since the type should be a valid tables or extern types part from normalization. + LUAU_ASSERT(false); + return false; +} + +} + +TypeFunctionReductionResult keyofFunctionImpl( + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx, + bool isRaw +) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("keyof type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId operandTy = follow(typeParams.at(0)); + + std::shared_ptr normTy = ctx->normalizer->normalize(operandTy); + + // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normTy) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // if we don't have either just tables or just extern types, we've got nothing to get keys of (at least until a future version perhaps adds extern + // types as well) + if (normTy->hasTables() == normTy->hasExternTypes()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + // this is sort of atrocious, but we're trying to reject any type that has not normalized to a table or a union of tables. + if (normTy->hasTops() || normTy->hasBooleans() || normTy->hasErrors() || normTy->hasNils() || normTy->hasNumbers() || normTy->hasStrings() || + normTy->hasThreads() || normTy->hasBuffers() || normTy->hasFunctions() || normTy->hasTyvars()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + if (FFlag::LuauEmptyStringInKeyOf) + { + // We're going to collect the keys in here, and we use optional strings + // so that we can differentiate between the empty string and _no_ string. + Set> keys{std::nullopt}; + + // computing the keys for extern types + if (normTy->hasExternTypes()) + { + LUAU_ASSERT(!normTy->hasTables()); + + // seen set for key computation for extern types + DenseHashSet seen{{}}; + + auto externTypeIter = normTy->externTypes.ordering.begin(); + auto externTypeIterEnd = normTy->externTypes.ordering.end(); + LUAU_ASSERT(externTypeIter != externTypeIterEnd); // should be guaranteed by the `hasExternTypes` check earlier + + // collect all the properties from the first class type + if (!computeKeysOf(*externTypeIter, keys, seen, isRaw, ctx)) + return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; // if it failed, we have a top type! + + // we need to look at each class to remove any keys that are not common amongst them all + while (++externTypeIter != externTypeIterEnd) + { + seen.clear(); // we'll reuse the same seen set + + Set> localKeys{std::nullopt}; + + // we can skip to the next class if this one is a top type + if (!computeKeysOf(*externTypeIter, localKeys, seen, isRaw, ctx)) + continue; + + for (auto& key : keys) + { + // remove any keys that are not present in each class + if (!localKeys.contains(key)) + keys.erase(key); + } + } + } + + // computing the keys for tables + if (normTy->hasTables()) + { + LUAU_ASSERT(!normTy->hasExternTypes()); + + // seen set for key computation for tables + DenseHashSet seen{{}}; + + auto tablesIter = normTy->tables.begin(); + LUAU_ASSERT(tablesIter != normTy->tables.end()); // should be guaranteed by the `hasTables` check earlier + + // collect all the properties from the first table type + if (!computeKeysOf(*tablesIter, keys, seen, isRaw, ctx)) + return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; // if it failed, we have the top table type! + + // we need to look at each tables to remove any keys that are not common amongst them all + while (++tablesIter != normTy->tables.end()) + { + seen.clear(); // we'll reuse the same seen set + + Set> localKeys{std::nullopt}; + + // we can skip to the next table if this one is the top table type + if (!computeKeysOf(*tablesIter, localKeys, seen, isRaw, ctx)) + continue; + + for (auto& key : keys) + { + // remove any keys that are not present in each table + if (!localKeys.contains(key)) + keys.erase(key); + } + } + } + + // if the set of keys is empty, `keyof` is `never` + if (keys.empty()) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + // everything is validated, we need only construct our big union of singletons now! + std::vector singletons; + singletons.reserve(keys.size()); + + for (const auto& key : keys) + { + if (key) + singletons.push_back(ctx->arena->addType(SingletonType{StringSingleton{*key}})); + } + + // If there's only one entry, we don't need a UnionType. + // We can take straight take it from the first entry + // because it was added into the type arena already. + if (singletons.size() == 1) + return {singletons.front(), Reduction::MaybeOk, {}, {}}; + + return {ctx->arena->addType(UnionType{std::move(singletons)}), Reduction::MaybeOk, {}, {}}; + } + else + { + + // we're going to collect the keys in here + Set keys{{}}; + + // computing the keys for extern types + if (normTy->hasExternTypes()) + { + LUAU_ASSERT(!normTy->hasTables()); + + // seen set for key computation for extern types + DenseHashSet seen{{}}; + + auto externTypeIter = normTy->externTypes.ordering.begin(); + auto externTypeIterEnd = normTy->externTypes.ordering.end(); + LUAU_ASSERT(externTypeIter != externTypeIterEnd); // should be guaranteed by the `hasExternTypes` check earlier + + // collect all the properties from the first class type + if (!computeKeysOf_DEPRECATED(*externTypeIter, keys, seen, isRaw, ctx)) + return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; // if it failed, we have a top type! + + // we need to look at each class to remove any keys that are not common amongst them all + while (++externTypeIter != externTypeIterEnd) + { + seen.clear(); // we'll reuse the same seen set + + Set localKeys{{}}; + + // we can skip to the next class if this one is a top type + if (!computeKeysOf_DEPRECATED(*externTypeIter, localKeys, seen, isRaw, ctx)) + continue; + + for (auto& key : keys) + { + // remove any keys that are not present in each class + if (!localKeys.contains(key)) + keys.erase(key); + } + } + } + + // computing the keys for tables + if (normTy->hasTables()) + { + LUAU_ASSERT(!normTy->hasExternTypes()); + + // seen set for key computation for tables + DenseHashSet seen{{}}; + + auto tablesIter = normTy->tables.begin(); + LUAU_ASSERT(tablesIter != normTy->tables.end()); // should be guaranteed by the `hasTables` check earlier + + // collect all the properties from the first table type + if (!computeKeysOf_DEPRECATED(*tablesIter, keys, seen, isRaw, ctx)) + return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; // if it failed, we have the top table type! + + // we need to look at each tables to remove any keys that are not common amongst them all + while (++tablesIter != normTy->tables.end()) + { + seen.clear(); // we'll reuse the same seen set + + Set localKeys{{}}; + + // we can skip to the next table if this one is the top table type + if (!computeKeysOf_DEPRECATED(*tablesIter, localKeys, seen, isRaw, ctx)) + continue; + + for (auto& key : keys) + { + // remove any keys that are not present in each table + if (!localKeys.contains(key)) + keys.erase(key); + } + } + } + + // if the set of keys is empty, `keyof` is `never` + if (keys.empty()) + return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; + + // everything is validated, we need only construct our big union of singletons now! + std::vector singletons; + singletons.reserve(keys.size()); + + for (const std::string& key : keys) + singletons.push_back(ctx->arena->addType(SingletonType{StringSingleton{key}})); + + // If there's only one entry, we don't need a UnionType. + // We can take straight take it from the first entry + // because it was added into the type arena already. + if (singletons.size() == 1) + return {singletons.front(), Reduction::MaybeOk, {}, {}}; + + return {ctx->arena->addType(UnionType{std::move(singletons)}), Reduction::MaybeOk, {}, {}}; + } +} + +TypeFunctionReductionResult keyofTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("keyof type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return keyofFunctionImpl(typeParams, packParams, ctx, /* isRaw */ false); +} + +TypeFunctionReductionResult rawkeyofTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("rawkeyof type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return keyofFunctionImpl(typeParams, packParams, ctx, /* isRaw */ true); +} + +/* Searches through table's or class's props/indexer to find the property of `ty` + If found, appends that property to `result` and returns true + Else, returns false */ +bool searchPropsAndIndexer( + TypeId ty, + TableType::Props tblProps, + std::optional tblIndexer, + DenseHashSet& result, + NotNull ctx +) +{ + ty = follow(ty); + + // index into tbl's properties + if (auto stringSingleton = get(get(ty))) + { + if (tblProps.find(stringSingleton->value) != tblProps.end()) + { + + TypeId propTy; + if (FFlag::LuauRemoveTypeCallsForReadWriteProps) + { + Property& prop = tblProps.at(stringSingleton->value); + + if (prop.readTy) + propTy = follow(*prop.readTy); + else if (prop.writeTy) + propTy = follow(*prop.writeTy); + else // found the property, but there was no type associated with it + return false; + } + else + propTy = follow(tblProps.at(stringSingleton->value).type_DEPRECATED()); + + // property is a union type -> we need to extend our reduction type + if (auto propUnionTy = get(propTy)) + { + for (TypeId option : propUnionTy->options) + { + result.insert(follow(option)); + } + } + else // property is a singular type or intersection type -> we can simply append + result.insert(propTy); + + return true; + } + } + + // index into tbl's indexer + if (tblIndexer) + { + TypeId indexType = follow(tblIndexer->indexType); + + if (auto tfit = get(indexType)) + { + // if we have an index function here, it means we're in a cycle, so let's see if it's well-founded if we tie the knot + if (tfit->function.get() == &builtinTypeFunctions().indexFunc) + indexType = follow(tblIndexer->indexResultType); + } + + if (isSubtype(ty, indexType, ctx->scope, ctx->builtins, ctx->simplifier, *ctx->ice, SolverMode::New)) + { + TypeId idxResultTy = follow(tblIndexer->indexResultType); + + // indexResultType is a union type -> we need to extend our reduction type + if (auto idxResUnionTy = get(idxResultTy)) + { + for (TypeId option : idxResUnionTy->options) + { + result.insert(follow(option)); + } + } + else // indexResultType is a singular type or intersection type -> we can simply append + result.insert(idxResultTy); + + return true; + } + } + + return false; +} + +bool tblIndexInto( + TypeId indexer, + TypeId indexee, + DenseHashSet& result, + DenseHashSet& seenSet, + NotNull ctx, + bool isRaw +) +{ + indexer = follow(indexer); + indexee = follow(indexee); + + if (seenSet.contains(indexee)) + return false; + seenSet.insert(indexee); + + if (auto unionTy = get(indexee)) + { + bool res = true; + for (auto component : unionTy) + { + // if the component is in the seen set and isn't the indexee itself, + // we can skip it cause it means we encountered it in an earlier component in the union. + if (seenSet.contains(component) && component != indexee) + continue; + + res = res && tblIndexInto(indexer, component, result, seenSet, ctx, isRaw); + } + return res; + } + + if (get(indexee)) + { + TypePackId argPack = ctx->arena->addTypePack({indexer}); + SolveResult solveResult = solveFunctionCall( + ctx->arena, + ctx->builtins, + ctx->simplifier, + ctx->normalizer, + ctx->typeFunctionRuntime, + ctx->ice, + ctx->limits, + ctx->scope, + ctx->scope->location, + indexee, + argPack + ); + + if (!solveResult.typePackId.has_value()) + return false; + + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); + if (extracted.head.empty()) + return false; + + result.insert(follow(extracted.head.front())); + return true; + } + + // we have a table type to try indexing + if (auto tableTy = get(indexee)) + { + return searchPropsAndIndexer(indexer, tableTy->props, tableTy->indexer, result, ctx); + } + + // we have a metatable type to try indexing + if (auto metatableTy = get(indexee)) + { + if (auto tableTy = get(follow(metatableTy->table))) + { + + // try finding all properties within the current scope of the table + if (searchPropsAndIndexer(indexer, tableTy->props, tableTy->indexer, result, ctx)) + return true; + } + + // if the code reached here, it means we weren't able to find all properties -> look into __index metamethod + if (!isRaw) + { + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, indexee, "__index", Location{}); + if (mmType) + return tblIndexInto(indexer, *mmType, result, seenSet, ctx, isRaw); + } + } + + return false; +} + +bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet& result, NotNull ctx, bool isRaw) +{ + DenseHashSet seenSet{{}}; + return tblIndexInto(indexer, indexee, result, seenSet, ctx, isRaw); +} + +/* Vocabulary note: indexee refers to the type that contains the properties, + indexer refers to the type that is used to access indexee + Example: index => `Person` is the indexee and `"name"` is the indexer */ +TypeFunctionReductionResult indexFunctionImpl( + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx, + bool isRaw +) +{ + TypeId indexeeTy = follow(typeParams.at(0)); + + if (isPending(indexeeTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {indexeeTy}, {}}; + + std::shared_ptr indexeeNormTy = ctx->normalizer->normalize(indexeeTy); + + // if the indexee failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!indexeeNormTy) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // if the indexee is `any`, then indexing also gives us `any`. + if (indexeeNormTy->shouldSuppressErrors()) + return {ctx->builtins->anyType, Reduction::MaybeOk, {}, {}}; + + // if we don't have either just tables or just extern types, we've got nothing to index into + if (indexeeNormTy->hasTables() == indexeeNormTy->hasExternTypes()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + // we're trying to reject any type that has not normalized to a table or extern type or a union of tables or extern types. + if (indexeeNormTy->hasTops() || indexeeNormTy->hasBooleans() || indexeeNormTy->hasErrors() || indexeeNormTy->hasNils() || + indexeeNormTy->hasNumbers() || indexeeNormTy->hasStrings() || indexeeNormTy->hasThreads() || indexeeNormTy->hasBuffers() || + indexeeNormTy->hasFunctions() || indexeeNormTy->hasTyvars()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + TypeId indexerTy = follow(typeParams.at(1)); + + if (isPending(indexerTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {indexerTy}, {}}; + + std::shared_ptr indexerNormTy = ctx->normalizer->normalize(indexerTy); + + // if the indexer failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!indexerNormTy) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // we're trying to reject any type that is not a string singleton or primitive (string, number, boolean, thread, nil, function, table, or buffer) + if (indexerNormTy->hasTops() || indexerNormTy->hasErrors()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + // indexer can be a union —> break them down into a vector + const std::vector* typesToFind = nullptr; + const std::vector singleType{indexerTy}; + if (auto unionTy = get(indexerTy)) + typesToFind = &unionTy->options; + else + typesToFind = &singleType; + + DenseHashSet properties{{}}; // vector of types that will be returned + + if (indexeeNormTy->hasExternTypes()) + { + LUAU_ASSERT(!indexeeNormTy->hasTables()); + + if (isRaw) // rawget should never reduce for extern types (to match the behavior of the rawget global function) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + // at least one class is guaranteed to be in the iterator by .hasExternTypes() + for (auto externTypeIter = indexeeNormTy->externTypes.ordering.begin(); externTypeIter != indexeeNormTy->externTypes.ordering.end(); + ++externTypeIter) + { + auto externTy = get(*externTypeIter); + if (!externTy) + { + LUAU_ASSERT(false); // this should not be possible according to normalization's spec + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + + for (TypeId ty : *typesToFind) + { + // Search for all instances of indexer in class->props and class->indexer + if (searchPropsAndIndexer(ty, externTy->props, externTy->indexer, properties, ctx)) + continue; // Indexer was found in this class, so we can move on to the next + + auto parent = externTy->parent; + bool foundInParent = false; + while (parent && !foundInParent) + { + auto parentExternType = get(follow(*parent)); + foundInParent = searchPropsAndIndexer(ty, parentExternType->props, parentExternType->indexer, properties, ctx); + parent = parentExternType->parent; + } + + // we move on to the next type if any of the parents we went through had the property. + if (foundInParent) + continue; + + // If code reaches here,that means the property not found -> check in the metatable's __index + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, *externTypeIter, "__index", Location{}); + if (!mmType) // if a metatable does not exist, there is no where else to look + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + if (!tblIndexInto(ty, *mmType, properties, ctx, isRaw)) // if indexer is not in the metatable, we fail to reduce + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + } + } + + if (indexeeNormTy->hasTables()) + { + LUAU_ASSERT(!indexeeNormTy->hasExternTypes()); + + // at least one table is guaranteed to be in the iterator by .hasTables() + for (auto tablesIter = indexeeNormTy->tables.begin(); tablesIter != indexeeNormTy->tables.end(); ++tablesIter) + { + for (TypeId ty : *typesToFind) + if (!tblIndexInto(ty, *tablesIter, properties, ctx, isRaw)) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + } + + // If the type being reduced to is a single type, no need to union + if (properties.size() == 1) + return {*properties.begin(), Reduction::MaybeOk, {}, {}}; + + return {ctx->arena->addType(UnionType{std::vector(properties.begin(), properties.end())}), Reduction::MaybeOk, {}, {}}; +} + +TypeFunctionReductionResult indexTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("index type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return indexFunctionImpl(typeParams, packParams, ctx, /* isRaw */ false); +} + +TypeFunctionReductionResult rawgetTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("rawget type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return indexFunctionImpl(typeParams, packParams, ctx, /* isRaw */ true); +} + +TypeFunctionReductionResult setmetatableTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("setmetatable type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + const Location location = ctx->constraint ? ctx->constraint->location : Location{}; + + TypeId targetTy = follow(typeParams.at(0)); + TypeId metatableTy = follow(typeParams.at(1)); + + std::shared_ptr targetNorm = ctx->normalizer->normalize(targetTy); + + // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!targetNorm) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + // cannot setmetatable on something without table parts. + if (!targetNorm->hasTables()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + // we're trying to reject any type that has not normalized to a table or a union/intersection of tables. + if (targetNorm->hasTops() || targetNorm->hasBooleans() || targetNorm->hasErrors() || targetNorm->hasNils() || targetNorm->hasNumbers() || + targetNorm->hasStrings() || targetNorm->hasThreads() || targetNorm->hasBuffers() || targetNorm->hasFunctions() || targetNorm->hasTyvars() || + targetNorm->hasExternTypes()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + // if the supposed metatable is not a table, we will fail to reduce. + if (!get(metatableTy) && !get(metatableTy)) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + if (targetNorm->tables.size() == 1) + { + TypeId table = *targetNorm->tables.begin(); + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional metatableMetamethod = findMetatableEntry(ctx->builtins, dummy, table, "__metatable", location); + + // if the `__metatable` metamethod is present, then the table is locked and we cannot `setmetatable` on it. + if (metatableMetamethod) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + TypeId withMetatable = ctx->arena->addType(MetatableType{table, metatableTy}); + + return {withMetatable, Reduction::MaybeOk, {}, {}}; + } + + TypeId result = ctx->builtins->neverType; + + for (auto componentTy : targetNorm->tables) + { + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional metatableMetamethod = findMetatableEntry(ctx->builtins, dummy, componentTy, "__metatable", location); + + // if the `__metatable` metamethod is present, then the table is locked and we cannot `setmetatable` on it. + if (metatableMetamethod) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + TypeId withMetatable = ctx->arena->addType(MetatableType{componentTy, metatableTy}); + SimplifyResult simplified = simplifyUnion(ctx->builtins, ctx->arena, result, withMetatable); + + if (!simplified.blockedTypes.empty()) + { + std::vector blockedTypes{}; + blockedTypes.reserve(simplified.blockedTypes.size()); + for (auto ty : simplified.blockedTypes) + blockedTypes.push_back(ty); + return {std::nullopt, Reduction::MaybeOk, std::move(blockedTypes), {}}; + } + + result = simplified.result; + } + + return {result, Reduction::MaybeOk, {}, {}}; +} + +static TypeFunctionReductionResult getmetatableHelper(TypeId targetTy, const Location& location, NotNull ctx) +{ + targetTy = follow(targetTy); + + std::optional result = std::nullopt; + bool erroneous = true; + + if (auto table = get(targetTy)) + erroneous = false; + + if (auto mt = get(targetTy)) + { + result = mt->metatable; + erroneous = false; + } + + if (auto clazz = get(targetTy)) + { + result = clazz->metatable; + erroneous = false; + } + + if (auto primitive = get(targetTy)) + { + result = primitive->metatable; + erroneous = false; + } + + if (auto singleton = get(targetTy)) + { + if (get(singleton)) + { + auto primitiveString = get(ctx->builtins->stringType); + result = primitiveString->metatable; + } + erroneous = false; + } + + if (FFlag::LuauUpdateGetMetatableTypeSignature && get(targetTy)) + { + // getmetatable ~ any + result = targetTy; + erroneous = false; + } + + if (erroneous) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional metatableMetamethod = findMetatableEntry(ctx->builtins, dummy, targetTy, "__metatable", location); + + if (metatableMetamethod) + return {metatableMetamethod, Reduction::MaybeOk, {}, {}}; + + if (result) + return {result, Reduction::MaybeOk, {}, {}}; + + return {ctx->builtins->nilType, Reduction::MaybeOk, {}, {}}; +} + +TypeFunctionReductionResult getmetatableTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("getmetatable type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + const Location location = ctx->constraint ? ctx->constraint->location : Location{}; + + TypeId targetTy = follow(typeParams.at(0)); + + if (isPending(targetTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; + + if (auto ut = get(targetTy)) + { + std::vector options{}; + options.reserve(ut->options.size()); + + for (auto option : ut->options) + { + TypeFunctionReductionResult result = getmetatableHelper(option, location, ctx); + + if (!result.result) + return result; + + options.push_back(*result.result); + } + + return {ctx->arena->addType(UnionType{std::move(options)}), Reduction::MaybeOk, {}, {}}; + } + + if (auto it = get(targetTy)) + { + std::vector parts{}; + parts.reserve(it->parts.size()); + + bool erroredWithUnknown = false; + + for (auto part : it->parts) + { + TypeFunctionReductionResult result = getmetatableHelper(part, location, ctx); + + if (!result.result) + { + // Don't immediately error if part is unknown + if (FFlag::LuauUpdateGetMetatableTypeSignature && get(follow(part))) + { + erroredWithUnknown = true; + continue; + } + else + return result; + } + + parts.push_back(*result.result); + } + + // If all parts are unknown, return erroneous reduction + if (FFlag::LuauUpdateGetMetatableTypeSignature && erroredWithUnknown && parts.empty()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + if (FFlag::LuauUpdateGetMetatableTypeSignature && parts.size() == 1) + return {parts.front(), Reduction::MaybeOk, {}, {}}; + + return {ctx->arena->addType(IntersectionType{std::move(parts)}), Reduction::MaybeOk, {}, {}}; + } + + return getmetatableHelper(targetTy, location, ctx); +} + +TypeFunctionReductionResult weakoptionalTypeFunc( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("weakoptional type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId targetTy = follow(typeParams.at(0)); + + if (isPending(targetTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; + + if (is(instance)) + return {ctx->builtins->nilType, Reduction::MaybeOk, {}, {}}; + + std::shared_ptr targetNorm = ctx->normalizer->normalize(targetTy); + + if (!targetNorm) + return {std::nullopt, Reduction::MaybeOk, {}, {}}; + + auto result = ctx->normalizer->isInhabited(targetNorm.get()); + if (result == NormalizationResult::False) + return {ctx->builtins->nilType, Reduction::MaybeOk, {}, {}}; + + return {targetTy, Reduction::MaybeOk, {}, {}}; +} + +BuiltinTypeFunctions::BuiltinTypeFunctions() + : userFunc{"user", userDefinedTypeFunction} + , notFunc{"not", notTypeFunction} + , lenFunc{"len", lenTypeFunction} + , unmFunc{"unm", unmTypeFunction} + , addFunc{"add", addTypeFunction} + , subFunc{"sub", subTypeFunction} + , mulFunc{"mul", mulTypeFunction} + , divFunc{"div", divTypeFunction} + , idivFunc{"idiv", idivTypeFunction} + , powFunc{"pow", powTypeFunction} + , modFunc{"mod", modTypeFunction} + , concatFunc{"concat", concatTypeFunction} + , andFunc{"and", andTypeFunction, /*canReduceGenerics*/ true} + , orFunc{"or", orTypeFunction, /*canReduceGenerics*/ true} + , ltFunc{"lt", ltTypeFunction} + , leFunc{"le", leTypeFunction} + , eqFunc{"eq", eqTypeFunction} + , refineFunc{"refine", refineTypeFunction, /*canReduceGenerics*/ FFlag::LuauEagerGeneralization4} + , singletonFunc{"singleton", singletonTypeFunction} + , unionFunc{"union", unionTypeFunction} + , intersectFunc{"intersect", intersectTypeFunction} + , keyofFunc{"keyof", keyofTypeFunction} + , rawkeyofFunc{"rawkeyof", rawkeyofTypeFunction} + , indexFunc{"index", indexTypeFunction} + , rawgetFunc{"rawget", rawgetTypeFunction} + , setmetatableFunc{"setmetatable", setmetatableTypeFunction} + , getmetatableFunc{"getmetatable", getmetatableTypeFunction} + , weakoptionalFunc{"weakoptional", weakoptionalTypeFunc} +{ +} + +void BuiltinTypeFunctions::addToScope(NotNull arena, NotNull scope) const +{ + // make a type function for a one-argument type function + auto mkUnaryTypeFunction = [&](const TypeFunction* tf) + { + TypeId t = arena->addType(GenericType{"T", Polarity::Negative}); + GenericTypeDefinition genericT{t}; + + return TypeFun{{genericT}, arena->addType(TypeFunctionInstanceType{NotNull{tf}, {t}, {}})}; + }; + + // make a type function for a two-argument type function with a default argument for the second type being the first + auto mkBinaryTypeFunctionWithDefault = [&](const TypeFunction* tf) + { + TypeId t = arena->addType(GenericType{"T", Polarity::Negative}); + TypeId u = arena->addType(GenericType{"U", Polarity::Negative}); + GenericTypeDefinition genericT{t}; + GenericTypeDefinition genericU{u, {t}}; + + return TypeFun{{genericT, genericU}, arena->addType(TypeFunctionInstanceType{NotNull{tf}, {t, u}, {}})}; + }; + + // make a two-argument type function without the default arguments + auto mkBinaryTypeFunction = [&](const TypeFunction* tf) + { + TypeId t = arena->addType(GenericType{"T", Polarity::Negative}); + TypeId u = arena->addType(GenericType{"U", Polarity::Negative}); + GenericTypeDefinition genericT{t}; + GenericTypeDefinition genericU{u}; + + return TypeFun{{genericT, genericU}, arena->addType(TypeFunctionInstanceType{NotNull{tf}, {t, u}, {}})}; + }; + + scope->exportedTypeBindings[lenFunc.name] = mkUnaryTypeFunction(&lenFunc); + scope->exportedTypeBindings[unmFunc.name] = mkUnaryTypeFunction(&unmFunc); + + scope->exportedTypeBindings[addFunc.name] = mkBinaryTypeFunctionWithDefault(&addFunc); + scope->exportedTypeBindings[subFunc.name] = mkBinaryTypeFunctionWithDefault(&subFunc); + scope->exportedTypeBindings[mulFunc.name] = mkBinaryTypeFunctionWithDefault(&mulFunc); + scope->exportedTypeBindings[divFunc.name] = mkBinaryTypeFunctionWithDefault(&divFunc); + scope->exportedTypeBindings[idivFunc.name] = mkBinaryTypeFunctionWithDefault(&idivFunc); + scope->exportedTypeBindings[powFunc.name] = mkBinaryTypeFunctionWithDefault(&powFunc); + scope->exportedTypeBindings[modFunc.name] = mkBinaryTypeFunctionWithDefault(&modFunc); + scope->exportedTypeBindings[concatFunc.name] = mkBinaryTypeFunctionWithDefault(&concatFunc); + + scope->exportedTypeBindings[ltFunc.name] = mkBinaryTypeFunctionWithDefault(<Func); + scope->exportedTypeBindings[leFunc.name] = mkBinaryTypeFunctionWithDefault(&leFunc); + scope->exportedTypeBindings[eqFunc.name] = mkBinaryTypeFunctionWithDefault(&eqFunc); + + scope->exportedTypeBindings[keyofFunc.name] = mkUnaryTypeFunction(&keyofFunc); + scope->exportedTypeBindings[rawkeyofFunc.name] = mkUnaryTypeFunction(&rawkeyofFunc); + + if (FFlag::LuauNotAllBinaryTypeFunsHaveDefaults) + { + scope->exportedTypeBindings[indexFunc.name] = mkBinaryTypeFunction(&indexFunc); + scope->exportedTypeBindings[rawgetFunc.name] = mkBinaryTypeFunction(&rawgetFunc); + } + else + { + scope->exportedTypeBindings[indexFunc.name] = mkBinaryTypeFunctionWithDefault(&indexFunc); + scope->exportedTypeBindings[rawgetFunc.name] = mkBinaryTypeFunctionWithDefault(&rawgetFunc); + } + + if (FFlag::LuauNotAllBinaryTypeFunsHaveDefaults) + scope->exportedTypeBindings[setmetatableFunc.name] = mkBinaryTypeFunction(&setmetatableFunc); + else + scope->exportedTypeBindings[setmetatableFunc.name] = mkBinaryTypeFunctionWithDefault(&setmetatableFunc); + scope->exportedTypeBindings[getmetatableFunc.name] = mkUnaryTypeFunction(&getmetatableFunc); +} + +const BuiltinTypeFunctions& builtinTypeFunctions() +{ + static std::unique_ptr result = std::make_unique(); + + return *result; +} + +} diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 9a5879b1..d083c733 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -4,6 +4,7 @@ #include "Luau/Common.h" #include "Luau/NotNull.h" #include "Luau/Type.h" +#include "Luau/TypeOrPack.h" #include "Luau/TypePack.h" #include "Luau/Unifiable.h" #include "Luau/VisitType.h" @@ -20,14 +21,6 @@ namespace Luau namespace { -using Kind = Variant; - -template -const T* get(const Kind& kind) -{ - return get_if(&kind); -} - class TypeCloner { @@ -38,7 +31,7 @@ protected: // A queue of kinds where we cloned it, but whose interior types hasn't // been updated to point to new clones. Once all of its interior types // has been updated, it gets removed from the queue. - std::vector queue; + std::vector queue; NotNull types; NotNull packs; @@ -116,7 +109,7 @@ private: if (hasExceededIterationLimit()) break; - Kind kind = queue.back(); + TypeOrPack kind = queue.back(); queue.pop_back(); if (find(kind)) @@ -147,7 +140,7 @@ protected: return std::nullopt; } - std::optional find(Kind kind) const + std::optional find(TypeOrPack kind) const { if (auto ty = get(kind)) return find(*ty); @@ -265,7 +258,7 @@ private: ); } - void cloneChildren(Kind kind) + void cloneChildren(TypeOrPack kind) { if (auto ty = get(kind)) return cloneChildren(*ty); diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index f938c615..6bf192d1 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -3,6 +3,7 @@ #include "Luau/Ast.h" #include "Luau/BuiltinDefinitions.h" +#include "Luau/BuiltinTypeFunctions.h" #include "Luau/Common.h" #include "Luau/Constraint.h" #include "Luau/ControlFlow.h" @@ -38,8 +39,6 @@ LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauGlobalVariableModuleIsolation) LUAU_FASTFLAGVARIABLE(LuauEnableWriteOnlyProperties) -LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) -LUAU_FASTFLAG(LuauDoNotAddUpvalueTypesToLocalType) LUAU_FASTFLAGVARIABLE(LuauAvoidDoubleNegation) LUAU_FASTFLAGVARIABLE(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck2) @@ -1330,22 +1329,19 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI loopScope, getLocation(forIn->values), IterableConstraint{iterator, variableTypes, forIn->values.data[0], &module->astForInNextTypes} ); - if (FFlag::LuauAddCallConstraintForIterableFunctions) - { - // Add an intersection ReduceConstraint for the key variable to denote that it can't be nil - AstLocal* keyVar = *forIn->vars.begin(); - const DefId keyDef = dfg->getDef(keyVar); - const TypeId loopVar = loopScope->lvalueTypes[keyDef]; + // Add an intersection ReduceConstraint for the key variable to denote that it can't be nil + AstLocal* keyVar = *forIn->vars.begin(); + const DefId keyDef = dfg->getDef(keyVar); + const TypeId loopVar = loopScope->lvalueTypes[keyDef]; - const TypeId intersectionTy = - createTypeFunctionInstance(builtinTypeFunctions().intersectFunc, {loopVar, builtinTypes->notNilType}, {}, loopScope, keyVar->location); + const TypeId intersectionTy = + createTypeFunctionInstance(builtinTypeFunctions().intersectFunc, {loopVar, builtinTypes->notNilType}, {}, loopScope, keyVar->location); - loopScope->bindings[keyVar] = Binding{intersectionTy, keyVar->location}; - loopScope->lvalueTypes[keyDef] = intersectionTy; + loopScope->bindings[keyVar] = Binding{intersectionTy, keyVar->location}; + loopScope->lvalueTypes[keyDef] = intersectionTy; - auto c = addConstraint(loopScope, keyVar->location, ReduceConstraint{intersectionTy}); - c->dependencies.push_back(iterable); - } + auto c = addConstraint(loopScope, keyVar->location, ReduceConstraint{intersectionTy}); + c->dependencies.push_back(iterable); for (TypeId var : variableTypes) { @@ -3302,7 +3298,7 @@ void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprLocal* local if (ty) { TypeIds* localDomain = localTypes.find(*ty); - if (localDomain && !(FFlag::LuauDoNotAddUpvalueTypesToLocalType && local->upvalue)) + if (localDomain && !local->upvalue) localDomain->insert(rhsType); } else @@ -3333,10 +3329,6 @@ void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprLocal* local if (annotatedTy) addConstraint(scope, local->location, SubtypeConstraint{rhsType, *annotatedTy}); - // This is vestigial. - if (!FFlag::LuauDoNotAddUpvalueTypesToLocalType) - if (TypeIds* localDomain = localTypes.find(*ty)) - localDomain->insert(rhsType); } void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId rhsType) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 5ba5def2..f6c3ff01 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -34,7 +34,6 @@ LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500) LUAU_FASTFLAGVARIABLE(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauStuckTypeFunctionsStillDispatch) -LUAU_FASTFLAGVARIABLE(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAGVARIABLE(LuauGuardAgainstMalformedTypeAliasExpansion2) LUAU_FASTFLAGVARIABLE(LuauInsertErrorTypesIntoIndexerResult) LUAU_FASTFLAGVARIABLE(LuauAvoidGenericsLeakingDuringFunctionCallCheck) @@ -633,8 +632,9 @@ void ConstraintSolver::finalizeTypeFunctions() TypeId ty = follow(t); if (get(ty)) { + TypeFunctionContext context{NotNull{this}, constraint->scope, NotNull{constraint}}; FunctionGraphReductionResult result = - reduceTypeFunctions(t, constraint->location, TypeFunctionContext{NotNull{this}, constraint->scope, NotNull{constraint}}, true); + reduceTypeFunctions(t, constraint->location, NotNull{&context}, true); for (TypeId r : result.reducedTypes) unblock(r, constraint->location); @@ -2772,8 +2772,9 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull constraint, bool force) { TypeId ty = follow(c.ty); - FunctionGraphReductionResult result = - reduceTypeFunctions(ty, constraint->location, TypeFunctionContext{NotNull{this}, constraint->scope, constraint}, force); + + TypeFunctionContext context{NotNull{this}, constraint->scope, constraint}; + FunctionGraphReductionResult result = reduceTypeFunctions(ty, constraint->location, NotNull{&context}, force); for (TypeId r : result.reducedTypes) unblock(r, constraint->location); @@ -2824,8 +2825,9 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull constraint, bool force) { TypePackId tp = follow(c.tp); - FunctionGraphReductionResult result = - reduceTypeFunctions(tp, constraint->location, TypeFunctionContext{NotNull{this}, constraint->scope, constraint}, force); + + TypeFunctionContext context{NotNull{this}, constraint->scope, constraint}; + FunctionGraphReductionResult result = reduceTypeFunctions(tp, constraint->location, NotNull{&context}, force); for (TypeId r : result.reducedTypes) unblock(r, constraint->location); @@ -3204,50 +3206,20 @@ bool ConstraintSolver::tryDispatchIterableFunction(TypeId nextTy, TypeId tableTy // the type of the `nextAstFragment` is the `nextTy`. (*c.astForInNextTypes)[c.nextAstFragment] = nextTy; - if (FFlag::LuauAddCallConstraintForIterableFunctions) - { - // Construct a FunctionCallConstraint, to help us learn about the type of the loop variables being assigned to in this iterable - TypePackId tableTyPack = arena->addTypePack({tableTy}); + // Construct a FunctionCallConstraint, to help us learn about the type of the loop variables being assigned to in this iterable + TypePackId tableTyPack = arena->addTypePack({tableTy}); - TypePackId variablesPack = arena->addTypePack(BlockedTypePack{}); + TypePackId variablesPack = arena->addTypePack(BlockedTypePack{}); - auto callConstraint = pushConstraint(constraint->scope, constraint->location, FunctionCallConstraint{nextTy, tableTyPack, variablesPack}); + auto callConstraint = pushConstraint(constraint->scope, constraint->location, FunctionCallConstraint{nextTy, tableTyPack, variablesPack}); - getMutable(variablesPack)->owner = callConstraint.get(); + getMutable(variablesPack)->owner = callConstraint.get(); - auto unpackConstraint = unpackAndAssign(c.variables, variablesPack, constraint); + auto unpackConstraint = unpackAndAssign(c.variables, variablesPack, constraint); - inheritBlocks(constraint, callConstraint); - - inheritBlocks(unpackConstraint, callConstraint); - } - else - { - const TypePackId nextRetPack = nextFn->retTypes; - TypePackIterator it = begin(nextRetPack); - std::vector modifiedNextRetHead; - - // The first value is never nil in the context of the loop, even if it's nil - // in the next function's return type, because the loop will not advance if - // it's nil. - if (it != end(nextRetPack)) - { - TypeId firstRet = *it; - TypeId modifiedFirstRet = stripNil(builtinTypes, *arena, firstRet); - modifiedNextRetHead.push_back(modifiedFirstRet); - ++it; - } - - for (; it != end(nextRetPack); ++it) - modifiedNextRetHead.push_back(*it); - - TypePackId modifiedNextRetPack = arena->addTypePack(std::move(modifiedNextRetHead), it.tail()); - - auto unpackConstraint = unpackAndAssign(c.variables, modifiedNextRetPack, constraint); - - inheritBlocks(constraint, unpackConstraint); - } + inheritBlocks(constraint, callConstraint); + inheritBlocks(unpackConstraint, callConstraint); return true; } diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index 1de9d743..6ffe3353 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -14,8 +14,6 @@ LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauDfgScopeStackNotNull) -LUAU_FASTFLAGVARIABLE(LuauDoNotAddUpvalueTypesToLocalType) -LUAU_FASTFLAGVARIABLE(LuauDfgIfBlocksShouldRespectControlFlow) LUAU_FASTFLAGVARIABLE(LuauDfgAllowUpdatesInLoops) LUAU_FASTFLAG(LuauFragmentAutocompleteTracksRValueRefinements) LUAU_FASTFLAGVARIABLE(LuauDfgForwardNilFromAndOr) @@ -510,26 +508,14 @@ ControlFlow DataFlowGraphBuilder::visit(AstStatIf* i) } DfgScope* scope = FFlag::LuauDfgScopeStackNotNull ? currentScope() : currentScope_DEPRECATED(); - if (FFlag::LuauDfgIfBlocksShouldRespectControlFlow) - { - // If the control flow from the `if` or `else` block is non-linear, - // then we should assume that the _other_ branch is the one taken. - if (thencf != ControlFlow::None && elsecf == ControlFlow::None) - scope->inherit(elseScope); - else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) - scope->inherit(thenScope); - else if ((thencf | elsecf) == ControlFlow::None) - join(scope, thenScope, elseScope); - } - else - { - if (thencf != ControlFlow::None && elsecf == ControlFlow::None) - join(scope, scope, elseScope); - else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) - join(scope, thenScope, scope); - else if ((thencf | elsecf) == ControlFlow::None) - join(scope, thenScope, elseScope); - } + // If the control flow from the `if` or `else` block is non-linear, + // then we should assume that the _other_ branch is the one taken. + if (thencf != ControlFlow::None && elsecf == ControlFlow::None) + scope->inherit(elseScope); + else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) + scope->inherit(thenScope); + else if ((thencf | elsecf) == ControlFlow::None) + join(scope, thenScope, elseScope); if (thencf == elsecf) return thencf; @@ -1311,7 +1297,7 @@ DefId DataFlowGraphBuilder::visitLValue(AstExprLocal* l, DefId incomingDef) DfgScope* scope = FFlag::LuauDfgScopeStackNotNull ? currentScope() : currentScope_DEPRECATED(); // In order to avoid alias tracking, we need to clip the reference to the parent def. - if (scope->canUpdateDefinition(l->local) && !(FFlag::LuauDoNotAddUpvalueTypesToLocalType && l->upvalue)) + if (scope->canUpdateDefinition(l->local) && !l->upvalue) { DefId updated = defArena->freshCell(l->local, l->location, containsSubscriptedDefinition(incomingDef)); scope->bindings[l->local] = updated; diff --git a/Analysis/src/ExpectedTypeVisitor.cpp b/Analysis/src/ExpectedTypeVisitor.cpp index 2f5f1fe0..1aa3a073 100644 --- a/Analysis/src/ExpectedTypeVisitor.cpp +++ b/Analysis/src/ExpectedTypeVisitor.cpp @@ -8,7 +8,7 @@ #include "Luau/TypeUtils.h" #include "Luau/VisitType.h" -LUAU_FASTFLAGVARIABLE(LuauImplicitTableIndexerKeys2) +LUAU_FASTFLAGVARIABLE(LuauImplicitTableIndexerKeys3) namespace Luau { @@ -147,7 +147,7 @@ struct IndexCollector : public TypeOnceVisitor bool ExpectedTypeVisitor::visit(AstExprIndexExpr* expr) { - if (!FFlag::LuauImplicitTableIndexerKeys2) + if (!FFlag::LuauImplicitTableIndexerKeys3) return true; if (auto ty = astTypes->find(expr->expr)) diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index b1afca8e..8334abf3 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -22,7 +22,6 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAGVARIABLE(LuauNewNonStrictVisitTypes2) LUAU_FASTFLAGVARIABLE(LuauNewNonStrictFixGenericTypePacks) LUAU_FASTFLAGVARIABLE(LuauNewNonStrictMoreUnknownSymbols) LUAU_FASTFLAGVARIABLE(LuauNewNonStrictNoErrorsPassingNever) @@ -240,14 +239,8 @@ struct NonStrictTypeChecker if (noTypeFunctionErrors.find(instance)) return instance; - ErrorVec errors = - reduceTypeFunctions( - instance, - location, - TypeFunctionContext{arena, builtinTypes, stack.back(), simplifier, NotNull{&normalizer}, typeFunctionRuntime, ice, limits}, - true - ) - .errors; + TypeFunctionContext context{arena, builtinTypes, stack.back(), simplifier, NotNull{&normalizer}, typeFunctionRuntime, ice, limits}; + ErrorVec errors = reduceTypeFunctions(instance, location, NotNull{&context}, true).errors; if (errors.empty()) noTypeFunctionErrors.insert(instance); @@ -340,8 +333,7 @@ struct NonStrictTypeChecker { ctx.remove(dfg->getDef(local)); - if (FFlag::LuauNewNonStrictVisitTypes2) - visit(local->annotation); + visit(local->annotation); } } else @@ -426,8 +418,7 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatFor* forStatement) { - if (FFlag::LuauNewNonStrictVisitTypes2) - visit(forStatement->var->annotation); + visit(forStatement->var->annotation); // TODO: throwing out context based on same principle as existing code? if (forStatement->from) @@ -441,11 +432,8 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatForIn* forInStatement) { - if (FFlag::LuauNewNonStrictVisitTypes2) - { - for (auto var : forInStatement->vars) - visit(var->annotation); - } + for (auto var : forInStatement->vars) + visit(var->annotation); for (AstExpr* rhs : forInStatement->values) visit(rhs, ValueContext::RValue); @@ -482,11 +470,8 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatTypeAlias* typeAlias) { - if (FFlag::LuauNewNonStrictVisitTypes2) - { - visitGenerics(typeAlias->generics, typeAlias->genericPacks); - visit(typeAlias->type); - } + visitGenerics(typeAlias->generics, typeAlias->genericPacks); + visit(typeAlias->type); return {}; } @@ -498,38 +483,31 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatDeclareFunction* declFn) { - if (FFlag::LuauNewNonStrictVisitTypes2) - { - visitGenerics(declFn->generics, declFn->genericPacks); - visit(declFn->params); - visit(declFn->retTypes); - } + visitGenerics(declFn->generics, declFn->genericPacks); + visit(declFn->params); + visit(declFn->retTypes); return {}; } NonStrictContext visit(AstStatDeclareGlobal* declGlobal) { - if (FFlag::LuauNewNonStrictVisitTypes2) - visit(declGlobal->type); + visit(declGlobal->type); return {}; } NonStrictContext visit(AstStatDeclareExternType* declClass) { - if (FFlag::LuauNewNonStrictVisitTypes2) + if (declClass->indexer) { - if (declClass->indexer) - { - visit(declClass->indexer->indexType); - visit(declClass->indexer->resultType); - } - - for (auto prop : declClass->props) - visit(prop.ty); + visit(declClass->indexer->indexType); + visit(declClass->indexer->resultType); } + for (auto prop : declClass->props) + visit(prop.ty); + return {}; } @@ -793,19 +771,15 @@ struct NonStrictTypeChecker } remainder.remove(dfg->getDef(local)); - if (FFlag::LuauNewNonStrictVisitTypes2) - visit(local->annotation); + visit(local->annotation); } - if (FFlag::LuauNewNonStrictVisitTypes2) - { - visitGenerics(exprFn->generics, exprFn->genericPacks); + visitGenerics(exprFn->generics, exprFn->genericPacks); - visit(exprFn->returnAnnotation); + visit(exprFn->returnAnnotation); - if (exprFn->varargAnnotation) - visit(exprFn->varargAnnotation); - } + if (exprFn->varargAnnotation) + visit(exprFn->varargAnnotation); return remainder; } @@ -836,8 +810,7 @@ struct NonStrictTypeChecker NonStrictContext visit(AstExprTypeAssertion* typeAssertion) { - if (FFlag::LuauNewNonStrictVisitTypes2) - visit(typeAssertion->annotation); + visit(typeAssertion->annotation); return visit(typeAssertion->expr, ValueContext::RValue); } @@ -868,8 +841,6 @@ struct NonStrictTypeChecker void visit(AstType* ty) { - LUAU_ASSERT(FFlag::LuauNewNonStrictVisitTypes2); - // If this node is `nullptr`, early exit. if (!ty) return; @@ -1081,8 +1052,6 @@ struct NonStrictTypeChecker void visit(AstTypePack* pack) { - LUAU_ASSERT(FFlag::LuauNewNonStrictVisitTypes2); - // If there is no pack node, early exit. if (!pack) return; diff --git a/Analysis/src/OverloadResolution.cpp b/Analysis/src/OverloadResolution.cpp index d4609691..9d7b71d0 100644 --- a/Analysis/src/OverloadResolution.cpp +++ b/Analysis/src/OverloadResolution.cpp @@ -246,8 +246,9 @@ std::pair OverloadResolver::checkOverload_ const std::vector* argExprs ) { + TypeFunctionContext context{arena, builtinTypes, scope, simplifier, normalizer, typeFunctionRuntime, ice, limits}; FunctionGraphReductionResult result = reduceTypeFunctions( - fnTy, callLoc, TypeFunctionContext{arena, builtinTypes, scope, simplifier, normalizer, typeFunctionRuntime, ice, limits}, /*force=*/true + fnTy, callLoc, NotNull{&context}, /*force=*/true ); if (!result.errors.empty()) return {OverloadIsNonviable, result.errors}; diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 68c207f6..538e9ae4 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -2151,7 +2151,7 @@ std::pair Subtyping::handleTypeFunctionReductionResult(const T { TypeFunctionContext context{arena, builtinTypes, scope, simplifier, normalizer, typeFunctionRuntime, iceReporter, NotNull{&limits}}; TypeId function = arena->addType(*functionInstance); - FunctionGraphReductionResult result = reduceTypeFunctions(function, {}, context, true); + FunctionGraphReductionResult result = reduceTypeFunctions(function, {}, NotNull{&context}, true); ErrorVec errors; if (result.blockedTypes.size() != 0 || result.blockedPacks.size() != 0) { diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 5b730eb8..18067dc8 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -16,7 +16,6 @@ #include "Luau/TypeOrPack.h" #include -#include #include LUAU_FASTFLAGVARIABLE(LuauEnableDenseTableAlias) diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 745fee3d..430f8c12 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -30,10 +30,8 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAGVARIABLE(LuauTypeCheckerStricterIndexCheck) LUAU_FASTFLAG(LuauEnableWriteOnlyProperties) LUAU_FASTFLAG(LuauNewNonStrictFixGenericTypePacks) -LUAU_FASTFLAGVARIABLE(LuauReportSubtypingErrors) LUAU_FASTFLAGVARIABLE(LuauSkipMalformedTypeAliases) LUAU_FASTFLAGVARIABLE(LuauTableLiteralSubtypeSpecificCheck2) LUAU_FASTFLAG(LuauStuckTypeFunctionsStillDispatch) @@ -499,16 +497,11 @@ TypeId TypeChecker2::checkForTypeFunctionInhabitance(TypeId instance, Location l return instance; seenTypeFunctionInstances.insert(instance); - ErrorVec errors = - reduceTypeFunctions( - instance, - location, - TypeFunctionContext{ - NotNull{&module->internalTypes}, builtinTypes, stack.back(), simplifier, NotNull{&normalizer}, typeFunctionRuntime, ice, limits - }, - true - ) - .errors; + TypeFunctionContext context{ + NotNull{&module->internalTypes}, builtinTypes, stack.back(), simplifier, NotNull{&normalizer}, typeFunctionRuntime, ice, limits + }; + + ErrorVec errors = reduceTypeFunctions(instance, location, NotNull{&context}, true).errors; if (!isErrorSuppressing(location, instance)) reportErrors(std::move(errors)); return instance; @@ -1417,24 +1410,16 @@ void TypeChecker2::visit(AstExprConstantBool* expr) NotNull scope{findInnermostScope(expr->location)}; SubtypingResult r = subtyping->isSubtype(bestType, inferredType, scope); - if (FFlag::LuauReportSubtypingErrors) + if (!isErrorSuppressing(expr->location, inferredType)) { - if (!isErrorSuppressing(expr->location, inferredType)) - { - if (!r.isSubtype) - reportError(TypeMismatch{inferredType, bestType}, expr->location); - if (FFlag::LuauSubtypingCheckFunctionGenericCounts) - { - for (auto& e : r.errors) - e.location = expr->location; - } - reportErrors(r.errors); - } - } - else - { - if (!r.isSubtype && !isErrorSuppressing(expr->location, inferredType)) + if (!r.isSubtype) reportError(TypeMismatch{inferredType, bestType}, expr->location); + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : r.errors) + e.location = expr->location; + } + reportErrors(r.errors); } } @@ -1459,24 +1444,16 @@ void TypeChecker2::visit(AstExprConstantString* expr) NotNull scope{findInnermostScope(expr->location)}; SubtypingResult r = subtyping->isSubtype(bestType, inferredType, scope); - if (FFlag::LuauReportSubtypingErrors) + if (!isErrorSuppressing(expr->location, inferredType)) { - if (!isErrorSuppressing(expr->location, inferredType)) - { - if (!r.isSubtype) - reportError(TypeMismatch{inferredType, bestType}, expr->location); - if (FFlag::LuauSubtypingCheckFunctionGenericCounts) - { - for (auto& e : r.errors) - e.location = expr->location; - } - reportErrors(r.errors); - } - } - else - { - if (!r.isSubtype && !isErrorSuppressing(expr->location, inferredType)) + if (!r.isSubtype) reportError(TypeMismatch{inferredType, bestType}, expr->location); + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : r.errors) + e.location = expr->location; + } + reportErrors(r.errors); } } @@ -1545,17 +1522,13 @@ void TypeChecker2::visitCall(AstExprCall* call) if (result.isSubtype) fnTy = follow(*selectedOverloadTy); - if (FFlag::LuauReportSubtypingErrors) - { - if (!isErrorSuppressing(call->location, *selectedOverloadTy)) - if (FFlag::LuauSubtypingCheckFunctionGenericCounts) - { - for (auto& e : result.errors) - e.location = call->location; - } - reportErrors(std::move(result.errors)); - } - + if (!isErrorSuppressing(call->location, *selectedOverloadTy)) + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : result.errors) + e.location = call->location; + } + reportErrors(std::move(result.errors)); if (result.normalizationTooComplex) { reportError(NormalizationTooComplex{}, call->func->location); @@ -3220,17 +3193,13 @@ bool TypeChecker2::testIsSubtype(TypeId subTy, TypeId superTy, Location location NotNull scope{findInnermostScope(location)}; SubtypingResult r = subtyping->isSubtype(subTy, superTy, scope); - if (FFlag::LuauReportSubtypingErrors) - { - if (!isErrorSuppressing(location, subTy)) - if (FFlag::LuauSubtypingCheckFunctionGenericCounts) - { - for (auto& e : r.errors) - e.location = location; - } - reportErrors(std::move(r.errors)); - } - + if (!isErrorSuppressing(location, subTy)) + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : r.errors) + e.location = location; + } + reportErrors(std::move(r.errors)); if (r.normalizationTooComplex) reportError(NormalizationTooComplex{}, location); @@ -3245,17 +3214,13 @@ bool TypeChecker2::testIsSubtype(TypePackId subTy, TypePackId superTy, Location NotNull scope{findInnermostScope(location)}; SubtypingResult r = subtyping->isSubtype(subTy, superTy, scope); - if (FFlag::LuauReportSubtypingErrors) - { - if (!isErrorSuppressing(location, subTy)) - if (FFlag::LuauSubtypingCheckFunctionGenericCounts) - { - for (auto& e : r.errors) - e.location = location; - } - reportErrors(std::move(r.errors)); - } - + if (!isErrorSuppressing(location, subTy)) + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : r.errors) + e.location = location; + } + reportErrors(std::move(r.errors)); if (r.normalizationTooComplex) reportError(NormalizationTooComplex{}, location); @@ -3501,17 +3466,7 @@ PropertyType TypeChecker2::hasIndexTypeFromType( return {NormalizationResult::True, {tt->indexer->indexResultType}}; } - if (FFlag::LuauTypeCheckerStricterIndexCheck) - { - return {NormalizationResult::False, {builtinTypes->unknownType}}; - } - else - { - // if we are in a conditional context, we treat the property as present and `unknown` because - // we may be _refining_ `tableTy` to include that property. we will want to revisit this a bit - // in the future once luau has support for exact tables since this only applies when inexact. - return {inConditional(typeContext) ? NormalizationResult::True : NormalizationResult::False, {builtinTypes->unknownType}}; - } + return {NormalizationResult::False, {builtinTypes->unknownType}}; } else if (const ExternType* cls = get(ty)) { diff --git a/Analysis/src/TypeFunction.cpp b/Analysis/src/TypeFunction.cpp index 291f170f..0f88ce5f 100644 --- a/Analysis/src/TypeFunction.cpp +++ b/Analysis/src/TypeFunction.cpp @@ -2,39 +2,23 @@ #include "Luau/TypeFunction.h" -#include "Luau/BytecodeBuilder.h" #include "Luau/Common.h" -#include "Luau/Compiler.h" #include "Luau/ConstraintSolver.h" #include "Luau/DenseHash.h" -#include "Luau/Instantiation.h" #include "Luau/Normalize.h" #include "Luau/NotNull.h" #include "Luau/OverloadResolution.h" -#include "Luau/Set.h" -#include "Luau/Simplify.h" #include "Luau/Subtyping.h" -#include "Luau/TimeTrace.h" #include "Luau/ToString.h" #include "Luau/TxnLog.h" #include "Luau/Type.h" #include "Luau/TypeChecker2.h" #include "Luau/TypeFunctionReductionGuesser.h" -#include "Luau/TypeFunctionRuntime.h" -#include "Luau/TypeFunctionRuntimeBuilder.h" #include "Luau/TypeFwd.h" #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" #include "Luau/VecDeque.h" #include "Luau/VisitType.h" -#include "Luau/ApplyTypeFunction.h" - -#include "lua.h" -#include "lualib.h" - -#include -#include -#include // used to control emitting CodeTooComplex warnings on type function reduction LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyGraphReductionMaximumSteps, 1'000'000); @@ -216,7 +200,7 @@ struct UnscopedGenericFinder : TypeOnceVisitor struct TypeFunctionReducer { - TypeFunctionContext ctx; + NotNull ctx; VecDeque queuedTys; VecDeque queuedTps; @@ -235,7 +219,7 @@ struct TypeFunctionReducer TypeOrTypePackIdSet shouldGuess, std::vector cyclicTypes, Location location, - TypeFunctionContext ctx, + NotNull ctx, bool force = false ) : ctx(ctx) @@ -378,7 +362,7 @@ struct TypeFunctionReducer template void replace(T subject, T replacement) { - if (subject->owningArena != ctx.arena.get()) + if (subject->owningArena != ctx->arena.get()) { result.errors.emplace_back(location, InternalError{"Attempting to modify a type function instance from another arena"}); return; @@ -404,7 +388,7 @@ struct TypeFunctionReducer void setState(TypeId ty, TypeFunctionInstanceState state) const { - if (ty->owningArena != ctx.arena) + if (ty->owningArena != ctx->arena) return; TypeFunctionInstanceType* tfit = getMutable(ty); @@ -458,7 +442,7 @@ struct TypeFunctionReducer setState(subject, TypeFunctionInstanceState::Stuck); } else - ctx.ice->ice("Unexpected TypeFunctionInstanceState"); + ctx->ice->ice("Unexpected TypeFunctionInstanceState"); } } @@ -583,7 +567,7 @@ struct TypeFunctionReducer if (FFlag::DebugLuauLogTypeFamilies) printf("Flagged %s for reduction with guesser.\n", toString(subject, {true}).c_str()); - TypeFunctionReductionGuesser guesser{ctx.arena, ctx.builtins, ctx.normalizer}; + TypeFunctionReductionGuesser guesser{ctx->arena, ctx->builtins, ctx->normalizer}; auto guessed = guesser.guess(subject); if (guessed) @@ -651,9 +635,9 @@ struct TypeFunctionReducer if (tryGuessing(subject)) return; - ctx.userFuncName = tfit->userFuncName; + ctx->userFuncName = tfit->userFuncName; - TypeFunctionReductionResult result = tfit->function->reducer(subject, tfit->typeArguments, tfit->packArguments, NotNull{&ctx}); + TypeFunctionReductionResult result = tfit->function->reducer(subject, tfit->typeArguments, tfit->packArguments, ctx); handleTypeFunctionReduction(subject, std::move(result)); } } @@ -678,7 +662,7 @@ struct TypeFunctionReducer return; TypeFunctionReductionResult result = - tfit->function->reducer(subject, tfit->typeArguments, tfit->packArguments, NotNull{&ctx}); + tfit->function->reducer(subject, tfit->typeArguments, tfit->packArguments, ctx); handleTypeFunctionReduction(subject, std::move(result)); } } @@ -692,48 +676,13 @@ struct TypeFunctionReducer } }; -struct LuauTempThreadPopper -{ - explicit LuauTempThreadPopper(lua_State* L) - : L(L) - { - } - ~LuauTempThreadPopper() - { - lua_pop(L, 1); - } - - lua_State* L = nullptr; -}; - -template -class ScopedAssign -{ -public: - ScopedAssign(T& target, const T& value) - : target(&target) - , oldValue(target) - { - target = value; - } - - ~ScopedAssign() - { - *target = oldValue; - } - -private: - T* target = nullptr; - T oldValue; -}; - static FunctionGraphReductionResult reduceFunctionsInternal( VecDeque queuedTys, VecDeque queuedTps, TypeOrTypePackIdSet shouldGuess, std::vector cyclics, Location location, - TypeFunctionContext ctx, + NotNull ctx, bool force ) { @@ -746,10 +695,10 @@ static FunctionGraphReductionResult reduceFunctionsInternal( // => back to type function reduction. At worst, if there's a reduction // that _doesn't_ loop forever and _needs_ reentrancy, we'll fail to // handle that and potentially emit an error when we didn't need to. - if (ctx.normalizer->sharedState->reentrantTypeReduction) + if (ctx->normalizer->sharedState->reentrantTypeReduction) return {}; - TypeReductionRentrancyGuard _{ctx.normalizer->sharedState}; + TypeReductionRentrancyGuard _{ctx->normalizer->sharedState}; while (!reducer.done()) { reducer.step(); @@ -765,7 +714,7 @@ static FunctionGraphReductionResult reduceFunctionsInternal( return std::move(reducer.result); } -FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, TypeFunctionContext ctx, bool force) +FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, NotNull ctx, bool force) { InstanceCollector collector; @@ -792,7 +741,7 @@ FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location loc ); } -FunctionGraphReductionResult reduceTypeFunctions(TypePackId entrypoint, Location location, TypeFunctionContext ctx, bool force) +FunctionGraphReductionResult reduceTypeFunctions(TypePackId entrypoint, Location location, NotNull ctx, bool force) { InstanceCollector collector; @@ -831,3227 +780,4 @@ bool isPending(TypeId ty, ConstraintSolver* solver) return is(ty) || (solver && solver->hasUnresolvedConstraints(ty)); } -template -static std::optional> tryDistributeTypeFunctionApp( - F f, - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx, - Args&&... args -) -{ - // op (a | b) (c | d) ~ (op a (c | d)) | (op b (c | d)) ~ (op a c) | (op a d) | (op b c) | (op b d) - Reduction reductionStatus = Reduction::MaybeOk; - std::vector blockedTypes; - std::vector results; - size_t cartesianProductSize = 1; - - const UnionType* firstUnion = nullptr; - size_t unionIndex = 0; - - std::vector arguments = typeParams; - for (size_t i = 0; i < arguments.size(); ++i) - { - const UnionType* ut = get(follow(arguments[i])); - if (!ut) - continue; - - // We want to find the first union type in the set of arguments to distribute that one and only that one union. - // The function `f` we have is recursive, so `arguments[unionIndex]` will be updated in-place for each option in - // the union we've found in this context, so that index will no longer be a union type. Any other arguments at - // index + 1 or after will instead be distributed, if those are a union, which will be subjected to the same rules. - if (!firstUnion && ut) - { - firstUnion = ut; - unionIndex = i; - } - - cartesianProductSize *= std::distance(begin(ut), end(ut)); - - // TODO: We'd like to report that the type function application is too complex here. - if (size_t(DFInt::LuauTypeFamilyApplicationCartesianProductLimit) <= cartesianProductSize) - return {{std::nullopt, Reduction::Erroneous, {}, {}}}; - } - - if (!firstUnion) - { - // If we couldn't find any union type argument, we're not distributing. - return std::nullopt; - } - - for (TypeId option : firstUnion) - { - arguments[unionIndex] = option; - - TypeFunctionReductionResult result = f(instance, arguments, packParams, ctx, args...); // NOLINT - blockedTypes.insert(blockedTypes.end(), result.blockedTypes.begin(), result.blockedTypes.end()); - if (result.reductionStatus != Reduction::MaybeOk) - reductionStatus = result.reductionStatus; - - if (reductionStatus != Reduction::MaybeOk || !result.result) - break; - else - results.push_back(*result.result); - } - - if (reductionStatus != Reduction::MaybeOk || !blockedTypes.empty()) - return {{std::nullopt, reductionStatus, std::move(blockedTypes), {}}}; - - if (!results.empty()) - { - if (results.size() == 1) - return {{results[0], Reduction::MaybeOk, {}, {}}}; - - TypeId resultTy = ctx->arena->addType(TypeFunctionInstanceType{ - NotNull{&builtinTypeFunctions().unionFunc}, - std::move(results), - {}, - }); - - if (ctx->solver) - ctx->pushConstraint(ReduceConstraint{resultTy}); - - return {{resultTy, Reduction::MaybeOk, {}, {}}}; - } - - return std::nullopt; -} - -struct FindUserTypeFunctionBlockers : TypeOnceVisitor -{ - NotNull ctx; - DenseHashSet blockingTypeMap{nullptr}; - std::vector blockingTypes; - - explicit FindUserTypeFunctionBlockers(NotNull ctx) - : TypeOnceVisitor(/* skipBoundTypes */ true) - , ctx(ctx) - { - } - - bool visit(TypeId ty) override - { - if (isPending(ty, ctx->solver)) - { - if (!blockingTypeMap.contains(ty)) - { - blockingTypeMap.insert(ty); - blockingTypes.push_back(ty); - } - } - return true; - } - - bool visit(TypePackId tp) override - { - return true; - } - - bool visit(TypeId ty, const ExternType&) override - { - return false; - } -}; - -static int evaluateTypeAliasCall(lua_State* L) -{ - TypeFun* tf = (TypeFun*)lua_tolightuserdata(L, lua_upvalueindex(1)); - - TypeFunctionRuntime* runtime = getTypeFunctionRuntime(L); - TypeFunctionRuntimeBuilderState* runtimeBuilder = runtime->runtimeBuilder; - - ApplyTypeFunction applyTypeFunction{runtimeBuilder->ctx->arena}; - - int argumentCount = lua_gettop(L); - std::vector rawTypeArguments; - - for (int i = 0; i < argumentCount; i++) - { - TypeFunctionTypeId tfty = getTypeUserData(L, i + 1); - TypeId ty = deserialize(tfty, runtimeBuilder); - - if (!runtimeBuilder->errors.empty()) - luaL_error(L, "failed to deserialize type at argument %d", i + 1); - - rawTypeArguments.push_back(ty); - } - - // Check if we have enough arguments, by typical typechecking rules - size_t typesRequired = tf->typeParams.size(); - size_t packsRequired = tf->typePackParams.size(); - - size_t typesProvided = rawTypeArguments.size() > typesRequired ? typesRequired : rawTypeArguments.size(); - size_t extraTypes = rawTypeArguments.size() > typesRequired ? rawTypeArguments.size() - typesRequired : 0; - size_t packsProvided = 0; - - if (extraTypes != 0 && packsProvided == 0) - { - // Extra types are only collected into a pack if a pack is expected - if (packsRequired != 0) - packsProvided += 1; - else - typesProvided += extraTypes; - } - - for (size_t i = typesProvided; i < typesRequired; ++i) - { - if (tf->typeParams[i].defaultValue) - typesProvided += 1; - } - - for (size_t i = packsProvided; i < packsRequired; ++i) - { - if (tf->typePackParams[i].defaultValue) - packsProvided += 1; - } - - if (extraTypes == 0 && packsProvided + 1 == packsRequired) - packsProvided += 1; - - if (typesProvided != typesRequired || packsProvided != packsRequired) - luaL_error(L, "not enough arguments to call"); - - // Prepare final types and packs - auto [types, packs] = saturateArguments(runtimeBuilder->ctx->arena, runtimeBuilder->ctx->builtins, *tf, rawTypeArguments, {}); - - for (size_t i = 0; i < types.size(); ++i) - applyTypeFunction.typeArguments[tf->typeParams[i].ty] = types[i]; - - for (size_t i = 0; i < packs.size(); ++i) - applyTypeFunction.typePackArguments[tf->typePackParams[i].tp] = packs[i]; - - std::optional maybeInstantiated = applyTypeFunction.substitute(tf->type); - - if (!maybeInstantiated.has_value()) - { - luaL_error(L, "failed to instantiate type alias"); - return true; - } - - TypeId target = follow(*maybeInstantiated); - - FunctionGraphReductionResult result = reduceTypeFunctions(target, Location{}, *runtimeBuilder->ctx); - - if (!result.errors.empty()) - luaL_error(L, "failed to reduce type function with: %s", toString(result.errors.front()).c_str()); - - TypeFunctionTypeId serializedTy = serialize(follow(target), runtimeBuilder); - - if (!runtimeBuilder->errors.empty()) - luaL_error(L, "%s", runtimeBuilder->errors.front().c_str()); - - allocTypeUserData(L, serializedTy->type); - return 1; -} - -TypeFunctionReductionResult userDefinedTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - auto typeFunction = getMutable(instance); - - if (typeFunction->userFuncData.owner.expired()) - { - ctx->ice->ice("user-defined type function module has expired"); - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - - if (!typeFunction->userFuncName || !typeFunction->userFuncData.definition) - { - ctx->ice->ice("all user-defined type functions must have an associated function definition"); - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - - // If type functions cannot be evaluated because of errors in the code, we do not generate any additional ones - if (!ctx->typeFunctionRuntime->allowEvaluation || typeFunction->userFuncData.definition->hasErrors) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - FindUserTypeFunctionBlockers check{ctx}; - - for (auto typeParam : typeParams) - check.traverse(follow(typeParam)); - - if (FFlag::LuauUserTypeFunctionAliases) - { - // Check that our environment doesn't depend on any type aliases that are blocked - for (auto& [name, definition] : typeFunction->userFuncData.environmentAlias) - { - if (definition.first->typeParams.empty() && definition.first->typePackParams.empty()) - check.traverse(follow(definition.first->type)); - } - } - - if (!check.blockingTypes.empty()) - return {std::nullopt, Reduction::MaybeOk, check.blockingTypes, {}}; - - // Ensure that whole type function environment is registered - for (auto& [name, definition] : typeFunction->userFuncData.environmentFunction) - { - // Cannot evaluate if a potential dependency couldn't be parsed - if (definition.first->hasErrors) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - if (std::optional error = ctx->typeFunctionRuntime->registerFunction(definition.first)) - { - // Failure to register at this point means that original definition had to error out and should not have been present in the - // environment - ctx->ice->ice("user-defined type function reference cannot be registered"); - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - } - - AstName name = typeFunction->userFuncData.definition->name; - - lua_State* global = ctx->typeFunctionRuntime->state.get(); - - if (global == nullptr) - return {std::nullopt, Reduction::Erroneous, {}, {}, format("'%s' type function: cannot be evaluated in this context", name.value)}; - - // Separate sandboxed thread for individual execution and private globals - lua_State* L = lua_newthread(global); - LuauTempThreadPopper popper(global); - - std::unique_ptr runtimeBuilder = std::make_unique(ctx); - - ScopedAssign setRuntimeBuilder(ctx->typeFunctionRuntime->runtimeBuilder, runtimeBuilder.get()); - ScopedAssign enableReduction(ctx->normalizer->sharedState->reentrantTypeReduction, false); - - // Build up the environment table of each function we have visible - for (auto& [_, curr] : typeFunction->userFuncData.environmentFunction) - { - // Environment table has to be filled only once in the current execution context - if (ctx->typeFunctionRuntime->initialized.find(curr.first)) - continue; - ctx->typeFunctionRuntime->initialized.insert(curr.first); - - lua_pushlightuserdata(L, curr.first); - lua_gettable(L, LUA_REGISTRYINDEX); - - if (!lua_isfunction(L, -1)) - { - ctx->ice->ice("user-defined type function reference cannot be found in the registry"); - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - - // Build up the environment of the current function, where some might not be visible - lua_getfenv(L, -1); - lua_setreadonly(L, -1, false); - - for (auto& [name, definition] : typeFunction->userFuncData.environmentFunction) - { - // Filter visibility based on original scope depth - if (definition.second >= curr.second) - { - lua_pushlightuserdata(L, definition.first); - lua_gettable(L, LUA_REGISTRYINDEX); - - if (!lua_isfunction(L, -1)) - break; // Don't have to report an error here, we will visit each function in outer loop - - lua_setfield(L, -2, name.c_str()); - } - } - - if (FFlag::LuauUserTypeFunctionAliases) - { - for (auto& [name, definition] : typeFunction->userFuncData.environmentAlias) - { - // Filter visibility based on original scope depth - if (definition.second >= curr.second) - { - if (definition.first->typeParams.empty() && definition.first->typePackParams.empty()) - { - TypeId ty = follow(definition.first->type); - - // This is checked at the top of the function, and should still be true. - LUAU_ASSERT(!isPending(ty, ctx->solver)); - - TypeFunctionTypeId serializedTy = serialize(ty, runtimeBuilder.get()); - - // Only register aliases that are representable in type environment - if (runtimeBuilder->errors.empty()) - { - allocTypeUserData(L, serializedTy->type); - lua_setfield(L, -2, name.c_str()); - } - } - else - { - lua_pushlightuserdata(L, definition.first); - lua_pushcclosure(L, evaluateTypeAliasCall, name.c_str(), 1); - lua_setfield(L, -2, name.c_str()); - } - } - } - } - - lua_setreadonly(L, -1, true); - lua_pop(L, 2); - } - - // Fetch the function we want to evaluate - lua_pushlightuserdata(L, typeFunction->userFuncData.definition); - lua_gettable(L, LUA_REGISTRYINDEX); - - if (!lua_isfunction(L, -1)) - { - ctx->ice->ice("user-defined type function reference cannot be found in the registry"); - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - - resetTypeFunctionState(L); - - // Push serialized arguments onto the stack - for (auto typeParam : typeParams) - { - TypeId ty = follow(typeParam); - // This is checked at the top of the function, and should still be true. - LUAU_ASSERT(!isPending(ty, ctx->solver)); - - TypeFunctionTypeId serializedTy = serialize(ty, runtimeBuilder.get()); - // Check if there were any errors while serializing - if (runtimeBuilder->errors.size() != 0) - return {std::nullopt, Reduction::Erroneous, {}, {}, runtimeBuilder->errors.front()}; - - allocTypeUserData(L, serializedTy->type); - } - - // Set up an interrupt handler for type functions to respect type checking limits and LSP cancellation requests. - lua_callbacks(L)->interrupt = [](lua_State* L, int gc) - { - auto ctx = static_cast(lua_getthreaddata(lua_mainthread(L))); - if (ctx->limits->finishTime && TimeTrace::getClock() > *ctx->limits->finishTime) - throw TimeLimitError(ctx->ice->moduleName); - - if (ctx->limits->cancellationToken && ctx->limits->cancellationToken->requested()) - throw UserCancelError(ctx->ice->moduleName); - }; - - ctx->typeFunctionRuntime->messages.clear(); - - if (auto error = checkResultForError(L, name.value, lua_pcall(L, int(typeParams.size()), 1, 0))) - return {std::nullopt, Reduction::Erroneous, {}, {}, std::move(error), ctx->typeFunctionRuntime->messages}; - - // If the return value is not a type userdata, return with error message - if (!isTypeUserData(L, 1)) - { - return { - std::nullopt, - Reduction::Erroneous, - {}, - {}, - format("'%s' type function: returned a non-type value", name.value), - ctx->typeFunctionRuntime->messages - }; - } - - TypeFunctionTypeId retTypeFunctionTypeId = getTypeUserData(L, 1); - - // No errors should be present here since we should've returned already if any were raised during serialization. - LUAU_ASSERT(runtimeBuilder->errors.size() == 0); - - TypeId retTypeId = deserialize(retTypeFunctionTypeId, runtimeBuilder.get()); - - // At least 1 error occurred while deserializing - if (runtimeBuilder->errors.size() > 0) - return {std::nullopt, Reduction::Erroneous, {}, {}, runtimeBuilder->errors.front(), ctx->typeFunctionRuntime->messages}; - - return {retTypeId, Reduction::MaybeOk, {}, {}, std::nullopt, ctx->typeFunctionRuntime->messages}; -} - -TypeFunctionReductionResult notTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 1 || !packParams.empty()) - { - ctx->ice->ice("not type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId ty = follow(typeParams.at(0)); - - if (ty == instance) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - if (isPending(ty, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {ty}, {}}; - - if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx)) - return *result; - - // `not` operates on anything and returns a `boolean` always. - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; -} - -TypeFunctionReductionResult lenTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 1 || !packParams.empty()) - { - ctx->ice->ice("len type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId operandTy = follow(typeParams.at(0)); - - if (operandTy == instance) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - // check to see if the operand type is resolved enough, and wait to reduce if not - // the use of `typeFromNormal` later necessitates blocking on local types. - if (isPending(operandTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; - - std::shared_ptr normTy = ctx->normalizer->normalize(operandTy); - NormalizationResult inhabited = ctx->normalizer->isInhabited(normTy.get()); - - // if the type failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!normTy || inhabited == NormalizationResult::HitLimits) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // if the operand type is error suppressing, we can immediately reduce to `number`. - if (normTy->shouldSuppressErrors()) - return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; - - // # always returns a number, even if its operand is never. - // if we're checking the length of a string, that works! - if (inhabited == NormalizationResult::False || normTy->isSubtypeOfString()) - return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; - - // we use the normalized operand here in case there was an intersection or union. - TypeId normalizedOperand = follow(ctx->normalizer->typeFromNormal(*normTy)); - if (normTy->hasTopTable() || get(normalizedOperand)) - return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; - - if (auto result = tryDistributeTypeFunctionApp(lenTypeFunction, instance, typeParams, packParams, ctx)) - return *result; - - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, operandTy, "__len", Location{}); - if (!mmType) - { - // If we have a metatable type with no __len, this means we still have a table with default length function - if (get(normalizedOperand)) - return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; - - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - - mmType = follow(*mmType); - if (isPending(*mmType, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - TypePackId inferredArgPack = ctx->arena->addTypePack({operandTy}); - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - // `len` must return a `number`. - return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; -} - -TypeFunctionReductionResult unmTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 1 || !packParams.empty()) - { - ctx->ice->ice("unm type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId operandTy = follow(typeParams.at(0)); - - if (operandTy == instance) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - // check to see if the operand type is resolved enough, and wait to reduce if not - if (isPending(operandTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; - - if (FFlag::LuauEagerGeneralization4) - operandTy = follow(operandTy); - - std::shared_ptr normTy = ctx->normalizer->normalize(operandTy); - - // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!normTy) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // if the operand is error suppressing, we can just go ahead and reduce. - if (normTy->shouldSuppressErrors()) - return {operandTy, Reduction::MaybeOk, {}, {}}; - - // if we have a `never`, we can never observe that the operation didn't work. - if (is(operandTy)) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - // If the type is exactly `number`, we can reduce now. - if (normTy->isExactlyNumber()) - return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; - - if (auto result = tryDistributeTypeFunctionApp(unmTypeFunction, instance, typeParams, packParams, ctx)) - return *result; - - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, operandTy, "__unm", Location{}); - if (!mmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - mmType = follow(*mmType); - if (isPending(*mmType, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - TypePackId inferredArgPack = ctx->arena->addTypePack({operandTy}); - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - if (std::optional ret = first(instantiatedMmFtv->retTypes)) - return {ret, Reduction::MaybeOk, {}, {}}; - else - return {std::nullopt, Reduction::Erroneous, {}, {}}; -} - -void dummyStateClose(lua_State*) {} - -TypeFunctionRuntime::TypeFunctionRuntime(NotNull ice, NotNull limits) - : ice(ice) - , limits(limits) - , state(nullptr, dummyStateClose) -{ -} - -TypeFunctionRuntime::~TypeFunctionRuntime() {} - -std::optional TypeFunctionRuntime::registerFunction(AstStatTypeFunction* function) -{ - // If evaluation is disabled, we do not generate additional error messages - if (!allowEvaluation) - return std::nullopt; - - // Do not evaluate type functions with parse errors inside - if (function->hasErrors) - return std::nullopt; - - prepareState(); - - lua_State* global = state.get(); - - // Fetch to check if function is already registered - lua_pushlightuserdata(global, function); - lua_gettable(global, LUA_REGISTRYINDEX); - - if (!lua_isnil(global, -1)) - { - lua_pop(global, 1); - return std::nullopt; - } - - lua_pop(global, 1); - - AstName name = function->name; - - // Construct ParseResult containing the type function - Allocator allocator; - AstNameTable names(allocator); - - AstExpr* exprFunction = function->body; - AstArray exprReturns{&exprFunction, 1}; - AstStatReturn stmtReturn{Location{}, exprReturns}; - AstStat* stmtArray[] = {&stmtReturn}; - AstArray stmts{stmtArray, 1}; - AstStatBlock exec{Location{}, stmts}; - ParseResult parseResult{&exec, 1, {}, {}, {}, CstNodeMap{nullptr}}; - - BytecodeBuilder builder; - try - { - compileOrThrow(builder, parseResult, names); - } - catch (CompileError& e) - { - return format("'%s' type function failed to compile with error message: %s", name.value, e.what()); - } - - std::string bytecode = builder.getBytecode(); - - // Separate sandboxed thread for individual execution and private globals - lua_State* L = lua_newthread(global); - LuauTempThreadPopper popper(global); - - // Create individual environment for the type function - luaL_sandboxthread(L); - - // Do not allow global writes to that environment - lua_pushvalue(L, LUA_GLOBALSINDEX); - lua_setreadonly(L, -1, true); - lua_pop(L, 1); - - // Load bytecode into Luau state - if (auto error = checkResultForError(L, name.value, luau_load(L, name.value, bytecode.data(), bytecode.size(), 0))) - return error; - - // Execute the global function which should return our user-defined type function - if (auto error = checkResultForError(L, name.value, lua_resume(L, nullptr, 0))) - return error; - - if (!lua_isfunction(L, -1)) - { - lua_pop(L, 1); - return format("Could not find '%s' type function in the global scope", name.value); - } - - // Store resulting function in the registry - lua_pushlightuserdata(global, function); - lua_xmove(L, global, 1); - lua_settable(global, LUA_REGISTRYINDEX); - - return std::nullopt; -} - -void TypeFunctionRuntime::prepareState() -{ - if (state) - return; - - state = StateRef(lua_newstate(typeFunctionAlloc, nullptr), lua_close); - lua_State* L = state.get(); - - lua_setthreaddata(L, this); - - setTypeFunctionEnvironment(L); - - registerTypeUserData(L); - - registerTypesLibrary(L); - - luaL_sandbox(L); - luaL_sandboxthread(L); -} - -TypeFunctionContext::TypeFunctionContext(NotNull cs, NotNull scope, NotNull constraint) - : arena(cs->arena) - , builtins(cs->builtinTypes) - , scope(scope) - , simplifier(cs->simplifier) - , normalizer(cs->normalizer) - , typeFunctionRuntime(cs->typeFunctionRuntime) - , ice(NotNull{&cs->iceReporter}) - , limits(NotNull{&cs->limits}) - , solver(cs.get()) - , constraint(constraint.get()) -{ -} - -NotNull TypeFunctionContext::pushConstraint(ConstraintV&& c) const -{ - LUAU_ASSERT(solver); - NotNull newConstraint = solver->pushConstraint(scope, constraint ? constraint->location : Location{}, std::move(c)); - - // Every constraint that is blocked on the current constraint must also be - // blocked on this new one. - if (constraint) - solver->inheritBlocks(NotNull{constraint}, newConstraint); - - return newConstraint; -} - -TypeFunctionReductionResult numericBinopTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx, - const std::string metamethod -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId lhsTy = follow(typeParams.at(0)); - TypeId rhsTy = follow(typeParams.at(1)); - - // isPending of `lhsTy` or `rhsTy` would return true, even if it cycles. We want a different answer for that. - if (lhsTy == instance || rhsTy == instance) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - // if we have a `never`, we can never observe that the math operator is unreachable. - if (is(lhsTy) || is(rhsTy)) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - const Location location = ctx->constraint ? ctx->constraint->location : Location{}; - - // check to see if both operand types are resolved enough, and wait to reduce if not - if (isPending(lhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (isPending(rhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - // TODO: Normalization needs to remove cyclic type functions from a `NormalizedType`. - std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); - std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); - - // if either failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!normLhsTy || !normRhsTy) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // if one of the types is error suppressing, we can reduce to `any` since we should suppress errors in the result of the usage. - if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) - return {ctx->builtins->anyType, Reduction::MaybeOk, {}, {}}; - - // if we're adding two `number` types, the result is `number`. - if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber()) - return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; - - if (auto result = tryDistributeTypeFunctionApp(numericBinopTypeFunction, instance, typeParams, packParams, ctx, metamethod)) - return *result; - - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, metamethod, location); - bool reversed = false; - if (!mmType) - { - mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, metamethod, location); - reversed = true; - } - - if (!mmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - mmType = follow(*mmType); - if (isPending(*mmType, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - - TypePackId argPack = ctx->arena->addTypePack({lhsTy, rhsTy}); - SolveResult solveResult; - - if (!reversed) - solveResult = solveFunctionCall( - ctx->arena, - ctx->builtins, - ctx->simplifier, - ctx->normalizer, - ctx->typeFunctionRuntime, - ctx->ice, - ctx->limits, - ctx->scope, - location, - *mmType, - argPack - ); - else - { - TypePack* p = getMutable(argPack); - std::swap(p->head.front(), p->head.back()); - solveResult = solveFunctionCall( - ctx->arena, - ctx->builtins, - ctx->simplifier, - ctx->normalizer, - ctx->typeFunctionRuntime, - ctx->ice, - ctx->limits, - ctx->scope, - location, - *mmType, - argPack - ); - } - - if (!solveResult.typePackId.has_value()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); - if (extracted.head.empty()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - return {extracted.head.front(), Reduction::MaybeOk, {}, {}}; -} - -TypeFunctionReductionResult addTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("add type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__add"); -} - -TypeFunctionReductionResult subTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("sub type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__sub"); -} - -TypeFunctionReductionResult mulTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("mul type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__mul"); -} - -TypeFunctionReductionResult divTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("div type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__div"); -} - -TypeFunctionReductionResult idivTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("integer div type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__idiv"); -} - -TypeFunctionReductionResult powTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("pow type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__pow"); -} - -TypeFunctionReductionResult modTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("modulo type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return numericBinopTypeFunction(instance, typeParams, packParams, ctx, "__mod"); -} - -TypeFunctionReductionResult concatTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("concat type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId lhsTy = follow(typeParams.at(0)); - TypeId rhsTy = follow(typeParams.at(1)); - - // isPending of `lhsTy` or `rhsTy` would return true, even if it cycles. We want a different answer for that. - if (lhsTy == instance || rhsTy == instance) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - // check to see if both operand types are resolved enough, and wait to reduce if not - if (isPending(lhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (isPending(rhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); - std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); - - // if either failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!normLhsTy || !normRhsTy) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // if one of the types is error suppressing, we can reduce to `any` since we should suppress errors in the result of the usage. - if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) - return {ctx->builtins->anyType, Reduction::MaybeOk, {}, {}}; - - // if we have a `never`, we can never observe that the operator didn't work. - if (is(lhsTy) || is(rhsTy)) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - // if we're concatenating two elements that are either strings or numbers, the result is `string`. - if ((normLhsTy->isSubtypeOfString() || normLhsTy->isExactlyNumber()) && (normRhsTy->isSubtypeOfString() || normRhsTy->isExactlyNumber())) - return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; - - if (auto result = tryDistributeTypeFunctionApp(concatTypeFunction, instance, typeParams, packParams, ctx)) - return *result; - - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, "__concat", Location{}); - bool reversed = false; - if (!mmType) - { - mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, "__concat", Location{}); - reversed = true; - } - - if (!mmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - mmType = follow(*mmType); - if (isPending(*mmType, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - std::vector inferredArgs; - if (!reversed) - inferredArgs = {lhsTy, rhsTy}; - else - inferredArgs = {rhsTy, lhsTy}; - - TypePackId inferredArgPack = ctx->arena->addTypePack(std::move(inferredArgs)); - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; -} - -TypeFunctionReductionResult andTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("and type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId lhsTy = follow(typeParams.at(0)); - TypeId rhsTy = follow(typeParams.at(1)); - - // t1 = and ~> lhs - if (follow(rhsTy) == instance && lhsTy != rhsTy) - return {lhsTy, Reduction::MaybeOk, {}, {}}; - // t1 = and ~> rhs - if (follow(lhsTy) == instance && lhsTy != rhsTy) - return {rhsTy, Reduction::MaybeOk, {}, {}}; - - // check to see if both operand types are resolved enough, and wait to reduce if not - if (isPending(lhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (isPending(rhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - // And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is truthy. - SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->falsyType); - SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); - std::vector blockedTypes{}; - for (auto ty : filteredLhs.blockedTypes) - blockedTypes.push_back(ty); - for (auto ty : overallResult.blockedTypes) - blockedTypes.push_back(ty); - return {overallResult.result, Reduction::MaybeOk, std::move(blockedTypes), {}}; -} - -TypeFunctionReductionResult orTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("or type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId lhsTy = follow(typeParams.at(0)); - TypeId rhsTy = follow(typeParams.at(1)); - - // t1 = or ~> lhs - if (follow(rhsTy) == instance && lhsTy != rhsTy) - return {lhsTy, Reduction::MaybeOk, {}, {}}; - // t1 = or ~> rhs - if (follow(lhsTy) == instance && lhsTy != rhsTy) - return {rhsTy, Reduction::MaybeOk, {}, {}}; - - // check to see if both operand types are resolved enough, and wait to reduce if not - if (FFlag::LuauEagerGeneralization4) - { - if (is(lhsTy)) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (is(rhsTy)) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - } - else - { - if (isPending(lhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (isPending(rhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - } - - // Or evalutes to the LHS type if the LHS is truthy, and the RHS type if LHS is falsy. - SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->truthyType); - SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); - std::vector blockedTypes{}; - for (auto ty : filteredLhs.blockedTypes) - blockedTypes.push_back(ty); - for (auto ty : overallResult.blockedTypes) - blockedTypes.push_back(ty); - return {overallResult.result, Reduction::MaybeOk, std::move(blockedTypes), {}}; -} - -static TypeFunctionReductionResult comparisonTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx, - const std::string metamethod -) -{ - - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId lhsTy = follow(typeParams.at(0)); - TypeId rhsTy = follow(typeParams.at(1)); - - if (lhsTy == instance || rhsTy == instance) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - if (FFlag::LuauEagerGeneralization4) - { - if (is(lhsTy)) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (is(rhsTy)) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - } - else - { - if (isPending(lhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (isPending(rhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - } - - // Algebra Reduction Rules for comparison type functions - // Note that comparing to never tells you nothing about the other operand - // lt< 'a , never> -> continue - // lt< never, 'a> -> continue - // lt< 'a, t> -> 'a is t - we'll solve the constraint, return and solve lt -> bool - // lt< t, 'a> -> same as above - bool canSubmitConstraint = ctx->solver && ctx->constraint; - bool lhsFree = get(lhsTy) != nullptr; - bool rhsFree = get(rhsTy) != nullptr; - if (canSubmitConstraint) - { - // Implement injective type functions for comparison type functions - // lt implies t is number - // lt implies t is number - if (lhsFree && isNumber(rhsTy)) - emplaceType(asMutable(lhsTy), ctx->builtins->numberType); - else if (rhsFree && isNumber(lhsTy)) - emplaceType(asMutable(rhsTy), ctx->builtins->numberType); - } - - // The above might have caused the operand types to be rebound, we need to follow them again - lhsTy = follow(lhsTy); - rhsTy = follow(rhsTy); - - // check to see if both operand types are resolved enough, and wait to reduce if not - - std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); - std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); - NormalizationResult lhsInhabited = ctx->normalizer->isInhabited(normLhsTy.get()); - NormalizationResult rhsInhabited = ctx->normalizer->isInhabited(normRhsTy.get()); - - // if either failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!normLhsTy || !normRhsTy || lhsInhabited == NormalizationResult::HitLimits || rhsInhabited == NormalizationResult::HitLimits) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // if one of the types is error suppressing, we can just go ahead and reduce. - if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; - - // if we have an uninhabited type (e.g. `never`), we can never observe that the comparison didn't work. - if (lhsInhabited == NormalizationResult::False || rhsInhabited == NormalizationResult::False) - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; - - // If both types are some strict subset of `string`, we can reduce now. - if (normLhsTy->isSubtypeOfString() && normRhsTy->isSubtypeOfString()) - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; - - // If both types are exactly `number`, we can reduce now. - if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber()) - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; - - if (auto result = tryDistributeTypeFunctionApp(comparisonTypeFunction, instance, typeParams, packParams, ctx, metamethod)) - return *result; - - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, metamethod, Location{}); - if (!mmType) - mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, metamethod, Location{}); - - if (!mmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - mmType = follow(*mmType); - if (isPending(*mmType, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - TypePackId inferredArgPack = ctx->arena->addTypePack({lhsTy, rhsTy}); - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; -} - -TypeFunctionReductionResult ltTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("lt type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return comparisonTypeFunction(instance, typeParams, packParams, ctx, "__lt"); -} - -TypeFunctionReductionResult leTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("le type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return comparisonTypeFunction(instance, typeParams, packParams, ctx, "__le"); -} - -TypeFunctionReductionResult eqTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("eq type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId lhsTy = follow(typeParams.at(0)); - TypeId rhsTy = follow(typeParams.at(1)); - - // check to see if both operand types are resolved enough, and wait to reduce if not - if (isPending(lhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (isPending(rhsTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); - std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); - NormalizationResult lhsInhabited = ctx->normalizer->isInhabited(normLhsTy.get()); - NormalizationResult rhsInhabited = ctx->normalizer->isInhabited(normRhsTy.get()); - - // if either failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!normLhsTy || !normRhsTy || lhsInhabited == NormalizationResult::HitLimits || rhsInhabited == NormalizationResult::HitLimits) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // if one of the types is error suppressing, we can just go ahead and reduce. - if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; - - // if we have a `never`, we can never observe that the comparison didn't work. - if (lhsInhabited == NormalizationResult::False || rhsInhabited == NormalizationResult::False) - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; - - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, "__eq", Location{}); - if (!mmType) - mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, "__eq", Location{}); - - // if neither type has a metatable entry for `__eq`, then we'll check for inhabitance of the intersection! - NormalizationResult intersectInhabited = ctx->normalizer->isIntersectionInhabited(lhsTy, rhsTy); - if (!mmType) - { - if (intersectInhabited == NormalizationResult::True) - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; // if it's inhabited, everything is okay! - - // we might be in a case where we still want to accept the comparison... - if (intersectInhabited == NormalizationResult::False) - { - // if they're both subtypes of `string` but have no common intersection, the comparison is allowed but always `false`. - if (normLhsTy->isSubtypeOfString() && normRhsTy->isSubtypeOfString()) - return {ctx->builtins->falseType, Reduction::MaybeOk, {}, {}}; - - // if they're both subtypes of `boolean` but have no common intersection, the comparison is allowed but always `false`. - if (normLhsTy->isSubtypeOfBooleans() && normRhsTy->isSubtypeOfBooleans()) - return {ctx->builtins->falseType, Reduction::MaybeOk, {}, {}}; - } - - return {std::nullopt, Reduction::Erroneous, {}, {}}; // if it's not, then this type function is irreducible! - } - - mmType = follow(*mmType); - if (isPending(*mmType, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - TypePackId inferredArgPack = ctx->arena->addTypePack({lhsTy, rhsTy}); - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; -} - -// Collect types that prevent us from reducing a particular refinement. -struct FindRefinementBlockers : TypeOnceVisitor -{ - DenseHashSet found{nullptr}; - bool visit(TypeId ty, const BlockedType&) override - { - found.insert(ty); - return false; - } - - bool visit(TypeId ty, const PendingExpansionType&) override - { - found.insert(ty); - return false; - } - - bool visit(TypeId ty, const ExternType&) override - { - return false; - } -}; - -struct ContainsRefinableType : TypeOnceVisitor -{ - bool found = false; - ContainsRefinableType() - : TypeOnceVisitor(/* skipBoundTypes */ true) - { - } - - - bool visit(TypeId ty) override - { - // Default case: if we find *some* type that's worth refining against, - // then we can claim that this type contains a refineable type. - found = true; - return false; - } - - bool visit(TypeId Ty, const NoRefineType&) override - { - // No refine types aren't interesting - return false; - } - - bool visit(TypeId ty, const TableType&) override - { - return !found; - } - bool visit(TypeId ty, const MetatableType&) override - { - return !found; - } - bool visit(TypeId ty, const FunctionType&) override - { - return !found; - } - bool visit(TypeId ty, const UnionType&) override - { - return !found; - } - bool visit(TypeId ty, const IntersectionType&) override - { - return !found; - } - bool visit(TypeId ty, const NegationType&) override - { - return !found; - } -}; - -namespace -{ - -bool isTruthyOrFalsyType(TypeId ty) -{ - ty = follow(ty); - return isApproximatelyTruthyType(ty) || isApproximatelyFalsyType(ty); -} - -struct RefineTypeScrubber : public Substitution -{ - NotNull ctx; - TypeId needle; - - explicit RefineTypeScrubber(NotNull ctx, TypeId needle) - : Substitution(ctx->arena) - , ctx{ctx} - , needle{needle} - { - } - - bool isDirty(TypePackId tp) override - { - return false; - } - - bool ignoreChildren(TypePackId tp) override - { - return false; - } - - TypePackId clean(TypePackId tp) override - { - return tp; - } - - bool isDirty(TypeId ty) override - { - if (auto ut = get(ty)) - { - for (auto option : ut) - { - if (option == needle) - return true; - } - } - else if (auto it = get(ty)) - { - for (auto part : it) - { - if (part == needle) - return true; - } - } - return false; - } - - bool ignoreChildren(TypeId ty) override - { - return !is(ty); - } - - TypeId clean(TypeId ty) override - { - // NOTE: this feels pretty similar to other places where we try to - // filter over a set type, may be worth combining those in the future. - if (auto ut = get(ty)) - { - TypeIds newOptions; - for (auto option : ut) - { - if (option != needle && !is(option)) - newOptions.insert(option); - } - if (newOptions.empty()) - return ctx->builtins->neverType; - else if (newOptions.size() == 1) - return *newOptions.begin(); - else - return ctx->arena->addType(UnionType{newOptions.take()}); - } - else if (auto it = get(ty)) - { - TypeIds newParts; - for (auto part : it) - { - if (part != needle && !is(part)) - newParts.insert(part); - } - if (newParts.empty()) - return ctx->builtins->unknownType; - else if (newParts.size() == 1) - return *newParts.begin(); - else - return ctx->arena->addType(IntersectionType{newParts.take()}); - } - return ty; - } - -}; - -bool occurs(TypeId haystack, TypeId needle, DenseHashSet& seen) -{ - if (needle == haystack) - return true; - - if (seen.contains(haystack)) - return false; - - seen.insert(haystack); - - if (auto ut = get(haystack)) - { - for (auto option : ut) - if (occurs(option, needle, seen)) - return true; - } - - if (auto it = get(haystack)) - { - for (auto part : it) - if (occurs(part, needle, seen)) - return true; - } - - return false; -} - -bool occurs(TypeId haystack, TypeId needle) -{ - DenseHashSet seen{nullptr}; - return occurs(haystack, needle, seen); -} - -} // namespace - -TypeFunctionReductionResult refineTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() < 2 || !packParams.empty()) - { - ctx->ice->ice("refine type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId targetTy = follow(typeParams.at(0)); - - if (FFlag::LuauOccursCheckForRefinement) - { - // If we end up minting a refine type like: - // - // t1 where t1 = refine - // - // This can create a degenerate set type such as: - // - // t1 where t1 = (T | t1) & Y - // - // Instead, we can clip the recursive part: - // - // t1 where t1 = refine => refine - if (!FFlag::LuauAvoidExcessiveTypeCopying || occurs(targetTy, instance)) - { - RefineTypeScrubber rts{ctx, instance}; - if (auto result = rts.substitute(targetTy)) - targetTy = *result; - } - } - - std::vector discriminantTypes; - for (size_t i = 1; i < typeParams.size(); i++) - discriminantTypes.push_back(follow(typeParams.at(i))); - - const bool targetIsPending = FFlag::LuauEagerGeneralization4 ? is(targetTy) - : isPending(targetTy, ctx->solver); - - // check to see if both operand types are resolved enough, and wait to reduce if not - if (targetIsPending) - return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; - else - { - for (auto t : discriminantTypes) - { - if (isPending(t, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {t}, {}}; - } - } - - // If we have a blocked type in the target, we *could* potentially - // refine it, but more likely we end up with some type explosion in - // normalization. - FindRefinementBlockers frb; - frb.traverse(targetTy); - if (!frb.found.empty()) - return {std::nullopt, Reduction::MaybeOk, {frb.found.begin(), frb.found.end()}, {}}; - - // Refine a target type and a discriminant one at a time. - // Returns result : TypeId, toBlockOn : vector - auto stepRefine = [&ctx](TypeId target, TypeId discriminant) -> std::pair> - { - std::vector toBlock; - // we need a more complex check for blocking on the discriminant in particular - FindRefinementBlockers frb; - frb.traverse(discriminant); - - if (!frb.found.empty()) - return {nullptr, {frb.found.begin(), frb.found.end()}}; - - if (FFlag::DebugLuauEqSatSimplification) - { - auto simplifyResult = eqSatSimplify(ctx->simplifier, ctx->arena->addType(IntersectionType{{target, discriminant}})); - if (simplifyResult) - { - if (ctx->solver) - { - for (TypeId newTf : simplifyResult->newTypeFunctions) - ctx->pushConstraint(ReduceConstraint{newTf}); - } - - return {simplifyResult->result, {}}; - } - else - return {nullptr, {}}; - } - else - { - // If the discriminant type is only: - // - The `*no-refine*` type or, - // - tables, metatables, unions, intersections, functions, or negations _containing_ `*no-refine*`. - // There's no point in refining against it. - ContainsRefinableType crt; - crt.traverse(discriminant); - if (!crt.found) - return {target, {}}; - - if (FFlag::LuauRefineTablesWithReadType) - { - if (auto ty = intersectWithSimpleDiscriminant(ctx->builtins, ctx->arena, target, discriminant)) - return {*ty, {}}; - } - - // NOTE: This block causes us to refine too early in some cases. - if (auto negation = get(discriminant)) - { - if (auto primitive = get(follow(negation->ty)); primitive && primitive->type == PrimitiveType::NilType) - { - SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, target, discriminant); - return {result.result, {}}; - } - } - - // If the target type is a table, then simplification already implements the logic to deal with refinements properly since the - // type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type. - // We also fire for simple discriminants such as false? and ~(false?): the falsy and truthy types respectively. - if (is(target) || isTruthyOrFalsyType(discriminant)) - { - SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, target, discriminant); - if (FFlag::LuauEagerGeneralization4) - { - // Simplification considers free and generic types to be - // 'blocking', but that's not suitable for refine<>. - // - // If we are only blocked on those types, we consider - // the simplification a success and reduce. - if (std::all_of( - begin(result.blockedTypes), - end(result.blockedTypes), - [](auto&& v) - { - return is(follow(v)); - } - )) - { - return {result.result, {}}; - } - else - return {nullptr, {result.blockedTypes.begin(), result.blockedTypes.end()}}; - } - else - { - if (!result.blockedTypes.empty()) - return {nullptr, {result.blockedTypes.begin(), result.blockedTypes.end()}}; - } - return {result.result, {}}; - } - - - // In the general case, we'll still use normalization though. - TypeId intersection = ctx->arena->addType(IntersectionType{{target, discriminant}}); - std::shared_ptr normIntersection = ctx->normalizer->normalize(intersection); - std::shared_ptr normType = ctx->normalizer->normalize(target); - - // if the intersection failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!normIntersection || !normType) - return {nullptr, {}}; - - TypeId resultTy = ctx->normalizer->typeFromNormal(*normIntersection); - // include the error type if the target type is error-suppressing and the intersection we computed is not - if (normType->shouldSuppressErrors() && !normIntersection->shouldSuppressErrors()) - resultTy = ctx->arena->addType(UnionType{{resultTy, ctx->builtins->errorType}}); - - return {resultTy, {}}; - } - - }; - - // refine target with each discriminant type in sequence (reverse of insertion order) - // If we cannot proceed, block. If all discriminant types refine successfully, return - // the result - TypeId target = targetTy; - while (!discriminantTypes.empty()) - { - TypeId discriminant = discriminantTypes.back(); - auto [refined, blocked] = stepRefine(target, discriminant); - - if (blocked.empty() && refined == nullptr) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - if (!blocked.empty()) - return {std::nullopt, Reduction::MaybeOk, blocked, {}}; - - target = refined; - discriminantTypes.pop_back(); - } - return {target, Reduction::MaybeOk, {}, {}}; -} - -TypeFunctionReductionResult singletonTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 1 || !packParams.empty()) - { - ctx->ice->ice("singleton type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId type = follow(typeParams.at(0)); - - // check to see if both operand types are resolved enough, and wait to reduce if not - if (isPending(type, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {type}, {}}; - - TypeId followed = type; - // we want to follow through a negation here as well. - if (auto negation = get(followed)) - followed = follow(negation->ty); - - // if we have a singleton type or `nil`, which is its own singleton type... - if (get(followed) || isNil(followed)) - return {type, Reduction::MaybeOk, {}, {}}; - - // otherwise, we'll return the top type, `unknown`. - return {ctx->builtins->unknownType, Reduction::MaybeOk, {}, {}}; -} - -struct CollectUnionTypeOptions : TypeOnceVisitor -{ - NotNull ctx; - DenseHashSet options{nullptr}; - DenseHashSet blockingTypes{nullptr}; - - explicit CollectUnionTypeOptions(NotNull ctx) - : TypeOnceVisitor(/* skipBoundTypes */ true) - , ctx(ctx) - { - } - - bool visit(TypeId ty) override - { - options.insert(ty); - if (isPending(ty, ctx->solver)) - blockingTypes.insert(ty); - return false; - } - - bool visit(TypePackId tp) override - { - return false; - } - - bool visit(TypeId ty, const UnionType& ut) override - { - // If we have something like: - // - // union - // - // We probably just want to consider this to be the same as - // - // union - return true; - } - - bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override - { - if (tfit.function->name != builtinTypeFunctions().unionFunc.name) - { - options.insert(ty); - blockingTypes.insert(ty); - return false; - } - return true; - } -}; - -TypeFunctionReductionResult unionTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (!packParams.empty()) - { - ctx->ice->ice("union type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - // if we only have one parameter, there's nothing to do. - if (typeParams.size() == 1) - return {follow(typeParams[0]), Reduction::MaybeOk, {}, {}}; - - - CollectUnionTypeOptions collector{ctx}; - collector.traverse(instance); - - if (!collector.blockingTypes.empty()) - { - std::vector blockingTypes{collector.blockingTypes.begin(), collector.blockingTypes.end()}; - return {std::nullopt, Reduction::MaybeOk, std::move(blockingTypes), {}}; - } - - TypeId resultTy = ctx->builtins->neverType; - for (auto ty : collector.options) - { - SimplifyResult result = simplifyUnion(ctx->builtins, ctx->arena, resultTy, ty); - // This condition might fire if one of the arguments to this type - // function is a free type somewhere deep in a nested union or - // intersection type, even though we ran a pass above to capture - // some blocked types. - if (!result.blockedTypes.empty()) - return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}}; - - resultTy = result.result; - } - - return {resultTy, Reduction::MaybeOk, {}, {}}; -} - - -TypeFunctionReductionResult intersectTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (!packParams.empty()) - { - ctx->ice->ice("intersect type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - // if we only have one parameter, there's nothing to do. - if (typeParams.size() == 1) - return {follow(typeParams[0]), Reduction::MaybeOk, {}, {}}; - - // we need to follow all of the type parameters. - std::vector types; - types.reserve(typeParams.size()); - for (auto ty : typeParams) - types.emplace_back(follow(ty)); - - // if we only have two parameters and one is `*no-refine*`, we're all done. - if (types.size() == 2 && get(types[1])) - return {types[0], Reduction::MaybeOk, {}, {}}; - else if (types.size() == 2 && get(types[0])) - return {types[1], Reduction::MaybeOk, {}, {}}; - - // check to see if the operand types are resolved enough, and wait to reduce if not - // if any of them are `never`, the intersection will always be `never`, so we can reduce directly. - for (auto ty : types) - { - if (isPending(ty, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {ty}, {}}; - else if (get(ty)) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - } - - // fold over the types with `simplifyIntersection` - TypeId resultTy = ctx->builtins->unknownType; - // collect types which caused intersection to return never - DenseHashSet unintersectableTypes{nullptr}; - for (auto ty : types) - { - // skip any `*no-refine*` types. - if (get(ty)) - continue; - - if (FFlag::LuauRefineTablesWithReadType) - { - if (auto simpleResult = intersectWithSimpleDiscriminant(ctx->builtins, ctx->arena, resultTy, ty)) - { - if (get(*simpleResult)) - unintersectableTypes.insert(follow(ty)); - else - resultTy = *simpleResult; - continue; - } - } - - SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, resultTy, ty); - - // If simplifying the intersection returned never, note the type we tried to intersect it with, and continue trying to intersect with the - // rest - if (get(result.result)) - { - unintersectableTypes.insert(follow(ty)); - continue; - } - for (TypeId blockedType : result.blockedTypes) - { - if (!get(blockedType)) - return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}}; - } - - resultTy = result.result; - } - - if (!unintersectableTypes.empty()) - { - unintersectableTypes.insert(resultTy); - if (unintersectableTypes.size() > 1) - { - TypeId intersection = - ctx->arena->addType(IntersectionType{std::vector(unintersectableTypes.begin(), unintersectableTypes.end())}); - return {intersection, Reduction::MaybeOk, {}, {}}; - } - else - { - return {*unintersectableTypes.begin(), Reduction::MaybeOk, {}, {}}; - } - } - // if the intersection simplifies to `never`, this gives us bad autocomplete. - // we'll just produce the intersection plainly instead, but this might be revisitable - // if we ever give `never` some kind of "explanation" trail. - if (get(resultTy)) - { - TypeId intersection = ctx->arena->addType(IntersectionType{typeParams}); - return {intersection, Reduction::MaybeOk, {}, {}}; - } - - return {resultTy, Reduction::MaybeOk, {}, {}}; -} - -// computes the keys of `ty` into `result` -// `isRaw` parameter indicates whether or not we should follow __index metamethods -// returns `false` if `result` should be ignored because the answer is "all strings" -bool computeKeysOf_DEPRECATED(TypeId ty, Set& result, DenseHashSet& seen, bool isRaw, NotNull ctx) -{ - // if the type is the top table type, the answer is just "all strings" - if (get(ty)) - return false; - - // if we've already seen this type, we can do nothing - if (seen.contains(ty)) - return true; - seen.insert(ty); - - // if we have a particular table type, we can insert the keys - if (auto tableTy = get(ty)) - { - if (tableTy->indexer) - { - // if we have a string indexer, the answer is, again, "all strings" - if (isString(tableTy->indexer->indexType)) - return false; - } - - for (auto [key, _] : tableTy->props) - result.insert(key); - return true; - } - - // otherwise, we have a metatable to deal with - if (auto metatableTy = get(ty)) - { - bool res = true; - - if (!isRaw) - { - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, ty, "__index", Location{}); - if (mmType) - res = res && computeKeysOf_DEPRECATED(*mmType, result, seen, isRaw, ctx); - } - - res = res && computeKeysOf_DEPRECATED(metatableTy->table, result, seen, isRaw, ctx); - - return res; - } - - if (auto classTy = get(ty)) - { - for (auto [key, _] : classTy->props) // NOLINT(performance-for-range-copy) - result.insert(key); - - bool res = true; - if (classTy->metatable && !isRaw) - { - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, ty, "__index", Location{}); - if (mmType) - res = res && computeKeysOf_DEPRECATED(*mmType, result, seen, isRaw, ctx); - } - - if (classTy->parent) - res = res && computeKeysOf_DEPRECATED(follow(*classTy->parent), result, seen, isRaw, ctx); - - return res; - } - - // this should not be reachable since the type should be a valid tables or extern types part from normalization. - LUAU_ASSERT(false); - return false; -} - -namespace { - -/** - * Computes the keys of `ty` into `result` - * `isRaw` parameter indicates whether or not we should follow __index metamethods - * returns `false` if `result` should be ignored because the answer is "all strings" - */ -bool computeKeysOf(TypeId ty, Set>& result, DenseHashSet& seen, bool isRaw, NotNull ctx) -{ - - // if the type is the top table type, the answer is just "all strings" - if (get(ty)) - return false; - - // if we've already seen this type, we can do nothing - if (seen.contains(ty)) - return true; - seen.insert(ty); - - // if we have a particular table type, we can insert the keys - if (auto tableTy = get(ty)) - { - if (tableTy->indexer) - { - // if we have a string indexer, the answer is, again, "all strings" - if (isString(tableTy->indexer->indexType)) - return false; - } - - for (const auto& [key, _] : tableTy->props) - result.insert(key); - return true; - } - - // otherwise, we have a metatable to deal with - if (auto metatableTy = get(ty)) - { - bool res = true; - - if (!isRaw) - { - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, ty, "__index", Location{}); - if (mmType) - res = res && computeKeysOf(*mmType, result, seen, isRaw, ctx); - } - - res = res && computeKeysOf(metatableTy->table, result, seen, isRaw, ctx); - - return res; - } - - if (auto classTy = get(ty)) - { - for (const auto& [key, _] : classTy->props) - result.insert(key); - - bool res = true; - if (classTy->metatable && !isRaw) - { - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, ty, "__index", Location{}); - if (mmType) - res = res && computeKeysOf(*mmType, result, seen, isRaw, ctx); - } - - if (classTy->parent) - res = res && computeKeysOf(follow(*classTy->parent), result, seen, isRaw, ctx); - - return res; - } - - // this should not be reachable since the type should be a valid tables or extern types part from normalization. - LUAU_ASSERT(false); - return false; -} - -} - -TypeFunctionReductionResult keyofFunctionImpl( - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx, - bool isRaw -) -{ - if (typeParams.size() != 1 || !packParams.empty()) - { - ctx->ice->ice("keyof type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId operandTy = follow(typeParams.at(0)); - - std::shared_ptr normTy = ctx->normalizer->normalize(operandTy); - - // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!normTy) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // if we don't have either just tables or just extern types, we've got nothing to get keys of (at least until a future version perhaps adds extern - // types as well) - if (normTy->hasTables() == normTy->hasExternTypes()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - // this is sort of atrocious, but we're trying to reject any type that has not normalized to a table or a union of tables. - if (normTy->hasTops() || normTy->hasBooleans() || normTy->hasErrors() || normTy->hasNils() || normTy->hasNumbers() || normTy->hasStrings() || - normTy->hasThreads() || normTy->hasBuffers() || normTy->hasFunctions() || normTy->hasTyvars()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - if (FFlag::LuauEmptyStringInKeyOf) - { - // We're going to collect the keys in here, and we use optional strings - // so that we can differentiate between the empty string and _no_ string. - Set> keys{std::nullopt}; - - // computing the keys for extern types - if (normTy->hasExternTypes()) - { - LUAU_ASSERT(!normTy->hasTables()); - - // seen set for key computation for extern types - DenseHashSet seen{{}}; - - auto externTypeIter = normTy->externTypes.ordering.begin(); - auto externTypeIterEnd = normTy->externTypes.ordering.end(); - LUAU_ASSERT(externTypeIter != externTypeIterEnd); // should be guaranteed by the `hasExternTypes` check earlier - - // collect all the properties from the first class type - if (!computeKeysOf(*externTypeIter, keys, seen, isRaw, ctx)) - return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; // if it failed, we have a top type! - - // we need to look at each class to remove any keys that are not common amongst them all - while (++externTypeIter != externTypeIterEnd) - { - seen.clear(); // we'll reuse the same seen set - - Set> localKeys{std::nullopt}; - - // we can skip to the next class if this one is a top type - if (!computeKeysOf(*externTypeIter, localKeys, seen, isRaw, ctx)) - continue; - - for (auto& key : keys) - { - // remove any keys that are not present in each class - if (!localKeys.contains(key)) - keys.erase(key); - } - } - } - - // computing the keys for tables - if (normTy->hasTables()) - { - LUAU_ASSERT(!normTy->hasExternTypes()); - - // seen set for key computation for tables - DenseHashSet seen{{}}; - - auto tablesIter = normTy->tables.begin(); - LUAU_ASSERT(tablesIter != normTy->tables.end()); // should be guaranteed by the `hasTables` check earlier - - // collect all the properties from the first table type - if (!computeKeysOf(*tablesIter, keys, seen, isRaw, ctx)) - return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; // if it failed, we have the top table type! - - // we need to look at each tables to remove any keys that are not common amongst them all - while (++tablesIter != normTy->tables.end()) - { - seen.clear(); // we'll reuse the same seen set - - Set> localKeys{std::nullopt}; - - // we can skip to the next table if this one is the top table type - if (!computeKeysOf(*tablesIter, localKeys, seen, isRaw, ctx)) - continue; - - for (auto& key : keys) - { - // remove any keys that are not present in each table - if (!localKeys.contains(key)) - keys.erase(key); - } - } - } - - // if the set of keys is empty, `keyof` is `never` - if (keys.empty()) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - // everything is validated, we need only construct our big union of singletons now! - std::vector singletons; - singletons.reserve(keys.size()); - - for (const auto& key : keys) - { - if (key) - singletons.push_back(ctx->arena->addType(SingletonType{StringSingleton{*key}})); - } - - // If there's only one entry, we don't need a UnionType. - // We can take straight take it from the first entry - // because it was added into the type arena already. - if (singletons.size() == 1) - return {singletons.front(), Reduction::MaybeOk, {}, {}}; - - return {ctx->arena->addType(UnionType{std::move(singletons)}), Reduction::MaybeOk, {}, {}}; - } - else - { - - // we're going to collect the keys in here - Set keys{{}}; - - // computing the keys for extern types - if (normTy->hasExternTypes()) - { - LUAU_ASSERT(!normTy->hasTables()); - - // seen set for key computation for extern types - DenseHashSet seen{{}}; - - auto externTypeIter = normTy->externTypes.ordering.begin(); - auto externTypeIterEnd = normTy->externTypes.ordering.end(); - LUAU_ASSERT(externTypeIter != externTypeIterEnd); // should be guaranteed by the `hasExternTypes` check earlier - - // collect all the properties from the first class type - if (!computeKeysOf_DEPRECATED(*externTypeIter, keys, seen, isRaw, ctx)) - return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; // if it failed, we have a top type! - - // we need to look at each class to remove any keys that are not common amongst them all - while (++externTypeIter != externTypeIterEnd) - { - seen.clear(); // we'll reuse the same seen set - - Set localKeys{{}}; - - // we can skip to the next class if this one is a top type - if (!computeKeysOf_DEPRECATED(*externTypeIter, localKeys, seen, isRaw, ctx)) - continue; - - for (auto& key : keys) - { - // remove any keys that are not present in each class - if (!localKeys.contains(key)) - keys.erase(key); - } - } - } - - // computing the keys for tables - if (normTy->hasTables()) - { - LUAU_ASSERT(!normTy->hasExternTypes()); - - // seen set for key computation for tables - DenseHashSet seen{{}}; - - auto tablesIter = normTy->tables.begin(); - LUAU_ASSERT(tablesIter != normTy->tables.end()); // should be guaranteed by the `hasTables` check earlier - - // collect all the properties from the first table type - if (!computeKeysOf_DEPRECATED(*tablesIter, keys, seen, isRaw, ctx)) - return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; // if it failed, we have the top table type! - - // we need to look at each tables to remove any keys that are not common amongst them all - while (++tablesIter != normTy->tables.end()) - { - seen.clear(); // we'll reuse the same seen set - - Set localKeys{{}}; - - // we can skip to the next table if this one is the top table type - if (!computeKeysOf_DEPRECATED(*tablesIter, localKeys, seen, isRaw, ctx)) - continue; - - for (auto& key : keys) - { - // remove any keys that are not present in each table - if (!localKeys.contains(key)) - keys.erase(key); - } - } - } - - // if the set of keys is empty, `keyof` is `never` - if (keys.empty()) - return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - - // everything is validated, we need only construct our big union of singletons now! - std::vector singletons; - singletons.reserve(keys.size()); - - for (const std::string& key : keys) - singletons.push_back(ctx->arena->addType(SingletonType{StringSingleton{key}})); - - // If there's only one entry, we don't need a UnionType. - // We can take straight take it from the first entry - // because it was added into the type arena already. - if (singletons.size() == 1) - return {singletons.front(), Reduction::MaybeOk, {}, {}}; - - return {ctx->arena->addType(UnionType{std::move(singletons)}), Reduction::MaybeOk, {}, {}}; - } -} - -TypeFunctionReductionResult keyofTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 1 || !packParams.empty()) - { - ctx->ice->ice("keyof type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return keyofFunctionImpl(typeParams, packParams, ctx, /* isRaw */ false); -} - -TypeFunctionReductionResult rawkeyofTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 1 || !packParams.empty()) - { - ctx->ice->ice("rawkeyof type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return keyofFunctionImpl(typeParams, packParams, ctx, /* isRaw */ true); -} - -/* Searches through table's or class's props/indexer to find the property of `ty` - If found, appends that property to `result` and returns true - Else, returns false */ -bool searchPropsAndIndexer( - TypeId ty, - TableType::Props tblProps, - std::optional tblIndexer, - DenseHashSet& result, - NotNull ctx -) -{ - ty = follow(ty); - - // index into tbl's properties - if (auto stringSingleton = get(get(ty))) - { - if (tblProps.find(stringSingleton->value) != tblProps.end()) - { - - TypeId propTy; - if (FFlag::LuauRemoveTypeCallsForReadWriteProps) - { - Property& prop = tblProps.at(stringSingleton->value); - - if (prop.readTy) - propTy = follow(*prop.readTy); - else if (prop.writeTy) - propTy = follow(*prop.writeTy); - else // found the property, but there was no type associated with it - return false; - } - else - propTy = follow(tblProps.at(stringSingleton->value).type_DEPRECATED()); - - // property is a union type -> we need to extend our reduction type - if (auto propUnionTy = get(propTy)) - { - for (TypeId option : propUnionTy->options) - { - result.insert(follow(option)); - } - } - else // property is a singular type or intersection type -> we can simply append - result.insert(propTy); - - return true; - } - } - - // index into tbl's indexer - if (tblIndexer) - { - TypeId indexType = follow(tblIndexer->indexType); - - if (auto tfit = get(indexType)) - { - // if we have an index function here, it means we're in a cycle, so let's see if it's well-founded if we tie the knot - if (tfit->function.get() == &builtinTypeFunctions().indexFunc) - indexType = follow(tblIndexer->indexResultType); - } - - if (isSubtype(ty, indexType, ctx->scope, ctx->builtins, ctx->simplifier, *ctx->ice, SolverMode::New)) - { - TypeId idxResultTy = follow(tblIndexer->indexResultType); - - // indexResultType is a union type -> we need to extend our reduction type - if (auto idxResUnionTy = get(idxResultTy)) - { - for (TypeId option : idxResUnionTy->options) - { - result.insert(follow(option)); - } - } - else // indexResultType is a singular type or intersection type -> we can simply append - result.insert(idxResultTy); - - return true; - } - } - - return false; -} - -bool tblIndexInto( - TypeId indexer, - TypeId indexee, - DenseHashSet& result, - DenseHashSet& seenSet, - NotNull ctx, - bool isRaw -) -{ - indexer = follow(indexer); - indexee = follow(indexee); - - if (seenSet.contains(indexee)) - return false; - seenSet.insert(indexee); - - if (auto unionTy = get(indexee)) - { - bool res = true; - for (auto component : unionTy) - { - // if the component is in the seen set and isn't the indexee itself, - // we can skip it cause it means we encountered it in an earlier component in the union. - if (seenSet.contains(component) && component != indexee) - continue; - - res = res && tblIndexInto(indexer, component, result, seenSet, ctx, isRaw); - } - return res; - } - - if (get(indexee)) - { - TypePackId argPack = ctx->arena->addTypePack({indexer}); - SolveResult solveResult = solveFunctionCall( - ctx->arena, - ctx->builtins, - ctx->simplifier, - ctx->normalizer, - ctx->typeFunctionRuntime, - ctx->ice, - ctx->limits, - ctx->scope, - ctx->scope->location, - indexee, - argPack - ); - - if (!solveResult.typePackId.has_value()) - return false; - - TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); - if (extracted.head.empty()) - return false; - - result.insert(follow(extracted.head.front())); - return true; - } - - // we have a table type to try indexing - if (auto tableTy = get(indexee)) - { - return searchPropsAndIndexer(indexer, tableTy->props, tableTy->indexer, result, ctx); - } - - // we have a metatable type to try indexing - if (auto metatableTy = get(indexee)) - { - if (auto tableTy = get(follow(metatableTy->table))) - { - - // try finding all properties within the current scope of the table - if (searchPropsAndIndexer(indexer, tableTy->props, tableTy->indexer, result, ctx)) - return true; - } - - // if the code reached here, it means we weren't able to find all properties -> look into __index metamethod - if (!isRaw) - { - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, indexee, "__index", Location{}); - if (mmType) - return tblIndexInto(indexer, *mmType, result, seenSet, ctx, isRaw); - } - } - - return false; -} - -bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet& result, NotNull ctx, bool isRaw) -{ - DenseHashSet seenSet{{}}; - return tblIndexInto(indexer, indexee, result, seenSet, ctx, isRaw); -} - -/* Vocabulary note: indexee refers to the type that contains the properties, - indexer refers to the type that is used to access indexee - Example: index => `Person` is the indexee and `"name"` is the indexer */ -TypeFunctionReductionResult indexFunctionImpl( - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx, - bool isRaw -) -{ - TypeId indexeeTy = follow(typeParams.at(0)); - - if (isPending(indexeeTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {indexeeTy}, {}}; - - std::shared_ptr indexeeNormTy = ctx->normalizer->normalize(indexeeTy); - - // if the indexee failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!indexeeNormTy) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // if the indexee is `any`, then indexing also gives us `any`. - if (indexeeNormTy->shouldSuppressErrors()) - return {ctx->builtins->anyType, Reduction::MaybeOk, {}, {}}; - - // if we don't have either just tables or just extern types, we've got nothing to index into - if (indexeeNormTy->hasTables() == indexeeNormTy->hasExternTypes()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - // we're trying to reject any type that has not normalized to a table or extern type or a union of tables or extern types. - if (indexeeNormTy->hasTops() || indexeeNormTy->hasBooleans() || indexeeNormTy->hasErrors() || indexeeNormTy->hasNils() || - indexeeNormTy->hasNumbers() || indexeeNormTy->hasStrings() || indexeeNormTy->hasThreads() || indexeeNormTy->hasBuffers() || - indexeeNormTy->hasFunctions() || indexeeNormTy->hasTyvars()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - TypeId indexerTy = follow(typeParams.at(1)); - - if (isPending(indexerTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {indexerTy}, {}}; - - std::shared_ptr indexerNormTy = ctx->normalizer->normalize(indexerTy); - - // if the indexer failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!indexerNormTy) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // we're trying to reject any type that is not a string singleton or primitive (string, number, boolean, thread, nil, function, table, or buffer) - if (indexerNormTy->hasTops() || indexerNormTy->hasErrors()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - // indexer can be a union —> break them down into a vector - const std::vector* typesToFind = nullptr; - const std::vector singleType{indexerTy}; - if (auto unionTy = get(indexerTy)) - typesToFind = &unionTy->options; - else - typesToFind = &singleType; - - DenseHashSet properties{{}}; // vector of types that will be returned - - if (indexeeNormTy->hasExternTypes()) - { - LUAU_ASSERT(!indexeeNormTy->hasTables()); - - if (isRaw) // rawget should never reduce for extern types (to match the behavior of the rawget global function) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - // at least one class is guaranteed to be in the iterator by .hasExternTypes() - for (auto externTypeIter = indexeeNormTy->externTypes.ordering.begin(); externTypeIter != indexeeNormTy->externTypes.ordering.end(); - ++externTypeIter) - { - auto externTy = get(*externTypeIter); - if (!externTy) - { - LUAU_ASSERT(false); // this should not be possible according to normalization's spec - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - - for (TypeId ty : *typesToFind) - { - // Search for all instances of indexer in class->props and class->indexer - if (searchPropsAndIndexer(ty, externTy->props, externTy->indexer, properties, ctx)) - continue; // Indexer was found in this class, so we can move on to the next - - auto parent = externTy->parent; - bool foundInParent = false; - while (parent && !foundInParent) - { - auto parentExternType = get(follow(*parent)); - foundInParent = searchPropsAndIndexer(ty, parentExternType->props, parentExternType->indexer, properties, ctx); - parent = parentExternType->parent; - } - - // we move on to the next type if any of the parents we went through had the property. - if (foundInParent) - continue; - - // If code reaches here,that means the property not found -> check in the metatable's __index - - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - std::optional mmType = findMetatableEntry(ctx->builtins, dummy, *externTypeIter, "__index", Location{}); - if (!mmType) // if a metatable does not exist, there is no where else to look - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - if (!tblIndexInto(ty, *mmType, properties, ctx, isRaw)) // if indexer is not in the metatable, we fail to reduce - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - } - } - - if (indexeeNormTy->hasTables()) - { - LUAU_ASSERT(!indexeeNormTy->hasExternTypes()); - - // at least one table is guaranteed to be in the iterator by .hasTables() - for (auto tablesIter = indexeeNormTy->tables.begin(); tablesIter != indexeeNormTy->tables.end(); ++tablesIter) - { - for (TypeId ty : *typesToFind) - if (!tblIndexInto(ty, *tablesIter, properties, ctx, isRaw)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - } - - // If the type being reduced to is a single type, no need to union - if (properties.size() == 1) - return {*properties.begin(), Reduction::MaybeOk, {}, {}}; - - return {ctx->arena->addType(UnionType{std::vector(properties.begin(), properties.end())}), Reduction::MaybeOk, {}, {}}; -} - -TypeFunctionReductionResult indexTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("index type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return indexFunctionImpl(typeParams, packParams, ctx, /* isRaw */ false); -} - -TypeFunctionReductionResult rawgetTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("rawget type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - return indexFunctionImpl(typeParams, packParams, ctx, /* isRaw */ true); -} - -TypeFunctionReductionResult setmetatableTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 2 || !packParams.empty()) - { - ctx->ice->ice("setmetatable type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - const Location location = ctx->constraint ? ctx->constraint->location : Location{}; - - TypeId targetTy = follow(typeParams.at(0)); - TypeId metatableTy = follow(typeParams.at(1)); - - std::shared_ptr targetNorm = ctx->normalizer->normalize(targetTy); - - // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance. - if (!targetNorm) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - // cannot setmetatable on something without table parts. - if (!targetNorm->hasTables()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - // we're trying to reject any type that has not normalized to a table or a union/intersection of tables. - if (targetNorm->hasTops() || targetNorm->hasBooleans() || targetNorm->hasErrors() || targetNorm->hasNils() || targetNorm->hasNumbers() || - targetNorm->hasStrings() || targetNorm->hasThreads() || targetNorm->hasBuffers() || targetNorm->hasFunctions() || targetNorm->hasTyvars() || - targetNorm->hasExternTypes()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - // if the supposed metatable is not a table, we will fail to reduce. - if (!get(metatableTy) && !get(metatableTy)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - if (targetNorm->tables.size() == 1) - { - TypeId table = *targetNorm->tables.begin(); - - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional metatableMetamethod = findMetatableEntry(ctx->builtins, dummy, table, "__metatable", location); - - // if the `__metatable` metamethod is present, then the table is locked and we cannot `setmetatable` on it. - if (metatableMetamethod) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - TypeId withMetatable = ctx->arena->addType(MetatableType{table, metatableTy}); - - return {withMetatable, Reduction::MaybeOk, {}, {}}; - } - - TypeId result = ctx->builtins->neverType; - - for (auto componentTy : targetNorm->tables) - { - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional metatableMetamethod = findMetatableEntry(ctx->builtins, dummy, componentTy, "__metatable", location); - - // if the `__metatable` metamethod is present, then the table is locked and we cannot `setmetatable` on it. - if (metatableMetamethod) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - TypeId withMetatable = ctx->arena->addType(MetatableType{componentTy, metatableTy}); - SimplifyResult simplified = simplifyUnion(ctx->builtins, ctx->arena, result, withMetatable); - - if (!simplified.blockedTypes.empty()) - { - std::vector blockedTypes{}; - blockedTypes.reserve(simplified.blockedTypes.size()); - for (auto ty : simplified.blockedTypes) - blockedTypes.push_back(ty); - return {std::nullopt, Reduction::MaybeOk, std::move(blockedTypes), {}}; - } - - result = simplified.result; - } - - return {result, Reduction::MaybeOk, {}, {}}; -} - -static TypeFunctionReductionResult getmetatableHelper(TypeId targetTy, const Location& location, NotNull ctx) -{ - targetTy = follow(targetTy); - - std::optional result = std::nullopt; - bool erroneous = true; - - if (auto table = get(targetTy)) - erroneous = false; - - if (auto mt = get(targetTy)) - { - result = mt->metatable; - erroneous = false; - } - - if (auto clazz = get(targetTy)) - { - result = clazz->metatable; - erroneous = false; - } - - if (auto primitive = get(targetTy)) - { - result = primitive->metatable; - erroneous = false; - } - - if (auto singleton = get(targetTy)) - { - if (get(singleton)) - { - auto primitiveString = get(ctx->builtins->stringType); - result = primitiveString->metatable; - } - erroneous = false; - } - - if (FFlag::LuauUpdateGetMetatableTypeSignature && get(targetTy)) - { - // getmetatable ~ any - result = targetTy; - erroneous = false; - } - - if (erroneous) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - // findMetatableEntry demands the ability to emit errors, so we must give it - // the necessary state to do that, even if we intend to just eat the errors. - ErrorVec dummy; - - std::optional metatableMetamethod = findMetatableEntry(ctx->builtins, dummy, targetTy, "__metatable", location); - - if (metatableMetamethod) - return {metatableMetamethod, Reduction::MaybeOk, {}, {}}; - - if (result) - return {result, Reduction::MaybeOk, {}, {}}; - - return {ctx->builtins->nilType, Reduction::MaybeOk, {}, {}}; -} - -TypeFunctionReductionResult getmetatableTypeFunction( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 1 || !packParams.empty()) - { - ctx->ice->ice("getmetatable type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - const Location location = ctx->constraint ? ctx->constraint->location : Location{}; - - TypeId targetTy = follow(typeParams.at(0)); - - if (isPending(targetTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; - - if (auto ut = get(targetTy)) - { - std::vector options{}; - options.reserve(ut->options.size()); - - for (auto option : ut->options) - { - TypeFunctionReductionResult result = getmetatableHelper(option, location, ctx); - - if (!result.result) - return result; - - options.push_back(*result.result); - } - - return {ctx->arena->addType(UnionType{std::move(options)}), Reduction::MaybeOk, {}, {}}; - } - - if (auto it = get(targetTy)) - { - std::vector parts{}; - parts.reserve(it->parts.size()); - - bool erroredWithUnknown = false; - - for (auto part : it->parts) - { - TypeFunctionReductionResult result = getmetatableHelper(part, location, ctx); - - if (!result.result) - { - // Don't immediately error if part is unknown - if (FFlag::LuauUpdateGetMetatableTypeSignature && get(follow(part))) - { - erroredWithUnknown = true; - continue; - } - else - return result; - } - - parts.push_back(*result.result); - } - - // If all parts are unknown, return erroneous reduction - if (FFlag::LuauUpdateGetMetatableTypeSignature && erroredWithUnknown && parts.empty()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - if (FFlag::LuauUpdateGetMetatableTypeSignature && parts.size() == 1) - return {parts.front(), Reduction::MaybeOk, {}, {}}; - - return {ctx->arena->addType(IntersectionType{std::move(parts)}), Reduction::MaybeOk, {}, {}}; - } - - return getmetatableHelper(targetTy, location, ctx); -} - -TypeFunctionReductionResult weakoptionalTypeFunc( - TypeId instance, - const std::vector& typeParams, - const std::vector& packParams, - NotNull ctx -) -{ - if (typeParams.size() != 1 || !packParams.empty()) - { - ctx->ice->ice("weakoptional type function: encountered a type function instance without the required argument structure"); - LUAU_ASSERT(false); - } - - TypeId targetTy = follow(typeParams.at(0)); - - if (isPending(targetTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; - - if (is(instance)) - return {ctx->builtins->nilType, Reduction::MaybeOk, {}, {}}; - - std::shared_ptr targetNorm = ctx->normalizer->normalize(targetTy); - - if (!targetNorm) - return {std::nullopt, Reduction::MaybeOk, {}, {}}; - - auto result = ctx->normalizer->isInhabited(targetNorm.get()); - if (result == NormalizationResult::False) - return {ctx->builtins->nilType, Reduction::MaybeOk, {}, {}}; - - return {targetTy, Reduction::MaybeOk, {}, {}}; -} - -BuiltinTypeFunctions::BuiltinTypeFunctions() - : userFunc{"user", userDefinedTypeFunction} - , notFunc{"not", notTypeFunction} - , lenFunc{"len", lenTypeFunction} - , unmFunc{"unm", unmTypeFunction} - , addFunc{"add", addTypeFunction} - , subFunc{"sub", subTypeFunction} - , mulFunc{"mul", mulTypeFunction} - , divFunc{"div", divTypeFunction} - , idivFunc{"idiv", idivTypeFunction} - , powFunc{"pow", powTypeFunction} - , modFunc{"mod", modTypeFunction} - , concatFunc{"concat", concatTypeFunction} - , andFunc{"and", andTypeFunction, /*canReduceGenerics*/ true} - , orFunc{"or", orTypeFunction, /*canReduceGenerics*/ true} - , ltFunc{"lt", ltTypeFunction} - , leFunc{"le", leTypeFunction} - , eqFunc{"eq", eqTypeFunction} - , refineFunc{"refine", refineTypeFunction, /*canReduceGenerics*/ FFlag::LuauEagerGeneralization4} - , singletonFunc{"singleton", singletonTypeFunction} - , unionFunc{"union", unionTypeFunction} - , intersectFunc{"intersect", intersectTypeFunction} - , keyofFunc{"keyof", keyofTypeFunction} - , rawkeyofFunc{"rawkeyof", rawkeyofTypeFunction} - , indexFunc{"index", indexTypeFunction} - , rawgetFunc{"rawget", rawgetTypeFunction} - , setmetatableFunc{"setmetatable", setmetatableTypeFunction} - , getmetatableFunc{"getmetatable", getmetatableTypeFunction} - , weakoptionalFunc{"weakoptional", weakoptionalTypeFunc} -{ -} - -void BuiltinTypeFunctions::addToScope(NotNull arena, NotNull scope) const -{ - // make a type function for a one-argument type function - auto mkUnaryTypeFunction = [&](const TypeFunction* tf) - { - TypeId t = arena->addType(GenericType{"T", Polarity::Negative}); - GenericTypeDefinition genericT{t}; - - return TypeFun{{genericT}, arena->addType(TypeFunctionInstanceType{NotNull{tf}, {t}, {}})}; - }; - - // make a type function for a two-argument type function with a default argument for the second type being the first - auto mkBinaryTypeFunctionWithDefault = [&](const TypeFunction* tf) - { - TypeId t = arena->addType(GenericType{"T", Polarity::Negative}); - TypeId u = arena->addType(GenericType{"U", Polarity::Negative}); - GenericTypeDefinition genericT{t}; - GenericTypeDefinition genericU{u, {t}}; - - return TypeFun{{genericT, genericU}, arena->addType(TypeFunctionInstanceType{NotNull{tf}, {t, u}, {}})}; - }; - - // make a two-argument type function without the default arguments - auto mkBinaryTypeFunction = [&](const TypeFunction* tf) - { - TypeId t = arena->addType(GenericType{"T", Polarity::Negative}); - TypeId u = arena->addType(GenericType{"U", Polarity::Negative}); - GenericTypeDefinition genericT{t}; - GenericTypeDefinition genericU{u}; - - return TypeFun{{genericT, genericU}, arena->addType(TypeFunctionInstanceType{NotNull{tf}, {t, u}, {}})}; - }; - - scope->exportedTypeBindings[lenFunc.name] = mkUnaryTypeFunction(&lenFunc); - scope->exportedTypeBindings[unmFunc.name] = mkUnaryTypeFunction(&unmFunc); - - scope->exportedTypeBindings[addFunc.name] = mkBinaryTypeFunctionWithDefault(&addFunc); - scope->exportedTypeBindings[subFunc.name] = mkBinaryTypeFunctionWithDefault(&subFunc); - scope->exportedTypeBindings[mulFunc.name] = mkBinaryTypeFunctionWithDefault(&mulFunc); - scope->exportedTypeBindings[divFunc.name] = mkBinaryTypeFunctionWithDefault(&divFunc); - scope->exportedTypeBindings[idivFunc.name] = mkBinaryTypeFunctionWithDefault(&idivFunc); - scope->exportedTypeBindings[powFunc.name] = mkBinaryTypeFunctionWithDefault(&powFunc); - scope->exportedTypeBindings[modFunc.name] = mkBinaryTypeFunctionWithDefault(&modFunc); - scope->exportedTypeBindings[concatFunc.name] = mkBinaryTypeFunctionWithDefault(&concatFunc); - - scope->exportedTypeBindings[ltFunc.name] = mkBinaryTypeFunctionWithDefault(<Func); - scope->exportedTypeBindings[leFunc.name] = mkBinaryTypeFunctionWithDefault(&leFunc); - scope->exportedTypeBindings[eqFunc.name] = mkBinaryTypeFunctionWithDefault(&eqFunc); - - scope->exportedTypeBindings[keyofFunc.name] = mkUnaryTypeFunction(&keyofFunc); - scope->exportedTypeBindings[rawkeyofFunc.name] = mkUnaryTypeFunction(&rawkeyofFunc); - - if (FFlag::LuauNotAllBinaryTypeFunsHaveDefaults) - { - scope->exportedTypeBindings[indexFunc.name] = mkBinaryTypeFunction(&indexFunc); - scope->exportedTypeBindings[rawgetFunc.name] = mkBinaryTypeFunction(&rawgetFunc); - } - else - { - scope->exportedTypeBindings[indexFunc.name] = mkBinaryTypeFunctionWithDefault(&indexFunc); - scope->exportedTypeBindings[rawgetFunc.name] = mkBinaryTypeFunctionWithDefault(&rawgetFunc); - } - - if (FFlag::LuauNotAllBinaryTypeFunsHaveDefaults) - scope->exportedTypeBindings[setmetatableFunc.name] = mkBinaryTypeFunction(&setmetatableFunc); - else - scope->exportedTypeBindings[setmetatableFunc.name] = mkBinaryTypeFunctionWithDefault(&setmetatableFunc); - scope->exportedTypeBindings[getmetatableFunc.name] = mkUnaryTypeFunction(&getmetatableFunc); -} - -const BuiltinTypeFunctions& builtinTypeFunctions() -{ - static std::unique_ptr result = std::make_unique(); - - return *result; -} - } // namespace Luau diff --git a/Analysis/src/TypeFunctionRuntime.cpp b/Analysis/src/TypeFunctionRuntime.cpp index 254d5916..5a6657e0 100644 --- a/Analysis/src/TypeFunctionRuntime.cpp +++ b/Analysis/src/TypeFunctionRuntime.cpp @@ -2,9 +2,16 @@ #include "Luau/TypeFunctionRuntime.h" +#include "Luau/Allocator.h" +#include "Luau/Lexer.h" +#include "Luau/BuiltinTypeFunctions.h" +#include "Luau/BytecodeBuilder.h" +#include "Luau/ParseResult.h" +#include "Luau/Compiler.h" #include "Luau/DenseHash.h" #include "Luau/StringUtils.h" #include "Luau/TypeFunction.h" +#include "Luau/TypeFunctionRuntimeBuilder.h" #include "lua.h" #include "lualib.h" @@ -19,6 +26,133 @@ LUAU_FASTFLAGVARIABLE(LuauTypeFunOptional) namespace Luau { +LuauTempThreadPopper::LuauTempThreadPopper(lua_State* L) + : L(L) +{ +} + +LuauTempThreadPopper::~LuauTempThreadPopper() +{ + lua_pop(L, 1); +} + +static void dummyStateClose(lua_State*) {} + +TypeFunctionRuntime::TypeFunctionRuntime(NotNull ice, NotNull limits) + : ice(ice) + , limits(limits) + , state(nullptr, dummyStateClose) +{ +} + +TypeFunctionRuntime::~TypeFunctionRuntime() {} + +std::optional TypeFunctionRuntime::registerFunction(AstStatTypeFunction* function) +{ + // If evaluation is disabled, we do not generate additional error messages + if (!allowEvaluation) + return std::nullopt; + + // Do not evaluate type functions with parse errors inside + if (function->hasErrors) + return std::nullopt; + + prepareState(); + + lua_State* global = state.get(); + + // Fetch to check if function is already registered + lua_pushlightuserdata(global, function); + lua_gettable(global, LUA_REGISTRYINDEX); + + if (!lua_isnil(global, -1)) + { + lua_pop(global, 1); + return std::nullopt; + } + + lua_pop(global, 1); + + AstName name = function->name; + + // Construct ParseResult containing the type function + Allocator allocator; + AstNameTable names(allocator); + + AstExpr* exprFunction = function->body; + AstArray exprReturns{&exprFunction, 1}; + AstStatReturn stmtReturn{Location{}, exprReturns}; + AstStat* stmtArray[] = {&stmtReturn}; + AstArray stmts{stmtArray, 1}; + AstStatBlock exec{Location{}, stmts}; + ParseResult parseResult{&exec, 1, {}, {}, {}, CstNodeMap{nullptr}}; + + BytecodeBuilder builder; + try + { + compileOrThrow(builder, parseResult, names); + } + catch (CompileError& e) + { + return format("'%s' type function failed to compile with error message: %s", name.value, e.what()); + } + + std::string bytecode = builder.getBytecode(); + + // Separate sandboxed thread for individual execution and private globals + lua_State* L = lua_newthread(global); + LuauTempThreadPopper popper(global); + + // Create individual environment for the type function + luaL_sandboxthread(L); + + // Do not allow global writes to that environment + lua_pushvalue(L, LUA_GLOBALSINDEX); + lua_setreadonly(L, -1, true); + lua_pop(L, 1); + + // Load bytecode into Luau state + if (auto error = checkResultForError(L, name.value, luau_load(L, name.value, bytecode.data(), bytecode.size(), 0))) + return error; + + // Execute the global function which should return our user-defined type function + if (auto error = checkResultForError(L, name.value, lua_resume(L, nullptr, 0))) + return error; + + if (!lua_isfunction(L, -1)) + { + lua_pop(L, 1); + return format("Could not find '%s' type function in the global scope", name.value); + } + + // Store resulting function in the registry + lua_pushlightuserdata(global, function); + lua_xmove(L, global, 1); + lua_settable(global, LUA_REGISTRYINDEX); + + return std::nullopt; +} + +void TypeFunctionRuntime::prepareState() +{ + if (state) + return; + + state = StateRef(lua_newstate(typeFunctionAlloc, nullptr), lua_close); + lua_State* L = state.get(); + + lua_setthreaddata(L, this); + + setTypeFunctionEnvironment(L); + + registerTypeUserData(L); + + registerTypesLibrary(L); + + luaL_sandbox(L); + luaL_sandboxthread(L); +} + constexpr int kTypeUserdataTag = 42; void* typeFunctionAlloc(void* ud, void* ptr, size_t osize, size_t nsize) @@ -2108,13 +2242,6 @@ bool TypeFunctionProperty::isWriteOnly() const * Below is a helper class for type.copy() * Forked version of Clone.cpp */ -using TypeFunctionKind = Variant; - -template -const T* get(const TypeFunctionKind& kind) -{ - return get_if(&kind); -} class TypeFunctionCloner { diff --git a/Analysis/src/TypeFunctionRuntimeBuilder.cpp b/Analysis/src/TypeFunctionRuntimeBuilder.cpp index e8f0a93b..68f61246 100644 --- a/Analysis/src/TypeFunctionRuntimeBuilder.cpp +++ b/Analysis/src/TypeFunctionRuntimeBuilder.cpp @@ -2,7 +2,6 @@ #include "Luau/TypeFunctionRuntimeBuilder.h" -#include "Luau/Ast.h" #include "Luau/BuiltinDefinitions.h" #include "Luau/Common.h" #include "Luau/DenseHash.h" @@ -12,6 +11,7 @@ #include "Luau/TypeFwd.h" #include "Luau/TypeFunctionRuntime.h" #include "Luau/TypePack.h" +#include "Luau/TypeOrPack.h" #include "Luau/ToString.h" #include @@ -41,7 +41,7 @@ class TypeFunctionSerializer // queue.back() should always return two of same type in their respective sides // For example `auto [first, second] = queue.back()`: if first is PrimitiveType, // second must be TypeFunctionPrimitiveType; else there should be an error - std::vector> queue; + std::vector> queue; SeenTypes types; // Mapping of TypeIds that have been shallow serialized to TypeFunctionTypeIds SeenTypePacks packs; // Mapping of TypePackIds that have been shallow serialized to TypeFunctionTypePackIds @@ -121,7 +121,7 @@ private: return std::nullopt; } - std::optional find(Kind kind) const + std::optional find(TypeOrPack kind) const { if (auto ty = get(kind)) return find(*ty); @@ -316,7 +316,7 @@ private: } } - void serializeChildren(Kind kind, TypeFunctionKind tfkind) + void serializeChildren(TypeOrPack kind, TypeFunctionKind tfkind) { if (auto [ty, tfty] = std::tuple{get(kind), get(tfkind)}; ty && tfty) serializeChildren(*ty, *tfty); @@ -496,7 +496,7 @@ class TypeFunctionDeserializer // queue.back() should always return two of same type in their respective sides // For example `auto [first, second] = queue.back()`: if first is TypeFunctionPrimitiveType, // second must be PrimitiveType; else there should be an error - std::vector> queue; + std::vector> queue; // Generic types and packs currently in scope // Generics are resolved by name even if runtime generic type pointers are different @@ -600,7 +600,7 @@ private: return std::nullopt; } - std::optional find(TypeFunctionKind kind) const + std::optional find(TypeFunctionKind kind) const { if (auto ty = get(kind)) return find(*ty); @@ -824,7 +824,7 @@ private: state->ctx->ice->ice("Deserializing user defined type function arguments: mysterious type is being deserialized"); } - void deserializeChildren(TypeFunctionKind tfkind, Kind kind) + void deserializeChildren(TypeFunctionKind tfkind, TypeOrPack kind) { if (auto [ty, tfty] = std::tuple{get(kind), get(tfkind)}; ty && tfty) deserializeChildren(*tfty, *ty); diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index b2b1f407..ea5606a1 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -737,8 +737,6 @@ std::string toString(const TypePath::Path& path, bool prefixDot) std::string toStringHuman(const TypePath::Path& path) { - LUAU_ASSERT(FFlag::LuauSolverV2); - enum class State { Initial, diff --git a/Analysis/src/UserDefinedTypeFunction.cpp b/Analysis/src/UserDefinedTypeFunction.cpp new file mode 100644 index 00000000..b2f81e2f --- /dev/null +++ b/Analysis/src/UserDefinedTypeFunction.cpp @@ -0,0 +1,391 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/ApplyTypeFunction.h" +#include "Luau/BuiltinTypeFunctions.h" +#include "Luau/ConstraintSolver.h" +#include "Luau/Normalize.h" +#include "Luau/StringUtils.h" +#include "Luau/TimeTrace.h" +#include "Luau/UserDefinedTypeFunction.h" +#include "Luau/VisitType.h" + +#include "lua.h" +#include "lualib.h" + +LUAU_FASTFLAG(LuauUserTypeFunctionAliases) + +namespace Luau +{ + +namespace +{ + +template +class ScopedAssign +{ +public: + ScopedAssign(T& target, const T& value) + : target(&target) + , oldValue(target) + { + target = value; + } + + ~ScopedAssign() + { + *target = oldValue; + } + +private: + T* target = nullptr; + T oldValue; +}; + +} + +struct FindUserTypeFunctionBlockers : TypeOnceVisitor +{ + NotNull ctx; + DenseHashSet blockingTypeMap{nullptr}; + std::vector blockingTypes; + + explicit FindUserTypeFunctionBlockers(NotNull ctx) + : TypeOnceVisitor(/* skipBoundTypes */ true) + , ctx(ctx) + { + } + + bool visit(TypeId ty) override + { + if (isPending(ty, ctx->solver)) + { + if (!blockingTypeMap.contains(ty)) + { + blockingTypeMap.insert(ty); + blockingTypes.push_back(ty); + } + } + return true; + } + + bool visit(TypePackId tp) override + { + return true; + } + + bool visit(TypeId ty, const ExternType&) override + { + return false; + } +}; + +static int evaluateTypeAliasCall(lua_State* L) +{ + TypeFun* tf = static_cast(lua_tolightuserdata(L, lua_upvalueindex(1))); + + TypeFunctionRuntime* runtime = getTypeFunctionRuntime(L); + TypeFunctionRuntimeBuilderState* runtimeBuilder = runtime->runtimeBuilder; + + ApplyTypeFunction applyTypeFunction{runtimeBuilder->ctx->arena}; + + int argumentCount = lua_gettop(L); + std::vector rawTypeArguments; + + for (int i = 0; i < argumentCount; i++) + { + TypeFunctionTypeId tfty = getTypeUserData(L, i + 1); + TypeId ty = deserialize(tfty, runtimeBuilder); + + if (!runtimeBuilder->errors.empty()) + luaL_error(L, "failed to deserialize type at argument %d", i + 1); + + rawTypeArguments.push_back(ty); + } + + // Check if we have enough arguments, by typical typechecking rules + size_t typesRequired = tf->typeParams.size(); + size_t packsRequired = tf->typePackParams.size(); + + size_t typesProvided = rawTypeArguments.size() > typesRequired ? typesRequired : rawTypeArguments.size(); + size_t extraTypes = rawTypeArguments.size() > typesRequired ? rawTypeArguments.size() - typesRequired : 0; + size_t packsProvided = 0; + + if (extraTypes != 0 && packsProvided == 0) + { + // Extra types are only collected into a pack if a pack is expected + if (packsRequired != 0) + packsProvided += 1; + else + typesProvided += extraTypes; + } + + for (size_t i = typesProvided; i < typesRequired; ++i) + { + if (tf->typeParams[i].defaultValue) + typesProvided += 1; + } + + for (size_t i = packsProvided; i < packsRequired; ++i) + { + if (tf->typePackParams[i].defaultValue) + packsProvided += 1; + } + + if (extraTypes == 0 && packsProvided + 1 == packsRequired) + packsProvided += 1; + + if (typesProvided != typesRequired || packsProvided != packsRequired) + luaL_error(L, "not enough arguments to call"); + + // Prepare final types and packs + auto [types, packs] = saturateArguments(runtimeBuilder->ctx->arena, runtimeBuilder->ctx->builtins, *tf, rawTypeArguments, {}); + + for (size_t i = 0; i < types.size(); ++i) + applyTypeFunction.typeArguments[tf->typeParams[i].ty] = types[i]; + + for (size_t i = 0; i < packs.size(); ++i) + applyTypeFunction.typePackArguments[tf->typePackParams[i].tp] = packs[i]; + + std::optional maybeInstantiated = applyTypeFunction.substitute(tf->type); + + if (!maybeInstantiated.has_value()) + { + luaL_error(L, "failed to instantiate type alias"); + return 1; + } + + TypeId target = follow(*maybeInstantiated); + + FunctionGraphReductionResult result = reduceTypeFunctions(target, Location{}, runtimeBuilder->ctx); + + if (!result.errors.empty()) + luaL_error(L, "failed to reduce type function with: %s", toString(result.errors.front()).c_str()); + + TypeFunctionTypeId serializedTy = serialize(follow(target), runtimeBuilder); + + if (!runtimeBuilder->errors.empty()) + luaL_error(L, "%s", runtimeBuilder->errors.front().c_str()); + + allocTypeUserData(L, serializedTy->type); + return 1; +} + +TypeFunctionReductionResult userDefinedTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + auto typeFunction = getMutable(instance); + + if (typeFunction->userFuncData.owner.expired()) + { + ctx->ice->ice("user-defined type function module has expired"); + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + + if (!typeFunction->userFuncName || !typeFunction->userFuncData.definition) + { + ctx->ice->ice("all user-defined type functions must have an associated function definition"); + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + + // If type functions cannot be evaluated because of errors in the code, we do not generate any additional ones + if (!ctx->typeFunctionRuntime->allowEvaluation || typeFunction->userFuncData.definition->hasErrors) + return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; + + FindUserTypeFunctionBlockers check{ctx}; + + for (auto typeParam : typeParams) + check.traverse(follow(typeParam)); + + if (FFlag::LuauUserTypeFunctionAliases) + { + // Check that our environment doesn't depend on any type aliases that are blocked + for (auto& [name, definition] : typeFunction->userFuncData.environmentAlias) + { + if (definition.first->typeParams.empty() && definition.first->typePackParams.empty()) + check.traverse(follow(definition.first->type)); + } + } + + if (!check.blockingTypes.empty()) + return {std::nullopt, Reduction::MaybeOk, check.blockingTypes, {}}; + + // Ensure that whole type function environment is registered + for (auto& [name, definition] : typeFunction->userFuncData.environmentFunction) + { + // Cannot evaluate if a potential dependency couldn't be parsed + if (definition.first->hasErrors) + return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; + + if (std::optional error = ctx->typeFunctionRuntime->registerFunction(definition.first)) + { + // Failure to register at this point means that original definition had to error out and should not have been present in the + // environment + ctx->ice->ice("user-defined type function reference cannot be registered"); + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + } + + AstName name = typeFunction->userFuncData.definition->name; + + lua_State* global = ctx->typeFunctionRuntime->state.get(); + + if (global == nullptr) + return {std::nullopt, Reduction::Erroneous, {}, {}, format("'%s' type function: cannot be evaluated in this context", name.value)}; + + // Separate sandboxed thread for individual execution and private globals + lua_State* L = lua_newthread(global); + LuauTempThreadPopper popper(global); + + std::unique_ptr runtimeBuilder = std::make_unique(ctx); + + ScopedAssign setRuntimeBuilder(ctx->typeFunctionRuntime->runtimeBuilder, runtimeBuilder.get()); + ScopedAssign enableReduction(ctx->normalizer->sharedState->reentrantTypeReduction, false); + + // Build up the environment table of each function we have visible + for (auto& [_, curr] : typeFunction->userFuncData.environmentFunction) + { + // Environment table has to be filled only once in the current execution context + if (ctx->typeFunctionRuntime->initialized.find(curr.first)) + continue; + ctx->typeFunctionRuntime->initialized.insert(curr.first); + + lua_pushlightuserdata(L, curr.first); + lua_gettable(L, LUA_REGISTRYINDEX); + + if (!lua_isfunction(L, -1)) + { + ctx->ice->ice("user-defined type function reference cannot be found in the registry"); + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + + // Build up the environment of the current function, where some might not be visible + lua_getfenv(L, -1); + lua_setreadonly(L, -1, false); + + for (auto& [name, definition] : typeFunction->userFuncData.environmentFunction) + { + // Filter visibility based on original scope depth + if (definition.second >= curr.second) + { + lua_pushlightuserdata(L, definition.first); + lua_gettable(L, LUA_REGISTRYINDEX); + + if (!lua_isfunction(L, -1)) + break; // Don't have to report an error here, we will visit each function in outer loop + + lua_setfield(L, -2, name.c_str()); + } + } + + if (FFlag::LuauUserTypeFunctionAliases) + { + for (auto& [name, definition] : typeFunction->userFuncData.environmentAlias) + { + // Filter visibility based on original scope depth + if (definition.second >= curr.second) + { + if (definition.first->typeParams.empty() && definition.first->typePackParams.empty()) + { + TypeId ty = follow(definition.first->type); + + // This is checked at the top of the function, and should still be true. + LUAU_ASSERT(!isPending(ty, ctx->solver)); + + TypeFunctionTypeId serializedTy = serialize(ty, runtimeBuilder.get()); + + // Only register aliases that are representable in type environment + if (runtimeBuilder->errors.empty()) + { + allocTypeUserData(L, serializedTy->type); + lua_setfield(L, -2, name.c_str()); + } + } + else + { + lua_pushlightuserdata(L, definition.first); + lua_pushcclosure(L, evaluateTypeAliasCall, name.c_str(), 1); + lua_setfield(L, -2, name.c_str()); + } + } + } + } + + lua_setreadonly(L, -1, true); + lua_pop(L, 2); + } + + // Fetch the function we want to evaluate + lua_pushlightuserdata(L, typeFunction->userFuncData.definition); + lua_gettable(L, LUA_REGISTRYINDEX); + + if (!lua_isfunction(L, -1)) + { + ctx->ice->ice("user-defined type function reference cannot be found in the registry"); + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + + resetTypeFunctionState(L); + + // Push serialized arguments onto the stack + for (auto typeParam : typeParams) + { + TypeId ty = follow(typeParam); + // This is checked at the top of the function, and should still be true. + LUAU_ASSERT(!isPending(ty, ctx->solver)); + + TypeFunctionTypeId serializedTy = serialize(ty, runtimeBuilder.get()); + // Check if there were any errors while serializing + if (runtimeBuilder->errors.size() != 0) + return {std::nullopt, Reduction::Erroneous, {}, {}, runtimeBuilder->errors.front()}; + + allocTypeUserData(L, serializedTy->type); + } + + // Set up an interrupt handler for type functions to respect type checking limits and LSP cancellation requests. + lua_callbacks(L)->interrupt = [](lua_State* L, int gc) + { + auto ctx = static_cast(lua_getthreaddata(lua_mainthread(L))); + if (ctx->limits->finishTime && TimeTrace::getClock() > *ctx->limits->finishTime) + throw TimeLimitError(ctx->ice->moduleName); + + if (ctx->limits->cancellationToken && ctx->limits->cancellationToken->requested()) + throw UserCancelError(ctx->ice->moduleName); + }; + + ctx->typeFunctionRuntime->messages.clear(); + + if (auto error = checkResultForError(L, name.value, lua_pcall(L, int(typeParams.size()), 1, 0))) + return {std::nullopt, Reduction::Erroneous, {}, {}, std::move(error), ctx->typeFunctionRuntime->messages}; + + // If the return value is not a type userdata, return with error message + if (!isTypeUserData(L, 1)) + { + return { + std::nullopt, + Reduction::Erroneous, + {}, + {}, + format("'%s' type function: returned a non-type value", name.value), + ctx->typeFunctionRuntime->messages + }; + } + + TypeFunctionTypeId retTypeFunctionTypeId = getTypeUserData(L, 1); + + // No errors should be present here since we should've returned already if any were raised during serialization. + LUAU_ASSERT(runtimeBuilder->errors.size() == 0); + + TypeId retTypeId = deserialize(retTypeFunctionTypeId, runtimeBuilder.get()); + + // At least 1 error occurred while deserializing + if (runtimeBuilder->errors.size() > 0) + return {std::nullopt, Reduction::Erroneous, {}, {}, runtimeBuilder->errors.front(), ctx->typeFunctionRuntime->messages}; + + return {retTypeId, Reduction::MaybeOk, {}, {}, std::nullopt, ctx->typeFunctionRuntime->messages}; +} + +} \ No newline at end of file diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 0d2bf696..4f73fe1b 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -20,7 +20,6 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTFLAGVARIABLE(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauDeclareExternType) LUAU_FASTFLAGVARIABLE(LuauParseStringIndexer) -LUAU_FASTFLAGVARIABLE(LuauCSTForReturnTypeFunctionTail) LUAU_FASTFLAGVARIABLE(LuauParseAttributeFixUninit) LUAU_DYNAMIC_FASTFLAGVARIABLE(DebugLuauReportReturnTypeVariadicWithTypeSuffix, false) @@ -1903,10 +1902,8 @@ AstTypePack* Parser::parseReturnType() // possibly () -> ReturnType if (lexer.current().type != ')') { - if (FFlag::LuauCSTForReturnTypeFunctionTail && options.storeCstData) + if (options.storeCstData) varargAnnotation = parseTypeList(result, resultNames, &commaPositions, &nameColonPositions); - else if (options.storeCstData) - varargAnnotation = parseTypeList(result, resultNames, &commaPositions); else varargAnnotation = parseTypeList(result, resultNames); } @@ -1950,7 +1947,7 @@ AstTypePack* Parser::parseReturnType() Position returnArrowPosition = lexer.current().location.begin; AstType* tail = parseFunctionTypeTail(begin, {nullptr, 0}, {}, {}, copy(result), copy(resultNames), varargAnnotation); - if (FFlag::LuauCSTForReturnTypeFunctionTail && options.storeCstData && tail->is()) + if (options.storeCstData && tail->is()) { cstNodeMap[tail] = allocator.alloc( Position{0, 0}, diff --git a/CodeGen/include/Luau/IrData.h b/CodeGen/include/Luau/IrData.h index 0dd3ca21..2f02f994 100644 --- a/CodeGen/include/Luau/IrData.h +++ b/CodeGen/include/Luau/IrData.h @@ -392,7 +392,7 @@ enum class IrCmd : uint8_t // C: Rn or unsigned int (key) SET_TABLE, - // TODO: remove with FFlagLuauCodeGenSimplifyImport + // TODO: remove with FFlagLuauCodeGenSimplifyImport2 // Lookup a value in the environment // A: Rn (where to store the result) // B: unsigned int (import path) diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 8957c01c..48c2b9a9 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -1384,7 +1384,10 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) break; case IrCmd::GET_CACHED_IMPORT: { + regs.spill(build, index); + Label skip, exit; + RegisterA64 tempTag = regs.allocTemp(KindA64::w); AddressA64 addrConstTag = tempAddr(inst.b, offsetof(TValue, tt)); @@ -1395,8 +1398,6 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.cbnz(tempTag, skip); { - size_t spills = regs.spill(build, index); - build.mov(x0, rState); build.add(x1, rBase, uint16_t(vmRegOp(inst.a) * sizeof(TValue))); build.mov(w2, importOp(inst.c)); @@ -1404,16 +1405,14 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.ldr(x4, mem(rNativeContext, offsetof(NativeContext, getImport))); build.blr(x4); - regs.restore(build, spills); // Need to restore before skip so that registers are in a consistent state - emitUpdateBase(build); build.b(exit); } - RegisterA64 tempTv = regs.allocTemp(KindA64::q); - build.setLabel(skip); + RegisterA64 tempTv = regs.allocTemp(KindA64::q); + AddressA64 addrConst = tempAddr(inst.b, 0); build.ldr(tempTv, addrConst); diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index 068c2d4e..2c4d2837 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -1221,6 +1221,9 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } case IrCmd::GET_CACHED_IMPORT: { + regs.assertAllFree(); + regs.assertNoSpills(); + Label skip, exit; // If the constant for the import is set, we will use it directly, otherwise we have to call an import path lookup function @@ -1241,9 +1244,10 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) emitUpdateBase(build); build.jmp(exit); + build.setLabel(skip); + ScopedRegX64 tmp1{regs, SizeX64::xmmword}; - build.setLabel(skip); build.vmovups(tmp1.reg, luauConstant(vmConstOp(inst.b))); build.vmovups(luauReg(vmRegOp(inst.a)), tmp1.reg); build.setLabel(exit); diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index fe9a7eb6..9a5a488d 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -12,7 +12,7 @@ #include "lstate.h" #include "ltm.h" -LUAU_FASTFLAGVARIABLE(LuauCodeGenSimplifyImport) +LUAU_FASTFLAGVARIABLE(LuauCodeGenSimplifyImport2) namespace Luau { @@ -1217,7 +1217,7 @@ void translateInstGetImport(IrBuilder& build, const Instruction* pc, int pcpos) int k = LUAU_INSN_D(*pc); uint32_t aux = pc[1]; - if (FFlag::LuauCodeGenSimplifyImport) + if (FFlag::LuauCodeGenSimplifyImport2) { build.inst(IrCmd::CHECK_SAFE_ENV, build.vmExit(pcpos)); build.inst(IrCmd::GET_CACHED_IMPORT, build.vmReg(ra), build.vmConst(k), build.constImport(aux), build.constUint(pcpos + 1)); diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 31d88fec..4c19edf9 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -1545,6 +1545,8 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& // Outside of safe environment, environment traversal for an import can execute custom code if (!state.inSafeEnv) state.invalidateUserCall(); + + state.invalidateValuePropagation(); break; case IrCmd::CONCAT: state.invalidateRegisterRange(vmRegOp(inst.a), function.uintOp(inst.b)); diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index dec886e0..82e0d4fd 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -4300,40 +4300,20 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c } // computes type information for all functions based on type annotations - if (FFlag::LuauSeparateCompilerTypeInfo) - { - if (options.typeInfoLevel >= 1 || options.optimizationLevel >= 2) - buildTypeMap( - compiler.functionTypes, - compiler.localTypes, - compiler.exprTypes, - root, - options.vectorType, - compiler.userdataTypes, - compiler.builtinTypes, - compiler.builtins, - compiler.globals, - options.libraryMemberTypeCb, - bytecode - ); - } - else - { - if (options.typeInfoLevel >= 1) - buildTypeMap( - compiler.functionTypes, - compiler.localTypes, - compiler.exprTypes, - root, - options.vectorType, - compiler.userdataTypes, - compiler.builtinTypes, - compiler.builtins, - compiler.globals, - options.libraryMemberTypeCb, - bytecode - ); - } + if (options.typeInfoLevel >= 1 || options.optimizationLevel >= 2) + buildTypeMap( + compiler.functionTypes, + compiler.localTypes, + compiler.exprTypes, + root, + options.vectorType, + compiler.userdataTypes, + compiler.builtinTypes, + compiler.builtins, + compiler.globals, + options.libraryMemberTypeCb, + bytecode + ); for (AstExprFunction* expr : functions) { diff --git a/Sources.cmake b/Sources.cmake index d574e921..990326b5 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -175,6 +175,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Autocomplete.h Analysis/include/Luau/AutocompleteTypes.h Analysis/include/Luau/BuiltinDefinitions.h + Analysis/include/Luau/BuiltinTypeFunctions.h Analysis/include/Luau/Cancellation.h Analysis/include/Luau/Clone.h Analysis/include/Luau/Constraint.h @@ -248,6 +249,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Unifier.h Analysis/include/Luau/Unifier2.h Analysis/include/Luau/UnifierSharedState.h + Analysis/include/Luau/UserDefinedTypeFunction.h Analysis/include/Luau/VisitType.h Analysis/src/Anyification.cpp @@ -257,6 +259,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/Autocomplete.cpp Analysis/src/AutocompleteCore.cpp Analysis/src/BuiltinDefinitions.cpp + Analysis/src/BuiltinTypeFunctions.cpp Analysis/src/Clone.cpp Analysis/src/Constraint.cpp Analysis/src/ConstraintGenerator.cpp @@ -316,6 +319,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/Unifiable.cpp Analysis/src/Unifier.cpp Analysis/src/Unifier2.cpp + Analysis/src/UserDefinedTypeFunction.cpp ) # Luau.EqSat Sources diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 5178a9b4..20a7ee08 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -21,7 +21,7 @@ LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauExpectedTypeVisitor) -LUAU_FASTFLAG(LuauImplicitTableIndexerKeys2) +LUAU_FASTFLAG(LuauImplicitTableIndexerKeys3) LUAU_FASTFLAG(LuauPushFunctionTypesInFunctionStatement) using namespace Luau; @@ -4622,7 +4622,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_implicit_named_index_index_expr") // Somewhat surprisingly, the old solver didn't cover this case. {FFlag::LuauSolverV2, true}, {FFlag::LuauExpectedTypeVisitor, true}, - {FFlag::LuauImplicitTableIndexerKeys2, true}, + {FFlag::LuauImplicitTableIndexerKeys3, true}, }; check(R"( @@ -4649,7 +4649,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_implicit_named_index_index_expr_witho ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, {FFlag::LuauExpectedTypeVisitor, true}, - {FFlag::LuauImplicitTableIndexerKeys2, false}, + {FFlag::LuauImplicitTableIndexerKeys3, true}, }; check(R"( diff --git a/tests/DataFlowGraph.test.cpp b/tests/DataFlowGraph.test.cpp index b8c53cc2..2bf5d563 100644 --- a/tests/DataFlowGraph.test.cpp +++ b/tests/DataFlowGraph.test.cpp @@ -13,7 +13,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAG(LuauDoNotAddUpvalueTypesToLocalType) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) struct DataFlowGraphFixture @@ -447,8 +446,6 @@ TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node_3") TEST_CASE_FIXTURE(DataFlowGraphFixture, "function_captures_are_phi_nodes_of_all_versions") { - ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true}; - dfg(R"( local x = 5 diff --git a/tests/Fixture.h b/tests/Fixture.h index 032e7374..91b7bdd5 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/BuiltinTypeFunctions.h" #include "Luau/Config.h" #include "Luau/EqSatSimplification.h" #include "Luau/Error.h" diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 984198a7..cab7b19a 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -16,7 +16,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(LuauNewNonStrictVisitTypes2) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck2) namespace @@ -977,10 +976,7 @@ TEST_CASE_FIXTURE(FrontendFixture, "environments") LUAU_REQUIRE_NO_ERRORS(resultA); CheckResult resultB = getFrontend().check("B"); - if (FFlag::LuauSolverV2 && !FFlag::LuauNewNonStrictVisitTypes2) - LUAU_REQUIRE_NO_ERRORS(resultB); - else - LUAU_REQUIRE_ERROR_COUNT(1, resultB); + LUAU_REQUIRE_ERROR_COUNT(1, resultB); CheckResult resultC = getFrontend().check("C"); LUAU_REQUIRE_ERROR_COUNT(1, resultC); diff --git a/tests/IrLowering.test.cpp b/tests/IrLowering.test.cpp index c8e6632a..59e56c32 100644 --- a/tests/IrLowering.test.cpp +++ b/tests/IrLowering.test.cpp @@ -16,7 +16,7 @@ #include #include -LUAU_FASTFLAG(LuauCodeGenSimplifyImport) +LUAU_FASTFLAG(LuauCodeGenSimplifyImport2) static void luauLibraryConstantLookup(const char* library, const char* member, Luau::CompileConstant* constant) { @@ -500,7 +500,7 @@ bb_bytecode_1: TEST_CASE("DseInitialStackState") { - ScopedFastFlag luauCodeGenSimplifyImport{FFlag::LuauCodeGenSimplifyImport, true}; + ScopedFastFlag luauCodeGenSimplifyImport{FFlag::LuauCodeGenSimplifyImport2, true}; CHECK_EQ( "\n" + getCodegenAssembly(R"( @@ -1567,7 +1567,7 @@ end TEST_CASE("ForInManualAnnotation") { - ScopedFastFlag luauCodeGenSimplifyImport{FFlag::LuauCodeGenSimplifyImport, true}; + ScopedFastFlag luauCodeGenSimplifyImport{FFlag::LuauCodeGenSimplifyImport2, true}; CHECK_EQ( "\n" + getCodegenAssembly( diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index 1d9eec9f..190366ad 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -15,7 +15,6 @@ #include "doctest.h" #include -LUAU_FASTFLAG(LuauNewNonStrictVisitTypes2) LUAU_FASTFLAG(LuauNewNonStrictFixGenericTypePacks) LUAU_FASTFLAG(LuauNewNonStrictMoreUnknownSymbols) LUAU_FASTFLAG(LuauNewNonStrictNoErrorsPassingNever) @@ -737,8 +736,6 @@ TEST_CASE_FIXTURE(Fixture, "unknown_globals_in_non_strict_1") TEST_CASE_FIXTURE(BuiltinsFixture, "unknown_types_in_non_strict") { - ScopedFastFlag sff{FFlag::LuauNewNonStrictVisitTypes2, true}; - CheckResult result = check(Mode::Nonstrict, R"( --!nonstrict local foo: Foo = 1 @@ -752,8 +749,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "unknown_types_in_non_strict") TEST_CASE_FIXTURE(BuiltinsFixture, "unknown_types_in_non_strict_2") { - ScopedFastFlag sff{FFlag::LuauNewNonStrictVisitTypes2, true}; - CheckResult result = check(Mode::Nonstrict, R"( --!nonstrict local foo = 1 :: Foo diff --git a/tests/Transpiler.test.cpp b/tests/Transpiler.test.cpp index e4d415b5..d57b1473 100644 --- a/tests/Transpiler.test.cpp +++ b/tests/Transpiler.test.cpp @@ -12,8 +12,6 @@ using namespace Luau; -LUAU_FASTFLAG(LuauCSTForReturnTypeFunctionTail) - TEST_SUITE_BEGIN("TranspilerTests"); TEST_CASE("test_1") @@ -2055,9 +2053,6 @@ TEST_CASE("transpile_type_function_return_types") TEST_CASE("transpile_chained_function_types") { - ScopedFastFlag fflags[] = { - {FFlag::LuauCSTForReturnTypeFunctionTail, true}, - }; std::string code = R"( type Foo = () -> () -> () )"; CHECK_EQ(code, transpile(code, {}, true).code); diff --git a/tests/TypeFunction.test.cpp b/tests/TypeFunction.test.cpp index 6deb5d33..5d459961 100644 --- a/tests/TypeFunction.test.cpp +++ b/tests/TypeFunction.test.cpp @@ -1813,7 +1813,7 @@ struct TFFixture BuiltinTypeFunctions builtinTypeFunctions; - TypeFunctionContext tfc{ + TypeFunctionContext tfc_{ arena, getBuiltins(), NotNull{globalScope.get()}, @@ -1823,6 +1823,8 @@ struct TFFixture NotNull{&ice}, NotNull{&limits} }; + + NotNull tfc{&tfc_}; }; TEST_CASE_FIXTURE(TFFixture, "refine") diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 4ba69f5a..b2bb3448 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -10,7 +10,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauNewNonStrictVisitTypes2) LUAU_FASTFLAG(LuauGuardAgainstMalformedTypeAliasExpansion2) LUAU_FASTFLAG(LuauSkipMalformedTypeAliases) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck2) @@ -1180,10 +1179,7 @@ TEST_CASE_FIXTURE(Fixture, "bound_type_in_alias_segfault") export type FieldConfigMap = Map> )"); - if (FFlag::LuauNewNonStrictVisitTypes2) - LUAU_CHECK_ERROR_COUNT(2, result); - else - LUAU_CHECK_NO_ERRORS(result); + LUAU_CHECK_ERROR_COUNT(2, result); } TEST_CASE_FIXTURE(BuiltinsFixture, "gh1632_no_infinite_recursion_in_normalization") diff --git a/tests/TypeInfer.anyerror.test.cpp b/tests/TypeInfer.anyerror.test.cpp index 521ed781..db202f65 100644 --- a/tests/TypeInfer.anyerror.test.cpp +++ b/tests/TypeInfer.anyerror.test.cpp @@ -14,7 +14,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) LUAU_FASTFLAG(LuauRemoveTypeCallsForReadWriteProps) @@ -36,16 +35,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any") LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) - { - if (FFlag::LuauAddCallConstraintForIterableFunctions) - { - CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); - } - else - { - CHECK("any?" == toString(requireType("a"))); - } - } + CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); else CHECK(getBuiltins()->anyType == requireType("a")); } @@ -66,16 +56,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2") LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) - { - if (FFlag::LuauAddCallConstraintForIterableFunctions) - { - CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); - } - else - { - CHECK("any?" == toString(requireType("a"))); - } - } + CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); else CHECK("any" == toString(requireType("a"))); } @@ -94,16 +75,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any") LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) - { - if (FFlag::LuauAddCallConstraintForIterableFunctions) - { - CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); - } - else - { - CHECK("any?" == toString(requireType("a"))); - } - } + CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); else CHECK("any" == toString(requireType("a"))); } @@ -120,16 +92,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any2") )"); if (FFlag::LuauSolverV2) - { - if (FFlag::LuauAddCallConstraintForIterableFunctions) - { - CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); - } - else - { - CHECK("any?" == toString(requireType("a"))); - } - } + CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); else CHECK("any" == toString(requireType("a"))); } @@ -148,16 +111,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any_pack") LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) - { - if (FFlag::LuauAddCallConstraintForIterableFunctions) - { - CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); - } - else - { - CHECK("any?" == toString(requireType("a"))); - } - } + CHECK("(*error-type* | ~nil)?" == toString(requireType("a"))); else CHECK("any" == toString(requireType("a"))); } diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index c2d99da8..e0a6ea57 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -18,7 +18,6 @@ LUAU_FASTFLAG(LuauRemoveTypeCallsForReadWriteProps) LUAU_FASTFLAG(LuauWriteOnlyPropertyMangling) LUAU_FASTFLAG(LuauEnableWriteOnlyProperties) LUAU_FASTFLAG(LuauTableLiteralSubtypeCheckFunctionCalls) -LUAU_FASTFLAG(LuauTypeCheckerStricterIndexCheck) LUAU_FASTFLAG(LuauSuppressErrorsForMultipleNonviableOverloads) TEST_SUITE_BEGIN("BuiltinTests"); @@ -1767,8 +1766,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "read_refinements_on_persistent_tables_known_ TEST_CASE_FIXTURE(BuiltinsFixture, "read_refinements_on_persistent_tables_unknown_property") { - ScopedFastFlag _{FFlag::LuauTypeCheckerStricterIndexCheck, true}; - CheckResult results = check(R"( if bit32.scrambleEggs then end diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index b04c1984..5818e0f8 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -25,7 +25,6 @@ LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauArityMismatchOnUndersaturatedUnknownArguments) LUAU_FASTFLAG(LuauFormatUseLastPosition) -LUAU_FASTFLAG(LuauDoNotAddUpvalueTypesToLocalType) LUAU_FASTFLAG(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck2) LUAU_FASTFLAG(LuauStuckTypeFunctionsStillDispatch) @@ -3162,7 +3161,7 @@ TEST_CASE_FIXTURE(Fixture, "recursive_function_calls_should_not_use_the_generali TEST_CASE_FIXTURE(Fixture, "fuzz_unwind_mutually_recursive_union_type_func") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauDoNotAddUpvalueTypesToLocalType, true}}; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; // Previously, this block minted a type like: // diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index d923e13e..d9b66bbc 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -9,12 +9,10 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck2) LUAU_FASTFLAG(LuauIntersectNotNil) LUAU_FASTFLAG(LuauSubtypingCheckFunctionGenericCounts) -LUAU_FASTFLAG(LuauReportSubtypingErrors) LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauStuckTypeFunctionsStillDispatch) LUAU_FASTFLAG(LuauRemoveTypeCallsForReadWriteProps) @@ -785,7 +783,7 @@ TEST_CASE_FIXTURE(Fixture, "instantiated_function_argument_names_old_solver") TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_generic_types") { - ScopedFastFlag sffs[] = {{FFlag::LuauReportSubtypingErrors, true}, {FFlag::LuauSubtypingCheckFunctionGenericCounts, true}}; + ScopedFastFlag _{FFlag::LuauSubtypingCheckFunctionGenericCounts, true}; CheckResult result = check(R"( type C = () -> () @@ -818,7 +816,7 @@ local d: D = c } TEST_CASE_FIXTURE(Fixture, "generic_function_mismatch_with_argument") { - ScopedFastFlag sffs[] = {{FFlag::LuauReportSubtypingErrors, true}, {FFlag::LuauSubtypingCheckFunctionGenericCounts, true}}; + ScopedFastFlag _{FFlag::LuauSubtypingCheckFunctionGenericCounts, true}; CheckResult result = check(R"( type C = (number) -> () @@ -852,7 +850,7 @@ local d: D = c TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_generic_pack") { - ScopedFastFlag sffs[] = {{FFlag::LuauReportSubtypingErrors, true}, {FFlag::LuauSubtypingCheckFunctionGenericCounts, true}}; + ScopedFastFlag _{FFlag::LuauSubtypingCheckFunctionGenericCounts, true}; CheckResult result = check(R"( type C = () -> () @@ -1530,7 +1528,6 @@ TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument_overloaded" TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_infer_generic_functions") { ScopedFastFlag _[] = { - {FFlag::LuauReportSubtypingErrors, true}, {FFlag::LuauSubtypingCheckFunctionGenericCounts, true}, {FFlag::LuauEagerGeneralization4, true}, }; diff --git a/tests/TypeInfer.loops.test.cpp b/tests/TypeInfer.loops.test.cpp index c588272c..bd606338 100644 --- a/tests/TypeInfer.loops.test.cpp +++ b/tests/TypeInfer.loops.test.cpp @@ -15,9 +15,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAG(LuauSimplifyOutOfLine2) -LUAU_FASTFLAG(LuauDfgIfBlocksShouldRespectControlFlow) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) TEST_SUITE_BEGIN("TypeInferLoops"); @@ -158,7 +156,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop") TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_next") { - ScopedFastFlag _{FFlag::LuauAddCallConstraintForIterableFunctions, true}; CheckResult result = check(R"( local n local s @@ -187,7 +184,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_next") TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_next_and_multiple_elements") { ScopedFastFlag sffs[] = { - {FFlag::LuauAddCallConstraintForIterableFunctions, true}, {FFlag::LuauSimplifyOutOfLine2, true}, {FFlag::LuauDfgAllowUpdatesInLoops, true}, }; @@ -289,14 +285,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_with_a_custom_iterator_should_type_ch end )"); - if (FFlag::LuauSolverV2 && FFlag::LuauAddCallConstraintForIterableFunctions) - { + if (FFlag::LuauSolverV2) LUAU_REQUIRE_NO_ERRORS(result); - } else - { LUAU_REQUIRE_ERROR_COUNT(1, result); - } } TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_error") @@ -1286,7 +1278,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "forin_metatable_iter_mm") TEST_CASE_FIXTURE(BuiltinsFixture, "iteration_preserves_error_suppression") { - ScopedFastFlag _{FFlag::LuauAddCallConstraintForIterableFunctions, true}; ScopedFastFlag v1{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( @@ -1504,10 +1495,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "repeat_is_linearish") if (!FFlag::LuauSolverV2) return; - ScopedFastFlag sffs[] = { - {FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}, - {FFlag::LuauDfgAllowUpdatesInLoops, true}, - }; + ScopedFastFlag _{FFlag::LuauDfgAllowUpdatesInLoops, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( local x = nil diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index dcd2409c..3ea0832f 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -12,7 +12,6 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping) diff --git a/tests/TypeInfer.negations.test.cpp b/tests/TypeInfer.negations.test.cpp index 60316955..2a190aab 100644 --- a/tests/TypeInfer.negations.test.cpp +++ b/tests/TypeInfer.negations.test.cpp @@ -9,6 +9,8 @@ using namespace Luau; +LUAU_FASTFLAG(LuauEagerGeneralization4) + namespace { @@ -50,9 +52,8 @@ TEST_CASE_FIXTURE(NegationFixture, "string_is_not_a_subtype_of_negated_string") TEST_CASE_FIXTURE(Fixture, "cofinite_strings_can_be_compared_for_equality") { - // CLI-117082 Cofinite strings cannot be compared for equality because normalization produces a large type with cycles - if (FFlag::LuauSolverV2) - return; + ScopedFastFlag sff{FFlag::LuauEagerGeneralization4, true}; + CheckResult result = check(R"( function f(e) if e == 'strictEqual' then diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 8cc6f48c..35f93128 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -12,15 +12,14 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauFunctionCallsAreNotNilable) -LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAG(LuauSimplificationTableExternType) LUAU_FASTFLAG(LuauBetterCannotCallFunctionPrimitive) LUAU_FASTFLAG(LuauStuckTypeFunctionsStillDispatch) -LUAU_FASTFLAG(LuauTypeCheckerStricterIndexCheck) LUAU_FASTFLAG(LuauNormalizationIntersectTablesPreservesExternTypes) LUAU_FASTFLAG(LuauNormalizationReorderFreeTypeIntersect) LUAU_FASTFLAG(LuauAvoidDoubleNegation) LUAU_FASTFLAG(LuauRefineTablesWithReadType) +LUAU_FASTFLAG(LuauRefineNoRefineAlways) using namespace Luau; @@ -1944,10 +1943,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_clone_it") TEST_CASE_FIXTURE(RefinementExternTypeFixture, "refine_a_param_that_got_resolved_during_constraint_solving_stage") { - // CLI-117134 - Applying a refinement causes an optional value access error. - if (FFlag::LuauSolverV2) - return; - CheckResult result = check(R"( type Id = T @@ -2216,14 +2211,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauAddCallConstraintForIterableFunctions) - { - CHECK_EQ("(unknown) -> (~nil, unknown)", toString(requireType("f"))); - } - else - { - CHECK_EQ("(unknown) -> (unknown, unknown)", toString(requireType("f"))); - } + CHECK_EQ("(unknown) -> (~nil, unknown)", toString(requireType("f"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing") @@ -2713,7 +2701,6 @@ TEST_CASE_FIXTURE(RefinementExternTypeFixture, "cannot_call_a_function_single") ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, {FFlag::LuauBetterCannotCallFunctionPrimitive, true}, - {FFlag::LuauTypeCheckerStricterIndexCheck, true}, {FFlag::LuauRefineTablesWithReadType, true}, }; @@ -2734,7 +2721,6 @@ TEST_CASE_FIXTURE(RefinementExternTypeFixture, "cannot_call_a_function_union") ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, {FFlag::LuauBetterCannotCallFunctionPrimitive, true}, - {FFlag::LuauTypeCheckerStricterIndexCheck, true}, {FFlag::LuauRefineTablesWithReadType, true}, }; @@ -2830,4 +2816,34 @@ TEST_CASE_FIXTURE(Fixture, "limit_complexity_of_arithmetic_type_functions" * doc LUAU_REQUIRE_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "refine_by_no_refine_should_always_reduce") +{ + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + ScopedFastFlag refineNoRefineAlways{FFlag::LuauRefineNoRefineAlways, true}; + + CheckResult result = check(R"( + function foo(t): boolean return true end + + function select(t: { [K]: V }, columns: { K }): { [K]: V } + local result = {} + if foo(t) then + for k, v in t do + if table.find(columns, k) then + result[k] = v -- was TypeError: Type function instance refine, *no-refine*> is uninhabited + end + end + else + for k, v in pairs(t) do + if table.find(columns, k) then + result[k] = v + end + end + end + return result + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index ec67a9d3..1df666d4 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -23,8 +23,6 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering) LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) -LUAU_FASTFLAG(LuauTypeCheckerStricterIndexCheck) -LUAU_FASTFLAG(LuauReportSubtypingErrors) LUAU_FASTFLAG(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck2) LUAU_FASTFLAG(LuauEnableWriteOnlyProperties) @@ -3818,10 +3816,7 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_a_subtype_of_a_compatible_polymorphic_shap TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type") { - ScopedFastFlag sff[] = { - {FFlag::LuauReportSubtypingErrors, true}, - {FFlag::LuauEagerGeneralization4, true}, - }; + ScopedFastFlag _{FFlag::LuauEagerGeneralization4, true}; CheckResult result = check(R"( local function f(s) @@ -5696,7 +5691,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "function_call_in_indexer_with_compound_assig TEST_CASE_FIXTURE(Fixture, "stop_refining_new_table_indices_for_non_primitive_tables") { ScopedFastFlag _{FFlag::LuauSolverV2, true}; - ScopedFastFlag stricterIndexCheck{FFlag::LuauTypeCheckerStricterIndexCheck, true}; CheckResult result = check(R"( local foo:{val:number} = {val = 1} diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index b54d4552..dc7af332 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -24,9 +24,7 @@ LUAU_FASTINT(LuauNormalizeCacheLimit) LUAU_FASTINT(LuauRecursionLimit) LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) -LUAU_FASTFLAG(LuauMagicFreezeCheckBlocked2) LUAU_FASTFLAG(LuauEagerGeneralization4) -LUAU_FASTFLAG(LuauReportSubtypingErrors) LUAU_FASTFLAG(LuauAvoidDoubleNegation) LUAU_FASTFLAG(LuauInsertErrorTypesIntoIndexerResult) LUAU_FASTFLAG(LuauSimplifyOutOfLine2) @@ -1214,27 +1212,14 @@ TEST_CASE_FIXTURE(Fixture, "type_infer_recursion_limit_normalizer") if (FFlag::LuauSolverV2) { - if (FFlag::LuauReportSubtypingErrors) - { - CHECK(4 == result.errors.size()); - CHECK(Location{{2, 22}, {2, 42}} == result.errors[0].location); - CHECK(Location{{3, 22}, {3, 42}} == result.errors[1].location); - CHECK(Location{{3, 45}, {3, 46}} == result.errors[2].location); - CHECK(Location{{3, 22}, {3, 41}} == result.errors[3].location); + CHECK(4 == result.errors.size()); + CHECK(Location{{2, 22}, {2, 42}} == result.errors[0].location); + CHECK(Location{{3, 22}, {3, 42}} == result.errors[1].location); + CHECK(Location{{3, 45}, {3, 46}} == result.errors[2].location); + CHECK(Location{{3, 22}, {3, 41}} == result.errors[3].location); - for (const TypeError& e : result.errors) - CHECK_EQ("Code is too complex to typecheck! Consider simplifying the code around this area", toString(e)); - } - else - { - CHECK(3 == result.errors.size()); - CHECK(Location{{2, 22}, {2, 42}} == result.errors[0].location); - CHECK(Location{{3, 22}, {3, 42}} == result.errors[1].location); - CHECK(Location{{3, 22}, {3, 41}} == result.errors[2].location); - CHECK_EQ("Code is too complex to typecheck! Consider simplifying the code around this area", toString(result.errors[0])); - CHECK_EQ("Code is too complex to typecheck! Consider simplifying the code around this area", toString(result.errors[1])); - CHECK_EQ("Code is too complex to typecheck! Consider simplifying the code around this area", toString(result.errors[2])); - } + for (const TypeError& e : result.errors) + CHECK_EQ("Code is too complex to typecheck! Consider simplifying the code around this area", toString(e)); } else { @@ -1986,7 +1971,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_allows_singleton_union_or_intersection") TEST_CASE_FIXTURE(BuiltinsFixture, "assert_table_freeze_constraint_solving") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauMagicFreezeCheckBlocked2, true}}; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( local f = table.freeze f(table) @@ -1995,7 +1980,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "assert_table_freeze_constraint_solving") TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_assert_table_freeze_constraint_solving") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauMagicFreezeCheckBlocked2, true}}; + ScopedFastFlag _ {FFlag::LuauSolverV2, true}; // This is the original fuzzer version of the above issue. CheckResult results = check(R"( local function l0() @@ -2116,8 +2101,6 @@ local _ TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_missing_follow_table_freeze") { - ScopedFastFlag _{FFlag::LuauMagicFreezeCheckBlocked2, true}; - LUAU_REQUIRE_ERRORS(check(R"( if _:freeze(_)[_][_] then else diff --git a/tests/TypeInfer.typePacks.test.cpp b/tests/TypeInfer.typePacks.test.cpp index 80f50f99..330976e0 100644 --- a/tests/TypeInfer.typePacks.test.cpp +++ b/tests/TypeInfer.typePacks.test.cpp @@ -13,7 +13,6 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauEagerGeneralization4) -LUAU_FASTFLAG(LuauReportSubtypingErrors) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck2) LUAU_FASTFLAG(LuauFixEmptyTypePackStringification) @@ -1063,8 +1062,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "detect_cyclic_typepacks") TEST_CASE_FIXTURE(BuiltinsFixture, "detect_cyclic_typepacks2") { - ScopedFastFlag _{FFlag::LuauReportSubtypingErrors, true}; - CheckResult result = check(R"( function _(l0:((typeof((pcall)))|((((t0)->())|(typeof(-67108864)))|(any)))|(any),...):(((typeof(0))|(any))|(any),typeof(-67108864),any) xpcall(_,_,_) diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index ff5c34f0..ae8a3aa3 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -4,9 +4,6 @@ #include "doctest.h" LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauDoNotAddUpvalueTypesToLocalType) -LUAU_FASTFLAG(LuauDfgIfBlocksShouldRespectControlFlow) -LUAU_FASTFLAG(LuauReportSubtypingErrors) LUAU_FASTFLAG(LuauEagerGeneralization4) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck2) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) @@ -269,8 +266,6 @@ TEST_CASE_FIXTURE(TypeStateFixture, "local_assigned_in_only_one_branch_that_fall TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_and_else_branch_also_assigns_but_is_met_with_return") { - ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}; - CheckResult result = check(R"( local x = nil if math.random() > 0.5 then @@ -288,8 +283,6 @@ TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_and_else_branch_also_as TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_but_is_met_with_return_and_else_branch_assigns") { - ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}; - CheckResult result = check(R"( local x = nil if math.random() > 0.5 then @@ -350,7 +343,6 @@ TEST_CASE_FIXTURE(TypeStateFixture, "local_t_is_assigned_a_fresh_table_with_x_as TEST_CASE_FIXTURE(TypeStateFixture, "captured_locals_do_not_mutate_upvalue_type") { - ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true}; CheckResult result = check(R"( local x = nil @@ -372,7 +364,6 @@ TEST_CASE_FIXTURE(TypeStateFixture, "captured_locals_do_not_mutate_upvalue_type" TEST_CASE_FIXTURE(TypeStateFixture, "captured_locals_do_not_mutate_upvalue_type_2") { - ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true}; CheckResult result = check(R"( local t = {x = nil} @@ -413,7 +404,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyped_recursive_functions_but_has_futur { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauReportSubtypingErrors, true}, {FFlag::LuauEagerGeneralization4, true}, }; @@ -477,25 +467,6 @@ TEST_CASE_FIXTURE(TypeStateFixture, "typestates_preserve_error_suppression") CHECK("*error-type* | string" == toString(requireTypeAtPosition({3, 14}), {true})); } - -TEST_CASE_FIXTURE(BuiltinsFixture, "typestates_preserve_error_suppression_properties") -{ - // early return if the flag isn't set since this is blocking gated commits - // unconditional return - // CLI-117098 Type states with error suppressing properties doesn't infer the correct type for properties. - if (!FFlag::LuauSolverV2 || FFlag::LuauSolverV2) - return; - - CheckResult result = check(R"( - local a: {x: any} = {x = 51} - a.x = "pickles" -- We'll have a new DefId for this iteration of `a.x`. Its type must also be error-suppressing - print(a.x) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK("*error-type* | string" == toString(requireTypeAtPosition({3, 16}), {true})); -} - TEST_CASE_FIXTURE(BuiltinsFixture, "typestates_do_not_apply_to_the_initial_local_definition") { // early return if the flag isn't set since this is blocking gated commits @@ -577,8 +548,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_normalized_type_variables_are_bad" * TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1547_simple") { - ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( local rand = 0 @@ -594,8 +563,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1547_simple") TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1547") { - ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( local rand = 0 @@ -629,8 +596,6 @@ TEST_CASE_FIXTURE(Fixture, "modify_captured_table_field") TEST_CASE_FIXTURE(Fixture, "oss_1561") { - ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true}; - loadDefinition(R"( declare class Vector3 X: number @@ -655,8 +620,6 @@ TEST_CASE_FIXTURE(Fixture, "oss_1561") TEST_CASE_FIXTURE(Fixture, "oss_1575") { - ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( local flag = true local function Flip() @@ -667,8 +630,6 @@ TEST_CASE_FIXTURE(Fixture, "oss_1575") TEST_CASE_FIXTURE(Fixture, "capture_upvalue_in_returned_function") { - ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( function def() local i : number = 0 @@ -684,8 +645,6 @@ TEST_CASE_FIXTURE(Fixture, "capture_upvalue_in_returned_function") TEST_CASE_FIXTURE(BuiltinsFixture, "throw_in_else_branch") { - ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}; - CheckResult result = check(R"( --!strict local x @@ -707,8 +666,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "throw_in_else_branch") TEST_CASE_FIXTURE(BuiltinsFixture, "throw_in_if_branch") { - ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}; - CheckResult result = check(R"( --!strict local x @@ -731,8 +688,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "throw_in_if_branch") TEST_CASE_FIXTURE(BuiltinsFixture, "refinement_through_erroring") { - ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}; - CheckResult result = check(R"( --!strict type Payload = { payload: number } @@ -756,7 +711,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refinement_through_erroring") TEST_CASE_FIXTURE(BuiltinsFixture, "refinement_through_erroring_in_loop") { ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, {FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}, {FFlag::LuauDfgAllowUpdatesInLoops, true} + {FFlag::LuauSolverV2, true}, {FFlag::LuauDfgAllowUpdatesInLoops, true} }; CheckResult result = check(R"( @@ -778,8 +733,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refinement_through_erroring_in_loop") TEST_CASE_FIXTURE(BuiltinsFixture, "type_refinement_in_loop") { - ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}; - CheckResult result = check(R"( --!strict local function onEachString(t: { string | number }) @@ -800,10 +753,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_refinement_in_loop") TEST_CASE_FIXTURE(BuiltinsFixture, "throw_in_if_branch_and_do_nothing_in_else") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}, - }; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( --!strict @@ -825,10 +775,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "throw_in_if_branch_and_do_nothing_in_else") TEST_CASE_FIXTURE(BuiltinsFixture, "assign_in_an_if_branch_without_else") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}, - }; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( --!strict diff --git a/tests/conformance/native.luau b/tests/conformance/native.luau index 16172bab..9372c8e9 100644 --- a/tests/conformance/native.luau +++ b/tests/conformance/native.luau @@ -513,6 +513,23 @@ end assert(extramath3(2) == "number") assert(extramath3("2") == "number") +local function scopedrestorefailurea(format: string, b: buffer): (string, buffer) + local H1F, H2F, H3F, H4F = buffer.readu32(b, 0), buffer.readu32(b, 8), buffer.readu32(b, 16), buffer.readu32(b, 24) + local H5F, H6F, _, _ = buffer.readu32(b, 32), buffer.readu32(b, 40), buffer.readu32(b, 48), buffer.readu32(b, 56) + local H1B, H2B, H3B, H4B = buffer.readu32(b, 4), buffer.readu32(b, 12), buffer.readu32(b, 20), buffer.readu32(b, 28) + local H5B, H6B, _, _ = buffer.readu32(b, 36), buffer.readu32(b, 44), buffer.readu32(b, 52), buffer.readu32(b, 60) + + return string.format(format, H1F, H1B, H2F, H2B, H3F, H3B, H4F, H4B, H5F, H5B, H6F, H6B), b +end + +local function scopedrestorefailureb() + local data = buffer.create(64) + buffer.fill(data, 0, 0x12, 64) + assert(scopedrestorefailurea(string.rep("%08x", 12), data) == string.rep("12", 48)) +end + +scopedrestorefailureb() + local function slotcachelimit1() local tbl = { f1 = function() return 1 end,