diff --git a/Analysis/include/Luau/Anyification.h b/Analysis/include/Luau/Anyification.h index 7b6f71716..c81dc3fb5 100644 --- a/Analysis/include/Luau/Anyification.h +++ b/Analysis/include/Luau/Anyification.h @@ -4,7 +4,7 @@ #include "Luau/NotNull.h" #include "Luau/Substitution.h" -#include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include @@ -39,4 +39,4 @@ struct Anyification : Substitution bool ignoreChildren(TypePackId ty) override; }; -} // namespace Luau \ No newline at end of file +} // namespace Luau diff --git a/Analysis/include/Luau/ApplyTypeFunction.h b/Analysis/include/Luau/ApplyTypeFunction.h index 3f5f47fd4..71430b28f 100644 --- a/Analysis/include/Luau/ApplyTypeFunction.h +++ b/Analysis/include/Luau/ApplyTypeFunction.h @@ -3,7 +3,7 @@ #include "Luau/Substitution.h" #include "Luau/TxnLog.h" -#include "Luau/Type.h" +#include "Luau/TypeFwd.h" namespace Luau { diff --git a/Analysis/include/Luau/AstQuery.h b/Analysis/include/Luau/AstQuery.h index e7a018c0a..7652a89f0 100644 --- a/Analysis/include/Luau/AstQuery.h +++ b/Analysis/include/Luau/AstQuery.h @@ -3,6 +3,7 @@ #include "Luau/Ast.h" #include "Luau/Documentation.h" +#include "Luau/TypeFwd.h" #include @@ -13,9 +14,6 @@ struct Binding; struct SourceModule; struct Module; -struct Type; -using TypeId = const Type*; - using ScopePtr = std::shared_ptr; struct ExprOrLocal diff --git a/Analysis/include/Luau/Breadcrumb.h b/Analysis/include/Luau/Breadcrumb.h deleted file mode 100644 index 59b293a0b..000000000 --- a/Analysis/include/Luau/Breadcrumb.h +++ /dev/null @@ -1,75 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#pragma once - -#include "Luau/Def.h" -#include "Luau/NotNull.h" -#include "Luau/Variant.h" - -#include -#include - -namespace Luau -{ - -using NullableBreadcrumbId = const struct Breadcrumb*; -using BreadcrumbId = NotNull; - -struct FieldMetadata -{ - std::string prop; -}; - -struct SubscriptMetadata -{ - BreadcrumbId key; -}; - -using Metadata = Variant; - -struct Breadcrumb -{ - NullableBreadcrumbId previous; - DefId def; - std::optional metadata; - std::vector children; -}; - -inline Breadcrumb* asMutable(NullableBreadcrumbId breadcrumb) -{ - LUAU_ASSERT(breadcrumb); - return const_cast(breadcrumb); -} - -template -const T* getMetadata(NullableBreadcrumbId breadcrumb) -{ - if (!breadcrumb || !breadcrumb->metadata) - return nullptr; - - return get_if(&*breadcrumb->metadata); -} - -struct BreadcrumbArena -{ - TypedAllocator allocator; - - template - BreadcrumbId add(NullableBreadcrumbId previous, DefId def, Args&&... args) - { - Breadcrumb* bc = allocator.allocate(Breadcrumb{previous, def, std::forward(args)...}); - if (previous) - asMutable(previous)->children.push_back(NotNull{bc}); - return NotNull{bc}; - } - - template - BreadcrumbId emplace(NullableBreadcrumbId previous, DefId def, Args&&... args) - { - Breadcrumb* bc = allocator.allocate(Breadcrumb{previous, def, Metadata{T{std::forward(args)...}}}); - if (previous) - asMutable(previous)->children.push_back(NotNull{bc}); - return NotNull{bc}; - } -}; - -} // namespace Luau diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index 67f9470ee..4598efc41 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -4,8 +4,8 @@ #include "Luau/Ast.h" // Used for some of the enumerations #include "Luau/DenseHash.h" #include "Luau/NotNull.h" -#include "Luau/Type.h" #include "Luau/Variant.h" +#include "Luau/TypeFwd.h" #include #include @@ -16,12 +16,6 @@ namespace Luau struct Scope; -struct Type; -using TypeId = const Type*; - -struct TypePackVar; -using TypePackId = const TypePackVar*; - // subType <: superType struct SubtypeConstraint { @@ -55,31 +49,6 @@ struct InstantiationConstraint TypeId superType; }; -struct UnaryConstraint -{ - AstExprUnary::Op op; - TypeId operandType; - TypeId resultType; -}; - -// let L : leftType -// let R : rightType -// in -// L op R : resultType -struct BinaryConstraint -{ - AstExprBinary::Op op; - TypeId leftType; - TypeId rightType; - TypeId resultType; - - // When we dispatch this constraint, we update the key at this map to record - // the overload that we selected. - const AstNode* astFragment; - DenseHashMap* astOriginalCallTypes; - DenseHashMap* astOverloadResolvedTypes; -}; - // iteratee is iterable // iterators is the iteration types. struct IterableConstraint @@ -241,6 +210,22 @@ struct RefineConstraint TypeId discriminant; }; +// resultType ~ T0 op T1 op ... op TN +// +// op is either union or intersection. If any of the input types are blocked, +// this constraint will block unless forced. +struct SetOpConstraint +{ + enum + { + Intersection, + Union + } mode; + + TypeId resultType; + std::vector types; +}; + // ty ~ reduce ty // // Try to reduce ty, if it is a TypeFamilyInstanceType. Otherwise, do nothing. @@ -257,10 +242,9 @@ struct ReducePackConstraint TypePackId tp; }; -using ConstraintV = Variant; +using ConstraintV = Variant; struct Constraint { diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGraphBuilder.h index 955a683ec..bba5ebd93 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGraphBuilder.h @@ -5,15 +5,17 @@ #include "Luau/Constraint.h" #include "Luau/ControlFlow.h" #include "Luau/DataFlowGraph.h" +#include "Luau/InsertionOrderedMap.h" #include "Luau/Module.h" #include "Luau/ModuleResolver.h" +#include "Luau/Normalize.h" #include "Luau/NotNull.h" #include "Luau/Refinement.h" #include "Luau/Symbol.h" -#include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/TypeUtils.h" #include "Luau/Variant.h" -#include "Normalize.h" +#include "Luau/Normalize.h" #include #include @@ -69,11 +71,18 @@ struct ConstraintGraphBuilder // This is null when the CGB is initially constructed. Scope* rootScope; + struct InferredBinding + { + Scope* scope; + Location location; + TypeIds types; + }; + // 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; + // postprocessing step because we have not yet allocated the types that will + // be assigned to those unannotated symbols, so we queue them up here. + std::map inferredBindings; // Constraints that go straight to the solver. std::vector constraints; @@ -155,6 +164,18 @@ struct ConstraintGraphBuilder */ NotNull addConstraint(const ScopePtr& scope, std::unique_ptr c); + struct RefinementPartition + { + // Types that we want to intersect against the type of the expression. + std::vector discriminantTypes; + + // Sometimes the type we're discriminating against is implicitly nil. + bool shouldAppendNilType = false; + }; + + using RefinementContext = InsertionOrderedMap; + void unionRefinements(const RefinementContext& lhs, const RefinementContext& rhs, RefinementContext& dest, std::vector* constraints); + void computeRefinement(const ScopePtr& scope, RefinementId refinement, RefinementContext* refis, bool sense, bool eq, std::vector* constraints); void applyRefinements(const ScopePtr& scope, Location location, RefinementId refinement); ControlFlow visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block); @@ -211,13 +232,15 @@ struct ConstraintGraphBuilder Inference check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType); std::tuple checkBinary(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType); - TypeId checkLValue(const ScopePtr& scope, AstExpr* expr); - TypeId checkLValue(const ScopePtr& scope, AstExprLocal* local); - TypeId checkLValue(const ScopePtr& scope, AstExprGlobal* global); - TypeId checkLValue(const ScopePtr& scope, AstExprIndexName* indexName); - TypeId checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr); + std::optional checkLValue(const ScopePtr& scope, AstExpr* expr); + std::optional checkLValue(const ScopePtr& scope, AstExprLocal* local); + std::optional checkLValue(const ScopePtr& scope, AstExprGlobal* global); + std::optional checkLValue(const ScopePtr& scope, AstExprIndexName* indexName); + std::optional checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr); TypeId updateProperty(const ScopePtr& scope, AstExpr* expr); + void updateLValueType(AstExpr* lvalue, TypeId ty); + struct FunctionSignature { // The type of the function. diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index 672b46030..3f2feaef1 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -111,8 +111,6 @@ struct ConstraintSolver bool tryDispatch(const PackSubtypeConstraint& c, NotNull constraint, bool force); bool tryDispatch(const GeneralizationConstraint& c, NotNull constraint, bool force); bool tryDispatch(const InstantiationConstraint& c, NotNull constraint, bool force); - bool tryDispatch(const UnaryConstraint& c, NotNull constraint, bool force); - bool tryDispatch(const BinaryConstraint& c, NotNull constraint, bool force); bool tryDispatch(const IterableConstraint& c, NotNull constraint, bool force); bool tryDispatch(const NameConstraint& c, NotNull constraint); bool tryDispatch(const TypeAliasExpansionConstraint& c, NotNull constraint); @@ -124,6 +122,7 @@ struct ConstraintSolver bool tryDispatch(const SingletonOrTopTypeConstraint& c, NotNull constraint); bool tryDispatch(const UnpackConstraint& c, NotNull constraint); bool tryDispatch(const RefineConstraint& c, NotNull constraint, bool force); + bool tryDispatch(const SetOpConstraint& c, NotNull constraint, bool force); bool tryDispatch(const ReduceConstraint& c, NotNull constraint, bool force); bool tryDispatch(const ReducePackConstraint& c, NotNull constraint, bool force); diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index 63be6b942..34a0484a3 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -3,29 +3,46 @@ // Do not include LValue. It should never be used here. #include "Luau/Ast.h" -#include "Luau/Breadcrumb.h" #include "Luau/DenseHash.h" #include "Luau/Def.h" #include "Luau/Symbol.h" +#include "Luau/TypedAllocator.h" #include namespace Luau { +struct RefinementKey +{ + const RefinementKey* parent = nullptr; + DefId def; + std::optional propName; +}; + +struct RefinementKeyArena +{ + TypedAllocator allocator; + + const RefinementKey* leaf(DefId def); + const RefinementKey* node(const RefinementKey* parent, DefId def, const std::string& propName); +}; + struct DataFlowGraph { DataFlowGraph(DataFlowGraph&&) = default; DataFlowGraph& operator=(DataFlowGraph&&) = default; - NullableBreadcrumbId getBreadcrumb(const AstExpr* expr) const; + DefId getDef(const AstExpr* expr) const; + // Look up for the rvalue breadcrumb for a compound assignment. + std::optional getRValueDefForCompoundAssign(const AstExpr* expr) const; - BreadcrumbId getBreadcrumb(const AstLocal* local) const; - BreadcrumbId getBreadcrumb(const AstExprLocal* local) const; - BreadcrumbId getBreadcrumb(const AstExprGlobal* global) const; + DefId getDef(const AstLocal* local) const; - BreadcrumbId getBreadcrumb(const AstStatDeclareGlobal* global) const; - BreadcrumbId getBreadcrumb(const AstStatDeclareFunction* func) const; + DefId getDef(const AstStatDeclareGlobal* global) const; + DefId getDef(const AstStatDeclareFunction* func) const; + + const RefinementKey* getRefinementKey(const AstExpr* expr) const; private: DataFlowGraph() = default; @@ -33,17 +50,23 @@ struct DataFlowGraph DataFlowGraph(const DataFlowGraph&) = delete; DataFlowGraph& operator=(const DataFlowGraph&) = delete; - DefArena defs; - BreadcrumbArena breadcrumbs; + DefArena defArena; + RefinementKeyArena keyArena; - DenseHashMap astBreadcrumbs{nullptr}; + DenseHashMap astDefs{nullptr}; // Sometimes we don't have the AstExprLocal* but we have AstLocal*, and sometimes we need to extract that DefId. - DenseHashMap localBreadcrumbs{nullptr}; + DenseHashMap localDefs{nullptr}; // There's no AstStatDeclaration, and it feels useless to introduce it just to enforce an invariant in one place. // All keys in this maps are really only statements that ambiently declares a symbol. - DenseHashMap declaredBreadcrumbs{nullptr}; + DenseHashMap declaredDefs{nullptr}; + + // Compound assignments are in a weird situation where the local being assigned to is also being used at its + // previous type implicitly in an rvalue position. This map provides the previous binding. + DenseHashMap compoundAssignBreadcrumbs{nullptr}; + + DenseHashMap astRefinementKeys{nullptr}; friend struct DataFlowGraphBuilder; }; @@ -51,15 +74,19 @@ struct DataFlowGraph struct DfgScope { DfgScope* parent; - DenseHashMap bindings{Symbol{}}; - DenseHashMap> props{nullptr}; + DenseHashMap bindings{Symbol{}}; + DenseHashMap> props{nullptr}; - NullableBreadcrumbId lookup(Symbol symbol) const; - NullableBreadcrumbId lookup(DefId def, const std::string& key) const; + std::optional lookup(Symbol symbol) const; + std::optional lookup(DefId def, const std::string& key) const; +}; + +struct DataFlowResult +{ + DefId def; + const RefinementKey* parent = nullptr; }; -// Currently unsound. We do not presently track the control flow of the program. -// Additionally, we do not presently track assignments. struct DataFlowGraphBuilder { static DataFlowGraph build(AstStatBlock* root, NotNull handle); @@ -71,8 +98,8 @@ struct DataFlowGraphBuilder DataFlowGraphBuilder& operator=(const DataFlowGraphBuilder&) = delete; DataFlowGraph graph; - NotNull defs{&graph.defs}; - NotNull breadcrumbs{&graph.breadcrumbs}; + NotNull defArena{&graph.defArena}; + NotNull keyArena{&graph.keyArena}; struct InternalErrorReporter* handle = nullptr; DfgScope* moduleScope = nullptr; @@ -105,27 +132,28 @@ struct DataFlowGraphBuilder void visit(DfgScope* scope, AstStatDeclareClass* d); void visit(DfgScope* scope, AstStatError* error); - BreadcrumbId visitExpr(DfgScope* scope, AstExpr* e); - BreadcrumbId visitExpr(DfgScope* scope, AstExprLocal* l); - BreadcrumbId visitExpr(DfgScope* scope, AstExprGlobal* g); - BreadcrumbId visitExpr(DfgScope* scope, AstExprCall* c); - BreadcrumbId visitExpr(DfgScope* scope, AstExprIndexName* i); - BreadcrumbId visitExpr(DfgScope* scope, AstExprIndexExpr* i); - BreadcrumbId visitExpr(DfgScope* scope, AstExprFunction* f); - BreadcrumbId visitExpr(DfgScope* scope, AstExprTable* t); - BreadcrumbId visitExpr(DfgScope* scope, AstExprUnary* u); - BreadcrumbId visitExpr(DfgScope* scope, AstExprBinary* b); - BreadcrumbId visitExpr(DfgScope* scope, AstExprTypeAssertion* t); - BreadcrumbId visitExpr(DfgScope* scope, AstExprIfElse* i); - BreadcrumbId visitExpr(DfgScope* scope, AstExprInterpString* i); - BreadcrumbId visitExpr(DfgScope* scope, AstExprError* error); - - 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); + DataFlowResult visitExpr(DfgScope* scope, AstExpr* e); + DataFlowResult visitExpr(DfgScope* scope, AstExprGroup* group); + DataFlowResult visitExpr(DfgScope* scope, AstExprLocal* l); + DataFlowResult visitExpr(DfgScope* scope, AstExprGlobal* g); + DataFlowResult visitExpr(DfgScope* scope, AstExprCall* c); + DataFlowResult visitExpr(DfgScope* scope, AstExprIndexName* i); + DataFlowResult visitExpr(DfgScope* scope, AstExprIndexExpr* i); + DataFlowResult visitExpr(DfgScope* scope, AstExprFunction* f); + DataFlowResult visitExpr(DfgScope* scope, AstExprTable* t); + DataFlowResult visitExpr(DfgScope* scope, AstExprUnary* u); + DataFlowResult visitExpr(DfgScope* scope, AstExprBinary* b); + DataFlowResult visitExpr(DfgScope* scope, AstExprTypeAssertion* t); + DataFlowResult visitExpr(DfgScope* scope, AstExprIfElse* i); + DataFlowResult visitExpr(DfgScope* scope, AstExprInterpString* i); + DataFlowResult visitExpr(DfgScope* scope, AstExprError* error); + + void visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef, bool isCompoundAssignment = false); + void visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment); + void visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment); + void visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef); + void visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef); + void visitLValue(DfgScope* scope, AstExprError* e, DefId incomingDef); void visitType(DfgScope* scope, AstType* t); void visitType(DfgScope* scope, AstTypeReference* r); diff --git a/Analysis/include/Luau/Def.h b/Analysis/include/Luau/Def.h index 10d81367e..0a286ae98 100644 --- a/Analysis/include/Luau/Def.h +++ b/Analysis/include/Luau/Def.h @@ -23,6 +23,7 @@ using DefId = NotNull; */ struct Cell { + bool subscripted = false; }; /** @@ -71,11 +72,13 @@ const T* get(DefId def) return get_if(&def->v); } +bool containsSubscriptedDefinition(DefId def); + struct DefArena { TypedAllocator allocator; - DefId freshCell(); + DefId freshCell(bool subscripted = false); // TODO: implement once we have cases where we need to merge in definitions // DefId phi(const std::vector& defs); }; diff --git a/Analysis/include/Luau/Differ.h b/Analysis/include/Luau/Differ.h index 43c44b71d..587ed5ae7 100644 --- a/Analysis/include/Luau/Differ.h +++ b/Analysis/include/Luau/Differ.h @@ -2,11 +2,12 @@ #pragma once #include "Luau/DenseHash.h" -#include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/UnifierSharedState.h" + #include #include -#include +#include namespace Luau { diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index 71442fb1d..4b6c64c33 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -5,6 +5,9 @@ #include "Luau/NotNull.h" #include "Luau/Type.h" #include "Luau/Variant.h" +#include "Luau/Ast.h" + +#include namespace Luau { diff --git a/Analysis/include/Luau/GlobalTypes.h b/Analysis/include/Luau/GlobalTypes.h index 7a34f9354..55a6d6c73 100644 --- a/Analysis/include/Luau/GlobalTypes.h +++ b/Analysis/include/Luau/GlobalTypes.h @@ -6,12 +6,11 @@ #include "Luau/NotNull.h" #include "Luau/Scope.h" #include "Luau/TypeArena.h" +#include "Luau/TypeFwd.h" namespace Luau { -struct BuiltinTypes; - struct GlobalTypes { explicit GlobalTypes(NotNull builtinTypes); diff --git a/Analysis/include/Luau/Instantiation.h b/Analysis/include/Luau/Instantiation.h index 1dbf6b67b..2122f0faf 100644 --- a/Analysis/include/Luau/Instantiation.h +++ b/Analysis/include/Luau/Instantiation.h @@ -3,13 +3,12 @@ #include "Luau/NotNull.h" #include "Luau/Substitution.h" -#include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/Unifiable.h" namespace Luau { -struct BuiltinTypes; struct TxnLog; struct TypeArena; struct TypeCheckLimits; diff --git a/Analysis/include/Luau/IostreamHelpers.h b/Analysis/include/Luau/IostreamHelpers.h index 42b362bee..a16455dfa 100644 --- a/Analysis/include/Luau/IostreamHelpers.h +++ b/Analysis/include/Luau/IostreamHelpers.h @@ -5,6 +5,7 @@ #include "Luau/Location.h" #include "Luau/Type.h" #include "Luau/Ast.h" +#include "Luau/TypePath.h" #include @@ -48,4 +49,14 @@ std::ostream& operator<<(std::ostream& lhs, const TypePackVar& tv); std::ostream& operator<<(std::ostream& lhs, const TypeErrorData& ted); +std::ostream& operator<<(std::ostream& lhs, TypeId ty); +std::ostream& operator<<(std::ostream& lhs, TypePackId tp); + +namespace TypePath +{ + +std::ostream& operator<<(std::ostream& lhs, const Path& path); + +}; // namespace TypePath + } // namespace Luau diff --git a/Analysis/include/Luau/LValue.h b/Analysis/include/Luau/LValue.h index 9a8b863b3..e20d9901e 100644 --- a/Analysis/include/Luau/LValue.h +++ b/Analysis/include/Luau/LValue.h @@ -3,6 +3,7 @@ #include "Luau/Variant.h" #include "Luau/Symbol.h" +#include "Luau/TypeFwd.h" #include #include @@ -10,9 +11,6 @@ namespace Luau { -struct Type; -using TypeId = const Type*; - struct Field; // Deprecated. Do not use in new work. diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index 0f9352d15..ebb80e0f7 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -2,10 +2,15 @@ #pragma once #include "Luau/NotNull.h" -#include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/UnifierSharedState.h" +#include +#include #include +#include +#include +#include namespace Luau { @@ -13,7 +18,6 @@ namespace Luau struct InternalErrorReporter; struct Module; struct Scope; -struct BuiltinTypes; using ModulePtr = std::shared_ptr; @@ -33,10 +37,15 @@ class TypeIds using iterator = std::vector::iterator; using const_iterator = std::vector::const_iterator; - TypeIds(const TypeIds&) = default; - TypeIds(TypeIds&&) = default; TypeIds() = default; ~TypeIds() = default; + + TypeIds(std::initializer_list tys); + + TypeIds(const TypeIds&) = default; + TypeIds& operator=(const TypeIds&) = default; + + TypeIds(TypeIds&&) = default; TypeIds& operator=(TypeIds&&) = default; void insert(TypeId ty); diff --git a/Analysis/include/Luau/Predicate.h b/Analysis/include/Luau/Predicate.h index 50fd7edd8..52ee1f298 100644 --- a/Analysis/include/Luau/Predicate.h +++ b/Analysis/include/Luau/Predicate.h @@ -4,15 +4,13 @@ #include "Luau/Location.h" #include "Luau/LValue.h" #include "Luau/Variant.h" +#include "Luau/TypeFwd.h" #include namespace Luau { -struct Type; -using TypeId = const Type*; - struct TruthyPredicate; struct IsAPredicate; struct TypeGuardPredicate; diff --git a/Analysis/include/Luau/Quantify.h b/Analysis/include/Luau/Quantify.h index b562c54c7..bae3751da 100644 --- a/Analysis/include/Luau/Quantify.h +++ b/Analysis/include/Luau/Quantify.h @@ -1,10 +1,12 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/DenseHash.h" +#include "Luau/Unifiable.h" #include +#include namespace Luau { diff --git a/Analysis/include/Luau/Refinement.h b/Analysis/include/Luau/Refinement.h index fecf459ad..3fea78688 100644 --- a/Analysis/include/Luau/Refinement.h +++ b/Analysis/include/Luau/Refinement.h @@ -4,14 +4,13 @@ #include "Luau/NotNull.h" #include "Luau/TypedAllocator.h" #include "Luau/Variant.h" +#include "Luau/TypeFwd.h" namespace Luau { -using BreadcrumbId = NotNull; - -struct Type; -using TypeId = const Type*; +struct RefinementKey; +using DefId = NotNull; struct Variadic; struct Negation; @@ -52,7 +51,7 @@ struct Equivalence struct Proposition { - BreadcrumbId breadcrumb; + const RefinementKey* key; TypeId discriminantTy; }; @@ -69,7 +68,7 @@ struct RefinementArena RefinementId conjunction(RefinementId lhs, RefinementId rhs); RefinementId disjunction(RefinementId lhs, RefinementId rhs); RefinementId equivalence(RefinementId lhs, RefinementId rhs); - RefinementId proposition(BreadcrumbId breadcrumb, TypeId discriminantTy); + RefinementId proposition(const RefinementKey* key, TypeId discriminantTy); private: TypedAllocator allocator; diff --git a/Analysis/include/Luau/Scope.h b/Analysis/include/Luau/Scope.h index a0b115ad7..8cdffcb49 100644 --- a/Analysis/include/Luau/Scope.h +++ b/Analysis/include/Luau/Scope.h @@ -2,9 +2,13 @@ #pragma once #include "Luau/Def.h" +#include "Luau/LValue.h" #include "Luau/Location.h" #include "Luau/NotNull.h" #include "Luau/Type.h" +#include "Luau/DenseHash.h" +#include "Luau/Symbol.h" +#include "Luau/Unifiable.h" #include #include @@ -54,6 +58,7 @@ struct Scope std::optional lookup(Symbol sym) const; std::optional lookupLValue(DefId def) const; std::optional lookup(DefId def) const; + std::optional> lookupEx(DefId def); std::optional> lookupEx(Symbol sym); std::optional lookupType(const Name& name) const; diff --git a/Analysis/include/Luau/Simplify.h b/Analysis/include/Luau/Simplify.h index 27ed44f8f..064648d73 100644 --- a/Analysis/include/Luau/Simplify.h +++ b/Analysis/include/Luau/Simplify.h @@ -2,7 +2,8 @@ #pragma once -#include "Luau/Type.h" +#include "Luau/NotNull.h" +#include "Luau/TypeFwd.h" #include @@ -10,7 +11,6 @@ namespace Luau { struct TypeArena; -struct BuiltinTypes; struct SimplifyResult { diff --git a/Analysis/include/Luau/Substitution.h b/Analysis/include/Luau/Substitution.h index b18c64846..d4c0f166b 100644 --- a/Analysis/include/Luau/Substitution.h +++ b/Analysis/include/Luau/Substitution.h @@ -2,8 +2,7 @@ #pragma once #include "Luau/TypeArena.h" -#include "Luau/TypePack.h" -#include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/DenseHash.h" // We provide an implementation of substitution on types, diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index f8aa584db..321563e7c 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -1,10 +1,10 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include "Luau/Type.h" -#include "Luau/TypePack.h" +#include "Luau/TypeFwd.h" #include "Luau/TypePairHash.h" #include "Luau/UnifierSharedState.h" +#include "Luau/TypePath.h" #include #include @@ -23,6 +23,13 @@ struct NormalizedClassType; struct NormalizedStringType; struct NormalizedFunctionType; +struct SubtypingReasoning +{ + Path subPath; + Path superPath; + + bool operator==(const SubtypingReasoning& other) const; +}; struct SubtypingResult { @@ -31,8 +38,18 @@ struct SubtypingResult bool normalizationTooComplex = false; bool isCacheable = true; + /// The reason for isSubtype to be false. May not be present even if + /// isSubtype is false, depending on the input types. + std::optional reasoning; + SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other); + SubtypingResult& withBothComponent(TypePath::Component component); + SubtypingResult& withSuperComponent(TypePath::Component component); + SubtypingResult& withSubComponent(TypePath::Component component); + SubtypingResult& withBothPath(TypePath::Path path); + SubtypingResult& withSubPath(TypePath::Path path); + SubtypingResult& withSuperPath(TypePath::Path path); // Only negates the `isSubtype`. static SubtypingResult negate(const SubtypingResult& result); diff --git a/Analysis/include/Luau/ToDot.h b/Analysis/include/Luau/ToDot.h index 1a9c2811a..6fa99ec3f 100644 --- a/Analysis/include/Luau/ToDot.h +++ b/Analysis/include/Luau/ToDot.h @@ -2,16 +2,12 @@ #pragma once #include "Luau/Common.h" +#include "Luau/TypeFwd.h" #include namespace Luau { -struct Type; -using TypeId = const Type*; - -struct TypePackVar; -using TypePackId = const TypePackVar*; struct ToDotOptions { diff --git a/Analysis/include/Luau/ToString.h b/Analysis/include/Luau/ToString.h index a256e12f2..091d8dd1f 100644 --- a/Analysis/include/Luau/ToString.h +++ b/Analysis/include/Luau/ToString.h @@ -2,6 +2,7 @@ #pragma once #include "Luau/Common.h" +#include "Luau/TypeFwd.h" #include #include @@ -20,13 +21,6 @@ class AstExpr; struct Scope; -struct Type; -using TypeId = const Type*; - -struct TypePackVar; -using TypePackId = const TypePackVar*; - -struct FunctionType; struct Constraint; struct Position; @@ -149,4 +143,14 @@ std::string generateName(size_t n); std::string toString(const Position& position); std::string toString(const Location& location, int offset = 0, bool useBegin = true); +std::string toString(const TypeOrPack& tyOrTp, ToStringOptions& opts); + +inline std::string toString(const TypeOrPack& tyOrTp) +{ + ToStringOptions opts{}; + return toString(tyOrTp, opts); +} + +std::string dump(const TypeOrPack& tyOrTp); + } // namespace Luau diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index 2eb7d749f..9b8564fb8 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -1,6 +1,8 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/TypeFwd.h" + #include "Luau/Ast.h" #include "Luau/Common.h" #include "Luau/Refinement.h" @@ -9,6 +11,7 @@ #include "Luau/Predicate.h" #include "Luau/Unifiable.h" #include "Luau/Variant.h" +#include "Luau/TypeFwd.h" #include #include @@ -59,22 +62,6 @@ struct TypeFamily; * ``` */ -// So... why `const T*` here rather than `T*`? -// It's because we've had problems caused by the type graph being mutated -// in ways it shouldn't be, for example mutating types from other modules. -// To try to control this, we make the use of types immutable by default, -// then provide explicit mutable access via getMutable and asMutable. -// This means we can grep for all the places we're mutating the type graph, -// and it makes it possible to provide other APIs (e.g. the txn log) -// which control mutable access to the type graph. -struct TypePackVar; -using TypePackId = const TypePackVar*; - -struct Type; - -// Should never be null -using TypeId = const Type*; - using Name = std::string; // A free type is one whose exact shape has yet to be fully determined. @@ -244,22 +231,6 @@ const T* get(const SingletonType* stv) return nullptr; } -struct GenericTypeDefinition -{ - TypeId ty; - std::optional defaultValue; - - bool operator==(const GenericTypeDefinition& rhs) const; -}; - -struct GenericTypePackDefinition -{ - TypePackId tp; - std::optional defaultValue; - - bool operator==(const GenericTypePackDefinition& rhs) const; -}; - struct FunctionArgument { Name name; @@ -549,42 +520,6 @@ struct TypeFamilyInstanceType std::vector packArguments; }; -struct TypeFun -{ - // These should all be generic - std::vector typeParams; - std::vector typePackParams; - - /** The underlying type. - * - * WARNING! This is not safe to use as a type if typeParams is not empty!! - * You must first use TypeChecker::instantiateTypeFun to turn it into a real type. - */ - TypeId type; - - TypeFun() = default; - - explicit TypeFun(TypeId ty) - : type(ty) - { - } - - TypeFun(std::vector typeParams, TypeId type) - : typeParams(std::move(typeParams)) - , type(type) - { - } - - TypeFun(std::vector typeParams, std::vector typePackParams, TypeId type) - : typeParams(std::move(typeParams)) - , typePackParams(std::move(typePackParams)) - , type(type) - { - } - - bool operator==(const TypeFun& rhs) const; -}; - /** Represents a pending type alias instantiation. * * In order to afford (co)recursive type aliases, we need to reason about a @@ -729,6 +664,58 @@ struct Type final Type& operator=(const Type& rhs); }; +struct GenericTypeDefinition +{ + TypeId ty; + std::optional defaultValue; + + bool operator==(const GenericTypeDefinition& rhs) const; +}; + +struct GenericTypePackDefinition +{ + TypePackId tp; + std::optional defaultValue; + + bool operator==(const GenericTypePackDefinition& rhs) const; +}; + +struct TypeFun +{ + // These should all be generic + std::vector typeParams; + std::vector typePackParams; + + /** The underlying type. + * + * WARNING! This is not safe to use as a type if typeParams is not empty!! + * You must first use TypeChecker::instantiateTypeFun to turn it into a real type. + */ + TypeId type; + + TypeFun() = default; + + explicit TypeFun(TypeId ty) + : type(ty) + { + } + + TypeFun(std::vector typeParams, TypeId type) + : typeParams(std::move(typeParams)) + , type(type) + { + } + + TypeFun(std::vector typeParams, std::vector typePackParams, TypeId type) + : typeParams(std::move(typeParams)) + , typePackParams(std::move(typePackParams)) + , type(type) + { + } + + bool operator==(const TypeFun& rhs) const; +}; + using SeenSet = std::set>; bool areEqual(SeenSet& seen, const Type& lhs, const Type& rhs); diff --git a/Analysis/include/Luau/TypeFamily.h b/Analysis/include/Luau/TypeFamily.h index ede104b95..4a7366471 100644 --- a/Analysis/include/Luau/TypeFamily.h +++ b/Analysis/include/Luau/TypeFamily.h @@ -1,13 +1,12 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include "ConstraintSolver.h" -#include "Error.h" +#include "Luau/ConstraintSolver.h" #include "Luau/Error.h" #include "Luau/NotNull.h" +#include "Luau/TypeCheckLimits.h" +#include "Luau/TypeFwd.h" #include "Luau/Variant.h" -#include "NotNull.h" -#include "TypeCheckLimits.h" #include #include @@ -16,14 +15,7 @@ namespace Luau { -struct Type; -using TypeId = const Type*; - -struct TypePackVar; -using TypePackId = const TypePackVar*; - struct TypeArena; -struct BuiltinTypes; struct TxnLog; class Normalizer; @@ -150,6 +142,8 @@ struct BuiltinTypeFamilies BuiltinTypeFamilies(); TypeFamily notFamily; + TypeFamily lenFamily; + TypeFamily unmFamily; TypeFamily addFamily; TypeFamily subFamily; diff --git a/Analysis/include/Luau/TypeFwd.h b/Analysis/include/Luau/TypeFwd.h new file mode 100644 index 000000000..e43181566 --- /dev/null +++ b/Analysis/include/Luau/TypeFwd.h @@ -0,0 +1,59 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Variant.h" + +#include + +namespace Luau +{ + +// So... why `const T*` here rather than `T*`? +// It's because we've had problems caused by the type graph being mutated +// in ways it shouldn't be, for example mutating types from other modules. +// To try to control this, we make the use of types immutable by default, +// then provide explicit mutable access via getMutable and asMutable. +// This means we can grep for all the places we're mutating the type graph, +// and it makes it possible to provide other APIs (e.g. the txn log) +// which control mutable access to the type graph. + +struct Type; +using TypeId = const Type*; + +struct FreeType; +struct GenericType; +struct PrimitiveType; +struct BlockedType; +struct PendingExpansionType; +struct SingletonType; +struct FunctionType; +struct TableType; +struct MetatableType; +struct ClassType; +struct AnyType; +struct UnionType; +struct IntersectionType; +struct LazyType; +struct UnknownType; +struct NeverType; +struct NegationType; +struct TypeFamilyInstanceType; + +struct TypePackVar; +using TypePackId = const TypePackVar*; + +struct FreeTypePack; +struct GenericTypePack; +struct TypePack; +struct VariadicTypePack; +struct BlockedTypePack; +struct TypeFamilyInstanceTypePack; + +using Name = std::string; +using ModuleName = std::string; + +struct BuiltinTypes; + +using TypeOrPack = Variant; + +} // namespace Luau diff --git a/Analysis/include/Luau/TypeInfer.h b/Analysis/include/Luau/TypeInfer.h index abae8b923..7de014068 100644 --- a/Analysis/include/Luau/TypeInfer.h +++ b/Analysis/include/Luau/TypeInfer.h @@ -9,9 +9,8 @@ #include "Luau/Substitution.h" #include "Luau/Symbol.h" #include "Luau/TxnLog.h" -#include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/TypeCheckLimits.h" -#include "Luau/TypePack.h" #include "Luau/TypeUtils.h" #include "Luau/Unifier.h" #include "Luau/UnifierSharedState.h" diff --git a/Analysis/include/Luau/TypeOrPack.h b/Analysis/include/Luau/TypeOrPack.h new file mode 100644 index 000000000..2bdca1df4 --- /dev/null +++ b/Analysis/include/Luau/TypeOrPack.h @@ -0,0 +1,45 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Type.h" +#include "Luau/TypePack.h" +#include "Luau/Variant.h" + +#include + +namespace Luau +{ + +const void* ptr(TypeOrPack ty); + +template +const T* get(TypeOrPack ty) +{ + if constexpr (std::is_same_v) + return ty.get_if(); + else if constexpr (std::is_same_v) + return ty.get_if(); + else if constexpr (TypeVariant::is_part_of_v) + { + if (auto innerTy = ty.get_if()) + return get(*innerTy); + else + return nullptr; + } + else if constexpr (TypePackVariant::is_part_of_v) + { + if (auto innerTp = ty.get_if()) + return get(*innerTp); + else + return nullptr; + } + else + { + static_assert(always_false_v, "invalid T to get from TypeOrPack"); + LUAU_UNREACHABLE(); + } +} + +TypeOrPack follow(TypeOrPack ty); + +} // namespace Luau diff --git a/Analysis/include/Luau/TypePack.h b/Analysis/include/Luau/TypePack.h index d159aa45d..11721f401 100644 --- a/Analysis/include/Luau/TypePack.h +++ b/Analysis/include/Luau/TypePack.h @@ -1,12 +1,15 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include "Luau/Type.h" #include "Luau/Unifiable.h" #include "Luau/Variant.h" +#include "Luau/TypeFwd.h" +#include "Luau/NotNull.h" +#include "Luau/Common.h" #include #include +#include namespace Luau { @@ -20,9 +23,6 @@ struct VariadicTypePack; struct BlockedTypePack; struct TypeFamilyInstanceTypePack; -struct TypePackVar; -using TypePackId = const TypePackVar*; - struct FreeTypePack { explicit FreeTypePack(TypeLevel level); diff --git a/Analysis/include/Luau/TypePairHash.h b/Analysis/include/Luau/TypePairHash.h index 029818ba1..5cddebef0 100644 --- a/Analysis/include/Luau/TypePairHash.h +++ b/Analysis/include/Luau/TypePairHash.h @@ -1,7 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include "Type.h" +#include "Luau/TypeFwd.h" #include diff --git a/Analysis/include/Luau/TypePath.h b/Analysis/include/Luau/TypePath.h new file mode 100644 index 000000000..96fcdcb15 --- /dev/null +++ b/Analysis/include/Luau/TypePath.h @@ -0,0 +1,220 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/TypeFwd.h" +#include "Luau/Variant.h" +#include "Luau/NotNull.h" +#include "Luau/TypeOrPack.h" + +#include +#include +#include + +namespace Luau +{ + +namespace TypePath +{ + +/// Represents a property of a class, table, or anything else with a concept of +/// a named property. +struct Property +{ + /// The name of the property. + std::string name; + /// Whether to look at the read or the write type. + bool isRead = true; + + explicit Property(std::string name); + Property(std::string name, bool read) + : name(std::move(name)) + , isRead(read) + { + } + + static Property read(std::string name); + static Property write(std::string name); + + bool operator==(const Property& other) const; +}; + +/// Represents an index into a type or a pack. For a type, this indexes into a +/// union or intersection's list. For a pack, this indexes into the pack's nth +/// element. +struct Index +{ + /// The 0-based index to use for the lookup. + size_t index; + + bool operator==(const Index& other) const; +}; + +/// Represents fields of a type or pack that contain a type. +enum class TypeField +{ + /// The metatable of a type. This could be a metatable type, a primitive + /// type, a class type, or perhaps even a string singleton type. + Metatable, + /// The lower bound of this type, if one is present. + LowerBound, + /// The upper bound of this type, if present. + UpperBound, + /// The index type. + IndexLookup, + /// The indexer result type. + IndexResult, + /// The negated type, for negations. + Negated, + /// The variadic type for a type pack. + Variadic, +}; + +/// Represents fields of a type or type pack that contain a type pack. +enum class PackField +{ + /// What arguments this type accepts. + Arguments, + /// What this type returns when called. + Returns, + /// The tail of a type pack. + Tail, +}; + +/// A single component of a path, representing one inner type or type pack to +/// traverse into. +using Component = Luau::Variant; + +/// A path through a type or type pack accessing a particular type or type pack +/// contained within. +/// +/// Paths are always relative; to make use of a Path, you need to specify an +/// entry point. They are not canonicalized; two Paths may not compare equal but +/// may point to the same result, depending on the layout of the entry point. +/// +/// Paths always descend through an entry point. This doesn't mean that they +/// cannot reach "upwards" in the actual type hierarchy in some cases, but it +/// does mean that there is no equivalent to `../` in file system paths. This is +/// intentional and unavoidable, because types and type packs don't have a +/// concept of a parent - they are a directed cyclic graph, with no hierarchy +/// that actually holds in all cases. +struct Path +{ + /// The Components of this Path. + std::vector components; + + /// Creates a new empty Path. + Path() + { + } + + /// Creates a new Path from a list of components. + explicit Path(std::vector components) + : components(std::move(components)) + { + } + + /// Creates a new single-component Path. + explicit Path(Component component) + : components({component}) + { + } + + /// Creates a new Path by appending another Path to this one. + /// @param suffix the Path to append + /// @return a new Path representing `this + suffix` + Path append(const Path& suffix) const; + + /// Creates a new Path by appending a Component to this Path. + /// @param component the Component to append + /// @return a new Path with `component` appended to it. + Path push(Component component) const; + + /// Creates a new Path by prepending a Component to this Path. + /// @param component the Component to prepend + /// @return a new Path with `component` prepended to it. + Path push_front(Component component) const; + + /// Creates a new Path by removing the last Component of this Path. + /// If the Path is empty, this is a no-op. + /// @return a Path with the last component removed. + Path pop() const; + + /// Returns the last Component of this Path, if present. + std::optional last() const; + + /// Returns whether this Path is empty, meaning it has no components at all. + /// Traversing an empty Path results in the type you started with. + bool empty() const; + + bool operator==(const Path& other) const; + bool operator!=(const Path& other) const + { + return !(*this == other); + } +}; + +/// The canonical "empty" Path, meaning a Path with no components. +static const Path kEmpty{}; + +struct PathBuilder +{ + std::vector components; + + Path build(); + + PathBuilder& readProp(std::string name); + PathBuilder& writeProp(std::string name); + PathBuilder& prop(std::string name); + PathBuilder& index(size_t i); + PathBuilder& mt(); + PathBuilder& lb(); + PathBuilder& ub(); + PathBuilder& indexKey(); + PathBuilder& indexValue(); + PathBuilder& negated(); + PathBuilder& variadic(); + PathBuilder& args(); + PathBuilder& rets(); + PathBuilder& tail(); +}; + +} // namespace TypePath + +using Path = TypePath::Path; + +/// Converts a Path to a string for debugging purposes. This output may not be +/// terribly clear to end users of the Luau type system. +std::string toString(const TypePath::Path& path); + +std::optional traverse(TypeId root, const Path& path, NotNull builtinTypes); +std::optional traverse(TypePackId root, const Path& path, NotNull builtinTypes); + +/// Traverses a path from a type to its end point, which must be a type. +/// @param root the entry point of the traversal +/// @param path the path to traverse +/// @param builtinTypes the built-in types in use (used to acquire the string metatable) +/// @returns the TypeId at the end of the path, or nullopt if the traversal failed. +std::optional traverseForType(TypeId root, const Path& path, NotNull builtinTypes); + +/// Traverses a path from a type pack to its end point, which must be a type. +/// @param root the entry point of the traversal +/// @param path the path to traverse +/// @param builtinTypes the built-in types in use (used to acquire the string metatable) +/// @returns the TypeId at the end of the path, or nullopt if the traversal failed. +std::optional traverseForType(TypePackId root, const Path& path, NotNull builtinTypes); + +/// Traverses a path from a type to its end point, which must be a type pack. +/// @param root the entry point of the traversal +/// @param path the path to traverse +/// @param builtinTypes the built-in types in use (used to acquire the string metatable) +/// @returns the TypePackId at the end of the path, or nullopt if the traversal failed. +std::optional traverseForPack(TypeId root, const Path& path, NotNull builtinTypes); + +/// Traverses a path from a type pack to its end point, which must be a type pack. +/// @param root the entry point of the traversal +/// @param path the path to traverse +/// @param builtinTypes the built-in types in use (used to acquire the string metatable) +/// @returns the TypePackId at the end of the path, or nullopt if the traversal failed. +std::optional traverseForPack(TypePackId root, const Path& path, NotNull builtinTypes); + +} // namespace Luau diff --git a/Analysis/include/Luau/Unifier.h b/Analysis/include/Luau/Unifier.h index 1260ac93c..d1a3b3584 100644 --- a/Analysis/include/Luau/Unifier.h +++ b/Analysis/include/Luau/Unifier.h @@ -9,7 +9,7 @@ #include "Luau/TxnLog.h" #include "Luau/TypeArena.h" #include "Luau/UnifierSharedState.h" -#include "Normalize.h" +#include "Luau/Normalize.h" #include diff --git a/Analysis/include/Luau/Unifier2.h b/Analysis/include/Luau/Unifier2.h index 7b6fa608a..3d5b5a1a1 100644 --- a/Analysis/include/Luau/Unifier2.h +++ b/Analysis/include/Luau/Unifier2.h @@ -4,10 +4,10 @@ #include "Luau/DenseHash.h" #include "Luau/NotNull.h" -#include "Type.h" -#include "TypePairHash.h" -#include "TypeCheckLimits.h" -#include "TypeChecker2.h" +#include "Luau/TypePairHash.h" +#include "Luau/TypeCheckLimits.h" +#include "Luau/TypeChecker2.h" +#include "Luau/TypeFwd.h" #include #include @@ -16,10 +16,6 @@ namespace Luau { -using TypeId = const struct Type*; -using TypePackId = const struct TypePackVar*; - -struct BuiltinTypes; struct InternalErrorReporter; struct Scope; struct TypeArena; diff --git a/Analysis/include/Luau/UnifierSharedState.h b/Analysis/include/Luau/UnifierSharedState.h index ada56ec56..de69c17cf 100644 --- a/Analysis/include/Luau/UnifierSharedState.h +++ b/Analysis/include/Luau/UnifierSharedState.h @@ -3,8 +3,7 @@ #include "Luau/DenseHash.h" #include "Luau/Error.h" -#include "Luau/Type.h" -#include "Luau/TypePack.h" +#include "Luau/TypeFwd.h" #include diff --git a/Analysis/include/Luau/Variant.h b/Analysis/include/Luau/Variant.h index 95fdfac4d..eeaa2c01a 100644 --- a/Analysis/include/Luau/Variant.h +++ b/Analysis/include/Luau/Variant.h @@ -44,6 +44,9 @@ class Variant public: using first_alternative = typename First::type; + template + static constexpr bool is_part_of_v = std::disjunction_v, T>...>; + Variant() { static_assert(std::is_default_constructible_v, "first alternative type must be default constructible"); diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 4d6bcf03a..01b0bdfd5 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -701,6 +701,7 @@ void TypeCloner::operator()(const FunctionType& t) ftv->argNames = t.argNames; ftv->retTypes = clone(t.retTypes, dest, cloneState); ftv->hasNoFreeOrGenericTypes = t.hasNoFreeOrGenericTypes; + ftv->isCheckedFunction = t.isCheckedFunction; } void TypeCloner::operator()(const TableType& t) diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index 5eb2ce49f..4f4ff3065 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -2,7 +2,7 @@ #include "Luau/ConstraintGraphBuilder.h" #include "Luau/Ast.h" -#include "Luau/Breadcrumb.h" +#include "Luau/Def.h" #include "Luau/Common.h" #include "Luau/Constraint.h" #include "Luau/ControlFlow.h" @@ -216,19 +216,7 @@ NotNull ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, return NotNull{constraints.emplace_back(std::move(c)).get()}; } -struct RefinementPartition -{ - // Types that we want to intersect against the type of the expression. - std::vector discriminantTypes; - - // Sometimes the type we're discriminating against is implicitly nil. - bool shouldAppendNilType = false; -}; - -using RefinementContext = InsertionOrderedMap; - -static void unionRefinements(NotNull builtinTypes, NotNull arena, const RefinementContext& lhs, const RefinementContext& rhs, - RefinementContext& dest, std::vector* constraints) +void ConstraintGraphBuilder::unionRefinements(const RefinementContext& lhs, const RefinementContext& rhs, RefinementContext& dest, std::vector* constraints) { const auto intersect = [&](const std::vector& types) { if (1 == types.size()) @@ -264,44 +252,43 @@ static void unionRefinements(NotNull builtinTypes, NotNull builtinTypes, NotNull arena, const ScopePtr& scope, RefinementId refinement, - RefinementContext* refis, bool sense, bool eq, std::vector* constraints) +void ConstraintGraphBuilder::computeRefinement(const ScopePtr& scope, RefinementId refinement, RefinementContext* refis, bool sense, bool eq, std::vector* constraints) { if (!refinement) return; else if (auto variadic = get(refinement)) { for (RefinementId refi : variadic->refinements) - computeRefinement(builtinTypes, arena, scope, refi, refis, sense, eq, constraints); + computeRefinement(scope, refi, refis, sense, eq, constraints); } else if (auto negation = get(refinement)) - return computeRefinement(builtinTypes, arena, scope, negation->refinement, refis, !sense, eq, constraints); + return computeRefinement(scope, negation->refinement, refis, !sense, eq, constraints); else if (auto conjunction = get(refinement)) { RefinementContext lhsRefis; RefinementContext rhsRefis; - computeRefinement(builtinTypes, arena, scope, conjunction->lhs, sense ? refis : &lhsRefis, sense, eq, constraints); - computeRefinement(builtinTypes, arena, scope, conjunction->rhs, sense ? refis : &rhsRefis, sense, eq, constraints); + computeRefinement(scope, conjunction->lhs, sense ? refis : &lhsRefis, sense, eq, constraints); + computeRefinement(scope, conjunction->rhs, sense ? refis : &rhsRefis, sense, eq, constraints); if (!sense) - unionRefinements(builtinTypes, arena, lhsRefis, rhsRefis, *refis, constraints); + unionRefinements(lhsRefis, rhsRefis, *refis, constraints); } else if (auto disjunction = get(refinement)) { RefinementContext lhsRefis; RefinementContext rhsRefis; - computeRefinement(builtinTypes, arena, scope, disjunction->lhs, sense ? &lhsRefis : refis, sense, eq, constraints); - computeRefinement(builtinTypes, arena, scope, disjunction->rhs, sense ? &rhsRefis : refis, sense, eq, constraints); + computeRefinement(scope, disjunction->lhs, sense ? &lhsRefis : refis, sense, eq, constraints); + computeRefinement(scope, disjunction->rhs, sense ? &rhsRefis : refis, sense, eq, constraints); if (sense) - unionRefinements(builtinTypes, arena, lhsRefis, rhsRefis, *refis, constraints); + unionRefinements(lhsRefis, rhsRefis, *refis, constraints); } else if (auto equivalence = get(refinement)) { - computeRefinement(builtinTypes, arena, scope, equivalence->lhs, refis, sense, true, constraints); - computeRefinement(builtinTypes, arena, scope, equivalence->rhs, refis, sense, true, constraints); + computeRefinement(scope, equivalence->lhs, refis, sense, true, constraints); + computeRefinement(scope, equivalence->rhs, refis, sense, true, constraints); } else if (auto proposition = get(refinement)) { @@ -314,40 +301,27 @@ static void computeRefinement(NotNull builtinTypes, NotNullpush_back(SingletonOrTopTypeConstraint{discriminantTy, proposition->discriminantTy, !sense}); } - RefinementContext uncommittedRefis; - uncommittedRefis.insert(proposition->breadcrumb->def, {}); - uncommittedRefis.get(proposition->breadcrumb->def)->discriminantTypes.push_back(discriminantTy); - - // When the top-level expression is `t[x]`, we want to refine it into `nil`, not `never`. - if ((sense || !eq) && getMetadata(proposition->breadcrumb)) - uncommittedRefis.get(proposition->breadcrumb->def)->shouldAppendNilType = true; - - for (NullableBreadcrumbId current = proposition->breadcrumb; current && current->previous; current = current->previous) + for (const RefinementKey* key = proposition->key; key; key = key->parent) { - LUAU_ASSERT(get(current->def)); - - // If this current breadcrumb has no metadata, it's no-op for the purpose of building a discriminant type. - if (!current->metadata) - continue; - else if (auto field = getMetadata(current)) - { - TableType::Props props{{field->prop, Property{discriminantTy}}}; - discriminantTy = arena->addType(TableType{std::move(props), std::nullopt, TypeLevel{}, scope.get(), TableState::Sealed}); - uncommittedRefis.insert(current->previous->def, {}); - uncommittedRefis.get(current->previous->def)->discriminantTypes.push_back(discriminantTy); - } - } + refis->insert(key->def, {}); + refis->get(key->def)->discriminantTypes.push_back(discriminantTy); - // And now it's time to commit it. - for (auto& [def, partition] : uncommittedRefis) - { - (*refis).insert(def, {}); + // Reached leaf node + if (!key->propName) + break; - for (TypeId discriminantTy : partition.discriminantTypes) - (*refis).get(def)->discriminantTypes.push_back(discriminantTy); + TypeId nextDiscriminantTy = arena->addType(TableType{}); + NotNull table{getMutable(nextDiscriminantTy)}; + table->props[*key->propName] = {discriminantTy}; + table->scope = scope.get(); + table->state = TableState::Sealed; - (*refis).get(def)->shouldAppendNilType |= partition.shouldAppendNilType; + discriminantTy = nextDiscriminantTy; } + + // When the top-level expression is `t[x]`, we want to refine it into `nil`, not `never`. + LUAU_ASSERT(refis->get(proposition->key->def)); + refis->get(proposition->key->def)->shouldAppendNilType = (sense || !eq) && containsSubscriptedDefinition(proposition->key->def); } } @@ -415,7 +389,7 @@ void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location lo RefinementContext refinements; std::vector constraints; - computeRefinement(builtinTypes, arena, scope, refinement, &refinements, /*sense*/ true, /*eq*/ false, &constraints); + computeRefinement(scope, refinement, &refinements, /*sense*/ true, /*eq*/ false, &constraints); for (auto& [def, partition] : refinements) { @@ -586,20 +560,22 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) } } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* local) +ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* statLocal) { std::vector> varTypes; - varTypes.reserve(local->vars.size); + varTypes.reserve(statLocal->vars.size); std::vector assignees; - assignees.reserve(local->vars.size); + assignees.reserve(statLocal->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) + for (AstLocal* local : statLocal->vars) { + const Location location = local->location; + TypeId assignee = arena->addType(BlockedType{}); assignees.push_back(assignee); @@ -612,21 +588,27 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l varTypes.push_back(annotationTy); addConstraint(scope, local->location, SubtypeConstraint{assignee, annotationTy}); + + scope->bindings[local] = Binding{annotationTy, location}; } else + { varTypes.push_back(std::nullopt); - BreadcrumbId bc = dfg->getBreadcrumb(local); - scope->lvalueTypes[bc->def] = assignee; + inferredBindings[local] = {scope.get(), location, {assignee}}; + } + + DefId def = dfg->getDef(local); + scope->lvalueTypes[def] = assignee; } - TypePackId resultPack = checkPack(scope, local->values, varTypes).tp; - addConstraint(scope, local->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); + TypePackId resultPack = checkPack(scope, statLocal->values, varTypes).tp; + addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); - if (local->vars.size == 1 && local->values.size == 1 && firstValueType && scope.get() == rootScope) + if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope) { - AstLocal* var = local->vars.data[0]; - AstExpr* value = local->values.data[0]; + AstLocal* var = statLocal->vars.data[0]; + AstExpr* value = statLocal->values.data[0]; if (value->is()) addConstraint(scope, value->location, NameConstraint{*firstValueType, var->name.value, /*synthetic*/ true}); @@ -639,29 +621,12 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l } } - for (size_t i = 0; i < local->vars.size; ++i) - { - AstLocal* l = local->vars.data[i]; - Location location = l->location; - - std::optional annotation = varTypes[i]; - BreadcrumbId bc = dfg->getBreadcrumb(l); - - 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) + if (statLocal->values.size > 0) { // To correctly handle 'require', we need to import the exported type bindings into the variable 'namespace'. - for (size_t i = 0; i < local->values.size && i < local->vars.size; ++i) + for (size_t i = 0; i < statLocal->values.size && i < statLocal->vars.size; ++i) { - const AstExprCall* call = local->values.data[i]->as(); + const AstExprCall* call = statLocal->values.data[i]->as(); if (!call) continue; @@ -679,7 +644,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l if (!module) continue; - const Name name{local->vars.data[i]->name.value}; + const Name name{statLocal->vars.data[i]->name.value}; scope->importedTypeBindings[name] = module->exportedTypeBindings; scope->importedModules[name] = moduleInfo->name; @@ -719,9 +684,9 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for ScopePtr forScope = childScope(for_, scope); forScope->bindings[for_->var] = Binding{annotationTy, for_->var->location}; - BreadcrumbId bc = dfg->getBreadcrumb(for_->var); - forScope->lvalueTypes[bc->def] = annotationTy; - forScope->rvalueRefinements[bc->def] = annotationTy; + DefId def = dfg->getDef(for_->var); + forScope->lvalueTypes[def] = annotationTy; + forScope->rvalueRefinements[def] = annotationTy; visit(forScope, for_->body); @@ -750,8 +715,8 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f TypeId assignee = arena->addType(BlockedType{}); variableTypes.push_back(assignee); - BreadcrumbId bc = dfg->getBreadcrumb(var); - loopScope->lvalueTypes[bc->def] = assignee; + DefId def = dfg->getDef(var); + loopScope->lvalueTypes[def] = assignee; } TypePackId variablePack = arena->addTypePack(std::move(variableTypes)); @@ -803,11 +768,11 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFun FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location); sig.bodyScope->bindings[function->name] = Binding{sig.signature, function->func->location}; - BreadcrumbId bc = dfg->getBreadcrumb(function->name); - scope->lvalueTypes[bc->def] = functionType; - scope->rvalueRefinements[bc->def] = functionType; - sig.bodyScope->lvalueTypes[bc->def] = sig.signature; - sig.bodyScope->rvalueRefinements[bc->def] = sig.signature; + DefId def = dfg->getDef(function->name); + scope->lvalueTypes[def] = functionType; + scope->rvalueRefinements[def] = functionType; + sig.bodyScope->lvalueTypes[def] = sig.signature; + sig.bodyScope->rvalueRefinements[def] = sig.signature; Checkpoint start = checkpoint(this); checkFunctionBody(sig.bodyScope, function->func); @@ -848,11 +813,8 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction std::unordered_set excludeList; - const NullableBreadcrumbId functionBreadcrumb = dfg->getBreadcrumb(function->name); - - std::optional existingFunctionTy; - if (functionBreadcrumb) - existingFunctionTy = scope->lookupLValue(functionBreadcrumb->def); + DefId def = dfg->getDef(function->name); + std::optional existingFunctionTy = scope->lookupLValue(def); if (AstExprLocal* localName = function->name->as()) { @@ -867,12 +829,8 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction scope->bindings[localName->local] = Binding{generalizedType, localName->location}; sig.bodyScope->bindings[localName->local] = Binding{sig.signature, localName->location}; - - if (functionBreadcrumb) - { - sig.bodyScope->lvalueTypes[functionBreadcrumb->def] = sig.signature; - sig.bodyScope->rvalueRefinements[functionBreadcrumb->def] = sig.signature; - } + sig.bodyScope->lvalueTypes[def] = sig.signature; + sig.bodyScope->rvalueRefinements[def] = sig.signature; } else if (AstExprGlobal* globalName = function->name->as()) { @@ -882,17 +840,14 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction generalizedType = *existingFunctionTy; sig.bodyScope->bindings[globalName->name] = Binding{sig.signature, globalName->location}; - - if (functionBreadcrumb) - { - sig.bodyScope->lvalueTypes[functionBreadcrumb->def] = sig.signature; - sig.bodyScope->rvalueRefinements[functionBreadcrumb->def] = sig.signature; - } + sig.bodyScope->lvalueTypes[def] = sig.signature; + sig.bodyScope->rvalueRefinements[def] = sig.signature; } else if (AstExprIndexName* indexName = function->name->as()) { Checkpoint check1 = checkpoint(this); - TypeId lvalueType = checkLValue(scope, indexName); + std::optional lvalueType = checkLValue(scope, indexName); + LUAU_ASSERT(lvalueType); Checkpoint check2 = checkpoint(this); forEachConstraint(check1, check2, this, [&excludeList](const ConstraintPtr& c) { @@ -901,10 +856,13 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction // TODO figure out how to populate the location field of the table Property. - if (get(lvalueType)) - asMutable(lvalueType)->ty.emplace(generalizedType); - else - addConstraint(scope, indexName->location, SubtypeConstraint{lvalueType, generalizedType}); + if (lvalueType) + { + if (get(*lvalueType)) + asMutable(*lvalueType)->ty.emplace(generalizedType); + else + addConstraint(scope, indexName->location, SubtypeConstraint{*lvalueType, generalizedType}); + } } else if (AstExprError* err = function->name->as()) { @@ -914,8 +872,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction if (generalizedType == nullptr) ice->ice("generalizedType == nullptr", function->location); - if (functionBreadcrumb) - scope->rvalueRefinements[functionBreadcrumb->def] = generalizedType; + scope->rvalueRefinements[def] = generalizedType; checkFunctionBody(sig.bodyScope, function->func); Checkpoint end = checkpoint(this); @@ -997,18 +954,23 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* for (AstExpr* lvalue : assign->vars) { - TypeId upperBound = follow(checkLValue(scope, lvalue)); - if (get(upperBound)) - expectedTypes.push_back(std::nullopt); - else - expectedTypes.push_back(upperBound); - TypeId assignee = arena->addType(BlockedType{}); assignees.push_back(assignee); - addConstraint(scope, lvalue->location, SubtypeConstraint{assignee, upperBound}); - if (NullableBreadcrumbId bc = dfg->getBreadcrumb(lvalue)) - scope->lvalueTypes[bc->def] = assignee; + std::optional upperBound = follow(checkLValue(scope, lvalue)); + if (upperBound) + { + if (get(*upperBound)) + expectedTypes.push_back(std::nullopt); + else + expectedTypes.push_back(*upperBound); + + addConstraint(scope, lvalue->location, SubtypeConstraint{assignee, *upperBound}); + } + + DefId def = dfg->getDef(lvalue); + scope->lvalueTypes[def] = assignee; + updateLValueType(lvalue, assignee); } TypePackId resultPack = checkPack(scope, assign->values, expectedTypes).tp; @@ -1019,15 +981,15 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompoundAssign* assign) { - // We need to tweak the BinaryConstraint that we emit, so we cannot use the - // strategy of falsifying an AST fragment. - TypeId varTy = checkLValue(scope, assign->var); - TypeId valueTy = check(scope, assign->value).ty; + std::optional varTy = checkLValue(scope, assign->var); + + AstExprBinary binop = AstExprBinary{assign->location, assign->op, assign->var, assign->value}; + TypeId resultTy = check(scope, &binop).ty; + if (varTy) + addConstraint(scope, assign->location, SubtypeConstraint{resultTy, *varTy}); - TypeId resultType = arena->addType(BlockedType{}); - addConstraint(scope, assign->location, - BinaryConstraint{assign->op, varTy, valueTy, resultType, assign, &module->astOriginalCallTypes, &module->astOverloadResolvedTypes}); - addConstraint(scope, assign->location, SubtypeConstraint{resultType, varTy}); + DefId def = dfg->getDef(assign->var); + scope->lvalueTypes[def] = resultTy; return ControlFlow::None; } @@ -1138,9 +1100,9 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareG module->declaredGlobals[globalName] = globalTy; rootScope->bindings[global->name] = Binding{globalTy, global->location}; - BreadcrumbId bc = dfg->getBreadcrumb(global); - rootScope->lvalueTypes[bc->def] = globalTy; - rootScope->rvalueRefinements[bc->def] = globalTy; + DefId def = dfg->getDef(global); + rootScope->lvalueTypes[def] = globalTy; + rootScope->rvalueRefinements[def] = globalTy; return ControlFlow::None; } @@ -1310,9 +1272,9 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareF module->declaredGlobals[fnName] = fnType; scope->bindings[global->name] = Binding{fnType, global->location}; - BreadcrumbId bc = dfg->getBreadcrumb(global); - rootScope->lvalueTypes[bc->def] = fnType; - rootScope->rvalueRefinements[bc->def] = fnType; + DefId def = dfg->getDef(global); + rootScope->lvalueTypes[def] = fnType; + rootScope->rvalueRefinements[def] = fnType; return ControlFlow::None; } @@ -1409,10 +1371,10 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa exprArgs.push_back(indexExpr->expr); - if (auto bc = dfg->getBreadcrumb(indexExpr->expr)) + if (auto key = dfg->getRefinementKey(indexExpr->expr)) { TypeId discriminantTy = arena->addType(BlockedType{}); - returnRefinements.push_back(refinementArena.proposition(NotNull{bc}, discriminantTy)); + returnRefinements.push_back(refinementArena.proposition(key, discriminantTy)); discriminantTypes.push_back(discriminantTy); } else @@ -1423,10 +1385,10 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa { exprArgs.push_back(arg); - if (auto bc = dfg->getBreadcrumb(arg)) + if (auto key = dfg->getRefinementKey(arg)) { TypeId discriminantTy = arena->addType(BlockedType{}); - returnRefinements.push_back(refinementArena.proposition(NotNull{bc}, discriminantTy)); + returnRefinements.push_back(refinementArena.proposition(key, discriminantTy)); discriminantTypes.push_back(discriminantTy); } else @@ -1525,9 +1487,12 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa { scope->bindings[targetLocal->local].typeId = resultTy; - BreadcrumbId bc = dfg->getBreadcrumb(targetLocal); - scope->lvalueTypes[bc->def] = resultTy; // TODO: typestates: track this as an assignment - scope->rvalueRefinements[bc->def] = resultTy; // TODO: typestates: track this as an assignment + DefId def = dfg->getDef(targetLocal); + scope->lvalueTypes[def] = resultTy; // TODO: typestates: track this as an assignment + scope->rvalueRefinements[def] = resultTy; // TODO: typestates: track this as an assignment + + if (auto it = inferredBindings.find(targetLocal->local); it != inferredBindings.end()) + it->second.types.insert(resultTy); } return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}}; @@ -1686,27 +1651,51 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantBo Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* local) { - BreadcrumbId bc = dfg->getBreadcrumb(local); + const RefinementKey* key = dfg->getRefinementKey(local); + std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(local); + LUAU_ASSERT(key || rvalueDef); + + std::optional maybeTy; + + // if we have a refinement key, we can look up its type. + if (key) + maybeTy = scope->lookup(key->def); + + // if the current def doesn't have a type, we might be doing a compound assignment + // and therefore might need to look at the rvalue def instead. + if (!maybeTy && rvalueDef) + maybeTy = scope->lookup(*rvalueDef); + + if (maybeTy) + { + TypeId ty = follow(*maybeTy); + if (auto it = inferredBindings.find(local->local); it != inferredBindings.end()) + it->second.types.insert(ty); - if (auto ty = scope->lookup(bc->def)) - return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)}; + return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)}; + } else ice->ice("CGB: AstExprLocal came before its declaration?"); } Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* global) { - BreadcrumbId bc = dfg->getBreadcrumb(global); + const RefinementKey* key = dfg->getRefinementKey(global); + std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(global); + LUAU_ASSERT(key || rvalueDef); + + // we'll use whichever of the two definitions we have here. + DefId def = key ? key->def : *rvalueDef; /* prepopulateGlobalScope() has already added all global functions to the environment by this point, so any * global that is not already in-scope is definitely an unknown symbol. */ - if (auto ty = scope->lookup(bc->def)) - return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)}; + if (auto ty = scope->lookup(def)) + return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; else if (auto ty = scope->lookup(global->name)) { - rootScope->rvalueRefinements[bc->def] = *ty; - return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)}; + rootScope->rvalueRefinements[key->def] = *ty; + return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; } else { @@ -1720,19 +1709,19 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName* TypeId obj = check(scope, indexName->expr).ty; TypeId result = arena->addType(BlockedType{}); - NullableBreadcrumbId bc = dfg->getBreadcrumb(indexName); - if (bc) + const RefinementKey* key = dfg->getRefinementKey(indexName); + if (key) { - if (auto ty = scope->lookup(bc->def)) - return Inference{*ty, refinementArena.proposition(NotNull{bc}, builtinTypes->truthyType)}; + if (auto ty = scope->lookup(key->def)) + return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; - scope->rvalueRefinements[bc->def] = result; + scope->rvalueRefinements[key->def] = result; } addConstraint(scope, indexName->expr->location, HasPropConstraint{result, obj, indexName->index.value}); - if (bc) - return Inference{result, refinementArena.proposition(NotNull{bc}, builtinTypes->truthyType)}; + if (key) + return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)}; else return Inference{result}; } @@ -1743,13 +1732,13 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* TypeId indexType = check(scope, indexExpr->index).ty; TypeId result = freshType(scope); - NullableBreadcrumbId bc = dfg->getBreadcrumb(indexExpr); - if (bc) + const RefinementKey* key = dfg->getRefinementKey(indexExpr); + if (key) { - if (auto ty = scope->lookup(bc->def)) - return Inference{*ty, refinementArena.proposition(NotNull{bc}, builtinTypes->truthyType)}; + if (auto ty = scope->lookup(key->def)) + return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; - scope->rvalueRefinements[bc->def] = result; + scope->rvalueRefinements[key->def] = result; } TableIndexer indexer{indexType, result}; @@ -1757,8 +1746,8 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* addConstraint(scope, indexExpr->expr->location, SubtypeConstraint{obj, tableType}); - if (bc) - return Inference{result, refinementArena.proposition(NotNull{bc}, builtinTypes->truthyType)}; + if (key) + return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)}; else return Inference{result}; } @@ -1812,12 +1801,28 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* una addConstraint(scope, unary->location, ReduceConstraint{resultType}); return Inference{resultType, refinementArena.negation(refinement)}; } - default: + case AstExprUnary::Op::Len: { - TypeId resultType = arena->addType(BlockedType{}); - addConstraint(scope, unary->location, UnaryConstraint{unary->op, operandType, resultType}); - return Inference{resultType}; + TypeId resultType = arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.lenFamily}, + {operandType}, + {}, + }); + addConstraint(scope, unary->location, ReduceConstraint{resultType}); + return Inference{resultType, refinementArena.negation(refinement)}; + } + case AstExprUnary::Op::Minus: + { + TypeId resultType = arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.unmFamily}, + {operandType}, + {}, + }); + addConstraint(scope, unary->location, ReduceConstraint{resultType}); + return Inference{resultType, refinementArena.negation(refinement)}; } + default: // msvc can't prove that this is exhaustive. + LUAU_UNREACHABLE(); } } @@ -1978,13 +1983,10 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi addConstraint(scope, binary->location, ReduceConstraint{resultType}); return Inference{resultType, std::move(refinement)}; } - default: - { - TypeId resultType = arena->addType(BlockedType{}); - addConstraint(scope, binary->location, - BinaryConstraint{binary->op, leftType, rightType, resultType, binary, &module->astOriginalCallTypes, &module->astOverloadResolvedTypes}); - return Inference{resultType, std::move(refinement)}; - } + case AstExprBinary::Op::Op__Count: + ice->ice("Op__Count should never be generated in an AST."); + default: // msvc can't prove that this is exhaustive. + LUAU_UNREACHABLE(); } } @@ -2056,9 +2058,10 @@ std::tuple ConstraintGraphBuilder::checkBinary( TypeId leftType = check(scope, binary->left).ty; TypeId rightType = check(scope, binary->right).ty; - NullableBreadcrumbId bc = dfg->getBreadcrumb(typeguard->target); - if (!bc) + const RefinementKey* key = dfg->getRefinementKey(typeguard->target); + if (!key) return {leftType, rightType, nullptr}; + auto augmentForErrorSupression = [&](TypeId ty) -> TypeId { return arena->addType(UnionType{{ty, builtinTypes->errorType}}); }; @@ -2096,7 +2099,7 @@ std::tuple ConstraintGraphBuilder::checkBinary( discriminantTy = ty; } - RefinementId proposition = refinementArena.proposition(NotNull{bc}, discriminantTy); + RefinementId proposition = refinementArena.proposition(key, discriminantTy); if (binary->op == AstExprBinary::CompareEq) return {leftType, rightType, proposition}; else if (binary->op == AstExprBinary::CompareNe) @@ -2111,13 +2114,8 @@ std::tuple ConstraintGraphBuilder::checkBinary( TypeId leftType = check(scope, binary->left, {}, true).ty; TypeId rightType = check(scope, binary->right, {}, true).ty; - RefinementId leftRefinement = nullptr; - if (auto bc = dfg->getBreadcrumb(binary->left)) - leftRefinement = refinementArena.proposition(NotNull{bc}, rightType); - - RefinementId rightRefinement = nullptr; - if (auto bc = dfg->getBreadcrumb(binary->right)) - rightRefinement = refinementArena.proposition(NotNull{bc}, leftType); + RefinementId leftRefinement = refinementArena.proposition(dfg->getRefinementKey(binary->left), rightType); + RefinementId rightRefinement = refinementArena.proposition(dfg->getRefinementKey(binary->right), leftType); if (binary->op == AstExprBinary::CompareNe) { @@ -2135,7 +2133,7 @@ std::tuple ConstraintGraphBuilder::checkBinary( } } -TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr) +std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr) { if (auto local = expr->as()) return checkLValue(scope, local); @@ -2154,24 +2152,42 @@ TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr) ice->ice("checkLValue is inexhaustive"); } -TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprLocal* local) +std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprLocal* local) { - std::optional upperBound = scope->lookup(Symbol{local->local}); - LUAU_ASSERT(upperBound); - return *upperBound; + /* + * The caller of this method uses the returned type to emit the proper + * SubtypeConstraint. + * + * At this point during constraint generation, the binding table is only + * populated by symbols that have type annotations. + * + * If this local has an interesting type annotation, it is important that we + * return that. + */ + std::optional annotatedTy = scope->lookup(local->local); + if (annotatedTy) + return annotatedTy; + + /* + * As a safety measure, we'll assert that no type has yet been ascribed to + * the corresponding def. We'll populate this when we generate + * constraints for assignment and compound assignment statements. + */ + LUAU_ASSERT(!scope->lookupLValue(dfg->getDef(local))); + return std::nullopt; } -TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprGlobal* global) +std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprGlobal* global) { - return scope->lookup(Symbol{global->name}).value_or(builtinTypes->errorRecoveryType()); + return scope->lookup(Symbol{global->name}); } -TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName) +std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName) { return updateProperty(scope, indexName); } -TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr) +std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr) { return updateProperty(scope, indexExpr); } @@ -2226,7 +2242,7 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex return check(scope, expr).ty; Symbol sym; - NullableBreadcrumbId bc = nullptr; + const Def* def = nullptr; std::vector segments; std::vector exprs; @@ -2236,13 +2252,13 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex if (auto global = e->as()) { sym = global->name; - bc = dfg->getBreadcrumb(global); + def = dfg->getDef(global); break; } else if (auto local = e->as()) { sym = local->local; - bc = dfg->getBreadcrumb(local); + def = dfg->getDef(local); break; } else if (auto indexName = e->as()) @@ -2275,20 +2291,12 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex std::reverse(begin(segments), end(segments)); std::reverse(begin(exprs), end(exprs)); - auto lookupResult = scope->lookupEx(sym); + LUAU_ASSERT(def); + std::optional> lookupResult = scope->lookupEx(NotNull{def}); if (!lookupResult) return check(scope, expr).ty; - const auto [subjectBinding, symbolScope] = std::move(*lookupResult); - - 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); + const auto [subjectType, subjectScope] = *lookupResult; TypeId propTy = freshType(scope); @@ -2311,20 +2319,29 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex if (!subjectType->persistent) { - symbolScope->bindings[sym].typeId = updatedType; + subjectScope->bindings[sym].typeId = updatedType; // This can fail if the user is erroneously trying to augment a builtin // table like os or string. - if (auto bc = dfg->getBreadcrumb(e)) + if (auto key = dfg->getRefinementKey(e)) { - symbolScope->lvalueTypes[bc->def] = updatedType; - symbolScope->rvalueRefinements[bc->def] = updatedType; + subjectScope->lvalueTypes[key->def] = updatedType; + subjectScope->rvalueRefinements[key->def] = updatedType; } } return propTy; } +void ConstraintGraphBuilder::updateLValueType(AstExpr* lvalue, TypeId ty) +{ + if (auto local = lvalue->as()) + { + if (auto it = inferredBindings.find(local->local); it != inferredBindings.end()) + it->second.types.insert(ty); + } +} + Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) { const bool expectedTypeIsFree = expectedType && get(follow(*expectedType)); @@ -2525,9 +2542,9 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS argNames.emplace_back(FunctionArgument{fn->self->name.value, fn->self->location}); signatureScope->bindings[fn->self] = Binding{selfType, fn->self->location}; - BreadcrumbId bc = dfg->getBreadcrumb(fn->self); - signatureScope->lvalueTypes[bc->def] = selfType; - signatureScope->rvalueRefinements[bc->def] = selfType; + DefId def = dfg->getDef(fn->self); + signatureScope->lvalueTypes[def] = selfType; + signatureScope->rvalueRefinements[def] = selfType; } for (size_t i = 0; i < fn->args.size; ++i) @@ -2552,14 +2569,13 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS 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); + inferredBindings[local] = {signatureScope.get(), {}}; } - BreadcrumbId bc = dfg->getBreadcrumb(local); - signatureScope->lvalueTypes[bc->def] = argTy; - signatureScope->rvalueRefinements[bc->def] = argTy; + DefId def = dfg->getDef(local); + signatureScope->lvalueTypes[def] = argTy; + signatureScope->rvalueRefinements[def] = argTy; } TypePackId varargPack = nullptr; @@ -2854,7 +2870,10 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b } else if (auto boolAnnotation = ty->as()) { - result = arena->addType(SingletonType(BooleanSingleton{boolAnnotation->value})); + if (boolAnnotation->value) + result = builtinTypes->trueType; + else + result = builtinTypes->falseType; } else if (auto stringAnnotation = ty->as()) { @@ -3042,10 +3061,8 @@ struct GlobalPrepopulator : AstVisitor TypeId bt = arena->addType(BlockedType{}); globalScope->bindings[g->name] = Binding{bt}; - NullableBreadcrumbId bc = dfg->getBreadcrumb(function->name); - LUAU_ASSERT(bc); - - globalScope->lvalueTypes[bc->def] = bt; + DefId def = dfg->getDef(function->name); + globalScope->lvalueTypes[def] = bt; } return true; @@ -3064,32 +3081,16 @@ void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, void ConstraintGraphBuilder::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) { - std::deque queue; - - for (const auto& [scope, symbol, breadcrumb] : inferredBindings) + for (const auto& [symbol, p] : inferredBindings) { - LUAU_ASSERT(queue.empty()); - - queue.push_back(breadcrumb); + const auto& [scope, location, types] = p; - TypeId ty = builtinTypes->neverType; + std::vector tys(types.begin(), types.end()); - 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); - } + TypeId ty = arena->addType(BlockedType{}); + addConstraint(globalScope, Location{}, SetOpConstraint{SetOpConstraint::Union, ty, std::move(tys)}); - scope->bindings[symbol].typeId = ty; + scope->bindings[symbol] = Binding{ty, location}; } } diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index bdcd1663f..924958ab3 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -516,10 +516,6 @@ bool ConstraintSolver::tryDispatch(NotNull constraint, bool fo success = tryDispatch(*gc, constraint, force); else if (auto ic = get(*constraint)) success = tryDispatch(*ic, constraint, force); - else if (auto uc = get(*constraint)) - success = tryDispatch(*uc, constraint, force); - else if (auto bc = get(*constraint)) - success = tryDispatch(*bc, constraint, force); else if (auto ic = get(*constraint)) success = tryDispatch(*ic, constraint, force); else if (auto nc = get(*constraint)) @@ -542,6 +538,8 @@ bool ConstraintSolver::tryDispatch(NotNull constraint, bool fo success = tryDispatch(*uc, constraint); else if (auto rc = get(*constraint)) success = tryDispatch(*rc, constraint, force); + else if (auto soc = get(*constraint)) + success = tryDispatch(*soc, constraint, force); else if (auto rc = get(*constraint)) success = tryDispatch(*rc, constraint, force); else if (auto rpc = get(*constraint)) @@ -652,335 +650,6 @@ bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNull constraint, bool force) -{ - TypeId operandType = follow(c.operandType); - - if (isBlocked(operandType)) - return block(operandType, constraint); - - if (!force && get(operandType)) - return block(operandType, constraint); - - LUAU_ASSERT(get(c.resultType)); - - switch (c.op) - { - case AstExprUnary::Not: - { - asMutable(c.resultType)->ty.emplace(builtinTypes->booleanType); - - unblock(c.resultType, constraint->location); - return true; - } - case AstExprUnary::Len: - { - // __len must return a number. - asMutable(c.resultType)->ty.emplace(builtinTypes->numberType); - - unblock(c.resultType, constraint->location); - return true; - } - case AstExprUnary::Minus: - { - if (isNumber(operandType) || get(operandType) || get(operandType) || get(operandType)) - { - asMutable(c.resultType)->ty.emplace(c.operandType); - } - else if (std::optional mm = findMetatableEntry(builtinTypes, errors, operandType, "__unm", constraint->location)) - { - TypeId mmTy = follow(*mm); - - if (get(mmTy) && !force) - return block(mmTy, constraint); - - TypePackId argPack = arena->addTypePack(TypePack{{operandType}, {}}); - TypePackId retPack = arena->addTypePack(BlockedTypePack{}); - - TypeId res = freshType(arena, builtinTypes, constraint->scope); - asMutable(c.resultType)->ty.emplace(res); - - pushConstraint(constraint->scope, constraint->location, PackSubtypeConstraint{retPack, arena->addTypePack(TypePack{{c.resultType}})}); - - pushConstraint(constraint->scope, constraint->location, FunctionCallConstraint{mmTy, argPack, retPack, nullptr}); - } - else - { - asMutable(c.resultType)->ty.emplace(builtinTypes->errorRecoveryType()); - } - - unblock(c.resultType, constraint->location); - return true; - } - } - - LUAU_ASSERT(false); - return false; -} - -bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull constraint, bool force) -{ - TypeId leftType = follow(c.leftType); - TypeId rightType = follow(c.rightType); - TypeId resultType = follow(c.resultType); - - LUAU_ASSERT(get(resultType)); - - bool isLogical = c.op == AstExprBinary::Op::And || c.op == AstExprBinary::Op::Or; - - /* Compound assignments create constraints of the form - * - * A <: Binary - * - * This constraint is the one that is meant to unblock A, so it doesn't - * make any sense to stop and wait for someone else to do it. - */ - - // If any is present, the expression must evaluate to any as well. - bool leftAny = get(leftType) || get(leftType); - bool rightAny = get(rightType) || get(rightType); - bool anyPresent = leftAny || rightAny; - - if (isBlocked(leftType) && leftType != resultType) - return block(c.leftType, constraint); - - if (isBlocked(rightType) && rightType != resultType) - return block(c.rightType, constraint); - - if (!force) - { - // Logical expressions may proceed if the LHS is free. - if (hasTypeInIntersection(leftType) && !isLogical) - return block(leftType, constraint); - } - - // Logical expressions may proceed if the LHS is free. - if (isBlocked(leftType) || (hasTypeInIntersection(leftType) && !isLogical)) - { - asMutable(resultType)->ty.emplace(errorRecoveryType()); - unblock(resultType, constraint->location); - return true; - } - - // Metatables go first, even if there is primitive behavior. - if (auto it = kBinaryOpMetamethods.find(c.op); it != kBinaryOpMetamethods.end()) - { - LUAU_ASSERT(FFlag::LuauFloorDivision || c.op != AstExprBinary::Op::FloorDiv); - - // Metatables are not the same. The metamethod will not be invoked. - if ((c.op == AstExprBinary::Op::CompareEq || c.op == AstExprBinary::Op::CompareNe) && - getMetatable(leftType, builtinTypes) != getMetatable(rightType, builtinTypes)) - { - // TODO: Boolean singleton false? The result is _always_ boolean false. - asMutable(resultType)->ty.emplace(builtinTypes->booleanType); - unblock(resultType, constraint->location); - return true; - } - - std::optional mm; - - // The LHS metatable takes priority over the RHS metatable, where - // present. - if (std::optional leftMm = findMetatableEntry(builtinTypes, errors, leftType, it->second, constraint->location)) - mm = leftMm; - else if (std::optional rightMm = findMetatableEntry(builtinTypes, errors, rightType, it->second, constraint->location)) - mm = rightMm; - - if (mm) - { - std::optional instantiatedMm = instantiate(builtinTypes, arena, NotNull{&limits}, constraint->scope, *mm); - if (!instantiatedMm) - { - reportError(CodeTooComplex{}, constraint->location); - return true; - } - - // TODO: Is a table with __call legal here? - // TODO: Overloads - if (const FunctionType* ftv = get(follow(*instantiatedMm))) - { - TypePackId inferredArgs; - // For >= and > we invoke __lt and __le respectively with - // swapped argument ordering. - if (c.op == AstExprBinary::Op::CompareGe || c.op == AstExprBinary::Op::CompareGt) - { - inferredArgs = arena->addTypePack({rightType, leftType}); - } - else - { - inferredArgs = arena->addTypePack({leftType, rightType}); - } - - unify(constraint->scope, constraint->location, inferredArgs, ftv->argTypes); - - TypeId mmResult; - - // Comparison operations always evaluate to a boolean, - // regardless of what the metamethod returns. - switch (c.op) - { - case AstExprBinary::Op::CompareEq: - case AstExprBinary::Op::CompareNe: - case AstExprBinary::Op::CompareGe: - case AstExprBinary::Op::CompareGt: - case AstExprBinary::Op::CompareLe: - case AstExprBinary::Op::CompareLt: - mmResult = builtinTypes->booleanType; - break; - default: - if (get(leftType) || get(rightType)) - mmResult = builtinTypes->neverType; - else - mmResult = first(ftv->retTypes).value_or(errorRecoveryType()); - } - - asMutable(resultType)->ty.emplace(mmResult); - unblock(resultType, constraint->location); - - (*c.astOriginalCallTypes)[c.astFragment] = *mm; - (*c.astOverloadResolvedTypes)[c.astFragment] = *instantiatedMm; - return true; - } - } - - // If there's no metamethod available, fall back to primitive behavior. - } - - switch (c.op) - { - // For arithmetic operators, if the LHS is a number, the RHS must be a - // number as well. The result will also be a number. - case AstExprBinary::Op::Add: - case AstExprBinary::Op::Sub: - case AstExprBinary::Op::Mul: - case AstExprBinary::Op::Div: - case AstExprBinary::Op::FloorDiv: - case AstExprBinary::Op::Pow: - case AstExprBinary::Op::Mod: - { - LUAU_ASSERT(FFlag::LuauFloorDivision || c.op != AstExprBinary::Op::FloorDiv); - - const NormalizedType* normLeftTy = normalizer->normalize(leftType); - if (hasTypeInIntersection(leftType) && force) - asMutable(leftType)->ty.emplace(anyPresent ? builtinTypes->anyType : builtinTypes->numberType); - // We want to check if the left type has tops because `any` is a valid type for the lhs - if (normLeftTy && (normLeftTy->isExactlyNumber() || get(normLeftTy->tops))) - { - unify(constraint->scope, constraint->location, leftType, rightType); - asMutable(resultType)->ty.emplace(anyPresent ? builtinTypes->anyType : leftType); - unblock(resultType, constraint->location); - return true; - } - else if (get(leftType) || get(rightType)) - { - unify(constraint->scope, constraint->location, leftType, rightType); - asMutable(resultType)->ty.emplace(builtinTypes->neverType); - unblock(resultType, constraint->location); - return true; - } - - break; - } - // For concatenation, if the LHS is a string, the RHS must be a string as - // well. The result will also be a string. - case AstExprBinary::Op::Concat: - { - if (hasTypeInIntersection(leftType) && force) - asMutable(leftType)->ty.emplace(anyPresent ? builtinTypes->anyType : builtinTypes->stringType); - const NormalizedType* leftNormTy = normalizer->normalize(leftType); - if (leftNormTy && leftNormTy->isSubtypeOfString()) - { - unify(constraint->scope, constraint->location, leftType, rightType); - asMutable(resultType)->ty.emplace(anyPresent ? builtinTypes->anyType : leftType); - unblock(resultType, constraint->location); - return true; - } - else if (get(leftType) || get(rightType)) - { - unify(constraint->scope, constraint->location, leftType, rightType); - asMutable(resultType)->ty.emplace(builtinTypes->neverType); - unblock(resultType, constraint->location); - return true; - } - - break; - } - // Inexact comparisons require that the types be both numbers or both - // strings, and evaluate to a boolean. - case AstExprBinary::Op::CompareGe: - case AstExprBinary::Op::CompareGt: - case AstExprBinary::Op::CompareLe: - case AstExprBinary::Op::CompareLt: - { - const NormalizedType* lt = normalizer->normalize(leftType); - const NormalizedType* rt = normalizer->normalize(rightType); - // If the lhs is any, comparisons should be valid. - if (lt && rt && (lt->isExactlyNumber() || get(lt->tops)) && rt->isExactlyNumber()) - { - asMutable(resultType)->ty.emplace(builtinTypes->booleanType); - unblock(resultType, constraint->location); - return true; - } - - if (lt && rt && (lt->isSubtypeOfString() || get(lt->tops)) && rt->isSubtypeOfString()) - { - asMutable(resultType)->ty.emplace(builtinTypes->booleanType); - unblock(resultType, constraint->location); - return true; - } - - - if (get(leftType) || get(rightType)) - { - asMutable(resultType)->ty.emplace(builtinTypes->booleanType); - unblock(resultType, constraint->location); - return true; - } - - break; - } - - // == and ~= always evaluate to a boolean, and impose no other constraints - // on their parameters. - case AstExprBinary::Op::CompareEq: - case AstExprBinary::Op::CompareNe: - asMutable(resultType)->ty.emplace(builtinTypes->booleanType); - unblock(resultType, constraint->location); - return true; - // And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is - // truthy. - case AstExprBinary::Op::And: - { - TypeId leftFilteredTy = simplifyIntersection(builtinTypes, arena, leftType, builtinTypes->falsyType).result; - - asMutable(resultType)->ty.emplace(simplifyUnion(builtinTypes, arena, rightType, leftFilteredTy).result); - unblock(resultType, constraint->location); - return true; - } - // Or evaluates to the LHS type if the LHS is truthy, and the RHS type if - // LHS is falsey. - case AstExprBinary::Op::Or: - { - TypeId leftFilteredTy = simplifyIntersection(builtinTypes, arena, leftType, builtinTypes->truthyType).result; - - asMutable(resultType)->ty.emplace(simplifyUnion(builtinTypes, arena, rightType, leftFilteredTy).result); - unblock(resultType, constraint->location); - return true; - } - default: - iceReporter.ice("Unhandled AstExprBinary::Op for binary operation", constraint->location); - break; - } - - // We failed to either evaluate a metamethod or invoke primitive behavior. - unify(constraint->scope, constraint->location, leftType, errorRecoveryType()); - unify(constraint->scope, constraint->location, rightType, errorRecoveryType()); - asMutable(resultType)->ty.emplace(errorRecoveryType()); - unblock(resultType, constraint->location); - - return true; -} - bool ConstraintSolver::tryDispatch(const IterableConstraint& c, NotNull constraint, bool force) { /* @@ -1946,6 +1615,32 @@ bool ConstraintSolver::tryDispatch(const RefineConstraint& c, NotNull constraint, bool force) +{ + bool blocked = false; + for (TypeId ty : c.types) + { + if (isBlocked(ty)) + { + blocked = true; + block(ty, constraint); + } + } + if (blocked && !force) + return false; + + LUAU_ASSERT(SetOpConstraint::Union == c.mode); + + TypeId res = builtinTypes->neverType; + + for (TypeId ty : c.types) + res = simplifyUnion(builtinTypes, arena, res, ty).result; + + asMutable(c.resultType)->ty.emplace(res); + + return true; +} + bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull constraint, bool force) { TypeId ty = follow(c.ty); diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index 523f8df60..e39335742 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -2,11 +2,12 @@ #include "Luau/DataFlowGraph.h" #include "Luau/Ast.h" -#include "Luau/Breadcrumb.h" +#include "Luau/Def.h" +#include "Luau/Common.h" #include "Luau/Error.h" -#include "Luau/Refinement.h" #include +#include LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) @@ -14,74 +15,81 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) namespace Luau { -NullableBreadcrumbId DataFlowGraph::getBreadcrumb(const AstExpr* expr) const +const RefinementKey* RefinementKeyArena::leaf(DefId def) { - // We need to skip through AstExprGroup because DFG doesn't try its best to transitively - while (auto group = expr->as()) - expr = group->expr; - if (auto bc = astBreadcrumbs.find(expr)) - return *bc; - return nullptr; + return allocator.allocate(RefinementKey{nullptr, def, std::nullopt}); } -BreadcrumbId DataFlowGraph::getBreadcrumb(const AstLocal* local) const +const RefinementKey* RefinementKeyArena::node(const RefinementKey* parent, DefId def, const std::string& propName) { - auto bc = localBreadcrumbs.find(local); - LUAU_ASSERT(bc); - return NotNull{*bc}; + return allocator.allocate(RefinementKey{parent, def, propName}); } -BreadcrumbId DataFlowGraph::getBreadcrumb(const AstExprLocal* local) const +DefId DataFlowGraph::getDef(const AstExpr* expr) const { - auto bc = astBreadcrumbs.find(local); - LUAU_ASSERT(bc); - return NotNull{*bc}; + auto def = astDefs.find(expr); + LUAU_ASSERT(def); + return NotNull{*def}; } -BreadcrumbId DataFlowGraph::getBreadcrumb(const AstExprGlobal* global) const +std::optional DataFlowGraph::getRValueDefForCompoundAssign(const AstExpr* expr) const { - auto bc = astBreadcrumbs.find(global); - LUAU_ASSERT(bc); - return NotNull{*bc}; + auto def = compoundAssignBreadcrumbs.find(expr); + return def ? std::optional(*def) : std::nullopt; } -BreadcrumbId DataFlowGraph::getBreadcrumb(const AstStatDeclareGlobal* global) const +DefId DataFlowGraph::getDef(const AstLocal* local) const { - auto bc = declaredBreadcrumbs.find(global); - LUAU_ASSERT(bc); - return NotNull{*bc}; + auto def = localDefs.find(local); + LUAU_ASSERT(def); + return NotNull{*def}; } -BreadcrumbId DataFlowGraph::getBreadcrumb(const AstStatDeclareFunction* func) const +DefId DataFlowGraph::getDef(const AstStatDeclareGlobal* global) const { - auto bc = declaredBreadcrumbs.find(func); - LUAU_ASSERT(bc); - return NotNull{*bc}; + auto def = declaredDefs.find(global); + LUAU_ASSERT(def); + return NotNull{*def}; +} + +DefId DataFlowGraph::getDef(const AstStatDeclareFunction* func) const +{ + auto def = declaredDefs.find(func); + LUAU_ASSERT(def); + return NotNull{*def}; +} + +const RefinementKey* DataFlowGraph::getRefinementKey(const AstExpr* expr) const +{ + if (auto key = astRefinementKeys.find(expr)) + return *key; + + return nullptr; } -NullableBreadcrumbId DfgScope::lookup(Symbol symbol) const +std::optional DfgScope::lookup(Symbol symbol) const { for (const DfgScope* current = this; current; current = current->parent) { - if (auto breadcrumb = current->bindings.find(symbol)) - return *breadcrumb; + if (auto def = current->bindings.find(symbol)) + return NotNull{*def}; } - return nullptr; + return std::nullopt; } -NullableBreadcrumbId DfgScope::lookup(DefId def, const std::string& key) const +std::optional DfgScope::lookup(DefId def, const std::string& key) const { for (const DfgScope* current = this; current; current = current->parent) { if (auto map = props.find(def)) { if (auto it = map->find(key); it != map->end()) - return it->second; + return NotNull{it->second}; } } - return nullptr; + return std::nullopt; } DataFlowGraph DataFlowGraphBuilder::build(AstStatBlock* block, NotNull handle) @@ -95,8 +103,8 @@ DataFlowGraph DataFlowGraphBuilder::build(AstStatBlock* block, NotNullallocator.freeze(); - builder.breadcrumbs->allocator.freeze(); + builder.defArena->allocator.freeze(); + builder.keyArena->allocator.freeze(); } return std::move(builder.graph); @@ -217,10 +225,10 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatExpr* e) void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l) { // We're gonna need a `visitExprList` and `visitVariadicExpr` (function calls and `...`) - std::vector bcs; - bcs.reserve(l->values.size); + std::vector defs; + defs.reserve(l->values.size); for (AstExpr* e : l->values) - bcs.push_back(visitExpr(scope, e)); + defs.push_back(visitExpr(scope, e).def); for (size_t i = 0; i < l->vars.size; ++i) { @@ -228,10 +236,12 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l) if (local->annotation) visitType(scope, local->annotation); - // We need to create a new breadcrumb with new defs to intentionally avoid alias tracking. - BreadcrumbId bc = breadcrumbs->add(nullptr, defs->freshCell(), i < bcs.size() ? bcs[i]->metadata : std::nullopt); - graph.localBreadcrumbs[local] = bc; - scope->bindings[local] = bc; + // We need to create a new def to intentionally avoid alias tracking, but we'd like to + // make sure that the non-aliased defs are also marked as a subscript for refinements. + bool subscripted = i < defs.size() && containsSubscriptedDefinition(defs[i]); + DefId def = defArena->freshCell(subscripted); + graph.localDefs[local] = def; + scope->bindings[local] = def; } } @@ -247,10 +257,9 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f) if (f->var->annotation) visitType(forScope, f->var->annotation); - // TODO: RangeMetadata. - BreadcrumbId bc = breadcrumbs->add(nullptr, defs->freshCell()); - graph.localBreadcrumbs[f->var] = bc; - scope->bindings[f->var] = bc; + DefId def = defArena->freshCell(); + graph.localDefs[f->var] = def; + scope->bindings[f->var] = def; // TODO(controlflow): entry point has a back edge from exit point visit(forScope, f->body); @@ -265,10 +274,9 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) if (local->annotation) visitType(forScope, local->annotation); - // TODO: IterMetadata (different from RangeMetadata) - BreadcrumbId bc = breadcrumbs->add(nullptr, defs->freshCell()); - graph.localBreadcrumbs[local] = bc; - forScope->bindings[local] = bc; + DefId def = defArena->freshCell(); + graph.localDefs[local] = def; + forScope->bindings[local] = def; } // TODO(controlflow): entry point has a back edge from exit point @@ -281,11 +289,15 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a) { - for (size_t i = 0; i < std::max(a->vars.size, a->values.size); ++i) + std::vector defs; + defs.reserve(a->values.size); + for (AstExpr* e : a->values) + defs.push_back(visitExpr(scope, e).def); + + for (size_t i = 0; i < a->vars.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); + AstExpr* v = a->vars.data[i]; + visitLValue(scope, v, i < defs.size() ? defs[i] : defArena->freshCell()); } } @@ -297,9 +309,9 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) // // 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)); + DefId def = visitExpr(scope, c->value).def; + visitLValue(scope, c->var, def, /* isCompoundAssignment */ true); } void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) @@ -314,14 +326,17 @@ 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)); + DefId prototype = defArena->freshCell(); + visitLValue(scope, f->name, prototype); + visitExpr(scope, f->func); } void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l) { - BreadcrumbId bc = visitExpr(scope, l->func); - graph.localBreadcrumbs[l->name] = bc; - scope->bindings[l->name] = bc; + DefId def = defArena->freshCell(); + graph.localDefs[l->name] = def; + scope->bindings[l->name] = def; + visitExpr(scope, l->func); } void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t) @@ -334,20 +349,18 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t) void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareGlobal* d) { - // TODO: AmbientDeclarationMetadata. - BreadcrumbId bc = breadcrumbs->add(nullptr, defs->freshCell()); - graph.declaredBreadcrumbs[d] = bc; - scope->bindings[d->name] = bc; + DefId def = defArena->freshCell(); + graph.declaredDefs[d] = def; + scope->bindings[d->name] = def; visitType(scope, d->type); } void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareFunction* d) { - // TODO: AmbientDeclarationMetadata. - BreadcrumbId bc = breadcrumbs->add(nullptr, defs->freshCell()); - graph.declaredBreadcrumbs[d] = bc; - scope->bindings[d->name] = bc; + DefId def = defArena->freshCell(); + graph.declaredDefs[d] = def; + scope->bindings[d->name] = def; DfgScope* unreachable = childScope(scope); visitGenerics(unreachable, d->generics); @@ -375,116 +388,125 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatError* error) visitExpr(unreachable, e); } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExpr* e) -{ - if (auto g = e->as()) - return visitExpr(scope, g->expr); - else if (auto c = e->as()) - return breadcrumbs->add(nullptr, defs->freshCell()); // ok - else if (auto c = e->as()) - return breadcrumbs->add(nullptr, defs->freshCell()); // ok - else if (auto c = e->as()) - return breadcrumbs->add(nullptr, defs->freshCell()); // ok - else if (auto c = e->as()) - return breadcrumbs->add(nullptr, defs->freshCell()); // ok - else if (auto l = e->as()) - return visitExpr(scope, l); - else if (auto g = e->as()) - return visitExpr(scope, g); - else if (auto v = e->as()) - return breadcrumbs->add(nullptr, defs->freshCell()); // ok - else if (auto c = e->as()) - return visitExpr(scope, c); - else if (auto i = e->as()) - return visitExpr(scope, i); - else if (auto i = e->as()) - return visitExpr(scope, i); - else if (auto f = e->as()) - return visitExpr(scope, f); - else if (auto t = e->as()) - return visitExpr(scope, t); - else if (auto u = e->as()) - return visitExpr(scope, u); - else if (auto b = e->as()) - return visitExpr(scope, b); - else if (auto t = e->as()) - return visitExpr(scope, t); - else if (auto i = e->as()) - return visitExpr(scope, i); - else if (auto i = e->as()) - return visitExpr(scope, i); - else if (auto error = e->as()) - return visitExpr(scope, error); - else - handle->ice("Unknown AstExpr in DataFlowGraphBuilder::visitExpr"); +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExpr* e) +{ + auto go = [&]() -> DataFlowResult { + if (auto g = e->as()) + return visitExpr(scope, g); + else if (auto c = e->as()) + return {defArena->freshCell(), nullptr}; // ok + else if (auto c = e->as()) + return {defArena->freshCell(), nullptr}; // ok + else if (auto c = e->as()) + return {defArena->freshCell(), nullptr}; // ok + else if (auto c = e->as()) + return {defArena->freshCell(), nullptr}; // ok + else if (auto l = e->as()) + return visitExpr(scope, l); + else if (auto g = e->as()) + return visitExpr(scope, g); + else if (auto v = e->as()) + return {defArena->freshCell(), nullptr}; // ok + else if (auto c = e->as()) + return visitExpr(scope, c); + else if (auto i = e->as()) + return visitExpr(scope, i); + else if (auto i = e->as()) + return visitExpr(scope, i); + else if (auto f = e->as()) + return visitExpr(scope, f); + else if (auto t = e->as()) + return visitExpr(scope, t); + else if (auto u = e->as()) + return visitExpr(scope, u); + else if (auto b = e->as()) + return visitExpr(scope, b); + else if (auto t = e->as()) + return visitExpr(scope, t); + else if (auto i = e->as()) + return visitExpr(scope, i); + else if (auto i = e->as()) + return visitExpr(scope, i); + else if (auto error = e->as()) + return visitExpr(scope, error); + else + handle->ice("Unknown AstExpr in DataFlowGraphBuilder::visitExpr"); + }; + + auto [def, key] = go(); + graph.astDefs[e] = def; + if (key) + graph.astRefinementKeys[e] = key; + return {def, key}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGroup* group) { - NullableBreadcrumbId breadcrumb = scope->lookup(l->local); - if (!breadcrumb) - handle->ice("DFG: AstExprLocal came before its declaration?"); - - graph.astBreadcrumbs[l] = breadcrumb; - return NotNull{breadcrumb}; + return visitExpr(scope, group->expr); } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) { - NullableBreadcrumbId bc = scope->lookup(g->name); - if (!bc) + if (auto def = scope->lookup(l->local)) { - bc = breadcrumbs->add(nullptr, defs->freshCell()); - moduleScope->bindings[g->name] = bc; + const RefinementKey* key = keyArena->leaf(*def); + return {*def, key}; } - graph.astBreadcrumbs[g] = bc; - return NotNull{bc}; + handle->ice("DFG: AstExprLocal came before its declaration?"); +} + +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g) +{ + if (auto def = scope->lookup(g->name)) + return {*def, keyArena->leaf(*def)}; + + DefId def = defArena->freshCell(); + moduleScope->bindings[g->name] = def; + return {def, keyArena->leaf(def)}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprCall* c) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprCall* c) { visitExpr(scope, c->func); for (AstExpr* arg : c->args) visitExpr(scope, arg); - return breadcrumbs->add(nullptr, defs->freshCell()); + return {defArena->freshCell(), nullptr}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexName* i) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexName* i) { - BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr); + auto [parentDef, parentKey] = visitExpr(scope, i->expr); - std::string key = i->index.value; - NullableBreadcrumbId& propBreadcrumb = moduleScope->props[parentBreadcrumb->def][key]; - if (!propBreadcrumb) - propBreadcrumb = breadcrumbs->emplace(parentBreadcrumb, defs->freshCell(), key); + std::string index = i->index.value; + auto& propDef = moduleScope->props[parentDef][index]; + if (!propDef) + propDef = defArena->freshCell(); - graph.astBreadcrumbs[i] = propBreadcrumb; - return NotNull{propBreadcrumb}; + return {NotNull{propDef}, keyArena->node(parentKey, NotNull{propDef}, index)}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i) { - BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr); - BreadcrumbId key = visitExpr(scope, i->index); + auto [parentDef, parentKey] = 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 = moduleScope->props[parentBreadcrumb->def][key]; - if (!propBreadcrumb) - propBreadcrumb = breadcrumbs->emplace(parentBreadcrumb, defs->freshCell(), key); + std::string index{string->value.data, string->value.size}; + auto& propDef = moduleScope->props[parentDef][index]; + if (!propDef) + propDef = defArena->freshCell(); - graph.astBreadcrumbs[i] = NotNull{propBreadcrumb}; - return NotNull{propBreadcrumb}; + return {NotNull{propDef}, keyArena->node(parentKey, NotNull{propDef}, index)}; } - return breadcrumbs->emplace(nullptr, defs->freshCell(), key); + return {defArena->freshCell(/* subscripted= */true), nullptr}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction* f) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction* f) { DfgScope* signatureScope = childScope(scope); @@ -493,10 +515,9 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction* f // There's no syntax for `self` to have an annotation if using `function t:m()` LUAU_ASSERT(!self->annotation); - // TODO: ParameterMetadata. - BreadcrumbId bc = breadcrumbs->add(nullptr, defs->freshCell()); - graph.localBreadcrumbs[self] = bc; - signatureScope->bindings[self] = bc; + DefId def = defArena->freshCell(); + graph.localDefs[self] = def; + signatureScope->bindings[self] = def; } for (AstLocal* param : f->args) @@ -504,10 +525,9 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction* f if (param->annotation) visitType(signatureScope, param->annotation); - // TODO: ParameterMetadata. - BreadcrumbId bc = breadcrumbs->add(nullptr, defs->freshCell()); - graph.localBreadcrumbs[param] = bc; - signatureScope->bindings[param] = bc; + DefId def = defArena->freshCell(); + graph.localDefs[param] = def; + signatureScope->bindings[param] = def; } if (f->varargAnnotation) @@ -526,10 +546,10 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction* f // g() --> 5 visit(signatureScope, f->body); - return breadcrumbs->add(nullptr, defs->freshCell()); + return {defArena->freshCell(), nullptr}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprTable* t) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprTable* t) { for (AstExprTable::Item item : t->items) { @@ -538,120 +558,132 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprTable* t) visitExpr(scope, item.value); } - return breadcrumbs->add(nullptr, defs->freshCell()); + return {defArena->freshCell(), nullptr}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprUnary* u) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprUnary* u) { visitExpr(scope, u->expr); - return breadcrumbs->add(nullptr, defs->freshCell()); + return {defArena->freshCell(), nullptr}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprBinary* b) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprBinary* b) { visitExpr(scope, b->left); visitExpr(scope, b->right); - return breadcrumbs->add(nullptr, defs->freshCell()); + return {defArena->freshCell(), nullptr}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprTypeAssertion* t) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprTypeAssertion* t) { - // TODO: TypeAssertionMetadata? - BreadcrumbId bc = visitExpr(scope, t->expr); + auto [def, key] = visitExpr(scope, t->expr); visitType(scope, t->annotation); - return bc; + return {def, key}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIfElse* i) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIfElse* i) { visitExpr(scope, i->condition); visitExpr(scope, i->trueExpr); visitExpr(scope, i->falseExpr); - return breadcrumbs->add(nullptr, defs->freshCell()); + return {defArena->freshCell(), nullptr}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprInterpString* i) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprInterpString* i) { for (AstExpr* e : i->expressions) visitExpr(scope, e); - return breadcrumbs->add(nullptr, defs->freshCell()); + return {defArena->freshCell(), nullptr}; } -BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprError* error) +DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprError* error) { DfgScope* unreachable = childScope(scope); for (AstExpr* e : error->expressions) visitExpr(unreachable, e); - return breadcrumbs->add(nullptr, defs->freshCell()); + return {defArena->freshCell(), nullptr}; } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, BreadcrumbId bc) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef, bool isCompoundAssignment) { if (auto l = e->as()) - return visitLValue(scope, l, bc); + return visitLValue(scope, l, incomingDef, isCompoundAssignment); else if (auto g = e->as()) - return visitLValue(scope, g, bc); + return visitLValue(scope, g, incomingDef, isCompoundAssignment); else if (auto i = e->as()) - return visitLValue(scope, i, bc); + return visitLValue(scope, i, incomingDef); else if (auto i = e->as()) - return visitLValue(scope, i, bc); + return visitLValue(scope, i, incomingDef); else if (auto error = e->as()) - return visitLValue(scope, error, bc); + return visitLValue(scope, error, incomingDef); else handle->ice("Unknown AstExpr in DataFlowGraphBuilder::visitLValue"); } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, BreadcrumbId bc) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment) { - // 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; + // We need to keep the previous breadcrumb around for a compound assignment. + if (isCompoundAssignment) + { + if (auto def = scope->lookup(l->local)) + graph.compoundAssignBreadcrumbs[l] = *def; + } + + // In order to avoid alias tracking, we need to clip the reference to the parent def. + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[l] = updated; scope->bindings[l->local] = updated; } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, BreadcrumbId bc) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment) { - // 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; + // We need to keep the previous breadcrumb around for a compound assignment. + if (isCompoundAssignment) + { + if (auto def = scope->lookup(g->name)) + graph.compoundAssignBreadcrumbs[g] = *def; + } + + // In order to avoid alias tracking, we need to clip the reference to the parent def. + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[g] = updated; scope->bindings[g->name] = updated; } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i, BreadcrumbId bc) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef) { - BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr); + DefId parentDef = visitExpr(scope, i->expr).def; - 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; + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[i] = updated; + scope->props[parentDef][i->index.value] = updated; } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, BreadcrumbId bc) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef) { - BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr); + DefId parentDef = visitExpr(scope, i->expr).def; visitExpr(scope, i->index); if (auto string = i->index->as()) { - 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; + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[i] = updated; + scope->props[parentDef][string->value.data] = updated; } + + graph.astDefs[i] = defArena->freshCell(); } -void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprError* error, BreadcrumbId bc) +void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprError* error, DefId incomingDef) { - visitExpr(scope, error); + DefId def = visitExpr(scope, error).def; + graph.astDefs[error] = def; } void DataFlowGraphBuilder::visitType(DfgScope* scope, AstType* t) diff --git a/Analysis/src/Def.cpp b/Analysis/src/Def.cpp index 7be075c25..d34b5cdc7 100644 --- a/Analysis/src/Def.cpp +++ b/Analysis/src/Def.cpp @@ -1,12 +1,22 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Def.h" +#include "Luau/Common.h" namespace Luau { -DefId DefArena::freshCell() +bool containsSubscriptedDefinition(DefId def) { - return NotNull{allocator.allocate(Def{Cell{}})}; + if (auto cell = get(def)) + return cell->subscripted; + + LUAU_ASSERT(!"Phi nodes not implemented yet"); + return false; +} + +DefId DefArena::freshCell(bool subscripted) +{ + return NotNull{allocator.allocate(Def{Cell{subscripted}})}; } } // namespace Luau diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 75e1f7c31..7a58a2444 100644 --- a/Analysis/src/IostreamHelpers.cpp +++ b/Analysis/src/IostreamHelpers.cpp @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/IostreamHelpers.h" #include "Luau/ToString.h" +#include "Luau/TypePath.h" namespace Luau { @@ -236,4 +237,34 @@ std::ostream& operator<<(std::ostream& stream, const TypePackVar& tv) return stream << toString(tv); } +std::ostream& operator<<(std::ostream& stream, TypeId ty) +{ + // we commonly use a null pointer when a type may not be present; we need to + // account for that here. + if (!ty) + return stream << ""; + + return stream << toString(ty); +} + +std::ostream& operator<<(std::ostream& stream, TypePackId tp) +{ + // we commonly use a null pointer when a type may not be present; we need to + // account for that here. + if (!tp) + return stream << ""; + + return stream << toString(tp); +} + +namespace TypePath +{ + +std::ostream& operator<<(std::ostream& stream, const Path& path) +{ + return stream << toString(path); +} + +} // namespace TypePath + } // namespace Luau diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index a7bb9d291..595794a0f 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -325,6 +325,7 @@ struct NonStrictTypeChecker return; TypeId fnTy = *originalCallTy; + // TODO: how should we link this to the passed in context here NonStrictContext fresh{}; if (auto fn = get(follow(fnTy))) { @@ -351,28 +352,20 @@ struct NonStrictTypeChecker // We will compare arg and ~number AstExpr* arg = call->args.data[i]; TypeId expectedArgType = argTypes[i]; - NullableBreadcrumbId bc = dfg->getBreadcrumb(arg); + DefId def = dfg->getDef(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; - } + TypeId runTimeErrorTy = arena.addType(NegationType{expectedArgType}); + fresh.context[def.get()] = runTimeErrorTy; } // Populate the context and now iterate through each of the arguments to the call to find out if we satisfy the types + AstName name = getIdentifier(call->func); 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); + reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, name.value, i}, arg->location); } } } @@ -401,25 +394,22 @@ struct NonStrictTypeChecker // 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)) + DefId def = dfg->getDef(fragment); + if (std::optional contextTy = context.find(def)) { - 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); + 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 && !r.isErrorSuppressing) + reportError(TypeMismatch{actualType, *contextTy}, fragment->location); - if (r.isSubtype) - return {actualType}; - } + if (r.isSubtype) + return {actualType}; } + return {}; } }; diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 4f9532c06..52bbc5d9c 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -22,6 +22,13 @@ LUAU_FASTFLAG(DebugLuauReadWriteProperties) namespace Luau { + +TypeIds::TypeIds(std::initializer_list tys) +{ + for (TypeId ty : tys) + insert(ty); +} + void TypeIds::insert(TypeId ty) { ty = follow(ty); diff --git a/Analysis/src/Refinement.cpp b/Analysis/src/Refinement.cpp index a81063c7b..e98b6e5a9 100644 --- a/Analysis/src/Refinement.cpp +++ b/Analysis/src/Refinement.cpp @@ -1,37 +1,60 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Refinement.h" +#include namespace Luau { RefinementId RefinementArena::variadic(const std::vector& refis) { + bool hasRefinements = false; + for (RefinementId r : refis) + hasRefinements |= bool(r); + + if (!hasRefinements) + return nullptr; + return NotNull{allocator.allocate(Variadic{refis})}; } RefinementId RefinementArena::negation(RefinementId refinement) { + if (!refinement) + return nullptr; + return NotNull{allocator.allocate(Negation{refinement})}; } RefinementId RefinementArena::conjunction(RefinementId lhs, RefinementId rhs) { + if (!lhs && !rhs) + return nullptr; + return NotNull{allocator.allocate(Conjunction{lhs, rhs})}; } RefinementId RefinementArena::disjunction(RefinementId lhs, RefinementId rhs) { + if (!lhs && !rhs) + return nullptr; + return NotNull{allocator.allocate(Disjunction{lhs, rhs})}; } RefinementId RefinementArena::equivalence(RefinementId lhs, RefinementId rhs) { + if (!lhs && !rhs) + return nullptr; + return NotNull{allocator.allocate(Equivalence{lhs, rhs})}; } -RefinementId RefinementArena::proposition(BreadcrumbId breadcrumb, TypeId discriminantTy) +RefinementId RefinementArena::proposition(const RefinementKey* key, TypeId discriminantTy) { - return NotNull{allocator.allocate(Proposition{breadcrumb, discriminantTy})}; + if (!key) + return nullptr; + + return NotNull{allocator.allocate(Proposition{key, discriminantTy})}; } } // namespace Luau diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index 929125305..2ca40bdd9 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -38,6 +38,23 @@ std::optional Scope::lookup(Symbol sym) const return std::nullopt; } +std::optional> Scope::lookupEx(DefId def) +{ + Scope* s = this; + + while (true) + { + TypeId* it = s->lvalueTypes.find(def); + if (it) + return std::pair{*it, s}; + + if (s->parent) + s = s->parent.get(); + else + return std::nullopt; + } +} + std::optional> Scope::lookupEx(Symbol sym) { Scope* s = this; diff --git a/Analysis/src/Substitution.cpp b/Analysis/src/Substitution.cpp index a34a45c8e..176f15069 100644 --- a/Analysis/src/Substitution.cpp +++ b/Analysis/src/Substitution.cpp @@ -75,6 +75,7 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a clone.dcrMagicRefinement = a.dcrMagicRefinement; clone.tags = a.tags; clone.argNames = a.argNames; + clone.isCheckedFunction = a.isCheckedFunction; return dest.addType(std::move(clone)); } else if constexpr (std::is_same_v) diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 79bedec20..e386bf7b9 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -11,6 +11,7 @@ #include "Luau/Type.h" #include "Luau/TypeArena.h" #include "Luau/TypePack.h" +#include "Luau/TypePath.h" #include "Luau/TypeUtils.h" #include @@ -44,8 +45,19 @@ struct VarianceFlipper } }; +bool SubtypingReasoning::operator==(const SubtypingReasoning& other) const +{ + return subPath == other.subPath && superPath == other.superPath; +} + SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) { + // If this result is a subtype, we take the other result's reasoning. If + // this result is not a subtype, we keep the current reasoning, even if the + // other isn't a subtype. + if (isSubtype) + reasoning = other.reasoning; + isSubtype &= other.isSubtype; // `|=` is intentional here, we want to preserve error related flags. isErrorSuppressing |= other.isErrorSuppressing; @@ -57,6 +69,11 @@ SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other) { + // If the other result is not a subtype, we take the other result's + // reasoning. + if (!other.isSubtype) + reasoning = other.reasoning; + isSubtype |= other.isSubtype; isErrorSuppressing |= other.isErrorSuppressing; normalizationTooComplex |= other.normalizationTooComplex; @@ -65,6 +82,56 @@ SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other) return *this; } +SubtypingResult& SubtypingResult::withBothComponent(TypePath::Component component) +{ + return withSubComponent(component).withSuperComponent(component); +} + +SubtypingResult& SubtypingResult::withSubComponent(TypePath::Component component) +{ + if (!reasoning) + reasoning = SubtypingReasoning{Path(), Path()}; + + reasoning->subPath = reasoning->subPath.push_front(component); + + return *this; +} + +SubtypingResult& SubtypingResult::withSuperComponent(TypePath::Component component) +{ + if (!reasoning) + reasoning = SubtypingReasoning{Path(), Path()}; + + reasoning->superPath = reasoning->superPath.push_front(component); + + return *this; +} + +SubtypingResult& SubtypingResult::withBothPath(TypePath::Path path) +{ + return withSubPath(path).withSuperPath(path); +} + +SubtypingResult& SubtypingResult::withSubPath(TypePath::Path path) +{ + if (!reasoning) + reasoning = SubtypingReasoning{Path(), Path()}; + + reasoning->subPath = path.append(reasoning->subPath); + + return *this; +} + +SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path) +{ + if (!reasoning) + reasoning = SubtypingReasoning{Path(), Path()}; + + reasoning->superPath = path.append(reasoning->superPath); + + return *this; +} + SubtypingResult SubtypingResult::negate(const SubtypingResult& result) { return SubtypingResult{ @@ -287,7 +354,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (get(subTy)) result = {false, true}; else if (auto p = get2(subTy, superTy)) - result = isCovariantWith(env, p.first->ty, p.second->ty); + result = isCovariantWith(env, p.first->ty, p.second->ty).withBothComponent(TypePath::TypeField::Negated); else if (auto subNegation = get(subTy)) result = isCovariantWith(env, subNegation, superTy); else if (auto superNegation = get(superTy)) @@ -350,9 +417,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId for (size_t i = 0; i < headSize; ++i) { - results.push_back(isCovariantWith(env, subHead[i], superHead[i])); + results.push_back(isCovariantWith(env, subHead[i], superHead[i]).withBothComponent(TypePath::Index{i})); if (!results.back().isSubtype) - return {false}; + return results.back(); } // Handle mismatched head sizes @@ -364,7 +431,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId if (auto vt = get(*subTail)) { for (size_t i = headSize; i < superHead.size(); ++i) - results.push_back(isCovariantWith(env, vt->ty, superHead[i])); + results.push_back(isCovariantWith(env, vt->ty, superHead[i]) + .withSubComponent(TypePath::TypeField::Variadic) + .withSuperComponent(TypePath::Index{i})); } else if (auto gt = get(*subTail)) { @@ -379,7 +448,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId TypePackId superTailPack = arena->addTypePack(std::move(headSlice), superTail); if (TypePackId* other = env.mappedGenericPacks.find(*subTail)) - results.push_back(isCovariantWith(env, *other, superTailPack)); + // TODO: TypePath can't express "slice of a pack + its tail". + results.push_back(isCovariantWith(env, *other, superTailPack).withSubComponent(TypePath::PackField::Tail)); else env.mappedGenericPacks.try_insert(*subTail, superTailPack); @@ -393,7 +463,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId // // (T) -> () (X) -> () // - return {false}; + return SubtypingResult{false}.withSubComponent(TypePath::PackField::Tail); } } else @@ -409,7 +479,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId if (auto vt = get(*superTail)) { for (size_t i = headSize; i < subHead.size(); ++i) - results.push_back(isCovariantWith(env, subHead[i], vt->ty)); + results.push_back(isCovariantWith(env, subHead[i], vt->ty) + .withSubComponent(TypePath::Index{i}) + .withSuperComponent(TypePath::TypeField::Variadic)); } else if (auto gt = get(*superTail)) { @@ -424,7 +496,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId TypePackId subTailPack = arena->addTypePack(std::move(headSlice), subTail); if (TypePackId* other = env.mappedGenericPacks.find(*superTail)) - results.push_back(isCovariantWith(env, *other, subTailPack)); + // TODO: TypePath can't express "slice of a pack + its tail". + results.push_back(isCovariantWith(env, *other, subTailPack).withSuperComponent(TypePath::PackField::Tail)); else env.mappedGenericPacks.try_insert(*superTail, subTailPack); @@ -437,7 +510,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId // For any non-generic type T: // // () -> T () -> X... - return {false}; + return SubtypingResult{false}.withSuperComponent(TypePath::PackField::Tail); } } else @@ -453,12 +526,14 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { if (auto p = get2(*subTail, *superTail)) { - results.push_back(isCovariantWith(env, p)); + // Variadic component is added by the isCovariantWith + // implementation; no need to add it here. + results.push_back(isCovariantWith(env, p).withBothComponent(TypePath::PackField::Tail)); } else if (auto p = get2(*subTail, *superTail)) { bool ok = bindGeneric(env, *subTail, *superTail); - results.push_back({ok}); + results.push_back(SubtypingResult{ok}.withBothComponent(TypePath::PackField::Tail)); } else if (get2(*subTail, *superTail)) { @@ -466,12 +541,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { // (A...) -> number <: (...number) -> number bool ok = bindGeneric(env, *subTail, *superTail); - results.push_back({ok}); + results.push_back(SubtypingResult{ok}.withBothComponent(TypePath::PackField::Tail)); } else { // (number) -> ...number (number) -> A... - results.push_back({false}); + results.push_back(SubtypingResult{false}.withBothComponent(TypePath::PackField::Tail)); } } else if (get2(*subTail, *superTail)) @@ -479,13 +554,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId if (variance == Variance::Contravariant) { // (...number) -> number (A...) -> number - results.push_back({false}); + results.push_back(SubtypingResult{false}.withBothComponent(TypePath::PackField::Tail)); } else { // () -> A... <: () -> ...number bool ok = bindGeneric(env, *subTail, *superTail); - results.push_back({ok}); + results.push_back(SubtypingResult{ok}.withBothComponent(TypePath::PackField::Tail)); } } else @@ -496,12 +571,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { if (get(*subTail)) { - return {false}; + return SubtypingResult{false}.withSubComponent(TypePath::PackField::Tail); } else if (get(*subTail)) { bool ok = bindGeneric(env, *subTail, builtinTypes->emptyTypePack); - return {ok}; + return SubtypingResult{ok}.withSubComponent(TypePath::PackField::Tail); } else unexpected(*subTail); @@ -525,10 +600,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId if (variance == Variance::Contravariant) { bool ok = bindGeneric(env, builtinTypes->emptyTypePack, *superTail); - results.push_back({ok}); + results.push_back(SubtypingResult{ok}.withSuperComponent(TypePath::PackField::Tail)); } else - results.push_back({false}); + results.push_back(SubtypingResult{false}.withSuperComponent(TypePath::PackField::Tail)); } else iceReporter->ice("Subtyping test encountered the unexpected type pack: " + toString(*superTail)); @@ -540,7 +615,15 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId template SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { - return isCovariantWith(env, superTy, subTy); + SubtypingResult result = isCovariantWith(env, superTy, subTy); + // If we don't swap the paths here, we will end up producing an invalid path + // whenever we involve contravariance. We'll end up appending path + // components that should belong to the supertype to the subtype, and vice + // versa. + if (result.reasoning) + std::swap(result.reasoning->subPath, result.reasoning->superPath); + + return result; } template @@ -602,8 +685,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub { // As per TAPL: T <: A | B iff T <: A || T <: B std::vector subtypings; + size_t i = 0; for (TypeId ty : superUnion) - subtypings.push_back(isCovariantWith(env, subTy, ty)); + subtypings.push_back(isCovariantWith(env, subTy, ty).withSuperComponent(TypePath::Index{i++})); return SubtypingResult::any(subtypings); } @@ -611,8 +695,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Unio { // As per TAPL: A | B <: T iff A <: T && B <: T std::vector subtypings; + size_t i = 0; for (TypeId ty : subUnion) - subtypings.push_back(isCovariantWith(env, ty, superTy)); + subtypings.push_back(isCovariantWith(env, ty, superTy).withSubComponent(TypePath::Index{i++})); return SubtypingResult::all(subtypings); } @@ -620,8 +705,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub { // As per TAPL: T <: A & B iff T <: A && T <: B std::vector subtypings; + size_t i = 0; for (TypeId ty : superIntersection) - subtypings.push_back(isCovariantWith(env, subTy, ty)); + subtypings.push_back(isCovariantWith(env, subTy, ty).withSuperComponent(TypePath::Index{i++})); return SubtypingResult::all(subtypings); } @@ -629,8 +715,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Inte { // As per TAPL: A & B <: T iff A <: T || B <: T std::vector subtypings; + size_t i = 0; for (TypeId ty : subIntersection) - subtypings.push_back(isCovariantWith(env, ty, superTy)); + subtypings.push_back(isCovariantWith(env, ty, superTy).withSubComponent(TypePath::Index{i++})); return SubtypingResult::any(subtypings); } @@ -638,23 +725,25 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega { TypeId negatedTy = follow(subNegation->ty); + SubtypingResult result; + // In order to follow a consistent codepath, rather than folding the // isCovariantWith test down to its conclusion here, we test the subtyping test // of the result of negating the type for never, unknown, any, and error. if (is(negatedTy)) { // ¬never ~ unknown - return isCovariantWith(env, builtinTypes->unknownType, superTy); + result = isCovariantWith(env, builtinTypes->unknownType, superTy); } else if (is(negatedTy)) { // ¬unknown ~ never - return isCovariantWith(env, builtinTypes->neverType, superTy); + result = isCovariantWith(env, builtinTypes->neverType, superTy); } else if (is(negatedTy)) { // ¬any ~ any - return isCovariantWith(env, negatedTy, superTy); + result = isCovariantWith(env, negatedTy, superTy); } else if (auto u = get(negatedTy)) { @@ -668,7 +757,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy)); } - return SubtypingResult::all(subtypings); + result = SubtypingResult::all(subtypings); } else if (auto i = get(negatedTy)) { @@ -687,7 +776,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega } } - return SubtypingResult::any(subtypings); + result = SubtypingResult::any(subtypings); } else if (is(negatedTy)) { @@ -697,28 +786,32 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega // subtype of other stuff. else { - return {false}; + result = {false}; } + + return result.withSubComponent(TypePath::TypeField::Negated); } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const NegationType* superNegation) { TypeId negatedTy = follow(superNegation->ty); + SubtypingResult result; + if (is(negatedTy)) { // ¬never ~ unknown - return isCovariantWith(env, subTy, builtinTypes->unknownType); + result = isCovariantWith(env, subTy, builtinTypes->unknownType); } else if (is(negatedTy)) { // ¬unknown ~ never - return isCovariantWith(env, subTy, builtinTypes->neverType); + result = isCovariantWith(env, subTy, builtinTypes->neverType); } else if (is(negatedTy)) { // ¬any ~ any - return isSubtype(subTy, negatedTy); + result = isSubtype(subTy, negatedTy); } else if (auto u = get(negatedTy)) { @@ -737,7 +830,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type } } - return SubtypingResult::all(subtypings); + result = SubtypingResult::all(subtypings); } else if (auto i = get(negatedTy)) { @@ -756,53 +849,55 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type } } - return SubtypingResult::any(subtypings); + result = SubtypingResult::any(subtypings); } else if (auto p = get2(subTy, negatedTy)) { // number <: ¬boolean // number type != p.second->type}; + result = {p.first->type != p.second->type}; } else if (auto p = get2(subTy, negatedTy)) { // "foo" (p.first) && p.second->type == PrimitiveType::String) - return {false}; + result = {false}; // false (p.first) && p.second->type == PrimitiveType::Boolean) - return {false}; + result = {false}; // other cases are true else - return {true}; + result = {true}; } else if (auto p = get2(subTy, negatedTy)) { if (p.first->type == PrimitiveType::String && get(p.second)) - return {false}; + result = {false}; else if (p.first->type == PrimitiveType::Boolean && get(p.second)) - return {false}; + result = {false}; else - return {true}; + result = {true}; } // the top class type is not actually a primitive type, so the negation of // any one of them includes the top class type. else if (auto p = get2(subTy, negatedTy)) - return {true}; + result = {true}; else if (auto p = get(negatedTy); p && is(subTy)) - return {p->type != PrimitiveType::Table}; + result = {p->type != PrimitiveType::Table}; else if (auto p = get2(subTy, negatedTy)) - return {p.second->type != PrimitiveType::Function}; + result = {p.second->type != PrimitiveType::Function}; else if (auto p = get2(subTy, negatedTy)) - return {*p.first != *p.second}; + result = {*p.first != *p.second}; else if (auto p = get2(subTy, negatedTy)) - return SubtypingResult::negate(isCovariantWith(env, p.first, p.second)); + result = SubtypingResult::negate(isCovariantWith(env, p.first, p.second)); else if (get2(subTy, negatedTy)) - return {true}; + result = {true}; else if (is(negatedTy)) iceReporter->ice("attempting to negate a non-testable type"); + else + result = {false}; - return {false}; + return result.withSuperComponent(TypePath::TypeField::Negated); } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const PrimitiveType* superPrim) @@ -836,16 +931,19 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl { std::vector results; if (auto it = subTable->props.find(name); it != subTable->props.end()) - results.push_back(isInvariantWith(env, it->second.type(), prop.type())); + results.push_back(isInvariantWith(env, it->second.type(), prop.type()) + .withBothComponent(TypePath::Property(name))); if (subTable->indexer) { if (isInvariantWith(env, subTable->indexer->indexType, builtinTypes->stringType).isSubtype) - results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, prop.type())); + results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, prop.type()) + .withSubComponent(TypePath::TypeField::IndexResult) + .withSuperComponent(TypePath::Property(name))); } if (results.empty()) - return {false}; + return SubtypingResult{false}; result.andAlso(SubtypingResult::all(results)); } @@ -863,7 +961,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt) { - return isCovariantWith(env, subMt->table, superMt->table).andAlso(isCovariantWith(env, subMt->metatable, superMt->metatable)); + return isCovariantWith(env, subMt->table, superMt->table) + .andAlso(isCovariantWith(env, subMt->metatable, superMt->metatable).withBothComponent(TypePath::TypeField::Metatable)); } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable) @@ -900,7 +999,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Clas for (const auto& [name, prop] : superTable->props) { if (auto classProp = lookupClassProp(subClass, name)) - result.andAlso(isInvariantWith(env, prop.type(), classProp->type())); + result.andAlso(isInvariantWith(env, prop.type(), classProp->type()).withBothComponent(TypePath::Property(name))); else return SubtypingResult{false}; } @@ -913,10 +1012,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Func SubtypingResult result; { VarianceFlipper vf{&variance}; - result.orElse(isContravariantWith(env, subFunction->argTypes, superFunction->argTypes)); + result.orElse(isContravariantWith(env, subFunction->argTypes, superFunction->argTypes).withBothComponent(TypePath::PackField::Arguments)); } - result.andAlso(isCovariantWith(env, subFunction->retTypes, superFunction->retTypes)); + result.andAlso(isCovariantWith(env, subFunction->retTypes, superFunction->retTypes).withBothComponent(TypePath::PackField::Returns)); return result; } @@ -933,7 +1032,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Prim if (auto it = mttv->props.find("__index"); it != mttv->props.end()) { if (auto stringTable = get(it->second.type())) - result.orElse(isCovariantWith(env, stringTable, superTable)); + result.orElse( + isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().prop("__index").build())); } } } @@ -954,7 +1054,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Sing if (auto it = mttv->props.find("__index"); it != mttv->props.end()) { if (auto stringTable = get(it->second.type())) - result.orElse(isCovariantWith(env, stringTable, superTable)); + result.orElse( + isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().prop("__index").build())); } } } @@ -965,7 +1066,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Sing SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TableIndexer& subIndexer, const TableIndexer& superIndexer) { return isInvariantWith(env, subIndexer.indexType, superIndexer.indexType) - .andAlso(isInvariantWith(env, superIndexer.indexResultType, subIndexer.indexResultType)); + .withBothComponent(TypePath::TypeField::IndexLookup) + .andAlso(isInvariantWith(env, superIndexer.indexResultType, subIndexer.indexResultType).withBothComponent(TypePath::TypeField::IndexResult)); } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm) @@ -1092,11 +1194,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type { std::vector results; + size_t i = 0; for (TypeId subTy : subTypes) { results.emplace_back(); for (TypeId superTy : superTypes) - results.back().orElse(isCovariantWith(env, subTy, superTy)); + results.back().orElse(isCovariantWith(env, subTy, superTy).withBothComponent(TypePath::Index{i++})); } return SubtypingResult::all(results); @@ -1104,7 +1207,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic) { - return isCovariantWith(env, subVariadic->ty, superVariadic->ty); + return isCovariantWith(env, subVariadic->ty, superVariadic->ty).withBothComponent(TypePath::TypeField::Variadic); } bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId superTy) diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 8123e09f9..58c03db43 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/ToString.h" +#include "Luau/Common.h" #include "Luau/Constraint.h" #include "Luau/Location.h" #include "Luau/Scope.h" @@ -10,6 +11,7 @@ #include "Luau/Type.h" #include "Luau/TypeFamily.h" #include "Luau/VisitType.h" +#include "Luau/TypeOrPack.h" #include #include @@ -620,6 +622,12 @@ struct TypeStringifier state.emit(">"); } + if (FFlag::DebugLuauDeferredConstraintResolution) + { + if (ftv.isCheckedFunction) + state.emit("@checked "); + } + state.emit("("); if (state.opts.functionTypeArguments) @@ -1686,21 +1694,6 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) std::string superStr = tos(c.superType); return subStr + " ~ inst " + superStr; } - else if constexpr (std::is_same_v) - { - std::string resultStr = tos(c.resultType); - std::string operandStr = tos(c.operandType); - - return resultStr + " ~ Unary<" + toString(c.op) + ", " + operandStr + ">"; - } - else if constexpr (std::is_same_v) - { - std::string resultStr = tos(c.resultType); - std::string leftStr = tos(c.leftType); - std::string rightStr = tos(c.rightType); - - return resultStr + " ~ Binary<" + toString(c.op) + ", " + leftStr + ", " + rightStr + ">"; - } else if constexpr (std::is_same_v) { std::string iteratorStr = tos(c.iterator); @@ -1756,6 +1749,23 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) const char* op = c.mode == RefineConstraint::Union ? "union" : "intersect"; return tos(c.resultType) + " ~ refine " + tos(c.type) + " " + op + " " + tos(c.discriminant); } + else if constexpr (std::is_same_v) + { + const char* op = c.mode == SetOpConstraint::Union ? " | " : " & "; + std::string res = tos(c.resultType) + " ~ "; + bool first = true; + for (TypeId t : c.types) + { + if (first) + first = false; + else + res += op; + + res += tos(t); + } + + return res; + } else if constexpr (std::is_same_v) return "reduce " + tos(c.ty); else if constexpr (std::is_same_v) @@ -1834,4 +1844,24 @@ std::string toString(const Location& location, int offset, bool useBegin) } } +std::string toString(const TypeOrPack& tyOrTp, ToStringOptions& opts) +{ + if (const TypeId* ty = get(tyOrTp)) + return toString(*ty, opts); + else if (const TypePackId* tp = get(tyOrTp)) + return toString(*tp, opts); + else + LUAU_UNREACHABLE(); +} + +std::string dump(const TypeOrPack& tyOrTp) +{ + ToStringOptions opts; + opts.exhaustive = true; + opts.functionTypeArguments = true; + std::string s = toString(tyOrTp, opts); + printf("%s\n", s.c_str()); + return s; +} + } // namespace Luau diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 24925a1a8..166b7525f 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -15,9 +15,11 @@ #include "Luau/ToString.h" #include "Luau/TxnLog.h" #include "Luau/Type.h" +#include "Luau/TypeFamily.h" +#include "Luau/TypeFwd.h" #include "Luau/TypePack.h" +#include "Luau/TypePath.h" #include "Luau/TypeUtils.h" -#include "Luau/TypeFamily.h" #include "Luau/VisitType.h" #include @@ -2395,7 +2397,27 @@ struct TypeChecker2 reportError(NormalizationTooComplex{}, location); if (!r.isSubtype && !r.isErrorSuppressing) - reportError(TypeMismatch{superTy, subTy}, location); + { + if (r.reasoning) + { + std::optional subLeaf = traverse(subTy, r.reasoning->subPath, builtinTypes); + std::optional superLeaf = traverse(superTy, r.reasoning->superPath, builtinTypes); + + if (!subLeaf || !superLeaf) + ice->ice("Subtyping test returned a reasoning with an invalid path", location); + + if (!get2(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) + ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); + + std::string reason = "type " + toString(subTy) + toString(r.reasoning->subPath) + " (" + toString(*subLeaf) + + ") is not a subtype of " + toString(superTy) + toString(r.reasoning->superPath) + " (" + toString(*superLeaf) + + ")"; + + reportError(TypeMismatch{superTy, subTy, reason}, location); + } + else + reportError(TypeMismatch{superTy, subTy}, location); + } return r.isSubtype; } diff --git a/Analysis/src/TypeFamily.cpp b/Analysis/src/TypeFamily.cpp index f631d795f..7e4a591d2 100644 --- a/Analysis/src/TypeFamily.cpp +++ b/Analysis/src/TypeFamily.cpp @@ -338,6 +338,150 @@ TypeFamilyReductionResult notFamilyFn(const std::vector& typePar return {ctx->builtins->booleanType, false, {}, {}}; } +TypeFamilyReductionResult lenFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull ctx) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("len type family: encountered a type family instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId operandTy = follow(typeParams.at(0)); + const NormalizedType* normTy = ctx->normalizer->normalize(operandTy); + + // if the type failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normTy) + return {std::nullopt, false, {}, {}}; + + // if the operand type is error suppressing, we can immediately reduce to `number`. + if (normTy->shouldSuppressErrors()) + return {ctx->builtins->numberType, false, {}, {}}; + + // if we have a `never`, we can never observe that the operator didn't work. + if (is(operandTy)) + return {ctx->builtins->neverType, false, {}, {}}; + + // if we're checking the length of a string, that works! + if (normTy->isSubtypeOfString()) + return {ctx->builtins->numberType, false, {}, {}}; + + // we use the normalized operand here in case there was an intersection or union. + TypeId normalizedOperand = ctx->normalizer->typeFromNormal(*normTy); + if (normTy->hasTopTable() || get(normalizedOperand)) + return {ctx->builtins->numberType, false, {}, {}}; + + // otherwise, we wait to see if the operand type is resolved + if (isPending(operandTy, ctx->solver)) + return {std::nullopt, false, {operandTy}, {}}; + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, operandTy, "__len", Location{}); + if (!mmType) + 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({operandTy}); + 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, {}, {}}; + + // `len` must return a `number`. + return {ctx->builtins->numberType, false, {}, {}}; +} + +TypeFamilyReductionResult unmFamilyFn( + const std::vector& typeParams, const std::vector& packParams, NotNull ctx) +{ + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("unm type family: encountered a type family instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId operandTy = follow(typeParams.at(0)); + const NormalizedType* normTy = ctx->normalizer->normalize(operandTy); + + // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance. + if (!normTy) + return {std::nullopt, false, {}, {}}; + + // if the operand is error suppressing, we can just go ahead and reduce. + if (normTy->shouldSuppressErrors()) + return {operandTy, false, {}, {}}; + + // if we have a `never`, we can never observe that the operation didn't work. + if (is(operandTy)) + return {ctx->builtins->neverType, false, {}, {}}; + + // If the type is exactly `number`, we can reduce now. + if (normTy->isExactlyNumber()) + return {ctx->builtins->numberType, false, {}, {}}; + + // otherwise, check if we need to wait on the type to be further resolved + if (isPending(operandTy, ctx->solver)) + return {std::nullopt, false, {operandTy}, {}}; + + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, operandTy, "__unm", Location{}); + if (!mmType) + return {std::nullopt, 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({operandTy}); + 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, {}, {}}; + + if (std::optional ret = first(instantiatedMmFtv->retTypes)) + return {*ret, false, {}, {}}; + else + return {std::nullopt, true, {}, {}}; +} + TypeFamilyReductionResult numericBinopFamilyFn( const std::vector& typeParams, const std::vector& packParams, NotNull ctx, const std::string metamethod) { @@ -816,6 +960,8 @@ TypeFamilyReductionResult eqFamilyFn(const std::vector& typePara BuiltinTypeFamilies::BuiltinTypeFamilies() : notFamily{"not", notFamilyFn} + , lenFamily{"len", lenFamilyFn} + , unmFamily{"unm", unmFamilyFn} , addFamily{"add", addFamilyFn} , subFamily{"sub", subFamilyFn} , mulFamily{"mul", mulFamilyFn} @@ -834,6 +980,14 @@ BuiltinTypeFamilies::BuiltinTypeFamilies() void BuiltinTypeFamilies::addToScope(NotNull arena, NotNull scope) const { + // make a type function for a one-argument type family + auto mkUnaryTypeFamily = [&](const TypeFamily* family) { + TypeId t = arena->addType(GenericType{"T"}); + GenericTypeDefinition genericT{t}; + + return TypeFun{{genericT}, arena->addType(TypeFamilyInstanceType{NotNull{family}, {t}, {}})}; + }; + // make a type function for a two-argument type family auto mkBinaryTypeFamily = [&](const TypeFamily* family) { TypeId t = arena->addType(GenericType{"T"}); @@ -844,6 +998,9 @@ void BuiltinTypeFamilies::addToScope(NotNull arena, NotNull sc return TypeFun{{genericT, genericU}, arena->addType(TypeFamilyInstanceType{NotNull{family}, {t, u}, {}})}; }; + scope->exportedTypeBindings[lenFamily.name] = mkUnaryTypeFamily(&lenFamily); + scope->exportedTypeBindings[unmFamily.name] = mkUnaryTypeFamily(&unmFamily); + scope->exportedTypeBindings[addFamily.name] = mkBinaryTypeFamily(&addFamily); scope->exportedTypeBindings[subFamily.name] = mkBinaryTypeFamily(&subFamily); scope->exportedTypeBindings[mulFamily.name] = mkBinaryTypeFamily(&mulFamily); diff --git a/Analysis/src/TypeOrPack.cpp b/Analysis/src/TypeOrPack.cpp new file mode 100644 index 000000000..86652141d --- /dev/null +++ b/Analysis/src/TypeOrPack.cpp @@ -0,0 +1,29 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/TypeOrPack.h" +#include "Luau/Common.h" + +namespace Luau +{ + +const void* ptr(TypeOrPack tyOrTp) +{ + if (auto ty = get(tyOrTp)) + return static_cast(*ty); + else if (auto tp = get(tyOrTp)) + return static_cast(*tp); + else + LUAU_UNREACHABLE(); +} + +TypeOrPack follow(TypeOrPack tyOrTp) +{ + if (auto ty = get(tyOrTp)) + return follow(*ty); + else if (auto tp = get(tyOrTp)) + return follow(*tp); + else + LUAU_UNREACHABLE(); +} + +} // namespace Luau diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp new file mode 100644 index 000000000..ff515bed1 --- /dev/null +++ b/Analysis/src/TypePath.cpp @@ -0,0 +1,633 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/TypePath.h" +#include "Luau/Common.h" +#include "Luau/DenseHash.h" +#include "Luau/Type.h" +#include "Luau/TypeFwd.h" +#include "Luau/TypePack.h" +#include "Luau/TypeUtils.h" + +#include +#include +#include +#include + +LUAU_FASTFLAG(DebugLuauReadWriteProperties); + +// Maximum number of steps to follow when traversing a path. May not always +// equate to the number of components in a path, depending on the traversal +// logic. +LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypePathMaximumTraverseSteps, 100); + +namespace Luau +{ + +namespace TypePath +{ + +Property::Property(std::string name) + : name(std::move(name)) +{ + LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties); +} + +Property Property::read(std::string name) +{ + return Property(std::move(name), true); +} + +Property Property::write(std::string name) +{ + return Property(std::move(name), false); +} + +bool Property::operator==(const Property& other) const +{ + return name == other.name && isRead == other.isRead; +} + +bool Index::operator==(const Index& other) const +{ + return index == other.index; +} + +Path Path::append(const Path& suffix) const +{ + std::vector joined(components); + joined.reserve(suffix.components.size()); + joined.insert(joined.end(), suffix.components.begin(), suffix.components.end()); + return Path(std::move(joined)); +} + +Path Path::push(Component component) const +{ + std::vector joined(components); + joined.push_back(component); + return Path(std::move(joined)); +} + +Path Path::push_front(Component component) const +{ + std::vector joined{}; + joined.reserve(components.size() + 1); + joined.push_back(std::move(component)); + joined.insert(joined.end(), components.begin(), components.end()); + return Path(std::move(joined)); +} + +Path Path::pop() const +{ + if (empty()) + return kEmpty; + + std::vector popped(components); + popped.pop_back(); + return Path(std::move(popped)); +} + +std::optional Path::last() const +{ + if (empty()) + return std::nullopt; + + return components.back(); +} + +bool Path::empty() const +{ + return components.empty(); +} + +bool Path::operator==(const Path& other) const +{ + return components == other.components; +} + +Path PathBuilder::build() +{ + return Path(std::move(components)); +} + +PathBuilder& PathBuilder::readProp(std::string name) +{ + LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); + components.push_back(Property{std::move(name), true}); + return *this; +} + +PathBuilder& PathBuilder::writeProp(std::string name) +{ + LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); + components.push_back(Property{std::move(name), false}); + return *this; +} + +PathBuilder& PathBuilder::prop(std::string name) +{ + LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties); + components.push_back(Property{std::move(name)}); + return *this; +} + +PathBuilder& PathBuilder::index(size_t i) +{ + components.push_back(Index{i}); + return *this; +} + +PathBuilder& PathBuilder::mt() +{ + components.push_back(TypeField::Metatable); + return *this; +} + +PathBuilder& PathBuilder::lb() +{ + components.push_back(TypeField::LowerBound); + return *this; +} + +PathBuilder& PathBuilder::ub() +{ + components.push_back(TypeField::UpperBound); + return *this; +} + +PathBuilder& PathBuilder::indexKey() +{ + components.push_back(TypeField::IndexLookup); + return *this; +} + +PathBuilder& PathBuilder::indexValue() +{ + components.push_back(TypeField::IndexResult); + return *this; +} + +PathBuilder& PathBuilder::negated() +{ + components.push_back(TypeField::Negated); + return *this; +} + +PathBuilder& PathBuilder::variadic() +{ + components.push_back(TypeField::Variadic); + return *this; +} + +PathBuilder& PathBuilder::args() +{ + components.push_back(PackField::Arguments); + return *this; +} + +PathBuilder& PathBuilder::rets() +{ + components.push_back(PackField::Returns); + return *this; +} + +PathBuilder& PathBuilder::tail() +{ + components.push_back(PackField::Tail); + return *this; +} + +} // namespace TypePath + +namespace +{ + +struct TraversalState +{ + TraversalState(TypeId root, NotNull builtinTypes) + : current(root) + , builtinTypes(builtinTypes) + { + } + TraversalState(TypePackId root, NotNull builtinTypes) + : current(root) + , builtinTypes(builtinTypes) + { + } + + TypeOrPack current; + NotNull builtinTypes; + + DenseHashSet seen{nullptr}; + int steps = 0; + + void updateCurrent(TypeId ty) + { + LUAU_ASSERT(ty); + current = follow(ty); + } + + void updateCurrent(TypePackId tp) + { + LUAU_ASSERT(tp); + current = follow(tp); + } + + bool haveCycle() + { + const void* currentPtr = ptr(current); + + if (seen.contains(currentPtr)) + return true; + else + seen.insert(currentPtr); + + return false; + } + + bool tooLong() + { + return ++steps > DFInt::LuauTypePathMaximumTraverseSteps; + } + + bool checkInvariants() + { + return haveCycle() || tooLong(); + } + + bool traverse(const TypePath::Property& property) + { + auto currentType = get(current); + if (!currentType) + return false; + + if (checkInvariants()) + return false; + + const Property* prop = nullptr; + + if (auto t = get(*currentType)) + { + auto it = t->props.find(property.name); + if (it != t->props.end()) + { + prop = &it->second; + } + } + else if (auto c = get(*currentType)) + { + prop = lookupClassProp(c, property.name); + } + else if (auto m = getMetatable(*currentType, builtinTypes)) + { + // Weird: rather than use findMetatableEntry, which requires a lot + // of stuff that we don't have and don't want to pull in, we use the + // path traversal logic to grab __index and then re-enter the lookup + // logic there. + updateCurrent(*m); + + if (!traverse(TypePath::Property{"__index"})) + return false; + + return traverse(property); + } + + if (prop) + { + std::optional maybeType; + if (FFlag::DebugLuauReadWriteProperties) + maybeType = property.isRead ? prop->readType() : prop->writeType(); + else + maybeType = prop->type(); + + if (maybeType) + { + updateCurrent(*maybeType); + return true; + } + } + + return false; + } + + bool traverse(const TypePath::Index& index) + { + if (checkInvariants()) + return false; + + if (auto currentType = get(current)) + { + if (auto u = get(*currentType)) + { + auto it = begin(u); + std::advance(it, index.index); + if (it != end(u)) + { + updateCurrent(*it); + return true; + } + } + else if (auto i = get(*currentType)) + { + auto it = begin(i); + std::advance(it, index.index); + if (it != end(i)) + { + updateCurrent(*it); + return true; + } + } + } + else + { + auto currentPack = get(current); + LUAU_ASSERT(currentPack); + if (get(*currentPack)) + { + auto it = begin(*currentPack); + + for (size_t i = 0; i < index.index && it != end(*currentPack); ++i) + ++it; + + if (it != end(*currentPack)) + { + updateCurrent(*it); + return true; + } + } + } + + return false; + } + + bool traverse(TypePath::TypeField field) + { + if (checkInvariants()) + return false; + + switch (field) + { + case TypePath::TypeField::Metatable: + if (auto currentType = get(current)) + { + if (std::optional mt = getMetatable(*currentType, builtinTypes)) + { + updateCurrent(*mt); + return true; + } + } + + return false; + case TypePath::TypeField::LowerBound: + case TypePath::TypeField::UpperBound: + if (auto ft = get(current)) + { + updateCurrent(field == TypePath::TypeField::LowerBound ? ft->lowerBound : ft->upperBound); + return true; + } + + return false; + case TypePath::TypeField::IndexLookup: + case TypePath::TypeField::IndexResult: + { + const TableIndexer* indexer = nullptr; + + if (auto tt = get(current); tt && tt->indexer) + indexer = &(*tt->indexer); + // Note: we don't appear to walk the class hierarchy for indexers + else if (auto ct = get(current); ct && ct->indexer) + indexer = &(*ct->indexer); + + if (indexer) + { + updateCurrent(field == TypePath::TypeField::IndexLookup ? indexer->indexType : indexer->indexResultType); + return true; + } + + return false; + } + case TypePath::TypeField::Negated: + if (auto nt = get(current)) + { + updateCurrent(nt->ty); + return true; + } + + return false; + case TypePath::TypeField::Variadic: + if (auto vtp = get(current)) + { + updateCurrent(vtp->ty); + return true; + } + + return false; + } + + return false; + } + + bool traverse(TypePath::PackField field) + { + if (checkInvariants()) + return false; + + switch (field) + { + case TypePath::PackField::Arguments: + case TypePath::PackField::Returns: + if (auto ft = get(current)) + { + updateCurrent(field == TypePath::PackField::Arguments ? ft->argTypes : ft->retTypes); + return true; + } + + return false; + case TypePath::PackField::Tail: + if (auto currentPack = get(current)) + { + auto it = begin(*currentPack); + while (it != end(*currentPack)) + ++it; + + if (auto tail = it.tail()) + { + updateCurrent(*tail); + return true; + } + } + + return false; + } + + return false; + } +}; + +} // namespace + +std::string toString(const TypePath::Path& path) +{ + std::stringstream result; + bool first = true; + + auto strComponent = [&](auto&& c) { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + result << '['; + if (FFlag::DebugLuauReadWriteProperties) + { + if (c.isRead) + result << "read "; + else + result << "write "; + } + + result << '"' << c.name << '"' << ']'; + } + else if constexpr (std::is_same_v) + { + result << '[' << std::to_string(c.index) << ']'; + } + else if constexpr (std::is_same_v) + { + if (!first) + result << '.'; + + switch (c) + { + case TypePath::TypeField::Metatable: + result << "metatable"; + break; + case TypePath::TypeField::LowerBound: + result << "lowerBound"; + break; + case TypePath::TypeField::UpperBound: + result << "upperBound"; + break; + case TypePath::TypeField::IndexLookup: + result << "indexer"; + break; + case TypePath::TypeField::IndexResult: + result << "indexResult"; + break; + case TypePath::TypeField::Negated: + result << "negated"; + break; + case TypePath::TypeField::Variadic: + result << "variadic"; + break; + } + + result << "()"; + } + else if constexpr (std::is_same_v) + { + if (!first) + result << '.'; + + switch (c) + { + case TypePath::PackField::Arguments: + result << "arguments"; + break; + case TypePath::PackField::Returns: + result << "returns"; + break; + case TypePath::PackField::Tail: + result << "tail"; + break; + } + + result << "()"; + } + else + { + static_assert(always_false_v, "Unhandled Component variant"); + } + + first = false; + }; + + for (const TypePath::Component& component : path.components) + Luau::visit(strComponent, component); + + return result.str(); +} + +static bool traverse(TraversalState& state, const Path& path) +{ + auto step = [&state](auto&& c) { + return state.traverse(c); + }; + + for (const TypePath::Component& component : path.components) + { + bool stepSuccess = visit(step, component); + if (!stepSuccess) + return false; + } + + return true; +} + +std::optional traverse(TypeId root, const Path& path, NotNull builtinTypes) +{ + TraversalState state(follow(root), builtinTypes); + if (traverse(state, path)) + return state.current; + else + return std::nullopt; +} + +std::optional traverse(TypePackId root, const Path& path, NotNull builtinTypes); + +std::optional traverseForType(TypeId root, const Path& path, NotNull builtinTypes) +{ + TraversalState state(follow(root), builtinTypes); + if (traverse(state, path)) + { + auto ty = get(state.current); + return ty ? std::make_optional(*ty) : std::nullopt; + } + else + return std::nullopt; +} + +std::optional traverseForType(TypePackId root, const Path& path, NotNull builtinTypes) +{ + TraversalState state(follow(root), builtinTypes); + if (traverse(state, path)) + { + auto ty = get(state.current); + return ty ? std::make_optional(*ty) : std::nullopt; + } + else + return std::nullopt; +} + +std::optional traverseForPack(TypeId root, const Path& path, NotNull builtinTypes) +{ + TraversalState state(follow(root), builtinTypes); + if (traverse(state, path)) + { + auto ty = get(state.current); + return ty ? std::make_optional(*ty) : std::nullopt; + } + else + return std::nullopt; +} + +std::optional traverseForPack(TypePackId root, const Path& path, NotNull builtinTypes) +{ + TraversalState state(follow(root), builtinTypes); + if (traverse(state, path)) + { + auto ty = get(state.current); + return ty ? std::make_optional(*ty) : std::nullopt; + } + else + return std::nullopt; +} + +} // namespace Luau diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index 0b878180c..f746f6209 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypeUtils.h" +#include "Luau/Common.h" #include "Luau/Normalize.h" #include "Luau/Scope.h" #include "Luau/ToString.h" diff --git a/CLI/Compile.cpp b/CLI/Compile.cpp index 2059deaff..4f6b54b65 100644 --- a/CLI/Compile.cpp +++ b/CLI/Compile.cpp @@ -33,6 +33,13 @@ enum class CompileFormat Null }; +enum class RecordStats +{ + None, + Total, + Split +}; + struct GlobalOptions { int optimizationLevel = 1; @@ -122,6 +129,57 @@ struct CompileStats double codegenTime; Luau::CodeGen::LoweringStats lowerStats; + + void serializeToJson(FILE* fp) + { + // use compact one-line formatting to reduce file length + fprintf(fp, "{\ +\"lines\": %zu, \ +\"bytecode\": %zu, \ +\"codegen\": %zu, \ +\"readTime\": %f, \ +\"miscTime\": %f, \ +\"parseTime\": %f, \ +\"compileTime\": %f, \ +\"codegenTime\": %f, \ +\"lowerStats\": {\ +\"totalFunctions\": %u, \ +\"skippedFunctions\": %u, \ +\"spillsToSlot\": %d, \ +\"spillsToRestore\": %d, \ +\"maxSpillSlotsUsed\": %u, \ +\"blocksPreOpt\": %u, \ +\"blocksPostOpt\": %u, \ +\"maxBlockInstructions\": %u, \ +\"regAllocErrors\": %d, \ +\"loweringErrors\": %d\ +}}", + lines, bytecode, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions, lowerStats.skippedFunctions, + lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt, lowerStats.blocksPostOpt, + lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors); + } + + CompileStats& operator+=(const CompileStats& that) + { + this->lines += that.lines; + this->bytecode += that.bytecode; + this->codegen += that.codegen; + this->readTime += that.readTime; + this->miscTime += that.miscTime; + this->parseTime += that.parseTime; + this->compileTime += that.compileTime; + this->codegenTime += that.codegenTime; + this->lowerStats += that.lowerStats; + + return *this; + } + + CompileStats operator+(const CompileStats& other) const + { + CompileStats result(*this); + result += other; + return result; + } }; static double recordDeltaTime(double& timer) @@ -254,6 +312,7 @@ static void displayHelp(const char* argv0) printf(" -g: compile with debug level n (default 1, n should be between 0 and 2).\n"); printf(" --target=: compile code for specific architecture (a64, x64, a64_nf, x64_ms).\n"); printf(" --timetrace: record compiler time tracing information into trace.json\n"); + printf(" --record-stats=