From 24fdac4c05f5ae189ede5153a83af4c1bd25351d Mon Sep 17 00:00:00 2001 From: Lily Brown Date: Fri, 13 Oct 2023 13:20:12 -0700 Subject: [PATCH] Sync to upstream/release/599 (#1069) ## What's Changed - Improve POSIX compliance in `CLI/FileUtils.cpp` by @SamuraiCrow #1064 - `AstStat*::hasEnd` is deprecated; use `AstStatBlock::hasEnd` instead - Added a lint for common misuses of the `#` operator - Luau now issues deprecated diagnostics for some uses of `getfenv` and `setfenv` - Fixed a case where we included a trailing space in some error stringifications ### Compiler - Do not do further analysis in O2 on self functions - Improve detection of invalid repeat..until expressions vs continue ### New Type Solver - We now cache subtype test results to improve performance - Improved operator inference mechanics (aka type families) - Further work towards type states - Work towards [new non-strict mode](https://github.com/Roblox/luau/blob/master/rfcs/new-nonstrict.md) continues ### Native Codegen - Instruction last use locations should follow the order in which blocks are lowered - Add a bonus assertion to IrLoweringA64::tempAddr --- Co-authored-by: Arseny Kapoulkine Co-authored-by: Vyacheslav Egorov Co-authored-by: Andy Friesen Co-authored-by: Aaron Weiss Co-authored-by: Alexander McCord Co-authored-by: Vighnesh Vijay --- .../include/Luau/ConstraintGraphBuilder.h | 10 +- Analysis/include/Luau/DataFlowGraph.h | 12 +- Analysis/include/Luau/Error.h | 12 +- Analysis/include/Luau/Frontend.h | 4 +- Analysis/include/Luau/NonStrictTypeChecker.h | 6 +- Analysis/include/Luau/Subtyping.h | 131 +++--- Analysis/include/Luau/TypeFamily.h | 12 +- Analysis/include/Luau/TypePairHash.h | 34 ++ Analysis/include/Luau/Unifier2.h | 24 +- Analysis/src/AstJsonEncoder.cpp | 26 +- Analysis/src/Autocomplete.cpp | 91 +++- Analysis/src/ConstraintGraphBuilder.cpp | 343 +++++++++------ Analysis/src/ConstraintSolver.cpp | 55 +-- Analysis/src/DataFlowGraph.cpp | 108 +++-- Analysis/src/Error.cpp | 20 +- Analysis/src/Frontend.cpp | 19 +- Analysis/src/IostreamHelpers.cpp | 3 + Analysis/src/Linter.cpp | 77 +++- Analysis/src/NonStrictTypeChecker.cpp | 389 ++++++++++++++++- Analysis/src/Subtyping.cpp | 372 +++++++++------- Analysis/src/TypeFamily.cpp | 406 +++++++++++++++--- Analysis/src/Unifier.cpp | 27 +- Ast/include/Luau/Ast.h | 40 +- Ast/src/Ast.cpp | 26 +- Ast/src/Parser.cpp | 35 +- CodeGen/include/Luau/IrAnalysis.h | 2 +- CodeGen/include/Luau/IrUtils.h | 2 +- CodeGen/src/CodeGenLower.h | 24 +- CodeGen/src/IrAnalysis.cpp | 48 ++- CodeGen/src/IrLoweringA64.cpp | 5 +- CodeGen/src/IrLoweringX64.cpp | 3 - CodeGen/src/IrUtils.cpp | 2 +- Compiler/src/Compiler.cpp | 131 +++--- Sources.cmake | 2 + tests/AstJsonEncoder.test.cpp | 34 +- tests/Compiler.test.cpp | 38 +- tests/Differ.test.cpp | 4 +- tests/Fixture.cpp | 5 +- tests/Fixture.h | 2 +- tests/Linter.test.cpp | 86 ++++ tests/NonStrictTypeChecker.test.cpp | 102 +---- tests/Parser.test.cpp | 106 +++++ tests/Subtyping.test.cpp | 41 ++ tests/ToString.test.cpp | 8 +- tests/TypeInfer.builtins.test.cpp | 4 +- tests/TypeInfer.classes.test.cpp | 4 +- tests/TypeInfer.functions.test.cpp | 12 +- tests/TypeInfer.generics.test.cpp | 4 +- tests/TypeInfer.intersectionTypes.test.cpp | 2 +- tests/TypeInfer.modules.test.cpp | 4 +- tests/TypeInfer.operators.test.cpp | 85 +++- tests/TypeInfer.provisional.test.cpp | 38 +- tests/TypeInfer.refinements.test.cpp | 17 +- tests/TypeInfer.singletons.test.cpp | 4 +- tests/TypeInfer.tables.test.cpp | 30 +- tests/TypeInfer.test.cpp | 6 +- tests/TypeInfer.typestates.test.cpp | 122 ++++++ tests/TypeInfer.unionTypes.test.cpp | 10 +- tools/faillist.txt | 118 ++++- 59 files changed, 2437 insertions(+), 950 deletions(-) create mode 100644 Analysis/include/Luau/TypePairHash.h create mode 100644 tests/TypeInfer.typestates.test.cpp diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGraphBuilder.h index 526101a45..955a683ec 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGraphBuilder.h @@ -69,6 +69,12 @@ struct ConstraintGraphBuilder // This is null when the CGB is initially constructed. Scope* rootScope; + // During constraint generation, we only populate the Scope::bindings + // property for annotated symbols. Unannotated symbols must be handled in a + // postprocessing step because we do not yet have the full breadcrumb graph. + // We queue them up here. + std::vector> inferredBindings; + // Constraints that go straight to the solver. std::vector constraints; @@ -205,8 +211,6 @@ struct ConstraintGraphBuilder Inference check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType); std::tuple checkBinary(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType); - std::vector checkLValues(const ScopePtr& scope, AstArray exprs); - TypeId checkLValue(const ScopePtr& scope, AstExpr* expr); TypeId checkLValue(const ScopePtr& scope, AstExprLocal* local); TypeId checkLValue(const ScopePtr& scope, AstExprGlobal* global); @@ -303,6 +307,8 @@ struct ConstraintGraphBuilder */ void prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program); + void fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block); + /** Given a function type annotation, return a vector describing the expected types of the calls to the function * For example, calling a function with annotation ((number) -> string & ((string) -> number)) * yields a vector of size 1, with value: [number | string] diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index ce4ecb04c..63be6b942 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -120,12 +120,12 @@ struct DataFlowGraphBuilder BreadcrumbId visitExpr(DfgScope* scope, AstExprInterpString* i); BreadcrumbId visitExpr(DfgScope* scope, AstExprError* error); - void visitLValue(DfgScope* scope, AstExpr* e); - void visitLValue(DfgScope* scope, AstExprLocal* l); - void visitLValue(DfgScope* scope, AstExprGlobal* g); - void visitLValue(DfgScope* scope, AstExprIndexName* i); - void visitLValue(DfgScope* scope, AstExprIndexExpr* i); - void visitLValue(DfgScope* scope, AstExprError* e); + void visitLValue(DfgScope* scope, AstExpr* e, BreadcrumbId bc); + void visitLValue(DfgScope* scope, AstExprLocal* l, BreadcrumbId bc); + void visitLValue(DfgScope* scope, AstExprGlobal* g, BreadcrumbId bc); + void visitLValue(DfgScope* scope, AstExprIndexName* i, BreadcrumbId bc); + void visitLValue(DfgScope* scope, AstExprIndexExpr* i, BreadcrumbId bc); + void visitLValue(DfgScope* scope, AstExprError* e, BreadcrumbId bc); void visitType(DfgScope* scope, AstType* t); void visitType(DfgScope* scope, AstTypeReference* r); diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index db3e8e557..71442fb1d 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -358,13 +358,23 @@ struct PackWhereClauseNeeded bool operator==(const PackWhereClauseNeeded& rhs) const; }; +struct CheckedFunctionCallError +{ + TypeId expected; + TypeId passed; + std::string checkedFunctionName; + // TODO: make this a vector + size_t argumentIndex; + bool operator==(const CheckedFunctionCallError& rhs) const; +}; + using TypeErrorData = Variant; + UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError>; struct TypeErrorSummary { diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index afe47322d..25af52005 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -245,12 +245,12 @@ struct Frontend std::vector moduleQueue; }; -ModulePtr check(const SourceModule& sourceModule, const std::vector& requireCycles, NotNull builtinTypes, +ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector& requireCycles, NotNull builtinTypes, NotNull iceHandler, NotNull moduleResolver, NotNull fileResolver, const ScopePtr& globalScope, std::function prepareModuleScope, FrontendOptions options, TypeCheckLimits limits); -ModulePtr check(const SourceModule& sourceModule, const std::vector& requireCycles, NotNull builtinTypes, +ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector& requireCycles, NotNull builtinTypes, NotNull iceHandler, NotNull moduleResolver, NotNull fileResolver, const ScopePtr& globalScope, std::function prepareModuleScope, FrontendOptions options, TypeCheckLimits limits, bool recordJsonLog); diff --git a/Analysis/include/Luau/NonStrictTypeChecker.h b/Analysis/include/Luau/NonStrictTypeChecker.h index 6f12998e3..ad9a8d7f1 100644 --- a/Analysis/include/Luau/NonStrictTypeChecker.h +++ b/Analysis/include/Luau/NonStrictTypeChecker.h @@ -3,13 +3,17 @@ #include "Luau/Module.h" #include "Luau/NotNull.h" +#include "Luau/DataFlowGraph.h" namespace Luau { struct BuiltinTypes; +struct UnifierSharedState; +struct TypeCheckLimits; +void checkNonStrict(NotNull builtinTypes, NotNull ice, NotNull unifierState, + NotNull dfg, NotNull limits, const SourceModule& sourceModule, Module* module); -void checkNonStrict(NotNull builtinTypes, Module* module); } // namespace Luau diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index c3fb87f9d..f8aa584db 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -3,6 +3,7 @@ #include "Luau/Type.h" #include "Luau/TypePack.h" +#include "Luau/TypePairHash.h" #include "Luau/UnifierSharedState.h" #include @@ -28,6 +29,7 @@ struct SubtypingResult bool isSubtype = false; bool isErrorSuppressing = false; bool normalizationTooComplex = false; + bool isCacheable = true; SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other); @@ -38,6 +40,24 @@ struct SubtypingResult static SubtypingResult any(const std::vector& results); }; +struct SubtypingEnvironment +{ + struct GenericBounds + { + DenseHashSet lowerBound{nullptr}; + DenseHashSet upperBound{nullptr}; + }; + + /* + * When we encounter a generic over the course of a subtyping test, we need + * to tentatively map that generic onto a type on the other side. + */ + DenseHashMap mappedGenerics{nullptr}; + DenseHashMap mappedGenericPacks{nullptr}; + + DenseHashMap, SubtypingResult, TypePairHash> ephemeralCache{{}}; +}; + struct Subtyping { NotNull builtinTypes; @@ -55,29 +75,25 @@ struct Subtyping Variance variance = Variance::Covariant; - struct GenericBounds - { - DenseHashSet lowerBound{nullptr}; - DenseHashSet upperBound{nullptr}; - }; - - /* - * When we encounter a generic over the course of a subtyping test, we need - * to tentatively map that generic onto a type on the other side. - */ - DenseHashMap mappedGenerics{nullptr}; - DenseHashMap mappedGenericPacks{nullptr}; - using SeenSet = std::unordered_set, TypeIdPairHash>; SeenSet seenTypes; + Subtyping(NotNull builtinTypes, NotNull typeArena, NotNull normalizer, + NotNull iceReporter, NotNull scope); + Subtyping(const Subtyping&) = delete; Subtyping& operator=(const Subtyping&) = delete; Subtyping(Subtyping&&) = default; Subtyping& operator=(Subtyping&&) = default; + // Only used by unit tests to test that the cache works. + const DenseHashMap, SubtypingResult, TypePairHash>& peekCache() const + { + return resultCache; + } + // TODO cache // TODO cyclic types // TODO recursion limits @@ -86,58 +102,63 @@ struct Subtyping SubtypingResult isSubtype(TypePackId subTy, TypePackId superTy); private: - SubtypingResult isCovariantWith(TypeId subTy, TypeId superTy); - SubtypingResult isCovariantWith(TypePackId subTy, TypePackId superTy); + DenseHashMap, SubtypingResult, TypePairHash> resultCache{{}}; + + SubtypingResult cache(SubtypingEnvironment& env, SubtypingResult res, TypeId subTy, TypeId superTy); + + SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypePackId subTy, TypePackId superTy); template - SubtypingResult isContravariantWith(SubTy&& subTy, SuperTy&& superTy); + SubtypingResult isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy); template - SubtypingResult isInvariantWith(SubTy&& subTy, SuperTy&& superTy); + SubtypingResult isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy); template - SubtypingResult isCovariantWith(const TryPair& pair); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TryPair& pair); template - SubtypingResult isContravariantWith(const TryPair& pair); + SubtypingResult isContravariantWith(SubtypingEnvironment& env, const TryPair& pair); template - SubtypingResult isInvariantWith(const TryPair& pair); - - SubtypingResult isCovariantWith(TypeId subTy, const UnionType* superUnion); - SubtypingResult isCovariantWith(const UnionType* subUnion, TypeId superTy); - SubtypingResult isCovariantWith(TypeId subTy, const IntersectionType* superIntersection); - SubtypingResult isCovariantWith(const IntersectionType* subIntersection, TypeId superTy); - - SubtypingResult isCovariantWith(const NegationType* subNegation, TypeId superTy); - SubtypingResult isCovariantWith(const TypeId subTy, const NegationType* superNegation); - - SubtypingResult isCovariantWith(const PrimitiveType* subPrim, const PrimitiveType* superPrim); - SubtypingResult isCovariantWith(const SingletonType* subSingleton, const PrimitiveType* superPrim); - SubtypingResult isCovariantWith(const SingletonType* subSingleton, const SingletonType* superSingleton); - SubtypingResult isCovariantWith(const TableType* subTable, const TableType* superTable); - SubtypingResult isCovariantWith(const MetatableType* subMt, const MetatableType* superMt); - SubtypingResult isCovariantWith(const MetatableType* subMt, const TableType* superTable); - SubtypingResult isCovariantWith(const ClassType* subClass, const ClassType* superClass); - SubtypingResult isCovariantWith(const ClassType* subClass, const TableType* superTable); - SubtypingResult isCovariantWith(const FunctionType* subFunction, const FunctionType* superFunction); - SubtypingResult isCovariantWith(const PrimitiveType* subPrim, const TableType* superTable); - SubtypingResult isCovariantWith(const SingletonType* subSingleton, const TableType* superTable); - - SubtypingResult isCovariantWith(const TableIndexer& subIndexer, const TableIndexer& superIndexer); - - SubtypingResult isCovariantWith(const NormalizedType* subNorm, const NormalizedType* superNorm); - SubtypingResult isCovariantWith(const NormalizedClassType& subClass, const NormalizedClassType& superClass); - SubtypingResult isCovariantWith(const NormalizedClassType& subClass, const TypeIds& superTables); - SubtypingResult isCovariantWith(const NormalizedStringType& subString, const NormalizedStringType& superString); - SubtypingResult isCovariantWith(const NormalizedStringType& subString, const TypeIds& superTables); - SubtypingResult isCovariantWith(const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction); - SubtypingResult isCovariantWith(const TypeIds& subTypes, const TypeIds& superTypes); - - SubtypingResult isCovariantWith(const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic); - - bool bindGeneric(TypeId subTp, TypeId superTp); - bool bindGeneric(TypePackId subTp, TypePackId superTp); + SubtypingResult isInvariantWith(SubtypingEnvironment& env, const TryPair& pair); + + SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const UnionType* superUnion); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, TypeId superTy); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const IntersectionType* superIntersection); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const IntersectionType* subIntersection, TypeId superTy); + + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NegationType* subNegation, TypeId superTy); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const NegationType* superNegation); + + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const PrimitiveType* superPrim); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const PrimitiveType* superPrim); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const SingletonType* superSingleton); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const ClassType* superClass); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const TableType* superTable); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const FunctionType* subFunction, const FunctionType* superFunction); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const TableType* superTable); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable); + + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableIndexer& subIndexer, const TableIndexer& superIndexer); + + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const NormalizedClassType& superClass); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const TypeIds& superTables); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const NormalizedStringType& superString); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const TypeIds& superTables); + SubtypingResult isCovariantWith( + SubtypingEnvironment& env, const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TypeIds& subTypes, const TypeIds& superTypes); + + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic); + + bool bindGeneric(SubtypingEnvironment& env, TypeId subTp, TypeId superTp); + bool bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp); template TypeId makeAggregateType(const Container& container, TypeId orElse); diff --git a/Analysis/include/Luau/TypeFamily.h b/Analysis/include/Luau/TypeFamily.h index ad8fdbef3..ede104b95 100644 --- a/Analysis/include/Luau/TypeFamily.h +++ b/Analysis/include/Luau/TypeFamily.h @@ -93,7 +93,7 @@ struct TypeFamily std::string name; /// The reducer function for the type family. - std::function(std::vector, std::vector, NotNull)> reducer; + std::function(const std::vector&, const std::vector&, NotNull)> reducer; }; /// Represents a type function that may be applied to map a series of types and @@ -105,7 +105,7 @@ struct TypePackFamily std::string name; /// The reducer function for the type pack family. - std::function(std::vector, std::vector, NotNull)> reducer; + std::function(const std::vector&, const std::vector&, NotNull)> reducer; }; struct FamilyGraphReductionResult @@ -149,6 +149,8 @@ struct BuiltinTypeFamilies { BuiltinTypeFamilies(); + TypeFamily notFamily; + TypeFamily addFamily; TypeFamily subFamily; TypeFamily mulFamily; @@ -157,9 +159,15 @@ struct BuiltinTypeFamilies TypeFamily powFamily; TypeFamily modFamily; + TypeFamily concatFamily; + TypeFamily andFamily; TypeFamily orFamily; + TypeFamily ltFamily; + TypeFamily leFamily; + TypeFamily eqFamily; + void addToScope(NotNull arena, NotNull scope) const; }; diff --git a/Analysis/include/Luau/TypePairHash.h b/Analysis/include/Luau/TypePairHash.h new file mode 100644 index 000000000..029818ba1 --- /dev/null +++ b/Analysis/include/Luau/TypePairHash.h @@ -0,0 +1,34 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Type.h" + +#include + +namespace Luau +{ + +struct TypePairHash +{ + size_t hashOne(TypeId key) const + { + return (uintptr_t(key) >> 4) ^ (uintptr_t(key) >> 9); + } + + size_t hashOne(TypePackId key) const + { + return (uintptr_t(key) >> 4) ^ (uintptr_t(key) >> 9); + } + + size_t operator()(const std::pair& x) const + { + return hashOne(x.first) ^ (hashOne(x.second) << 1); + } + + size_t operator()(const std::pair& x) const + { + return hashOne(x.first) ^ (hashOne(x.second) << 1); + } +}; + +} // namespace Luau diff --git a/Analysis/include/Luau/Unifier2.h b/Analysis/include/Luau/Unifier2.h index 4478857e4..7b6fa608a 100644 --- a/Analysis/include/Luau/Unifier2.h +++ b/Analysis/include/Luau/Unifier2.h @@ -5,6 +5,7 @@ #include "Luau/DenseHash.h" #include "Luau/NotNull.h" #include "Type.h" +#include "TypePairHash.h" #include "TypeCheckLimits.h" #include "TypeChecker2.h" @@ -29,29 +30,6 @@ enum class OccursCheckResult Fail }; -struct TypePairHash -{ - size_t hashOne(Luau::TypeId key) const - { - return (uintptr_t(key) >> 4) ^ (uintptr_t(key) >> 9); - } - - size_t hashOne(Luau::TypePackId key) const - { - return (uintptr_t(key) >> 4) ^ (uintptr_t(key) >> 9); - } - - size_t operator()(const std::pair& x) const - { - return hashOne(x.first) ^ (hashOne(x.second) << 1); - } - - size_t operator()(const std::pair& x) const - { - return hashOne(x.first) ^ (hashOne(x.second) << 1); - } -}; - struct Unifier2 { NotNull arena; diff --git a/Analysis/src/AstJsonEncoder.cpp b/Analysis/src/AstJsonEncoder.cpp index 5c4f95045..920517c28 100644 --- a/Analysis/src/AstJsonEncoder.cpp +++ b/Analysis/src/AstJsonEncoder.cpp @@ -8,7 +8,8 @@ #include -LUAU_FASTFLAG(LuauFloorDivision) +LUAU_FASTFLAG(LuauFloorDivision); +LUAU_FASTFLAG(LuauClipExtraHasEndProps); namespace Luau { @@ -376,7 +377,8 @@ struct AstJsonEncoder : public AstVisitor PROP(body); PROP(functionDepth); PROP(debugname); - PROP(hasEnd); + if (!FFlag::LuauClipExtraHasEndProps) + write("hasEnd", node->DEPRECATED_hasEnd); }); } @@ -574,6 +576,11 @@ struct AstJsonEncoder : public AstVisitor void write(class AstStatBlock* node) { writeNode(node, "AstStatBlock", [&]() { + if (FFlag::LuauClipExtraHasEndProps) + { + writeRaw(",\"hasEnd\":"); + write(node->hasEnd); + } writeRaw(",\"body\":["); bool comma = false; for (AstStat* stat : node->body) @@ -597,7 +604,8 @@ struct AstJsonEncoder : public AstVisitor if (node->elsebody) PROP(elsebody); write("hasThen", node->thenLocation.has_value()); - PROP(hasEnd); + if (!FFlag::LuauClipExtraHasEndProps) + write("hasEnd", node->DEPRECATED_hasEnd); }); } @@ -607,7 +615,8 @@ struct AstJsonEncoder : public AstVisitor PROP(condition); PROP(body); PROP(hasDo); - PROP(hasEnd); + if (!FFlag::LuauClipExtraHasEndProps) + write("hasEnd", node->DEPRECATED_hasEnd); }); } @@ -616,7 +625,8 @@ struct AstJsonEncoder : public AstVisitor writeNode(node, "AstStatRepeat", [&]() { PROP(condition); PROP(body); - PROP(hasUntil); + if (!FFlag::LuauClipExtraHasEndProps) + write("hasUntil", node->DEPRECATED_hasUntil); }); } @@ -662,7 +672,8 @@ struct AstJsonEncoder : public AstVisitor PROP(step); PROP(body); PROP(hasDo); - PROP(hasEnd); + if (!FFlag::LuauClipExtraHasEndProps) + write("hasEnd", node->DEPRECATED_hasEnd); }); } @@ -674,7 +685,8 @@ struct AstJsonEncoder : public AstVisitor PROP(body); PROP(hasIn); PROP(hasDo); - PROP(hasEnd); + if (!FFlag::LuauClipExtraHasEndProps) + write("hasEnd", node->DEPRECATED_hasEnd); }); } diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index 3eba2e126..d73598c75 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -12,8 +12,9 @@ #include #include -LUAU_FASTFLAG(DebugLuauReadWriteProperties) -LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false) +LUAU_FASTFLAG(DebugLuauReadWriteProperties); +LUAU_FASTFLAG(LuauClipExtraHasEndProps); +LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false); LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false); static const std::unordered_set kStatementStartingKeywords = { @@ -1055,22 +1056,56 @@ static AutocompleteEntryMap autocompleteStatement( for (const auto& kw : kStatementStartingKeywords) result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword}); - for (auto it = ancestry.rbegin(); it != ancestry.rend(); ++it) - { - if (AstStatForIn* statForIn = (*it)->as(); statForIn && !statForIn->hasEnd) - result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); - else if (AstStatFor* statFor = (*it)->as(); statFor && !statFor->hasEnd) - result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); - else if (AstStatIf* statIf = (*it)->as(); statIf && !statIf->hasEnd) - result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); - else if (AstStatWhile* statWhile = (*it)->as(); statWhile && !statWhile->hasEnd) - result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); - else if (AstExprFunction* exprFunction = (*it)->as(); exprFunction && !exprFunction->hasEnd) - result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); - if (FFlag::LuauAutocompleteDoEnd) + if (FFlag::LuauClipExtraHasEndProps) + { + for (auto it = ancestry.rbegin(); it != ancestry.rend(); ++it) + { + if (AstStatForIn* statForIn = (*it)->as(); statForIn && !statForIn->body->hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + else if (AstStatFor* statFor = (*it)->as(); statFor && !statFor->body->hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + else if (AstStatIf* statIf = (*it)->as()) + { + bool hasEnd = statIf->thenbody->hasEnd; + if (statIf->elsebody) + { + if (AstStatBlock* elseBlock = statIf->elsebody->as()) + hasEnd = elseBlock->hasEnd; + } + + if (!hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } + else if (AstStatWhile* statWhile = (*it)->as(); statWhile && !statWhile->body->hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + else if (AstExprFunction* exprFunction = (*it)->as(); exprFunction && !exprFunction->body->hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + if (FFlag::LuauAutocompleteDoEnd) + { + if (AstStatBlock* exprBlock = (*it)->as(); exprBlock && !exprBlock->hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } + } + } + else + { + for (auto it = ancestry.rbegin(); it != ancestry.rend(); ++it) { - if (AstStatBlock* exprBlock = (*it)->as(); exprBlock && !exprBlock->hasEnd) + if (AstStatForIn* statForIn = (*it)->as(); statForIn && !statForIn->DEPRECATED_hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + else if (AstStatFor* statFor = (*it)->as(); statFor && !statFor->DEPRECATED_hasEnd) result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + else if (AstStatIf* statIf = (*it)->as(); statIf && !statIf->DEPRECATED_hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + else if (AstStatWhile* statWhile = (*it)->as(); statWhile && !statWhile->DEPRECATED_hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + else if (AstExprFunction* exprFunction = (*it)->as(); exprFunction && !exprFunction->DEPRECATED_hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + if (FFlag::LuauAutocompleteDoEnd) + { + if (AstStatBlock* exprBlock = (*it)->as(); exprBlock && !exprBlock->hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } } } @@ -1086,8 +1121,16 @@ static AutocompleteEntryMap autocompleteStatement( } } - if (AstStatRepeat* statRepeat = parent->as(); statRepeat && !statRepeat->hasUntil) - result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + if (FFlag::LuauClipExtraHasEndProps) + { + if (AstStatRepeat* statRepeat = parent->as(); statRepeat && !statRepeat->body->hasEnd) + result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } + else + { + if (AstStatRepeat* statRepeat = parent->as(); statRepeat && !statRepeat->DEPRECATED_hasUntil) + result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } } if (ancestry.size() >= 4) @@ -1101,8 +1144,16 @@ static AutocompleteEntryMap autocompleteStatement( } } - if (AstStatRepeat* statRepeat = extractStat(ancestry); statRepeat && !statRepeat->hasUntil) - result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + if (FFlag::LuauClipExtraHasEndProps) + { + if (AstStatRepeat* statRepeat = extractStat(ancestry); statRepeat && !statRepeat->body->hasEnd) + result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } + else + { + if (AstStatRepeat* statRepeat = extractStat(ancestry); statRepeat && !statRepeat->DEPRECATED_hasUntil) + result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } return result; } diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index 5c96abb79..5eb2ce49f 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -175,6 +175,8 @@ void ConstraintGraphBuilder::visitModuleRoot(AstStatBlock* block) visitBlockWithoutChildScope(scope, block); + fillInInferredBindings(scope, block); + if (logger) logger->captureGenerationModule(module); } @@ -586,90 +588,41 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* local) { - std::vector varTypes; + std::vector> varTypes; varTypes.reserve(local->vars.size); + std::vector assignees; + assignees.reserve(local->vars.size); + // Used to name the first value type, even if it's not placed in varTypes, // for the purpose of synthetic name attribution. std::optional firstValueType; for (AstLocal* local : local->vars) { - TypeId ty = nullptr; - - if (local->annotation) - ty = resolveType(scope, local->annotation, /* inTypeArguments */ false); - - varTypes.push_back(ty); - } - - for (size_t i = 0; i < local->values.size; ++i) - { - AstExpr* value = local->values.data[i]; - const bool hasAnnotation = i < local->vars.size && nullptr != local->vars.data[i]->annotation; + TypeId assignee = arena->addType(BlockedType{}); + assignees.push_back(assignee); - if (value->is()) - { - // HACK: we leave nil-initialized things floating under the - // assumption that they will later be populated. - // - // See the test TypeInfer/infer_locals_with_nil_value. Better flow - // awareness should make this obsolete. + if (!firstValueType) + firstValueType = assignee; - if (i < varTypes.size() && !varTypes[i]) - varTypes[i] = freshType(scope); - } - // Only function calls and vararg expressions can produce packs. All - // other expressions produce exactly one value. - else if (i != local->values.size - 1 || (!value->is() && !value->is())) + if (local->annotation) { - std::optional expectedType; - if (hasAnnotation) - expectedType = varTypes.at(i); + TypeId annotationTy = resolveType(scope, local->annotation, /* inTypeArguments */ false); + varTypes.push_back(annotationTy); - TypeId exprType = check(scope, value, expectedType, /*forceSingleton*/ false, /*generalize*/ true).ty; - if (i < varTypes.size()) - { - if (varTypes[i]) - addConstraint(scope, local->location, SubtypeConstraint{exprType, varTypes[i]}); - else - varTypes[i] = exprType; - } - - if (i == 0) - firstValueType = exprType; + addConstraint(scope, local->location, SubtypeConstraint{assignee, annotationTy}); } else - { - std::vector> expectedTypes; - if (hasAnnotation) - expectedTypes.insert(begin(expectedTypes), begin(varTypes) + i, end(varTypes)); - - TypePackId exprPack = checkPack(scope, value, expectedTypes, /*generalize*/ true).tp; + varTypes.push_back(std::nullopt); - if (i < local->vars.size) - { - TypePack packTypes = extendTypePack(*arena, builtinTypes, exprPack, varTypes.size() - i); - - // fill out missing values in varTypes with values from exprPack - for (size_t j = i; j < varTypes.size(); ++j) - { - if (!varTypes[j]) - { - if (j - i < packTypes.head.size()) - varTypes[j] = packTypes.head[j - i]; - else - varTypes[j] = arena->addType(BlockedType{}); - } - } - - std::vector tailValues{varTypes.begin() + i, varTypes.end()}; - TypePackId tailPack = arena->addTypePack(std::move(tailValues)); - addConstraint(scope, local->location, UnpackConstraint{tailPack, exprPack}); - } - } + BreadcrumbId bc = dfg->getBreadcrumb(local); + scope->lvalueTypes[bc->def] = assignee; } + TypePackId resultPack = checkPack(scope, local->values, varTypes).tp; + addConstraint(scope, local->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); + if (local->vars.size == 1 && local->values.size == 1 && firstValueType && scope.get() == rootScope) { AstLocal* var = local->vars.data[0]; @@ -691,16 +644,16 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l AstLocal* l = local->vars.data[i]; Location location = l->location; - if (!varTypes[i]) - varTypes[i] = freshType(scope); - - scope->bindings[l] = Binding{varTypes[i], location}; - - // HACK: In the greedy solver, we say the type state of a variable is the type annotation itself, but - // the actual type state is the corresponding initializer expression (if it exists) or nil otherwise. + std::optional annotation = varTypes[i]; BreadcrumbId bc = dfg->getBreadcrumb(l); - scope->lvalueTypes[bc->def] = varTypes[i]; - scope->rvalueRefinements[bc->def] = varTypes[i]; + + if (annotation) + scope->bindings[l] = Binding{*annotation, location}; + else + { + scope->bindings[l] = Binding{builtinTypes->neverType, location}; + inferredBindings.emplace_back(scope.get(), l, bc); + } } if (local->values.size > 0) @@ -712,30 +665,32 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l if (!call) continue; - if (auto maybeRequire = matchRequire(*call)) - { - AstExpr* require = *maybeRequire; + auto maybeRequire = matchRequire(*call); + if (!maybeRequire) + continue; - if (auto moduleInfo = moduleResolver->resolveModuleInfo(module->name, *require)) - { - const Name name{local->vars.data[i]->name.value}; + AstExpr* require = *maybeRequire; - if (ModulePtr module = moduleResolver->getModule(moduleInfo->name)) - { - scope->importedTypeBindings[name] = module->exportedTypeBindings; - scope->importedModules[name] = moduleInfo->name; - - // Imported types of requires that transitively refer to current module have to be replaced with 'any' - for (const auto& [location, path] : requireCycles) - { - if (!path.empty() && path.front() == moduleInfo->name) - { - for (auto& [name, tf] : scope->importedTypeBindings[name]) - tf = TypeFun{{}, {}, builtinTypes->anyType}; - } - } - } - } + auto moduleInfo = moduleResolver->resolveModuleInfo(module->name, *require); + if (!moduleInfo) + continue; + + ModulePtr module = moduleResolver->getModule(moduleInfo->name); + if (!module) + continue; + + const Name name{local->vars.data[i]->name.value}; + scope->importedTypeBindings[name] = module->exportedTypeBindings; + scope->importedModules[name] = moduleInfo->name; + + // Imported types of requires that transitively refer to current module have to be replaced with 'any' + for (const auto& [location, path] : requireCycles) + { + if (path.empty() || path.front() != moduleInfo->name) + continue; + + for (auto& [name, tf] : scope->importedTypeBindings[name]) + tf = TypeFun{{}, {}, builtinTypes->anyType}; } } } @@ -781,6 +736,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f std::vector variableTypes; variableTypes.reserve(forIn->vars.size); + for (AstLocal* var : forIn->vars) { TypeId ty = nullptr; @@ -790,18 +746,18 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f ty = freshType(loopScope); loopScope->bindings[var] = Binding{ty, var->location}; - variableTypes.push_back(ty); + + TypeId assignee = arena->addType(BlockedType{}); + variableTypes.push_back(assignee); BreadcrumbId bc = dfg->getBreadcrumb(var); - loopScope->lvalueTypes[bc->def] = ty; - loopScope->rvalueRefinements[bc->def] = ty; + loopScope->lvalueTypes[bc->def] = assignee; } - // It is always ok to provide too few variables, so we give this pack a free tail. - TypePackId variablePack = arena->addTypePack(std::move(variableTypes), freshTypePack(loopScope)); - + TypePackId variablePack = arena->addTypePack(std::move(variableTypes)); addConstraint( loopScope, getLocation(forIn->values), IterableConstraint{iterator, variablePack, forIn->values.data[0], &module->astForInNextTypes}); + visit(loopScope, forIn->body); return ControlFlow::None; @@ -1033,24 +989,30 @@ static void bindFreeType(TypeId a, TypeId b) ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign) { - std::vector varTypes = checkLValues(scope, assign->vars); - std::vector> expectedTypes; - expectedTypes.reserve(varTypes.size()); + expectedTypes.reserve(assign->vars.size); - for (TypeId ty : varTypes) + std::vector assignees; + assignees.reserve(assign->vars.size); + + for (AstExpr* lvalue : assign->vars) { - ty = follow(ty); - if (get(ty)) + TypeId upperBound = follow(checkLValue(scope, lvalue)); + if (get(upperBound)) expectedTypes.push_back(std::nullopt); else - expectedTypes.push_back(ty); - } + expectedTypes.push_back(upperBound); + + TypeId assignee = arena->addType(BlockedType{}); + assignees.push_back(assignee); + addConstraint(scope, lvalue->location, SubtypeConstraint{assignee, upperBound}); - TypePackId exprPack = checkPack(scope, assign->values, expectedTypes).tp; - TypePackId varPack = arena->addTypePack({varTypes}); + if (NullableBreadcrumbId bc = dfg->getBreadcrumb(lvalue)) + scope->lvalueTypes[bc->def] = assignee; + } - addConstraint(scope, assign->location, PackSubtypeConstraint{exprPack, varPack}); + TypePackId resultPack = checkPack(scope, assign->values, expectedTypes).tp; + addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); return ControlFlow::None; } @@ -1729,7 +1691,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* loc if (auto ty = scope->lookup(bc->def)) return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)}; else - ice->ice("AstExprLocal came before its declaration?"); + ice->ice("CGB: AstExprLocal came before its declaration?"); } Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* global) @@ -1837,13 +1799,26 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* unary) { auto [operandType, refinement] = check(scope, unary->expr); - TypeId resultType = arena->addType(BlockedType{}); - addConstraint(scope, unary->location, UnaryConstraint{unary->op, operandType, resultType}); - if (unary->op == AstExprUnary::Not) + switch (unary->op) + { + case AstExprUnary::Op::Not: + { + TypeId resultType = arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.notFamily}, + {operandType}, + {}, + }); + addConstraint(scope, unary->location, ReduceConstraint{resultType}); return Inference{resultType, refinementArena.negation(refinement)}; - else + } + default: + { + TypeId resultType = arena->addType(BlockedType{}); + addConstraint(scope, unary->location, UnaryConstraint{unary->op, operandType, resultType}); return Inference{resultType}; + } + } } Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) @@ -1922,6 +1897,16 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi addConstraint(scope, binary->location, ReduceConstraint{resultType}); return Inference{resultType, std::move(refinement)}; } + case AstExprBinary::Op::Concat: + { + TypeId resultType = arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.concatFamily}, + {leftType, rightType}, + {}, + }); + addConstraint(scope, binary->location, ReduceConstraint{resultType}); + return Inference{resultType, std::move(refinement)}; + } case AstExprBinary::Op::And: { TypeId resultType = arena->addType(TypeFamilyInstanceType{ @@ -1942,6 +1927,57 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi addConstraint(scope, binary->location, ReduceConstraint{resultType}); return Inference{resultType, std::move(refinement)}; } + case AstExprBinary::Op::CompareLt: + { + TypeId resultType = arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.ltFamily}, + {leftType, rightType}, + {}, + }); + addConstraint(scope, binary->location, ReduceConstraint{resultType}); + return Inference{resultType, std::move(refinement)}; + } + case AstExprBinary::Op::CompareGe: + { + TypeId resultType = arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.ltFamily}, + {rightType, leftType}, // lua decided that `__ge(a, b)` is instead just `__lt(b, a)` + {}, + }); + addConstraint(scope, binary->location, ReduceConstraint{resultType}); + return Inference{resultType, std::move(refinement)}; + } + case AstExprBinary::Op::CompareLe: + { + TypeId resultType = arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.leFamily}, + {leftType, rightType}, + {}, + }); + addConstraint(scope, binary->location, ReduceConstraint{resultType}); + return Inference{resultType, std::move(refinement)}; + } + case AstExprBinary::Op::CompareGt: + { + TypeId resultType = arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.leFamily}, + {rightType, leftType}, // lua decided that `__gt(a, b)` is instead just `__le(b, a)` + {}, + }); + addConstraint(scope, binary->location, ReduceConstraint{resultType}); + return Inference{resultType, std::move(refinement)}; + } + case AstExprBinary::Op::CompareEq: + case AstExprBinary::Op::CompareNe: + { + TypeId resultType = arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.eqFamily}, + {leftType, rightType}, + {}, + }); + addConstraint(scope, binary->location, ReduceConstraint{resultType}); + return Inference{resultType, std::move(refinement)}; + } default: { TypeId resultType = arena->addType(BlockedType{}); @@ -2099,17 +2135,6 @@ std::tuple ConstraintGraphBuilder::checkBinary( } } -std::vector ConstraintGraphBuilder::checkLValues(const ScopePtr& scope, AstArray exprs) -{ - std::vector types; - types.reserve(exprs.size); - - for (AstExpr* expr : exprs) - types.push_back(checkLValue(scope, expr)); - - return types; -} - TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr) { if (auto local = expr->as()) @@ -2201,6 +2226,7 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex return check(scope, expr).ty; Symbol sym; + NullableBreadcrumbId bc = nullptr; std::vector segments; std::vector exprs; @@ -2210,11 +2236,13 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex if (auto global = e->as()) { sym = global->name; + bc = dfg->getBreadcrumb(global); break; } else if (auto local = e->as()) { sym = local->local; + bc = dfg->getBreadcrumb(local); break; } else if (auto indexName = e->as()) @@ -2251,7 +2279,16 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex if (!lookupResult) return check(scope, expr).ty; const auto [subjectBinding, symbolScope] = std::move(*lookupResult); - TypeId subjectType = subjectBinding->typeId; + + LUAU_ASSERT(bc); + std::optional subjectTy = scope->lookup(bc->def); + + /* If we have a breadcrumb but no type, it can only mean that we're setting + * a property of some builtin table. This isn't legal, but we still want to + * wire up the constraints properly so that we can report why it is not + * legal. + */ + TypeId subjectType = subjectTy.value_or(subjectBinding->typeId); TypeId propTy = freshType(scope); @@ -2502,7 +2539,6 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS argTy = resolveType(signatureScope, local->annotation, /* inTypeArguments */ false, /* replaceErrorWithFresh*/ true); else { - if (i < expectedArgPack.head.size()) argTy = expectedArgPack.head[i]; else @@ -2511,7 +2547,15 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS argTypes.push_back(argTy); argNames.emplace_back(FunctionArgument{local->name.value, local->location}); - signatureScope->bindings[local] = Binding{argTy, local->location}; + + if (local->annotation) + signatureScope->bindings[local] = Binding{argTy, local->location}; + else + { + BreadcrumbId bc = dfg->getBreadcrumb(local); + signatureScope->bindings[local] = Binding{builtinTypes->neverType, local->location}; + inferredBindings.emplace_back(signatureScope.get(), local, bc); + } BreadcrumbId bc = dfg->getBreadcrumb(local); signatureScope->lvalueTypes[bc->def] = argTy; @@ -3018,6 +3062,37 @@ void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, program->visit(&gp); } +void ConstraintGraphBuilder::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) +{ + std::deque queue; + + for (const auto& [scope, symbol, breadcrumb] : inferredBindings) + { + LUAU_ASSERT(queue.empty()); + + queue.push_back(breadcrumb); + + TypeId ty = builtinTypes->neverType; + + while (!queue.empty()) + { + const BreadcrumbId bc = queue.front(); + queue.pop_front(); + + TypeId* lvalueType = scope->lvalueTypes.find(bc->def); + if (!lvalueType) + continue; + + ty = simplifyUnion(builtinTypes, arena, ty, *lvalueType).result; + + for (BreadcrumbId child : bc->children) + queue.push_back(child); + } + + scope->bindings[symbol].typeId = ty; + } +} + std::vector> ConstraintGraphBuilder::getExpectedCallTypesForFunctionOverloads(const TypeId fnType) { std::vector funTys; diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 320ff9176..bdcd1663f 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -1785,13 +1785,13 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNullty.emplace(builtinTypes->errorRecoveryType()); + asMutable(*destIter)->ty.emplace(builtinTypes->nilType); unblock(*destIter, constraint->location); } @@ -2013,57 +2013,26 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl if (get(iteratorTy)) return block_(iteratorTy); - auto anyify = [&](auto ty) { - Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, builtinTypes->anyType, builtinTypes->anyTypePack}; - std::optional anyified = anyify.substitute(ty); - if (!anyified) - reportError(CodeTooComplex{}, constraint->location); - else - unify(constraint->scope, constraint->location, *anyified, ty); - }; - - auto unknownify = [&](auto ty) { - Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, builtinTypes->unknownType, builtinTypes->anyTypePack}; - std::optional anyified = anyify.substitute(ty); - if (!anyified) - reportError(CodeTooComplex{}, constraint->location); - else - unify(constraint->scope, constraint->location, *anyified, ty); - }; - - auto errorify = [&](auto ty) { - Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()}; - std::optional errorified = anyify.substitute(ty); - if (!errorified) - reportError(CodeTooComplex{}, constraint->location); - else - unify(constraint->scope, constraint->location, *errorified, ty); - }; - - auto neverify = [&](auto ty) { - Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, builtinTypes->neverType, builtinTypes->neverTypePack}; - std::optional neverified = anyify.substitute(ty); - if (!neverified) - reportError(CodeTooComplex{}, constraint->location); - else - unify(constraint->scope, constraint->location, *neverified, ty); + auto unpack = [&](TypeId ty) { + TypePackId variadic = arena->addTypePack(VariadicTypePack{ty}); + pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, variadic}); }; if (get(iteratorTy)) { - anyify(c.variables); + unpack(builtinTypes->anyType); return true; } if (get(iteratorTy)) { - errorify(c.variables); + unpack(builtinTypes->errorType); return true; } if (get(iteratorTy)) { - neverify(c.variables); + unpack(builtinTypes->neverType); return true; } @@ -2088,7 +2057,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl unify(constraint->scope, constraint->location, c.variables, expectedVariablePack); } else - errorify(c.variables); + unpack(builtinTypes->errorType); } else if (std::optional iterFn = findMetatableEntry(builtinTypes, errors, iteratorTy, "__iter", Location{})) { @@ -2128,7 +2097,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl const TypeId expectedNextTy = arena->addType(FunctionType{nextArgPack, nextRetPack}); unify(constraint->scope, constraint->location, *instantiatedNextFn, expectedNextTy); - pushConstraint(constraint->scope, constraint->location, PackSubtypeConstraint{c.variables, nextRetPack}); + pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, nextRetPack}); } else { @@ -2154,9 +2123,9 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl LUAU_ASSERT(false); } else if (auto primitiveTy = get(iteratorTy); primitiveTy && primitiveTy->type == PrimitiveType::Type::Table) - unknownify(c.variables); + unpack(builtinTypes->unknownType); else - errorify(c.variables); + unpack(builtinTypes->errorType); return true; } diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index e73c7e8c9..523f8df60 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -1,10 +1,13 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/DataFlowGraph.h" +#include "Luau/Ast.h" #include "Luau/Breadcrumb.h" #include "Luau/Error.h" #include "Luau/Refinement.h" +#include + LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) @@ -278,11 +281,12 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a) { - for (AstExpr* r : a->values) - visitExpr(scope, r); - - for (AstExpr* l : a->vars) - visitLValue(scope, l); + for (size_t i = 0; i < std::max(a->vars.size, a->values.size); ++i) + { + BreadcrumbId bc = i < a->values.size ? visitExpr(scope, a->values.data[i]) : breadcrumbs->add(nullptr, defs->freshCell()); + if (i < a->vars.size) + visitLValue(scope, a->vars.data[i], bc); + } } void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) @@ -291,12 +295,11 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) // but the `c->var` only has one pointer address, so we need to come up with a way to store both. // For now, it's not important because we don't have type states, but it is going to be important, e.g. // - // local a = 5 -- a[1] - // a += 5 -- a[2] = a[1] + 5 + // local a = 5 -- a-1 + // a += 5 -- a-2 = a-1 + 5 // // We can't just visit `c->var` as a rvalue and then separately traverse `c->var` as an lvalue, since that's O(n^2). - visitLValue(scope, c->var); - visitExpr(scope, c->value); + visitLValue(scope, c->var, visitExpr(scope, c->value)); } void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) @@ -311,17 +314,14 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) // // which is evidence that references to variables must be a phi node of all possible definitions, // but for bug compatibility, we'll assume the same thing here. - visitLValue(scope, f->name); - visitExpr(scope, f->func); + visitLValue(scope, f->name, visitExpr(scope, f->func)); } void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l) { - BreadcrumbId bc = breadcrumbs->add(nullptr, defs->freshCell()); + BreadcrumbId bc = visitExpr(scope, l->func); graph.localBreadcrumbs[l->name] = bc; scope->bindings[l->name] = bc; - - visitExpr(scope, l->func); } void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t) @@ -423,7 +423,7 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) { NullableBreadcrumbId breadcrumb = scope->lookup(l->local); if (!breadcrumb) - handle->ice("AstExprLocal came before its declaration?"); + handle->ice("DFG: AstExprLocal came before its declaration?"); graph.astBreadcrumbs[l] = breadcrumb; return NotNull{breadcrumb}; @@ -591,81 +591,69 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprError* erro return breadcrumbs->add(nullptr, defs->freshCell()); } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, BreadcrumbId bc) { if (auto l = e->as()) - return visitLValue(scope, l); + return visitLValue(scope, l, bc); else if (auto g = e->as()) - return visitLValue(scope, g); + return visitLValue(scope, g, bc); else if (auto i = e->as()) - return visitLValue(scope, i); + return visitLValue(scope, i, bc); else if (auto i = e->as()) - return visitLValue(scope, i); + return visitLValue(scope, i, bc); else if (auto error = e->as()) - { - visitExpr(scope, error); // TODO: is this right? - return; - } + return visitLValue(scope, error, bc); else handle->ice("Unknown AstExpr in DataFlowGraphBuilder::visitLValue"); } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, BreadcrumbId bc) { - // Bug compatibility: we don't support type states yet, so we need to do this. - NullableBreadcrumbId bc = scope->lookup(l->local); - LUAU_ASSERT(bc); - - graph.astBreadcrumbs[l] = bc; - scope->bindings[l->local] = bc; + // In order to avoid alias tracking, we need to clip the reference to the parent breadcrumb + // as well as the def that was about to be assigned onto this lvalue. However, we want to + // copy the metadata so that refinements can be consistent. + BreadcrumbId updated = breadcrumbs->add(scope->lookup(l->local), defs->freshCell(), bc->metadata); + graph.astBreadcrumbs[l] = updated; + scope->bindings[l->local] = updated; } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, BreadcrumbId bc) { - // Bug compatibility: we don't support type states yet, so we need to do this. - NullableBreadcrumbId bc = scope->lookup(g->name); - if (!bc) - bc = breadcrumbs->add(nullptr, defs->freshCell()); - - graph.astBreadcrumbs[g] = bc; - scope->bindings[g->name] = bc; + // In order to avoid alias tracking, we need to clip the reference to the parent breadcrumb + // as well as the def that was about to be assigned onto this lvalue. However, we want to + // copy the metadata so that refinements can be consistent. + BreadcrumbId updated = breadcrumbs->add(scope->lookup(g->name), defs->freshCell(), bc->metadata); + graph.astBreadcrumbs[g] = updated; + scope->bindings[g->name] = updated; } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i, BreadcrumbId bc) { - // Bug compatibility: we don't support type states yet, so we need to do this. BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr); - std::string key = i->index.value; - NullableBreadcrumbId propBreadcrumb = scope->lookup(parentBreadcrumb->def, key); - if (!propBreadcrumb) - { - propBreadcrumb = breadcrumbs->emplace(parentBreadcrumb, defs->freshCell(), key); - moduleScope->props[parentBreadcrumb->def][key] = propBreadcrumb; - } - - graph.astBreadcrumbs[i] = propBreadcrumb; + BreadcrumbId updated = breadcrumbs->add(scope->props[parentBreadcrumb->def][i->index.value], defs->freshCell(), bc->metadata); + graph.astBreadcrumbs[i] = updated; + scope->props[parentBreadcrumb->def][i->index.value] = updated; } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, BreadcrumbId bc) { BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr); visitExpr(scope, i->index); if (auto string = i->index->as()) { - std::string key{string->value.data, string->value.size}; - NullableBreadcrumbId propBreadcrumb = scope->lookup(parentBreadcrumb->def, key); - if (!propBreadcrumb) - { - propBreadcrumb = breadcrumbs->add(parentBreadcrumb, parentBreadcrumb->def); - moduleScope->props[parentBreadcrumb->def][key] = propBreadcrumb; - } - - graph.astBreadcrumbs[i] = propBreadcrumb; + BreadcrumbId updated = breadcrumbs->add(scope->props[parentBreadcrumb->def][string->value.data], defs->freshCell(), bc->metadata); + graph.astBreadcrumbs[i] = updated; + scope->props[parentBreadcrumb->def][string->value.data] = updated; } } +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprError* error, BreadcrumbId bc) +{ + visitExpr(scope, error); +} + void DataFlowGraphBuilder::visitType(DfgScope* scope, AstType* t) { if (auto r = t->as()) diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index 4505f6274..db6a240a3 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -112,7 +112,7 @@ struct ErrorConverter result += "\ncaused by:\n "; if (!tm.reason.empty()) - result += tm.reason + " \n"; + result += tm.reason + "\n"; result += Luau::toString(*tm.error, TypeErrorToStringOptions{fileResolver}); } @@ -521,6 +521,13 @@ struct ErrorConverter " depends on generic function parameters but does not appear in the function signature; this construct cannot be type-checked at this " "time"; } + + std::string operator()(const CheckedFunctionCallError& e) const + { + // TODO: What happens if checkedFunctionName cannot be found?? + return "Function '" + e.checkedFunctionName + "' expects '" + toString(e.expected) + "' at argument #" + std::to_string(e.argumentIndex) + + ", but got '" + Luau::toString(e.passed) + "'"; + } }; struct InvalidNameChecker @@ -843,6 +850,12 @@ bool PackWhereClauseNeeded::operator==(const PackWhereClauseNeeded& rhs) const return tp == rhs.tp; } +bool CheckedFunctionCallError::operator==(const CheckedFunctionCallError& rhs) const +{ + return *expected == *rhs.expected && *passed == *rhs.passed && checkedFunctionName == rhs.checkedFunctionName && + argumentIndex == rhs.argumentIndex; +} + std::string toString(const TypeError& error) { return toString(error, TypeErrorToStringOptions{}); @@ -1009,6 +1022,11 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState) e.ty = clone(e.ty); else if constexpr (std::is_same_v) e.tp = clone(e.tp); + else if constexpr (std::is_same_v) + { + e.expected = clone(e.expected); + e.passed = clone(e.passed); + } else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 4872843ea..6cbc19fa0 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -15,6 +15,7 @@ #include "Luau/StringUtils.h" #include "Luau/TimeTrace.h" #include "Luau/TypeChecker2.h" +#include "Luau/NonStrictTypeChecker.h" #include "Luau/TypeInfer.h" #include "Luau/Variant.h" @@ -37,7 +38,6 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false) LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false) LUAU_FASTFLAGVARIABLE(LuauTypecheckLimitControls, false) LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false) -LUAU_FASTFLAGVARIABLE(DebugLuauNewNonStrictMode, false) namespace Luau { @@ -1212,17 +1212,17 @@ const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) cons return const_cast(this)->getSourceModule(moduleName); } -ModulePtr check(const SourceModule& sourceModule, const std::vector& requireCycles, NotNull builtinTypes, +ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector& requireCycles, NotNull builtinTypes, NotNull iceHandler, NotNull moduleResolver, NotNull fileResolver, const ScopePtr& parentScope, std::function prepareModuleScope, FrontendOptions options, TypeCheckLimits limits) { const bool recordJsonLog = FFlag::DebugLuauLogSolverToJson; - return check(sourceModule, requireCycles, builtinTypes, iceHandler, moduleResolver, fileResolver, parentScope, std::move(prepareModuleScope), - options, limits, recordJsonLog); + return check(sourceModule, mode, requireCycles, builtinTypes, iceHandler, moduleResolver, fileResolver, parentScope, + std::move(prepareModuleScope), options, limits, recordJsonLog); } -ModulePtr check(const SourceModule& sourceModule, const std::vector& requireCycles, NotNull builtinTypes, +ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector& requireCycles, NotNull builtinTypes, NotNull iceHandler, NotNull moduleResolver, NotNull fileResolver, const ScopePtr& parentScope, std::function prepareModuleScope, FrontendOptions options, TypeCheckLimits limits, bool recordJsonLog) @@ -1303,7 +1303,10 @@ ModulePtr check(const SourceModule& sourceModule, const std::vector requireCycles, std::optional environmentScope, bool forAutocomplete, bool recordJsonLog, TypeCheckLimits typeCheckLimits) { - if (FFlag::DebugLuauDeferredConstraintResolution && mode == Mode::Strict) + if (FFlag::DebugLuauDeferredConstraintResolution) { auto prepareModuleScopeWrap = [this, forAutocomplete](const ModuleName& name, const ScopePtr& scope) { if (prepareModuleScope) @@ -1341,7 +1344,7 @@ ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vect try { - return Luau::check(sourceModule, requireCycles, builtinTypes, NotNull{&iceHandler}, + return Luau::check(sourceModule, mode, requireCycles, builtinTypes, NotNull{&iceHandler}, NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}, NotNull{fileResolver}, environmentScope ? *environmentScope : globals.globalScope, prepareModuleScopeWrap, options, typeCheckLimits, recordJsonLog); } diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 54a7dbff4..75e1f7c31 100644 --- a/Analysis/src/IostreamHelpers.cpp +++ b/Analysis/src/IostreamHelpers.cpp @@ -200,6 +200,9 @@ static void errorToString(std::ostream& stream, const T& err) stream << "WhereClauseNeeded { " << toString(err.ty) << " }"; else if constexpr (std::is_same_v) stream << "PackWhereClauseNeeded { " << toString(err.tp) << " }"; + else if constexpr (std::is_same_v) + stream << "CheckedFunctionCallError { expected = '" << toString(err.expected) << "', passed = '" << toString(err.passed) + << "', checkedFunctionName = " << err.checkedFunctionName << ", argumentIndex = " << std::to_string(err.argumentIndex) << " }"; else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index d4a16a75b..e957eee78 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -14,6 +14,9 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) +LUAU_FASTFLAGVARIABLE(LuauLintDeprecatedFenv, false) +LUAU_FASTFLAGVARIABLE(LuauLintTableIndexer, false) + namespace Luau { @@ -2085,6 +2088,32 @@ class LintDeprecatedApi : AstVisitor return true; } + bool visit(AstExprCall* node) override + { + // getfenv/setfenv are deprecated, however they are still used in some test frameworks and don't have a great general replacement + // for now we warn about the deprecation only when they are used with a numeric first argument; this produces fewer warnings and makes use + // of getfenv/setfenv a little more localized + if (FFlag::LuauLintDeprecatedFenv && !node->self && node->args.size >= 1) + { + if (AstExprGlobal* fenv = node->func->as(); fenv && (fenv->name == "getfenv" || fenv->name == "setfenv")) + { + AstExpr* level = node->args.data[0]; + std::optional ty = context->getType(level); + + if ((ty && isNumber(*ty)) || level->is()) + { + // some common uses of getfenv(n) can be replaced by debug.info if the goal is to get the caller's identity + const char* suggestion = (fenv->name == "getfenv") ? "; consider using 'debug.info' instead" : ""; + + emitWarning( + *context, LintWarning::Code_DeprecatedApi, node->location, "Function '%s' is deprecated%s", fenv->name.value, suggestion); + } + } + } + + return true; + } + void check(AstExprIndexName* node, TypeId ty) { if (const ClassType* cty = get(ty)) @@ -2154,16 +2183,50 @@ class LintTableOperations : AstVisitor { } + bool visit(AstExprUnary* node) override + { + if (FFlag::LuauLintTableIndexer && node->op == AstExprUnary::Len) + checkIndexer(node, node->expr, "#"); + + return true; + } + bool visit(AstExprCall* node) override { - AstExprIndexName* func = node->func->as(); - if (!func) - return true; + if (AstExprGlobal* func = node->func->as()) + { + if (FFlag::LuauLintTableIndexer && func->name == "ipairs" && node->args.size == 1) + checkIndexer(node, node->args.data[0], "ipairs"); + } + else if (AstExprIndexName* func = node->func->as()) + { + if (AstExprGlobal* tablib = func->expr->as(); tablib && tablib->name == "table") + checkTableCall(node, func); + } - AstExprGlobal* tablib = func->expr->as(); - if (!tablib || tablib->name != "table") - return true; + return true; + } + void checkIndexer(AstExpr* node, AstExpr* expr, const char* op) + { + LUAU_ASSERT(FFlag::LuauLintTableIndexer); + + std::optional ty = context->getType(expr); + if (!ty) + return; + + const TableType* tty = get(follow(*ty)); + if (!tty) + return; + + if (!tty->indexer && !tty->props.empty() && tty->state != TableState::Generic) + emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table without an array part is likely a bug", op); + else if (tty->indexer && isString(tty->indexer->indexType)) // note: to avoid complexity of subtype tests we just check if the key is a string + emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table with string keys is likely a bug", op); + } + + void checkTableCall(AstExprCall* node, AstExprIndexName* func) + { AstExpr** args = node->args.data; if (func->index == "insert" && node->args.size == 2) @@ -2245,8 +2308,6 @@ class LintTableOperations : AstVisitor emitWarning(*context, LintWarning::Code_TableOperations, as->expr->location, "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"); } - - return true; } bool isConstant(AstExpr* expr, double value) diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 4ec3ade81..a7bb9d291 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -1,19 +1,60 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/NonStrictTypeChecker.h" +#include "Luau/Ast.h" +#include "Luau/Common.h" #include "Luau/Type.h" #include "Luau/Subtyping.h" #include "Luau/Normalize.h" #include "Luau/Error.h" #include "Luau/TypeArena.h" +#include "Luau/TypeFamily.h" #include "Luau/Def.h" +#include + namespace Luau { +/* Push a scope onto the end of a stack for the lifetime of the StackPusher instance. + * NonStrictTypeChecker uses this to maintain knowledge about which scope encloses every + * given AstNode. + */ +struct StackPusher +{ + std::vector>* stack; + NotNull scope; + + explicit StackPusher(std::vector>& stack, Scope* scope) + : stack(&stack) + , scope(scope) + { + stack.push_back(NotNull{scope}); + } + + ~StackPusher() + { + if (stack) + { + LUAU_ASSERT(stack->back() == scope); + stack->pop_back(); + } + } + + StackPusher(const StackPusher&) = delete; + StackPusher&& operator=(const StackPusher&) = delete; + + StackPusher(StackPusher&& other) + : stack(std::exchange(other.stack, nullptr)) + , scope(other.scope) + { + } +}; + + struct NonStrictContext { - std::unordered_map context; + std::unordered_map context; NonStrictContext() = default; @@ -38,24 +79,14 @@ struct NonStrictContext // TODO: unimplemented } - std::optional find(const DefId& def) + std::optional find(const DefId& def) const { - // TODO: unimplemented + const Def* d = def.get(); + auto it = context.find(d); + if (it != context.end()) + return {it->second}; return {}; } - - // Satisfies means that for a given DefId n, and an actual type t for `n`, t satisfies the context if t <: context[n] - // ice if the DefId is not in the context - bool satisfies(const DefId& def, TypeId inferredType) - { - // TODO: unimplemented - return false; - } - - bool willRunTimeError(const DefId& def, TypeId inferredType) - { - return satisfies(def, inferredType); - } }; struct NonStrictTypeChecker @@ -67,21 +98,341 @@ struct NonStrictTypeChecker Module* module; Normalizer normalizer; Subtyping subtyping; + NotNull dfg; + DenseHashSet noTypeFamilyErrors{nullptr}; + std::vector> stack; + const NotNull limits; - NonStrictTypeChecker(NotNull builtinTypes, Subtyping subtyping, const NotNull ice, - NotNull unifierState, Module* module) + NonStrictTypeChecker(NotNull builtinTypes, const NotNull ice, NotNull unifierState, + NotNull dfg, NotNull limits, Module* module) : builtinTypes(builtinTypes) , ice(ice) , module(module) , normalizer{&arena, builtinTypes, unifierState, /* cache inhabitance */ true} , subtyping{builtinTypes, NotNull{&arena}, NotNull(&normalizer), ice, NotNull{module->getModuleScope().get()}} + , dfg(dfg) + , limits(limits) + { + } + + std::optional pushStack(AstNode* node) + { + if (Scope** scope = module->astScopes.find(node)) + return StackPusher{stack, *scope}; + else + return std::nullopt; + } + + TypeId flattenPack(TypePackId pack) + { + pack = follow(pack); + + if (auto fst = first(pack, /*ignoreHiddenVariadics*/ false)) + return *fst; + else if (auto ftp = get(pack)) + { + TypeId result = arena.addType(FreeType{ftp->scope}); + TypePackId freeTail = arena.addTypePack(FreeTypePack{ftp->scope}); + + TypePack& resultPack = asMutable(pack)->ty.emplace(); + resultPack.head.assign(1, result); + resultPack.tail = freeTail; + + return result; + } + else if (get(pack)) + return builtinTypes->errorRecoveryType(); + else if (finite(pack) && size(pack) == 0) + return builtinTypes->nilType; // `(f())` where `f()` returns no values is coerced into `nil` + else + ice->ice("flattenPack got a weird pack!"); + } + + + TypeId checkForFamilyInhabitance(TypeId instance, Location location) + { + if (noTypeFamilyErrors.find(instance)) + return instance; + + ErrorVec errors = reduceFamilies( + instance, location, TypeFamilyContext{NotNull{&arena}, builtinTypes, stack.back(), NotNull{&normalizer}, ice, limits}, true) + .errors; + + if (errors.empty()) + noTypeFamilyErrors.insert(instance); + // TODO?? + // if (!isErrorSuppressing(location, instance)) + // reportErrors(std::move(errors)); + return instance; + } + + + TypeId lookupType(AstExpr* expr) + { + TypeId* ty = module->astTypes.find(expr); + if (ty) + return checkForFamilyInhabitance(follow(*ty), expr->location); + + TypePackId* tp = module->astTypePacks.find(expr); + if (tp) + return checkForFamilyInhabitance(flattenPack(*tp), expr->location); + return builtinTypes->anyType; + } + + + void visit(AstStat* stat) + { + NonStrictContext fresh{}; + visit(stat, fresh); + } + + void visit(AstStat* stat, NonStrictContext& context) + { + auto pusher = pushStack(stat); + if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else if (auto s = stat->as()) + return visit(s, context); + else + LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown node type"); + } + + void visit(AstStatBlock* block, NonStrictContext& context) + { + auto StackPusher = pushStack(block); + for (AstStat* statement : block->body) + visit(statement, context); + } + + void visit(AstStatIf* ifStatement, NonStrictContext& context) {} + void visit(AstStatWhile* whileStatement, NonStrictContext& context) {} + void visit(AstStatRepeat* repeatStatement, NonStrictContext& context) {} + void visit(AstStatBreak* breakStatement, NonStrictContext& context) {} + void visit(AstStatContinue* continueStatement, NonStrictContext& context) {} + void visit(AstStatReturn* returnStatement, NonStrictContext& context) {} + void visit(AstStatExpr* expr, NonStrictContext& context) + { + visit(expr->expr, context); + } + void visit(AstStatLocal* local, NonStrictContext& context) {} + void visit(AstStatFor* forStatement, NonStrictContext& context) {} + void visit(AstStatForIn* forInStatement, NonStrictContext& context) {} + void visit(AstStatAssign* assign, NonStrictContext& context) {} + void visit(AstStatCompoundAssign* compoundAssign, NonStrictContext& context) {} + void visit(AstStatFunction* statFn, NonStrictContext& context) {} + void visit(AstStatLocalFunction* localFn, NonStrictContext& context) {} + void visit(AstStatTypeAlias* typeAlias, NonStrictContext& context) {} + void visit(AstStatDeclareFunction* declFn, NonStrictContext& context) {} + void visit(AstStatDeclareGlobal* declGlobal, NonStrictContext& context) {} + void visit(AstStatDeclareClass* declClass, NonStrictContext& context) {} + void visit(AstStatError* error, NonStrictContext& context) {} + + void visit(AstExpr* expr, NonStrictContext& context) + { + auto pusher = pushStack(expr); + if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else if (auto e = expr->as()) + return visit(e, context); + else + LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown expression type"); + } + + void visit(AstExprGroup* group, NonStrictContext& context) {} + void visit(AstExprConstantNil* expr, NonStrictContext& context) {} + void visit(AstExprConstantBool* expr, NonStrictContext& context) {} + void visit(AstExprConstantNumber* expr, NonStrictContext& context) {} + void visit(AstExprConstantString* expr, NonStrictContext& context) {} + void visit(AstExprLocal* local, NonStrictContext& context) {} + void visit(AstExprGlobal* global, NonStrictContext& context) {} + void visit(AstExprVarargs* global, NonStrictContext& context) {} + + void visit(AstExprCall* call, NonStrictContext& context) + { + TypeId* originalCallTy = module->astOriginalCallTypes.find(call); + if (!originalCallTy) + return; + + TypeId fnTy = *originalCallTy; + NonStrictContext fresh{}; + if (auto fn = get(follow(fnTy))) + { + if (fn->isCheckedFunction) + { + // We know fn is a checked function, which means it looks like: + // (S1, ... SN) -> T & + // (~S1, unknown^N-1) -> error & + // (unknown, ~S2, unknown^N-2) -> error + // ... + // ... + // (unknown^N-1, ~S_N) -> error + std::vector argTypes; + for (TypeId ty : fn->argTypes) + argTypes.push_back(ty); + // For a checked function, these gotta be the same size + LUAU_ASSERT(call->args.size == argTypes.size()); + for (size_t i = 0; i < call->args.size; i++) + { + // For example, if the arg is "hi" + // The actual arg type is string + // The expected arg type is number + // The type of the argument in the overload is ~number + // We will compare arg and ~number + AstExpr* arg = call->args.data[i]; + TypeId expectedArgType = argTypes[i]; + NullableBreadcrumbId bc = dfg->getBreadcrumb(arg); + // TODO: Cache negations created here!!! + // See Jira Ticket: https://roblox.atlassian.net/browse/CLI-87539 + if (bc) + { + TypeId runTimeErrorTy = arena.addType(NegationType{expectedArgType}); + DefId def = bc->def; + fresh.context[def.get()] = runTimeErrorTy; + } + else + { + std::cout << "bad" << std::endl; + } + } + + // Populate the context and now iterate through each of the arguments to the call to find out if we satisfy the types + for (size_t i = 0; i < call->args.size; i++) + { + AstExpr* arg = call->args.data[i]; + // TODO: pipe in name of checked function to report Error + if (auto runTimeFailureType = willRunTimeError(arg, fresh)) + reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, "", i}, arg->location); + } + } + } + } + + void visit(AstExprIndexName* indexName, NonStrictContext& context) {} + void visit(AstExprIndexExpr* indexExpr, NonStrictContext& context) {} + void visit(AstExprFunction* exprFn, NonStrictContext& context) + { + auto pusher = pushStack(exprFn); + } + void visit(AstExprTable* table, NonStrictContext& context) {} + void visit(AstExprUnary* unary, NonStrictContext& context) {} + void visit(AstExprBinary* binary, NonStrictContext& context) {} + void visit(AstExprTypeAssertion* typeAssertion, NonStrictContext& context) {} + void visit(AstExprIfElse* ifElse, NonStrictContext& context) {} + void visit(AstExprInterpString* interpString, NonStrictContext& context) {} + void visit(AstExprError* error, NonStrictContext& context) {} + + void reportError(TypeErrorData data, const Location& location) { + module->errors.emplace_back(location, module->name, std::move(data)); + // TODO: weave in logger here? + } + + // If this fragment of the ast will run time error, return the type that causes this + std::optional willRunTimeError(AstExpr* fragment, const NonStrictContext& context) + { + + if (NullableBreadcrumbId bc = dfg->getBreadcrumb(fragment)) + { + std::optional contextTy = context.find(bc->def); + if (contextTy) + { + + TypeId actualType = lookupType(fragment); + SubtypingResult r = subtyping.isSubtype(actualType, *contextTy); + if (r.normalizationTooComplex) + reportError(NormalizationTooComplex{}, fragment->location); + + if (!r.isSubtype && !r.isErrorSuppressing) + reportError(TypeMismatch{actualType, *contextTy}, fragment->location); + + if (r.isSubtype) + return {actualType}; + } + } + return {}; } }; -void checkNonStrict(NotNull builtinTypes, Module* module) +void checkNonStrict(NotNull builtinTypes, NotNull ice, NotNull unifierState, + NotNull dfg, NotNull limits, const SourceModule& sourceModule, Module* module) { // TODO: unimplemented + NonStrictTypeChecker typeChecker{builtinTypes, ice, unifierState, dfg, limits, module}; + typeChecker.visit(sourceModule.root); + unfreeze(module->interfaceTypes); + copyErrors(module->errors, module->interfaceTypes, builtinTypes); + freeze(module->interfaceTypes); } + } // namespace Luau diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 8ee1d2c87..79bedec20 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -50,6 +50,8 @@ SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) // `|=` is intentional here, we want to preserve error related flags. isErrorSuppressing |= other.isErrorSuppressing; normalizationTooComplex |= other.normalizationTooComplex; + isCacheable &= other.isCacheable; + return *this; } @@ -58,6 +60,8 @@ SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other) isSubtype |= other.isSubtype; isErrorSuppressing |= other.isErrorSuppressing; normalizationTooComplex |= other.normalizationTooComplex; + isCacheable &= other.isCacheable; + return *this; } @@ -86,14 +90,23 @@ SubtypingResult SubtypingResult::any(const std::vector& results return acc; } +Subtyping::Subtyping(NotNull builtinTypes, NotNull typeArena, NotNull normalizer, + NotNull iceReporter, NotNull scope) + : builtinTypes(builtinTypes) + , arena(typeArena) + , normalizer(normalizer) + , iceReporter(iceReporter) + , scope(scope) +{ +} + SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy) { - mappedGenerics.clear(); - mappedGenericPacks.clear(); + SubtypingEnvironment env; - SubtypingResult result = isCovariantWith(subTy, superTy); + SubtypingResult result = isCovariantWith(env, subTy, superTy); - for (const auto& [subTy, bounds] : mappedGenerics) + for (const auto& [subTy, bounds] : env.mappedGenerics) { const auto& lb = bounds.lowerBound; const auto& ub = bounds.upperBound; @@ -122,15 +135,42 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy) result.isSubtype = false; } - result.andAlso(isCovariantWith(lowerBound, upperBound)); + result.andAlso(isCovariantWith(env, lowerBound, upperBound)); } + /* TODO: We presently don't store subtype test results in the persistent + * cache if the left-side type is a generic function. + * + * The implementation would be a bit tricky and we haven't seen any material + * impact on benchmarks. + * + * What we would want to do is to remember points within the type where + * mapped generics are introduced. When all the contingent generics are + * introduced at which we're doing the test, we can mark the result as + * cacheable. + */ + + if (result.isCacheable) + resultCache[{subTy, superTy}] = result; + return result; } SubtypingResult Subtyping::isSubtype(TypePackId subTp, TypePackId superTp) { - return isCovariantWith(subTp, superTp); + SubtypingEnvironment env; + return isCovariantWith(env, subTp, superTp); +} + +SubtypingResult Subtyping::cache(SubtypingEnvironment& env, SubtypingResult result, TypeId subTy, TypeId superTy) +{ + const std::pair p{subTy, superTy}; + if (result.isCacheable) + resultCache[p] = result; + else + env.ephemeralCache[p] = result; + + return result; } namespace @@ -153,11 +193,19 @@ struct SeenSetPopper }; } // namespace -SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy) { subTy = follow(subTy); superTy = follow(superTy); + SubtypingResult* cachedResult = resultCache.find({subTy, superTy}); + if (cachedResult) + return *cachedResult; + + cachedResult = env.ephemeralCache.find({subTy, superTy}); + if (cachedResult) + return *cachedResult; + // TODO: Do we care about returning a proof that this is error-suppressing? // e.g. given `a | error <: a | error` where both operands are pointer equal, // then should it also carry the information that it's error-suppressing? @@ -167,7 +215,29 @@ SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy) std::pair typePair{subTy, superTy}; if (!seenTypes.insert(typePair).second) - return {true}; + { + /* TODO: Caching results for recursive types is really tricky to think + * about. + * + * We'd like to cache at the outermost level where we encounter the + * recursive type, but we do not want to cache interior results that + * involve the cycle. + * + * Presently, we stop at cycles and assume that the subtype check will + * succeed because we'll eventually get there if it won't. However, if + * that cyclic type turns out not to have the asked-for subtyping + * relation, then all the intermediate cached results that were + * contingent on that assumption need to be evicted from the cache, or + * not entered into the cache, or something. + * + * For now, we do the conservative thing and refuse to cache anything + * that touches a cycle. + */ + SubtypingResult res; + res.isSubtype = true; + res.isCacheable = false; + return res; + } SeenSetPopper ssp{&seenTypes, typePair}; @@ -175,31 +245,31 @@ SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy) // tested as though it were its upper bounds. We do not yet support bounded // generics, so the upper bound is always unknown. if (auto subGeneric = get(subTy); subGeneric && subsumes(subGeneric->scope, scope)) - return isCovariantWith(builtinTypes->unknownType, superTy); + return isCovariantWith(env, builtinTypes->unknownType, superTy); if (auto superGeneric = get(superTy); superGeneric && subsumes(superGeneric->scope, scope)) - return isCovariantWith(subTy, builtinTypes->unknownType); + return isCovariantWith(env, subTy, builtinTypes->unknownType); + + SubtypingResult result; if (auto subUnion = get(subTy)) - return isCovariantWith(subUnion, superTy); + result = isCovariantWith(env, subUnion, superTy); else if (auto superUnion = get(superTy)) - return isCovariantWith(subTy, superUnion); + result = isCovariantWith(env, subTy, superUnion); else if (auto superIntersection = get(superTy)) - return isCovariantWith(subTy, superIntersection); + result = isCovariantWith(env, subTy, superIntersection); else if (auto subIntersection = get(subTy)) { - SubtypingResult result = isCovariantWith(subIntersection, superTy); - if (result.isSubtype || result.isErrorSuppressing || result.normalizationTooComplex) - return result; - else - return isCovariantWith(normalizer->normalize(subTy), normalizer->normalize(superTy)); + result = isCovariantWith(env, subIntersection, superTy); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + result = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); } else if (get(superTy)) - return {true}; // This is always true. + result = {true}; else if (get(subTy)) { // any = unknown | error, so we rewrite this to match. // As per TAPL: A | B <: T iff A <: T && B <: T - return isCovariantWith(builtinTypes->unknownType, superTy).andAlso(isCovariantWith(builtinTypes->errorType, superTy)); + result = isCovariantWith(env, builtinTypes->unknownType, superTy).andAlso(isCovariantWith(env, builtinTypes->errorType, superTy)); } else if (get(superTy)) { @@ -208,57 +278,59 @@ SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy) LUAU_ASSERT(!get(subTy)); // TODO: replace with ice. bool errorSuppressing = get(subTy); - return {!errorSuppressing, errorSuppressing}; + result = {!errorSuppressing, errorSuppressing}; } else if (get(subTy)) - return {true}; + result = {true}; else if (get(superTy)) - return {false, true}; + result = {false, true}; else if (get(subTy)) - return {false, true}; + result = {false, true}; else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p.first->ty, p.second->ty); + result = isCovariantWith(env, p.first->ty, p.second->ty); else if (auto subNegation = get(subTy)) - return isCovariantWith(subNegation, superTy); + result = isCovariantWith(env, subNegation, superTy); else if (auto superNegation = get(superTy)) - return isCovariantWith(subTy, superNegation); + result = isCovariantWith(env, subTy, superNegation); else if (auto subGeneric = get(subTy); subGeneric && variance == Variance::Covariant) { - bool ok = bindGeneric(subTy, superTy); - return {ok}; + bool ok = bindGeneric(env, subTy, superTy); + result.isSubtype = ok; + result.isCacheable = false; } else if (auto superGeneric = get(superTy); superGeneric && variance == Variance::Contravariant) { - bool ok = bindGeneric(subTy, superTy); - return {ok}; + bool ok = bindGeneric(env, subTy, superTy); + result.isSubtype = ok; + result.isCacheable = false; } else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - return isCovariantWith(p); + result = isCovariantWith(env, p); - return {false}; + return cache(env, result, subTy, superTy); } -SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp) { subTp = follow(subTp); superTp = follow(superTp); @@ -278,7 +350,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) for (size_t i = 0; i < headSize; ++i) { - results.push_back(isCovariantWith(subHead[i], superHead[i])); + results.push_back(isCovariantWith(env, subHead[i], superHead[i])); if (!results.back().isSubtype) return {false}; } @@ -292,7 +364,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) if (auto vt = get(*subTail)) { for (size_t i = headSize; i < superHead.size(); ++i) - results.push_back(isCovariantWith(vt->ty, superHead[i])); + results.push_back(isCovariantWith(env, vt->ty, superHead[i])); } else if (auto gt = get(*subTail)) { @@ -306,10 +378,10 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) std::vector headSlice(begin(superHead), begin(superHead) + headSize); TypePackId superTailPack = arena->addTypePack(std::move(headSlice), superTail); - if (TypePackId* other = mappedGenericPacks.find(*subTail)) - results.push_back(isCovariantWith(*other, superTailPack)); + if (TypePackId* other = env.mappedGenericPacks.find(*subTail)) + results.push_back(isCovariantWith(env, *other, superTailPack)); else - mappedGenericPacks.try_insert(*subTail, superTailPack); + env.mappedGenericPacks.try_insert(*subTail, superTailPack); // FIXME? Not a fan of the early return here. It makes the // control flow harder to reason about. @@ -337,7 +409,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) if (auto vt = get(*superTail)) { for (size_t i = headSize; i < subHead.size(); ++i) - results.push_back(isCovariantWith(subHead[i], vt->ty)); + results.push_back(isCovariantWith(env, subHead[i], vt->ty)); } else if (auto gt = get(*superTail)) { @@ -351,10 +423,10 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) std::vector headSlice(begin(subHead), begin(subHead) + headSize); TypePackId subTailPack = arena->addTypePack(std::move(headSlice), subTail); - if (TypePackId* other = mappedGenericPacks.find(*superTail)) - results.push_back(isCovariantWith(*other, subTailPack)); + if (TypePackId* other = env.mappedGenericPacks.find(*superTail)) + results.push_back(isCovariantWith(env, *other, subTailPack)); else - mappedGenericPacks.try_insert(*superTail, subTailPack); + env.mappedGenericPacks.try_insert(*superTail, subTailPack); // FIXME? Not a fan of the early return here. It makes the // control flow harder to reason about. @@ -381,11 +453,11 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) { if (auto p = get2(*subTail, *superTail)) { - results.push_back(isCovariantWith(p)); + results.push_back(isCovariantWith(env, p)); } else if (auto p = get2(*subTail, *superTail)) { - bool ok = bindGeneric(*subTail, *superTail); + bool ok = bindGeneric(env, *subTail, *superTail); results.push_back({ok}); } else if (get2(*subTail, *superTail)) @@ -393,7 +465,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) if (variance == Variance::Contravariant) { // (A...) -> number <: (...number) -> number - bool ok = bindGeneric(*subTail, *superTail); + bool ok = bindGeneric(env, *subTail, *superTail); results.push_back({ok}); } else @@ -412,7 +484,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) else { // () -> A... <: () -> ...number - bool ok = bindGeneric(*subTail, *superTail); + bool ok = bindGeneric(env, *subTail, *superTail); results.push_back({ok}); } } @@ -428,7 +500,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) } else if (get(*subTail)) { - bool ok = bindGeneric(*subTail, builtinTypes->emptyTypePack); + bool ok = bindGeneric(env, *subTail, builtinTypes->emptyTypePack); return {ok}; } else @@ -452,7 +524,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) { if (variance == Variance::Contravariant) { - bool ok = bindGeneric(builtinTypes->emptyTypePack, *superTail); + bool ok = bindGeneric(env, builtinTypes->emptyTypePack, *superTail); results.push_back({ok}); } else @@ -466,33 +538,33 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) } template -SubtypingResult Subtyping::isContravariantWith(SubTy&& subTy, SuperTy&& superTy) +SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { - return isCovariantWith(superTy, subTy); + return isCovariantWith(env, superTy, subTy); } template -SubtypingResult Subtyping::isInvariantWith(SubTy&& subTy, SuperTy&& superTy) +SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { - return isCovariantWith(subTy, superTy).andAlso(isContravariantWith(subTy, superTy)); + return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)); } template -SubtypingResult Subtyping::isCovariantWith(const TryPair& pair) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(pair.first, pair.second); + return isCovariantWith(env, pair.first, pair.second); } template -SubtypingResult Subtyping::isContravariantWith(const TryPair& pair) +SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(pair.second, pair.first); + return isCovariantWith(env, pair.second, pair.first); } template -SubtypingResult Subtyping::isInvariantWith(const TryPair& pair) +SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(pair).andAlso(isContravariantWith(pair)); + return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)); } /* @@ -526,43 +598,43 @@ SubtypingResult Subtyping::isInvariantWith(const TryPair subtypings; for (TypeId ty : superUnion) - subtypings.push_back(isCovariantWith(subTy, ty)); + subtypings.push_back(isCovariantWith(env, subTy, ty)); return SubtypingResult::any(subtypings); } -SubtypingResult Subtyping::isCovariantWith(const UnionType* subUnion, TypeId superTy) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, TypeId superTy) { // As per TAPL: A | B <: T iff A <: T && B <: T std::vector subtypings; for (TypeId ty : subUnion) - subtypings.push_back(isCovariantWith(ty, superTy)); + subtypings.push_back(isCovariantWith(env, ty, superTy)); return SubtypingResult::all(subtypings); } -SubtypingResult Subtyping::isCovariantWith(TypeId subTy, const IntersectionType* superIntersection) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const IntersectionType* superIntersection) { // As per TAPL: T <: A & B iff T <: A && T <: B std::vector subtypings; for (TypeId ty : superIntersection) - subtypings.push_back(isCovariantWith(subTy, ty)); + subtypings.push_back(isCovariantWith(env, subTy, ty)); return SubtypingResult::all(subtypings); } -SubtypingResult Subtyping::isCovariantWith(const IntersectionType* subIntersection, TypeId superTy) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const IntersectionType* subIntersection, TypeId superTy) { // As per TAPL: A & B <: T iff A <: T || B <: T std::vector subtypings; for (TypeId ty : subIntersection) - subtypings.push_back(isCovariantWith(ty, superTy)); + subtypings.push_back(isCovariantWith(env, ty, superTy)); return SubtypingResult::any(subtypings); } -SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, TypeId superTy) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NegationType* subNegation, TypeId superTy) { TypeId negatedTy = follow(subNegation->ty); @@ -572,17 +644,17 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type if (is(negatedTy)) { // ¬never ~ unknown - return isCovariantWith(builtinTypes->unknownType, superTy); + return isCovariantWith(env, builtinTypes->unknownType, superTy); } else if (is(negatedTy)) { // ¬unknown ~ never - return isCovariantWith(builtinTypes->neverType, superTy); + return isCovariantWith(env, builtinTypes->neverType, superTy); } else if (is(negatedTy)) { // ¬any ~ any - return isCovariantWith(negatedTy, superTy); + return isCovariantWith(env, negatedTy, superTy); } else if (auto u = get(negatedTy)) { @@ -593,7 +665,7 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type for (TypeId ty : u) { NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(&negatedTmp, superTy)); + subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy)); } return SubtypingResult::all(subtypings); @@ -607,11 +679,11 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type for (TypeId ty : i) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(negatedPart->ty, superTy)); + subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy)); else { NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(&negatedTmp, superTy)); + subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy)); } } @@ -629,19 +701,19 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type } } -SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationType* superNegation) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const NegationType* superNegation) { TypeId negatedTy = follow(superNegation->ty); if (is(negatedTy)) { // ¬never ~ unknown - return isCovariantWith(subTy, builtinTypes->unknownType); + return isCovariantWith(env, subTy, builtinTypes->unknownType); } else if (is(negatedTy)) { // ¬unknown ~ never - return isCovariantWith(subTy, builtinTypes->neverType); + return isCovariantWith(env, subTy, builtinTypes->neverType); } else if (is(negatedTy)) { @@ -657,11 +729,11 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp for (TypeId ty : u) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(subTy, negatedPart->ty)); + subtypings.push_back(isCovariantWith(env, subTy, negatedPart->ty)); else { NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(subTy, &negatedTmp)); + subtypings.push_back(isCovariantWith(env, subTy, &negatedTmp)); } } @@ -676,11 +748,11 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp for (TypeId ty : i) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(subTy, negatedPart->ty)); + subtypings.push_back(isCovariantWith(env, subTy, negatedPart->ty)); else { NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(subTy, &negatedTmp)); + subtypings.push_back(isCovariantWith(env, subTy, &negatedTmp)); } } @@ -724,7 +796,7 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp else if (auto p = get2(subTy, negatedTy)) return {*p.first != *p.second}; else if (auto p = get2(subTy, negatedTy)) - return SubtypingResult::negate(isCovariantWith(p.first, p.second)); + return SubtypingResult::negate(isCovariantWith(env, p.first, p.second)); else if (get2(subTy, negatedTy)) return {true}; else if (is(negatedTy)) @@ -733,12 +805,12 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp return {false}; } -SubtypingResult Subtyping::isCovariantWith(const PrimitiveType* subPrim, const PrimitiveType* superPrim) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const PrimitiveType* superPrim) { return {subPrim->type == superPrim->type}; } -SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, const PrimitiveType* superPrim) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const PrimitiveType* superPrim) { if (get(subSingleton) && superPrim->type == PrimitiveType::String) return {true}; @@ -748,12 +820,12 @@ SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, co return {false}; } -SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, const SingletonType* superSingleton) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const SingletonType* superSingleton) { return {*subSingleton == *superSingleton}; } -SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const TableType* superTable) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable) { SubtypingResult result{true}; @@ -764,12 +836,12 @@ SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const Tabl { std::vector results; if (auto it = subTable->props.find(name); it != subTable->props.end()) - results.push_back(isInvariantWith(it->second.type(), prop.type())); + results.push_back(isInvariantWith(env, it->second.type(), prop.type())); if (subTable->indexer) { - if (isInvariantWith(subTable->indexer->indexType, builtinTypes->stringType).isSubtype) - results.push_back(isInvariantWith(subTable->indexer->indexResultType, prop.type())); + if (isInvariantWith(env, subTable->indexer->indexType, builtinTypes->stringType).isSubtype) + results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, prop.type())); } if (results.empty()) @@ -781,7 +853,7 @@ SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const Tabl if (superTable->indexer) { if (subTable->indexer) - result.andAlso(isInvariantWith(*subTable->indexer, *superTable->indexer)); + result.andAlso(isInvariantWith(env, *subTable->indexer, *superTable->indexer)); else return {false}; } @@ -789,12 +861,12 @@ SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const Tabl return result; } -SubtypingResult Subtyping::isCovariantWith(const MetatableType* subMt, const MetatableType* superMt) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt) { - return isCovariantWith(subMt->table, superMt->table).andAlso(isCovariantWith(subMt->metatable, superMt->metatable)); + return isCovariantWith(env, subMt->table, superMt->table).andAlso(isCovariantWith(env, subMt->metatable, superMt->metatable)); } -SubtypingResult Subtyping::isCovariantWith(const MetatableType* subMt, const TableType* superTable) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable) { if (auto subTable = get(subMt->table)) { @@ -807,7 +879,7 @@ SubtypingResult Subtyping::isCovariantWith(const MetatableType* subMt, const Tab // that the metatable isn't a subtype of the table, even though they have // compatible properties/shapes. We'll revisit this later when we have a // better understanding of how important this is. - return isCovariantWith(subTable, superTable); + return isCovariantWith(env, subTable, superTable); } else { @@ -816,19 +888,19 @@ SubtypingResult Subtyping::isCovariantWith(const MetatableType* subMt, const Tab } } -SubtypingResult Subtyping::isCovariantWith(const ClassType* subClass, const ClassType* superClass) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const ClassType* superClass) { return {isSubclass(subClass, superClass)}; } -SubtypingResult Subtyping::isCovariantWith(const ClassType* subClass, const TableType* superTable) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const TableType* superTable) { SubtypingResult result{true}; for (const auto& [name, prop] : superTable->props) { if (auto classProp = lookupClassProp(subClass, name)) - result.andAlso(isInvariantWith(prop.type(), classProp->type())); + result.andAlso(isInvariantWith(env, prop.type(), classProp->type())); else return SubtypingResult{false}; } @@ -836,20 +908,20 @@ SubtypingResult Subtyping::isCovariantWith(const ClassType* subClass, const Tabl return result; } -SubtypingResult Subtyping::isCovariantWith(const FunctionType* subFunction, const FunctionType* superFunction) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const FunctionType* subFunction, const FunctionType* superFunction) { SubtypingResult result; { VarianceFlipper vf{&variance}; - result.orElse(isContravariantWith(subFunction->argTypes, superFunction->argTypes)); + result.orElse(isContravariantWith(env, subFunction->argTypes, superFunction->argTypes)); } - result.andAlso(isCovariantWith(subFunction->retTypes, superFunction->retTypes)); + result.andAlso(isCovariantWith(env, subFunction->retTypes, superFunction->retTypes)); return result; } -SubtypingResult Subtyping::isCovariantWith(const PrimitiveType* subPrim, const TableType* superTable) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const TableType* superTable) { SubtypingResult result{false}; if (subPrim->type == PrimitiveType::String) @@ -861,7 +933,7 @@ SubtypingResult Subtyping::isCovariantWith(const PrimitiveType* subPrim, const T if (auto it = mttv->props.find("__index"); it != mttv->props.end()) { if (auto stringTable = get(it->second.type())) - result.orElse(isCovariantWith(stringTable, superTable)); + result.orElse(isCovariantWith(env, stringTable, superTable)); } } } @@ -870,7 +942,7 @@ SubtypingResult Subtyping::isCovariantWith(const PrimitiveType* subPrim, const T return result; } -SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, const TableType* superTable) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable) { SubtypingResult result{false}; if (auto stringleton = get(subSingleton)) @@ -882,7 +954,7 @@ SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, co if (auto it = mttv->props.find("__index"); it != mttv->props.end()) { if (auto stringTable = get(it->second.type())) - result.orElse(isCovariantWith(stringTable, superTable)); + result.orElse(isCovariantWith(env, stringTable, superTable)); } } } @@ -890,32 +962,33 @@ SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, co return result; } -SubtypingResult Subtyping::isCovariantWith(const TableIndexer& subIndexer, const TableIndexer& superIndexer) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TableIndexer& subIndexer, const TableIndexer& superIndexer) { - return isInvariantWith(subIndexer.indexType, superIndexer.indexType).andAlso(isInvariantWith(superIndexer.indexResultType, subIndexer.indexResultType)); + return isInvariantWith(env, subIndexer.indexType, superIndexer.indexType) + .andAlso(isInvariantWith(env, superIndexer.indexResultType, subIndexer.indexResultType)); } -SubtypingResult Subtyping::isCovariantWith(const NormalizedType* subNorm, const NormalizedType* superNorm) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm) { if (!subNorm || !superNorm) return {false, true, true}; - SubtypingResult result = isCovariantWith(subNorm->tops, superNorm->tops); - result.andAlso(isCovariantWith(subNorm->booleans, superNorm->booleans)); - result.andAlso(isCovariantWith(subNorm->classes, superNorm->classes).orElse(isCovariantWith(subNorm->classes, superNorm->tables))); - result.andAlso(isCovariantWith(subNorm->errors, superNorm->errors)); - result.andAlso(isCovariantWith(subNorm->nils, superNorm->nils)); - result.andAlso(isCovariantWith(subNorm->numbers, superNorm->numbers)); - result.andAlso(isCovariantWith(subNorm->strings, superNorm->strings)); - result.andAlso(isCovariantWith(subNorm->strings, superNorm->tables)); - result.andAlso(isCovariantWith(subNorm->threads, superNorm->threads)); - result.andAlso(isCovariantWith(subNorm->tables, superNorm->tables)); - result.andAlso(isCovariantWith(subNorm->functions, superNorm->functions)); + SubtypingResult result = isCovariantWith(env, subNorm->tops, superNorm->tops); + result.andAlso(isCovariantWith(env, subNorm->booleans, superNorm->booleans)); + result.andAlso(isCovariantWith(env, subNorm->classes, superNorm->classes).orElse(isCovariantWith(env, subNorm->classes, superNorm->tables))); + result.andAlso(isCovariantWith(env, subNorm->errors, superNorm->errors)); + result.andAlso(isCovariantWith(env, subNorm->nils, superNorm->nils)); + result.andAlso(isCovariantWith(env, subNorm->numbers, superNorm->numbers)); + result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->strings)); + result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->tables)); + result.andAlso(isCovariantWith(env, subNorm->threads, superNorm->threads)); + result.andAlso(isCovariantWith(env, subNorm->tables, superNorm->tables)); + result.andAlso(isCovariantWith(env, subNorm->functions, superNorm->functions)); // isCovariantWith(subNorm->tyvars, superNorm->tyvars); return result; } -SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass, const NormalizedClassType& superClass) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const NormalizedClassType& superClass) { for (const auto& [subClassTy, _] : subClass.classes) { @@ -923,13 +996,13 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass, for (const auto& [superClassTy, superNegations] : superClass.classes) { - result.orElse(isCovariantWith(subClassTy, superClassTy)); + result.orElse(isCovariantWith(env, subClassTy, superClassTy)); if (!result.isSubtype) continue; for (TypeId negation : superNegations) { - result.andAlso(SubtypingResult::negate(isCovariantWith(subClassTy, negation))); + result.andAlso(SubtypingResult::negate(isCovariantWith(env, subClassTy, negation))); if (result.isSubtype) break; } @@ -942,14 +1015,14 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass, return {true}; } -SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass, const TypeIds& superTables) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const TypeIds& superTables) { for (const auto& [subClassTy, _] : subClass.classes) { SubtypingResult result; for (TypeId superTableTy : superTables) - result.orElse(isCovariantWith(subClassTy, superTableTy)); + result.orElse(isCovariantWith(env, subClassTy, superTableTy)); if (!result.isSubtype) return result; @@ -958,13 +1031,13 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass, return {true}; } -SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString, const NormalizedStringType& superString) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const NormalizedStringType& superString) { bool isSubtype = Luau::isSubtype(subString, superString); return {isSubtype}; } -SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString, const TypeIds& superTables) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const TypeIds& superTables) { if (subString.isNever()) return {true}; @@ -974,7 +1047,7 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString SubtypingResult result; for (const auto& superTable : superTables) { - result.orElse(isCovariantWith(builtinTypes->stringType, superTable)); + result.orElse(isCovariantWith(env, builtinTypes->stringType, superTable)); if (result.isSubtype) return result; } @@ -990,7 +1063,7 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString SubtypingResult result{true}; for (const auto& [_, subString] : subString.singletons) { - result.andAlso(isCovariantWith(subString, superTable)); + result.andAlso(isCovariantWith(env, subString, superTable)); if (!result.isSubtype) break; } @@ -1004,17 +1077,18 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString return {false}; } -SubtypingResult Subtyping::isCovariantWith(const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction) +SubtypingResult Subtyping::isCovariantWith( + SubtypingEnvironment& env, const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction) { if (subFunction.isNever()) return {true}; else if (superFunction.isTop) return {true}; else - return isCovariantWith(subFunction.parts, superFunction.parts); + return isCovariantWith(env, subFunction.parts, superFunction.parts); } -SubtypingResult Subtyping::isCovariantWith(const TypeIds& subTypes, const TypeIds& superTypes) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeIds& subTypes, const TypeIds& superTypes) { std::vector results; @@ -1022,32 +1096,32 @@ SubtypingResult Subtyping::isCovariantWith(const TypeIds& subTypes, const TypeId { results.emplace_back(); for (TypeId superTy : superTypes) - results.back().orElse(isCovariantWith(subTy, superTy)); + results.back().orElse(isCovariantWith(env, subTy, superTy)); } return SubtypingResult::all(results); } -SubtypingResult Subtyping::isCovariantWith(const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic) { - return isCovariantWith(subVariadic->ty, superVariadic->ty); + return isCovariantWith(env, subVariadic->ty, superVariadic->ty); } -bool Subtyping::bindGeneric(TypeId subTy, TypeId superTy) +bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId superTy) { if (variance == Variance::Covariant) { if (!get(subTy)) return false; - mappedGenerics[subTy].upperBound.insert(superTy); + env.mappedGenerics[subTy].upperBound.insert(superTy); } else { if (!get(superTy)) return false; - mappedGenerics[superTy].lowerBound.insert(subTy); + env.mappedGenerics[superTy].lowerBound.insert(subTy); } return true; @@ -1058,7 +1132,7 @@ bool Subtyping::bindGeneric(TypeId subTy, TypeId superTy) * side, it is permissible to tentatively bind that generic to the right side * type. */ -bool Subtyping::bindGeneric(TypePackId subTp, TypePackId superTp) +bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp) { if (variance == Variance::Contravariant) std::swap(superTp, subTp); @@ -1066,10 +1140,10 @@ bool Subtyping::bindGeneric(TypePackId subTp, TypePackId superTp) if (!get(subTp)) return false; - if (TypePackId* m = mappedGenericPacks.find(subTp)) + if (TypePackId* m = env.mappedGenericPacks.find(subTp)) return *m == superTp; - mappedGenericPacks[subTp] = superTp; + env.mappedGenericPacks[subTp] = superTp; return true; } diff --git a/Analysis/src/TypeFamily.cpp b/Analysis/src/TypeFamily.cpp index 06574d97c..f631d795f 100644 --- a/Analysis/src/TypeFamily.cpp +++ b/Analysis/src/TypeFamily.cpp @@ -8,6 +8,7 @@ #include "Luau/Normalize.h" #include "Luau/Simplify.h" #include "Luau/Substitution.h" +#include "Luau/Subtyping.h" #include "Luau/ToString.h" #include "Luau/TxnLog.h" #include "Luau/TypeCheckLimits.h" @@ -320,8 +321,25 @@ bool isPending(TypeId ty, ConstraintSolver* solver) return is(ty) || is(ty) || is(ty) || (solver && solver->hasUnresolvedConstraints(ty)); } +TypeFamilyReductionResult notFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("not type family: encountered a type family instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId ty = follow(typeParams.at(0)); + + if (isPending(ty, ctx->solver)) + return {std::nullopt, false, {ty}, {}}; + + // `not` operates on anything and returns a `boolean` always. + return {ctx->builtins->booleanType, false, {}, {}}; +} + TypeFamilyReductionResult numericBinopFamilyFn( - std::vector typeParams, std::vector packParams, NotNull ctx, const std::string metamethod) + const std::vector& typeParams, const std::vector& packParams, NotNull ctx, const std::string metamethod) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -333,34 +351,28 @@ TypeFamilyReductionResult numericBinopFamilyFn( TypeId rhsTy = follow(typeParams.at(1)); const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy); const NormalizedType* 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, false, {}, {}}; - } - else if (is(normLhsTy->tops) || is(normRhsTy->tops)) - { + + // 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, false, {}, {}}; - } - else if ((normLhsTy->hasNumbers() || normLhsTy->hasTops()) && (normRhsTy->hasNumbers() || normRhsTy->hasTops())) - { - return {ctx->builtins->numberType, false, {}, {}}; - } - else if (is(lhsTy) || is(rhsTy)) - { - return {ctx->builtins->errorRecoveryType(), false, {}, {}}; - } - else if (is(lhsTy) || is(rhsTy)) - { + + // if we have a `never`, we can never observe that the numeric operator didn't work. + if (is(lhsTy) || is(rhsTy)) return {ctx->builtins->neverType, false, {}, {}}; - } - else if (isPending(lhsTy, ctx->solver)) - { + + // if we're adding two `number` types, the result is `number`. + if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber()) + return {ctx->builtins->numberType, false, {}, {}}; + + // otherwise, check if we need to wait on either type to be further resolved + if (isPending(lhsTy, ctx->solver)) return {std::nullopt, false, {lhsTy}, {}}; - } else if (isPending(rhsTy, ctx->solver)) - { return {std::nullopt, false, {rhsTy}, {}}; - } // 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. @@ -385,39 +397,32 @@ TypeFamilyReductionResult numericBinopFamilyFn( if (!mmFtv) return {std::nullopt, true, {}, {}}; - if (std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType)) - { - if (const FunctionType* instantiatedMmFtv = get(*instantiatedMmType)) - { - std::vector inferredArgs; - if (!reversed) - inferredArgs = {lhsTy, rhsTy}; - else - inferredArgs = {rhsTy, lhsTy}; + std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); + if (!instantiatedMmType) + return {std::nullopt, true, {}, {}}; - 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, true, {}, {}}; // occurs check failed + const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); + if (!instantiatedMmFtv) + return {ctx->builtins->errorRecoveryType(), false, {}, {}}; - if (std::optional ret = first(instantiatedMmFtv->retTypes)) - return {*ret, false, {}, {}}; - else - return {std::nullopt, true, {}, {}}; - } - else - { - return {ctx->builtins->errorRecoveryType(), false, {}, {}}; - } - } + 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, true, {}, {}}; // occurs check failed + + if (std::optional ret = first(instantiatedMmFtv->retTypes)) + return {*ret, false, {}, {}}; else - { - // TODO: Not the nicest logic here. return {std::nullopt, true, {}, {}}; - } } -TypeFamilyReductionResult addFamilyFn(std::vector typeParams, std::vector packParams, NotNull ctx) +TypeFamilyReductionResult addFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -428,7 +433,7 @@ TypeFamilyReductionResult addFamilyFn(std::vector typeParams, st return numericBinopFamilyFn(typeParams, packParams, ctx, "__add"); } -TypeFamilyReductionResult subFamilyFn(std::vector typeParams, std::vector packParams, NotNull ctx) +TypeFamilyReductionResult subFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -439,7 +444,7 @@ TypeFamilyReductionResult subFamilyFn(std::vector typeParams, st return numericBinopFamilyFn(typeParams, packParams, ctx, "__sub"); } -TypeFamilyReductionResult mulFamilyFn(std::vector typeParams, std::vector packParams, NotNull ctx) +TypeFamilyReductionResult mulFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -450,7 +455,7 @@ TypeFamilyReductionResult mulFamilyFn(std::vector typeParams, st return numericBinopFamilyFn(typeParams, packParams, ctx, "__mul"); } -TypeFamilyReductionResult divFamilyFn(std::vector typeParams, std::vector packParams, NotNull ctx) +TypeFamilyReductionResult divFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -461,7 +466,7 @@ TypeFamilyReductionResult divFamilyFn(std::vector typeParams, st return numericBinopFamilyFn(typeParams, packParams, ctx, "__div"); } -TypeFamilyReductionResult idivFamilyFn(std::vector typeParams, std::vector packParams, NotNull ctx) +TypeFamilyReductionResult idivFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -472,7 +477,7 @@ TypeFamilyReductionResult idivFamilyFn(std::vector typeParams, s return numericBinopFamilyFn(typeParams, packParams, ctx, "__idiv"); } -TypeFamilyReductionResult powFamilyFn(std::vector typeParams, std::vector packParams, NotNull ctx) +TypeFamilyReductionResult powFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -483,7 +488,7 @@ TypeFamilyReductionResult powFamilyFn(std::vector typeParams, st return numericBinopFamilyFn(typeParams, packParams, ctx, "__pow"); } -TypeFamilyReductionResult modFamilyFn(std::vector typeParams, std::vector packParams, NotNull ctx) +TypeFamilyReductionResult modFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -494,7 +499,91 @@ TypeFamilyReductionResult modFamilyFn(std::vector typeParams, st return numericBinopFamilyFn(typeParams, packParams, ctx, "__mod"); } -TypeFamilyReductionResult andFamilyFn(std::vector typeParams, std::vector packParams, NotNull ctx) +TypeFamilyReductionResult concatFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("concat type family: encountered a type family instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId lhsTy = follow(typeParams.at(0)); + TypeId rhsTy = follow(typeParams.at(1)); + const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy); + const NormalizedType* 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, false, {}, {}}; + + // 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, false, {}, {}}; + + // if we have a `never`, we can never observe that the numeric operator didn't work. + if (is(lhsTy) || is(rhsTy)) + return {ctx->builtins->neverType, false, {}, {}}; + + // 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, false, {}, {}}; + + // otherwise, check if we need to wait on either type to be further resolved + if (isPending(lhsTy, ctx->solver)) + return {std::nullopt, false, {lhsTy}, {}}; + else if (isPending(rhsTy, ctx->solver)) + return {std::nullopt, false, {rhsTy}, {}}; + + // 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, true, {}, {}}; + + mmType = follow(*mmType); + if (isPending(*mmType, ctx->solver)) + return {std::nullopt, false, {*mmType}, {}}; + + const FunctionType* mmFtv = get(*mmType); + if (!mmFtv) + return {std::nullopt, true, {}, {}}; + + std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); + if (!instantiatedMmType) + return {std::nullopt, true, {}, {}}; + + const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); + if (!instantiatedMmFtv) + return {ctx->builtins->errorRecoveryType(), false, {}, {}}; + + 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, true, {}, {}}; // occurs check failed + + Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->ice, ctx->scope}; + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes).isSubtype) // TODO: is this the right variance? + return {std::nullopt, true, {}, {}}; + + return {ctx->builtins->stringType, false, {}, {}}; +} + +TypeFamilyReductionResult andFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -522,7 +611,7 @@ TypeFamilyReductionResult andFamilyFn(std::vector typeParams, st return {overallResult.result, false, std::move(blockedTypes), {}}; } -TypeFamilyReductionResult orFamilyFn(std::vector typeParams, std::vector packParams, NotNull ctx) +TypeFamilyReductionResult orFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) { @@ -550,16 +639,196 @@ TypeFamilyReductionResult orFamilyFn(std::vector typeParams, std return {overallResult.result, false, std::move(blockedTypes), {}}; } +static TypeFamilyReductionResult comparisonFamilyFn( + 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 family instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId lhsTy = follow(typeParams.at(0)); + TypeId rhsTy = follow(typeParams.at(1)); + const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy); + const NormalizedType* 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, false, {}, {}}; + + // if one of the types is error suppressing, we can just go ahead and reduce. + if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) + return {ctx->builtins->booleanType, false, {}, {}}; + + // if we have a `never`, we can never observe that the comparison didn't work. + if (is(lhsTy) || is(rhsTy)) + return {ctx->builtins->booleanType, false, {}, {}}; + + // If both types are some strict subset of `string`, we can reduce now. + if (normLhsTy->isSubtypeOfString() && normRhsTy->isSubtypeOfString()) + return {ctx->builtins->booleanType, false, {}, {}}; + + // If both types are exactly `number`, we can reduce now. + if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber()) + return {ctx->builtins->booleanType, false, {}, {}}; + + // otherwise, check if we need to wait on either type to be further resolved + if (isPending(lhsTy, ctx->solver)) + return {std::nullopt, false, {lhsTy}, {}}; + else if (isPending(rhsTy, ctx->solver)) + return {std::nullopt, false, {rhsTy}, {}}; + + // 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, true, {}, {}}; + + mmType = follow(*mmType); + if (isPending(*mmType, ctx->solver)) + return {std::nullopt, false, {*mmType}, {}}; + + const FunctionType* mmFtv = get(*mmType); + if (!mmFtv) + return {std::nullopt, true, {}, {}}; + + std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); + if (!instantiatedMmType) + return {std::nullopt, true, {}, {}}; + + const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); + if (!instantiatedMmFtv) + return {ctx->builtins->errorRecoveryType(), false, {}, {}}; + + 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, true, {}, {}}; // occurs check failed + + Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->ice, ctx->scope}; + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes).isSubtype) // TODO: is this the right variance? + return {std::nullopt, true, {}, {}}; + + return {ctx->builtins->booleanType, false, {}, {}}; +} + +TypeFamilyReductionResult ltFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("lt type family: encountered a type family instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return comparisonFamilyFn(typeParams, packParams, ctx, "__lt"); +} + +TypeFamilyReductionResult leFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("le type family: encountered a type family instance without the required argument structure"); + LUAU_ASSERT(false); + } + + return comparisonFamilyFn(typeParams, packParams, ctx, "__le"); +} + +TypeFamilyReductionResult eqFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) +{ + if (typeParams.size() != 2 || !packParams.empty()) + { + ctx->ice->ice("eq type family: encountered a type family instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId lhsTy = follow(typeParams.at(0)); + TypeId rhsTy = follow(typeParams.at(1)); + const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy); + const NormalizedType* 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, false, {}, {}}; + + // if one of the types is error suppressing, we can just go ahead and reduce. + if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors()) + return {ctx->builtins->booleanType, false, {}, {}}; + + // if we have a `never`, we can never observe that the comparison didn't work. + if (is(lhsTy) || is(rhsTy)) + return {ctx->builtins->booleanType, false, {}, {}}; + + // otherwise, check if we need to wait on either type to be further resolved + if (isPending(lhsTy, ctx->solver)) + return {std::nullopt, false, {lhsTy}, {}}; + else if (isPending(rhsTy, ctx->solver)) + return {std::nullopt, false, {rhsTy}, {}}; + + // 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! + if (!mmType && ctx->normalizer->isIntersectionInhabited(lhsTy, rhsTy)) + return {ctx->builtins->booleanType, false, {}, {}}; // if it's inhabited, everything is okay! + else if (!mmType) + return {std::nullopt, true, {}, {}}; // if it's not, then this family is irreducible! + + mmType = follow(*mmType); + if (isPending(*mmType, ctx->solver)) + return {std::nullopt, false, {*mmType}, {}}; + + const FunctionType* mmFtv = get(*mmType); + if (!mmFtv) + return {std::nullopt, true, {}, {}}; + + std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); + if (!instantiatedMmType) + return {std::nullopt, true, {}, {}}; + + const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); + if (!instantiatedMmFtv) + return {ctx->builtins->errorRecoveryType(), false, {}, {}}; + + 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, true, {}, {}}; // occurs check failed + + Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->ice, ctx->scope}; + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes).isSubtype) // TODO: is this the right variance? + return {std::nullopt, true, {}, {}}; + + return {ctx->builtins->booleanType, false, {}, {}}; +} + BuiltinTypeFamilies::BuiltinTypeFamilies() - : addFamily{"Add", addFamilyFn} - , subFamily{"Sub", subFamilyFn} - , mulFamily{"Mul", mulFamilyFn} - , divFamily{"Div", divFamilyFn} - , idivFamily{"FloorDiv", idivFamilyFn} - , powFamily{"Exp", powFamilyFn} - , modFamily{"Mod", modFamilyFn} - , andFamily{"And", andFamilyFn} - , orFamily{"Or", orFamilyFn} + : notFamily{"not", notFamilyFn} + , addFamily{"add", addFamilyFn} + , subFamily{"sub", subFamilyFn} + , mulFamily{"mul", mulFamilyFn} + , divFamily{"div", divFamilyFn} + , idivFamily{"idiv", idivFamilyFn} + , powFamily{"pow", powFamilyFn} + , modFamily{"mod", modFamilyFn} + , concatFamily{"concat", concatFamilyFn} + , andFamily{"and", andFamilyFn} + , orFamily{"or", orFamilyFn} + , ltFamily{"lt", ltFamilyFn} + , leFamily{"le", leFamilyFn} + , eqFamily{"eq", eqFamilyFn} { } @@ -582,6 +851,11 @@ void BuiltinTypeFamilies::addToScope(NotNull arena, NotNull sc scope->exportedTypeBindings[idivFamily.name] = mkBinaryTypeFamily(&idivFamily); scope->exportedTypeBindings[powFamily.name] = mkBinaryTypeFamily(&powFamily); scope->exportedTypeBindings[modFamily.name] = mkBinaryTypeFamily(&modFamily); + scope->exportedTypeBindings[concatFamily.name] = mkBinaryTypeFamily(&concatFamily); + + scope->exportedTypeBindings[ltFamily.name] = mkBinaryTypeFamily(<Family); + scope->exportedTypeBindings[leFamily.name] = mkBinaryTypeFamily(&leFamily); + scope->exportedTypeBindings[eqFamily.name] = mkBinaryTypeFamily(&eqFamily); } } // namespace Luau diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index 223cd3dfc..c371e81ed 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -12,7 +12,6 @@ #include "Luau/TypeUtils.h" #include "Luau/Type.h" #include "Luau/VisitType.h" -#include "Luau/TypeFamily.h" #include @@ -453,32 +452,10 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool blockedTypes.push_back(superTy); if (log.get(superTy)) - { - // FIXME: we should be be ICEing here because the old unifier is legacy and should not interact with type families at all. - // Unfortunately, there are, at the time of writing, still uses of the old unifier under local type inference. - TypeCheckLimits limits; - reduceFamilies( - superTy, location, TypeFamilyContext{NotNull(types), builtinTypes, scope, normalizer, NotNull{sharedState.iceHandler}, NotNull{&limits}}); - superTy = log.follow(superTy); - } + ice("Unexpected TypeFamilyInstanceType superTy"); if (log.get(subTy)) - { - // FIXME: we should be be ICEing here because the old unifier is legacy and should not interact with type families at all. - // Unfortunately, there are, at the time of writing, still uses of the old unifier under local type inference. - TypeCheckLimits limits; - reduceFamilies( - subTy, location, TypeFamilyContext{NotNull(types), builtinTypes, scope, normalizer, NotNull{sharedState.iceHandler}, NotNull{&limits}}); - subTy = log.follow(subTy); - } - - // If we can't reduce the families down and we still have type family types - // here, we are stuck. Nothing meaningful can be done here. We don't wish to - // report an error, either. - if (log.get(superTy) || log.get(subTy)) - { - return; - } + ice("Unexpected TypeFamilyInstanceType subTy"); auto superFree = log.getMutable(superTy); auto subFree = log.getMutable(subTy); diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index 3e55a91eb..a3908a561 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -374,7 +374,7 @@ class AstExprFunction : public AstExpr AstExprFunction(const Location& location, const AstArray& generics, const AstArray& genericPacks, AstLocal* self, const AstArray& args, bool vararg, const Location& varargLocation, AstStatBlock* body, size_t functionDepth, const AstName& debugname, const std::optional& returnAnnotation = {}, AstTypePack* varargAnnotation = nullptr, - bool hasEnd = false, const std::optional& argLocation = std::nullopt); + bool DEPRECATED_hasEnd = false, const std::optional& argLocation = std::nullopt); void visit(AstVisitor* visitor) override; @@ -393,7 +393,8 @@ class AstExprFunction : public AstExpr AstName debugname; - bool hasEnd = false; + // TODO clip with FFlag::LuauClipExtraHasEndProps + bool DEPRECATED_hasEnd = false; std::optional argLocation; }; @@ -539,6 +540,17 @@ class AstStatBlock : public AstStat void visit(AstVisitor* visitor) override; AstArray body; + + /* Indicates whether or not this block has been terminated in a + * syntactically valid way. + * + * This is usually but not always done with the 'end' keyword. AstStatIf + * and AstStatRepeat are the two main exceptions to this. + * + * The 'then' clause of an if statement can properly be closed by the + * keywords 'else' or 'elseif'. A 'repeat' loop's body is closed with the + * 'until' keyword. + */ bool hasEnd = false; }; @@ -548,7 +560,7 @@ class AstStatIf : public AstStat LUAU_RTTI(AstStatIf) AstStatIf(const Location& location, AstExpr* condition, AstStatBlock* thenbody, AstStat* elsebody, const std::optional& thenLocation, - const std::optional& elseLocation, bool hasEnd); + const std::optional& elseLocation, bool DEPRECATED_hasEnd); void visit(AstVisitor* visitor) override; @@ -561,7 +573,8 @@ class AstStatIf : public AstStat // Active for 'elseif' as well std::optional elseLocation; - bool hasEnd = false; + // TODO clip with FFlag::LuauClipExtraHasEndProps + bool DEPRECATED_hasEnd = false; }; class AstStatWhile : public AstStat @@ -569,7 +582,7 @@ class AstStatWhile : public AstStat public: LUAU_RTTI(AstStatWhile) - AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation, bool hasEnd); + AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation, bool DEPRECATED_hasEnd); void visit(AstVisitor* visitor) override; @@ -579,7 +592,8 @@ class AstStatWhile : public AstStat bool hasDo = false; Location doLocation; - bool hasEnd = false; + // TODO clip with FFlag::LuauClipExtraHasEndProps + bool DEPRECATED_hasEnd = false; }; class AstStatRepeat : public AstStat @@ -587,14 +601,14 @@ class AstStatRepeat : public AstStat public: LUAU_RTTI(AstStatRepeat) - AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasUntil); + AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool DEPRECATED_hasUntil); void visit(AstVisitor* visitor) override; AstExpr* condition; AstStatBlock* body; - bool hasUntil = false; + bool DEPRECATED_hasUntil = false; }; class AstStatBreak : public AstStat @@ -663,7 +677,7 @@ class AstStatFor : public AstStat LUAU_RTTI(AstStatFor) AstStatFor(const Location& location, AstLocal* var, AstExpr* from, AstExpr* to, AstExpr* step, AstStatBlock* body, bool hasDo, - const Location& doLocation, bool hasEnd); + const Location& doLocation, bool DEPRECATED_hasEnd); void visit(AstVisitor* visitor) override; @@ -676,7 +690,8 @@ class AstStatFor : public AstStat bool hasDo = false; Location doLocation; - bool hasEnd = false; + // TODO clip with FFlag::LuauClipExtraHasEndProps + bool DEPRECATED_hasEnd = false; }; class AstStatForIn : public AstStat @@ -685,7 +700,7 @@ class AstStatForIn : public AstStat LUAU_RTTI(AstStatForIn) AstStatForIn(const Location& location, const AstArray& vars, const AstArray& values, AstStatBlock* body, bool hasIn, - const Location& inLocation, bool hasDo, const Location& doLocation, bool hasEnd); + const Location& inLocation, bool hasDo, const Location& doLocation, bool DEPRECATED_hasEnd); void visit(AstVisitor* visitor) override; @@ -699,7 +714,8 @@ class AstStatForIn : public AstStat bool hasDo = false; Location doLocation; - bool hasEnd = false; + // TODO clip with FFlag::LuauClipExtraHasEndProps + bool DEPRECATED_hasEnd = false; }; class AstStatAssign : public AstStat diff --git a/Ast/src/Ast.cpp b/Ast/src/Ast.cpp index ec5700dc7..a7d7eef7e 100644 --- a/Ast/src/Ast.cpp +++ b/Ast/src/Ast.cpp @@ -3,7 +3,7 @@ #include "Luau/Common.h" -LUAU_FASTFLAG(LuauFloorDivision) +LUAU_FASTFLAG(LuauFloorDivision); namespace Luau { @@ -164,7 +164,7 @@ void AstExprIndexExpr::visit(AstVisitor* visitor) AstExprFunction::AstExprFunction(const Location& location, const AstArray& generics, const AstArray& genericPacks, AstLocal* self, const AstArray& args, bool vararg, const Location& varargLocation, AstStatBlock* body, size_t functionDepth, - const AstName& debugname, const std::optional& returnAnnotation, AstTypePack* varargAnnotation, bool hasEnd, + const AstName& debugname, const std::optional& returnAnnotation, AstTypePack* varargAnnotation, bool DEPRECATED_hasEnd, const std::optional& argLocation) : AstExpr(ClassIndex(), location) , generics(generics) @@ -178,7 +178,7 @@ AstExprFunction::AstExprFunction(const Location& location, const AstArray& thenLocation, const std::optional& elseLocation, bool hasEnd) + const std::optional& thenLocation, const std::optional& elseLocation, bool DEPRECATED_hasEnd) : AstStat(ClassIndex(), location) , condition(condition) , thenbody(thenbody) , elsebody(elsebody) , thenLocation(thenLocation) , elseLocation(elseLocation) - , hasEnd(hasEnd) + , DEPRECATED_hasEnd(DEPRECATED_hasEnd) { } @@ -420,13 +420,13 @@ void AstStatIf::visit(AstVisitor* visitor) } } -AstStatWhile::AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation, bool hasEnd) +AstStatWhile::AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation, bool DEPRECATED_hasEnd) : AstStat(ClassIndex(), location) , condition(condition) , body(body) , hasDo(hasDo) , doLocation(doLocation) - , hasEnd(hasEnd) + , DEPRECATED_hasEnd(DEPRECATED_hasEnd) { } @@ -439,11 +439,11 @@ void AstStatWhile::visit(AstVisitor* visitor) } } -AstStatRepeat::AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasUntil) +AstStatRepeat::AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool DEPRECATED_hasUntil) : AstStat(ClassIndex(), location) , condition(condition) , body(body) - , hasUntil(hasUntil) + , DEPRECATED_hasUntil(DEPRECATED_hasUntil) { } @@ -528,7 +528,7 @@ void AstStatLocal::visit(AstVisitor* visitor) } AstStatFor::AstStatFor(const Location& location, AstLocal* var, AstExpr* from, AstExpr* to, AstExpr* step, AstStatBlock* body, bool hasDo, - const Location& doLocation, bool hasEnd) + const Location& doLocation, bool DEPRECATED_hasEnd) : AstStat(ClassIndex(), location) , var(var) , from(from) @@ -537,7 +537,7 @@ AstStatFor::AstStatFor(const Location& location, AstLocal* var, AstExpr* from, A , body(body) , hasDo(hasDo) , doLocation(doLocation) - , hasEnd(hasEnd) + , DEPRECATED_hasEnd(DEPRECATED_hasEnd) { } @@ -559,7 +559,7 @@ void AstStatFor::visit(AstVisitor* visitor) } AstStatForIn::AstStatForIn(const Location& location, const AstArray& vars, const AstArray& values, AstStatBlock* body, - bool hasIn, const Location& inLocation, bool hasDo, const Location& doLocation, bool hasEnd) + bool hasIn, const Location& inLocation, bool hasDo, const Location& doLocation, bool DEPRECATED_hasEnd) : AstStat(ClassIndex(), location) , vars(vars) , values(values) @@ -568,7 +568,7 @@ AstStatForIn::AstStatForIn(const Location& location, const AstArray& , inLocation(inLocation) , hasDo(hasDo) , doLocation(doLocation) - , hasEnd(hasEnd) + , DEPRECATED_hasEnd(DEPRECATED_hasEnd) { } diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index dfb0a540e..3b9e95f8d 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -15,6 +15,7 @@ LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false) +LUAU_FASTFLAGVARIABLE(LuauClipExtraHasEndProps, false) LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauCheckedFunctionSyntax) @@ -371,16 +372,18 @@ AstStat* Parser::parseIf() AstStat* elsebody = nullptr; Location end = start; std::optional elseLocation; - bool hasEnd = false; + bool DEPRECATED_hasEnd = false; if (lexer.current().type == Lexeme::ReservedElseif) { + if (FFlag::LuauClipExtraHasEndProps) + thenbody->hasEnd = true; unsigned int recursionCounterOld = recursionCounter; incrementRecursionCounter("elseif"); elseLocation = lexer.current().location; elsebody = parseIf(); end = elsebody->location; - hasEnd = elsebody->as()->hasEnd; + DEPRECATED_hasEnd = elsebody->as()->DEPRECATED_hasEnd; recursionCounter = recursionCounterOld; } else @@ -389,6 +392,8 @@ AstStat* Parser::parseIf() if (lexer.current().type == Lexeme::ReservedElse) { + if (FFlag::LuauClipExtraHasEndProps) + thenbody->hasEnd = true; elseLocation = lexer.current().location; matchThenElse = lexer.current(); nextLexeme(); @@ -399,10 +404,22 @@ AstStat* Parser::parseIf() end = lexer.current().location; - hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchThenElse); + bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchThenElse); + DEPRECATED_hasEnd = hasEnd; + + if (FFlag::LuauClipExtraHasEndProps) + { + if (elsebody) + { + if (AstStatBlock* elseBlock = elsebody->as()) + elseBlock->hasEnd = hasEnd; + } + else + thenbody->hasEnd = hasEnd; + } } - return allocator.alloc(Location(start, end), cond, thenbody, elsebody, thenLocation, elseLocation, hasEnd); + return allocator.alloc(Location(start, end), cond, thenbody, elsebody, thenLocation, elseLocation, DEPRECATED_hasEnd); } // while exp do block end @@ -426,6 +443,8 @@ AstStat* Parser::parseWhile() Location end = lexer.current().location; bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); + if (FFlag::LuauClipExtraHasEndProps) + body->hasEnd = hasEnd; return allocator.alloc(Location(start, end), cond, body, hasDo, matchDo.location, hasEnd); } @@ -447,6 +466,8 @@ AstStat* Parser::parseRepeat() functionStack.back().loopDepth--; bool hasUntil = expectMatchEndAndConsume(Lexeme::ReservedUntil, matchRepeat); + if (FFlag::LuauClipExtraHasEndProps) + body->hasEnd = hasUntil; AstExpr* cond = parseExpr(); @@ -543,6 +564,8 @@ AstStat* Parser::parseFor() Location end = lexer.current().location; bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); + if (FFlag::LuauClipExtraHasEndProps) + body->hasEnd = hasEnd; return allocator.alloc(Location(start, end), var, from, to, step, body, hasDo, matchDo.location, hasEnd); } @@ -585,6 +608,8 @@ AstStat* Parser::parseFor() Location end = lexer.current().location; bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); + if (FFlag::LuauClipExtraHasEndProps) + body->hasEnd = hasEnd; return allocator.alloc( Location(start, end), copy(vars), copy(values), body, hasIn, inLocation, hasDo, matchDo.location, hasEnd); @@ -1074,6 +1099,8 @@ std::pair Parser::parseFunctionBody( Location end = lexer.current().location; bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchFunction); + if (FFlag::LuauClipExtraHasEndProps) + body->hasEnd = hasEnd; return {allocator.alloc(Location(start, end), generics, genericPacks, self, vars, vararg, varargLocation, body, functionStack.size(), debugname, typelist, varargAnnotation, hasEnd, argLocation), diff --git a/CodeGen/include/Luau/IrAnalysis.h b/CodeGen/include/Luau/IrAnalysis.h index 5fcaf46be..0f9ce795b 100644 --- a/CodeGen/include/Luau/IrAnalysis.h +++ b/CodeGen/include/Luau/IrAnalysis.h @@ -20,7 +20,7 @@ struct IrFunction; void updateUseCounts(IrFunction& function); -void updateLastUseLocations(IrFunction& function); +void updateLastUseLocations(IrFunction& function, const std::vector& sortedBlocks); uint32_t getNextInstUse(IrFunction& function, uint32_t targetInstIdx, uint32_t startInstIdx); diff --git a/CodeGen/include/Luau/IrUtils.h b/CodeGen/include/Luau/IrUtils.h index 50b3dc7cb..d513f1b4d 100644 --- a/CodeGen/include/Luau/IrUtils.h +++ b/CodeGen/include/Luau/IrUtils.h @@ -271,7 +271,7 @@ std::vector getSortedBlockOrder(IrFunction& function); // Returns first non-dead block that comes after block at index 'i' in the sorted blocks array // 'dummy' block is returned if the end of array was reached -IrBlock& getNextBlock(IrFunction& function, std::vector& sortedBlocks, IrBlock& dummy, size_t i); +IrBlock& getNextBlock(IrFunction& function, const std::vector& sortedBlocks, IrBlock& dummy, size_t i); } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/src/CodeGenLower.h b/CodeGen/src/CodeGenLower.h index e8eaa502c..fb10799b8 100644 --- a/CodeGen/src/CodeGenLower.h +++ b/CodeGen/src/CodeGenLower.h @@ -48,10 +48,9 @@ inline void gatherFunctions(std::vector& results, Proto* proto, unsigned } template -inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& function, int bytecodeid, AssemblyOptions options) +inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& function, const std::vector& sortedBlocks, int bytecodeid, + AssemblyOptions options) { - std::vector sortedBlocks = getSortedBlockOrder(function); - // For each IR instruction that begins a bytecode instruction, which bytecode instruction is it? std::vector bcLocations(function.instructions.size() + 1, ~0u); @@ -202,22 +201,22 @@ inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& return true; } -inline bool lowerIr( - X64::AssemblyBuilderX64& build, IrBuilder& ir, ModuleHelpers& helpers, Proto* proto, AssemblyOptions options, LoweringStats* stats) +inline bool lowerIr(X64::AssemblyBuilderX64& build, IrBuilder& ir, const std::vector& sortedBlocks, ModuleHelpers& helpers, Proto* proto, + AssemblyOptions options, LoweringStats* stats) { optimizeMemoryOperandsX64(ir.function); X64::IrLoweringX64 lowering(build, helpers, ir.function, stats); - return lowerImpl(build, lowering, ir.function, proto->bytecodeid, options); + return lowerImpl(build, lowering, ir.function, sortedBlocks, proto->bytecodeid, options); } -inline bool lowerIr( - A64::AssemblyBuilderA64& build, IrBuilder& ir, ModuleHelpers& helpers, Proto* proto, AssemblyOptions options, LoweringStats* stats) +inline bool lowerIr(A64::AssemblyBuilderA64& build, IrBuilder& ir, const std::vector& sortedBlocks, ModuleHelpers& helpers, Proto* proto, + AssemblyOptions options, LoweringStats* stats) { A64::IrLoweringA64 lowering(build, helpers, ir.function, stats); - return lowerImpl(build, lowering, ir.function, proto->bytecodeid, options); + return lowerImpl(build, lowering, ir.function, sortedBlocks, proto->bytecodeid, options); } template @@ -237,7 +236,12 @@ inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers& createLinearBlocks(ir, useValueNumbering); } - return lowerIr(build, ir, helpers, proto, options, stats); + std::vector sortedBlocks = getSortedBlockOrder(ir.function); + + // In order to allocate registers during lowering, we need to know where instruction results are last used + updateLastUseLocations(ir.function, sortedBlocks); + + return lowerIr(build, ir, sortedBlocks, helpers, proto, options, stats); } } // namespace CodeGen diff --git a/CodeGen/src/IrAnalysis.cpp b/CodeGen/src/IrAnalysis.cpp index 57cb22b29..63f48ed4c 100644 --- a/CodeGen/src/IrAnalysis.cpp +++ b/CodeGen/src/IrAnalysis.cpp @@ -54,31 +54,47 @@ void updateUseCounts(IrFunction& function) } } -void updateLastUseLocations(IrFunction& function) +void updateLastUseLocations(IrFunction& function, const std::vector& sortedBlocks) { std::vector& instructions = function.instructions; +#if defined(LUAU_ASSERTENABLED) + // Last use assignements should be called only once for (IrInst& inst : instructions) - inst.lastUse = 0; + LUAU_ASSERT(inst.lastUse == 0); +#endif - for (size_t instIdx = 0; instIdx < instructions.size(); ++instIdx) + for (size_t i = 0; i < sortedBlocks.size(); ++i) { - IrInst& inst = instructions[instIdx]; + uint32_t blockIndex = sortedBlocks[i]; + IrBlock& block = function.blocks[blockIndex]; - auto checkOp = [&](IrOp op) { - if (op.kind == IrOpKind::Inst) - instructions[op.index].lastUse = uint32_t(instIdx); - }; - - if (isPseudo(inst.cmd)) + if (block.kind == IrBlockKind::Dead) continue; - checkOp(inst.a); - checkOp(inst.b); - checkOp(inst.c); - checkOp(inst.d); - checkOp(inst.e); - checkOp(inst.f); + LUAU_ASSERT(block.start != ~0u); + LUAU_ASSERT(block.finish != ~0u); + + for (uint32_t instIdx = block.start; instIdx <= block.finish; instIdx++) + { + LUAU_ASSERT(instIdx < function.instructions.size()); + IrInst& inst = instructions[instIdx]; + + auto checkOp = [&](IrOp op) { + if (op.kind == IrOpKind::Inst) + instructions[op.index].lastUse = uint32_t(instIdx); + }; + + if (isPseudo(inst.cmd)) + continue; + + checkOp(inst.a); + checkOp(inst.b); + checkOp(inst.c); + checkOp(inst.d); + checkOp(inst.e); + checkOp(inst.f); + } } } diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 68378caa5..76f326cf2 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -249,9 +249,6 @@ IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, , valueTracker(function) , exitHandlerMap(~0u) { - // In order to allocate registers during lowering, we need to know where instruction results are last used - updateLastUseLocations(function); - valueTracker.setRestoreCallack(this, [](void* context, IrInst& inst) { IrLoweringA64* self = static_cast(context); self->regs.restoreReg(self->build, inst); @@ -2167,6 +2164,8 @@ AddressA64 IrLoweringA64::tempAddr(IrOp op, int offset) { // This is needed to tighten the bounds checks in the VmConst case below LUAU_ASSERT(offset % 4 == 0); + // Full encoded range is wider depending on the load size, but this assertion helps establish a smaller guaranteed working range [0..4096) + LUAU_ASSERT(offset >= 0 && unsigned(offset / 4) <= AssemblyBuilderA64::kMaxImmediate); if (op.kind == IrOpKind::VmReg) return mem(rBase, vmRegOp(op) * sizeof(TValue) + offset); diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index a9bbd8831..393475721 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -31,9 +31,6 @@ IrLoweringX64::IrLoweringX64(AssemblyBuilderX64& build, ModuleHelpers& helpers, , valueTracker(function) , exitHandlerMap(~0u) { - // In order to allocate registers during lowering, we need to know where instruction results are last used - updateLastUseLocations(function); - valueTracker.setRestoreCallack(®s, [](void* context, IrInst& inst) { ((IrRegAllocX64*)context)->restore(inst, false); }); diff --git a/CodeGen/src/IrUtils.cpp b/CodeGen/src/IrUtils.cpp index 687b0c2a1..7041c6a67 100644 --- a/CodeGen/src/IrUtils.cpp +++ b/CodeGen/src/IrUtils.cpp @@ -886,7 +886,7 @@ std::vector getSortedBlockOrder(IrFunction& function) return sortedBlocks; } -IrBlock& getNextBlock(IrFunction& function, std::vector& sortedBlocks, IrBlock& dummy, size_t i) +IrBlock& getNextBlock(IrFunction& function, const std::vector& sortedBlocks, IrBlock& dummy, size_t i) { for (size_t j = i + 1; j < sortedBlocks.size(); ++j) { diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index ef5c2369f..abf61a497 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -30,7 +30,7 @@ LUAU_FASTFLAGVARIABLE(LuauCompileFenvNoBuiltinFold, false) LUAU_FASTFLAGVARIABLE(LuauCompileTopCold, false) LUAU_FASTFLAG(LuauFloorDivision) -LUAU_FASTFLAGVARIABLE(LuauCompileFixContinueValidation, false) +LUAU_FASTFLAGVARIABLE(LuauCompileFixContinueValidation2, false) LUAU_FASTFLAGVARIABLE(LuauCompileContinueCloseUpvals, false) @@ -276,7 +276,7 @@ struct Compiler f.upvals = upvals; // record information for inlining - if (options.optimizationLevel >= 2 && !func->vararg && !getfenvUsed && !setfenvUsed) + if (options.optimizationLevel >= 2 && !func->vararg && !func->self && !getfenvUsed && !setfenvUsed) { f.canInline = true; f.stackSize = stackSize; @@ -665,14 +665,6 @@ struct Compiler else { locstants[arg.local] = arg.value; - - if (FFlag::LuauCompileFixContinueValidation) - { - // Mark that optimization skipped allocation of this local - Local& l = locals[arg.local]; - LUAU_ASSERT(!l.skipped); - l.skipped = true; - } } } @@ -721,23 +713,8 @@ struct Compiler { AstLocal* local = func->args.data[i]; - if (FFlag::LuauCompileFixContinueValidation) - { - if (Constant* var = locstants.find(local); var && var->type != Constant::Type_Unknown) - { - var->type = Constant::Type_Unknown; - - // Restore local allocation skip flag as well - Local& l = locals[local]; - LUAU_ASSERT(l.skipped); - l.skipped = false; - } - } - else - { - if (Constant* var = locstants.find(local)) - var->type = Constant::Type_Unknown; - } + if (Constant* var = locstants.find(local)) + var->type = Constant::Type_Unknown; } foldConstants(constants, variables, locstants, builtinsFold, builtinsFoldMathK, func->body); @@ -2510,12 +2487,18 @@ struct Compiler return; } - AstStat* continueStatement = extractStatContinue(stat->thenbody); + AstStatContinue* continueStatement = extractStatContinue(stat->thenbody); // Optimization: body is a "continue" statement with no "else" => we can directly continue in "then" case if (!stat->elsebody && continueStatement != nullptr && !areLocalsCaptured(loops.back().localOffsetContinue)) { - if (loops.back().untilCondition) + if (FFlag::LuauCompileFixContinueValidation2) + { + // track continue statement for repeat..until validation (validateContinueUntil) + if (!loops.back().continueUsed) + loops.back().continueUsed = continueStatement; + } + else if (loops.back().untilCondition) validateContinueUntil(continueStatement, loops.back().untilCondition); // fallthrough = proceed with the loop body as usual @@ -2577,7 +2560,7 @@ struct Compiler size_t oldJumps = loopJumps.size(); size_t oldLocals = localStack.size(); - loops.push_back({oldLocals, oldLocals, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); hasLoops = true; size_t loopLabel = bytecode.emitLabel(); @@ -2613,7 +2596,7 @@ struct Compiler size_t oldJumps = loopJumps.size(); size_t oldLocals = localStack.size(); - loops.push_back({oldLocals, oldLocals, stat->condition}); + loops.push_back({oldLocals, oldLocals, stat->condition, nullptr}); hasLoops = true; size_t loopLabel = bytecode.emitLabel(); @@ -2624,6 +2607,8 @@ struct Compiler RegScope rs(this); + bool continueValidated = false; + for (size_t i = 0; i < body->body.size; ++i) { compileStat(body->body.data[i]); @@ -2634,6 +2619,14 @@ struct Compiler // expression that continue will jump to. if (FFlag::LuauCompileContinueCloseUpvals) loops.back().localOffsetContinue = localStack.size(); + + // if continue was called from this statement, then any local defined after this in the loop body should not be accessed by until condition + // it is sufficient to check this condition once, as if this holds for the first continue, it must hold for all subsequent continues. + if (FFlag::LuauCompileFixContinueValidation2 && loops.back().continueUsed && !continueValidated) + { + validateContinueUntil(loops.back().continueUsed, stat->condition, body, i + 1); + continueValidated = true; + } } size_t contLabel = bytecode.emitLabel(); @@ -2762,19 +2755,7 @@ struct Compiler { // Optimization: we don't need to allocate and assign const locals, since their uses will be constant-folded if (options.optimizationLevel >= 1 && options.debugLevel <= 1 && areLocalsRedundant(stat)) - { - if (FFlag::LuauCompileFixContinueValidation) - { - // Mark that optimization skipped allocation of this local - for (AstLocal* local : stat->vars) - { - Local& l = locals[local]; - l.skipped = true; - } - } - return; - } // Optimization: for 1-1 local assignments, we can reuse the register *if* neither local is mutated if (options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1) @@ -2863,7 +2844,7 @@ struct Compiler size_t oldLocals = localStack.size(); size_t oldJumps = loopJumps.size(); - loops.push_back({oldLocals, oldLocals, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); for (int iv = 0; iv < tripCount; ++iv) { @@ -2914,7 +2895,7 @@ struct Compiler size_t oldLocals = localStack.size(); size_t oldJumps = loopJumps.size(); - loops.push_back({oldLocals, oldLocals, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); hasLoops = true; // register layout: limit, step, index @@ -2979,7 +2960,7 @@ struct Compiler size_t oldLocals = localStack.size(); size_t oldJumps = loopJumps.size(); - loops.push_back({oldLocals, oldLocals, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); hasLoops = true; // register layout: generator, state, index, variables... @@ -3391,7 +3372,13 @@ struct Compiler { LUAU_ASSERT(!loops.empty()); - if (loops.back().untilCondition) + if (FFlag::LuauCompileFixContinueValidation2) + { + // track continue statement for repeat..until validation (validateContinueUntil) + if (!loops.back().continueUsed) + loops.back().continueUsed = stat; + } + else if (loops.back().untilCondition) validateContinueUntil(stat, loops.back().untilCondition); // before continuing, we need to close all local variables that were captured in closures since loop start @@ -3477,6 +3464,7 @@ struct Compiler void validateContinueUntil(AstStat* cont, AstExpr* condition) { + LUAU_ASSERT(!FFlag::LuauCompileFixContinueValidation2); UndefinedLocalVisitor visitor(this); condition->visit(&visitor); @@ -3486,6 +3474,32 @@ struct Compiler visitor.undef->name.value, cont->location.begin.line + 1); } + void validateContinueUntil(AstStat* cont, AstExpr* condition, AstStatBlock* body, size_t start) + { + LUAU_ASSERT(FFlag::LuauCompileFixContinueValidation2); + UndefinedLocalVisitor visitor(this); + + for (size_t i = start; i < body->body.size; ++i) + { + if (AstStatLocal* stat = body->body.data[i]->as()) + { + for (AstLocal* local : stat->vars) + visitor.locals.insert(local); + } + else if (AstStatLocalFunction* stat = body->body.data[i]->as()) + { + visitor.locals.insert(stat->name); + } + } + + condition->visit(&visitor); + + if (visitor.undef) + CompileError::raise(condition->location, + "Local %s used in the repeat..until condition is undefined because continue statement on line %d jumps over it", + visitor.undef->name.value, cont->location.begin.line + 1); + } + void gatherConstUpvals(AstExprFunction* func) { ConstUpvalueVisitor visitor(this); @@ -3702,18 +3716,24 @@ struct Compiler UndefinedLocalVisitor(Compiler* self) : self(self) , undef(nullptr) + , locals(nullptr) { } void check(AstLocal* local) { - Local& l = self->locals[local]; - - if (FFlag::LuauCompileFixContinueValidation && l.skipped) - return; + if (FFlag::LuauCompileFixContinueValidation2) + { + if (!undef && locals.contains(local)) + undef = local; + } + else + { + Local& l = self->locals[local]; - if (!l.allocated && !undef) - undef = local; + if (!l.allocated && !undef) + undef = local; + } } bool visit(AstExprLocal* node) override @@ -3742,6 +3762,7 @@ struct Compiler Compiler* self; AstLocal* undef; + DenseHashSet locals; }; struct ConstUpvalueVisitor : AstVisitor @@ -3837,7 +3858,6 @@ struct Compiler uint8_t reg = 0; bool allocated = false; bool captured = false; - bool skipped = false; uint32_t debugpc = 0; }; @@ -3858,7 +3878,10 @@ struct Compiler size_t localOffset; size_t localOffsetContinue; + // TODO: Remove with LuauCompileFixContinueValidation2 AstExpr* untilCondition; + + AstStatContinue* continueUsed; }; struct InlineArg diff --git a/Sources.cmake b/Sources.cmake index ebd73c4fb..e4f0ef323 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -196,6 +196,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Transpiler.h Analysis/include/Luau/TxnLog.h Analysis/include/Luau/Type.h + Analysis/include/Luau/TypePairHash.h Analysis/include/Luau/TypeArena.h Analysis/include/Luau/TypeAttach.h Analysis/include/Luau/TypeChecker2.h @@ -441,6 +442,7 @@ if(TARGET Luau.UnitTest) tests/TypeInfer.test.cpp tests/TypeInfer.tryUnify.test.cpp tests/TypeInfer.typePacks.cpp + tests/TypeInfer.typestates.test.cpp tests/TypeInfer.unionTypes.test.cpp tests/TypeInfer.unknownnever.test.cpp tests/TypePack.test.cpp diff --git a/tests/AstJsonEncoder.test.cpp b/tests/AstJsonEncoder.test.cpp index a264d0e7f..5270bf9b7 100644 --- a/tests/AstJsonEncoder.test.cpp +++ b/tests/AstJsonEncoder.test.cpp @@ -16,6 +16,8 @@ struct JsonEncoderFixture Allocator allocator; AstNameTable names{allocator}; + ScopedFastFlag sff{"LuauClipExtraHasEndProps", true}; + ParseResult parse(std::string_view src) { ParseOptions opts; @@ -91,6 +93,8 @@ TEST_CASE("basic_escaping") TEST_CASE("encode_AstStatBlock") { + ScopedFastFlag sff{"LuauClipExtraHasEndProps", true}; + AstLocal astlocal{AstName{"a_local"}, Location(), nullptr, 0, 0, nullptr}; AstLocal* astlocalarray[] = {&astlocal}; @@ -103,9 +107,9 @@ TEST_CASE("encode_AstStatBlock") AstStatBlock block{Location(), bodyArray}; - CHECK_EQ( - (R"({"type":"AstStatBlock","location":"0,0 - 0,0","body":[{"type":"AstStatLocal","location":"0,0 - 0,0","vars":[{"luauType":null,"name":"a_local","type":"AstLocal","location":"0,0 - 0,0"}],"values":[]}]})"), - toJson(&block)); + CHECK( + toJson(&block) == + (R"({"type":"AstStatBlock","location":"0,0 - 0,0","hasEnd":true,"body":[{"type":"AstStatLocal","location":"0,0 - 0,0","vars":[{"luauType":null,"name":"a_local","type":"AstLocal","location":"0,0 - 0,0"}],"values":[]}]})")); } TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_tables") @@ -123,7 +127,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_tables") CHECK( json == - R"({"type":"AstStatBlock","location":"0,0 - 6,4","body":[{"type":"AstStatLocal","location":"1,8 - 5,9","vars":[{"luauType":{"type":"AstTypeTable","location":"1,17 - 3,9","props":[{"name":"foo","type":"AstTableProp","location":"2,12 - 2,15","propType":{"type":"AstTypeReference","location":"2,17 - 2,23","name":"number","nameLocation":"2,17 - 2,23","parameters":[]}}],"indexer":null},"name":"x","type":"AstLocal","location":"1,14 - 1,15"}],"values":[{"type":"AstExprTable","location":"3,12 - 5,9","items":[{"type":"AstExprTableItem","kind":"record","key":{"type":"AstExprConstantString","location":"4,12 - 4,15","value":"foo"},"value":{"type":"AstExprConstantNumber","location":"4,18 - 4,21","value":123}}]}]}]})"); + R"({"type":"AstStatBlock","location":"0,0 - 6,4","hasEnd":true,"body":[{"type":"AstStatLocal","location":"1,8 - 5,9","vars":[{"luauType":{"type":"AstTypeTable","location":"1,17 - 3,9","props":[{"name":"foo","type":"AstTableProp","location":"2,12 - 2,15","propType":{"type":"AstTypeReference","location":"2,17 - 2,23","name":"number","nameLocation":"2,17 - 2,23","parameters":[]}}],"indexer":null},"name":"x","type":"AstLocal","location":"1,14 - 1,15"}],"values":[{"type":"AstExprTable","location":"3,12 - 5,9","items":[{"type":"AstExprTableItem","kind":"record","key":{"type":"AstExprConstantString","location":"4,12 - 4,15","value":"foo"},"value":{"type":"AstExprConstantNumber","location":"4,18 - 4,21","value":123}}]}]}]})"); } TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_array") @@ -135,7 +139,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_array") CHECK( json == - R"({"type":"AstStatBlock","location":"0,0 - 0,17","body":[{"type":"AstStatTypeAlias","location":"0,0 - 0,17","name":"X","generics":[],"genericPacks":[],"type":{"type":"AstTypeTable","location":"0,9 - 0,17","props":[],"indexer":{"location":"0,10 - 0,16","indexType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"number","nameLocation":"0,10 - 0,16","parameters":[]},"resultType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"string","nameLocation":"0,10 - 0,16","parameters":[]}}},"exported":false}]})"); + R"({"type":"AstStatBlock","location":"0,0 - 0,17","hasEnd":true,"body":[{"type":"AstStatTypeAlias","location":"0,0 - 0,17","name":"X","generics":[],"genericPacks":[],"type":{"type":"AstTypeTable","location":"0,9 - 0,17","props":[],"indexer":{"location":"0,10 - 0,16","indexType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"number","nameLocation":"0,10 - 0,16","parameters":[]},"resultType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"string","nameLocation":"0,10 - 0,16","parameters":[]}}},"exported":false}]})"); } TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_indexer") @@ -147,7 +151,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_indexer") CHECK( json == - R"({"type":"AstStatBlock","location":"0,0 - 0,17","body":[{"type":"AstStatTypeAlias","location":"0,0 - 0,17","name":"X","generics":[],"genericPacks":[],"type":{"type":"AstTypeTable","location":"0,9 - 0,17","props":[],"indexer":{"location":"0,10 - 0,16","indexType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"number","nameLocation":"0,10 - 0,16","parameters":[]},"resultType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"string","nameLocation":"0,10 - 0,16","parameters":[]}}},"exported":false}]})"); + R"({"type":"AstStatBlock","location":"0,0 - 0,17","hasEnd":true,"body":[{"type":"AstStatTypeAlias","location":"0,0 - 0,17","name":"X","generics":[],"genericPacks":[],"type":{"type":"AstTypeTable","location":"0,9 - 0,17","props":[],"indexer":{"location":"0,10 - 0,16","indexType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"number","nameLocation":"0,10 - 0,16","parameters":[]},"resultType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"string","nameLocation":"0,10 - 0,16","parameters":[]}}},"exported":false}]})"); } TEST_CASE("encode_AstExprGroup") @@ -243,7 +247,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprFunction") AstExpr* expr = expectParseExpr("function (a) return a end"); std::string_view expected = - R"({"type":"AstExprFunction","location":"0,4 - 0,29","generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"a","type":"AstLocal","location":"0,14 - 0,15"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,16 - 0,26","body":[{"type":"AstStatReturn","location":"0,17 - 0,25","list":[{"type":"AstExprLocal","location":"0,24 - 0,25","local":{"luauType":null,"name":"a","type":"AstLocal","location":"0,14 - 0,15"}}]}]},"functionDepth":1,"debugname":"","hasEnd":true})"; + R"({"type":"AstExprFunction","location":"0,4 - 0,29","generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"a","type":"AstLocal","location":"0,14 - 0,15"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,16 - 0,26","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,17 - 0,25","list":[{"type":"AstExprLocal","location":"0,24 - 0,25","local":{"luauType":null,"name":"a","type":"AstLocal","location":"0,14 - 0,15"}}]}]},"functionDepth":1,"debugname":""})"; CHECK(toJson(expr) == expected); } @@ -311,7 +315,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatIf") AstStat* statement = expectParseStatement("if true then else end"); std::string_view expected = - R"({"type":"AstStatIf","location":"0,0 - 0,21","condition":{"type":"AstExprConstantBool","location":"0,3 - 0,7","value":true},"thenbody":{"type":"AstStatBlock","location":"0,12 - 0,13","body":[]},"elsebody":{"type":"AstStatBlock","location":"0,17 - 0,18","body":[]},"hasThen":true,"hasEnd":true})"; + R"({"type":"AstStatIf","location":"0,0 - 0,21","condition":{"type":"AstExprConstantBool","location":"0,3 - 0,7","value":true},"thenbody":{"type":"AstStatBlock","location":"0,12 - 0,13","hasEnd":true,"body":[]},"elsebody":{"type":"AstStatBlock","location":"0,17 - 0,18","hasEnd":true,"body":[]},"hasThen":true})"; CHECK(toJson(statement) == expected); } @@ -321,7 +325,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatWhile") AstStat* statement = expectParseStatement("while true do end"); std::string_view expected = - R"({"type":"AstStatWhile","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,14","body":[]},"hasDo":true,"hasEnd":true})"; + R"({"type":"AstStatWhile","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,14","hasEnd":true,"body":[]},"hasDo":true})"; CHECK(toJson(statement) == expected); } @@ -331,7 +335,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatRepeat") AstStat* statement = expectParseStatement("repeat until true"); std::string_view expected = - R"({"type":"AstStatRepeat","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,13 - 0,17","value":true},"body":{"type":"AstStatBlock","location":"0,6 - 0,7","body":[]},"hasUntil":true})"; + R"({"type":"AstStatRepeat","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,13 - 0,17","value":true},"body":{"type":"AstStatBlock","location":"0,6 - 0,7","hasEnd":true,"body":[]}})"; CHECK(toJson(statement) == expected); } @@ -341,7 +345,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatBreak") AstStat* statement = expectParseStatement("while true do break end"); std::string_view expected = - R"({"type":"AstStatWhile","location":"0,0 - 0,23","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,20","body":[{"type":"AstStatBreak","location":"0,14 - 0,19"}]},"hasDo":true,"hasEnd":true})"; + R"({"type":"AstStatWhile","location":"0,0 - 0,23","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,20","hasEnd":true,"body":[{"type":"AstStatBreak","location":"0,14 - 0,19"}]},"hasDo":true})"; CHECK(toJson(statement) == expected); } @@ -351,7 +355,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatContinue") AstStat* statement = expectParseStatement("while true do continue end"); std::string_view expected = - R"({"type":"AstStatWhile","location":"0,0 - 0,26","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,23","body":[{"type":"AstStatContinue","location":"0,14 - 0,22"}]},"hasDo":true,"hasEnd":true})"; + R"({"type":"AstStatWhile","location":"0,0 - 0,26","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,23","hasEnd":true,"body":[{"type":"AstStatContinue","location":"0,14 - 0,22"}]},"hasDo":true})"; CHECK(toJson(statement) == expected); } @@ -361,7 +365,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatFor") AstStat* statement = expectParseStatement("for a=0,1 do end"); std::string_view expected = - R"({"type":"AstStatFor","location":"0,0 - 0,16","var":{"luauType":null,"name":"a","type":"AstLocal","location":"0,4 - 0,5"},"from":{"type":"AstExprConstantNumber","location":"0,6 - 0,7","value":0},"to":{"type":"AstExprConstantNumber","location":"0,8 - 0,9","value":1},"body":{"type":"AstStatBlock","location":"0,12 - 0,13","body":[]},"hasDo":true,"hasEnd":true})"; + R"({"type":"AstStatFor","location":"0,0 - 0,16","var":{"luauType":null,"name":"a","type":"AstLocal","location":"0,4 - 0,5"},"from":{"type":"AstExprConstantNumber","location":"0,6 - 0,7","value":0},"to":{"type":"AstExprConstantNumber","location":"0,8 - 0,9","value":1},"body":{"type":"AstStatBlock","location":"0,12 - 0,13","hasEnd":true,"body":[]},"hasDo":true})"; CHECK(toJson(statement) == expected); } @@ -371,7 +375,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatForIn") AstStat* statement = expectParseStatement("for a in b do end"); std::string_view expected = - R"({"type":"AstStatForIn","location":"0,0 - 0,17","vars":[{"luauType":null,"name":"a","type":"AstLocal","location":"0,4 - 0,5"}],"values":[{"type":"AstExprGlobal","location":"0,9 - 0,10","global":"b"}],"body":{"type":"AstStatBlock","location":"0,13 - 0,14","body":[]},"hasIn":true,"hasDo":true,"hasEnd":true})"; + R"({"type":"AstStatForIn","location":"0,0 - 0,17","vars":[{"luauType":null,"name":"a","type":"AstLocal","location":"0,4 - 0,5"}],"values":[{"type":"AstExprGlobal","location":"0,9 - 0,10","global":"b"}],"body":{"type":"AstStatBlock","location":"0,13 - 0,14","hasEnd":true,"body":[]},"hasIn":true,"hasDo":true})"; CHECK(toJson(statement) == expected); } @@ -391,7 +395,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatLocalFunction") AstStat* statement = expectParseStatement("local function a(b) return end"); std::string_view expected = - R"({"type":"AstStatLocalFunction","location":"0,0 - 0,30","name":{"luauType":null,"name":"a","type":"AstLocal","location":"0,15 - 0,16"},"func":{"type":"AstExprFunction","location":"0,0 - 0,30","generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,17 - 0,18"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,19 - 0,27","body":[{"type":"AstStatReturn","location":"0,20 - 0,26","list":[]}]},"functionDepth":1,"debugname":"a","hasEnd":true}})"; + R"({"type":"AstStatLocalFunction","location":"0,0 - 0,30","name":{"luauType":null,"name":"a","type":"AstLocal","location":"0,15 - 0,16"},"func":{"type":"AstExprFunction","location":"0,0 - 0,30","generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,17 - 0,18"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,19 - 0,27","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,20 - 0,26","list":[]}]},"functionDepth":1,"debugname":"a"}})"; CHECK(toJson(statement) == expected); } diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index 68f2a9a01..ecd4847bd 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -1911,7 +1911,7 @@ RETURN R0 0 TEST_CASE("LoopContinueIgnoresImplicitConstant") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true}; + ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; // this used to crash the compiler :( CHECK_EQ("\n" + compileFunction0(R"( @@ -1928,7 +1928,7 @@ RETURN R0 0 TEST_CASE("LoopContinueIgnoresExplicitConstant") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true}; + ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; // Constants do not allocate locals and 'continue' validation should skip them if their lifetime already started CHECK_EQ("\n" + compileFunction0(R"( @@ -1945,7 +1945,7 @@ RETURN R0 0 TEST_CASE("LoopContinueRespectsExplicitConstant") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true}; + ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; // If local lifetime hasn't started, even if it's a constant that will not receive an allocation, it cannot be jumped over try @@ -1971,7 +1971,7 @@ until c TEST_CASE("LoopContinueIgnoresImplicitConstantAfterInline") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true}; + ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; // Inlining might also replace some locals with constants instead of allocating them CHECK_EQ("\n" + compileFunction(R"( @@ -1994,6 +1994,36 @@ RETURN R0 0 )"); } +TEST_CASE("LoopContinueCorrectlyHandlesImplicitConstantAfterUnroll") +{ + ScopedFastFlag sff{"LuauCompileFixContinueValidation2", true}; + ScopedFastInt sfi("LuauCompileLoopUnrollThreshold", 200); + + // access to implicit constant that depends on the unrolled loop constant is still invalid even though we can constant-propagate it + try + { + compileFunction(R"( +for i = 1, 2 do + s() + repeat + if i == 2 then + continue + end + local x = i == 1 or a + until f(x) +end +)", 0, 2); + + CHECK(!"Expected CompileError"); + } + catch (Luau::CompileError& e) + { + CHECK_EQ(e.getLocation().begin.line + 1, 9); + CHECK_EQ( + std::string(e.what()), "Local x used in the repeat..until condition is undefined because continue statement on line 6 jumps over it"); + } +} + TEST_CASE("LoopContinueUntilCapture") { ScopedFastFlag sff("LuauCompileContinueCloseUpvals", true); diff --git a/tests/Differ.test.cpp b/tests/Differ.test.cpp index 287f44c8c..7335e9440 100644 --- a/tests/Differ.test.cpp +++ b/tests/Differ.test.cpp @@ -757,8 +757,8 @@ TEST_CASE_FIXTURE(DifferFixture, "singleton_string") TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "negation") { - // Old solver does not correctly refine test types - ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; CheckResult result = check(R"( local bar: { x: { y: unknown }} diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index f31727e0a..8f40f5741 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -174,7 +174,8 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars { if (FFlag::DebugLuauDeferredConstraintResolution) { - ModulePtr module = Luau::check(*sourceModule, {}, builtinTypes, NotNull{&ice}, NotNull{&moduleResolver}, NotNull{&fileResolver}, + Mode mode = sourceModule->mode ? *sourceModule->mode : Mode::Strict; + ModulePtr module = Luau::check(*sourceModule, mode, {}, builtinTypes, NotNull{&ice}, NotNull{&moduleResolver}, NotNull{&fileResolver}, frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, frontend.options, {}); Luau::lint(sourceModule->root, *sourceModule->names, frontend.globals.globalScope, module.get(), sourceModule->hotcomments, {}); @@ -194,7 +195,7 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars return result.root; } -CheckResult Fixture::check(Mode mode, std::string source) +CheckResult Fixture::check(Mode mode, const std::string& source) { ModuleName mm = fromString(mainModuleName); configResolver.defaultConfig.mode = mode; diff --git a/tests/Fixture.h b/tests/Fixture.h index 8d9971400..fa3dfb067 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -65,7 +65,7 @@ struct Fixture // Throws Luau::ParseErrors if the parse fails. AstStatBlock* parse(const std::string& source, const ParseOptions& parseOptions = {}); - CheckResult check(Mode mode, std::string source); + CheckResult check(Mode mode, const std::string& source); CheckResult check(const std::string& source); LintResult lint(const std::string& source, const std::optional& lintOptions = {}); diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index 6c728da9b..9907d7a16 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -1502,6 +1502,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiUntyped") } LintResult result = lint(R"( +-- TODO return function () print(table.getn({})) table.foreach({}, function() end) @@ -1514,6 +1515,35 @@ end CHECK_EQ(result.warnings[1].text, "Member 'table.foreach' is deprecated"); } +TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiFenv") +{ + ScopedFastFlag sff("LuauLintDeprecatedFenv", true); + + LintResult result = lint(R"( +local f, g, h = ... + +getfenv(1) +getfenv(f :: () -> ()) +getfenv(g :: number) +getfenv(h :: any) + +setfenv(1, {}) +setfenv(f :: () -> (), {}) +setfenv(g :: number, {}) +setfenv(h :: any, {}) +)"); + + REQUIRE(4 == result.warnings.size()); + CHECK_EQ(result.warnings[0].text, "Function 'getfenv' is deprecated; consider using 'debug.info' instead"); + CHECK_EQ(result.warnings[0].location.begin.line + 1, 4); + CHECK_EQ(result.warnings[1].text, "Function 'getfenv' is deprecated; consider using 'debug.info' instead"); + CHECK_EQ(result.warnings[1].location.begin.line + 1, 6); + CHECK_EQ(result.warnings[2].text, "Function 'setfenv' is deprecated"); + CHECK_EQ(result.warnings[2].location.begin.line + 1, 9); + CHECK_EQ(result.warnings[3].text, "Function 'setfenv' is deprecated"); + CHECK_EQ(result.warnings[3].location.begin.line + 1, 11); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperations") { LintResult result = lint(R"( @@ -1559,6 +1589,62 @@ table.create(42, {} :: {}) result.warnings[9].text, "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"); } +TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperationsIndexer") +{ + ScopedFastFlag sff("LuauLintTableIndexer", true); + + LintResult result = lint(R"( +local t1 = {} -- ok: empty +local t2 = {1, 2} -- ok: array +local t3 = { a = 1, b = 2 } -- not ok: dictionary +local t4: {[number]: number} = {} -- ok: array +local t5: {[string]: number} = {} -- not ok: dictionary +local t6: typeof(setmetatable({1, 2}, {})) = {} -- ok: table with metatable +local t7: string = "hello" -- ok: string +local t8: {number} | {n: number} = {} -- ok: union + +-- not ok +print(#t3) +print(#t5) +ipairs(t5) + +-- disabled +-- ipairs(t3) adds indexer to t3, silencing error on #t3 + +-- ok +print(#t1) +print(#t2) +print(#t4) +print(#t6) +print(#t7) +print(#t8) + +ipairs(t1) +ipairs(t2) +ipairs(t4) +ipairs(t6) +ipairs(t7) +ipairs(t8) + +-- ok, subtle: text is a string here implicitly, but the type annotation isn't available +-- type checker assigns a type of generic table with the 'sub' member; we don't emit warnings on generic tables +-- to avoid generating a false positive here +function _impliedstring(element, text) + for i = 1, #text do + element:sendText(text:sub(i, i)) + end +end +)"); + + REQUIRE(3 == result.warnings.size()); + CHECK_EQ(result.warnings[0].location.begin.line + 1, 12); + CHECK_EQ(result.warnings[0].text, "Using '#' on a table without an array part is likely a bug"); + CHECK_EQ(result.warnings[1].location.begin.line + 1, 13); + CHECK_EQ(result.warnings[1].text, "Using '#' on a table with string keys is likely a bug"); + CHECK_EQ(result.warnings[2].location.begin.line + 1, 14); + CHECK_EQ(result.warnings[2].text, "Using 'ipairs' on a table with string keys is likely a bug"); +} + TEST_CASE_FIXTURE(Fixture, "DuplicateConditions") { LintResult result = lint(R"( diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index 00fd8ed15..eecb55bf5 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -15,102 +15,32 @@ using namespace Luau; struct NonStrictTypeCheckerFixture : Fixture { - ParseResult parse(std::string source) + CheckResult checkNonStrict(const std::string& code) { - ParseOptions opts; - opts.allowDeclarationSyntax = true; - ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true}; - return tryParse(source, opts); - } -}; - - -TEST_SUITE_BEGIN("NonStrictTypeCheckerTest"); + ScopedFastFlag flags[] = { + {"LuauCheckedFunctionSyntax", true}, + }; -TEST_CASE_FIXTURE(Fixture, "basic") -{ - Luau::checkNonStrict(builtinTypes, nullptr); -} + return check(Mode::Nonstrict, code); + } -TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_top_level_checked_fn") -{ - std::string src = R"BUILTIN_SRC( + std::string definitions = R"BUILTIN_SRC( declare function @checked abs(n: number): number +abs("hi") )BUILTIN_SRC"; +}; - ParseResult pr = parse(src); - LUAU_ASSERT(pr.errors.size() == 0); - - LUAU_ASSERT(pr.root->body.size == 1); - AstStat* root = *(pr.root->body.data); - auto func = root->as(); - LUAU_ASSERT(func); - LUAU_ASSERT(func->checkedFunction); -} - -TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_declared_table_checked_member") -{ - std::string src = R"BUILTIN_SRC( - declare math : { - abs : @checked (number) -> number -} -)BUILTIN_SRC"; - - ParseResult pr = parse(src); - LUAU_ASSERT(pr.errors.size() == 0); - - LUAU_ASSERT(pr.root->body.size == 1); - AstStat* root = *(pr.root->body.data); - auto glob = root->as(); - LUAU_ASSERT(glob); - auto tbl = glob->type->as(); - LUAU_ASSERT(tbl); - LUAU_ASSERT(tbl->props.size == 1); - auto prop = *tbl->props.data; - auto func = prop.type->as(); - LUAU_ASSERT(func); - LUAU_ASSERT(func->checkedFunction); -} - -TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_checked_outside_decl_fails") -{ - auto src = R"( - local @checked = 3 -)"; - - ParseResult pr = parse(src); - LUAU_ASSERT(pr.errors.size() > 0); - auto ts = pr.errors[1].getMessage(); -} - -TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_checked_in_and_out_of_decl_fails") -{ - auto src = R"( - local @checked = 3 - declare function @checked abs(n: number): number -)"; - auto pr = parse(src); - LUAU_ASSERT(pr.errors.size() == 2); - LUAU_ASSERT(pr.errors[0].getLocation().begin.line == 1); - LUAU_ASSERT(pr.errors[1].getLocation().begin.line == 1); -} -TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_checked_as_function_name_fails") -{ - auto pr = parse(R"( - function @checked(x: number) : number - end -)"); - LUAU_ASSERT(pr.errors.size() > 0); -} +TEST_SUITE_BEGIN("NonStrictTypeCheckerTest"); -TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "cannot_use_@_as_variable_name") +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "simple_non_strict") { - auto pr = parse(R"( - local @blah = 3 -)"); + auto res = checkNonStrict(R"BUILTIN_SRC( +declare function @checked abs(n: number): number +abs("hi") +)BUILTIN_SRC"); - LUAU_ASSERT(pr.errors.size() > 0); + // LUAU_REQUIRE_ERRORS(res); } TEST_SUITE_END(); diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index 2492c4a0b..a900b7ab8 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -3006,4 +3006,110 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_with_lookahead_involved2") REQUIRE_MESSAGE(result.errors.empty(), result.errors[0].getMessage()); } +TEST_CASE_FIXTURE(Fixture, "parse_top_level_checked_fn") +{ + ParseOptions opts; + opts.allowDeclarationSyntax = true; + ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true}; + + std::string src = R"BUILTIN_SRC( +declare function @checked abs(n: number): number +)BUILTIN_SRC"; + + ParseResult pr = tryParse(src, opts); + LUAU_ASSERT(pr.errors.size() == 0); + + LUAU_ASSERT(pr.root->body.size == 1); + AstStat* root = *(pr.root->body.data); + auto func = root->as(); + LUAU_ASSERT(func); + LUAU_ASSERT(func->checkedFunction); +} + +TEST_CASE_FIXTURE(Fixture, "parse_declared_table_checked_member") +{ + ParseOptions opts; + opts.allowDeclarationSyntax = true; + ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true}; + + const std::string src = R"BUILTIN_SRC( + declare math : { + abs : @checked (number) -> number +} +)BUILTIN_SRC"; + + ParseResult pr = tryParse(src, opts); + LUAU_ASSERT(pr.errors.size() == 0); + + LUAU_ASSERT(pr.root->body.size == 1); + AstStat* root = *(pr.root->body.data); + auto glob = root->as(); + LUAU_ASSERT(glob); + auto tbl = glob->type->as(); + LUAU_ASSERT(tbl); + LUAU_ASSERT(tbl->props.size == 1); + auto prop = *tbl->props.data; + auto func = prop.type->as(); + LUAU_ASSERT(func); + LUAU_ASSERT(func->checkedFunction); +} + +TEST_CASE_FIXTURE(Fixture, "parse_checked_outside_decl_fails") +{ + ParseOptions opts; + opts.allowDeclarationSyntax = true; + ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true}; + + ParseResult pr = tryParse(R"( + local @checked = 3 +)", + opts); + LUAU_ASSERT(pr.errors.size() > 0); + auto ts = pr.errors[1].getMessage(); +} + +TEST_CASE_FIXTURE(Fixture, "parse_checked_in_and_out_of_decl_fails") +{ + ParseOptions opts; + opts.allowDeclarationSyntax = true; + ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true}; + + auto pr = tryParse(R"( + local @checked = 3 + declare function @checked abs(n: number): number +)", + opts); + LUAU_ASSERT(pr.errors.size() == 2); + LUAU_ASSERT(pr.errors[0].getLocation().begin.line == 1); + LUAU_ASSERT(pr.errors[1].getLocation().begin.line == 1); +} + +TEST_CASE_FIXTURE(Fixture, "parse_checked_as_function_name_fails") +{ + ParseOptions opts; + opts.allowDeclarationSyntax = true; + ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true}; + + auto pr = tryParse(R"( + function @checked(x: number) : number + end +)", + opts); + LUAU_ASSERT(pr.errors.size() > 0); +} + +TEST_CASE_FIXTURE(Fixture, "cannot_use_@_as_variable_name") +{ + ParseOptions opts; + opts.allowDeclarationSyntax = true; + ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true}; + + auto pr = tryParse(R"( + local @blah = 3 +)", + opts); + + LUAU_ASSERT(pr.errors.size() > 0); +} + TEST_SUITE_END(); diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index a19b5f19f..ae6cd1293 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -1038,4 +1038,45 @@ TEST_IS_NOT_SUBTYPE(idx(builtinTypes->stringType, builtinTypes->numberType), tbl TEST_IS_SUBTYPE(tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}}), tbl({{"X", builtinTypes->numberType}})); TEST_IS_NOT_SUBTYPE(tbl({{"X", builtinTypes->numberType}}), tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}})); +TEST_CASE_FIXTURE(SubtypeFixture, "interior_tests_are_cached") +{ + TypeId tableA = tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}}); + TypeId tableB = tbl({{"X", builtinTypes->optionalNumberType}, {"Y", builtinTypes->optionalNumberType}}); + + CHECK_IS_NOT_SUBTYPE(tableA, tableB); + + const SubtypingResult* cachedResult = subtyping.peekCache().find({builtinTypes->numberType, builtinTypes->optionalNumberType}); + REQUIRE(cachedResult); + + CHECK(cachedResult->isSubtype); + + cachedResult = subtyping.peekCache().find({tableA, tableB}); + REQUIRE(cachedResult); + + CHECK(!cachedResult->isSubtype); +} + +TEST_CASE_FIXTURE(SubtypeFixture, "results_that_are_contingent_on_generics_are_not_cached") +{ + // (T) -> T <: (number) -> number + CHECK_IS_SUBTYPE(genericTToTType, numberToNumberType); + + CHECK(subtyping.peekCache().empty()); +} + +TEST_CASE_FIXTURE(SubtypeFixture, "dont_cache_tests_involving_cycles") +{ + TypeId tableA = arena.addType(BlockedType{}); + TypeId tableA2 = tbl({{"self", tableA}}); + asMutable(tableA)->ty.emplace(tableA2); + + TypeId tableB = arena.addType(BlockedType{}); + TypeId tableB2 = tbl({{"self", tableB}}); + asMutable(tableB)->ty.emplace(tableB2); + + CHECK_IS_SUBTYPE(tableA, tableB); + + CHECK(!subtyping.peekCache().find({tableA, tableB})); +} + TEST_SUITE_END(); diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index 32e107094..932b5b280 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -943,13 +943,13 @@ R"(Type could not be converted into '{ a: number, b: string, c: { d: number } }' caused by: - Property 'c' is not compatible. + Property 'c' is not compatible. Type '{| d: string |}' could not be converted into '{ d: number }' caused by: - Property 'd' is not compatible. + Property 'd' is not compatible. Type 'string' could not be converted into 'number' in an invariant context)" : R"(Type @@ -957,13 +957,13 @@ R"(Type could not be converted into '{| a: number, b: string, c: {| d: number |} |}' caused by: - Property 'c' is not compatible. + Property 'c' is not compatible. Type '{ d: string }' could not be converted into '{| d: number |}' caused by: - Property 'd' is not compatible. + Property 'd' is not compatible. Type 'string' could not be converted into 'number' in an invariant context)"; //clang-format on // diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 549bd3be6..2c68ae94b 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -149,13 +149,13 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate") could not be converted into '((string, string) -> boolean)?' caused by: - None of the union options are compatible. For example: + None of the union options are compatible. For example: Type '(number, number) -> boolean' could not be converted into '(string, string) -> boolean' caused by: - Argument #1 type is not compatible. + Argument #1 type is not compatible. Type 'string' could not be converted into 'number')"; CHECK_EQ(expected, toString(result.errors[0])); } diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 412ec1d04..94eec9614 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -383,7 +383,7 @@ b(a) LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type 'Vector2' could not be converted into '{- X: number, Y: string -}' caused by: - Property 'Y' is not compatible. + Property 'Y' is not compatible. Type 'number' could not be converted into 'string')"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -466,7 +466,7 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: - Property 'x' is not compatible. + Property 'x' is not compatible. Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)"; CHECK_EQ(expected, toString(result.errors[0])); } diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index bac70592c..1e9b8ad9b 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -1373,7 +1373,7 @@ local b: B = a could not be converted into '(number, string) -> string' caused by: - Argument #2 type is not compatible. + Argument #2 type is not compatible. Type 'string' could not be converted into 'number')"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -1414,7 +1414,7 @@ local b: B = a could not be converted into '(number, number) -> number' caused by: - Return type is not compatible. + Return type is not compatible. Type 'string' could not be converted into 'number')"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -1435,7 +1435,7 @@ local b: B = a could not be converted into '(number, number) -> (number, boolean)' caused by: - Return #2 type is not compatible. + Return #2 type is not compatible. Type 'string' could not be converted into 'boolean')"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -1565,13 +1565,13 @@ end could not be converted into '((number) -> number)?' caused by: - None of the union options are compatible. For example: + None of the union options are compatible. For example: Type '(string) -> string' could not be converted into '(number) -> number' caused by: - Argument #1 type is not compatible. + Argument #1 type is not compatible. Type 'number' could not be converted into 'string')"); CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')"); } @@ -2042,7 +2042,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_bu const std::string expected = R"(Type '{ x: number }' could not be converted into 'vec2?' caused by: - None of the union options are compatible. For example: + None of the union options are compatible. For example: Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y')"; CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ("Type 'vec2' could not be converted into 'number'", toString(result.errors[1])); diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index ff7d9909d..1fbd90082 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -727,10 +727,10 @@ y.a.c = y LUAU_REQUIRE_ERRORS(result); const std::string expected = R"(Type 'y' could not be converted into 'T' caused by: - Property 'a' is not compatible. + Property 'a' is not compatible. Type '{ c: T?, d: number }' could not be converted into 'U' caused by: - Property 'd' is not compatible. + Property 'd' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; CHECK_EQ(expected, toString(result.errors[0])); } diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 158e3682d..6892c78f7 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -397,7 +397,7 @@ local a: XYZ = 3 LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type 'number' could not be converted into 'X & Y & Z' caused by: - Not all intersection parts are compatible. + Not all intersection parts are compatible. Type 'number' could not be converted into 'X')"; CHECK_EQ(expected, toString(result.errors[0])); } diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index 2d7d47780..3c126c7ee 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -412,7 +412,7 @@ local b: B.T = a CheckResult result = frontend.check("game/C"); const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' caused by: - Property 'x' is not compatible. + Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ(expected, toString(result.errors[0])); @@ -447,7 +447,7 @@ local b: B.T = a CheckResult result = frontend.check("game/D"); const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' caused by: - Property 'x' is not compatible. + Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ(expected, toString(result.errors[0])); diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index d9cd16318..02a46d9e9 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -263,13 +263,18 @@ TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_m LUAU_REQUIRE_ERROR_COUNT(1, result); - GenericError* gen = get(result.errors[0]); - REQUIRE(gen != nullptr); - if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK(gen->message == "Types 'a' and 'b' cannot be compared with < because neither type has a metatable"); + { + UninhabitedTypeFamily* utf = get(result.errors[0]); + REQUIRE(utf); + REQUIRE_EQ(toString(utf->ty), "lt"); + } else + { + GenericError* gen = get(result.errors[0]); + REQUIRE(gen != nullptr); REQUIRE_EQ(gen->message, "Type a cannot be compared with < because it has no metatable"); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators") @@ -288,13 +293,18 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_indirectly_compare_types_that_do_not_ LUAU_REQUIRE_ERROR_COUNT(1, result); - GenericError* gen = get(result.errors[0]); - REQUIRE(gen != nullptr); - if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK(gen->message == "Types 'M' and 'M' cannot be compared with < because neither type's metatable has a '__lt' metamethod"); + { + UninhabitedTypeFamily* utf = get(result.errors[0]); + REQUIRE(utf); + REQUIRE_EQ(toString(utf->ty), "lt"); + } else + { + GenericError* gen = get(result.errors[0]); + REQUIRE(gen != nullptr); REQUIRE_EQ(gen->message, "Table M does not offer metamethod __lt"); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_compare_tables_that_do_not_have_the_same_metatable") @@ -727,13 +737,18 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato LUAU_REQUIRE_ERROR_COUNT(1, result); - GenericError* ge = get(result.errors[0]); - REQUIRE(ge); - if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK_EQ("Types 'boolean' and 'boolean' cannot be compared with relational operator <", ge->message); + { + UninhabitedTypeFamily* utf = get(result.errors[0]); + REQUIRE(utf); + REQUIRE_EQ(toString(utf->ty), "lt"); + } else + { + GenericError* ge = get(result.errors[0]); + REQUIRE(ge); CHECK_EQ("Type 'boolean' cannot be compared with relational operator <", ge->message); + } } TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators2") @@ -746,12 +761,18 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato LUAU_REQUIRE_ERROR_COUNT(1, result); - GenericError* ge = get(result.errors[0]); - REQUIRE(ge); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK_EQ("Types 'number | string' and 'number | string' cannot be compared with relational operator <", ge->message); + { + UninhabitedTypeFamily* utf = get(result.errors[0]); + REQUIRE(utf); + REQUIRE_EQ(toString(utf->ty), "lt"); + } else + { + GenericError* ge = get(result.errors[0]); + REQUIRE(ge); CHECK_EQ("Type 'number | string' cannot be compared with relational operator <", ge->message); + } } TEST_CASE_FIXTURE(Fixture, "cli_38355_recursive_union") @@ -915,7 +936,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_any_in_all_modes_when_lhs_is_unknown") if (FFlag::DebugLuauDeferredConstraintResolution) { LUAU_REQUIRE_NO_ERRORS(result); - CHECK(toString(requireType("f")) == "(a, b) -> Add"); + CHECK(toString(requireType("f")) == "(a, b) -> add"); } else { @@ -947,7 +968,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_subtraction") if (FFlag::DebugLuauDeferredConstraintResolution) { LUAU_REQUIRE_NO_ERRORS(result); - CHECK(toString(requireType("f")) == "(a, b) -> Sub"); + CHECK(toString(requireType("f")) == "(a, b) -> sub"); } else { @@ -967,7 +988,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_multiplication") if (FFlag::DebugLuauDeferredConstraintResolution) { LUAU_REQUIRE_NO_ERRORS(result); - CHECK(toString(requireType("f")) == "(a, b) -> Mul"); + CHECK(toString(requireType("f")) == "(a, b) -> mul"); } else { @@ -987,7 +1008,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_division") if (FFlag::DebugLuauDeferredConstraintResolution) { LUAU_REQUIRE_NO_ERRORS(result); - CHECK(toString(requireType("f")) == "(a, b) -> Div"); + CHECK(toString(requireType("f")) == "(a, b) -> div"); } else { @@ -1009,7 +1030,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_floor_division") if (FFlag::DebugLuauDeferredConstraintResolution) { LUAU_REQUIRE_NO_ERRORS(result); - CHECK(toString(requireType("f")) == "(a, b) -> FloorDiv"); + CHECK(toString(requireType("f")) == "(a, b) -> idiv"); } else { @@ -1029,7 +1050,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_exponentiation") if (FFlag::DebugLuauDeferredConstraintResolution) { LUAU_REQUIRE_NO_ERRORS(result); - CHECK(toString(requireType("f")) == "(a, b) -> Exp"); + CHECK(toString(requireType("f")) == "(a, b) -> pow"); } else { @@ -1049,7 +1070,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_modulo") if (FFlag::DebugLuauDeferredConstraintResolution) { LUAU_REQUIRE_NO_ERRORS(result); - CHECK(toString(requireType("f")) == "(a, b) -> Mod"); + CHECK(toString(requireType("f")) == "(a, b) -> mod"); } else { @@ -1058,6 +1079,26 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_modulo") } } +TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_concat") +{ + CheckResult result = check(Mode::Strict, R"( + local function f(x, y) + return x .. y + end + )"); + + if (FFlag::DebugLuauDeferredConstraintResolution) + { + LUAU_REQUIRE_NO_ERRORS(result); + CHECK(toString(requireType("f")) == "(a, b) -> concat"); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(toString(result.errors[0]), "Unknown type used in .. operation; consider adding a type annotation to 'x'"); + } +} + TEST_CASE_FIXTURE(BuiltinsFixture, "equality_operations_succeed_if_any_union_branch_succeeds") { CheckResult result = check(R"( diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index 4d7fceee4..65bb33530 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -817,7 +817,7 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty could not be converted into '{| x: number |}' caused by: - Property 'x' is not compatible. + Property 'x' is not compatible. Type 'number?' could not be converted into 'number' in an invariant context)"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -1099,4 +1099,40 @@ foo(1 :: any) LUAU_REQUIRE_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "luau_roact_useState_minimization") +{ + // We don't expect this test to work on the old solver, but it also does not yet work on the new solver. + // So, we can't just put a scoped fast flag here, or it would block CI. + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + CheckResult result = check(R"( + type BasicStateAction = ((S) -> S) | S + type Dispatch = (A) -> () + + local function useState( + initialState: (() -> S) | S + ): (S, Dispatch>) + -- fake impl that obeys types + local val = if type(initialState) == "function" then initialState() else initialState + return val, function(value) + return value + end + end + + local test, setTest = useState(nil :: string?) + + setTest(nil) -- this line causes the type to be narrowed in the old solver!!! + + local function update(value: string) + print(test) + setTest(value) + end + + update("hello") + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 374acc604..a1aa38c5e 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -1908,22 +1908,23 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table") // this test is DCR-only as an instance of DCR fixing a bug in the old solver CheckResult result = check(R"( - local a : unknown = nil - local idx, val - if typeof(a) == "table" then - for i, v in a do - idx = i - val = v + local function f(a: unknown) + if typeof(a) == "table" then + for i, v in a do + idx = i + val = v + end end end )"); LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("unknown", toString(requireType("idx"))); - CHECK_EQ("unknown", toString(requireType("val"))); + // TODO: they should be `unknown`, not `nil`. + CHECK_EQ("nil", toString(requireType("idx"))); + CHECK_EQ("nil", toString(requireType("val"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing") diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index aeef4bad3..16d421f82 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -327,7 +327,7 @@ local a: Animal = { tag = 'cat', cafood = 'something' } LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type 'a' could not be converted into 'Cat | Dog' caused by: - None of the union options are compatible. For example: + None of the union options are compatible. For example: Table type 'a' not compatible with type 'Cat' because the former is missing field 'catfood')"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -345,7 +345,7 @@ local a: Result = { success = false, result = 'something' } LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type 'a' could not be converted into 'Bad | Good' caused by: - None of the union options are compatible. For example: + None of the union options are compatible. For example: Table type 'a' not compatible with type 'Bad' because the former is missing field 'error')"; CHECK_EQ(expected, toString(result.errors[0])); } diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 47ddf9b5e..15104ab2b 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -2119,7 +2119,7 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: - Property 'y' is not compatible. + Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -2140,10 +2140,10 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: - Property 'b' is not compatible. + Property 'b' is not compatible. Type 'AS' could not be converted into 'BS' caused by: - Property 'y' is not compatible. + Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -2167,7 +2167,7 @@ caused by: could not be converted into '{ x: number, y: number }' caused by: - Property 'y' is not compatible. + Property 'y' is not compatible. Type 'string' could not be converted into 'number' in an invariant context)"; const std::string expected2 = R"(Type 'b2' could not be converted into 'a2' caused by: @@ -2176,7 +2176,7 @@ caused by: could not be converted into '{ __call: (a) -> () }' caused by: - Property '__call' is not compatible. + Property '__call' is not compatible. Type '(a, b) -> ()' could not be converted into @@ -2188,7 +2188,7 @@ caused by: could not be converted into '{ __call: (a) -> () }' caused by: - Property '__call' is not compatible. + Property '__call' is not compatible. Type '(a, b) -> ()' could not be converted into @@ -2209,7 +2209,7 @@ caused by: could not be converted into '{ __call: (a) -> () }' caused by: - Property '__call' is not compatible. + Property '__call' is not compatible. Type '(a, b) -> ()' could not be converted into @@ -2231,7 +2231,7 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key") LUAU_REQUIRE_ERRORS(result); const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: - Property '[indexer key]' is not compatible. + Property '[indexer key]' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -2249,7 +2249,7 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value") LUAU_REQUIRE_ERRORS(result); const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: - Property '[indexer value]' is not compatible. + Property '[indexer value]' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -2287,7 +2287,7 @@ local y: number = tmp.p.y LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type 'tmp' could not be converted into 'HasSuper' caused by: - Property 'p' is not compatible. + Property 'p' is not compatible. Table type '{ x: number, y: number }' not compatible with type 'Super' because the former has extra field 'y')"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -3399,7 +3399,7 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_ const std::string expected1 = R"(Type 'string' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' caused by: - The former's metatable does not satisfy the requirements. + The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; CHECK_EQ(expected1, toString(result.errors[0])); @@ -3407,7 +3407,7 @@ Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutel const std::string expected2 = R"(Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' caused by: - The former's metatable does not satisfy the requirements. + The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; CHECK_EQ(expected2, toString(result.errors[1])); @@ -3416,10 +3416,10 @@ Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutel could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' caused by: - Not all union options are compatible. + Not all union options are compatible. Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' caused by: - The former's metatable does not satisfy the requirements. + The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; CHECK_EQ(expected3, toString(result.errors[2])); } @@ -3450,7 +3450,7 @@ TEST_CASE_FIXTURE(Fixture, "a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_ const std::string expected = R"(Type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' could not be converted into 'string' caused by: - The former's metatable does not satisfy the requirements. + The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ(expected, toString(result.errors[0])); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 0e7a97159..a2a28ae52 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -1045,19 +1045,19 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error") LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule' caused by: - Property 'getStoreFieldName' is not compatible. + Property 'getStoreFieldName' is not compatible. Type '(Policies, FieldSpecifier & {| from: number? |}) -> (a, b...)' could not be converted into '(Policies, FieldSpecifier) -> string' caused by: - Argument #2 type is not compatible. + Argument #2 type is not compatible. Type 'FieldSpecifier' could not be converted into 'FieldSpecifier & {| from: number? |}' caused by: - Not all intersection parts are compatible. + Not all intersection parts are compatible. Table type 'FieldSpecifier' not compatible with type '{| from: number? |}' because the former has extra field 'fieldName')"; CHECK_EQ(expected, toString(result.errors[0])); } diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp new file mode 100644 index 000000000..9a938f901 --- /dev/null +++ b/tests/TypeInfer.typestates.test.cpp @@ -0,0 +1,122 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Fixture.h" + +#include "doctest.h" + +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) + +using namespace Luau; + +namespace +{ +struct TypeStateFixture : BuiltinsFixture +{ + ScopedFastFlag dcr{"DebugLuauDeferredConstraintResolution", true}; +}; +} + +TEST_SUITE_BEGIN("TypeStatesTest"); + +TEST_CASE_FIXTURE(TypeStateFixture, "initialize_x_of_type_string_or_nil_with_nil") +{ + CheckResult result = check(R"( + local x: string? = nil + local a = x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("nil" == toString(requireType("a"))); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "extraneous_lvalues_are_populated_with_nil") +{ + CheckResult result = check(R"( + local function f(): (string, number) + return "hello", 5 + end + + local x, y, z = f() + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK("Function only returns 2 values, but 3 are required here" == toString(result.errors[0])); + CHECK("string" == toString(requireType("x"))); + CHECK("number" == toString(requireType("y"))); + CHECK("nil" == toString(requireType("z"))); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "assign_different_values_to_x") +{ + CheckResult result = check(R"( + local x: string? = nil + local a = x + x = "hello!" + local b = x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("nil" == toString(requireType("a"))); + CHECK("string" == toString(requireType("b"))); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "parameter_x_was_constrained_by_two_types") +{ + // Parameter `x` has a fresh type `'x` bounded by `never` and `unknown`. + // The first use of `x` constrains `x`'s upper bound by `string | number`. + // The second use of `x`, aliased by `y`, constrains `x`'s upper bound by `string?`. + // This results in `'x <: (string | number) & (string?)`. + // The principal type of the upper bound is `string`. + CheckResult result = check(R"( + local function f(x): string? + local y: string | number = x + return y + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("(string) -> string?" == toString(requireType("f"))); +} + +#if 0 +TEST_CASE_FIXTURE(TypeStateFixture, "local_that_will_be_assigned_later") +{ + CheckResult result = check(R"( + local x: string + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "refine_a_local_and_then_assign_it") +{ + CheckResult result = check(R"( + local function f(x: string?) + if typeof(x) == "string" then + x = nil + end + + local y: nil = x + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} +#endif + +TEST_CASE_FIXTURE(TypeStateFixture, "assign_a_local_and_then_refine_it") +{ + CheckResult result = check(R"( + local function f(x: string?) + x = nil + + if typeof(x) == "string" then + local y: typeof(x) = "hello" + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK("Type 'string' could not be converted into 'never'" == toString(result.errors[0])); +} + +TEST_SUITE_END(); diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index bef02ded3..aaed7b0db 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -222,9 +222,9 @@ TEST_CASE_FIXTURE(Fixture, "union_equality_comparisons") type B = number | nil type C = number | boolean - local a: A = 1 - local b: B = nil - local c: C = true + local a = 1 :: A + local b = nil :: B + local c = true :: C local n = 1 local x = a == b @@ -517,7 +517,7 @@ local a: X? = { w = 4 } LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type 'a' could not be converted into 'X?' caused by: - None of the union options are compatible. For example: + None of the union options are compatible. For example: Table type 'a' not compatible with type 'X' because the former is missing field 'x')"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -843,7 +843,7 @@ TEST_CASE_FIXTURE(Fixture, "suppress_errors_for_prop_lookup_of_a_union_that_incl registerHiddenTypes(&frontend); CheckResult result = check(R"( - local a : err | Not + local a = 5 :: err | Not local b = a.foo )"); diff --git a/tools/faillist.txt b/tools/faillist.txt index a45b215b2..8bb028142 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -1,16 +1,24 @@ +AnnotationTests.cloned_interface_maintains_pointers_between_definitions AnnotationTests.infer_type_of_value_a_via_typeof_with_assignment +AnnotationTests.table_annotation AnnotationTests.two_type_params +AnnotationTests.typeof_expr AnnotationTests.use_generic_type_alias AstQuery.last_argument_function_call_type +AstQuery::getDocumentationSymbolAtPosition.class_method +AstQuery::getDocumentationSymbolAtPosition.overloaded_class_method AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg AutocompleteTest.autocomplete_interpolated_string_as_singleton AutocompleteTest.autocomplete_oop_implicit_self +AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.autocomplete_string_singleton_escape AutocompleteTest.autocomplete_string_singletons -AutocompleteTest.cyclic_table AutocompleteTest.do_wrong_compatible_nonself_calls +AutocompleteTest.frontend_use_correct_global_scope +AutocompleteTest.no_incompatible_self_calls_on_class +AutocompleteTest.string_singleton_in_if_statement AutocompleteTest.suggest_external_module_type AutocompleteTest.type_correct_expected_argument_type_pack_suggestion AutocompleteTest.type_correct_expected_argument_type_suggestion @@ -23,7 +31,6 @@ AutocompleteTest.type_correct_function_return_types AutocompleteTest.type_correct_keywords AutocompleteTest.type_correct_suggestion_for_overloads AutocompleteTest.type_correct_suggestion_in_argument -AutocompleteTest.unsealed_table_2 BuiltinTests.aliased_string_format BuiltinTests.assert_removes_falsy_types BuiltinTests.assert_removes_falsy_types2 @@ -32,6 +39,7 @@ BuiltinTests.assert_returns_false_and_string_iff_it_knows_the_first_argument_can BuiltinTests.bad_select_should_not_crash BuiltinTests.coroutine_resume_anything_goes BuiltinTests.debug_info_is_crazy +BuiltinTests.dont_add_definitions_to_persistent_types BuiltinTests.global_singleton_types_are_sealed BuiltinTests.gmatch_capture_types BuiltinTests.gmatch_capture_types2 @@ -59,6 +67,7 @@ BuiltinTests.string_format_as_method BuiltinTests.string_format_correctly_ordered_types BuiltinTests.string_format_report_all_type_errors_at_correct_positions BuiltinTests.string_format_use_correct_argument2 +BuiltinTests.string_match BuiltinTests.table_concat_returns_string BuiltinTests.table_dot_remove_optionally_returns_generic BuiltinTests.table_freeze_is_generic @@ -93,10 +102,12 @@ ControlFlowAnalysis.tagged_unions_breaking ControlFlowAnalysis.tagged_unions_continuing ControlFlowAnalysis.type_alias_does_not_leak_out_breaking ControlFlowAnalysis.type_alias_does_not_leak_out_continuing +DefinitionTests.class_definition_function_prop DefinitionTests.class_definition_indexer DefinitionTests.class_definition_overload_metamethods DefinitionTests.class_definition_string_props DefinitionTests.declaring_generic_functions +DefinitionTests.definition_file_class_function_args DefinitionTests.definition_file_classes Differ.equal_generictp_cyclic Differ.equal_table_A_B_C @@ -119,9 +130,10 @@ Differ.table_left_circle_right_measuring_tape FrontendTest.accumulate_cached_errors_in_consistent_order FrontendTest.it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded GenericsTests.apply_type_function_nested_generics1 -GenericsTests.apply_type_function_nested_generics3 +GenericsTests.apply_type_function_nested_generics2 GenericsTests.better_mismatch_error_messages GenericsTests.bound_tables_do_not_clone_original_fields +GenericsTests.calling_self_generic_methods GenericsTests.check_generic_function GenericsTests.check_generic_local_function GenericsTests.check_generic_typepack_function @@ -169,11 +181,17 @@ GenericsTests.rank_N_types_via_typeof GenericsTests.self_recursive_instantiated_param GenericsTests.type_parameters_can_be_polytypes GenericsTests.typefuns_sharing_types +IntersectionTypes.argument_is_intersection IntersectionTypes.error_detailed_intersection_all IntersectionTypes.error_detailed_intersection_part +IntersectionTypes.fx_intersection_as_argument +IntersectionTypes.index_on_an_intersection_type_with_mixed_types +IntersectionTypes.index_on_an_intersection_type_with_one_part_missing_the_property +IntersectionTypes.index_on_an_intersection_type_with_one_property_of_type_any +IntersectionTypes.index_on_an_intersection_type_with_property_guaranteed_to_exist +IntersectionTypes.index_on_an_intersection_type_works_at_arbitrary_depth IntersectionTypes.intersect_bool_and_false IntersectionTypes.intersect_false_and_bool_and_false -IntersectionTypes.intersect_metatables IntersectionTypes.intersect_saturate_overloaded_functions IntersectionTypes.intersection_of_tables IntersectionTypes.intersection_of_tables_with_never_properties @@ -192,12 +210,20 @@ IntersectionTypes.overloadeded_functions_with_weird_typepacks_1 IntersectionTypes.overloadeded_functions_with_weird_typepacks_2 IntersectionTypes.overloadeded_functions_with_weird_typepacks_3 IntersectionTypes.overloadeded_functions_with_weird_typepacks_4 +IntersectionTypes.propagates_name IntersectionTypes.select_correct_union_fn IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions +IntersectionTypes.table_extra_ok +IntersectionTypes.table_intersection_setmetatable +IntersectionTypes.table_intersection_write_sealed IntersectionTypes.table_intersection_write_sealed_indirect IntersectionTypes.table_write_sealed_indirect IntersectionTypes.union_saturate_overloaded_functions +Linter.DeprecatedApiFenv +Linter.FormatStringTyped +Linter.TableOperationsIndexer Negations.cofinite_strings_can_be_compared_for_equality +Negations.negated_string_is_a_subtype_of_string Normalize.higher_order_function_with_annotation Normalize.negations_of_tables Normalize.specific_functions_cannot_be_negated @@ -212,10 +238,12 @@ ProvisionalTests.free_options_can_be_unified_together ProvisionalTests.free_options_cannot_be_unified_together ProvisionalTests.function_returns_many_things_but_first_of_it_is_forgotten ProvisionalTests.generic_type_leak_to_module_interface +ProvisionalTests.generic_type_leak_to_module_interface_variadic ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns ProvisionalTests.it_should_be_agnostic_of_actual_size ProvisionalTests.luau-polyfill.Array.filter ProvisionalTests.luau-polyfill.Map.entries +ProvisionalTests.luau_roact_useState_minimization ProvisionalTests.optional_class_instances_are_invariant ProvisionalTests.pcall_returns_at_least_two_value_but_function_returns_nothing ProvisionalTests.setmetatable_constrains_free_type_into_free_table @@ -223,9 +251,11 @@ ProvisionalTests.specialization_binds_with_prototypes_too_early ProvisionalTests.table_insert_with_a_singleton_argument ProvisionalTests.table_unification_infinite_recursion ProvisionalTests.typeguard_inference_incomplete +ProvisionalTests.weirditer_should_not_loop_forever ProvisionalTests.while_body_are_also_refined ProvisionalTests.xpcall_returns_what_f_returns RefinementTest.apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string +RefinementTest.assert_a_to_be_truthy_then_assert_a_to_be_number RefinementTest.correctly_lookup_property_whose_base_was_previously_refined RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never RefinementTest.discriminate_from_truthiness_of_x @@ -241,6 +271,7 @@ RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true RefinementTest.not_t_or_some_prop_of_t RefinementTest.refine_a_property_of_some_global RefinementTest.refine_unknown_to_table_then_clone_it +RefinementTest.refinements_should_preserve_error_suppression RefinementTest.string_not_equal_to_string_or_nil RefinementTest.truthy_constraint_on_properties RefinementTest.type_annotations_arent_relevant_when_doing_dataflow_analysis @@ -294,22 +325,27 @@ TableTests.explicitly_typed_table_with_indexer TableTests.extend_unsealed_table_with_metatable TableTests.generalize_table_argument TableTests.generic_table_instantiation_potential_regression -TableTests.give_up_after_one_metatable_index_look_up TableTests.indexer_mismatch TableTests.indexer_on_sealed_table_must_unify_with_free_table TableTests.indexers_get_quantified_too +TableTests.indexing_from_a_table_should_prefer_properties_when_possible TableTests.inequality_operators_imply_exactly_matching_types TableTests.infer_array_2 TableTests.infer_indexer_for_left_unsealed_table_from_right_hand_table_with_indexer TableTests.infer_indexer_from_its_function_return_type +TableTests.infer_indexer_from_its_variable_type_and_unifiable TableTests.infer_indexer_from_value_property_in_literal +TableTests.infer_type_when_indexing_from_a_table_indexer TableTests.inferred_return_type_of_free_table TableTests.instantiate_table_cloning_3 TableTests.instantiate_tables_at_scope_level TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound TableTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound -TableTests.leaking_bad_metatable_errors +TableTests.length_operator_intersection +TableTests.length_operator_non_table_union +TableTests.length_operator_union TableTests.less_exponential_blowup_please +TableTests.meta_add_both_ways TableTests.metatable_mismatch_should_fail TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred TableTests.mixed_tables_with_implicit_numbered_keys @@ -330,6 +366,8 @@ TableTests.quantify_even_that_table_was_never_exported_at_all TableTests.quantify_metatables_of_metatables_of_table TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table TableTests.recursive_metatable_type_call +TableTests.result_is_always_any_if_lhs_is_any +TableTests.result_is_bool_for_equality_operators_if_lhs_is_any TableTests.right_table_missing_key2 TableTests.scalar_is_a_subtype_of_a_compatible_polymorphic_shape_type TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type @@ -341,6 +379,7 @@ TableTests.shared_selfs_through_metatables TableTests.subproperties_can_also_be_covariantly_tested TableTests.table_call_metamethod_basic TableTests.table_call_metamethod_generic +TableTests.table_call_metamethod_must_be_callable TableTests.table_function_check_use_after_free TableTests.table_insert_should_cope_with_optional_properties_in_strict TableTests.table_param_width_subtyping_1 @@ -354,6 +393,7 @@ TableTests.table_unification_4 TableTests.table_unifies_into_map TableTests.top_table_type TableTests.type_mismatch_on_massive_table_is_cut_short +TableTests.unification_of_unions_in_a_self_referential_type TableTests.unifying_tables_shouldnt_uaf1 TableTests.used_colon_instead_of_dot TableTests.used_dot_instead_of_colon @@ -362,11 +402,16 @@ ToDot.function ToString.exhaustive_toString_of_cyclic_table ToString.free_types ToString.named_metatable_toStringNamedFunction +ToString.no_parentheses_around_cyclic_function_type_in_union ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics +ToString.primitive ToString.tostring_error_mismatch +ToString.tostring_unsee_ttv_if_array ToString.toStringDetailed2 ToString.toStringErrorPack ToString.toStringNamedFunction_generic_pack +ToString.toStringNamedFunction_map +TranspilerTests.types_should_not_be_considered_cyclic_if_they_are_not_recursive TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType TryUnifyTests.result_of_failed_typepack_unification_is_constrained TryUnifyTests.typepack_unification_should_trim_free_tails @@ -374,6 +419,9 @@ TryUnifyTests.uninhabited_table_sub_anything TryUnifyTests.uninhabited_table_sub_never TryUnifyTests.variadics_should_use_reversed_properly TypeAliases.alias_expands_to_bare_reference_to_imported_type +TypeAliases.corecursive_types_generic +TypeAliases.cyclic_function_type_in_type_alias +TypeAliases.cyclic_types_of_named_table_fields_do_not_expand_when_stringified TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution TypeAliases.free_variables_from_typeof_in_aliases TypeAliases.generic_param_remap @@ -385,6 +433,7 @@ TypeAliases.mutually_recursive_types_restriction_not_ok_2 TypeAliases.mutually_recursive_types_swapsies_not_ok TypeAliases.recursive_types_restriction_not_ok TypeAliases.report_shadowed_aliases +TypeAliases.saturate_to_first_type_pack TypeAliases.stringify_optional_parameterized_alias TypeAliases.type_alias_local_mutation TypeAliases.type_alias_local_rename @@ -404,10 +453,10 @@ TypeInfer.check_expr_recursion_limit TypeInfer.check_type_infer_recursion_count TypeInfer.cli_39932_use_unifier_in_ensure_methods TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error +TypeInfer.dont_ice_when_failing_the_occurs_check TypeInfer.dont_report_type_errors_within_an_AstExprError TypeInfer.dont_report_type_errors_within_an_AstStatError TypeInfer.follow_on_new_types_in_substitution -TypeInfer.fuzz_free_table_type_change_during_index_check TypeInfer.globals_are_banned_in_strict_mode TypeInfer.if_statement TypeInfer.infer_locals_via_assignment_from_its_call_site @@ -419,12 +468,17 @@ TypeInfer.no_stack_overflow_from_isoptional TypeInfer.promote_tail_type_packs TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2 +TypeInfer.stringify_nested_unions_with_optionals TypeInfer.tc_after_error_recovery_no_replacement_name_in_error TypeInfer.tc_error TypeInfer.tc_error_2 TypeInfer.tc_if_else_expressions_expected_type_3 TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_normalizer +TypeInfer.unify_nearly_identical_recursive_types +TypeInferAnyError.any_type_propagates +TypeInferAnyError.assign_prop_to_table_by_calling_any_yields_any +TypeInferAnyError.call_to_any_yields_any TypeInferAnyError.can_subscript_any TypeInferAnyError.for_in_loop_iterator_is_any TypeInferAnyError.for_in_loop_iterator_is_any2 @@ -433,19 +487,25 @@ TypeInferAnyError.for_in_loop_iterator_is_error2 TypeInferAnyError.for_in_loop_iterator_returns_any TypeInferAnyError.for_in_loop_iterator_returns_any2 TypeInferAnyError.intersection_of_any_can_have_props +TypeInferAnyError.quantify_any_does_not_bind_to_itself TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any TypeInferAnyError.type_error_addition TypeInferClasses.callable_classes TypeInferClasses.can_read_prop_of_base_class_using_string TypeInferClasses.cannot_unify_class_instance_with_primitive TypeInferClasses.class_type_mismatch_with_name_conflict +TypeInferClasses.class_unification_type_mismatch_is_correct_order TypeInferClasses.detailed_class_unification_error TypeInferClasses.index_instance_property TypeInferClasses.indexable_classes +TypeInferClasses.optional_class_field_access_error TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties TypeInferClasses.table_indexers_are_invariant TypeInferClasses.type_mismatch_invariance_required_for_error TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class +TypeInferFunctions.another_higher_order_function +TypeInferFunctions.another_other_higher_order_function +TypeInferFunctions.another_recursive_local_function TypeInferFunctions.apply_of_lambda_with_inferred_and_explicit_types TypeInferFunctions.cannot_hoist_interior_defns_into_signature TypeInferFunctions.check_function_bodies @@ -478,9 +538,10 @@ TypeInferFunctions.infer_generic_lib_function_function_argument TypeInferFunctions.infer_return_type_from_selected_overload TypeInferFunctions.infer_return_value_type TypeInferFunctions.infer_that_function_does_not_return_a_table -TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time3 TypeInferFunctions.instantiated_type_packs_must_have_a_non_null_scope TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument +TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count +TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count TypeInferFunctions.luau_subtyping_is_np_hard TypeInferFunctions.no_lossy_function_type TypeInferFunctions.num_is_solved_after_num_or_str @@ -490,6 +551,7 @@ TypeInferFunctions.other_things_are_not_related_to_function TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2 TypeInferFunctions.record_matching_overload +TypeInferFunctions.recursive_local_function TypeInferFunctions.report_exiting_without_return_strict TypeInferFunctions.return_type_by_overload TypeInferFunctions.too_few_arguments_variadic @@ -501,6 +563,10 @@ TypeInferFunctions.too_many_return_values_no_function TypeInferFunctions.vararg_function_is_quantified TypeInferLoops.cli_68448_iterators_need_not_accept_nil TypeInferLoops.dcr_iteration_explore_raycast_minimization +TypeInferLoops.dcr_iteration_fragmented_keys +TypeInferLoops.dcr_iteration_minimized_fragmented_keys_1 +TypeInferLoops.dcr_iteration_minimized_fragmented_keys_2 +TypeInferLoops.dcr_iteration_minimized_fragmented_keys_3 TypeInferLoops.dcr_iteration_on_never_gives_never TypeInferLoops.dcr_xpath_candidates TypeInferLoops.for_in_loop @@ -511,6 +577,7 @@ TypeInferLoops.for_in_loop_on_non_function TypeInferLoops.for_in_loop_with_custom_iterator TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator TypeInferLoops.for_in_loop_with_next +TypeInferLoops.for_in_with_an_iterator_of_type_any TypeInferLoops.for_in_with_generic_next TypeInferLoops.for_in_with_just_one_iterator_is_ok TypeInferLoops.for_loop @@ -530,13 +597,17 @@ TypeInferLoops.unreachable_code_after_infinite_loop TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free TypeInferLoops.while_loop TypeInferModules.bound_free_table_export_is_ok +TypeInferModules.do_not_modify_imported_types TypeInferModules.do_not_modify_imported_types_4 TypeInferModules.do_not_modify_imported_types_5 TypeInferModules.general_require_call_expression TypeInferModules.module_type_conflict TypeInferModules.module_type_conflict_instantiated +TypeInferModules.require TypeInferModules.require_failed_module +TypeInferOOP.CheckMethodsOfSealed TypeInferOOP.cycle_between_object_constructor_and_alias +TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_another_overload_works TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2 TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory @@ -555,33 +626,37 @@ TypeInferOperators.compound_assign_result_must_be_compatible_with_var TypeInferOperators.concat_op_on_free_lhs_and_string_rhs TypeInferOperators.concat_op_on_string_lhs_and_free_rhs TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops -TypeInferOperators.luau-polyfill.Array.startswith +TypeInferOperators.equality_operations_succeed_if_any_union_branch_succeeds +TypeInferOperators.error_on_invalid_operand_types_to_relational_operators2 TypeInferOperators.luau_polyfill_is_array -TypeInferOperators.normalize_strings_comparison -TypeInferOperators.operator_eq_completely_incompatible +TypeInferOperators.mm_comparisons_must_return_a_boolean TypeInferOperators.operator_eq_verifies_types_do_intersect TypeInferOperators.reducing_and TypeInferOperators.refine_and_or +TypeInferOperators.reworked_and +TypeInferOperators.reworked_or TypeInferOperators.strict_binary_op_where_lhs_unknown TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs TypeInferOperators.typecheck_unary_len_error TypeInferOperators.typecheck_unary_minus TypeInferOperators.typecheck_unary_minus_error -TypeInferOperators.unrelated_classes_cannot_be_compared -TypeInferOperators.unrelated_primitives_cannot_be_compared TypeInferPrimitives.CheckMethodsOfNumber +TypeInferPrimitives.string_function_indirect TypeInferPrimitives.string_index TypeInferUnknownNever.array_like_table_of_never_is_inhabitable +TypeInferUnknownNever.assign_to_prop_which_is_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never TypeInferUnknownNever.length_of_never TypeInferUnknownNever.math_operators_and_never +TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable TypePackTests.fuzz_typepack_iter_follow_2 TypePackTests.pack_tail_unification_check TypePackTests.parenthesized_varargs_returns_any TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_default_type_errors +TypePackTests.type_alias_type_packs_import TypePackTests.type_packs_with_tails_in_vararg_adjustment TypePackTests.unify_variadic_tails_in_arguments TypePackTests.unify_variadic_tails_in_arguments_free @@ -592,7 +667,7 @@ TypeSingletons.error_detailed_tagged_union_mismatch_string TypeSingletons.function_args_infer_singletons TypeSingletons.function_call_with_singletons TypeSingletons.function_call_with_singletons_mismatch -TypeSingletons.overloaded_function_call_with_singletons +TypeSingletons.overloaded_function_call_with_singletons_mismatch TypeSingletons.return_type_of_f_is_not_widened TypeSingletons.table_properties_singleton_strings TypeSingletons.table_properties_type_error_escapes @@ -605,12 +680,24 @@ UnionTypes.error_detailed_union_all UnionTypes.error_detailed_union_part UnionTypes.generic_function_with_optional_arg UnionTypes.index_on_a_union_type_with_missing_property +UnionTypes.index_on_a_union_type_with_mixed_types +UnionTypes.index_on_a_union_type_with_one_optional_property +UnionTypes.index_on_a_union_type_with_one_property_of_type_any +UnionTypes.index_on_a_union_type_with_property_guaranteed_to_exist +UnionTypes.index_on_a_union_type_works_at_arbitrary_depth UnionTypes.less_greedy_unification_with_union_types UnionTypes.optional_arguments_table2 +UnionTypes.optional_assignment_errors +UnionTypes.optional_call_error +UnionTypes.optional_field_access_error UnionTypes.optional_index_error UnionTypes.optional_length_error +UnionTypes.optional_missing_key_error_details +UnionTypes.optional_union_follow +UnionTypes.optional_union_functions +UnionTypes.optional_union_members +UnionTypes.optional_union_methods UnionTypes.table_union_write_indirect -UnionTypes.unify_sealed_table_union_check UnionTypes.union_of_functions UnionTypes.union_of_functions_mentioning_generic_typepacks UnionTypes.union_of_functions_mentioning_generics @@ -620,3 +707,4 @@ UnionTypes.union_of_functions_with_mismatching_result_arities UnionTypes.union_of_functions_with_mismatching_result_variadics UnionTypes.union_of_functions_with_variadics UnionTypes.union_true_and_false +VisitType.throw_when_limit_is_exceeded