diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed971199..3a1c7b75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -118,12 +118,12 @@ jobs: shell: bash # necessary for fail-fast run: | RelWithDebInfo/Luau.Conformance.exe -O2 -# ServerLua: Some of these disabled due to extremely long runtime -# RelWithDebInfo/Luau.Conformance.exe -O2 --fflags=true -# RelWithDebInfo/Luau.Conformance.exe --codegen + # ServerLua: Some of these disabled due to extremely long runtime + # RelWithDebInfo/Luau.Conformance.exe -O2 --fflags=true + # RelWithDebInfo/Luau.Conformance.exe --codegen RelWithDebInfo/Luau.Conformance.exe --codegen --fflags=true -# RelWithDebInfo/Luau.Conformance.exe --codegen -O2 -# RelWithDebInfo/Luau.Conformance.exe --codegen -O2 --fflags=true + # RelWithDebInfo/Luau.Conformance.exe --codegen -O2 + # RelWithDebInfo/Luau.Conformance.exe --codegen -O2 --fflags=true - name: cmake cli shell: bash # necessary for fail-fast run: | diff --git a/ARES.bt b/ARES.bt index b6918be3..b5b6dc4a 100644 --- a/ARES.bt +++ b/ARES.bt @@ -33,63 +33,47 @@ const uchar UTAG_LLTIMERS = 30; const uchar UTAG_STRBUF = 31; const uchar UTAG_OPAQUE_BUFFER = 32; -#define LUA_TNONE (-1) - -#define LUA_TNIL 0 -#define LUA_TBOOLEAN 1 -#define LUA_TLIGHTUSERDATA 2 -#define LUA_TNUMBER 3 -#define LUA_TVECTOR 4 -#define LUA_TSTRING 5 -#define LUA_TTABLE 6 -#define LUA_TFUNCTION 7 -#define LUA_TUSERDATA 8 -#define LUA_TTHREAD 9 -#define LUA_TBUFFER 10 -#define LUA_NUMTAGS 11 - -#define LUA_TPROTO LUA_NUMTAGS -#define LUA_TUPVAL (LUA_NUMTAGS+1) -#define LUA_TDEADKEY (LUA_NUMTAGS+2) - -/* -** number of all possible tags (including LUA_TNONE but excluding DEADKEY) -*/ -#define LUA_TOTALTAGS (LUA_TUPVAL+1) - -#define ERIS_PERMANENT (LUA_TOTALTAGS + 1) -#define ERIS_REFERENCE (ERIS_PERMANENT + 1) - +/* Mirrors enum AresType in VM/src/ares.cpp. These are wire values and are + * assigned explicitly there - do not derive them from Luau's lua_Type. */ typedef enum { - ET_NIL = LUA_TNIL, - ET_BOOLEAN = LUA_TBOOLEAN, - ET_LIGHTUSERDATA = LUA_TLIGHTUSERDATA, - ET_NUMBER = LUA_TNUMBER, - ET_VECTOR = LUA_TVECTOR, - ET_STRING = LUA_TSTRING, - ET_TABLE = LUA_TTABLE, - ET_FUNCTION = LUA_TFUNCTION, - ET_USERDATA = LUA_TUSERDATA, - ET_THREAD = LUA_TTHREAD, - ET_BUFFER = LUA_TBUFFER, - ET_PROTO = LUA_TPROTO, - ET_UPVAL = LUA_TUPVAL, - ET_TDEADKEY = LUA_TDEADKEY, - ET_PERMANENT = ERIS_PERMANENT, - ET_REFERENCE = ERIS_REFERENCE, -} ErisType; - -// thread status; 0 is OK + ARES_T_NIL = 0, + ARES_T_BOOLEAN = 1, + ARES_T_LIGHTUSERDATA = 2, + ARES_T_NUMBER = 3, + ARES_T_INTEGER = 4, + ARES_T_VECTOR = 5, + + ARES_T_STRING = 16, + ARES_T_TABLE = 17, + ARES_T_FUNCTION = 18, + ARES_T_USERDATA = 19, + ARES_T_THREAD = 20, + ARES_T_BUFFER = 21, + ARES_T_CLASS = 22, + ARES_T_OBJECT = 23, + ARES_T_VECTORD = 24, + + ARES_T_DEADKEY = 40, + ARES_T_PROTO = 41, + ARES_T_UPVAL = 42, + + ARES_T_PERMANENT = 64, + ARES_T_REFERENCE = 65, +} AresType; + +/* Mirrors enum AresStatus in VM/src/ares.cpp. Like the type tags these are + * wire values, not Luau's lua_Status. */ typedef enum { - LUA_OK = 0, - LUA_YIELD, - LUA_ERRRUN, - LUA_ERRSYNTAX, - LUA_ERRMEM, - LUA_ERRERR, - LUA_BREAK, // yielded for a debug breakpoint -} lua_Status; + ARES_S_OK = 0, + ARES_S_YIELD = 1, + ARES_S_ERRRUN = 2, + ARES_S_ERRSYNTAX = 3, + ARES_S_ERRMEM = 4, + ARES_S_ERRERR = 5, + ARES_S_BREAK = 6, // yielded for a debug breakpoint + ARES_S_ERRKILL = 7, // uncatchable script termination error +} AresStatus; typedef enum eris_CIKind { ERIS_CI_KIND_NONE = 0, @@ -104,15 +88,6 @@ typedef enum eris_CIKind { #define LUA_CALLINFO_HANDLE (1 << 1) // should the error thrown during execution get handled by continuation from this callinfo? func must be C #define LUA_CALLINFO_NATIVE (1 << 2) // should this function be executed using execution callback for native code -/* thread status */ -#define LUA_OK 0 -#define LUA_YIELD 1 -#define LUA_ERRRUN 2 -#define LUA_ERRSYNTAX 3 -#define LUA_ERRMEM 4 -#define LUA_ERRGCMM 5 -#define LUA_ERRERR 6 - struct Header; struct Object; struct String; @@ -529,6 +504,9 @@ typedef struct { char sig[4] ; /* Header signature for rudimentary validation */ Assert(sig == "ARES"); uint32_t version; + /* Version 4 moved the type tags off of lua_Type and onto AresType, so + * older blobs cannot be parsed with this template. */ + Assert(version >= 4); uint8_t sizeof_number; /* sizeof(lua_Number) to check type compatibility */ lua_Number test; /* -1.234567890 to check representation compatibility */ uint8_t sizeof_int; /* sizeof(int) in persisted data */ @@ -542,11 +520,17 @@ typedef struct { // Define this up here so structs can reference the version Header header; /* The header used for basic validation. */ +// GC object header with memcat. +// Declarations leak into the calling struct. +void ParseGCHeader() { + uint8_t memcat ; +} + string ReadObject(Object &o) { // ref should be non-zero if present if (o.ourRef) { - // ET_REFERENCE refNums are references _to_ something - if (o.type == ET_REFERENCE) { + // ARES_T_REFERENCE refNums are references _to_ something + if (o.type == ARES_T_REFERENCE) { return Str("%s -> #%d @%d", EnumToString(o.type), o.ourRef, refPositions[o.ourRef]); } return Str("%s #%d", EnumToString(o.type), o.ourRef); @@ -558,58 +542,66 @@ typedef struct { // What the logical reference number of this object will be, note that this is // implicit and based on parse order of `Object`s! local int ourRef = 0; - ErisType type ; + AresType type ; switch(type) { // some kinds of things don't need reference to them stored, though // storing references would be a waste because they're so small. - case ET_NIL: - case ET_BOOLEAN: - case ET_LIGHTUSERDATA: - case ET_NUMBER: - case ET_VECTOR: + case ARES_T_NIL: + case ARES_T_BOOLEAN: + case ARES_T_LIGHTUSERDATA: + case ARES_T_NUMBER: + case ARES_T_INTEGER: + case ARES_T_VECTOR: // storing a reference to a reference? no. - case ET_REFERENCE: + case ARES_T_REFERENCE: // permanent never writes to the ref table, what would be the point? - case ET_PERMANENT: + case ARES_T_PERMANENT: break; default: { ourRef = ++refNum; // track where we saw this so we can show the address of what's - // being referenced in ET_REFERENCEs + // being referenced in ARES_T_REFERENCEs refPositions[refNum] = startof(this); } } switch(type) { - case ET_NIL: + case ARES_T_NIL: break; - case ET_BOOLEAN: + case ARES_T_BOOLEAN: int32_t val; break; - case ET_LIGHTUSERDATA: + case ARES_T_LIGHTUSERDATA: LightUserdata val; break; - case ET_NUMBER: + case ARES_T_NUMBER: Number val; break; - case ET_VECTOR: + case ARES_T_INTEGER: + int64_t val; break; + case ARES_T_VECTOR: float val[3]; break; - case ET_STRING: + /* Reserved but never emitted; p_vector refuses to write f64 vectors + * until they go through the GC object path. */ + case ARES_T_VECTORD: + ParseGCHeader(); + double val[3]; break; + case ARES_T_STRING: String val; break; - case ET_TABLE: + case ARES_T_TABLE: Table val; break; - case ET_FUNCTION: + case ARES_T_FUNCTION: Closure val; break; - case ET_USERDATA: + case ARES_T_USERDATA: Userdata val; break; - case ET_THREAD: + case ARES_T_THREAD: Thread val; break; - case ET_BUFFER: + case ARES_T_BUFFER: Buffer val; break; - case ET_PROTO: + case ARES_T_PROTO: Proto val; break; - case ET_UPVAL: + case ARES_T_UPVAL: UpVal val; break; - case ET_PERMANENT: + case ARES_T_PERMANENT: PermKey val; break; - case ET_REFERENCE: + case ARES_T_REFERENCE: /* If the object is not primitive (see list above) we remember it and * increment the reference counter, and point any future occurrences of * it to this one via a reference (see above, Reference r). */ @@ -624,14 +616,6 @@ typedef struct { } } Object; -// GC object header with memcat (version >= 2). -// Declarations leak into the calling struct. -void ParseGCHeader() { - if (header.version >= 2) { - uint8_t memcat ; - } -} - typedef struct { ParseGCHeader(); size_t length; /* The length of the string */ @@ -803,12 +787,10 @@ struct Thread { size_t top; /* top = L->top - L->stack; */ Object stack[top]; /* All stack values, bottom up */ - lua_Status status; /* current thread status (ok, yield) */ - // version >= 2 writes activememcat - if (header.version >= 2) { - uint8_t activememcat ; - } + AresStatus status; /* current thread status (ok, yield) */ + uint8_t activememcat ; size_t errfunc; /* NOT USED current error handling function (stack index) */ + Object namecall; /* The pending namecall string, nil for none */ int32_t num_cis; /* number of callinfo frames */ /* The CallInfo stack, starting with base_ci */ @@ -835,7 +817,7 @@ struct Thread { } } ci[num_cis] ; - if (status == LUA_YIELD) { + if (status == ARES_S_YIELD) { // size_t extra; /* value of thread->ci->extra, which is the original // * value of thread->ci->func */ } @@ -852,7 +834,7 @@ struct Thread { }; struct PermKey { - ErisType type; /* The actual LUA_TXXX of the original value. */ + AresType type; /* The actual tag of the original value. */ Object key; /* The value to use as a key when unpersisting. */ /* Note that we store the type of the original value (replaced by the * permanent table value used as a key when unpersisting) to ensure the diff --git a/Analysis/include/Luau/AstUtils.h b/Analysis/include/Luau/AstUtils.h index b914cedc..9ee17570 100644 --- a/Analysis/include/Luau/AstUtils.h +++ b/Analysis/include/Luau/AstUtils.h @@ -6,9 +6,22 @@ #include "Luau/NotNull.h" #include "Luau/TypeFwd.h" +#include +#include +#include + namespace Luau { +struct TypeGuard +{ + bool isTypeof; + AstExpr* target; + std::string type; +}; + +std::optional matchTypeGuard(AstExprBinary::Op op, AstExpr* left, AstExpr* right); + // Search through the expression 'expr' for typeArguments that are known to represent // uniquely held references. Append these typeArguments to 'uniqueTypes'. void findUniqueTypes(NotNull> uniqueTypes, AstExpr* expr, NotNull> astTypes); diff --git a/Analysis/include/Luau/BuiltinDefinitions.h b/Analysis/include/Luau/BuiltinDefinitions.h index f19e8a2e..ec1103bf 100644 --- a/Analysis/include/Luau/BuiltinDefinitions.h +++ b/Analysis/include/Luau/BuiltinDefinitions.h @@ -17,6 +17,17 @@ struct TypeChecker; struct TypeArena; struct Subtyping; +struct MagicRequire final : MagicFunction +{ + std::optional> handleOldSolver( + struct TypeChecker&, + const std::shared_ptr&, + const class AstExprCall&, + WithPredicate + ) override; + bool infer(const MagicFunctionCallContext& context) override; +}; + void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeCheckForAutocomplete = false); TypeId makeUnion(TypeArena& arena, std::vector&& types); TypeId makeIntersection(TypeArena& arena, std::vector&& types); @@ -65,7 +76,7 @@ TypeId makeFunction( // Polymorphic bool checked = false ); -void attachMagicFunction(TypeId ty, std::shared_ptr fn); +void attachMagicFunction(TypeId ty, std::shared_ptr magic); Property makeProperty(TypeId ty, std::optional documentationSymbol = std::nullopt); void assignPropDocumentationSymbols(TableType::Props& props, const std::string& baseName); diff --git a/Analysis/include/Luau/BuiltinTypeFunctions.h b/Analysis/include/Luau/BuiltinTypeFunctions.h index 02b7d817..d871b1f5 100644 --- a/Analysis/include/Luau/BuiltinTypeFunctions.h +++ b/Analysis/include/Luau/BuiltinTypeFunctions.h @@ -47,6 +47,8 @@ struct BuiltinTypeFunctions TypeFunction setmetatableFunc; TypeFunction getmetatableFunc; + TypeFunction objectofFunc; + TypeFunction weakoptionalFunc; void addToScope(NotNull arena, NotNull scope) const; diff --git a/Analysis/include/Luau/Clone.h b/Analysis/include/Luau/Clone.h index 53f0df10..4783876e 100644 --- a/Analysis/include/Luau/Clone.h +++ b/Analysis/include/Luau/Clone.h @@ -35,7 +35,7 @@ TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool clonePersistentTypes); TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState); -TypeId clone(TypeId tp, TypeArena& dest, CloneState& cloneState); +TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState); TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState); Binding clone(const Binding& binding, TypeArena& dest, CloneState& cloneState); diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index 3c4803fc..14f8631b 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -7,6 +7,7 @@ #include "Luau/Variant.h" #include "Luau/TypeFwd.h" #include "Luau/TypeIds.h" +#include "Luau/VisitType.h" #include #include @@ -50,9 +51,9 @@ struct GeneralizationConstraint TypeId generalizedType; TypeId sourceType; - std::vector interiorTypes; - bool hasDeprecatedAttribute = false; - AstAttr::DeprecatedInfo deprecatedInfo; + /// Potentially null pointer to a deprecated attribute. Used to attach + /// deprecation info to the generalized function type. + AstAttr* maybeDeprecatedAttr; /// If true, never introduce generics. Always replace free types by their /// bounds or unknown. Presently used only to generalize the whole module. @@ -102,6 +103,8 @@ struct FunctionCallConstraint std::vector typeArguments; std::vector typePackArguments; + DenseHashMap* astTypes = nullptr; + // When we dispatch this constraint, we update the key at this map to record // the overload that we selected. DenseHashMap* astOverloadResolvedTypes = nullptr; @@ -136,7 +139,9 @@ struct FunctionCheckConstraint // then FreeType is replaced by its lower bound // // else FreeType is replaced by PrimitiveType -struct PrimitiveTypeConstraint +// +// Clip with LuauRemovePrimitiveTypeConstraint +struct DEPRECATED_PrimitiveTypeConstraint { TypeId freeType; @@ -323,7 +328,7 @@ using ConstraintV = Variant< TypeAliasExpansionConstraint, FunctionCallConstraint, FunctionCheckConstraint, - PrimitiveTypeConstraint, + DEPRECATED_PrimitiveTypeConstraint, HasPropConstraint, HasIndexerConstraint, AssignPropConstraint, @@ -340,6 +345,7 @@ using ConstraintV = Variant< struct Constraint { Constraint(NotNull scope, const Location& location, ConstraintV&& c); + Constraint(NotNull scope, const Location& location, ConstraintV&& c, std::shared_ptr moduleName); Constraint(const Constraint&) = delete; Constraint& operator=(const Constraint&) = delete; @@ -347,14 +353,18 @@ struct Constraint NotNull scope; Location location; ConstraintV c; + std::shared_ptr moduleName; - std::vector> dependencies; - - TypeIds getMaybeMutatedFreeTypes() const; + /** + * Return the types and type packs that may be mutated by this constraint. + * Currently we do not do anything with type packs. + */ + std::pair getMaybeMutatedTypes() const; }; using ConstraintPtr = std::unique_ptr; +bool isReferenceCountedType(TypePackId tp); bool isReferenceCountedType(const TypeId typ); inline Constraint& asMutable(const Constraint& c) @@ -374,4 +384,28 @@ const T* get(const Constraint& c) return getMutable(asMutable(c)); } +struct ReferenceCountInitializer : TypeOnceVisitor +{ + NotNull mutatedTypes; + TypePackIds* mutatedTypePacks; + bool traverseIntoTypeFunctions = true; + + explicit ReferenceCountInitializer(NotNull mutatedTypes, NotNull mutatedTypePacks); + + bool visit(TypeId ty, const FreeType&) override; + + bool visit(TypeId ty, const BlockedType&) override; + + bool visit(TypeId ty, const PendingExpansionType&) override; + + bool visit(TypeId ty, const TableType& tt) override; + + bool visit(TypeId ty, const ExternType&) override; + + bool visit(TypeId, const TypeFunctionInstanceType& tfit) override; + + bool visit(TypePackId tp, const BlockedTypePack&) override; + bool visit(TypePackId tp, const FreeTypePack&) override; +}; + } // namespace Luau diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index d0b83d5d..c3db7bcb 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -3,8 +3,10 @@ #include "Luau/Ast.h" #include "Luau/Constraint.h" +#include "Luau/ConstraintGraph.h" #include "Luau/ConstraintSet.h" #include "Luau/ControlFlow.h" +#include "Luau/ControlFlowGraph.h" #include "Luau/DataFlowGraph.h" #include "Luau/HashUtil.h" #include "Luau/InsertionOrderedMap.h" @@ -13,9 +15,11 @@ #include "Luau/NotNull.h" #include "Luau/Polarity.h" #include "Luau/Refinement.h" +#include "Luau/Set.h" #include "Luau/Symbol.h" #include "Luau/TypeFwd.h" #include "Luau/TypeIds.h" +#include "Luau/TypeStateMap.h" #include "Luau/TypeUtils.h" #include @@ -63,6 +67,12 @@ struct Checkpoint size_t offset = 0; }; +struct ClassDeclRecord +{ + TypeId ty = nullptr; + DenseHashMap memberTypes{AstName{""}}; +}; + struct ConstraintGenerator { // A list of all the scopes in the module. This vector holds ownership of the @@ -71,6 +81,7 @@ struct ConstraintGenerator std::vector> scopes; ModulePtr module; + std::shared_ptr sharedModuleName; NotNull builtinTypes; const NotNull arena; // The root scope of the module we're generating constraints for. @@ -94,6 +105,7 @@ struct ConstraintGenerator // See the functions recordInferredBinding and fillInInferredBindings. DenseHashMap inferredBindings{{}}; + // Remove constraints, freeTypes, and scopeToFunction with DebugLuauCyclicRequireTypeInference: these move to ConstraintGraph (cgraph). // Constraints that go straight to the solver. std::vector constraints; @@ -136,10 +148,15 @@ struct ConstraintGenerator DenseHashMap inferredExprCache{nullptr}; + DenseHashMap> classDeclRecords{nullptr}; + DcrLogger* logger; bool recursionLimitMet = false; + NotNull cgraph; + + CFG::TypeStateMap* typestate = nullptr; ConstraintGenerator( ModulePtr module, NotNull normalizer, @@ -152,7 +169,9 @@ struct ConstraintGenerator std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, - std::vector requireCycles + std::vector requireCycles, + NotNull cgraph, + CFG::TypeStateMap* typestate = nullptr ); ConstraintSet run(AstStatBlock* block); @@ -178,6 +197,8 @@ struct ConstraintGenerator std::vector unionsToSimplify; + Set uninitializedGlobals{{}}; + Polarity polarity = Polarity::None; DenseHashMap, TypeId, PairHash> propIndexPairsSeen{{nullptr, ""}}; @@ -225,6 +246,9 @@ struct ConstraintGenerator * @param cv the constraint variant to add. * @return the pointer to the inserted constraint */ + TypeId resolveRHSType(const ScopePtr& scope, Location location, AstExpr* expr); + TypeId resolveLHSType(const ScopePtr& scope, Location location, const CFG::LValue& lv); + NotNull addConstraint(const ScopePtr& scope, const Location& location, ConstraintV cv); /** @@ -264,7 +288,7 @@ struct ConstraintGenerator ); void applyRefinements(const ScopePtr& scope, Location location, RefinementId refinement); - LUAU_NOINLINE void checkAliases(const ScopePtr& scope, AstStatBlock* block); + LUAU_NOINLINE void prototypeTypeDefinitions(const ScopePtr& scope, AstStatBlock* block); ControlFlow visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block); @@ -284,8 +308,9 @@ struct ConstraintGenerator ControlFlow visit(const ScopePtr& scope, AstStatTypeAlias* alias); ControlFlow visit(const ScopePtr& scope, AstStatTypeFunction* function); ControlFlow visit(const ScopePtr& scope, AstStatDeclareGlobal* declareGlobal); - ControlFlow visit(const ScopePtr& scope, AstStatDeclareExternType* declareExternType); - ControlFlow visit(const ScopePtr& scope, AstStatDeclareFunction* declareFunction); + ControlFlow visit(const ScopePtr& scope, AstStatDeclareExternType* declaredExternType); + ControlFlow visit(const ScopePtr& scope, AstStatDeclareFunction* global); + ControlFlow visit(const ScopePtr& scope, AstStatClass* statClass); ControlFlow visit(const ScopePtr& scope, AstStatError* error); InferencePack checkPack(const ScopePtr& scope, AstArray exprs, const std::vector>& expectedTypes = {}); @@ -356,7 +381,7 @@ struct ConstraintGenerator void visitLValue(const ScopePtr& scope, AstExpr* expr, TypeId rhsType); void visitLValue(const ScopePtr& scope, AstExprLocal* local, TypeId rhsType); void visitLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId rhsType); - void visitLValue(const ScopePtr& scope, AstExprIndexName* indexName, TypeId rhsType); + void visitLValue(const ScopePtr& scope, AstExprIndexName* expr, TypeId rhsType); void visitLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId rhsType); struct FunctionSignature @@ -374,6 +399,7 @@ struct ConstraintGenerator FunctionSignature checkFunctionSignature( const ScopePtr& parent, + ClassDeclRecord* enclosingClass, AstExprFunction* fn, std::optional expectedType = {}, std::optional originalName = {} @@ -446,6 +472,8 @@ struct ConstraintGenerator Polarity initialPolarity = Polarity::Positive ); + TypePackId resolveTypePack_(const ScopePtr& scope, const AstTypeList& list, bool inTypeArguments, bool replaceErrorWithFresh); + /** * Creates generic types given a list of AST definitions, resolving default * types as required. diff --git a/Analysis/include/Luau/ConstraintGraph.h b/Analysis/include/Luau/ConstraintGraph.h new file mode 100644 index 00000000..277293de --- /dev/null +++ b/Analysis/include/Luau/ConstraintGraph.h @@ -0,0 +1,272 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#pragma once + +#include "Luau/Constraint.h" +#include "Luau/Set.h" +#include "Luau/ToString.h" +#include "Luau/Type.h" + +/** + * It is said that there are only so many unique problems in computer science. + * + * We have stumbled upon one of them: build systems. + */ + +namespace Luau +{ + +using BlockedConstraintId = Variant; + +struct HashBlockedConstraintId +{ + size_t operator()(const BlockedConstraintId& bci) const; +}; + + +using ConstraintVertex = BlockedConstraintId; + +/** + * Helper data structure for `ConstraintGraph`: an ordered, mutable set. + */ +struct ConstraintList +{ + bool contains(ConstraintVertex vertex) const; + void insert(ConstraintVertex vertex); + void remove(ConstraintVertex vertex); + void clear(); + size_t size() const; + + struct Iterator + { + using value_type = const ConstraintVertex; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::input_iterator_tag; + + explicit Iterator(NotNull cl, size_t index); + + Iterator& operator++(); + + bool operator==(const Iterator& rhs) const; + bool operator!=(const Iterator& rhs) const; + + value_type operator*(); + + private: + NotNull cl; + size_t index; + + void advanceUntilPresentOrEnd(); + }; + + Iterator begin(); + Iterator end(); + +private: + DenseHashMap present{(TypeId) nullptr}; + std::vector order; + size_t entries = 0; +}; + + +/** + * Represents an (aspirationally) acyclic graph of constraints, types, and + * type packs. Edges in the graph represent dependencies. For example, a + * GeneralizationConstraint depends on all of the constraints of the + * function it is generalizing, and the blocked type representing the + * generalized function depends on said GeneralizationConstraint. + * + * We need to consider six (P(3, 2)) scenarios, though many overlap: + * - If constraint A depends on constraint B, we must dispatch B before A. + * - If constraint A depends on a type or type pack, we must wait for those + * types to be "unblocked." This could be via generalization, a blocked + * type being replaced by another type, or a type function being solved. + * - If a type or type pack depends on a constraint, then at present it must + * be a free type. Blocked types (and similar types such as type functions + * and pending expansion types) are not represented in the graph in this way. + * - Types should never depend on other types or type packs. + * + * For now, ConstraintGraph is not responsible for determining which constraint + * to dispatch next. + */ +struct ConstraintGraph +{ + using ConstraintMap = DenseHashMap; + + ConstraintGraph(NotNull builtinTypes); + + // Constraint data co-located with the dependency edges that reference them. + // In the SCC path, multiple ConstraintGenerators accumulate directly into these fields. + // Constraints that go straight to the solver. + std::vector constraints; + + // The set of all free types introduced during constraint generation. + TypeIds freeTypes; + + // Map a function's signature scope back to its signature type. + DenseHashMap scopeToFunction{nullptr}; + + /** + * Add [dependency] as a blocker for [target] + * + * Returns whether this is a fresh relationship (were we already tracking + * it). + */ + bool addDependencyOf(ConstraintVertex dependency, ConstraintVertex target); + + /** + * Semantically the same as [block(ConstraintVertex, ConstraintVertex)], + * this is a helper overload for ConstraintGenerator. + */ + bool addDependencyOf(Constraint* dependency, Constraint* target); + + /** + * Take all of the reverse dependencies of [existingVertex] and *also* + * make them reverse dependencies of [newVertex]. + */ + void inheritBlocks(ConstraintVertex existingVertex, ConstraintVertex newVertex); + + struct UnblockedTypes + { + TypeIds types; + TypePackIds packs; + }; + + /** + * Unblock constraint [c]: + * 1. Iterate over the reverse dependencies of [c] and remove [c] from their dep list. + * 2. Collect all of the reverse dependencies of [c] that are types or type packs. + * 3. Repair any bound types to ensure the constraint graph remains accurate. + * 4. Return unblocked types and type packs. + */ + UnblockedTypes unblockConstraint(NotNull c); + + /** + * Unblock type [vertex]. + * 1. If [vertex] is now a bound type, walk the chain of bound types and + * repair references to said type in the graph (see: `repairTypeReferneces`). + * 2. After references have been repaired, walk the reverse dependencies of + * [vertex] and remove [vertex] from each dependency list, and then clear + * the reverse dependency list of [vertex]. + */ + void unblockTypeOrPack(TypeId vertex); + + /** + * Unblock type *pack* [vertex]. + * 1. If [vertex] is now a bound type, walk the chain of bound types and + * repair references to said type in the graph (see: `repairTypeReferneces`). + * 2. After references have been repaired, walk the reverse dependencies of + * [vertex] and remove [vertex] from each dependency list, and then clear + * the reverse dependency list of [vertex]. + */ + void unblockTypeOrPack(TypePackId vertex); + + /** + * Return whether the vertex has any unsolved dependencies. + * + * HACK: For `PrimitiveTypeConstraint` we consider it unblocked if there is + * a single dependency. + */ + bool hasUnsolvedDependencies(ConstraintVertex vertex); + + /** + * HACK: Used for `PrimitiveTypeConstraint` to check whether the free type + * it "controls" has other outstanding dependencies. + */ + bool DEPRECATED_hasStrictlyMoreThanOneDependency(ConstraintVertex vertex); + + /** + * Find all of the reference counted types that are reachable from `target` + * and shift the dependencies (and reverse dependencies) of source over + * without rebinding source to target. + */ + template + void copyDependenciesOf(T source, T target); + + /** + * NOTE: You probably do not want to call this function directly. + * + * This attempts to find all the reachable mutable types from [target] and + * shift all references from the type [source] to [target]. You probably + * intend to use [copyDependenciesOf], the non-destructive version. + */ + template + void shiftReferences(T source, T target); + + [[maybe_unused]] + void dumpWith(const std::vector>& unsolvedConstraints, ToStringOptions& opts); + + [[maybe_unused]] + void dumpBlocked(NotNull c, ToStringOptions& opts); + +private: + NotNull builtinTypes; + + /** + * We need to handle arbitrary cases of types being rebound in the type + * graph. + * + * If [ty] is not bound, exit immediately. Otherwise, traverse the bound + * type chain from [ty] to its root and, for each type in the chain [ty'] + * that is not the root, shift the references *to* the root type. + */ + template + void repairTypeReferences(T ty); + + /** + * For all types and type packs [t] in the params [mutatedTypes] and [mutatedTypePacks], + * and vertex [v] in [originalSource], + * 1. Add [v] as a dependency of [t] + * 2. Add [t] to the reverse deps of [v] + * 3. If we have provided an [originalSource], remove it from the + * reverse dependencies of [v]. + * + * We use this function to either constructively or destructively copy + * references from one type to another as part of [repairTypeReferences] + * and [shiftReferences]. + */ + void copyDependenciesToReachableTypes( + std::optional originalVertex, + NotNull source, + TypeIds mutatedTypes, + TypePackIds mutatedTypePacks + ); + + /** + * For all the reverse dependencies of [vertex], remove [vertex] from their + * dependency list. Finally, remove the [vertex] entry from `reverseDependencies`. + */ + void clearReverseDependenciesOf(ConstraintVertex vertex); + + /** + * Mapping from vertices to their dependencies. A missing entry or an entry + * pointing to the empty set indicates no dependencies: + * - Any free type with no dependencies can be generalized; + * - Any free type pack with no dependencies can be generalized; + * - Any constraint with no dependencies can be dispatched. + */ + ConstraintMap dependencies{(TypeId) nullptr}; + + + NotNull findDependencyList(ConstraintVertex vertex); + + /** + * Inverse of the above mapping. Yes, the proper name for this is + * "dependents," but naming it such will result in hellish typos. + */ + ConstraintMap reverseDependencies{(TypeId) nullptr}; + NotNull findReverseDependencyList(ConstraintVertex vertex); + + /** + * We do the same pseudo-arena trick as constraints do right now. + */ + std::vector> constraintLists; + + [[maybe_unused]] + void dump(); +}; + +std::string dump(ConstraintVertex vertex); + +} // namespace Luau diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index 023e4dcb..5370079b 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -3,6 +3,7 @@ #pragma once #include "Luau/Constraint.h" +#include "Luau/ConstraintGraph.h" #include "Luau/ConstraintSet.h" #include "Luau/DataFlowGraph.h" #include "Luau/DenseHash.h" @@ -10,15 +11,14 @@ #include "Luau/Location.h" #include "Luau/Module.h" #include "Luau/Normalize.h" -#include "Luau/OrderedSet.h" #include "Luau/Substitution.h" +#include "Luau/Subtyping.h" #include "Luau/SubtypingVariance.h" #include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeCheckLimits.h" #include "Luau/TypeFunction.h" #include "Luau/TypeFwd.h" -#include "Luau/Variant.h" #include #include @@ -32,15 +32,6 @@ struct DcrLogger; class AstExpr; -// TypeId, TypePackId, or Constraint*. It is impossible to know which, but we -// never dereference this pointer. -using BlockedConstraintId = Variant; - -struct HashBlockedConstraintId -{ - size_t operator()(const BlockedConstraintId& bci) const; -}; - struct SubtypeConstraintRecord { TypeId subTy = nullptr; @@ -106,7 +97,10 @@ struct ConstraintSolver std::vector> constraints; NotNull> scopeToFunction; NotNull rootScope; - ModulePtr module; + ModulePtr module; // Clip with DebugLuauCyclicRequireTypeInference + // Used for solver-scoped errors not attributable to a specific constraint + // (e.g. ConstraintSolvingIncompleteError, time limits). + std::shared_ptr representativeModuleName; // The dataflow graph of the program, used in constraint generation and for magic functions. NotNull dfg; @@ -124,12 +118,6 @@ struct ConstraintSolver // A constraint can be both blocked and unsolved, for instance. std::vector> unsolvedConstraints; - // A mapping of constraint pointer to how many things the constraint is - // blocked on. Can be empty or 0 for constraints that are not blocked on - // anything. - std::unordered_map, size_t> blockedConstraints; - // A mapping of type/pack pointers to the constraints they block. - std::unordered_map, HashBlockedConstraintId> blocked; // Memoized instantiations of type aliases. DenseHashMap instantiatedAliases{{}}; // Breadcrumbs for where a free type's upper bound was expanded. We use @@ -137,12 +125,6 @@ struct ConstraintSolver // as never unexpectedly. DenseHashMap>> upperBoundContributors{nullptr}; - // A mapping from free types to the number of unresolved constraints that mention them. - DenseHashMap unresolvedConstraints{{}}; - - std::unordered_map, TypeIds> maybeMutatedFreeTypes; - std::unordered_map> mutatedFreeTypeToConstraint; - // Irreducible/uninhabited type functions or type pack functions. DenseHashSet uninhabitedTypeFunctions{{}}; @@ -172,7 +154,9 @@ struct ConstraintSolver DcrLogger* logger, NotNull dfg, TypeCheckLimits limits, - ConstraintSet constraintSet + ConstraintSet constraintSet, + NotNull cgraph, + NotNull subtyping ); // TODO CLI-169086: Replace all uses of this constructor with the ConstraintSet constructor, above. @@ -187,7 +171,9 @@ struct ConstraintSolver std::vector requireCycles, DcrLogger* logger, NotNull dfg, - TypeCheckLimits limits + TypeCheckLimits limits, + NotNull cgraph, + NotNull subtyping ); // Randomize the order in which to dispatch constraints @@ -214,11 +200,13 @@ struct ConstraintSolver void generalizeOneType(TypeId ty); + // Clip with LuauRemoveConstraintSolverEmplace template - void emplace(NotNull constraint, TypeId ty, Args&&... args); + void DEPRECATED_emplace(NotNull constraint, TypeId ty, Args&&... args); + // Clip with LuauRemoveConstraintSolverEmplace template - void emplace(NotNull constraint, TypePackId tp, Args&&... args); + void DEPRECATED_emplace(NotNull constraint, TypePackId tp, Args&&... args); public: /** Attempt to dispatch a constraint. Returns true if it was successful. If @@ -235,7 +223,8 @@ struct ConstraintSolver bool tryDispatch(const TypeAliasExpansionConstraint& c, NotNull constraint); bool tryDispatch(const FunctionCallConstraint& c, NotNull constraint, bool force); bool tryDispatch(const FunctionCheckConstraint& c, NotNull constraint, bool force); - bool tryDispatch(const PrimitiveTypeConstraint& c, NotNull constraint); + // Clip with LuauRemovePrimitiveTypeConstraint + bool DEPRECATED_tryDispatch(const DEPRECATED_PrimitiveTypeConstraint& c, NotNull constraint); bool tryDispatch(const HasPropConstraint& c, NotNull constraint); bool tryDispatch(const TypeInstantiationConstraint& c, NotNull constraint); @@ -328,21 +317,8 @@ struct ConstraintSolver */ void inheritBlocks(NotNull source, NotNull addition); - // Traverse the type. If any pending types are found, block the constraint - // on them. - // - // Returns false if a type blocks the constraint. - // - // FIXME: This use of a boolean for the return result is an appalling - // interface. - bool blockOnPendingTypes(TypeId target, NotNull constraint); - bool blockOnPendingTypes(TypePackId targetPack, NotNull constraint); - - void unblock(NotNull progressed); void unblock(TypeId ty, Location location); void unblock(TypePackId progressed, Location location); - void unblock(const std::vector& types, Location location); - void unblock(const std::vector& packs, Location location); /** * @returns true if the TypeId is in a blocked state. @@ -354,16 +330,13 @@ struct ConstraintSolver */ bool isBlocked(TypePackId tp) const; - /** - * Returns whether the constraint is blocked on anything. - * @param constraint the constraint to check. - */ - bool isBlocked(NotNull constraint) const; - /** Pushes a new solver constraint to the solver. * @param cv the body of the constraint. **/ - NotNull pushConstraint(NotNull scope, const Location& location, ConstraintV cv); + NotNull pushConstraint(NotNull scope, const Location& location, ConstraintV cv, std::shared_ptr moduleName); + + // Clip with DebugLuauCyclicRequireTypeInference + NotNull DEPRECATED_pushConstraint(NotNull scope, const Location& location, ConstraintV cv); /** * Attempts to resolve a module from its module information. Returns the @@ -374,19 +347,15 @@ struct ConstraintSolver * @param location the location where the require is taking place; used for * error locations. **/ - TypeId resolveModule(const ModuleInfo& info, const Location& location); + TypeId resolveModule(const ModuleInfo& info, const Location& location, const ModuleName& moduleName); - void reportError(TypeErrorData&& data, const Location& location); - void reportError(TypeError e); + void reportError(TypeErrorData&& data, const Location& location, const ModuleName& errorModule); - /** - * Shifts the count of references from `source` to `target`. This should be paired - * with any instance of binding a free type in order to maintain accurate refcounts. - * If `target` is not a free type, this is a noop. - * @param source the free type which is being bound - * @param target the type which the free type is being bound to - */ - void shiftReferences(TypeId source, TypeId target); + // Clip with DebugLuauCyclicRequireTypeInference + TypeId DEPRECATED_resolveModule(const ModuleInfo& info, const Location& location); + void DEPRECATED_reportError(TypeErrorData&& data, const Location& location); + void DEPRECATED_reportError(TypeError e); + void DEPRECATED_reportError(TypeError e, const ModuleName& errorModule); /** * Bind a type variable to another type. @@ -400,15 +369,6 @@ struct ConstraintSolver void bind(NotNull constraint, TypeId ty, TypeId boundTo); void bind(NotNull constraint, TypePackId tp, TypePackId boundTo); - /** - * Generalizes the given free type if the reference counting allows it. - * @param the scope to generalize in - * @param type the free type we want to generalize - * @returns a non-free type that generalizes the argument, or `std::nullopt` if one - * does not exist - */ - std::optional generalizeFreeType(NotNull scope, TypeId type); - /** * Checks the existing set of constraints to see if there exist any that contain * the provided free type, indicating that it is not yet ready to be replaced by @@ -433,29 +393,15 @@ struct ConstraintSolver template bool unify(NotNull constraint, TID subTy, TID superTy); - /** - * Marks a constraint as being blocked on a type or type pack. The constraint - * solver will not attempt to dispatch blocked constraints until their - * dependencies have made progress. - * @param target the type or type pack pointer that the constraint is blocked on. - * @param constraint the constraint to block. - **/ - bool block_(BlockedConstraintId target, NotNull constraint); - - /** - * Informs the solver that progress has been made on a type or type pack. The - * solver will wake up all constraints that are blocked on the type or type pack, - * and will resume attempting to dispatch them. - * @param progressed the type or type pack pointer that has progressed. - **/ - void unblock_(BlockedConstraintId progressed); - /** * Reproduces any constraints necessary for new types that are copied when applying a substitution. * At the time of writing, this pertains only to type functions. * @param subst the substitution that was applied **/ - void reproduceConstraints(NotNull scope, const Location& location, const Substitution& subst); + void reproduceConstraints(NotNull scope, const Location& location, const Substitution& subst, const std::shared_ptr& moduleName); + + // Clip with DebugLuauCyclicRequireTypeInference + void DEPRECATED_reproduceConstraints(NotNull scope, const Location& location, const Substitution& subst); TypeId simplifyIntersection(NotNull scope, Location location, TypeId left, TypeId right); @@ -478,6 +424,10 @@ struct ConstraintSolver ToStringOptions opts; + NotNull cgraph; + + NotNull subtyping; + void fillInDiscriminantTypes(NotNull constraint, const std::vector>& discriminantTypes); }; diff --git a/Analysis/include/Luau/ControlFlow.md b/Analysis/include/Luau/ControlFlow.md new file mode 100644 index 00000000..deca7d89 --- /dev/null +++ b/Analysis/include/Luau/ControlFlow.md @@ -0,0 +1,74 @@ +This file just summarizes some design decisions I am making/made for the Control Flow Graph for other folks who will work on this + for myself. + +## High level goal +- Make refinements and type stating more reliable (hopefully will bring down the devforum bug report rate) +- Make type stating aware of back edges in control flow. +- Make the implementation of these in the new solver easier to reason about. +- Open up new avenues for analysis (e.g effect tracking, generalizing scc's of mutually recursive functions etc) + +## Solution +Introduce a Control Flow Graph construct for Luau to replace the current Data Flow Graph based on [this paper](https://bernsteinbear.com/assets/img/braun13cc.pdf) + +## Decisions made +This CFG is in SSA form, and allows you to answer questions like: 'which version of this variable am I referring to'. + +### Definition +A `Definition` is a pointer that describes a versioned access to a variable. It is represented as a (AstLocal*, size_t) pair. +In the following code: +``` +local x = 0 +local y = 0 +local z = 5 +x = y + x +y = x + z +``` +we could rewrite this in a versioned form like: +``` +local x_0 ... +local y_0 ... +local z_0 ... +x_1 = y_0 + x_0 +y_0 = x_1 + z+0 +``` + +The CFG maintains a mapping, so that in rhs positions, you can ask questions like what is the def for the `AstExprLocal` associated with `y` on line 4 of the original code. + +### Joins +A `Join` represent a use of a variable that depends on > 1 definition of a variable in a predecessor block. + +``` +local x = 0 +if true then + x = nil +end +use(x) +``` +might be represented as something like: +``` +local x_0 ... +if true then + x_1 = nil +end +x_2 = join(x_0, x_1) +use(x_2) +``` + +## Conditions +Conditions in control flow have a number of interesting properties. +1) Sequences of conditions generate refinements that are only visible in the scope of the refinement. +For example: `a and a.|{b |c } and a.b.|{c | d | e}` generates a sequence of refinements that are visible +to `a.` and `a.b`, but might not be visible outside. +2) Type refinements are a way of generating predicates on types in select scopes +3) Type refinements go out of scope! + + +## Globals +I've already tweaked the representation of a Def to store a Symbol, which is basically like AstLocal* | Global. + +## Indexing +We should augment the definition of a Definition to something like: +``` +DefPtr = Variant +where Definition = (Symbol, version) +and IndexedDefinition = (Definition, ) +``` diff --git a/Analysis/include/Luau/ControlFlowGraph.h b/Analysis/include/Luau/ControlFlowGraph.h new file mode 100644 index 00000000..031d65c6 --- /dev/null +++ b/Analysis/include/Luau/ControlFlowGraph.h @@ -0,0 +1,418 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/DenseHash.h" +#include "Luau/NotNull.h" +#include "Luau/Symbol.h" +#include "Luau/TypedAllocator.h" +#include "Luau/Variant.h" +#include "Luau/Set.h" + +#include +#include +#include +#include + +namespace Luau::CFG +{ + +// The control flow graph is a layer over the existing AST that models +// explicit control flow for a single file of luau code +// The CFG maintains an SSA mapping from a source AST to definition version + +struct Block; +struct SymDef; +struct CFGAllocator; +using Definition = SymDef; + +struct Declare; +struct Assign; +struct Join; +struct Refine; +struct Dead; +using Instruction = Variant; + +using BlockId = NotNull; +using DefId = NotNull; +using InstrId = NotNull; + +using LValue = Variant; + +struct LValueHash +{ + size_t operator()(const LValue& lvalue) const + { + size_t seed = std::hash()(lvalue.index()); + if (auto* sym = lvalue.get_if()) + hashCombine(seed, std::hash()(*sym)); + else if (auto* expr = lvalue.get_if()) + hashCombine(seed, DenseHashPointer()(*expr)); + return seed; + } +}; + +namespace CFGRefinement +{ + +struct Proposition; +struct Conjunction; +struct Disjunction; +struct Negation; + +using Refinement = Variant; +using RefinementId = NotNull; + +// Proposition about a def: +// - if type is not nil: sense = true => ptr == type, sense = false => ptr ~- type +// - `type` nullopt: `def` & truthy when sense, `def` & falsy when !sense +struct Proposition +{ + DefId ptr; + std::optional type; + // Distinguishes between type and typeof + bool isTypeof; + bool sense; +}; + +struct Conjunction +{ + RefinementId lhs; + RefinementId rhs; +}; + +struct Disjunction +{ + RefinementId lhs; + RefinementId rhs; +}; + +struct Negation +{ + RefinementId refinement; +}; + +template +const T* get(RefinementId r) +{ + return get_if(r.get()); +} + +struct RefinementArena +{ + RefinementId proposition(DefId def, bool sense); + RefinementId typeProposition(DefId def, std::optional type, bool isTypeof, bool sense); + + RefinementId conjunction(RefinementId lhs, RefinementId rhs); + RefinementId disjunction(RefinementId lhs, RefinementId rhs); + RefinementId negation(RefinementId r); + + void freeze(); + +private: + TypedAllocator allocator; +}; + +} // namespace CFGRefinement + +// A versioned identity for a local variable at a specific point in the program +struct SymDef +{ + // Use a Symbol because this can wrap both globals and locals + Symbol sym; + size_t version; + + explicit SymDef(Symbol sym, size_t version) + : sym(sym) + , version(version) + { + } + + std::string name() const + { + return sym.c_str(); + } + + std::string versionedName() const + { + return name() + "-" + std::to_string(version); + } + + bool operator==(const SymDef& other) const + { + return sym == other.sym && version == other.version; + } + + bool operator!=(const SymDef& other) const + { + return !(*this == other); + } +}; + +// Declarations turn into one declaration per line +// E.g. local x, y, z ... +// local x_i +// local y_j +// local z_k +// Each of these are defs +struct Declare +{ + Declare(DefId def, AstStatLocal* source) + : def(def) + , source(source) + { + } + + DefId def; + AstStatLocal* source; +}; + +// Assignments turn into one 're-def' per line +// E.g. x, y, z = .... +// x_i . +// y_j . +// z_k . +struct Assign +{ + Assign(DefId def, AstStatAssign* source) + : def(def) + , source(source) + { + } + + DefId def; + AstStatAssign* source; +}; + +struct Dead +{ +}; + +// phi nodes - When multiple control flow paths hit this point, this denotes that +// a definition is influenced by multiple distinct control flow paths. +struct Join +{ + explicit Join(DefId definition) + : definition(definition) + { + } + DefId definition; + std::vector operands; +}; + +// Refine instructions attach type refinements to def. The attached +// Proposition tells you how to interpret the def. The Proposition is owned by +// the RefinementArena (lives inside a Refinement variant slot). +struct Refine +{ + Refine(DefId definition, const CFGRefinement::Proposition& prop) + : definition(definition) + , toRefine(prop.ptr) + , type(prop.type) + , isTypeof(prop.isTypeof) + , sense(prop.sense) + { + } + + DefId definition; + DefId toRefine; + std::optional type; + bool isTypeof; + bool sense; +}; + +enum class BlockKind +{ + Entry, + Linear, + Condition, +}; + +struct Block +{ + explicit Block(BlockKind kind, std::string debugName); + + void addSuccessor(Block* target); + + bool containsDefinition(Symbol sym) const; + Definition* getReachingDefinition(Symbol sym) const; + void setReachingDefinition(Symbol sym, DefId def); + + + const std::vector& getInstructions() const; + const std::vector& getPredecessors() const; + const std::vector& getSuccessors() const; + + BlockKind kind; + std::string debugName; + +private: + std::vector instructions; + std::vector predecessors; + std::vector successors; + DenseHashMap reachingDefinitions{Symbol{}}; + + friend struct CFGBuilder; +}; + +struct CFGAllocator +{ + Block* newBlock(BlockKind kind, std::string debugName); + + template + InstrId newInstruction(Args&&... args) + { + LUAU_ASSERT(!frozen); + Instruction* inst = instructions.allocate(T{std::forward(args)...}); + return NotNull{inst}; + } + + DefId newDefinition(Symbol sym, size_t version); + CFGRefinement::RefinementArena refinementArena; + + void freeze(); + +private: + TypedAllocator block; + TypedAllocator instructions; + TypedAllocator defs; + bool frozen = false; +}; + +struct ControlFlowGraph +{ + explicit ControlFlowGraph(NotNull allocator) + : allocator(allocator) + { + } + + Definition* getUseDef(AstExpr* expr) const; + Definition* getLhsDef(const LValue& lv) const; + Definition* resolve(Definition* def) const; + + std::vector blocks; + size_t entryIdx = 0; + + const std::vector& rpo() const + { + return rpoOrder; + } + +private: + DenseHashMap useDefs{nullptr}; + DenseHashMap lhsDefs{LValue{}}; + DenseHashMap forwards{nullptr}; + + BlockId newBlock(BlockKind kind, std::string debugName = ""); + void computeRPO(); + + std::vector rpoOrder; + NotNull allocator; + friend struct CFGBuilder; +}; + +struct CFGBuilder +{ + static std::unique_ptr makeCFG(NotNull allocator, AstStatBlock* block); + +private: + explicit CFGBuilder(NotNull allocator); + + void lower(AstStat* statement); + void lower(AstStatBlock* statement); + void lower(AstStatLocal* local); + void lower(AstStatAssign* assn); + void lower(AstStatIf* statIf); + void lower(AstStatWhile* statWhile); + void lower(AstStatExpr* stat); + void lowerExpr(AstExpr* expression); + void lowerExpr(AstExprLocal* local); + void lowerExpr(AstExprCall* call); + + bool tryLowerAssertion(AstExprCall* call); + + // Returns a refinement tree describing the truthy interpretation of `condition`, + // or nullopt if no refinement can be extracted. Records use->def for any reads. + std::optional resolveCondition(AstExpr* condition); + + // Walks `refinement` and emits one Refine instruction per Proposition into `block`, + // each with a fresh def for the refined symbol. + void emitRefineInstruction(Block* block, CFGRefinement::RefinementId refinement); + + // Allocates a fresh unsealed block. If `pred` is non-null, wires `pred -> b`. + Block* newBlock(BlockKind kind, std::string debugName, Block* pred = nullptr); + + // Allocates an Instruction of type T and appends it to `block`. + template + InstrId emit(Block* block, Args&&... args) + { + InstrId inst = allocator->newInstruction(std::forward(args)...); + recordUses(inst); + block->instructions.emplace_back(inst); + return inst; + } + + std::pair> emitJoin(Block* block, Symbol sym); + + // Mints a fresh SymDef with a monotonically increasing version per symbol. + DefId newDefinition(Symbol sym); + + // Def lookup for `sym` at `block`. + // If the block is unsealed: emits an incomplete phi and returns that def, since the block might + // be given a predecessor that would causes us to gain a new phi operand + // If block is sealed + single predecessor: recurses into the pred. + // If block is sealed + multiple predecessors: emits a complete phi and fills operands. + DefId readVariable(BlockId block, Symbol sym); + + // Reads `sym` from each predecessor of `block` to populate `j->operands`, + // then attempts to trim if the join collapses to a single def. + DefId fillJoinOperands(Block* block, InstrId instr, Join* j); + + DefId trimTrivialJoin(InstrId inst, Join* j); + + // Registers which defs `inst` consumes into the usingInstructions map. + void recordUses(InstrId inst); + + // Returns the next version index for `sym`; first call returns 0. + size_t nextVersionIndex(Symbol sym); + + // True iff `seal` has been called on `b` (no more predecessors will be added). + bool isSealed(Block* b); + // Marks `b` as sealed and flushes any incomplete phis by filling their operands. + void seal(Block* b); + + // RAII guard: switches currentBlock on construction, restores on destruction. + // Read currentBlock before the scope ends to capture the exit block. + struct BlockScope + { + BlockScope(CFGBuilder& builder, Block* target) + : builder(builder) + , saved(builder.currentBlock.get()) + { + builder.currentBlock = NotNull{target}; + } + + ~BlockScope() + { + builder.currentBlock = NotNull{saved}; + } + + BlockScope(const BlockScope&) = delete; + BlockScope& operator=(const BlockScope&) = delete; + + CFGBuilder& builder; + Block* saved; + }; + + std::unique_ptr cfg; + NotNull allocator; + NotNull currentBlock; + DenseHashSet sealedBlocks{nullptr}; + DenseHashMap> incompleteJoins; + DenseHashMap versionCounter{Symbol{}}; + + // Maps defs to the Instructions that use them + DenseHashMap> usingInstructions; +}; + +} // namespace Luau::CFG diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index aff6afda..21eb588b 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -147,7 +147,7 @@ struct DataFlowGraphBuilder void join(DfgScope* p, DfgScope* a, DfgScope* b); void joinBindings(DfgScope* p, const DfgScope& a, const DfgScope& b); - void joinProps(DfgScope* p, const DfgScope& a, const DfgScope& b); + void joinProps(DfgScope* result, const DfgScope& a, const DfgScope& b); DefId lookup(Symbol symbol, Location location); DefId lookup(DefId def, const std::string& key, Location location); @@ -175,6 +175,7 @@ struct DataFlowGraphBuilder ControlFlow visit(AstStatDeclareGlobal* d); ControlFlow visit(AstStatDeclareFunction* d); ControlFlow visit(AstStatDeclareExternType* d); + ControlFlow visit(AstStatClass* d); ControlFlow visit(AstStatError* error); DataFlowResult visitExpr(AstExpr* e); diff --git a/Analysis/include/Luau/DcrLogger.h b/Analysis/include/Luau/DcrLogger.h index d650d9e0..e0fee741 100644 --- a/Analysis/include/Luau/DcrLogger.h +++ b/Analysis/include/Luau/DcrLogger.h @@ -87,15 +87,26 @@ struct BoundarySnapshot DenseHashMap typeStrings{nullptr}; }; -struct StepSnapshot +struct ConstraintStepSnapshot { - const Constraint* currentConstraint; - bool forced; + const Constraint* currentConstraint = nullptr; + bool forced = false; DenseHashMap unsolvedConstraints{nullptr}; ScopeSnapshot rootScope; DenseHashMap typeStrings{nullptr}; }; +struct GeneralizeStepSnapshot +{ + std::string before; + std::string after; + DenseHashMap unsolvedConstraints{nullptr}; + ScopeSnapshot rootScope; + DenseHashMap typeStrings{nullptr}; +}; + +using StepSnapshot = Variant; + struct TypeSolveLog { BoundarySnapshot initialState; @@ -125,12 +136,17 @@ struct DcrLogger void popBlock(NotNull block); void captureInitialSolverState(const Scope* rootScope, const std::vector>& unsolvedConstraints); - StepSnapshot prepareStepSnapshot( + ConstraintStepSnapshot prepareStepSnapshot( const Scope* rootScope, NotNull current, bool force, const std::vector>& unsolvedConstraints ); + GeneralizeStepSnapshot prepareGeneralizationSnapshot( + std::string before, + const Scope* rootScope, + const std::vector>& unsolvedConstraints + ); void commitStepSnapshot(StepSnapshot snapshot); void captureFinalSolverState(const Scope* rootScope, const std::vector>& unsolvedConstraints); diff --git a/Analysis/include/Luau/DumpCFG.h b/Analysis/include/Luau/DumpCFG.h new file mode 100644 index 00000000..a82c77ab --- /dev/null +++ b/Analysis/include/Luau/DumpCFG.h @@ -0,0 +1,22 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include + +namespace Luau::CFG +{ +struct Block; +struct ControlFlowGraph; +} // namespace Luau::CFG + +namespace Luau +{ + +std::string dumpCFG(const CFG::ControlFlowGraph& cfg); + +// Emits a single-line JSON representation of the CFG in the format consumed by +// mozilla-spidermonkey/iongraph. Paste the output into a file and upload it to +// the iongraph viewer here: https://mozilla-spidermonkey.github.io/iongraph/ +std::string dumpCFGJson(const CFG::ControlFlowGraph& cfg); + +} // namespace Luau diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index 052eeaeb..fc0b7ac8 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -5,6 +5,7 @@ #include "Luau/Location.h" #include "Luau/NotNull.h" #include "Luau/Type.h" +#include "Luau/TypeFunctionError.h" #include "Luau/TypeIds.h" #include "Luau/Variant.h" @@ -235,6 +236,13 @@ struct ModuleHasCyclicDependency bool operator==(const ModuleHasCyclicDependency& rhs) const; }; +struct CyclicModuleGraphTooLarge +{ + size_t moduleCount; + std::vector members; + bool operator==(const CyclicModuleGraphTooLarge& rhs) const; +}; + struct FunctionExitsWithoutReturning { TypePackId expectedReturnType; @@ -465,6 +473,13 @@ struct UserDefinedTypeFunctionError bool operator==(const UserDefinedTypeFunctionError& rhs) const; }; +struct BuiltInTypeFunctionError +{ + TypeFunctionError error; + + bool operator==(const BuiltInTypeFunctionError& rhs) const; +}; + struct ReservedIdentifier { std::string name; @@ -620,6 +635,7 @@ using TypeErrorData = Variant< ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, + CyclicModuleGraphTooLarge, IllegalRequire, FunctionExitsWithoutReturning, DuplicateGenericParameter, @@ -645,6 +661,7 @@ using TypeErrorData = Variant< UnexpectedTypePackInSubtyping, ExplicitFunctionAnnotationRecommended, UserDefinedTypeFunctionError, + BuiltInTypeFunctionError, ReservedIdentifier, UnexpectedArrayLikeTableItem, CannotCheckDynamicStringFormatCalls, diff --git a/Analysis/include/Luau/ExpectedTypeVisitor.h b/Analysis/include/Luau/ExpectedTypeVisitor.h index 4b195359..bde8a1e4 100644 --- a/Analysis/include/Luau/ExpectedTypeVisitor.h +++ b/Analysis/include/Luau/ExpectedTypeVisitor.h @@ -16,6 +16,7 @@ struct ExpectedTypeVisitor : public AstVisitor NotNull> astTypes, NotNull> astExpectedTypes, NotNull> astResolvedTypes, + NotNull> astOverloadResolvedTypes, NotNull arena, NotNull builtinTypes, NotNull rootScope @@ -67,6 +68,7 @@ struct ExpectedTypeVisitor : public AstVisitor NotNull> astTypes; NotNull> astExpectedTypes; NotNull> astResolvedTypes; + NotNull> astOverloadResolvedTypes; NotNull arena; NotNull builtinTypes; NotNull rootScope; diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index c69eb322..d529ff1e 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -3,6 +3,7 @@ #include "Luau/Config.h" #include "Luau/ConfigResolver.h" +#include "Luau/DenseHash2.h" #include "Luau/GlobalTypes.h" #include "Luau/Module.h" #include "Luau/ModuleResolver.h" @@ -44,6 +45,13 @@ struct LoadDefinitionFileResult std::optional parseMode(const std::vector& hotcomments); +struct ModuleSCC +{ + std::vector members; + std::shared_ptr sharedArena; +}; +using ModuleSCCPtr = std::shared_ptr; + struct SourceNode { bool hasDirtySourceModule() const @@ -71,6 +79,7 @@ struct SourceNode ModuleName name; std::string humanReadableName; + std::weak_ptr scc; DenseHashSet requireSet{{}}; std::vector> requireLocations; Set dependents{{}}; @@ -140,6 +149,7 @@ struct FrontendModuleResolver : ModuleResolver std::string getHumanReadableModuleName(const ModuleName& moduleName) const override; bool setModule(const ModuleName& moduleName, ModulePtr module); + void eraseModule(const ModuleName& moduleName); void clearModules(); @@ -174,13 +184,13 @@ struct Frontend size_t dynamicConstraintsCreated = 0; }; - + Frontend(SolverMode mode, FileResolver* fileResolver, ConfigResolver* configResolver, FrontendOptions options = {}); Frontend(FileResolver* fileResolver, ConfigResolver* configResolver, const FrontendOptions& options = {}); void setLuauSolverMode(SolverMode mode); SolverMode getLuauSolverMode() const; // The default value assuming there is no workspace setup yet - std::atomic useNewLuauSolver{FFlag::LuauSolverV2 ? SolverMode::New : SolverMode::Old}; + std::atomic useNewLuauSolver; // Parse module graph and prepare SourceNode/SourceModule data, including required dependencies without running typechecking void parse(const ModuleName& name); void parseModules(const std::vector& name); @@ -208,6 +218,7 @@ struct Frontend void clearStats(); void clear(); + void clearModules(const std::vector& names); void clearBuiltinEnvironments(); ScopePtr addEnvironment(const std::string& environmentName); @@ -277,6 +288,8 @@ struct Frontend DenseHashSet& seen, const FrontendOptions& frontendOptions ); + void computeSCCs(const std::vector& buildQueue); + void checkSCCBuildQueueItem(BuildQueueItem& item); void checkBuildQueueItem(BuildQueueItem& item); void checkBuildQueueItems(std::vector& items); void recordItemResult(const BuildQueueItem& item); @@ -312,27 +325,13 @@ struct Frontend std::unordered_map> sourceNodes; std::unordered_map> sourceModules; std::unordered_map requireTrace; + DenseHashMap2 sccs; Stats stats = {}; std::vector moduleQueue; }; -ModulePtr check( - const SourceModule& sourceModule, - Mode mode, - const std::vector& requireCycles, - NotNull builtinTypes, - NotNull iceHandler, - NotNull moduleResolver, - NotNull fileResolver, - const ScopePtr& globalScope, - const ScopePtr& typeFunctionScope, - std::function prepareModuleScope, - FrontendOptions options, - TypeCheckLimits limits -); - ModulePtr check( const SourceModule& sourceModule, Mode mode, diff --git a/Analysis/include/Luau/Generalization.h b/Analysis/include/Luau/Generalization.h index 4860abe2..2d06fc09 100644 --- a/Analysis/include/Luau/Generalization.h +++ b/Analysis/include/Luau/Generalization.h @@ -52,6 +52,7 @@ GeneralizationResult generalizeTypePack( void sealTable(NotNull scope, TypeId ty); + /** Attempt to generalize a type. * * If generalizationTarget is set, then only that type will be replaced by its diff --git a/Analysis/include/Luau/InferPolarity.h b/Analysis/include/Luau/InferPolarity.h deleted file mode 100644 index d32ef49f..00000000 --- a/Analysis/include/Luau/InferPolarity.h +++ /dev/null @@ -1,19 +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/NotNull.h" -#include "Luau/TypeFwd.h" - - -// Clip this file (and InferPolarity.cpp) with LuauStorePolarityInline - -namespace Luau -{ - -struct Scope; -struct TypeArena; - -void inferGenericPolarities_DEPRECATED(NotNull arena, NotNull scope, TypeId ty); -void inferGenericPolarities_DEPRECATED(NotNull arena, NotNull scope, TypePackId tp); - -} // namespace Luau diff --git a/Analysis/include/Luau/Instantiation2.h b/Analysis/include/Luau/Instantiation2.h index 30f3f51d..e8564525 100644 --- a/Analysis/include/Luau/Instantiation2.h +++ b/Analysis/include/Luau/Instantiation2.h @@ -14,12 +14,12 @@ namespace Luau struct TypeArena; struct TypeCheckLimits; -struct Replacer : Substitution +struct Replacer_DEPRECATED : Substitution { DenseHashMap replacements; DenseHashMap replacementPacks; - Replacer(NotNull arena, DenseHashMap replacements, DenseHashMap replacementPacks) + Replacer_DEPRECATED(NotNull arena, DenseHashMap replacements, DenseHashMap replacementPacks) : Substitution(TxnLog::empty(), arena) , replacements(std::move(replacements)) , replacementPacks(std::move(replacementPacks)) @@ -53,8 +53,38 @@ struct Replacer : Substitution } }; +struct Replacer : Substitution +{ + NotNull> replacements; + NotNull> replacementPacks; + + Replacer( + NotNull arena, + NotNull> replacements, + NotNull> replacementPacks + ); + + bool isDirty(TypeId ty) override; + + bool isDirty(TypePackId tp) override; + + TypeId clean(TypeId ty) override; + + TypePackId clean(TypePackId tp) override; + + bool ignoreChildren(TypeId ty) override; + +private: + /** + * It is *very* easy to create the world's worst bug by using a bound type + * as key: this is a helper function we run in debug mode to confirm this + * isn't the case. + */ + bool checkReplacementKeys() const; +}; + // A substitution which replaces generic functions by monomorphic functions -struct Instantiation2 final : Substitution +struct Instantiation2_DEPRECATED final : Substitution { // Mapping from generic types to free types to be used in instantiation. DenseHashMap genericSubstitutions{nullptr}; @@ -65,14 +95,18 @@ struct Instantiation2 final : Substitution Subtyping* subtyping = nullptr; Scope* scope = nullptr; - Instantiation2(TypeArena* arena, DenseHashMap genericSubstitutions, DenseHashMap genericPackSubstitutions) + Instantiation2_DEPRECATED( + TypeArena* arena, + DenseHashMap genericSubstitutions, + DenseHashMap genericPackSubstitutions + ) : Substitution(TxnLog::empty(), arena) , genericSubstitutions(std::move(genericSubstitutions)) , genericPackSubstitutions(std::move(genericPackSubstitutions)) { } - Instantiation2( + Instantiation2_DEPRECATED( TypeArena* arena, DenseHashMap genericSubstitutions, DenseHashMap genericPackSubstitutions, @@ -94,22 +128,17 @@ struct Instantiation2 final : Substitution TypePackId clean(TypePackId tp) override; }; -// Clip with LuauInstantiationUsesGenericPolarity -std::optional instantiate2_DEPRECATED( +void resolveGenericSubstitutions( TypeArena* arena, - DenseHashMap genericSubstitutions, - DenseHashMap genericPackSubstitutions, - TypeId ty -); - -// Clip with LuauInstantiationUsesGenericPolarity -std::optional instantiate2_DEPRECATED( - TypeArena* arena, - DenseHashMap genericSubstitutions, - DenseHashMap genericPackSubstitutions, - TypePackId tp + DenseHashMap& genericSubstitutions, + DenseHashMap& genericPackSubstitutions, + NotNull subtyping, + NotNull scope ); +// FIXME: This process needs a rename. It's not really instantiation. It's the +// process of substituting generics in a function type for inferred +// substitutions. std::optional instantiate2( TypeArena* arena, DenseHashMap genericSubstitutions, diff --git a/Analysis/include/Luau/IostreamHelpers.h b/Analysis/include/Luau/IostreamHelpers.h index 3d6f7fb1..d2ce45ec 100644 --- a/Analysis/include/Luau/IostreamHelpers.h +++ b/Analysis/include/Luau/IostreamHelpers.h @@ -12,11 +12,11 @@ namespace Luau { -std::ostream& operator<<(std::ostream& lhs, const Position& position); -std::ostream& operator<<(std::ostream& lhs, const Location& location); -std::ostream& operator<<(std::ostream& lhs, const AstName& name); +std::ostream& operator<<(std::ostream& stream, const Position& position); +std::ostream& operator<<(std::ostream& stream, const Location& location); +std::ostream& operator<<(std::ostream& stream, const AstName& name); -std::ostream& operator<<(std::ostream& lhs, const TypeError& error); +std::ostream& operator<<(std::ostream& stream, const TypeError& error); std::ostream& operator<<(std::ostream& lhs, const TypeMismatch& error); std::ostream& operator<<(std::ostream& lhs, const UnknownSymbol& error); std::ostream& operator<<(std::ostream& lhs, const UnknownProperty& error); @@ -37,6 +37,7 @@ std::ostream& operator<<(std::ostream& lhs, const FunctionExitsWithoutReturning& std::ostream& operator<<(std::ostream& lhs, const MissingProperties& error); std::ostream& operator<<(std::ostream& lhs, const IllegalRequire& error); std::ostream& operator<<(std::ostream& lhs, const ModuleHasCyclicDependency& error); +std::ostream& operator<<(std::ostream& lhs, const CyclicModuleGraphTooLarge& error); std::ostream& operator<<(std::ostream& lhs, const DuplicateGenericParameter& error); std::ostream& operator<<(std::ostream& lhs, const CannotInferBinaryOperation& error); std::ostream& operator<<(std::ostream& lhs, const SwappedGenericTypeParameter& error); @@ -44,19 +45,19 @@ std::ostream& operator<<(std::ostream& lhs, const OptionalValueAccess& error); std::ostream& operator<<(std::ostream& lhs, const MissingUnionProperty& error); std::ostream& operator<<(std::ostream& lhs, const TypesAreUnrelated& error); -std::ostream& operator<<(std::ostream& lhs, const TableState& tv); -std::ostream& operator<<(std::ostream& lhs, const Type& tv); -std::ostream& operator<<(std::ostream& lhs, const TypePackVar& tv); +std::ostream& operator<<(std::ostream& stream, const TableState& tv); +std::ostream& operator<<(std::ostream& stream, const Type& tv); +std::ostream& operator<<(std::ostream& stream, const TypePackVar& tv); -std::ostream& operator<<(std::ostream& lhs, const TypeErrorData& ted); +std::ostream& operator<<(std::ostream& stream, const TypeErrorData& data); -std::ostream& operator<<(std::ostream& lhs, TypeId ty); -std::ostream& operator<<(std::ostream& lhs, TypePackId tp); +std::ostream& operator<<(std::ostream& stream, TypeId ty); +std::ostream& operator<<(std::ostream& stream, TypePackId tp); namespace TypePath { -std::ostream& operator<<(std::ostream& lhs, const Path& path); +std::ostream& operator<<(std::ostream& stream, const Path& path); }; // namespace TypePath diff --git a/Analysis/include/Luau/LValue.h b/Analysis/include/Luau/LValue.h index e20d9901..c6f449c7 100644 --- a/Analysis/include/Luau/LValue.h +++ b/Analysis/include/Luau/LValue.h @@ -32,7 +32,7 @@ struct LValueHasher const LValue* baseof(const LValue& lvalue); -std::optional tryGetLValue(const class AstExpr& expr); +std::optional tryGetLValue(const class AstExpr& node); // Utility function: breaks down an LValue to get at the Symbol Symbol getBaseSymbol(const LValue& lvalue); diff --git a/Analysis/include/Luau/Module.h b/Analysis/include/Luau/Module.h index 44700b19..7c8cb043 100644 --- a/Analysis/include/Luau/Module.h +++ b/Analysis/include/Luau/Module.h @@ -75,6 +75,12 @@ struct RequireCycle struct Module { + explicit Module(std::shared_ptr sharedInternalTypes) + : internalTypes(std::move(sharedInternalTypes)) + { + LUAU_ASSERT(internalTypes); + } + ~Module(); // TODO: Clip this when we clip FFlagLuauSolverV2 @@ -84,7 +90,9 @@ struct Module std::string humanReadableName; TypeArena interfaceTypes; - TypeArena internalTypes; + // For modules in a require cycle, internalTypes is shared across all members + // so that a single constraint solver pass can allocate and resolve types across the cycle. + std::shared_ptr internalTypes; // Scopes and AST types refer to parse data, so we need to keep that alive std::shared_ptr allocator; @@ -161,4 +169,6 @@ struct Module bool constraintGenerationDidNotComplete = true; }; +void synthesizeExportReturn(NotNull builtinTypes, NotNull module); + } // namespace Luau diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index 3b59a303..f9fb9d40 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -18,26 +18,10 @@ namespace Luau struct InternalErrorReporter; struct Module; struct Scope; +struct TypeFunctionRuntime; using ModulePtr = std::shared_ptr; -bool isSubtype( - TypeId subTy, - TypeId superTy, - NotNull scope, - NotNull builtinTypes, - InternalErrorReporter& ice, - SolverMode solverMode -); -bool isSubtype( - TypePackId subPack, - TypePackId superPack, - NotNull scope, - NotNull builtinTypes, - InternalErrorReporter& ice, - SolverMode solverMode -); - } // namespace Luau template<> @@ -230,6 +214,10 @@ struct NormalizedType // This type is either never or number. TypeId numbers; + // The integer part of the type. + // This type is either never or integer. + TypeId integers; + // The string part of the type. // This may be the `string` type, or a union of singletons. NormalizedStringType strings; @@ -296,6 +284,7 @@ struct NormalizedType bool hasErrors() const; bool hasNils() const; bool hasNumbers() const; + bool hasIntegers() const; bool hasStrings() const; bool hasThreads() const; bool hasBuffers() const; @@ -447,4 +436,16 @@ class Normalizer friend struct FuelInitializer; }; +bool isSubtype( + TypeId subTy, + TypeId superTy, + NotNull arena, + NotNull builtinTypes, + NotNull scope, + NotNull normalizer, + NotNull typeFunctionRuntime, + NotNull reporter +); + + } // namespace Luau diff --git a/Analysis/include/Luau/OrderedSet.h b/Analysis/include/Luau/OrderedSet.h index f13ecd1c..dfd37319 100644 --- a/Analysis/include/Luau/OrderedSet.h +++ b/Analysis/include/Luau/OrderedSet.h @@ -11,6 +11,7 @@ namespace Luau template struct OrderedSet { + static_assert(std::is_pointer_v, "OrderedSet can only be used with pointers!"); using iterator = typename std::vector::iterator; using const_iterator = typename std::vector::const_iterator; diff --git a/Analysis/include/Luau/OverloadResolution.h b/Analysis/include/Luau/OverloadResolver.h similarity index 73% rename from Analysis/include/Luau/OverloadResolution.h rename to Analysis/include/Luau/OverloadResolver.h index 857cae58..5e075c5f 100644 --- a/Analysis/include/Luau/OverloadResolution.h +++ b/Analysis/include/Luau/OverloadResolver.h @@ -112,14 +112,6 @@ struct OverloadResolution struct OverloadResolver { - enum Analysis - { - Ok, - TypeIsNotAFunction, - ArityMismatch, - OverloadIsNonviable, // Arguments were incompatible with the overloads parameters but were otherwise compatible by arity - }; - OverloadResolver( NotNull builtinTypes, NotNull arena, @@ -141,13 +133,6 @@ struct OverloadResolver Subtyping subtyping; Location callLoc; - // Resolver results - std::vector ok; - std::vector nonFunctions; - std::vector> arityMismatches; - std::vector> nonviableOverloads; - InsertionOrderedMap> resolution; - // Given a (potentially overloaded) function and a set of arguments, test each overload. OverloadResolution resolveOverload( TypeId fnTy, @@ -186,35 +171,6 @@ struct OverloadResolver NotNull> uniqueTypes ); -public: - // Clip this with LuauBuiltinTypeFunctionsUseNewOverloadResolution - std::pair selectOverload_DEPRECATED( - TypeId ty, - TypePackId args, - NotNull> uniqueTypes, - bool useFreeTypeBounds - ); - -private: - std::pair checkOverload( - TypeId fnTy, - const TypePack* args, - AstExpr* fnLoc, - const std::vector* argExprs, - NotNull> uniqueTypes, - bool callMetamethodOk = true - ); - LUAU_NOINLINE - std::pair checkOverload_( - TypeId fnTy, - const FunctionType* fn, - const TypePack* args, - AstExpr* fnExpr, - const std::vector* argExprs, - NotNull> uniqueTypes - ); - size_t indexof(Analysis analysis); - void add(Analysis analysis, TypeId ty, ErrorVec&& errors); void maybeEmplaceError( ErrorVec* errors, Location argLocation, @@ -254,48 +210,10 @@ struct OverloadResolver // Used during overload selection to do arity-based filtering of overloads. // We do not accept nil in place of a generic unless that generic is explicitly optional. bool isArityCompatible(TypePackId candidate, TypePackId desired, NotNull builtinTypes) const; - - bool testFunctionTypeForOverloadSelection( - const FunctionType* ftv, - NotNull> uniqueTypes, - TypePackId argsPack, - bool useFreeTypeBounds - ); -}; - -struct SolveResult -{ - enum OverloadCallResult - { - Ok, - CodeTooComplex, - OccursCheckFailed, - NoMatchingOverload, - }; - - OverloadCallResult result; - std::optional typePackId; // nullopt if result != Ok - - TypeId overloadToUse = nullptr; - TypeId inferredTy = nullptr; - DenseHashMap> expandedFreeTypes{nullptr}; }; // Helper utility, presently used for binary operator type functions. // // Given a function and a set of arguments, select a suitable overload. -// Clip with FFlag::LuauBuiltinTypeFunctionsUseNewOverloadResolution -SolveResult solveFunctionCall_DEPRECATED( - NotNull arena, - NotNull builtinTypes, - NotNull normalizer, - NotNull typeFunctionRuntime, - NotNull iceReporter, - NotNull limits, - NotNull scope, - const Location& location, - TypeId fn, - TypePackId argsPack -); } // namespace Luau diff --git a/Analysis/include/Luau/Scope.h b/Analysis/include/Luau/Scope.h index 55a49786..b6ba4940 100644 --- a/Analysis/include/Luau/Scope.h +++ b/Analysis/include/Luau/Scope.h @@ -111,11 +111,6 @@ struct Scope DenseHashMap invalidTypeAliases{{}}; std::optional isInvalidTypeAlias(const std::string& name) const; - // Clip with LuauReworkInfiniteTypeFinder - // A set of type alias names that are invalid because they violate the recursion restrictions of type aliases. - DenseHashSet invalidTypeAliasNames_DEPRECATED{""}; - bool isInvalidTypeAliasName_DEPRECATED(const std::string& name) const; - NotNull findNarrowestScopeContaining(Location); }; diff --git a/Analysis/include/Luau/Set.h b/Analysis/include/Luau/Set.h index e114e98d..78277b04 100644 --- a/Analysis/include/Luau/Set.h +++ b/Analysis/include/Luau/Set.h @@ -26,6 +26,13 @@ class Set class const_iterator; using iterator = const_iterator; + template, int> = 0> + explicit Set(const T& empty_key = nullptr) + : mapping{empty_key} + { + } + + template, int> = 0> explicit Set(const T& empty_key) : mapping{empty_key} { diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index 5b4a6189..0f8f3f5c 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -40,6 +40,11 @@ struct SubtypingReasoning // The path, relative to the _root supertype_, where subtyping failed. Path superPath; SubtypingVariance variance = SubtypingVariance::Covariant; + // Set when the failure is due to a property modifier mismatch (e.g. the + // sub property is read-only or write-only but the super property requires + // read-write). In this case the leaf types at the path ends are the same, + // so a plain "X is not a subtype of X" message would be misleading. + bool isPropertyModifierViolation = false; bool operator==(const SubtypingReasoning& other) const; }; @@ -127,8 +132,8 @@ struct SubtypingResult /// If any generic bounds were invalid, report them here std::vector genericBoundsMismatches; - SubtypingResult& andAlso(const SubtypingResult& other, SubtypingSuppressionPolicy policy = SubtypingSuppressionPolicy::Any); - SubtypingResult& orElse(const SubtypingResult& other); + SubtypingResult& andAlso(SubtypingResult other, SubtypingSuppressionPolicy policy = SubtypingSuppressionPolicy::Any); + SubtypingResult& orElse(SubtypingResult other); SubtypingResult& withBothComponent(TypePath::Component component); SubtypingResult& withSuperComponent(TypePath::Component component); SubtypingResult& withSubComponent(TypePath::Component component); @@ -137,13 +142,12 @@ struct SubtypingResult SubtypingResult& withSuperPath(TypePath::Path path); SubtypingResult& withErrors(ErrorVec& err); SubtypingResult& withError(TypeError err); + SubtypingResult& withPropertyModifierViolation(); SubtypingResult& withAssumedConstraint(ConstraintV constraint); // Only negates the `isSubtype`. static SubtypingResult negate(const SubtypingResult& result); - static SubtypingResult all(const std::vector& results); - static SubtypingResult any(const std::vector& results); }; struct SubtypingEnvironment @@ -202,6 +206,8 @@ struct SubtypingEnvironment int iterationCount = 0; }; +struct TypeFunctionRuntime; + struct Subtyping { NotNull builtinTypes; @@ -278,6 +284,7 @@ struct Subtyping template SubtypingResult isInvariantWith(SubtypingEnvironment& env, const TryPair& pair, NotNull); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, const UnionType* superUnion, NotNull scope); SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const UnionType* superUnion, NotNull scope); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, TypeId superTy, NotNull scope); SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const IntersectionType* superIntersection, NotNull scope); @@ -306,6 +313,7 @@ struct Subtyping bool forceCovariantTest, NotNull scope ); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt, NotNull scope); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable, NotNull scope); SubtypingResult isCovariantWith( @@ -407,9 +415,16 @@ struct Subtyping // Pack subtyping SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp, NotNull scope); - std::optional isSubTailCovariantWith( + + enum class EarlyExit + { + Yes, + No + }; + + EarlyExit isSubTailCovariantWith( SubtypingEnvironment& env, - std::vector& outputResults, + SubtypingResult& outputResult, TypePackId subTp, TypePackId subTail, TypePackId superTp, @@ -418,9 +433,10 @@ struct Subtyping std::optional superTail, NotNull scope ); - std::optional isCovariantWithSuperTail( + + EarlyExit isCovariantWithSuperTail( SubtypingEnvironment& env, - std::vector& outputResults, + SubtypingResult& outputResult, TypePackId subTp, size_t subHeadStartIndex, const std::vector& subHead, diff --git a/Analysis/include/Luau/SubtypingUnifier.h b/Analysis/include/Luau/SubtypingUnifier.h index 96793a5a..7f85b480 100644 --- a/Analysis/include/Luau/SubtypingUnifier.h +++ b/Analysis/include/Luau/SubtypingUnifier.h @@ -65,8 +65,6 @@ struct SubtypingUnifier UpperBounds& upperBoundContributors ) const; - OccursCheckResult occursCheck(TypePackId needle, TypePackId haystack) const; - bool canBeUnified(TypeId ty) const; }; diff --git a/Analysis/include/Luau/TableLiteralInference.h b/Analysis/include/Luau/TableLiteralInference.h index 475e5ee3..dac86f93 100644 --- a/Analysis/include/Luau/TableLiteralInference.h +++ b/Analysis/include/Luau/TableLiteralInference.h @@ -25,18 +25,6 @@ struct PushTypeResult std::vector incompleteTypes; }; -// Clip with LuauPushTypeConstraintLambdas3 -PushTypeResult pushTypeInto_DEPRECATED( - NotNull> astTypes, - NotNull> astExpectedTypes, - NotNull solver, - NotNull constraint, - NotNull unifier, - NotNull subtyping, - TypeId expectedType, - const AstExpr* expr -); - PushTypeResult pushTypeInto( NotNull> astTypes, NotNull> astExpectedTypes, diff --git a/Analysis/include/Luau/ToString.h b/Analysis/include/Luau/ToString.h index 612b06f7..25d89de9 100644 --- a/Analysis/include/Luau/ToString.h +++ b/Analysis/include/Luau/ToString.h @@ -77,10 +77,10 @@ struct ToStringResult }; ToStringResult toStringDetailed(TypeId ty, ToStringOptions& opts); -ToStringResult toStringDetailed(TypePackId ty, ToStringOptions& opts); +ToStringResult toStringDetailed(TypePackId tp, ToStringOptions& opts); std::string toString(TypeId ty, ToStringOptions& opts); -std::string toString(TypePackId ty, ToStringOptions& opts); +std::string toString(TypePackId tp, ToStringOptions& opts); // These overloads are selected when a temporary ToStringOptions is passed. (eg // via an initializer list) @@ -151,7 +151,7 @@ std::string dump(const std::optional& ty); std::string dump(TypePackId ty); std::string dump(const std::optional& ty); std::string dump(const std::vector& types); -std::string dump(const std::vector& types); +std::string dump(const std::vector& typePacks); std::string dump(DenseHashMap& types); std::string dump(DenseHashMap& types); @@ -159,7 +159,7 @@ std::string dump(const Constraint& c); std::string dump(const std::shared_ptr& scope, const char* name); -std::string generateName(size_t n); +std::string generateName(size_t i); std::string toString(const Position& position); std::string toString(const Location& location, int offset = 0, bool useBegin = true); diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index eb63ccc3..a1c82b71 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -97,6 +97,11 @@ struct FreeType TypeId upperBound = nullptr; Polarity polarity = Polarity::Unknown; + + // If set, this free type was created for a primitive literal (string or boolean). + // When generalized, it will be resolved to its lower-bound singleton if the upper + // bound was narrowed, or to this primitive type otherwise. + std::optional primitiveType; }; struct GenericType @@ -162,6 +167,7 @@ struct PrimitiveType NilType, // ObjC #defines Nil :( Boolean, Number, + Integer, String, Thread, Function, @@ -413,14 +419,16 @@ enum class TableState struct TableIndexer { - TableIndexer(TypeId indexType, TypeId indexResultType) + TableIndexer(TypeId indexType, TypeId indexResultType, bool isReadOnly = false) : indexType(indexType) , indexResultType(indexResultType) + , isReadOnly(isReadOnly) { } TypeId indexType; TypeId indexResultType; + bool isReadOnly = false; }; struct Property @@ -540,6 +548,18 @@ struct ClassUserData virtual ~ClassUserData() {} }; +struct Obj +{ + TypeId ty; +}; + +struct Klass +{ + TypeId ty; +}; + +using NominalRelation = Variant; + /** The type of an external userdata exposed to Luau. * * Extern types behave like tables in many ways, but there are some important differences: @@ -561,6 +581,13 @@ struct ExternType ModuleName definitionModuleName; std::optional definitionLocation; std::optional indexer; + /* This field represents a bidirectional relationship between classes and object types + Given a Class, this relation should be a Obj in the variant, representing an instantiation of the class + Given a Object, this relation should be a Klass in the variant, representing the class prototype + Other sources of Extern Types will not have this relation set - this is for the classes fixture so that + we can go between class and object easily, given just the extern type + */ + std::optional relation; ExternType( Name name, @@ -945,6 +972,7 @@ bool isPrim(TypeId ty, PrimitiveType::Type primType); bool isNil(TypeId ty); bool isBoolean(TypeId ty); bool isNumber(TypeId ty); +bool isInteger(TypeId ty); bool isString(TypeId ty); bool isThread(TypeId ty); bool isBuffer(TypeId ty); @@ -1003,12 +1031,15 @@ struct BuiltinTypes std::unique_ptr typeFunctions; const TypeId nilType; const TypeId numberType; + const TypeId integerType; const TypeId stringType; const TypeId booleanType; const TypeId threadType; const TypeId bufferType; const TypeId functionType; const TypeId externType; + const TypeId objectType; + const TypeId classType; const TypeId tableType; const TypeId emptyTableType; const TypeId trueType; diff --git a/Analysis/include/Luau/TypeArena.h b/Analysis/include/Luau/TypeArena.h index adc59aeb..e217b341 100644 --- a/Analysis/include/Luau/TypeArena.h +++ b/Analysis/include/Luau/TypeArena.h @@ -52,8 +52,8 @@ struct TypeArena TypePackId addTypePack(std::initializer_list types); TypePackId addTypePack(std::vector types, std::optional tail = {}); - TypePackId addTypePack(TypePack pack); - TypePackId addTypePack(TypePackVar pack); + TypePackId addTypePack(TypePack tp); + TypePackId addTypePack(TypePackVar tp); template TypePackId addTypePack(T tp) diff --git a/Analysis/include/Luau/TypeChecker2.h b/Analysis/include/Luau/TypeChecker2.h index 9e44cd72..d1c81ecd 100644 --- a/Analysis/include/Luau/TypeChecker2.h +++ b/Analysis/include/Luau/TypeChecker2.h @@ -11,8 +11,6 @@ #include "Luau/TypeFwd.h" #include "Luau/TypeUtils.h" -LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) - namespace Luau { @@ -45,7 +43,7 @@ struct Reasonings // sort the reasons here to achieve a stable error // stringification. std::sort(reasons.begin(), reasons.end()); - std::string allReasons = (FFlag::LuauBetterTypeMismatchErrors && reasons.size() < 2) ? "\n" : "\nthis is because "; + std::string allReasons = reasons.size() < 2 ? "\n" : "\nthis is because "; for (const std::string& reason : reasons) { if (reasons.size() > 1) @@ -148,12 +146,14 @@ struct TypeChecker2 void visit(AstStatDeclareFunction* stat); void visit(AstStatDeclareGlobal* stat); void visit(AstStatDeclareExternType* stat); + void visit(AstStatClass* stat); void visit(AstStatError* stat); void visit(AstExpr* expr, ValueContext context); void visit(AstExprGroup* expr, ValueContext context); void visit(AstExprConstantNil* expr); void visit(AstExprConstantBool* expr); void visit(AstExprConstantNumber* expr); + void visit(AstExprConstantInteger* expr); void visit(AstExprConstantString* expr); void visit(AstExprLocal* expr); void visit(AstExprGlobal* expr); diff --git a/Analysis/include/Luau/TypeFunction.h b/Analysis/include/Luau/TypeFunction.h index 6b6fd20b..7d512c6e 100644 --- a/Analysis/include/Luau/TypeFunction.h +++ b/Analysis/include/Luau/TypeFunction.h @@ -4,6 +4,7 @@ #include "Luau/Constraint.h" #include "Luau/Error.h" #include "Luau/NotNull.h" +#include "Luau/Subtyping.h" #include "Luau/TypeCheckLimits.h" #include "Luau/TypeFunctionRuntime.h" #include "Luau/TypeFwd.h" @@ -35,6 +36,7 @@ struct TypeFunctionContext NotNull typeFunctionRuntime; NotNull ice; NotNull limits; + NotNull subtyping; // nullptr if the type function is being reduced outside of the constraint solver. ConstraintSolver* solver; @@ -43,7 +45,17 @@ struct TypeFunctionContext std::optional userFuncName; // Name of the user-defined type function; only available for UDTFs - TypeFunctionContext(NotNull cs, NotNull scope, NotNull constraint); + // Some type functions will create fresh instances as part of + // being solved, for example: + // + // add + // + // ... will mint: + // + // union + std::vector freshInstances; + + TypeFunctionContext(NotNull cs, NotNull scope, NotNull constraint, NotNull subtyping); TypeFunctionContext( NotNull arena, @@ -52,7 +64,8 @@ struct TypeFunctionContext NotNull normalizer, NotNull typeFunctionRuntime, NotNull ice, - NotNull limits + NotNull limits, + NotNull subtyping ) : arena(arena) , builtins(builtins) @@ -61,6 +74,7 @@ struct TypeFunctionContext , typeFunctionRuntime(typeFunctionRuntime) , ice(ice) , limits(limits) + , subtyping(subtyping) , solver(nullptr) , constraint(nullptr) { @@ -104,10 +118,6 @@ struct TypeFunctionReductionResult std::optional error; /// Messages printed out from user-defined type functions std::vector messages; - /// Some type function reduction rules may _create_ type functions (e.g. - /// the numeric type functions can "distribute" over an inner union). If - /// any type functions were created this way, we must add them here. - std::vector freshTypes; }; template diff --git a/Analysis/include/Luau/TypeFunctionError.h b/Analysis/include/Luau/TypeFunctionError.h new file mode 100644 index 00000000..193f7cb6 --- /dev/null +++ b/Analysis/include/Luau/TypeFunctionError.h @@ -0,0 +1,96 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Location.h" +#include "Luau/TypeFwd.h" +#include "Luau/Variant.h" + +#include + +namespace Luau +{ + +// The type function serializer attempted to serialize an unsupported type. +struct UnsupportedType +{ + TypeId type; + + bool operator==(const UnsupportedType& rhs) const; +}; + +// The type function serializer attempted to serialize an unsupported type pack. +struct UnsupportedTypePack +{ + TypePackId pack; + + bool operator==(const UnsupportedTypePack& rhs) const; +}; + +// An error produced by the runtime during type function evaluation. +struct RuntimeError +{ + std::string message; + + bool operator==(const RuntimeError& rhs) const; +}; + +// The type function body failed to compile. +struct FailedToCompile +{ + std::string functionName; + std::string compileError; + + bool operator==(const FailedToCompile& rhs) const; +}; + +// The type function was not found in the global scope after registration. +struct TypeFunctionMissing +{ + std::string functionName; + + bool operator==(const TypeFunctionMissing& rhs) const; +}; + +using TypeFunctionErrorData = Variant; + +struct TypeFunctionError +{ + Location location; + ModuleName moduleName; + TypeFunctionErrorData data; + + static int minCode(); + int code() const; + + TypeFunctionError() = default; + + TypeFunctionError(const Location& location, const ModuleName moduleName, TypeFunctionErrorData data) + : location(location) + , moduleName(moduleName) + , data(std::move(data)) + { + } + + TypeFunctionError(const Location& location, const TypeFunctionErrorData& data) + : TypeFunctionError(location, {}, data) + { + } + + bool operator==(const TypeFunctionError& rhs) const; +}; + +template +const T* get(const TypeFunctionError& e) +{ + return get_if(&e.data); +} + +template +T* get(TypeFunctionError& e) +{ + return get_if(&e.data); +} + +std::string toString(const TypeFunctionError& error); + +} // namespace Luau diff --git a/Analysis/include/Luau/TypeFunctionReductionGuesser.h b/Analysis/include/Luau/TypeFunctionReductionGuesser.h index b6d4a74c..5a5e1af4 100644 --- a/Analysis/include/Luau/TypeFunctionReductionGuesser.h +++ b/Analysis/include/Luau/TypeFunctionReductionGuesser.h @@ -50,7 +50,7 @@ struct TypeFunctionReductionGuesser TypeFunctionReductionGuesser(NotNull arena, NotNull builtins, NotNull normalizer); std::optional guess(TypeId typ); - std::optional guess(TypePackId typ); + std::optional guess(TypePackId tp); TypeFunctionReductionGuessResult guessTypeFunctionReductionForFunctionExpr(const AstExprFunction& expr, const FunctionType* ftv, TypeId retTy); private: @@ -73,7 +73,7 @@ struct TypeFunctionReductionGuesser void infer(); bool done(); - bool isFunctionGenericsSaturated(const FunctionType& ftv, DenseHashSet& instanceArgs); + bool isFunctionGenericsSaturated(const FunctionType& ftv, DenseHashSet& argsUsed); void inferTypeFunctionSubstitutions(TypeId ty, const TypeFunctionInstanceType* instance); TypeFunctionInferenceResult inferNumericBinopFunction(const TypeFunctionInstanceType* instance); TypeFunctionInferenceResult inferComparisonFunction(const TypeFunctionInstanceType* instance); diff --git a/Analysis/include/Luau/TypeFunctionRuntime.h b/Analysis/include/Luau/TypeFunctionRuntime.h index 06a8916a..315c930e 100644 --- a/Analysis/include/Luau/TypeFunctionRuntime.h +++ b/Analysis/include/Luau/TypeFunctionRuntime.h @@ -3,6 +3,7 @@ #include "Luau/Common.h" #include "Luau/Scope.h" +#include "Luau/TypeFunctionError.h" #include "Luau/TypeFunctionRuntimeBuilder.h" #include "Luau/Type.h" #include "Luau/Variant.h" @@ -40,6 +41,7 @@ struct TypeFunctionPrimitiveType NilType, Boolean, Number, + Integer, String, Thread, Buffer, @@ -157,6 +159,9 @@ struct TypeFunctionFunctionType TypeFunctionTypePackId argTypes; TypeFunctionTypePackId retTypes; + + // Parallel to argTypes.head; nullopt entries mean the parameter has no name. + std::vector> argNames; }; template @@ -177,14 +182,16 @@ T* getMutable(TypeFunctionTypePackId tv) struct TypeFunctionTableIndexer { - TypeFunctionTableIndexer(TypeFunctionTypeId keyType, TypeFunctionTypeId valueType) + TypeFunctionTableIndexer(TypeFunctionTypeId keyType, TypeFunctionTypeId valueType, bool isReadOnly = false) : keyType(keyType) , valueType(valueType) + , isReadOnly(isReadOnly) { } TypeFunctionTypeId keyType; TypeFunctionTypeId valueType; + bool isReadOnly = false; }; struct TypeFunctionProperty @@ -288,8 +295,11 @@ struct TypeFunctionRuntime TypeFunctionRuntime(NotNull ice, NotNull limits); ~TypeFunctionRuntime(); - // Return value is an error message if registration failed - std::optional registerFunction(AstStatTypeFunction* function); + // Return value is an error message string if registration failed. + std::optional registerFunction_DEPRECATED(AstStatTypeFunction* function); + + // Return value is a structured error if registration failed. + std::optional registerFunction(AstStatTypeFunction* function); // For user-defined type functions, we store all generated types and packs for the duration of the typecheck TypedAllocator typeArena; @@ -319,7 +329,8 @@ struct TypeFunctionRuntime void prepareState(); }; -std::optional checkResultForError(lua_State* L, const char* typeFunctionName, int luaResult); +std::optional checkResultForError_DEPRECATED(lua_State* L, const char* typeFunctionName, int luaResult); +std::optional checkResultForError(lua_State* L, const char* typeFunctionName, int luaResult); TypeFunctionRuntime* getTypeFunctionRuntime(lua_State* L); diff --git a/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h b/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h index c1315135..363d0147 100644 --- a/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h +++ b/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h @@ -2,6 +2,7 @@ #pragma once #include "Luau/TypeFunction.h" +#include "Luau/TypeFunctionError.h" namespace Luau { @@ -22,7 +23,9 @@ struct TypeFunctionRuntimeBuilderState // List of errors that occur during serialization/deserialization // At every iteration of serialization/deserialization, if this list.size() != 0, we halt the process - std::vector errors{}; + std::vector errors_DEPRECATED{}; + + std::vector errors{}; TypeFunctionRuntimeBuilderState(NotNull ctx) : ctx(ctx) diff --git a/Analysis/include/Luau/TypeIds.h b/Analysis/include/Luau/TypeIds.h index 9058f7b4..487daca2 100644 --- a/Analysis/include/Luau/TypeIds.h +++ b/Analysis/include/Luau/TypeIds.h @@ -2,6 +2,7 @@ #pragma once #include "Luau/DenseHash.h" +#include "Luau/OrderedSet.h" #include "Luau/TypeFwd.h" #include @@ -53,6 +54,7 @@ class TypeIds size_t size() const; bool empty() const; size_t count(TypeId ty) const; + bool contains(TypeId ty) const; void reserve(size_t n); @@ -74,4 +76,6 @@ class TypeIds std::vector take(); }; +using TypePackIds = OrderedSet; + } // namespace Luau diff --git a/Analysis/include/Luau/TypeInfer.h b/Analysis/include/Luau/TypeInfer.h index a6d78117..c9b4b8d3 100644 --- a/Analysis/include/Luau/TypeInfer.h +++ b/Analysis/include/Luau/TypeInfer.h @@ -75,8 +75,8 @@ struct TypeChecker std::vector> getScopes() const; - ControlFlow check(const ScopePtr& scope, const AstStat& statement); - ControlFlow check(const ScopePtr& scope, const AstStatBlock& statement); + ControlFlow check(const ScopePtr& scope, const AstStat& program); + ControlFlow check(const ScopePtr& scope, const AstStatBlock& block); ControlFlow check(const ScopePtr& scope, const AstStatIf& statement); ControlFlow check(const ScopePtr& scope, const AstStatWhile& statement); ControlFlow check(const ScopePtr& scope, const AstStatRepeat& statement); @@ -84,20 +84,20 @@ struct TypeChecker ControlFlow check(const ScopePtr& scope, const AstStatAssign& assign); ControlFlow check(const ScopePtr& scope, const AstStatCompoundAssign& assign); ControlFlow check(const ScopePtr& scope, const AstStatLocal& local); - ControlFlow check(const ScopePtr& scope, const AstStatFor& local); + ControlFlow check(const ScopePtr& scope, const AstStatFor& expr); ControlFlow check(const ScopePtr& scope, const AstStatForIn& forin); ControlFlow check(const ScopePtr& scope, TypeId ty, const ScopePtr& funScope, const AstStatFunction& function); ControlFlow check(const ScopePtr& scope, TypeId ty, const ScopePtr& funScope, const AstStatLocalFunction& function); ControlFlow check(const ScopePtr& scope, const AstStatTypeAlias& typealias); ControlFlow check(const ScopePtr& scope, const AstStatTypeFunction& typefunction); ControlFlow check(const ScopePtr& scope, const AstStatDeclareExternType& declaredExternType); - ControlFlow check(const ScopePtr& scope, const AstStatDeclareFunction& declaredFunction); + ControlFlow check(const ScopePtr& scope, const AstStatDeclareFunction& global); void prototype(const ScopePtr& scope, const AstStatTypeAlias& typealias, int subLevel = 0); void prototype(const ScopePtr& scope, const AstStatDeclareExternType& declaredExternType); - ControlFlow checkBlock(const ScopePtr& scope, const AstStatBlock& statement); - ControlFlow checkBlockWithoutRecursionCheck(const ScopePtr& scope, const AstStatBlock& statement); + ControlFlow checkBlock(const ScopePtr& scope, const AstStatBlock& block); + ControlFlow checkBlockWithoutRecursionCheck(const ScopePtr& scope, const AstStatBlock& block); void checkBlockTypeAliases(const ScopePtr& scope, std::vector& sorted); WithPredicate checkExpr( @@ -134,7 +134,7 @@ struct TypeChecker WithPredicate checkExpr(const ScopePtr& scope, const AstExprError& expr); WithPredicate checkExpr(const ScopePtr& scope, const AstExprIfElse& expr, std::optional expectedType = std::nullopt); WithPredicate checkExpr(const ScopePtr& scope, const AstExprInterpString& expr); - WithPredicate checkExpr(const ScopePtr& scope, const AstExprInstantiate& expr); + WithPredicate checkExpr(const ScopePtr& scope, const AstExprInstantiate& explicitTypeInstantiation); TypeId checkExprTable( const ScopePtr& scope, @@ -168,8 +168,8 @@ struct TypeChecker const ScopePtr& scope, const AstExpr& funName, Unifier& state, - TypePackId paramPack, TypePackId argPack, + TypePackId paramPack, const std::vector& argLocations ); @@ -224,7 +224,7 @@ struct TypeChecker const Location& location, const AstArray& exprs, bool substituteFreeForNil = false, - const std::vector& lhsAnnotations = {}, + const std::vector& annotatedTypeArguments = {}, const std::vector>& expectedTypes = {} ); @@ -317,7 +317,7 @@ struct TypeChecker TypeId anyify(const ScopePtr& scope, TypeId ty, Location location); TypePackId anyify(const ScopePtr& scope, TypePackId ty, Location location); - TypePackId anyifyModuleReturnTypePackGenerics(TypePackId ty); + TypePackId anyifyModuleReturnTypePackGenerics(TypePackId tp); void reportError(const TypeError& error); void reportError(const Location& location, TypeErrorData error); @@ -382,7 +382,7 @@ struct TypeChecker TypeId addTV(Type&& tv); - TypePackId addTypePack(TypePackVar&& tp); + TypePackId addTypePack(TypePackVar&& tv); TypePackId addTypePack(TypePack&& tp); TypePackId addTypePack(const std::vector& ty); @@ -442,7 +442,7 @@ struct TypeChecker * The return vector is always of the exact requested length. In the event that the pack's length does * not match up, excess TypeIds will be ErrorTypes. */ - std::vector unTypePack(const ScopePtr& scope, TypePackId pack, size_t expectedLength, const Location& location); + std::vector unTypePack(const ScopePtr& scope, TypePackId tp, size_t expectedLength, const Location& location); const ScopePtr& globalScope; @@ -470,6 +470,7 @@ struct TypeChecker public: const TypeId nilType; const TypeId numberType; + const TypeId integerType; const TypeId stringType; const TypeId booleanType; const TypeId threadType; diff --git a/Analysis/include/Luau/TypePack.h b/Analysis/include/Luau/TypePack.h index 75f30e7c..9dc7c6d8 100644 --- a/Analysis/include/Luau/TypePack.h +++ b/Analysis/include/Luau/TypePack.h @@ -157,8 +157,8 @@ struct TypePackIterator using iterator_category = std::input_iterator_tag; TypePackIterator() = default; - explicit TypePackIterator(TypePackId tp); - TypePackIterator(TypePackId tp, const TxnLog* log); + explicit TypePackIterator(TypePackId typePack); + TypePackIterator(TypePackId typePack, const TxnLog* log); TypePackIterator& operator++(); TypePackIterator operator++(int); @@ -167,6 +167,10 @@ struct TypePackIterator const TypeId& operator*(); + // If the iterator currently points at the head of a type pack, return that + // pack. Else return nullopt. + std::optional tryGetHead() const; + /** Return the tail of a TypePack. * This may *only* be called on an iterator that has been incremented to the end. * Returns nullopt if the pack has fixed length. diff --git a/Analysis/include/Luau/TypeStateMap.h b/Analysis/include/Luau/TypeStateMap.h new file mode 100644 index 00000000..317e94b1 --- /dev/null +++ b/Analysis/include/Luau/TypeStateMap.h @@ -0,0 +1,62 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/Constraint.h" +#include "Luau/ControlFlowGraph.h" +#include "Luau/DenseHash.h" + +#include "Luau/NotNull.h" +#include "Luau/TypeFwd.h" +#include "Luau/Scope.h" + +#include + +namespace Luau +{ + +namespace CFG +{ + +// TypeStateMap walks a ControlFlowGraph in SSA form and assigns a TypeId to every +// Definition. The mapping is keyed on the definition. +// +// Rules: +// Declare (no annotation) -> BlockedType +// Assign -> BlockedTypes +// Join -> represents points where control flow merges - we allocate union types here, along with a simplification constraint +// Refine -> refine (type-function instance) +// +// The walk is two-pass and lazy-allocates BlockedType placeholders for any +// Join operand whose def hasn't been visited yet (back-edge phi operands). When +// the walk later reaches that def's Declare/Assign, the existing placeholder is +// reused. +struct TypeStateMap +{ + TypeStateMap(NotNull arena, NotNull globalScope, NotNull builtinTypes, NotNull g); + void computeTypes(); + TypeId getRHSType(AstExpr* expr); + TypeId getLHSType(const LValue& lv) const; + std::optional getOptionalConstraint(TypeId ty) const; + +private: + TypeId getType(Definition* def) const; + void handleInstruction(InstrId id); + TypeId getDiscriminantOf(const Refine& refine); + + DenseHashMap defTypes{nullptr}; + + // Mapping from types to the constraints they require to be solved + // Join instructions will require simplification constraints + // Refine instructions will require refinement constraints + DenseHashMap typesRequiringConstraint{nullptr}; + + NotNull arena; + NotNull builtinTypes; + NotNull g; + NotNull globalScope; +}; + +} // namespace CFG + +} // namespace Luau diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 7ed2cc9c..7d82dfd1 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -84,6 +84,16 @@ std::optional findTablePropertyRespectingMeta( bool occursCheck(TypeId needle, TypeId haystack); +// NOTE: This uses a custom enum as it is replacing several bespoke +// implementations of the same logic. +enum class OccursCheckResult +{ + Pass, + Fail +}; + +OccursCheckResult occursCheck(TypePackId needle, TypePackId haystack); + // Returns the minimum and maximum number of types the argument list can accept. std::pair> getParameterExtents(const TxnLog* log, TypePackId tp, bool includeHiddenVariadics = false); @@ -252,6 +262,7 @@ std::optional follow(std::optional ty) */ bool isLiteral(const AstExpr* expr); +// Clip with LuauRelaxConstraintOrderingForFunctionCheck /** * Given a function call and a mapping from expression to type, determine * whether the type of any argument in said call in depends on a blocked types. @@ -262,7 +273,7 @@ bool isLiteral(const AstExpr* expr); * @param astTypes Mapping from AST node to TypeID * @returns A vector of blocked types */ -std::vector findBlockedArgTypesIn(AstExprCall* expr, NotNull> astTypes); +std::vector findBlockedArgTypesIn_DEPRECATED(AstExprCall* expr, NotNull> astTypes); /** * Given a scope and a free type, find the closest parent that has a present @@ -283,7 +294,14 @@ bool fastIsSubtype(TypeId subTy, TypeId superTy); * @param exprType Type of the expression to match * @return An element of `tables` that best matches `exprType`. */ -std::optional extractMatchingTableType(std::vector& tables, TypeId exprType, NotNull builtinTypes); +std::optional extractMatchingTableType_DEPRECATED(const UnionType* expectedUnion, TypeId exprType, NotNull builtinTypes); + +std::optional extractMatchingTableType( + const UnionType* expectedUnion, + TypeId exprType, + NotNull builtinTypes, + NotNull arena +); /** * @param item A member of a table in an AST @@ -383,11 +401,12 @@ struct IntersectionBuilder TypeId addIntersection(NotNull arena, NotNull builtinTypes, std::initializer_list list); TypeId addUnion(NotNull arena, NotNull builtinTypes, std::initializer_list list); -struct ContainsAnyGeneric final : public TypeOnceVisitor +// Clip with LuauInstantiateFunctionTypeBeforePush +struct ContainsAnyGeneric_DEPRECATED final : public TypeOnceVisitor { bool found = false; - explicit ContainsAnyGeneric(); + explicit ContainsAnyGeneric_DEPRECATED(); bool visit(TypeId ty) override; bool visit(TypePackId ty) override; @@ -414,4 +433,16 @@ bool containsGeneric(TypePackId ty, NotNull> generics) */ bool isBlocked(TypeId ty); + +/** + * **YOU SHOULD PROBABLY NOT USE THIS FUNCTION.** + * + * This function is a stop-gap while we rework function call inference and + * eager generalization. + * + * @return An approximate return type of `ty`, assuming `ty` is a function or + * union of functions. + */ +std::optional getApproximateReturnTypeForFunctionCall(TypeId ty); + } // namespace Luau diff --git a/Analysis/include/Luau/Unifier.h b/Analysis/include/Luau/Unifier.h index 7e1ef957..2dee6473 100644 --- a/Analysis/include/Luau/Unifier.h +++ b/Analysis/include/Luau/Unifier.h @@ -40,7 +40,7 @@ struct Widen : Substitution bool ignoreChildren(TypeId ty) override; TypeId operator()(TypeId ty); - TypePackId operator()(TypePackId ty); + TypePackId operator()(TypePackId tp); }; /** @@ -103,7 +103,7 @@ struct Unifier TypeId superTy, bool isFunctionCall = false, bool isIntersection = false, - const LiteralProperties* aliasableMap = nullptr + const LiteralProperties* literalProperties = nullptr ); private: @@ -112,9 +112,9 @@ struct Unifier TypeId superTy, bool isFunctionCall = false, bool isIntersection = false, - const LiteralProperties* aliasableMap = nullptr + const LiteralProperties* literalProperties = nullptr ); - void tryUnifyUnionWithType(TypeId subTy, const UnionType* uv, TypeId superTy); + void tryUnifyUnionWithType(TypeId subTy, const UnionType* subUnion, TypeId superTy); // Traverse the two types provided and block on any BlockedTypes we find. // Returns true if any types were blocked on. @@ -134,7 +134,7 @@ struct Unifier void tryUnifyPrimitives(TypeId subTy, TypeId superTy); void tryUnifySingletons(TypeId subTy, TypeId superTy); void tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCall = false); - void tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection = false, const LiteralProperties* aliasableMap = nullptr); + void tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection = false, const LiteralProperties* literalProperties = nullptr); void tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed); void tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed); void tryUnifyWithExternType(TypeId subTy, TypeId superTy, bool reversed); @@ -151,11 +151,11 @@ struct Unifier void cacheResult(TypeId subTy, TypeId superTy, size_t prevErrorCount); public: - void tryUnify(TypePackId subTy, TypePackId superTy, bool isFunctionCall = false); + void tryUnify(TypePackId subTp, TypePackId superTp, bool isFunctionCall = false); private: - void tryUnify_(TypePackId subTy, TypePackId superTy, bool isFunctionCall = false); - void tryUnifyVariadics(TypePackId subTy, TypePackId superTy, bool reversed, int subOffset = 0); + void tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCall = false); + void tryUnifyVariadics(TypePackId subTp, TypePackId superTp, bool reversed, int subOffset = 0); void tryUnifyWithAny(TypeId subTy, TypeId anyTy); void tryUnifyWithAny(TypePackId subTy, TypePackId anyTp); diff --git a/Analysis/include/Luau/Unifier2.h b/Analysis/include/Luau/Unifier2.h index 0117ee82..572fbf83 100644 --- a/Analysis/include/Luau/Unifier2.h +++ b/Analysis/include/Luau/Unifier2.h @@ -8,6 +8,7 @@ #include "Luau/TypeCheckLimits.h" #include "Luau/TypeFwd.h" #include "Luau/TypePairHash.h" +#include "Luau/TypeUtils.h" #include #include @@ -20,12 +21,6 @@ struct InternalErrorReporter; struct Scope; struct TypeArena; -enum class OccursCheckResult -{ - Pass, - Fail -}; - enum class UnifyResult { Ok, @@ -124,7 +119,8 @@ struct Unifier2 UnifyResult unify_(TypePackId subTp, TypePackId superTp); - std::optional generalize(TypeId ty); + template + TID instantiateWithBoundTypes(TID ty); /** * @returns simplify(left | right) @@ -136,14 +132,6 @@ struct Unifier2 */ TypeId mkIntersection(TypeId left, TypeId right); - // Returns true if needle occurs within haystack already. ie if we bound - // needle to haystack, would a cyclic type result? - OccursCheckResult occursCheck(DenseHashSet& seen, TypeId needle, TypeId haystack); - - // Returns true if needle occurs within haystack already. ie if we bound - // needle to haystack, would a cyclic TypePack result? - OccursCheckResult occursCheck(DenseHashSet& seen, TypePackId needle, TypePackId haystack); - TypeId freshType(NotNull scope, Polarity polarity); TypePackId freshTypePack(NotNull scope, Polarity polarity); }; diff --git a/Analysis/include/Luau/VisitType.h b/Analysis/include/Luau/VisitType.h index cb9acf9d..33a35c9b 100644 --- a/Analysis/include/Luau/VisitType.h +++ b/Analysis/include/Luau/VisitType.h @@ -10,6 +10,7 @@ #include "Type.h" LUAU_FASTINT(LuauVisitRecursionLimit) +LUAU_FASTFLAG(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) namespace Luau { @@ -257,6 +258,12 @@ struct GenericTypeVisitor traverse(ftv->lowerBound); traverse(ftv->upperBound); + + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + if (ftv->primitiveType) + traverse(*ftv->primitiveType); + } } } else if (auto gtv = get(ty)) diff --git a/Analysis/src/AstJsonEncoder.cpp b/Analysis/src/AstJsonEncoder.cpp index 2c7e112e..37bb256a 100644 --- a/Analysis/src/AstJsonEncoder.cpp +++ b/Analysis/src/AstJsonEncoder.cpp @@ -8,6 +8,8 @@ #include +LUAU_FASTFLAG(LuauTrackPrefixLocal) + namespace Luau { @@ -131,21 +133,40 @@ struct AstJsonEncoder : public AstVisitor void writeString(std::string_view sv) { - // TODO escape more accurately? writeRaw("\""); for (char c : sv) { - if (c == '"') + switch (c) + { + case '"': writeRaw("\\\""); - else if (c == '\\') + break; + case '\\': writeRaw("\\\\"); - else if (c < ' ') - writeRaw(format("\\u%04x", c)); - else if (c == '\n') + break; + case '\b': + writeRaw("\\b"); + break; + case '\f': + writeRaw("\\f"); + break; + case '\n': writeRaw("\\n"); - else - writeRaw(c); + break; + case '\r': + writeRaw("\\r"); + break; + case '\t': + writeRaw("\\t"); + break; + default: + if (static_cast(c) < 0x20) + writeRaw(format("\\u%04x", static_cast(c))); + else + writeRaw(c); + break; + } } writeRaw("\""); @@ -241,6 +262,7 @@ struct AstJsonEncoder : public AstVisitor else write("luauType", nullptr); write("name", local->name); + write("isConst", local->isConst); writeType("AstLocal"); write("location", local->location); popComma(c); @@ -310,6 +332,18 @@ struct AstJsonEncoder : public AstVisitor ); } + void write(class AstExprConstantInteger* node) + { + writeNode( + node, + "AstExprConstantInteger", + [&]() + { + write("value", node->value); + } + ); + } + void write(class AstExprConstantString* node) { writeNode( @@ -493,11 +527,11 @@ struct AstJsonEncoder : public AstVisitor { switch (kind) { - case AstExprTable::Item::List: + case AstExprTable::Item::Kind::List: return writeString("item"); - case AstExprTable::Item::Record: + case AstExprTable::Item::Kind::Record: return writeString("record"); - case AstExprTable::Item::General: + case AstExprTable::Item::Kind::General: return writeString("general"); } } @@ -510,7 +544,7 @@ struct AstJsonEncoder : public AstVisitor write("kind", item.kind); switch (item.kind) { - case AstExprTable::Item::List: + case AstExprTable::Item::Kind::List: write("value", item.value); break; default: @@ -567,11 +601,11 @@ struct AstJsonEncoder : public AstVisitor { switch (op) { - case AstExprUnary::Not: + case AstExprUnary::Op::Not: return writeString("Not"); - case AstExprUnary::Minus: + case AstExprUnary::Op::Minus: return writeString("Minus"); - case AstExprUnary::Len: + case AstExprUnary::Op::Len: return writeString("Len"); } } @@ -984,6 +1018,8 @@ struct AstJsonEncoder : public AstVisitor PROP(prefix); if (node->prefixLocation) write("prefixLocation", *node->prefixLocation); + if (FFlag::LuauTrackPrefixLocal && node->prefixLocal) + write("prefixLocal", node->prefixLocal); PROP(name); PROP(nameLocation); PROP(parameters); @@ -1143,21 +1179,6 @@ struct AstJsonEncoder : public AstVisitor ); } - void write(AstAttr::Type type) - { - switch (type) - { - case AstAttr::Type::Checked: - return writeString("checked"); - case AstAttr::Type::Native: - return writeString("native"); - case AstAttr::Type::Deprecated: - return writeString("deprecated"); - case AstAttr::Type::Unknown: - return writeString("unknown"); - } - } - void write(class AstAttr* node) { writeNode( @@ -1233,6 +1254,12 @@ struct AstJsonEncoder : public AstVisitor return false; } + bool visit(class AstExprConstantInteger* node) override + { + write(node); + return false; + } + bool visit(class AstExprConstantString* node) override { write(node); diff --git a/Analysis/src/AstQuery.cpp b/Analysis/src/AstQuery.cpp index 2adad21f..068eeed0 100644 --- a/Analysis/src/AstQuery.cpp +++ b/Analysis/src/AstQuery.cpp @@ -12,10 +12,6 @@ #include -LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauQueryLocalFunctionBinding) -LUAU_FASTFLAG(LuauAnalysisUsesSolverMode) - namespace Luau { @@ -351,73 +347,39 @@ static std::optional findBindingLocalStatement(const SourceModule std::optional findBindingAtPosition(const Module& module, const SourceModule& source, Position pos) { - if (FFlag::LuauQueryLocalFunctionBinding) - { - ExprOrLocal exprOrLocal = findExprOrLocalAtPosition(source, pos); - - Symbol name; - if (auto expr = exprOrLocal.getExpr()) - { - if (auto g = expr->as()) - name = g->name; - else if (auto l = expr->as()) - name = l->local; - else - return std::nullopt; - } - else if (auto local = exprOrLocal.getLocal()) - name = local; - else - return std::nullopt; - - ScopePtr currentScope = findScopeAtPosition(module, pos); - - while (currentScope) - { - auto iter = currentScope->bindings.find(name); - if (iter != currentScope->bindings.end() && iter->second.location.begin <= pos) - { - // Ignore this binding if we're inside its definition. e.g. local abc = abc -- Will take the definition of abc from outer scope - std::optional bindingStatement = findBindingLocalStatement(source, iter->second); - if (!bindingStatement || !(*bindingStatement)->location.contains(pos)) - return iter->second; - } - currentScope = currentScope->parent; - } + ExprOrLocal exprOrLocal = findExprOrLocalAtPosition(source, pos); - return std::nullopt; - } - else + Symbol name; + if (auto expr = exprOrLocal.getExpr()) { - AstExpr* expr = findExprAtPosition(source, pos); - if (!expr) - return std::nullopt; - - Symbol name; if (auto g = expr->as()) name = g->name; else if (auto l = expr->as()) name = l->local; else return std::nullopt; + } + else if (auto local = exprOrLocal.getLocal()) + name = local; + else + return std::nullopt; - ScopePtr currentScope = findScopeAtPosition(module, pos); + ScopePtr currentScope = findScopeAtPosition(module, pos); - while (currentScope) + while (currentScope) + { + auto iter = currentScope->bindings.find(name); + if (iter != currentScope->bindings.end() && iter->second.location.begin <= pos) { - auto iter = currentScope->bindings.find(name); - if (iter != currentScope->bindings.end() && iter->second.location.begin <= pos) - { - // Ignore this binding if we're inside its definition. e.g. local abc = abc -- Will take the definition of abc from outer scope - std::optional bindingStatement = findBindingLocalStatement(source, iter->second); - if (!bindingStatement || !(*bindingStatement)->location.contains(pos)) - return iter->second; - } - currentScope = currentScope->parent; + // Ignore this binding if we're inside its definition. e.g. local abc = abc -- Will take the definition of abc from outer scope + std::optional bindingStatement = findBindingLocalStatement(source, iter->second); + if (!bindingStatement || !(*bindingStatement)->location.contains(pos)) + return iter->second; } - - return std::nullopt; + currentScope = currentScope->parent; } + + return std::nullopt; } namespace @@ -568,17 +530,13 @@ static std::optional getMetatableDocumentation( return std::nullopt; TypeId followed; - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) - { - if (indexIt->second.readTy) - followed = follow(*indexIt->second.readTy); - else if (indexIt->second.writeTy) - followed = follow(*indexIt->second.writeTy); - else - return std::nullopt; - } + if (indexIt->second.readTy) + followed = follow(*indexIt->second.readTy); + else if (indexIt->second.writeTy) + followed = follow(*indexIt->second.writeTy); else - followed = follow(indexIt->second.type_DEPRECATED()); + return std::nullopt; + const TableType* ttv = get(followed); if (!ttv) return std::nullopt; @@ -587,13 +545,8 @@ static std::optional getMetatableDocumentation( if (propIt == ttv->props.end()) return std::nullopt; - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) - { - if (auto ty = propIt->second.readTy) - return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol); - } - else - return checkOverloadedDocumentationSymbol(module, propIt->second.type_DEPRECATED(), parentExpr, propIt->second.documentationSymbol); + if (auto ty = propIt->second.readTy) + return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol); return std::nullopt; } @@ -605,12 +558,6 @@ std::optional getDocumentationSymbolAtPosition(const Source AstExpr* targetExpr = ancestry.size() >= 1 ? ancestry[ancestry.size() - 1]->asExpr() : nullptr; AstExpr* parentExpr = ancestry.size() >= 2 ? ancestry[ancestry.size() - 2]->asExpr() : nullptr; - if (!FFlag::LuauQueryLocalFunctionBinding) - { - if (std::optional binding = findBindingAtPosition(module, source, position)) - return checkOverloadedDocumentationSymbol(module, binding->typeId, parentExpr, binding->documentationSymbol); - } - if (targetExpr) { if (AstExprIndexName* indexName = targetExpr->as()) @@ -622,15 +569,8 @@ std::optional getDocumentationSymbolAtPosition(const Source { if (auto propIt = ttv->props.find(indexName->index.value); propIt != ttv->props.end()) { - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) - { - if (auto ty = propIt->second.readTy) - return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol); - } - else - return checkOverloadedDocumentationSymbol( - module, propIt->second.type_DEPRECATED(), parentExpr, propIt->second.documentationSymbol - ); + if (auto ty = propIt->second.readTy) + return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol); } } else if (const ExternType* etv = get(parentTy)) @@ -639,15 +579,9 @@ std::optional getDocumentationSymbolAtPosition(const Source { if (auto propIt = etv->props.find(indexName->index.value); propIt != etv->props.end()) { - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) - { - if (auto ty = propIt->second.readTy) - return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol); - } - else - return checkOverloadedDocumentationSymbol( - module, propIt->second.type_DEPRECATED(), parentExpr, propIt->second.documentationSymbol - ); + + if (auto ty = propIt->second.readTy) + return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol); } etv = etv->parent ? Luau::get(*etv->parent) : nullptr; } @@ -695,11 +629,8 @@ std::optional getDocumentationSymbolAtPosition(const Source } } - if (FFlag::LuauQueryLocalFunctionBinding) - { - if (std::optional binding = findBindingAtPosition(module, source, position)) - return checkOverloadedDocumentationSymbol(module, binding->typeId, parentExpr, binding->documentationSymbol); - } + if (std::optional binding = findBindingAtPosition(module, source, position)) + return checkOverloadedDocumentationSymbol(module, binding->typeId, parentExpr, binding->documentationSymbol); if (std::optional ty = findTypeAtPosition(module, source, position)) { diff --git a/Analysis/src/AstUtils.cpp b/Analysis/src/AstUtils.cpp index 65488202..6cec8a01 100644 --- a/Analysis/src/AstUtils.cpp +++ b/Analysis/src/AstUtils.cpp @@ -1,11 +1,45 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/AstUtils.h" #include "Luau/Ast.h" #include "Luau/Type.h" namespace Luau { +std::optional matchTypeGuard(AstExprBinary::Op op, AstExpr* left, AstExpr* right) +{ + if (op != AstExprBinary::CompareEq && op != AstExprBinary::CompareNe) + return std::nullopt; + + if (right->is()) + std::swap(left, right); + + if (!right->is()) + return std::nullopt; + + AstExprCall* call = left->as(); + AstExprConstantString* string = right->as(); + if (!call || !string) + return std::nullopt; + + AstExprGlobal* callee = call->func->as(); + if (!callee) + return std::nullopt; + + if (callee->name != "type" && callee->name != "typeof") + return std::nullopt; + + if (call->args.size != 1) + return std::nullopt; + + return TypeGuard{ + /*isTypeof*/ callee->name == "typeof", + /*target*/ call->args.data[0], + /*type*/ std::string(string->value.data, string->value.size), + }; +} + struct AstExprTableFinder : AstVisitor { NotNull> result; diff --git a/Analysis/src/AutocompleteCore.cpp b/Analysis/src/AutocompleteCore.cpp index 93d7f531..134e3e47 100644 --- a/Analysis/src/AutocompleteCore.cpp +++ b/Analysis/src/AutocompleteCore.cpp @@ -26,12 +26,23 @@ LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAGVARIABLE(DebugLuauMagicVariableNames) -LUAU_FASTFLAGVARIABLE(LuauAutocompleteFunctionCallArgTails2) -LUAU_FASTFLAGVARIABLE(LuauACOnMTTWriteOnlyPropNoCrash) - -static constexpr std::array kStatementStartingKeywords = +LUAU_FASTFLAGVARIABLE(LuauAutocompleteConst) +LUAU_FASTFLAGVARIABLE(LuauAutocompleteExport) +LUAU_FASTFLAG(LuauExportValueSyntax) +LUAU_FASTFLAGVARIABLE(LuauAutocompleteFunctionArglistSuggestion) +LUAU_FASTFLAGVARIABLE(LuauAutocompleteMetatableInheritance) +LUAU_FASTFLAGVARIABLE(LuauAutocompleteSkipErrorTypeInUnion) +LUAU_FASTFLAGVARIABLE(LuauCheckTypeForDeprecated) + +static constexpr std::array kStatementStartingKeywords_DEPRECATED = {"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; +static constexpr std::array kStatementStartingKeywords_CONST = + {"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export", "const"}; + +static constexpr std::array kStatementStartingKeywords_EXPORT = + {"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export", "const", "export"}; + static constexpr std::array kHotComments = {"nolint", "nocheck", "nonstrict", "strict", "optimize", "native"}; static const std::string kKnownAttributes[] = {"checked", "deprecated", "native"}; @@ -126,7 +137,7 @@ static std::optional findExpectedTypeAt(const Module& module, AstNode* n if (AstExprCall* exprCall = expr->as()) { if ((exprCall->args.size == 0 && exprCall->argLocation.contains(position)) || - (FFlag::LuauAutocompleteFunctionCallArgTails2 && exprCall->args.size > 0 && (*exprCall->args.begin())->as())) + (exprCall->args.size > 0 && (*exprCall->args.begin())->as())) { auto it = module.astTypes.find(exprCall->func); @@ -143,7 +154,7 @@ static std::optional findExpectedTypeAt(const Module& module, AstNode* n if (index < head.size()) return head[index]; - else if (FFlag::LuauAutocompleteFunctionCallArgTails2 && index == head.size() && tail.has_value() && isVariadic(*tail)) + else if (index == head.size() && tail.has_value() && isVariadic(*tail)) return first(*tail); return std::nullopt; @@ -249,6 +260,20 @@ static TypeCorrectKind checkTypeCorrectKind( return checkTypeMatch(module, ty, expectedType, moduleScope, typeArena, builtinTypes) ? TypeCorrectKind::Correct : TypeCorrectKind::None; } +static bool isTypeDeprecated(TypeId ty) +{ + LUAU_ASSERT(FFlag::LuauCheckTypeForDeprecated); + ty = follow(ty); + + if (const auto ftv = get(ty); ftv && ftv->isDeprecatedFunction) + return true; + + if (const auto itv = get(ty)) + return std::all_of(itv->parts.begin(), itv->parts.end(), isTypeDeprecated); + + return false; +} + enum class PropIndexType { Point, @@ -256,6 +281,25 @@ enum class PropIndexType Key, }; +/** + * When we perform autocomplete on a type, if we encounter a union we often + * need to provide the intersection of the union's options. However, for UX + * reasons we skip over types like `nil` and `never`. If we didn't, then + * something like ... + * + * local function foobar(tbl: { prop: number }?) + * return tbl.| + * end + * + * ... would never provide autocomplete options, even though `prop` is a + * reasonable option, even in strict mode. + */ +static bool isSkippableTypeInUnion(TypeId ty) +{ + ty = follow(ty); + return isNil(ty) || is(ty) || is(ty); +} + static void autocompleteProps( const Module& module, TypeArena* typeArena, @@ -364,10 +408,10 @@ static void autocompleteProps( if (result.count(name) == 0 && name != kParseNameError) { Luau::TypeId type; - if (auto ty = prop.readTy) - type = follow(*ty); - else - continue; + if (auto ty = prop.readTy) + type = follow(*ty); + else + continue; TypeCorrectKind typeCorrect = indexType == PropIndexType::Key ? TypeCorrectKind::Correct @@ -379,7 +423,7 @@ static void autocompleteProps( result[name] = AutocompleteEntry{ AutocompleteEntryKind::Property, type, - prop.deprecated, + prop.deprecated || (FFlag::LuauCheckTypeForDeprecated && isTypeDeprecated(type)), isWrongIndexer(type), typeCorrect, containingExternType, @@ -399,42 +443,21 @@ static void autocompleteProps( auto indexIt = mtable->props.find("__index"); if (indexIt != mtable->props.end()) { - if (FFlag::LuauACOnMTTWriteOnlyPropNoCrash) - { - TypeId followed = indexIt->second.readTy.value_or(nullptr); - if (followed == nullptr) - return; - followed = follow(followed); - LUAU_ASSERT(followed); + TypeId followed = indexIt->second.readTy.value_or(nullptr); + if (followed == nullptr) + return; + followed = follow(followed); + LUAU_ASSERT(followed); - if (get(followed) || get(followed)) - { - autocompleteProps(module, typeArena, builtinTypes, rootTy, followed, indexType, nodes, result, seen); - } - else if (auto indexFunction = get(followed)) - { - std::optional indexFunctionResult = first(indexFunction->retTypes); - if (indexFunctionResult) - autocompleteProps(module, typeArena, builtinTypes, rootTy, *indexFunctionResult, indexType, nodes, result, seen); - } + if (get(followed) || get(followed)) + { + autocompleteProps(module, typeArena, builtinTypes, rootTy, followed, indexType, nodes, result, seen); } - else + else if (auto indexFunction = get(followed)) { - TypeId followed; - if (auto propTy = indexIt->second.readTy) - { - followed = follow(*propTy); - } - if (get(followed) || get(followed)) - { - autocompleteProps(module, typeArena, builtinTypes, rootTy, followed, indexType, nodes, result, seen); - } - else if (auto indexFunction = get(followed)) - { - std::optional indexFunctionResult = first(indexFunction->retTypes); - if (indexFunctionResult) - autocompleteProps(module, typeArena, builtinTypes, rootTy, *indexFunctionResult, indexType, nodes, result, seen); - } + std::optional indexFunctionResult = first(indexFunction->retTypes); + if (indexFunctionResult) + autocompleteProps(module, typeArena, builtinTypes, rootTy, *indexFunctionResult, indexType, nodes, result, seen); } } }; @@ -465,7 +488,9 @@ static void autocompleteProps( { autocompleteProps(module, typeArena, builtinTypes, rootTy, mt->table, indexType, nodes, result, seen); - if (auto mtable = get(follow(mt->metatable))) + const TableType* mtable = + FFlag::LuauAutocompleteMetatableInheritance ? getTableType(follow(mt->metatable)) : get(follow(mt->metatable)); + if (mtable) fillMetatableProps(mtable); } else if (auto i = get(ty)) @@ -488,12 +513,20 @@ static void autocompleteProps( auto iter = begin(u); auto endIter = end(u); - while (iter != endIter) + if (FFlag::LuauAutocompleteSkipErrorTypeInUnion) { - if (isNil(*iter)) + while (iter != endIter && isSkippableTypeInUnion(*iter)) ++iter; - else - break; + } + else + { + while (iter != endIter) + { + if (isNil(*iter)) + ++iter; + else + break; + } } if (iter == endIter) @@ -520,10 +553,21 @@ static void autocompleteProps( innerSeen.insert(ty); } - if (isNil(*iter)) + if (FFlag::LuauAutocompleteSkipErrorTypeInUnion) { - ++iter; - continue; + if (isSkippableTypeInUnion(*iter)) + { + ++iter; + continue; + } + } + else + { + if (isNil(*iter)) + { + ++iter; + continue; + } } autocompleteProps(module, typeArena, builtinTypes, rootTy, *iter, indexType, nodes, inner, innerSeen); @@ -661,6 +705,11 @@ static void autocompleteStringSingleton(TypeId ty, bool addQuotes, AstNode* node } } } + else if (auto ity = get(ty)) + { + for (auto el : ity->parts) + autocompleteStringSingleton(el, addQuotes, node, position, result); + } }; static bool canSuggestInferredType(TypeId ty) @@ -1335,10 +1384,11 @@ static AutocompleteEntryMap autocompleteStatement( std::string n = toString(name); if (!result.count(n)) + { result[n] = { AutocompleteEntryKind::Binding, binding.typeId, - binding.deprecated, + binding.deprecated || (FFlag::LuauCheckTypeForDeprecated && isTypeDeprecated(binding.typeId)), false, TypeCorrectKind::None, std::nullopt, @@ -1347,16 +1397,37 @@ static AutocompleteEntryMap autocompleteStatement( {}, getParenRecommendation(binding.typeId, ancestry, TypeCorrectKind::None) }; + } } scope = scope->parent; } bool shouldIncludeBreakAndContinue = isValidBreakContinueContext(ancestry, position); - for (const std::string_view kw : kStatementStartingKeywords) + + if (FFlag::LuauExportValueSyntax && FFlag::LuauAutocompleteExport) { - if ((kw != "break" && kw != "continue") || shouldIncludeBreakAndContinue) - result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword}); + for (const std::string_view kw : kStatementStartingKeywords_EXPORT) + { + if ((kw != "break" && kw != "continue") || shouldIncludeBreakAndContinue) + result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } + } + else if (FFlag::LuauAutocompleteConst) + { + for (const std::string_view kw : kStatementStartingKeywords_CONST) + { + if ((kw != "break" && kw != "continue") || shouldIncludeBreakAndContinue) + result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } + } + else + { + for (const std::string_view kw : kStatementStartingKeywords_DEPRECATED) + { + if ((kw != "break" && kw != "continue") || shouldIncludeBreakAndContinue) + result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword}); + } } for (auto it = ancestry.rbegin(); it != ancestry.rend(); ++it) @@ -1518,7 +1589,7 @@ static AutocompleteContext autocompleteExpression( result[n] = { AutocompleteEntryKind::Binding, binding.typeId, - binding.deprecated, + binding.deprecated || (FFlag::LuauCheckTypeForDeprecated && isTypeDeprecated(binding.typeId)), false, typeCorrect, std::nullopt, @@ -1758,14 +1829,15 @@ static AutocompleteResult autocompleteWhileLoopKeywords(std::vector an return {std::move(ret), std::move(ancestry), AutocompleteContext::Keyword}; } -static std::string makeAnonymous(const ScopePtr& scope, const FunctionType& funcTy) +// Builds the argument parameter list string (what goes between the parentheses of a function expression). +// e.g. for (number, string) -> () returns "a0: number, a1: string" +static std::string makeAnonymousArgList(const ScopePtr& scope, const FunctionType& funcTy) { - std::string result = "function("; + std::string result; auto [args, tail] = Luau::flatten(funcTy.argTypes); bool first = true; - // Skip the implicit 'self' argument if call is indexed with ':' for (size_t argIdx = 0; argIdx < args.size(); ++argIdx) { if (!first) @@ -1803,6 +1875,61 @@ static std::string makeAnonymous(const ScopePtr& scope, const FunctionType& func result += "..."; } + return result; +} + +static std::string makeAnonymous(const ScopePtr& scope, const FunctionType& funcTy) +{ + std::string result = "function("; + + if (FFlag::LuauAutocompleteFunctionArglistSuggestion) + { + result += makeAnonymousArgList(scope, funcTy); + } + else + { + auto [args, tail] = Luau::flatten(funcTy.argTypes); + + bool first = true; + // Skip the implicit 'self' argument if call is indexed with ':' + for (size_t argIdx = 0; argIdx < args.size(); ++argIdx) + { + if (!first) + result += ", "; + else + first = false; + + std::string name; + if (argIdx < funcTy.argNames.size() && funcTy.argNames[argIdx]) + name = funcTy.argNames[argIdx]->name; + else + name = "a" + std::to_string(argIdx); + + if (std::optional type = tryGetTypeNameInScope(scope, args[argIdx], true)) + result += name + ": " + *type; + else + result += name; + } + + if (tail && (Luau::isVariadic(*tail) || Luau::get(Luau::follow(*tail)))) + { + if (!first) + result += ", "; + + std::optional varArgType; + if (const VariadicTypePack* pack = get(follow(*tail))) + { + if (std::optional res = tryGetTypeNameInScope(scope, pack->ty, true)) + varArgType = std::move(res); + } + + if (varArgType) + result += "...: " + *varArgType; + else + result += "..."; + } + } + result += ")"; auto [rets, retTail] = Luau::flatten(funcTy.retTypes); @@ -1892,7 +2019,15 @@ static std::optional makeAnonymousAutofilled( entry.kind = AutocompleteEntryKind::GeneratedFunction; entry.typeCorrect = TypeCorrectKind::Correct; entry.type = argType; - entry.insertText = makeAnonymous(scope, *type); + // When the cursor is inside the arg list of an already-typed "function(...)" (argLocation is set), + // only suggest the parameter list — not the full "function(...) end" expression. + // If argLocation is absent the user has typed the "function" keyword but not yet the "(", so + // the full expression is still the correct completion. + const AstExprFunction* exprFunc = node->as(); + if (FFlag::LuauAutocompleteFunctionArglistSuggestion && exprFunc && exprFunc->argLocation.has_value()) + entry.insertText = makeAnonymousArgList(scope, *type); + else + entry.insertText = makeAnonymous(scope, *type); return std::make_optional(std::move(entry)); } diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 481f6060..05712a67 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -10,7 +10,6 @@ #include "Luau/DenseHash.h" #include "Luau/Error.h" #include "Luau/Frontend.h" -#include "Luau/InferPolarity.h" #include "Luau/Module.h" #include "Luau/NotNull.h" #include "Luau/Subtyping.h" @@ -25,19 +24,14 @@ #include #include +LUAU_FASTFLAG(DebugLuauCyclicRequireTypeInference) + /** FIXME: Many of these type definitions are not quite completely accurate. * * Some of them require richer generics than we have. For instance, we do not yet have a way to talk * about a function that takes any number of values, but where each value must have some specific type. */ -LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType4) -LUAU_FASTFLAGVARIABLE(LuauCloneForIntersectionsUnions) -LUAU_FASTFLAG(LuauStorePolarityInline) -LUAU_FASTFLAGVARIABLE(LuauTableFreezeCheckIsSubtype) -LUAU_FASTFLAG(LuauAnalysisUsesSolverMode) - namespace Luau { @@ -49,7 +43,7 @@ struct MagicSelect final : MagicFunction const class AstExprCall&, WithPredicate ) override; - bool infer(const MagicFunctionCallContext& ctx) override; + bool infer(const MagicFunctionCallContext& context) override; }; struct MagicSetMetatable final : MagicFunction @@ -82,10 +76,10 @@ struct MagicPack final : MagicFunction const class AstExprCall&, WithPredicate ) override; - bool infer(const MagicFunctionCallContext& ctx) override; + bool infer(const MagicFunctionCallContext& context) override; }; -struct MagicRequire final : MagicFunction +struct MagicClone final : MagicFunction { std::optional> handleOldSolver( struct TypeChecker&, @@ -93,10 +87,10 @@ struct MagicRequire final : MagicFunction const class AstExprCall&, WithPredicate ) override; - bool infer(const MagicFunctionCallContext& ctx) override; + bool infer(const MagicFunctionCallContext& context) override; }; -struct MagicClone final : MagicFunction +struct MagicFreeze final : MagicFunction { std::optional> handleOldSolver( struct TypeChecker&, @@ -104,10 +98,11 @@ struct MagicClone final : MagicFunction const class AstExprCall&, WithPredicate ) override; - bool infer(const MagicFunctionCallContext& ctx) override; + bool infer(const MagicFunctionCallContext& context) override; + bool typeCheck(const MagicFunctionTypeCheckContext& ctx) override; }; -struct MagicFreeze final : MagicFunction +struct MagicFormat final : MagicFunction { std::optional> handleOldSolver( struct TypeChecker&, @@ -115,11 +110,11 @@ struct MagicFreeze final : MagicFunction const class AstExprCall&, WithPredicate ) override; - bool infer(const MagicFunctionCallContext& ctx) override; - bool typeCheck(const MagicFunctionTypeCheckContext& ctx) override; + bool infer(const MagicFunctionCallContext& context) override; + bool typeCheck(const MagicFunctionTypeCheckContext& context) override; }; -struct MagicFormat final : MagicFunction +struct MagicMatch final : MagicFunction { std::optional> handleOldSolver( struct TypeChecker&, @@ -127,11 +122,10 @@ struct MagicFormat final : MagicFunction const class AstExprCall&, WithPredicate ) override; - bool infer(const MagicFunctionCallContext& ctx) override; - bool typeCheck(const MagicFunctionTypeCheckContext& ctx) override; + bool infer(const MagicFunctionCallContext& context) override; }; -struct MagicMatch final : MagicFunction +struct MagicGmatch final : MagicFunction { std::optional> handleOldSolver( struct TypeChecker&, @@ -139,10 +133,10 @@ struct MagicMatch final : MagicFunction const class AstExprCall&, WithPredicate ) override; - bool infer(const MagicFunctionCallContext& ctx) override; + bool infer(const MagicFunctionCallContext& context) override; }; -struct MagicGmatch final : MagicFunction +struct MagicFind final : MagicFunction { std::optional> handleOldSolver( struct TypeChecker&, @@ -150,10 +144,10 @@ struct MagicGmatch final : MagicFunction const class AstExprCall&, WithPredicate ) override; - bool infer(const MagicFunctionCallContext& ctx) override; + bool infer(const MagicFunctionCallContext& context) override; }; -struct MagicFind final : MagicFunction +struct MagicPcall final : MagicFunction { std::optional> handleOldSolver( struct TypeChecker&, @@ -297,8 +291,6 @@ void addGlobalBinding(GlobalTypes& globals, const ScopePtr& scope, const std::st void addGlobalBinding(GlobalTypes& globals, const ScopePtr& scope, const std::string& name, Binding binding) { - if (!FFlag::LuauStorePolarityInline) - inferGenericPolarities_DEPRECATED(NotNull{&globals.globalTypes}, NotNull{scope.get()}, binding.typeId); scope->bindings[globals.globalNames.names->getOrAdd(name.c_str())] = binding; } @@ -372,10 +364,8 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC ); LUAU_ASSERT(loadResult.success); - TypeId genericK = - FFlag::LuauStorePolarityInline ? arena.addType(GenericType{globalScope, "K", Polarity::Mixed}) : arena.addType(GenericType{globalScope, "K"}); - TypeId genericV = - FFlag::LuauStorePolarityInline ? arena.addType(GenericType{globalScope, "V", Polarity::Mixed}) : arena.addType(GenericType{globalScope, "V"}); + TypeId genericK = arena.addType(GenericType{globalScope, "K", Polarity::Mixed}); + TypeId genericV = arena.addType(GenericType{globalScope, "V", Polarity::Mixed}); TypeId mapOfKtoV = arena.addType(TableType{{}, TableIndexer(genericK, genericV), globals.globalScope->level, TableState::Generic}); std::optional stringMetatableTy = getMetatable(builtinTypes->stringType, builtinTypes); @@ -424,16 +414,14 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC // pairs(t: Table) -> ((Table, K?) -> (K, V), Table, nil) addGlobalBinding(globals, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); - TypeId genericMT = FFlag::LuauStorePolarityInline ? arena.addType(GenericType{globalScope, "MT", Polarity::Mixed}) - : arena.addType(GenericType{globalScope, "MT"}); + TypeId genericMT = arena.addType(GenericType{globalScope, "MT", Polarity::Mixed}); TableType tab{TableState::Generic, globals.globalScope->level}; TypeId tabTy = arena.addType(std::move(tab)); TypeId tableMetaMT = arena.addType(MetatableType{tabTy, genericMT}); - TypeId genericT = - FFlag::LuauStorePolarityInline ? arena.addType(GenericType{globalScope, "T", Polarity::Mixed}) : arena.addType(GenericType{globalScope, "T"}); + TypeId genericT = arena.addType(GenericType{globalScope, "T", Polarity::Mixed}); if (frontend.getLuauSolverMode() == SolverMode::New) { @@ -475,12 +463,12 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC finalizeGlobalBindings(globals.globalScope); attachMagicFunction(getGlobalBinding(globals, "assert"), std::make_shared()); + attachMagicFunction(getGlobalBinding(globals, "pcall"), std::make_shared()); if (frontend.getLuauSolverMode() == SolverMode::New) { // declare function assert(value: T, errorMessage: string?): intersect - TypeId genericT = FFlag::LuauStorePolarityInline ? arena.addType(GenericType{globalScope, "T", Polarity::Mixed}) - : arena.addType(GenericType{globalScope, "T"}); + TypeId genericT = arena.addType(GenericType{globalScope, "T", Polarity::Mixed}); TypeId refinedTy = arena.addType( TypeFunctionInstanceType{ @@ -508,20 +496,13 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC // the top table type. We do the best we can by modelling these // functions using unconstrained generics. It's not quite right, // but it'll be ok for now. - TypeId genericTy = FFlag::LuauStorePolarityInline ? arena.addType(GenericType{globalScope, "T", Polarity::Mixed}) - : arena.addType(GenericType{globalScope, "T"}); + TypeId genericTy = arena.addType(GenericType{globalScope, "T", Polarity::Mixed}); TypePackId thePack = arena.addTypePack({genericTy}); TypeId idTyWithMagic = arena.addType(FunctionType{{genericTy}, {}, thePack, thePack}); ttv->props["freeze"] = makeProperty(idTyWithMagic, "@luau/global/table.freeze"); - if (!FFlag::LuauStorePolarityInline) - inferGenericPolarities_DEPRECATED(NotNull{&globals.globalTypes}, NotNull{globalScope}, idTyWithMagic); - TypeId idTy = arena.addType(FunctionType{{genericTy}, {}, thePack, thePack}); - if (!FFlag::LuauStorePolarityInline) - inferGenericPolarities_DEPRECATED(NotNull{&globals.globalTypes}, NotNull{globalScope}, idTy); - ttv->props["clone"] = makeProperty(idTy, "@luau/global/table.clone"); // ServerLua: shrink table to optimal size ttv->props["shrink"] = makeProperty(idTy, "@luau/global/table.shrink"); @@ -541,8 +522,7 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC ttv->props["foreachi"].deprecated = true; attachMagicFunction(*ttv->props["pack"].readTy, std::make_shared()); - if (FFlag::LuauTableCloneClonesType4) - attachMagicFunction(*ttv->props["clone"].readTy, std::make_shared()); + attachMagicFunction(*ttv->props["clone"].readTy, std::make_shared()); attachMagicFunction(*ttv->props["freeze"].readTy, std::make_shared()); } @@ -646,7 +626,7 @@ std::optional> MagicFormat::handleOldSolver( { auto [paramPack, _predicates] = std::move(withPredicate); - TypeArena& arena = typechecker.currentModule->internalTypes; + TypeArena& arena = *typechecker.currentModule->internalTypes; AstExprConstantString* fmt = nullptr; if (auto index = expr.func->as(); index && expr.self) @@ -736,7 +716,12 @@ bool MagicFormat::infer(const MagicFunctionCallContext& context) size_t numExpectedParams = expected.size() + 1; // + 1 for the format string if (numExpectedParams != numActualParams && (!tail || numExpectedParams < numActualParams)) - context.solver->reportError(TypeError{context.callSite->location, CountMismatch{numExpectedParams, std::nullopt, numActualParams}}); + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + context.solver->reportError(CountMismatch{numExpectedParams, std::nullopt, numActualParams}, context.callSite->location, *context.constraint->moduleName); + else + context.solver->DEPRECATED_reportError(TypeError{context.callSite->location, CountMismatch{numExpectedParams, std::nullopt, numActualParams}}); + } // This is invoked at solve time, so we just need to provide a type for the result of :/.format TypePackId resultPack = arena->addTypePack({context.solver->builtinTypes->stringType}); @@ -778,10 +763,7 @@ bool MagicFormat::typeCheck(const MagicFunctionTypeCheckContext& context) } if (!formatString) - { - context.typechecker->reportError(CannotCheckDynamicStringFormatCalls{}, context.callSite->location); return true; - } // CLI-150726: The block below effectively constructs a type pack and then type checks it by going parameter-by-parameter. // This does _not_ handle cases like: @@ -900,7 +882,7 @@ std::optional> MagicGmatch::handleOldSolver( if (params.size() != 2) return std::nullopt; - TypeArena& arena = typechecker.currentModule->internalTypes; + TypeArena& arena = *typechecker.currentModule->internalTypes; AstExprConstantString* pattern = nullptr; size_t index = expr.self ? 0 : 1; @@ -969,7 +951,7 @@ std::optional> MagicMatch::handleOldSolver( if (params.size() < 2 || params.size() > 3) return std::nullopt; - TypeArena& arena = typechecker.currentModule->internalTypes; + TypeArena& arena = *typechecker.currentModule->internalTypes; AstExprConstantString* pattern = nullptr; size_t patternIndex = expr.self ? 0 : 1; @@ -1045,7 +1027,7 @@ std::optional> MagicFind::handleOldSolver( if (params.size() < 2 || params.size() > 4) return std::nullopt; - TypeArena& arena = typechecker.currentModule->internalTypes; + TypeArena& arena = *typechecker.currentModule->internalTypes; AstExprConstantString* pattern = nullptr; size_t patternIndex = expr.self ? 0 : 1; @@ -1144,6 +1126,42 @@ bool MagicFind::infer(const MagicFunctionCallContext& context) return true; } +std::optional> MagicPcall::handleOldSolver( + TypeChecker& typechecker, + const ScopePtr& scope, + const AstExprCall& expr, + WithPredicate withPredicate +) +{ + // pcall() is only magic in the new solver. + return std::nullopt; +} + +// In the specific case that pcall's first argument returns 0 values, the result +// of pcall is itself (boolean, unknown) Else treat it as an ordinary function +// per its type in EmbeddedBuiltinDefinitions.cpp +bool MagicPcall::infer(const MagicFunctionCallContext& ctx) +{ + const auto [argHead, argTail] = flatten(ctx.arguments); + + if (argHead.empty()) + return false; + + TypeId fnTy = follow(argHead[0]); + const FunctionType* fn = get(fnTy); + if (!fn) + return false; + + const auto [fnReturnHead, fnReturnTail] = flatten(fn->retTypes); + if (!fnReturnHead.empty() || fnReturnTail.has_value()) + return false; + + TypePackId res = ctx.solver->arena->addTypePack({ctx.solver->builtinTypes->booleanType, ctx.solver->builtinTypes->unknownType}); + asMutable(ctx.result)->ty.emplace(res); + + return true; +} + TypeId makeStringMetatable(NotNull builtinTypes, SolverMode mode) { NotNull arena{builtinTypes->arena.get()}; @@ -1160,10 +1178,7 @@ TypeId makeStringMetatable(NotNull builtinTypes, SolverMode mode) const TypePackId oneStringPack = arena->addTypePack({stringType}); const TypePackId anyTypePack = builtinTypes->anyTypePack; - const TypePackId variadicTailPack = FFlag::LuauAnalysisUsesSolverMode ? (mode == SolverMode::New ? builtinTypes->unknownTypePack : anyTypePack) - : mode == SolverMode::New ? builtinTypes->unknownTypePack - : FFlag::LuauSolverV2 ? builtinTypes->unknownTypePack - : anyTypePack; + const TypePackId variadicTailPack = mode == SolverMode::New ? builtinTypes->unknownTypePack : anyTypePack; const TypePackId emptyPack = arena->addTypePack({}); const TypePackId stringVariadicList = arena->addTypePack(TypePackVar{VariadicTypePack{stringType}}); const TypePackId numberVariadicList = arena->addTypePack(TypePackVar{VariadicTypePack{numberType}}); @@ -1298,7 +1313,7 @@ std::optional> MagicSelect::handleOldSolver( if (size_t(offset) < v.size()) { std::vector result(v.begin() + offset, v.end()); - return WithPredicate{typechecker.currentModule->internalTypes.addTypePack(TypePack{std::move(result), tail})}; + return WithPredicate{typechecker.currentModule->internalTypes->addTypePack(TypePack{std::move(result), tail})}; } else if (tail) return WithPredicate{*tail}; @@ -1309,7 +1324,7 @@ std::optional> MagicSelect::handleOldSolver( else if (AstExprConstantString* str = arg1->as()) { if (str->value.size == 1 && str->value.data[0] == '#') - return WithPredicate{typechecker.currentModule->internalTypes.addTypePack({typechecker.numberType})}; + return WithPredicate{typechecker.currentModule->internalTypes->addTypePack({typechecker.numberType})}; } return std::nullopt; @@ -1319,7 +1334,10 @@ bool MagicSelect::infer(const MagicFunctionCallContext& context) { if (context.callSite->args.size <= 0) { - context.solver->reportError(TypeError{context.callSite->location, GenericError{"select should take 1 or more arguments"}}); + if (FFlag::DebugLuauCyclicRequireTypeInference) + context.solver->reportError(GenericError{"select should take 1 or more arguments"}, context.callSite->location, *context.constraint->moduleName); + else + context.solver->DEPRECATED_reportError(TypeError{context.callSite->location, GenericError{"select should take 1 or more arguments"}}); return false; } @@ -1372,7 +1390,7 @@ std::optional> MagicSetMetatable::handleOldSolver( if (size(paramPack) < 2 && finite(paramPack)) return std::nullopt; - TypeArena& arena = typechecker.currentModule->internalTypes; + TypeArena& arena = *typechecker.currentModule->internalTypes; std::vector expectedArgs = typechecker.unTypePack(scope, paramPack, 2, expr.location); @@ -1456,7 +1474,7 @@ std::optional> MagicAssert::handleOldSolver( { auto [paramPack, predicates] = std::move(withPredicate); - TypeArena& arena = typechecker.currentModule->internalTypes; + TypeArena& arena = *typechecker.currentModule->internalTypes; auto [head, tail] = flatten(paramPack); if (head.empty() && tail) @@ -1495,7 +1513,7 @@ std::optional> MagicPack::handleOldSolver( { auto [paramPack, _predicates] = std::move(withPredicate); - TypeArena& arena = typechecker.currentModule->internalTypes; + TypeArena& arena = *typechecker.currentModule->internalTypes; const auto& [paramTypes, paramTail] = flatten(paramPack); @@ -1578,11 +1596,9 @@ std::optional> MagicClone::handleOldSolver( WithPredicate withPredicate ) { - LUAU_ASSERT(FFlag::LuauTableCloneClonesType4); - auto [paramPack, _predicates] = std::move(withPredicate); - TypeArena& arena = typechecker.currentModule->internalTypes; + TypeArena& arena = *typechecker.currentModule->internalTypes; // in the old solver, nonstrict in particular is really bad about inferring `...any` for things that are definitely present // and the only real way for us to deal with this is to just be more permissive here @@ -1595,22 +1611,14 @@ std::optional> MagicClone::handleOldSolver( TypeId inputType = follow(paramTypes[0]); - if (FFlag::LuauCloneForIntersectionsUnions) - { - if (!get(inputType) && !get(inputType)) - return std::nullopt; + if (!get(inputType) && !get(inputType)) + return std::nullopt; - if (auto intersectionTy = get(inputType)) - { - for (auto ty : intersectionTy) - if (!get(ty)) - return std::nullopt; - } - } - else + if (auto intersectionTy = get(inputType)) { - if (!get(inputType)) - return std::nullopt; + for (auto ty : intersectionTy) + if (!get(ty)) + return std::nullopt; } CloneState cloneState{typechecker.builtinTypes}; @@ -1622,14 +1630,15 @@ std::optional> MagicClone::handleOldSolver( bool MagicClone::infer(const MagicFunctionCallContext& context) { - LUAU_ASSERT(FFlag::LuauTableCloneClonesType4); - TypeArena* arena = context.solver->arena; const auto& [paramTypes, paramTail] = flatten(context.arguments); if (paramTypes.empty() || context.callSite->args.size == 0) { - context.solver->reportError(CountMismatch{1, std::nullopt, 0}, context.callSite->argLocation); + if (FFlag::DebugLuauCyclicRequireTypeInference) + context.solver->reportError(CountMismatch{1, std::nullopt, 0}, context.callSite->argLocation, *context.constraint->moduleName); + else + context.solver->DEPRECATED_reportError(CountMismatch{1, std::nullopt, 0}, context.callSite->argLocation); return false; } @@ -1695,10 +1704,6 @@ static std::optional freezeTable(TypeId inputType, const MagicFunctionCa return resultType; } - if (!FFlag::LuauTableFreezeCheckIsSubtype) - { - context.solver->reportError(TypeMismatch{context.solver->builtinTypes->tableType, inputType}, context.callSite->argLocation); - } return std::nullopt; } @@ -1760,9 +1765,6 @@ bool MagicFreeze::infer(const MagicFunctionCallContext& context) // `table` and returns a read-only version of that table). bool MagicFreeze::typeCheck(const MagicFunctionTypeCheckContext& ctx) { - if (!FFlag::LuauTableFreezeCheckIsSubtype) - return false; - const auto& [paramTypes, paramTail] = flatten(ctx.arguments); if (paramTypes.size() < 1 && !paramTail) @@ -1795,7 +1797,7 @@ bool MagicFreeze::typeCheck(const MagicFunctionTypeCheckContext& ctx) { // If we can't get a type from the type or type pack, we testIsSubtype against the entire context's argument type pack to report a Type Pack // Mismatch error. - TypePackId tableTyPack = ctx.typechecker->module->internalTypes.addTypePack({ctx.typechecker->builtinTypes->tableType}); + TypePackId tableTyPack = ctx.typechecker->module->internalTypes->addTypePack({ctx.typechecker->builtinTypes->tableType}); ctx.typechecker->testIsSubtype(follow(ctx.arguments), tableTyPack, ctx.callSite->location); return true; } @@ -1838,7 +1840,7 @@ std::optional> MagicRequire::handleOldSolver( WithPredicate withPredicate ) { - TypeArena& arena = typechecker.currentModule->internalTypes; + TypeArena& arena = *typechecker.currentModule->internalTypes; if (expr.args.size != 1) { @@ -1855,7 +1857,30 @@ std::optional> MagicRequire::handleOldSolver( return std::nullopt; } -static bool checkRequirePathDcr(NotNull solver, AstExpr* expr) +static bool checkRequirePathNewSolver(NotNull solver, AstExpr* expr, const ModuleName& moduleName) +{ + // require(foo.parent.bar) will technically work, but it depends on legacy goop that + // Luau does not and could not support without a bunch of work. It's deprecated anyway, so + // we'll warn here if we see it. + bool good = true; + AstExprIndexName* indexExpr = expr->as(); + + while (indexExpr) + { + if (indexExpr->index == "parent") + { + solver->reportError(DeprecatedApiUsed{"parent", "Parent"}, indexExpr->indexLocation, moduleName); + good = false; + } + + indexExpr = indexExpr->expr->as(); + } + + return good; +} + +// Clip with DebugLuauCyclicRequireTypeInference +static bool DEPRECATED_checkRequirePathDcr(NotNull solver, AstExpr* expr) { // require(foo.parent.bar) will technically work, but it depends on legacy goop that // Luau does not and could not support without a bunch of work. It's deprecated anyway, so @@ -1867,7 +1892,7 @@ static bool checkRequirePathDcr(NotNull solver, AstExpr* expr) { if (indexExpr->index == "parent") { - solver->reportError(DeprecatedApiUsed{"parent", "Parent"}, indexExpr->indexLocation); + solver->DEPRECATED_reportError(DeprecatedApiUsed{"parent", "Parent"}, indexExpr->indexLocation); good = false; } @@ -1881,16 +1906,33 @@ bool MagicRequire::infer(const MagicFunctionCallContext& context) { if (context.callSite->args.size != 1) { - context.solver->reportError(GenericError{"require takes 1 argument"}, context.callSite->location); + if (FFlag::DebugLuauCyclicRequireTypeInference) + context.solver->reportError(GenericError{"require takes 1 argument"}, context.callSite->location, *context.constraint->moduleName); + else + context.solver->DEPRECATED_reportError(GenericError{"require takes 1 argument"}, context.callSite->location); return false; } - if (!checkRequirePathDcr(context.solver, context.callSite->args.data[0])) - return false; + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + if (!checkRequirePathNewSolver(context.solver, context.callSite->args.data[0], *context.constraint->moduleName)) + return false; + } + else + { + if (!DEPRECATED_checkRequirePathDcr(context.solver, context.callSite->args.data[0])) + return false; + } + + const ModuleName& resolveFrom = FFlag::DebugLuauCyclicRequireTypeInference + ? *context.constraint->moduleName + : context.solver->module->name; - if (auto moduleInfo = context.solver->moduleResolver->resolveModuleInfo(context.solver->module->name, *context.callSite)) + if (auto moduleInfo = context.solver->moduleResolver->resolveModuleInfo(resolveFrom, *context.callSite)) { - TypeId moduleType = context.solver->resolveModule(*moduleInfo, context.callSite->location); + TypeId moduleType = FFlag::DebugLuauCyclicRequireTypeInference + ? context.solver->resolveModule(*moduleInfo, context.callSite->location, *context.constraint->moduleName) + : context.solver->DEPRECATED_resolveModule(*moduleInfo, context.callSite->location); TypePackId moduleResult = context.solver->arena->addTypePack({moduleType}); asMutable(context.result)->ty.emplace(moduleResult); diff --git a/Analysis/src/BuiltinTypeFunctions.cpp b/Analysis/src/BuiltinTypeFunctions.cpp index bc1a8102..fddaa6c8 100644 --- a/Analysis/src/BuiltinTypeFunctions.cpp +++ b/Analysis/src/BuiltinTypeFunctions.cpp @@ -5,7 +5,7 @@ #include "Luau/Common.h" #include "Luau/ConstraintSolver.h" #include "Luau/Instantiation.h" -#include "Luau/OverloadResolution.h" +#include "Luau/OverloadResolver.h" #include "Luau/Scope.h" #include "Luau/Simplify.h" #include "Luau/Subtyping.h" @@ -20,9 +20,11 @@ LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) LUAU_DYNAMIC_FASTINTVARIABLE(LuauStepRefineRecursionLimit, 64) -LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity2) -LUAU_FASTFLAGVARIABLE(LuauBuiltinTypeFunctionsUseNewOverloadResolution) -LUAU_FASTFLAGVARIABLE(LuauTypeFunctionsUseSolveFunctionCall) +LUAU_FASTFLAGVARIABLE(LuauConcatDoesntAlwaysReturnString) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAG(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) +LUAU_FASTFLAG(LuauRemoveExtraSubtypingInstances) +LUAU_FASTFLAG(DebugLuauCyclicRequireTypeInference) namespace Luau { @@ -110,10 +112,8 @@ std::optional> tryDistributeTypeFunctionApp( } ); - if (ctx->solver) - ctx->pushConstraint(ReduceConstraint{resultTy}); - - return {{resultTy, Reduction::MaybeOk, {}, {}, {}, {}, {resultTy}}}; + ctx->freshInstances.emplace_back(resultTy); + return {{resultTy, Reduction::MaybeOk}}; } return std::nullopt; @@ -138,7 +138,7 @@ static std::optional solveFunctionCall(NotNull if (!selected.overload.has_value()) return std::nullopt; - TypePackId retPack = ctx->arena->freshTypePack(ctx->scope); + TypePackId retPack = ctx->arena->freshTypePack(ctx->scope, Polarity::Positive); TypeId prospectiveFunction = ctx->arena->addType(FunctionType{argsPack, retPack}); // FIXME: It's too bad that we have to bust out the Unifier here. We should @@ -161,20 +161,36 @@ static std::optional solveFunctionCall(NotNull return std::nullopt; } - LUAU_ASSERT(FFlag::LuauInstantiationUsesGenericPolarity2); - if (!unifier.genericSubstitutions.empty() || !unifier.genericPackSubstitutions.empty()) { - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; + Subtyping subtyping_DEPRECATED{ctx->builtins, ctx->arena, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; + auto newRetTp = getApproximateReturnTypeForFunctionCall(*selected.overload).value_or(ctx->builtins->errorTypePack); + std::optional subst = instantiate2( - ctx->arena, std::move(unifier.genericSubstitutions), std::move(unifier.genericPackSubstitutions), NotNull{&subtyping}, ctx->scope, retPack + ctx->arena, + std::move(unifier.genericSubstitutions), + std::move(unifier.genericPackSubstitutions), + FFlag::LuauRemoveExtraSubtypingInstances ? ctx->subtyping : NotNull{&subtyping_DEPRECATED}, + ctx->scope, + newRetTp ); + if (!subst) return std::nullopt; - else - retPack = *subst; + + retPack = *subst; } + // After we solve for the instantiated function type of this metamethod, + // we may have new free types if the metamethod was generic. We capture + // these so that they can be generalized later and we don't end up with + // free types in type checking. + for (const auto& ty : unifier.newFreshTypes) + trackInteriorFreeType(ctx->scope, ty); + + for (const auto& tp : unifier.newFreshTypePacks) + trackInteriorFreeTypePack(ctx->scope, tp); + return retPack; } @@ -271,37 +287,9 @@ TypeFunctionReductionResult lenTypeFunction( if (isPending(*mmType, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - if (FFlag::LuauTypeFunctionsUseSolveFunctionCall) - { - // We only care that we _can_ solve this function, it doesn't matter what it returns. - if (!solveFunctionCall(ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack({operandTy}))) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - else - { - - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - TypePackId inferredArgPack = ctx->arena->addTypePack({operandTy}); - - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (UnifyResult::Ok != u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } + // We only care that we _can_ solve this function, it doesn't matter what it returns. + if (!solveFunctionCall(ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack({operandTy}))) + return {std::nullopt, Reduction::Erroneous, {}, {}}; // `len` must return a `number`. return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; @@ -364,46 +352,22 @@ TypeFunctionReductionResult unmTypeFunction( if (isPending(*mmType, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - if (FFlag::LuauTypeFunctionsUseSolveFunctionCall) - { - auto result = solveFunctionCall(ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack({operandTy})); - if (!result) - return {std::nullopt, Reduction::Erroneous, {}, {}}; + auto result = solveFunctionCall(ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack({operandTy})); + if (!result) + return {std::nullopt, Reduction::Erroneous, {}, {}}; - if (auto ret = first(*result)) - return {ret, Reduction::MaybeOk, {}, {}}; - else - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } + if (auto ret = first(*result)) + return {ret, Reduction::MaybeOk, {}, {}}; else - { - - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - TypePackId inferredArgPack = ctx->arena->addTypePack({operandTy}); - - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (UnifyResult::Ok != u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - if (std::optional ret = first(instantiatedMmFtv->retTypes)) - return {ret, Reduction::MaybeOk, {}, {}}; - else - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } + return {std::nullopt, Reduction::Erroneous, {}, {}}; } -TypeFunctionContext::TypeFunctionContext(NotNull cs, NotNull scope, NotNull constraint) +TypeFunctionContext::TypeFunctionContext( + NotNull cs, + NotNull scope, + NotNull constraint, + NotNull subtyping +) : arena(cs->arena) , builtins(cs->builtinTypes) , scope(scope) @@ -411,6 +375,7 @@ TypeFunctionContext::TypeFunctionContext(NotNull cs, NotNulltypeFunctionRuntime) , ice(NotNull{&cs->iceReporter}) , limits(NotNull{&cs->limits}) + , subtyping(subtyping) , solver(cs.get()) , constraint(constraint.get()) { @@ -419,7 +384,10 @@ TypeFunctionContext::TypeFunctionContext(NotNull cs, NotNull TypeFunctionContext::pushConstraint(ConstraintV&& c) const { LUAU_ASSERT(solver); - NotNull newConstraint = solver->pushConstraint(scope, constraint ? constraint->location : Location{}, std::move(c)); + Location location = constraint ? constraint->location : Location{}; + NotNull newConstraint = FFlag::DebugLuauCyclicRequireTypeInference + ? solver->pushConstraint(scope, location, std::move(c), constraint ? constraint->moduleName : solver->representativeModuleName) + : solver->DEPRECATED_pushConstraint(scope, location, std::move(c)); // Every constraint that is blocked on the current constraint must also be // blocked on this new one. @@ -503,50 +471,21 @@ TypeFunctionReductionResult numericBinopTypeFunction( TypePackId argPack = ctx->arena->addTypePack({lhsTy, rhsTy}); - if (FFlag::LuauBuiltinTypeFunctionsUseNewOverloadResolution) + if (reversed) { - if (reversed) - { - TypePack* p = getMutable(argPack); - std::swap(p->head.front(), p->head.back()); - } - - std::optional retPack = solveFunctionCall(ctx, location, *mmType, argPack); - if (!retPack.has_value()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *retPack, 1); - if (extracted.head.empty()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - return {extracted.head.front(), Reduction::MaybeOk, {}, {}}; + TypePack* p = getMutable(argPack); + std::swap(p->head.front(), p->head.back()); } - else - { - SolveResult solveResult; - - if (!reversed) - solveResult = solveFunctionCall_DEPRECATED( - ctx->arena, ctx->builtins, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice, ctx->limits, ctx->scope, location, *mmType, argPack - ); - else - { - TypePack* p = getMutable(argPack); - std::swap(p->head.front(), p->head.back()); - solveResult = solveFunctionCall_DEPRECATED( - ctx->arena, ctx->builtins, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice, ctx->limits, ctx->scope, location, *mmType, argPack - ); - } - if (!solveResult.typePackId.has_value()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; + std::optional retPack = solveFunctionCall(ctx, location, *mmType, argPack); + if (!retPack.has_value()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; - TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); - if (extracted.head.empty()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *retPack, 1); + if (extracted.head.empty()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; - return {extracted.head.front(), Reduction::MaybeOk, {}, {}}; - } + return {extracted.head.front(), Reduction::MaybeOk, {}, {}}; } TypeFunctionReductionResult addTypeFunction( @@ -728,53 +667,35 @@ TypeFunctionReductionResult concatTypeFunction( if (isPending(*mmType, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - if (FFlag::LuauTypeFunctionsUseSolveFunctionCall) + std::vector inferredArgs; + if (!reversed) + inferredArgs = {lhsTy, rhsTy}; + else + inferredArgs = {rhsTy, lhsTy}; + + if (FFlag::LuauConcatDoesntAlwaysReturnString) { - std::vector inferredArgs; - if (!reversed) - inferredArgs = {lhsTy, rhsTy}; - else - inferredArgs = {rhsTy, lhsTy}; + std::optional retPack = solveFunctionCall( + ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack(std::move(inferredArgs)) + ); + if (!retPack) + return {std::nullopt, Reduction::Erroneous, {}, {}}; - if (!solveFunctionCall( - ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack(std::move(inferredArgs)) - )) + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *retPack, 1); + if (extracted.head.empty()) return {std::nullopt, Reduction::Erroneous, {}, {}}; + + return {extracted.head.front(), Reduction::MaybeOk, {}, {}}; } else { - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) + if (!solveFunctionCall( + ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack(std::move(inferredArgs)) + )) return {std::nullopt, Reduction::Erroneous, {}, {}}; - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - std::vector inferredArgs; - if (!reversed) - inferredArgs = {lhsTy, rhsTy}; - else - inferredArgs = {rhsTy, lhsTy}; - - TypePackId inferredArgPack = ctx->arena->addTypePack(std::move(inferredArgs)); - - - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (UnifyResult::Ok != u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) - return {std::nullopt, Reduction::Erroneous, {}, {}}; + return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; } - - - return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; } namespace @@ -962,36 +883,9 @@ static TypeFunctionReductionResult comparisonTypeFunction( if (isPending(*mmType, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - if (FFlag::LuauTypeFunctionsUseSolveFunctionCall) - { - // We only care that we _can_ solve this function, it doesn't matter what it returns. - if (!solveFunctionCall(ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack({lhsTy, rhsTy}))) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - else - { - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - TypePackId inferredArgPack = ctx->arena->addTypePack({lhsTy, rhsTy}); - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (UnifyResult::Ok != u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - + // We only care that we _can_ solve this function, it doesn't matter what it returns. + if (!solveFunctionCall(ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack({lhsTy, rhsTy}))) + return {std::nullopt, Reduction::Erroneous, {}, {}}; return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; } @@ -1101,35 +995,8 @@ TypeFunctionReductionResult eqTypeFunction( if (isPending(*mmType, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; - if (FFlag::LuauTypeFunctionsUseSolveFunctionCall) - { - if (!solveFunctionCall(ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack({lhsTy, rhsTy}))) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - else - { - const FunctionType* mmFtv = get(*mmType); - if (!mmFtv) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - std::optional instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType); - if (!instantiatedMmType) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - - const FunctionType* instantiatedMmFtv = get(*instantiatedMmType); - if (!instantiatedMmFtv) - return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - - TypePackId inferredArgPack = ctx->arena->addTypePack({lhsTy, rhsTy}); - Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; - if (UnifyResult::Ok != u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) - return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - + if (!solveFunctionCall(ctx, ctx->constraint ? ctx->constraint->location : Location{}, *mmType, ctx->arena->addTypePack({lhsTy, rhsTy}))) + return {std::nullopt, Reduction::Erroneous, {}, {}}; return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; } @@ -2107,7 +1974,7 @@ bool searchPropsAndIndexer( indexType = follow(tblIndexer->indexResultType); } - if (isSubtype(ty, indexType, ctx->scope, ctx->builtins, *ctx->ice, SolverMode::New)) + if (isSubtype(ty, indexType, ctx->arena, ctx->builtins, ctx->scope, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice)) { TypeId idxResultTy = follow(tblIndexer->indexResultType); @@ -2164,45 +2031,17 @@ bool tblIndexInto( { TypePackId argPack = ctx->arena->addTypePack({indexer}); - if (FFlag::LuauBuiltinTypeFunctionsUseNewOverloadResolution) - { - std::optional retPack = solveFunctionCall(ctx, ctx->scope->location, indexee, argPack); - - if (!retPack.has_value()) - return false; + std::optional retPack = solveFunctionCall(ctx, ctx->scope->location, indexee, argPack); - TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *retPack, 1); - if (extracted.head.empty()) - return false; - - result.insert(follow(extracted.head.front())); - return true; - } - else - { - SolveResult solveResult = solveFunctionCall_DEPRECATED( - ctx->arena, - ctx->builtins, - ctx->normalizer, - ctx->typeFunctionRuntime, - ctx->ice, - ctx->limits, - ctx->scope, - ctx->scope->location, - indexee, - argPack - ); - - if (!solveResult.typePackId.has_value()) - return false; + if (!retPack.has_value()) + return false; - TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); - if (extracted.head.empty()) - return false; + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *retPack, 1); + if (extracted.head.empty()) + return false; - result.insert(follow(extracted.head.front())); - return true; - } + result.insert(follow(extracted.head.front())); + return true; } // we have a table type to try indexing @@ -2430,8 +2269,18 @@ TypeFunctionReductionResult setmetatableTypeFunction( TypeId targetTy = follow(typeParams.at(0)); TypeId metatableTy = follow(typeParams.at(1)); - if (isPending(targetTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + // Having the target type be a pending table does not block dispatch. + if (isPending(targetTy, ctx->solver) && !is(targetTy)) + return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; + } + else + { + if (isPending(targetTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; + } + std::shared_ptr targetNorm = ctx->normalizer->normalize(targetTy); @@ -2449,8 +2298,17 @@ TypeFunctionReductionResult setmetatableTypeFunction( targetNorm->hasExternTypes()) return {std::nullopt, Reduction::Erroneous, {}, {}}; - if (isPending(metatableTy, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {metatableTy}, {}}; + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + // Having the metatable type be a pending table does not block dispatch. + if (isPending(metatableTy, ctx->solver) && !is(metatableTy)) + return {std::nullopt, Reduction::MaybeOk, {metatableTy}, {}}; + } + else + { + if (isPending(metatableTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {metatableTy}, {}}; + } // if the supposed metatable is not a table, we will fail to reduce. if (!get(metatableTy) && !get(metatableTy)) @@ -2663,6 +2521,34 @@ TypeFunctionReductionResult getmetatableTypeFunction( return getmetatableHelper(targetTy, location, ctx); } +TypeFunctionReductionResult objectofTypeFunction( + TypeId instance, + const std::vector& typeParams, + const std::vector& packParams, + NotNull ctx +) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + if (typeParams.size() != 1 || !packParams.empty()) + { + ctx->ice->ice("objectof type function: encountered a type function instance without the required argument structure"); + LUAU_ASSERT(false); + } + + TypeId targetTy = follow(typeParams.at(0)); + + if (isPending(targetTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; + + if (auto klass = get(targetTy); klass && klass->relation) + { + if (auto obj = klass->relation->get_if()) + return {obj->ty, Reduction::MaybeOk, {}, {}}; + } + + return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; +} + TypeFunctionReductionResult weakoptionalTypeFunc( TypeId instance, const std::vector& typeParams, @@ -2723,6 +2609,7 @@ BuiltinTypeFunctions::BuiltinTypeFunctions() , rawgetFunc{"rawget", rawgetTypeFunction} , setmetatableFunc{"setmetatable", setmetatableTypeFunction} , getmetatableFunc{"getmetatable", getmetatableTypeFunction} + , objectofFunc{"objectof", objectofTypeFunction} , weakoptionalFunc{"weakoptional", weakoptionalTypeFunc} { } diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 06448420..3b992069 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.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/Clone.h" +#include "Luau/Ast.h" #include "Luau/Common.h" #include "Luau/NotNull.h" #include "Luau/Type.h" @@ -14,6 +15,7 @@ LUAU_FASTFLAG(LuauSolverV2) // For each `Luau::clone` call, we will clone only up to N amount of types _and_ packs, as controlled by this limit. LUAU_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000) +LUAU_FASTFLAG(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) namespace Luau { @@ -269,6 +271,12 @@ class TypeCloner t->lowerBound = shallowClone(t->lowerBound); if (t->upperBound) t->upperBound = shallowClone(t->upperBound); + + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + if (t->primitiveType) + t->primitiveType = shallowClone(*t->primitiveType); + } } void cloneChildren(GenericType* t) @@ -348,6 +356,23 @@ class TypeCloner t->indexer->indexType = shallowClone(t->indexer->indexType); t->indexer->indexResultType = shallowClone(t->indexer->indexResultType); } + + if (FFlag::DebugLuauUserDefinedClasses && t->relation) + { + Luau::visit( + overloaded{ + [&](Obj& obj) + { + obj.ty = shallowClone(obj.ty); + }, + [&](Klass& klass) + { + klass.ty = shallowClone(klass.ty); + } + }, + *t->relation + ); + } } void cloneChildren(AnyType* t) diff --git a/Analysis/src/Constraint.cpp b/Analysis/src/Constraint.cpp index f9c88cbf..b2ddc891 100644 --- a/Analysis/src/Constraint.cpp +++ b/Analysis/src/Constraint.cpp @@ -4,9 +4,12 @@ #include "Luau/TypeFunction.h" #include "Luau/VisitType.h" +LUAU_FASTFLAG(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + namespace Luau { +// Clip with DebugLuauCyclicRequireTypeInference Constraint::Constraint(NotNull scope, const Location& location, ConstraintV&& c) : scope(scope) , location(location) @@ -14,54 +17,72 @@ Constraint::Constraint(NotNull scope, const Location& location, Constrain { } -struct ReferenceCountInitializer : TypeOnceVisitor +Constraint::Constraint(NotNull scope, const Location& location, ConstraintV&& c, std::shared_ptr moduleName) + : scope(scope) + , location(location) + , c(std::move(c)) + , moduleName(std::move(moduleName)) { - NotNull result; - bool traverseIntoTypeFunctions = true; +} - explicit ReferenceCountInitializer(NotNull result) - : TypeOnceVisitor("ReferenceCountInitializer", /* skipBoundTypes */ true) - , result(result) - { - } +ReferenceCountInitializer::ReferenceCountInitializer(NotNull mutatedTypes, NotNull mutatedTypePacks) + : TypeOnceVisitor("ReferenceCountInitializer", /* skipBoundTypes */ true) + , mutatedTypes(mutatedTypes) + , mutatedTypePacks(mutatedTypePacks.get()) +{ +} - bool visit(TypeId ty, const FreeType&) override - { - result->insert(ty); - return false; - } +bool ReferenceCountInitializer::visit(TypeId ty, const FreeType&) +{ + mutatedTypes->insert(ty); + return false; +} - bool visit(TypeId ty, const BlockedType&) override - { - result->insert(ty); - return false; - } +bool ReferenceCountInitializer::visit(TypeId ty, const BlockedType&) +{ + mutatedTypes->insert(ty); + return false; +} - bool visit(TypeId ty, const PendingExpansionType&) override - { - result->insert(ty); - return false; - } +bool ReferenceCountInitializer::visit(TypeId ty, const PendingExpansionType&) +{ + mutatedTypes->insert(ty); + return false; +} - bool visit(TypeId ty, const TableType& tt) override - { - if (tt.state == TableState::Unsealed || tt.state == TableState::Free) - result->insert(ty); +bool ReferenceCountInitializer::visit(TypeId ty, const TableType& tt) +{ + if (tt.state == TableState::Unsealed || tt.state == TableState::Free) + mutatedTypes->insert(ty); - return true; - } + return true; +} - bool visit(TypeId ty, const ExternType&) override - { - // ExternTypes never contain free types. - return false; - } +bool ReferenceCountInitializer::visit(TypeId ty, const ExternType&) +{ + // ExternTypes never contain free types. + return false; +} - bool visit(TypeId, const TypeFunctionInstanceType& tfit) override - { - return tfit.function->canReduceGenerics; - } -}; +bool ReferenceCountInitializer::visit(TypeId, const TypeFunctionInstanceType& tfit) +{ + return tfit.function->canReduceGenerics; +} + + +bool ReferenceCountInitializer::visit(TypePackId tp, const BlockedTypePack&) +{ + LUAU_ASSERT(mutatedTypePacks); + mutatedTypePacks->insert(tp); + return true; +} + +bool ReferenceCountInitializer::visit(TypePackId tp, const FreeTypePack&) +{ + LUAU_ASSERT(mutatedTypePacks); + mutatedTypePacks->insert(tp); + return true; +} bool isReferenceCountedType(const TypeId typ) { @@ -72,7 +93,7 @@ bool isReferenceCountedType(const TypeId typ) return get(typ) || get(typ) || get(typ); } -TypeIds Constraint::getMaybeMutatedFreeTypes() const +std::pair Constraint::getMaybeMutatedTypes() const { // For the purpose of this function and reference counting in general, we are only considering // mutations that affect the _bounds_ of the free type, and not something that may bind the free @@ -80,7 +101,12 @@ TypeIds Constraint::getMaybeMutatedFreeTypes() const // contribution to the output set here. TypeIds types; - ReferenceCountInitializer rci{NotNull{&types}}; + + // NOTE: In the future we'd like to track references to type packs, so we're + // adding this local, but we do not modify it. + TypePackIds typePacks; + + ReferenceCountInitializer rci{NotNull{&types}, NotNull{&typePacks}}; if (auto ec = get(*this)) { @@ -122,7 +148,7 @@ TypeIds Constraint::getMaybeMutatedFreeTypes() const rci.traverse(fcc->argsPack); rci.traverseIntoTypeFunctions = true; } - else if (auto ptc = get(*this)) + else if (auto ptc = get(*this); !FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier && ptc) { rci.traverse(ptc->freeType); } @@ -152,7 +178,24 @@ TypeIds Constraint::getMaybeMutatedFreeTypes() const { for (TypeId ty : uc->resultPack) rci.traverse(ty); - // `UnpackConstraint` should not mutate `sourcePack`. + // Consider: + // + // function set(dictionary, key, value) + // local new = table.clone(dictionary) + // new[key] = value + // return new + // end + // + // In this case, we would expect `dictionary` to be inferred as + // something like `{ [T]: K }` for some generic `T` and `K`. + // However, in order to avoid eagerly generalizing dictionary, + // we need to track that it may be mutated by the line: + // + // new[key] = value + // + // ... this implies that `UnpackConstraint` can mutate both + // it's LHS and RHS operands. LHS directly, and RHS by proxy. + rci.traverse(uc->sourcePack); } else if (auto rpc = get(*this)) { @@ -167,7 +210,7 @@ TypeIds Constraint::getMaybeMutatedFreeTypes() const rci.traverse(ptc->targetType); } - return types; + return {std::move(types), std::move(typePacks)}; } } // namespace Luau diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 87d5b842..036d8de2 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -2,15 +2,16 @@ #include "Luau/ConstraintGenerator.h" #include "Luau/Ast.h" +#include "Luau/AstUtils.h" #include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinTypeFunctions.h" #include "Luau/Common.h" #include "Luau/Constraint.h" #include "Luau/ControlFlow.h" +#include "Luau/ControlFlowGraph.h" #include "Luau/DcrLogger.h" #include "Luau/Def.h" #include "Luau/DenseHash.h" -#include "Luau/InferPolarity.h" #include "Luau/IterativeTypeVisitor.h" #include "Luau/ModuleResolver.h" #include "Luau/Normalize.h" @@ -24,12 +25,13 @@ #include "Luau/TimeTrace.h" #include "Luau/Type.h" #include "Luau/TypeFunction.h" +#include "Luau/TypeFunctionError.h" #include "Luau/TypePack.h" +#include "Luau/TypeStateMap.h" #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" #include "Luau/VisitType.h" -#include #include LUAU_DYNAMIC_FASTINTVARIABLE(LuauConstraintGeneratorRecursionLimit, 300) @@ -38,15 +40,15 @@ LUAU_FASTINT(LuauCheckRecursionLimit) LUAU_FASTFLAG(DebugLuauLogSolverToJson) LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTINTVARIABLE(LuauPrimitiveInferenceInTableLimit, 500) -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSyntax) -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSupport) -LUAU_FASTFLAG(LuauPushTypeConstraintLambdas3) -LUAU_FASTFLAGVARIABLE(LuauPropagateTypeAnnotationsInForInLoops) -LUAU_FASTFLAGVARIABLE(LuauStorePolarityInline) -LUAU_FASTFLAGVARIABLE(LuauDontIncludeVarargWithAnnotation) -LUAU_FASTFLAGVARIABLE(LuauUdtfIndirectAliases) LUAU_FASTFLAGVARIABLE(LuauDisallowRedefiningBuiltinTypes) -LUAU_FASTFLAGVARIABLE(LuauUnpackRespectsAnnotations) +LUAU_FASTFLAG(LuauTypeFunctionStructuredErrors) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAGVARIABLE(LuauDoNotEmplaceAnnotatedType) +LUAU_FASTFLAGVARIABLE(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) +LUAU_FLAGVERSION(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier, 2) +LUAU_FASTFLAGVARIABLE(LuauDeprecatedAttributeOnAnonymousFunctions) +LUAU_FASTFLAGVARIABLE(DebugLuauCFG) +LUAU_FASTFLAG(DebugLuauCyclicRequireTypeInference) namespace Luau { @@ -54,6 +56,13 @@ namespace Luau bool doesCallError(const AstExprCall* call); // TypeInfer.cpp const AstStat* getFallthrough(const AstStat* node); // TypeInfer.cpp +static bool isValidClassMetamethod(const Name& name) +{ + return name == "__call" || name == "__concat" || name == "__unm" || name == "__add" || name == "__sub" || name == "__mul" || name == "__div" || + name == "__mod" || name == "__pow" || name == "__tostring" || name == "__eq" || name == "__lt" || name == "__le" || name == "__iter" || + name == "__len" || name == "__idiv"; +} + static std::optional matchRequire(const AstExprCall& call) { const char* require = "require"; @@ -71,44 +80,23 @@ static std::optional matchRequire(const AstExprCall& call) return call.args.data[0]; } -struct TypeGuard -{ - bool isTypeof; - AstExpr* target; - std::string type; -}; -static std::optional matchTypeGuard(const AstExprBinary::Op op, AstExpr* left, AstExpr* right) +static const RefinementKey* matchIsInstanceGuard(const AstExprCall& call, NotNull dfg) { - if (op != AstExprBinary::CompareEq && op != AstExprBinary::CompareNe) - return std::nullopt; + AstExprIndexName* index = call.func->as(); + if (!index || index->op != '.') + return nullptr; - if (right->is()) - std::swap(left, right); + if (index->index != "isinstance") + return nullptr; - if (!right->is()) - return std::nullopt; + if (!index->expr->is()) + return nullptr; - AstExprCall* call = left->as(); - AstExprConstantString* string = right->as(); - if (!call || !string) - return std::nullopt; - - AstExprGlobal* callee = call->func->as(); - if (!callee) - return std::nullopt; + if (call.args.size < 1) + return nullptr; - if (callee->name != "type" && callee->name != "typeof") - return std::nullopt; - - if (call->args.size != 1) - return std::nullopt; - - return TypeGuard{ - /*isTypeof*/ callee->name == "typeof", - /*target*/ call->args.data[0], - /*type*/ std::string(string->value.data, string->value.size), - }; + return dfg->getRefinementKey(call.args.data[0]); } namespace @@ -116,14 +104,100 @@ namespace Checkpoint checkpoint(const ConstraintGenerator* cg) { + if (FFlag::DebugLuauCyclicRequireTypeInference) + return Checkpoint{cg->cgraph->constraints.size()}; return Checkpoint{cg->constraints.size()}; } template void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const ConstraintGenerator* cg, F f) { - for (size_t i = start.offset; i < end.offset; ++i) - f(cg->constraints[i]); + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + for (size_t i = start.offset; i < end.offset; ++i) + { + f(cg->cgraph->constraints[i]); + } + } + else + { + for (size_t i = start.offset; i < end.offset; ++i) + { + f(cg->constraints[i]); + } + } +} + +/** + * For all constraints [C] from [start] to [end], block dispatching + * [target] on [C]. + */ +LUAU_NOINLINE void addAllAsDependencies(const Checkpoint& start, const Checkpoint& end, const ConstraintGenerator* cg, NotNull target) +{ + forEachConstraint( + start, + end, + cg, + [cg, target](const ConstraintPtr& ptr) + { + cg->cgraph->addDependencyOf(ptr.get(), target); + } + ); +} + +/** + * For all constraints [C] from [start] to [end], block dispatching + * [C] on [target]. + */ +LUAU_NOINLINE void addAllAsReverseDependencies( + const Checkpoint& start, + const Checkpoint& end, + const ConstraintGenerator* cg, + NotNull target +) +{ + forEachConstraint( + start, + end, + cg, + [cg, target](const ConstraintPtr& ptr) + { + cg->cgraph->addDependencyOf(target, ptr.get()); + } + ); +} + +/** + * For all constraints [C] from [start] to [end], block dispatching + * [target] on [C]. + * + * HACK: Additionally, chain `PackSubtypeConstraint`s that are tied to return + * statements in order to preserve some behavior from the old solver. + */ +LUAU_NOINLINE void addAllAsDependenciesAndChainReturns( + const Checkpoint& start, + const Checkpoint& end, + const ConstraintGenerator* cg, + NotNull target +) +{ + Constraint* previous = nullptr; + forEachConstraint( + start, + end, + cg, + [cg, target, &previous](const ConstraintPtr& constraint) + { + cg->cgraph->addDependencyOf(constraint.get(), target); + if (auto psc = get(*constraint); psc && psc->returns) + { + if (previous) + cg->cgraph->addDependencyOf(previous, constraint.get()); + + previous = constraint.get(); + } + } + ); } struct HasFreeType : TypeOnceVisitor @@ -204,9 +278,12 @@ ConstraintGenerator::ConstraintGenerator( std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, - std::vector requireCycles + std::vector requireCycles, + NotNull cgraph, + CFG::TypeStateMap* typestate ) : module(module) + , sharedModuleName(std::make_shared(module->name)) , builtinTypes(builtinTypes) , arena(normalizer->arena) , rootScope(nullptr) @@ -220,6 +297,8 @@ ConstraintGenerator::ConstraintGenerator( , prepareModuleScope(std::move(prepareModuleScope)) , requireCycles(std::move(requireCycles)) , logger(logger) + , cgraph(cgraph) + , typestate(typestate) { LUAU_ASSERT(module); } @@ -228,6 +307,8 @@ ConstraintSet ConstraintGenerator::run(AstStatBlock* block) { visitModuleRoot(block); + if (FFlag::DebugLuauCyclicRequireTypeInference) + return ConstraintSet{NotNull{rootScope}, {}, {}, DenseHashMap{nullptr}, std::move(errors)}; return ConstraintSet{NotNull{rootScope}, std::move(constraints), std::move(freeTypes), std::move(scopeToFunction), std::move(errors)}; } @@ -235,6 +316,8 @@ ConstraintSet ConstraintGenerator::runOnFragment(const ScopePtr& resumeScope, As { visitFragmentRoot(resumeScope, block); + if (FFlag::DebugLuauCyclicRequireTypeInference) + return ConstraintSet{NotNull{rootScope}, {}, {}, DenseHashMap{nullptr}, std::move(errors)}; return ConstraintSet{NotNull{rootScope}, std::move(constraints), std::move(freeTypes), std::move(scopeToFunction), std::move(errors)}; } @@ -277,10 +360,8 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) GeneralizationConstraint{ result, moduleFnTy, - /*interiorTypes*/ std::vector{}, - /*hasDeprecatedAttribute*/ false, - /*deprecatedInfo*/ {}, - /*noGenerics*/ true + /* maybeDeprecatedAttr */ nullptr, + /* noGenerics */ true } ); @@ -288,19 +369,13 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) scope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); getMutable(result)->setOwner(genConstraint); - forEachConstraint( - start, - end, - this, - [genConstraint](const ConstraintPtr& c) - { - genConstraint->dependencies.emplace_back(c.get()); - } - ); + + addAllAsDependencies(start, end, this, genConstraint); interiorFreeTypes.pop_back(); - fillInInferredBindings(scope, block); + if (!FFlag::DebugLuauCFG) + fillInInferredBindings(scope, block); if (logger) logger->captureGenerationModule(module); @@ -335,7 +410,8 @@ void ConstraintGenerator::visitFragmentRoot(const ScopePtr& resumeScope, AstStat // Post interiorFreeTypes.pop_back(); - fillInInferredBindings(resumeScope, block); + if (!FFlag::DebugLuauCFG) + fillInInferredBindings(resumeScope, block); if (logger) logger->captureGenerationModule(module); @@ -362,7 +438,10 @@ TypeId ConstraintGenerator::freshType(const ScopePtr& scope, Polarity polarity) { const TypeId ft = Luau::freshType(arena, builtinTypes, scope.get(), polarity); interiorFreeTypes.back().types.push_back(ft); - freeTypes.insert(ft); + if (FFlag::DebugLuauCyclicRequireTypeInference) + cgraph->freeTypes.insert(ft); + else + freeTypes.insert(ft); return ft; } @@ -440,13 +519,52 @@ std::optional ConstraintGenerator::lookup(const ScopePtr& scope, Locatio ice->ice("ConstraintGenerator::lookup is inexhaustive?"); } +TypeId ConstraintGenerator::resolveRHSType(const ScopePtr& scope, Location location, AstExpr* expr) +{ + LUAU_ASSERT(FFlag::DebugLuauCFG); + TypeId ty = typestate->getRHSType(expr); + LUAU_ASSERT(ty); + ty = follow(ty); + if (auto c = typestate->getOptionalConstraint(ty)) + { + auto oc = addConstraint(scope, location, std::move(*c)); + if (auto bt = getMutable(ty)) + bt->setOwner(oc.get()); + } + + return ty; +} + +TypeId ConstraintGenerator::resolveLHSType(const ScopePtr& scope, Location location, const CFG::LValue& lv) +{ + LUAU_ASSERT(FFlag::DebugLuauCFG); + TypeId ty = typestate->getLHSType(lv); + LUAU_ASSERT(ty); + ty = follow(ty); + if (auto c = typestate->getOptionalConstraint(ty)) + { + auto oc = addConstraint(scope, location, std::move(*c)); + if (auto bt = getMutable(ty)) + bt->setOwner(oc.get()); + } + + return ty; +} + NotNull ConstraintGenerator::addConstraint(const ScopePtr& scope, const Location& location, ConstraintV cv) { + if (FFlag::DebugLuauCyclicRequireTypeInference) + return NotNull{cgraph->constraints.emplace_back(new Constraint{NotNull{scope.get()}, location, std::move(cv), sharedModuleName}).get()}; return NotNull{constraints.emplace_back(new Constraint{NotNull{scope.get()}, location, std::move(cv)}).get()}; } NotNull ConstraintGenerator::addConstraint(const ScopePtr& scope, std::unique_ptr c) { + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + c->moduleName = sharedModuleName; + return NotNull{cgraph->constraints.emplace_back(std::move(c)).get()}; + } return NotNull{constraints.emplace_back(std::move(c)).get()}; } @@ -733,10 +851,20 @@ void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location locat addConstraint(scope, location, c); } -void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* block) +/* + * To support things like recursive and corecursive type aliases, we handle them + * in two passes. First, we do a surface scan where we count generic arguments + * and stub types in with BlockedTypes. Later, we'll process the bodies of + * these statements and actually work out how to expand them. In the case of + * class definitions, we'll run type inference on class methods during that + * second pass. + * + * This function implements the early prototyping pass. The main execution flow + * of ConstraintGenerator handles the second pass. + */ +void ConstraintGenerator::prototypeTypeDefinitions(const ScopePtr& scope, AstStatBlock* block) { - std::unordered_map aliasDefinitionLocations; - std::unordered_map classDefinitionLocations; + DenseHashMap typeNameLocations{Name{}}; bool hasTypeFunction = false; ScopePtr typeFunctionEnvScope; @@ -754,19 +882,17 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc continue; } - if (scope->exportedTypeBindings.count(alias->name.value) || scope->privateTypeBindings.count(alias->name.value)) - { - auto it = aliasDefinitionLocations.find(alias->name.value); - LUAU_ASSERT(it != aliasDefinitionLocations.end()); - reportError(alias->location, DuplicateTypeDefinition{alias->name.value, it->second}); - continue; - } - // A type alias might have no name if the code is syntactically // illegal. We mustn't prepopulate anything in this case. if (alias->name == kParseNameError || alias->name == "typeof") continue; + if (const Location* loc = typeNameLocations.find(alias->name.value)) + { + reportError(alias->location, DuplicateTypeDefinition{alias->name.value, *loc}); + continue; + } + ScopePtr defnScope = childScope(alias, scope); TypeId initialType = arena->addType(BlockedType{}); @@ -797,18 +923,16 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc scope->privateTypeBindings[alias->name.value] = std::move(initialFun); astTypeAliasDefiningScopes[alias] = defnScope; - aliasDefinitionLocations[alias->name.value] = alias->location; + typeNameLocations[alias->name.value] = alias->location; } else if (auto function = stat->as()) { hasTypeFunction = true; // If a type function w/ same name has already been defined, error for having duplicates - if (scope->exportedTypeBindings.count(function->name.value) || scope->privateTypeBindings.count(function->name.value)) + if (const Location* loc = typeNameLocations.find(function->name.value)) { - auto it = aliasDefinitionLocations.find(function->name.value); - LUAU_ASSERT(it != aliasDefinitionLocations.end()); - reportError(function->location, DuplicateTypeDefinition{function->name.value, it->second}); + reportError(function->location, DuplicateTypeDefinition{function->name.value, *loc}); continue; } @@ -829,8 +953,16 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc quantifiedTypeParams.push_back(genericTy); } - if (std::optional error = typeFunctionRuntime->registerFunction(function)) - reportError(function->location, GenericError{*error}); + if (FFlag::LuauTypeFunctionStructuredErrors) + { + if (std::optional error = typeFunctionRuntime->registerFunction(function)) + reportError(function->location, BuiltInTypeFunctionError{*error}); + } + else + { + if (std::optional error = typeFunctionRuntime->registerFunction_DEPRECATED(function)) + reportError(function->location, GenericError{*error}); + } UserDefinedFunctionData udtfData; @@ -851,23 +983,21 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc else scope->privateTypeBindings[function->name.value] = std::move(typeFunction); - aliasDefinitionLocations[function->name.value] = function->location; + typeNameLocations[function->name.value] = function->location; } else if (auto classDeclaration = stat->as()) { - if (scope->exportedTypeBindings.count(classDeclaration->name.value)) - { - auto it = classDefinitionLocations.find(classDeclaration->name.value); - LUAU_ASSERT(it != classDefinitionLocations.end()); - reportError(classDeclaration->location, DuplicateTypeDefinition{classDeclaration->name.value, it->second}); - continue; - } - // A class might have no name if the code is syntactically // illegal. We mustn't prepopulate anything in this case. if (classDeclaration->name == kParseNameError) continue; + if (const Location* loc = typeNameLocations.find(classDeclaration->name.value)) + { + reportError(classDeclaration->location, DuplicateTypeDefinition{classDeclaration->name.value, *loc}); + continue; + } + ScopePtr defnScope = childScope(classDeclaration, scope); TypeId initialType = arena->addType(BlockedType{}); @@ -875,7 +1005,125 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc initialFun.definitionLocation = classDeclaration->location; scope->exportedTypeBindings[classDeclaration->name.value] = std::move(initialFun); - classDefinitionLocations[classDeclaration->name.value] = classDeclaration->location; + typeNameLocations[classDeclaration->name.value] = classDeclaration->location; + } + else if (auto classDecl = stat->as()) + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + + Name declName = classDecl->name->name.value; + DefId theDef = dfg->getDef(classDecl->name); + + if (Location* loc = typeNameLocations.find(declName)) + { + reportError(classDecl->location, DuplicateTypeDefinition{declName, *loc}); + scope->bindings[classDecl->name->name] = Binding{builtinTypes->errorType, classDecl->location}; + scope->lvalueTypes[theDef] = builtinTypes->errorType; + continue; + } + typeNameLocations[declName] = classDecl->location; + + TypeId theTy = arena->addType(BlockedType{}); + scope->bindings[classDecl->name->name] = Binding{theTy, classDecl->name->location}; + scope->lvalueTypes[theDef] = theTy; + + // Objects are ExternTypes, where the metatable field represents the metamethods associated with the instance, ** not ** the class itself. + // Class: ExternType { props, parent: top class type, metatable: {__call -- this lets it be called as a constructor } } + // Object: ExternType { props, parent: top object type for now, metatable: instance metamethods } + // TODO: we should add a direct reference to the `class` on the `object` type (probably useful for classof) + TableType::Props staticProps; + ExternType::Props props; + TableType::Props instanceMetatableProps; + DenseHashMap memberTypes{AstName{""}}; + + TypeId ctorArgTy = arena->addType(TableType{TableType::Props{}, std::nullopt, TypeLevel{}, scope.get(), TableState::Sealed}); + TableType* ctorArgTable = getMutable(ctorArgTy); + LUAU_ASSERT(ctorArgTable); + + for (const auto& member : classDecl->members) + { + Luau::visit( + overloaded{ + [&](const AstClassProperty& classProp) + { + if (memberTypes.contains(classProp.name)) + return; + + auto [propertyType, _] = memberTypes.try_insert(classProp.name, arena->addType(BlockedType{})); + auto& p = props[classProp.name.value]; + + // This needs to be blocked initially: if this + // type refers to a type that contains a typeof + // or an alias that we have yet to define, then + // we'll ICE or misbehave. + p = Property::rw(propertyType); + p.location = classProp.nameLocation; + + // We make the constructor take read-only args. + // This is true, in that we do not write to the + // table you pass for constructing an object. + ctorArgTable->props[classProp.name.value] = Property::readonly(propertyType); + }, + [&](const AstClassMethod& method) + { + if (memberTypes.contains(method.functionName)) + return; + + auto [propertyType, _] = memberTypes.try_insert(method.functionName, arena->addType(BlockedType{})); + + auto prop = Property::readonly(propertyType); + prop.location = method.nameLocation; + if (method.function->args.size < 1 || method.function->args.data[0]->name != "self") + staticProps[method.functionName.value] = prop; + // The parser will report an error for classes that define disallowed metamethods. + // The RFC also requires that it is a syntax error for methods to have __ in their name whos name is not in the + // validClassMetamethod set. + if (isValidClassMetamethod(method.functionName.value)) + instanceMetatableProps[method.functionName.value] = prop; + else + props[method.functionName.value] = prop; + } + }, + member + ); + } + + TypeId instanceMetatable = arena->addType(TableType{instanceMetatableProps, std::nullopt, TypeLevel{}, scope.get(), TableState::Sealed}); + + TypeId classInstanceTy = arena->addType( + ExternType{ + declName, std::move(props), builtinTypes->objectType, instanceMetatable, Tags{}, nullptr, module->name, classDecl->location + } + ); + + TypeId ctorTy = + arena->addType(FunctionType{arena->addTypePack({builtinTypes->unknownType, ctorArgTy}), arena->addTypePack({classInstanceTy})}); + + TypeId metatableTy = arena->addType( + TableType{TableType::Props{{"__call", Property::readonly(ctorTy)}}, std::nullopt, TypeLevel{}, scope.get(), TableState::Sealed} + ); + + TypeId externTy = arena->addType( + ExternType{declName, staticProps, builtinTypes->classType, metatableTy, Tags{}, nullptr, module->name, classDecl->location} + ); + + // Setup a bidirectional relationship between classes and objects + getMutable(externTy)->relation.emplace(Obj{classInstanceTy}); + getMutable(classInstanceTy)->relation.emplace(Klass{externTy}); + + LUAU_ASSERT(!is(theTy)); + [[maybe_unused]] const BlockedType* bt = get(theTy); + LUAU_ASSERT(bt); + LUAU_ASSERT(bt->getOwner() == nullptr); + + emplaceType(asMutable(theTy), externTy); + + if (classDecl->exported) + scope->exportedTypeBindings[classDecl->name->name.value] = TypeFun{{}, {}, classInstanceTy, classDecl->location}; + else + scope->privateTypeBindings[classDecl->name->name.value] = TypeFun{{}, {}, classInstanceTy, classDecl->location}; + + classDeclRecords[classDecl->name] = std::make_unique(ClassDeclRecord{classInstanceTy, std::move(memberTypes)}); } } @@ -910,8 +1158,7 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc // Fill it with all visible type functions and referenced type aliases if (mainTypeFun) { - if (FFlag::LuauUdtfIndirectAliases) - createdTypeFunctions.push_back(mainTypeFun); + createdTypeFunctions.push_back(mainTypeFun); GlobalNameCollector globalNameCollector; stat->visit(&globalNameCollector); @@ -930,8 +1177,7 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc userFuncData.environmentFunction[name] = std::make_pair(ty->userFuncData.definition, level); - if (FFlag::LuauUdtfIndirectAliases) - referencedTypeFunctions[ty->userFuncData.definition] = ty; + referencedTypeFunctions[ty->userFuncData.definition] = ty; if (auto it = astTypeFunctionEnvironmentScopes.find(ty->userFuncData.definition)) { @@ -975,27 +1221,24 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc } } - if (FFlag::LuauUdtfIndirectAliases) + // Finally, we need to include aliases from functions we might call + for (TypeFunctionInstanceType* type : createdTypeFunctions) { - // Finally, we need to include aliases from functions we might call - for (TypeFunctionInstanceType* type : createdTypeFunctions) - { - UserDefinedFunctionData& sourceFuncData = type->userFuncData; + UserDefinedFunctionData& sourceFuncData = type->userFuncData; - // Go over all functions in our environment - for (const auto& [targetFuncName, definitionAndLevel] : sourceFuncData.environmentFunction) + // Go over all functions in our environment + for (const auto& [targetFuncName, definitionAndLevel] : sourceFuncData.environmentFunction) + { + if (const TypeFunctionInstanceType** it = referencedTypeFunctions.find(definitionAndLevel.first)) { - if (const TypeFunctionInstanceType** it = referencedTypeFunctions.find(definitionAndLevel.first)) - { - const UserDefinedFunctionData& targetFuncData = (*it)->userFuncData; + const UserDefinedFunctionData& targetFuncData = (*it)->userFuncData; - for (const auto& [aliasName, typeAndLevel] : targetFuncData.environmentAlias) + for (const auto& [aliasName, typeAndLevel] : targetFuncData.environmentAlias) + { + if (!sourceFuncData.environmentAlias.find(aliasName)) { - if (!sourceFuncData.environmentAlias.find(aliasName)) - { - // Combine definition levels because we are viewing target function aliases from the perspective of the target function - sourceFuncData.environmentAlias[aliasName] = {typeAndLevel.first, typeAndLevel.second + definitionAndLevel.second}; - } + // Combine definition levels because we are viewing target function aliases from the perspective of the target function + sourceFuncData.environmentAlias[aliasName] = {typeAndLevel.first, typeAndLevel.second + definitionAndLevel.second}; } } } @@ -1013,7 +1256,7 @@ ControlFlow ConstraintGenerator::visitBlockWithoutChildScope(const ScopePtr& sco return ControlFlow::None; } - checkAliases(scope, block); + prototypeTypeDefinitions(scope, block); std::optional firstControlFlow; for (AstStat* stat : block->body) @@ -1084,6 +1327,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStat* stat) return visit(scope, s); else if (auto s = stat->as()) return visit(scope, s); + else if (auto s = stat->as()) + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + return visit(scope, s); + } else if (auto s = stat->as()) return visit(scope, s); else @@ -1113,9 +1361,17 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat { const Location location = local->location; - TypeId assignee = arena->addType(BlockedType{}); - localTypes.try_insert(assignee, {}); + TypeId assignee; + if (FFlag::DebugLuauCFG) + { + assignee = resolveLHSType(scope, location, CFG::LValue{Symbol{local}}); + } + else + { + assignee = arena->addType(BlockedType{}); + } + localTypes.try_insert(assignee, {}); assignees.push_back(assignee); if (!firstValueType) @@ -1141,8 +1397,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat inferredBindings[local] = {scope.get(), location, {assignee}}; } - DefId def = dfg->getDef(local); - scope->lvalueTypes[def] = assignee; + if (!FFlag::DebugLuauCFG) + { + DefId def = dfg->getDef(local); + scope->lvalueTypes[def] = assignee; + } } Checkpoint start = checkpoint(this); @@ -1163,8 +1422,18 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat if (statLocal->vars.data[i]->annotation) { localDomain->insert(annotatedTypes[i]); - if (FFlag::LuauUnpackRespectsAnnotations && i >= head.size() && tail) - deferredTypes.emplace_back(annotatedTypes[i]); + if (i >= head.size() && tail) + { + if (FFlag::LuauDoNotEmplaceAnnotatedType) + { + deferredTypes.push_back(arena->addType(BlockedType{})); + freshBlockedTypes.insert(getMutable(deferredTypes.back())); + } + else + { + deferredTypes.emplace_back(annotatedTypes[i]); + } + } } else { @@ -1176,8 +1445,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat { deferredTypes.push_back(arena->addType(BlockedType{})); localDomain->insert(deferredTypes.back()); - if (FFlag::LuauUnpackRespectsAnnotations) - freshBlockedTypes.insert(getMutable(deferredTypes.back())); + freshBlockedTypes.insert(getMutable(deferredTypes.back())); } else { @@ -1197,29 +1465,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat LUAU_ASSERT(tail); NotNull uc = addConstraint(scope, statLocal->location, UnpackConstraint{deferredTypes, *tail}); - forEachConstraint( - start, - end, - this, - [&uc](const ConstraintPtr& runBefore) - { - uc->dependencies.emplace_back(runBefore.get()); - } - ); - - if (FFlag::LuauUnpackRespectsAnnotations) - { - // This is a separate set from `deferredTypes` to - // distinguish between blocked types we just minted - // and blocked types that correspond to annotations. - for (BlockedType* bt : freshBlockedTypes) - bt->setOwner(uc); - } - else - { - for (TypeId t : deferredTypes) - getMutable(t)->setOwner(uc); - } + addAllAsDependencies(start, end, this, uc); + // This is a separate set from `deferredTypes` to + // distinguish between blocked types we just minted + // and blocked types that correspond to annotations. + for (BlockedType* bt : freshBlockedTypes) + bt->setOwner(uc); } if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope && !hasAnnotation) @@ -1324,35 +1575,18 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI TypeId loopVar = arena->addType(BlockedType{}); variableTypes.push_back(loopVar); - if (FFlag::LuauPropagateTypeAnnotationsInForInLoops) - { - DefId def = dfg->getDef(var); + DefId def = dfg->getDef(var); - if (var->annotation) - { - TypeId annotationTy = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); - loopScope->bindings[var] = Binding{annotationTy, var->location}; - addConstraint(scope, var->location, SubtypeConstraint{loopVar, annotationTy}); - loopScope->lvalueTypes[def] = annotationTy; - } - else - { - loopScope->bindings[var] = Binding{loopVar, var->location}; - loopScope->lvalueTypes[def] = loopVar; - } + if (var->annotation) + { + TypeId annotationTy = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); + loopScope->bindings[var] = Binding{annotationTy, var->location}; + addConstraint(scope, var->location, SubtypeConstraint{loopVar, annotationTy}); + loopScope->lvalueTypes[def] = annotationTy; } else { - if (var->annotation) - { - TypeId annotationTy = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); - loopScope->bindings[var] = Binding{annotationTy, var->location}; - addConstraint(scope, var->location, SubtypeConstraint{loopVar, annotationTy}); - } - else - loopScope->bindings[var] = Binding{loopVar, var->location}; - - DefId def = dfg->getDef(var); + loopScope->bindings[var] = Binding{loopVar, var->location}; loopScope->lvalueTypes[def] = loopVar; } } @@ -1373,8 +1607,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI loopScope->lvalueTypes[keyDef] = intersectionTy; auto c = addConstraint(loopScope, keyVar->location, ReduceConstraint{intersectionTy}); - c->dependencies.push_back(iterable); - + cgraph->addDependencyOf(iterable, c); for (TypeId var : variableTypes) { auto bt = getMutable(var); @@ -1389,21 +1622,20 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI scope->inheritAssignments(loopScope); // This iter constraint must dispatch first. - forEachConstraint( - start, - end, - this, - [&iterable](const ConstraintPtr& runLater) - { - runLater->dependencies.push_back(iterable); - } - ); - + addAllAsReverseDependencies(start, end, this, iterable); return ControlFlow::None; } ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatWhile* while_) { + if (FFlag::DebugLuauCFG) + { + check(scope, while_->condition); + ScopePtr whileScope = childScope(while_->body, scope); + visit(whileScope, while_->body); + return ControlFlow::None; + } + RefinementId refinement = check(scope, while_->condition).refinement; ScopePtr whileScope = childScope(while_, scope); @@ -1433,11 +1665,9 @@ static void propagateDeprecatedAttributeToConstraint(ConstraintV& c, const AstEx { if (GeneralizationConstraint* genConstraint = c.get_if()) { - AstAttr* deprecatedAttribute = func->getAttribute(AstAttr::Type::Deprecated); - genConstraint->hasDeprecatedAttribute = deprecatedAttribute != nullptr; - if (deprecatedAttribute) + if (AstAttr* deprecatedAttribute = func->getAttribute(AstAttr::Type::Deprecated)) { - genConstraint->deprecatedInfo = deprecatedAttribute->deprecatedInfo(); + genConstraint->maybeDeprecatedAttr = deprecatedAttribute; } } } @@ -1456,7 +1686,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti functionType = arena->addType(BlockedType{}); scope->bindings[function->name] = Binding{functionType, function->name->location}; - FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location); + FunctionSignature sig = checkFunctionSignature(scope, nullptr, function->func, /* expectedType */ std::nullopt, function->name->location); sig.bodyScope->bindings[function->name] = Binding{sig.signature, function->name->location}; DefId def = dfg->getDef(function->name); @@ -1470,31 +1700,13 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti Checkpoint end = checkpoint(this); NotNull constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}; - std::unique_ptr c = - std::make_unique(constraintScope, function->name->location, GeneralizationConstraint{functionType, sig.signature}); + std::unique_ptr c = FFlag::DebugLuauCyclicRequireTypeInference + ? std::make_unique(constraintScope, function->name->location, GeneralizationConstraint{functionType, sig.signature}, sharedModuleName) + : std::make_unique(constraintScope, function->name->location, GeneralizationConstraint{functionType, sig.signature}); propagateDeprecatedAttributeToConstraint(c->c, function->func); - Constraint* previous = nullptr; - forEachConstraint( - start, - end, - this, - [&c, &previous](const ConstraintPtr& constraint) - { - c->dependencies.emplace_back(constraint.get()); - if (auto psc = get(*constraint); psc && psc->returns) - { - if (previous) - { - constraint->dependencies.emplace_back(previous); - } - - previous = constraint.get(); - } - } - ); - + addAllAsDependenciesAndChainReturns(start, end, this, NotNull{c.get()}); getMutable(functionType)->setOwner(addConstraint(scope, std::move(c))); module->astTypes[function->func] = functionType; @@ -1507,7 +1719,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f // With or without self Checkpoint start = checkpoint(this); - FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location); + FunctionSignature sig = checkFunctionSignature(scope, nullptr, function->func, /* expectedType */ std::nullopt, function->name->location); DefId def = dfg->getDef(function->name); @@ -1543,27 +1755,14 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f /* isSelf */ indexName->op == ':', } ); - forEachConstraint( - beginProp, - endProp, - this, - [pftc](const ConstraintPtr& c) - { - pftc->dependencies.emplace_back(c.get()); - } - ); + + addAllAsDependencies(beginProp, endProp, this, pftc); + auto beginBody = checkpoint(this); checkFunctionBody(sig.bodyScope, function->func); auto endBody = checkpoint(this); - forEachConstraint( - beginBody, - endBody, - this, - [pftc](const ConstraintPtr& c) - { - c->dependencies.push_back(pftc); - } - ); + + addAllAsReverseDependencies(beginBody, endBody, this, pftc); } else { @@ -1580,26 +1779,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f propagateDeprecatedAttributeToConstraint(c->c, function->func); - Constraint* previous = nullptr; - forEachConstraint( - start, - end, - this, - [&c, &previous](const ConstraintPtr& constraint) - { - c->dependencies.emplace_back(constraint.get()); - if (auto psc = get(*constraint); psc && psc->returns) - { - if (previous) - { - constraint->dependencies.emplace_back(previous); - } - - previous = constraint.get(); - } - } - ); - + addAllAsDependenciesAndChainReturns(start, end, this, c); std::optional existingFunctionTy = follow(lookup(scope, function->name->location, def)); if (AstExprLocal* localName = function->name->as()) @@ -1614,10 +1794,13 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f if (!existingFunctionTy) ice->ice("prepopulateGlobalScope did not populate a global name", globalName->location); - // Sketchy: We're specifically looking for BlockedTypes that were - // initially created by ConstraintGenerator::prepopulateGlobalScope. - if (auto bt = get(*existingFunctionTy); bt && nullptr == bt->getOwner()) + if (auto bt = get(*existingFunctionTy); bt && uninitializedGlobals.contains(globalName->name)) + { + LUAU_ASSERT(bt->getOwner() == nullptr); + uninitializedGlobals.erase(globalName->name); emplaceType(asMutable(*existingFunctionTy), generalizedType); + } + scope->bindings[globalName->name] = Binding{sig.signature, globalName->location}; scope->lvalueTypes[def] = sig.signature; @@ -1662,8 +1845,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatBlock* bloc // An AstStatBlock has linear control flow, i.e. one entry and one exit, so we can inherit // all the changes to the environment occurred by the statements in that block. - scope->inheritRefinements(innerScope); - scope->inheritAssignments(innerScope); + if (!FFlag::DebugLuauCFG) + { + scope->inheritRefinements(innerScope); + scope->inheritAssignments(innerScope); + } + return flow; } @@ -1735,39 +1922,54 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatCompoundAss ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatIf* ifStatement) { - RefinementId refinement = [&]() + if (FFlag::DebugLuauCFG) { - InConditionalContext flipper{&typeContext}; - return check(scope, ifStatement->condition, std::nullopt).refinement; - }(); - - ScopePtr thenScope = childScope(ifStatement->thenbody, scope); - applyRefinements(thenScope, ifStatement->condition->location, refinement); - - ScopePtr elseScope = childScope(ifStatement->elsebody ? ifStatement->elsebody : ifStatement, scope); - applyRefinements(elseScope, ifStatement->elseLocation.value_or(ifStatement->condition->location), refinementArena.negation(refinement)); - - ControlFlow thencf = visit(thenScope, ifStatement->thenbody); - ControlFlow elsecf = ControlFlow::None; - if (ifStatement->elsebody) - elsecf = visit(elseScope, ifStatement->elsebody); - - if (thencf != ControlFlow::None && elsecf == ControlFlow::None) - scope->inheritRefinements(elseScope); - else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) - scope->inheritRefinements(thenScope); - - if (thencf == ControlFlow::None) - scope->inheritAssignments(thenScope); - if (elsecf == ControlFlow::None) - scope->inheritAssignments(elseScope); - - if (thencf == elsecf) - return thencf; - else if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) - return ControlFlow::Returns; - else + check(scope, ifStatement->condition, std::nullopt); + ScopePtr thenScope = childScope(ifStatement->thenbody, scope); + visit(thenScope, ifStatement->thenbody); + if (ifStatement->elsebody) + { + ScopePtr elseScope = childScope(ifStatement->elsebody ? ifStatement->elsebody : ifStatement, scope); + visit(elseScope, ifStatement->elsebody); + } return ControlFlow::None; + } + else + { + RefinementId refinement = [&]() + { + InConditionalContext flipper{&typeContext}; + return check(scope, ifStatement->condition, std::nullopt).refinement; + }(); + + ScopePtr thenScope = childScope(ifStatement->thenbody, scope); + applyRefinements(thenScope, ifStatement->condition->location, refinement); + + ScopePtr elseScope = childScope(ifStatement->elsebody ? ifStatement->elsebody : ifStatement, scope); + applyRefinements(elseScope, ifStatement->elseLocation.value_or(ifStatement->condition->location), refinementArena.negation(refinement)); + + ControlFlow thencf = visit(thenScope, ifStatement->thenbody); + ControlFlow elsecf = ControlFlow::None; + if (ifStatement->elsebody) + elsecf = visit(elseScope, ifStatement->elsebody); + + if (thencf != ControlFlow::None && elsecf == ControlFlow::None) + scope->inheritRefinements(elseScope); + else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) + scope->inheritRefinements(thenScope); + + if (thencf == ControlFlow::None) + scope->inheritAssignments(thenScope); + if (elsecf == ControlFlow::None) + scope->inheritAssignments(elseScope); + + if (thencf == elsecf) + return thencf; + else if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + return ControlFlow::Returns; + else + return ControlFlow::None; + } } void ConstraintGenerator::resolveGenericDefaultParameters(const ScopePtr& defnScope, AstStatTypeAlias* alias, const TypeFun& fun) @@ -1885,7 +2087,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunctio ScopePtr environmentScope = *scopeIt; Checkpoint startCheckpoint = checkpoint(this); - FunctionSignature sig = checkFunctionSignature(environmentScope, function->body, /* expectedType */ std::nullopt); + FunctionSignature sig = checkFunctionSignature(environmentScope, nullptr, function->body, /* expectedType */ std::nullopt); // Place this function as a child of the non-type function scope scope->children.emplace_back(sig.signatureScope.get()); @@ -1897,40 +2099,19 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunctio NotNull gc = addConstraint( sig.signatureScope, function->location, - GeneralizationConstraint{ - generalizedTy, - sig.signature, - std::vector{}, - } - ); - - sig.signatureScope->interiorFreeTypes = std::move(interiorFreeTypes.back().types); - sig.signatureScope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); - - getMutable(generalizedTy)->setOwner(gc); - interiorFreeTypes.pop_back(); - - Constraint* previous = nullptr; - forEachConstraint( - startCheckpoint, - endCheckpoint, - this, - [gc, &previous](const ConstraintPtr& constraint) - { - gc->dependencies.emplace_back(constraint.get()); - - if (auto psc = get(*constraint); psc && psc->returns) - { - if (previous) - { - constraint->dependencies.emplace_back(previous); - } - - previous = constraint.get(); - } + GeneralizationConstraint{ + generalizedTy, + sig.signature, } ); + sig.signatureScope->interiorFreeTypes = std::move(interiorFreeTypes.back().types); + sig.signatureScope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); + + getMutable(generalizedTy)->setOwner(gc); + interiorFreeTypes.pop_back(); + + addAllAsDependenciesAndChainReturns(startCheckpoint, endCheckpoint, this, gc); std::optional existingFunctionTy = environmentScope->lookup(function->name); if (!existingFunctionTy) @@ -2031,58 +2212,39 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte } else { - if (FFlag::LuauStorePolarityInline) - { - // I don't think extern types can *be* generic, but if they - // have an indexer over those generics, the polarity is - // mixed. - etv->indexer = TableIndexer{ - resolveType( - scope, - declaredExternType->indexer->indexType, - /* inTypeArguments */ false, - /* replaceErrorWithFresh */ false, - /* initialPolarity */ Polarity::Mixed - ), - resolveType( - scope, - declaredExternType->indexer->resultType, - /* inTypeArguments */ false, - /* replaceErrorWithFresh */ false, - /* initialPolarity */ Polarity::Mixed - ), - }; - } - else - { - etv->indexer = TableIndexer{ - resolveType(scope, declaredExternType->indexer->indexType, /* inTypeArguments */ false), - resolveType(scope, declaredExternType->indexer->resultType, /* inTypeArguments */ false), - }; - } + // I don't think extern types can *be* generic, but if they + // have an indexer over those generics, the polarity is + // mixed. + etv->indexer = TableIndexer{ + resolveType( + scope, + declaredExternType->indexer->indexType, + /* inTypeArguments */ false, + /* replaceErrorWithFresh */ false, + /* initialPolarity */ Polarity::Mixed + ), + resolveType( + scope, + declaredExternType->indexer->resultType, + /* inTypeArguments */ false, + /* replaceErrorWithFresh */ false, + /* initialPolarity */ Polarity::Mixed + ), + }; } } - for (const AstDeclaredExternTypeProperty& prop : declaredExternType->props) + for (const AstDeclaredExternTypeProperty& externProp : declaredExternType->props) { - Name propName(prop.name.value); - TypeId propTy; - if (FFlag::LuauStorePolarityInline) - { - propTy = - resolveType(scope, prop.ty, /* inTypeArguments */ false, /* replaceErrorWithFresh */ false, /* initialPolarity */ Polarity::Mixed); - } - else - { - propTy = resolveType(scope, prop.ty, /* inTypeArguments */ false); - } - + Name propName(externProp.name.value); + TypeId propTy = + resolveType(scope, externProp.ty, /* inTypeArguments */ false, /* replaceErrorWithFresh */ false, /* initialPolarity */ Polarity::Mixed); bool assignToMetatable = isMetamethod(propName); // Function typeArguments always take 'self', but this isn't reflected in the // parsed annotation. Add it here. - if (prop.isMethod) + if (externProp.isMethod) { if (FunctionType* ftv = getMutable(propTy)) { @@ -2094,9 +2256,9 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte FunctionDefinition defn; defn.definitionModuleName = module->name; - defn.definitionLocation = prop.location; + defn.definitionLocation = externProp.location; // No data is preserved for varargLocation - defn.originalNameLocation = prop.nameLocation; + defn.originalNameLocation = externProp.nameLocation; ftv->definition = defn; } @@ -2106,11 +2268,23 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte if (props.count(propName) == 0) { - props[propName] = {propTy, /*deprecated*/ false, /*deprecatedSuggestion*/ "", prop.location}; + Property tableProp; + + if (externProp.access == AstTableAccess::Read) + tableProp = Property::readonly(propTy); + else if (externProp.access == AstTableAccess::Write) + tableProp = Property::writeonly(propTy); + else + tableProp = Property::rw(propTy); + + tableProp.location = externProp.location; + + props[propName] = tableProp; } else { Luau::Property& prop = props[propName]; + bool addedWriteTypeByOverload = false; if (auto readTy = prop.readTy) { @@ -2130,16 +2304,19 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte prop.readTy = intersection; } - else + else if (externProp.access == AstTableAccess::Write && !prop.writeTy.has_value()) { + prop.writeTy = propTy; + addedWriteTypeByOverload = true; + } + else reportError( declaredExternType->location, - GenericError{format("Cannot overload read type of non-function class member '%s'", propName.c_str())} + GenericError{format("Cannot overload read type of non-function extern type member '%s'", propName.c_str())} ); - } } - if (auto writeTy = prop.writeTy) + if (auto writeTy = prop.writeTy; writeTy && !addedWriteTypeByOverload) { // We special-case this logic to keep the intersection flat; otherwise we // would create a ton of nested intersection typeArguments. @@ -2157,13 +2334,13 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte prop.writeTy = intersection; } + else if (externProp.access == AstTableAccess::Read && !prop.readTy.has_value()) + prop.readTy = propTy; else - { reportError( declaredExternType->location, - GenericError{format("Cannot overload write type of non-function class member '%s'", propName.c_str())} + GenericError{format("Cannot overload write type of non-function extern type member '%s'", propName.c_str())} ); - } } } } @@ -2194,22 +2371,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareFunc if (!generics.empty() || !genericPacks.empty()) funScope = childScope(global, scope); - TypePackId paramPack; - TypePackId retPack; - if (FFlag::LuauStorePolarityInline) - { - paramPack = resolveTypePack( - funScope, global->params, /* inTypeArguments */ false, /* replaceErrorWithFresh */ false, /* initialPolarity */ Polarity::Negative - ); - retPack = resolveTypePack( - funScope, global->retTypes, /* inTypeArguments */ false, /* replaceErrorWithFresh */ false, /* initialPolarity */ Polarity::Positive - ); - } - else - { - paramPack = resolveTypePack(funScope, global->params, /* inTypeArguments */ false); - retPack = resolveTypePack(funScope, global->retTypes, /* inTypeArguments */ false); - } + TypePackId paramPack = resolveTypePack( + funScope, global->params, /* inTypeArguments */ false, /* replaceErrorWithFresh */ false, /* initialPolarity */ Polarity::Negative + ); + TypePackId retPack = resolveTypePack( + funScope, global->retTypes, /* inTypeArguments */ false, /* replaceErrorWithFresh */ false, /* initialPolarity */ Polarity::Positive + ); FunctionDefinition defn; @@ -2220,9 +2387,6 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareFunc TypeId fnType = arena->addType(FunctionType{TypeLevel{}, std::move(genericTys), std::move(genericTps), paramPack, retPack, defn}); - if (!FFlag::LuauStorePolarityInline) - inferGenericPolarities_DEPRECATED(arena, NotNull{scope.get()}, fnType); - FunctionType* ftv = getMutable(fnType); ftv->isCheckedFunction = global->isCheckedFunction(); AstAttr* deprecatedAttr = global->getAttribute(AstAttr::Type::Deprecated); @@ -2247,6 +2411,79 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareFunc return ControlFlow::None; } +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatClass* statClass) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + + auto* classDeclRecordPtr = classDeclRecords.find(statClass->name); + // TODO CLI-199124: This is unpopulated in fragment autocomplete. + if (classDeclRecordPtr == nullptr) + return ControlFlow::None; + + auto classDeclRecord = classDeclRecordPtr->get(); + + for (const auto& member : statClass->members) + { + Luau::visit( + overloaded{ + [&](const AstClassProperty& classProp) + { + auto entry = classDeclRecord->memberTypes.find(classProp.name); + if (entry == nullptr) + { + LUAU_ASSERT(!"Unexpected missing class property type"); + return; + } + + auto blockedTy = follow(*entry); + if (!is(blockedTy)) + return; + + auto target = classProp.ty ? resolveType(scope, classProp.ty, false) : builtinTypes->anyType; + emplaceType(asMutable(blockedTy), target); + }, + [&](const AstClassMethod& method) + { + auto entry = classDeclRecord->memberTypes.find(method.functionName); + if (entry == nullptr) + { + LUAU_ASSERT(!"Unexpected missing class method type"); + return; + } + + auto functionType = follow(*entry); + + // TODO: This might have strange behavior if you ever + // copy a method. + if (!is(functionType)) + return; + + FunctionSignature sig = + checkFunctionSignature(scope, classDeclRecord, method.function, /* expectedType */ std::nullopt, method.function->location); + + Checkpoint start = checkpoint(this); + checkFunctionBody(sig.bodyScope, method.function); + Checkpoint end = checkpoint(this); + + NotNull constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}; + std::unique_ptr c = FFlag::DebugLuauCyclicRequireTypeInference + ? std::make_unique(constraintScope, method.function->location, GeneralizationConstraint{functionType, sig.signature}, sharedModuleName) + : std::make_unique(constraintScope, method.function->location, GeneralizationConstraint{functionType, sig.signature}); + + propagateDeprecatedAttributeToConstraint(c->c, method.function); + + addAllAsDependenciesAndChainReturns(start, end, this, NotNull{c.get()}); + + getMutable(functionType)->setOwner(addConstraint(scope, std::move(c))); + } + }, + member + ); + } + + return ControlFlow::None; +} + ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatError* error) { for (AstStat* stat : error->statements) @@ -2446,6 +2683,20 @@ InferencePack ConstraintGenerator::checkExprCall( Checkpoint argEndCheckpoint = checkpoint(this); + if (FFlag::DebugLuauUserDefinedClasses) + { + if (auto instanceGuard = matchIsInstanceGuard(*call, dfg)) + { + if (args.size() >= 2) + { + // The class type may not be solved yet (e.g. `A.Point` from a + // required module). + TypeId objectofInst = createTypeFunctionInstance(builtinTypes->typeFunctions->objectofFunc, {args[1]}, {}, scope, call->location); + returnRefinements.emplace_back(refinementArena.implicitProposition(instanceGuard, objectofInst)); + } + } + } + if (matchSetMetatable(*call)) { TypePack argTailPack; @@ -2542,9 +2793,8 @@ InferencePack ConstraintGenerator::checkExprCall( TypePackId argPack = addTypePack(std::move(args), argTail); FunctionType ftv(TypeLevel{}, argPack, rets, std::nullopt, call->self); - auto [explicitTypeIds, explicitTypePackIds] = FFlag::LuauExplicitTypeInstantiationSupport && call->typeArguments.size - ? resolveTypeArguments(scope, call->typeArguments) - : std::pair, std::vector>(); + auto [explicitTypeIds, explicitTypePackIds] = + call->typeArguments.size ? resolveTypeArguments(scope, call->typeArguments) : std::pair, std::vector>(); /* * To make bidirectional type checking work, we need to solve these constraints in a particular order: @@ -2559,15 +2809,7 @@ InferencePack ConstraintGenerator::checkExprCall( scope, call->func->location, FunctionCheckConstraint{fnType, argPack, call, NotNull{&module->astTypes}, NotNull{&module->astExpectedTypes}} ); - forEachConstraint( - funcBeginCheckpoint, - funcEndCheckpoint, - this, - [checkConstraint](const ConstraintPtr& constraint) - { - checkConstraint->dependencies.emplace_back(constraint.get()); - } - ); + addAllAsDependencies(funcBeginCheckpoint, funcEndCheckpoint, this, checkConstraint); NotNull callConstraint = addConstraint( scope, @@ -2580,22 +2822,22 @@ InferencePack ConstraintGenerator::checkExprCall( std::move(discriminantTypes), std::move(explicitTypeIds), std::move(explicitTypePackIds), + FFlag::DebugLuauCyclicRequireTypeInference ? &module->astTypes : nullptr, &module->astOverloadResolvedTypes, } ); getMutable(rets)->owner = callConstraint.get(); - callConstraint->dependencies.push_back(checkConstraint); - + cgraph->addDependencyOf(checkConstraint, callConstraint); forEachConstraint( argBeginCheckpoint, argEndCheckpoint, this, - [checkConstraint, callConstraint](const ConstraintPtr& constraint) + [this, checkConstraint, callConstraint](const ConstraintPtr& constraint) { - constraint->dependencies.emplace_back(checkConstraint); - callConstraint->dependencies.emplace_back(constraint.get()); + cgraph->addDependencyOf(checkConstraint, constraint.get()); + cgraph->addDependencyOf(constraint.get(), callConstraint); } ); @@ -2630,6 +2872,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExpr* expr, std:: result = check(scope, stringExpr, expectedType, forceSingleton); else if (expr->is()) result = Inference{builtinTypes->numberType}; + else if (expr->is()) + result = Inference{builtinTypes->integerType}; else if (auto boolExpr = expr->as()) result = check(scope, boolExpr, expectedType, forceSingleton); else if (expr->is()) @@ -2661,10 +2905,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExpr* expr, std:: else if (auto interpString = expr->as()) result = check(scope, interpString); else if (auto explicitTypeInstantiation = expr->as()) - { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); result = check(scope, explicitTypeInstantiation); - } else if (auto err = expr->as()) { // Open question: Should we traverse into this? @@ -2708,8 +2949,16 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantStrin LUAU_ASSERT(ft); ft->lowerBound = arena->addType(SingletonType{StringSingleton{std::string{string->value.data, string->value.size}}}); ft->upperBound = builtinTypes->stringType; - - addConstraint(scope, string->location, PrimitiveTypeConstraint{freeTy, expectedType, builtinTypes->stringType}); + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + ft->primitiveType = builtinTypes->stringType; + if (expectedType) + addConstraint(scope, string->location, SubtypeConstraint{freeTy, *expectedType}); + } + else + { + addConstraint(scope, string->location, DEPRECATED_PrimitiveTypeConstraint{freeTy, expectedType, builtinTypes->stringType}); + } return Inference{freeTy}; } @@ -2738,32 +2987,45 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantBool* LUAU_ASSERT(ft); ft->lowerBound = singletonType; ft->upperBound = builtinTypes->booleanType; - - addConstraint(scope, boolExpr->location, PrimitiveTypeConstraint{freeTy, expectedType, builtinTypes->booleanType}); + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + ft->primitiveType = builtinTypes->booleanType; + if (expectedType) + addConstraint(scope, boolExpr->location, SubtypeConstraint{freeTy, *expectedType}); + } + else + { + addConstraint(scope, boolExpr->location, DEPRECATED_PrimitiveTypeConstraint{freeTy, expectedType, builtinTypes->booleanType}); + } return Inference{freeTy}; } Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) { - const RefinementKey* key = dfg->getRefinementKey(local); - LUAU_ASSERT(key); + if (FFlag::DebugLuauCFG) + return Inference{resolveRHSType(scope, local->location, local), nullptr}; + else + { + const RefinementKey* key = dfg->getRefinementKey(local); + LUAU_ASSERT(key); - std::optional maybeTy; + std::optional maybeTy; - // if we have a refinement key, we can look up its type. - if (key) - maybeTy = lookup(scope, local->location, key->def); + // if we have a refinement key, we can look up its type. + if (key) + maybeTy = lookup(scope, local->location, key->def); - if (maybeTy) - { - TypeId ty = follow(*maybeTy); + if (maybeTy) + { + TypeId ty = follow(*maybeTy); - recordInferredBinding(local->local, ty); + recordInferredBinding(local->local, ty); - return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)}; + return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)}; + } + else + ice->ice("CG: AstExprLocal came before its declaration?"); } - else - ice->ice("CG: AstExprLocal came before its declaration?"); } Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* global) @@ -2778,7 +3040,6 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* globa */ if (auto ty = lookup(scope, global->location, def, /*prototype=*/false)) { - rootScope->lvalueTypes[def] = *ty; return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; } else @@ -2885,7 +3146,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* fun InConditionalContext inContext(&typeContext, TypeContext::Default); Checkpoint startCheckpoint = checkpoint(this); - FunctionSignature sig = checkFunctionSignature(scope, func, expectedType); + FunctionSignature sig = checkFunctionSignature(scope, nullptr, func, expectedType); interiorFreeTypes.emplace_back(); checkFunctionBody(sig.bodyScope, func); @@ -2898,37 +3159,19 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* fun GeneralizationConstraint{ generalizedTy, sig.signature, - std::vector{}, } ); + if (FFlag::LuauDeprecatedAttributeOnAnonymousFunctions) + propagateDeprecatedAttributeToConstraint(gc->c, func); + sig.signatureScope->interiorFreeTypes = std::move(interiorFreeTypes.back().types); sig.signatureScope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); interiorFreeTypes.pop_back(); getMutable(generalizedTy)->setOwner(gc); - Constraint* previous = nullptr; - forEachConstraint( - startCheckpoint, - endCheckpoint, - this, - [gc, &previous](const ConstraintPtr& constraint) - { - gc->dependencies.emplace_back(constraint.get()); - - if (auto psc = get(*constraint); psc && psc->returns) - { - if (previous) - { - constraint->dependencies.emplace_back(previous); - } - - previous = constraint.get(); - } - } - ); - + addAllAsDependenciesAndChainReturns(startCheckpoint, endCheckpoint, this, gc); if (generalize && hasFreeType(sig.signature)) { return Inference{generalizedTy}; @@ -3107,9 +3350,6 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprInterpString* Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprInstantiate* explicitTypeInstantiation) { - if (!FFlag::LuauExplicitTypeInstantiationSupport) - return check(scope, explicitTypeInstantiation->expr); - TypeId functionType = check(scope, explicitTypeInstantiation->expr, std::nullopt).ty; auto [explicitTypeIds, explicitTypePackIds] = resolveTypeArguments(scope, explicitTypeInstantiation->typeArguments); @@ -3132,8 +3372,6 @@ std::pair, std::vector> ConstraintGenerator::res const AstArray& typeArguments ) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSupport); - std::vector resolvedTypeArguments; std::vector resolvedTypePackArguments; @@ -3219,6 +3457,8 @@ std::tuple ConstraintGenerator::checkBinary( discriminantTy = builtinTypes->stringType; else if (typeguard->type == "number") discriminantTy = builtinTypes->numberType; + else if (typeguard->type == "integer") + discriminantTy = builtinTypes->integerType; else if (typeguard->type == "boolean") discriminantTy = builtinTypes->booleanType; else if (typeguard->type == "thread") @@ -3234,8 +3474,13 @@ std::tuple ConstraintGenerator::checkBinary( // For now, we don't really care about being accurate with userdata if the typeguard was using typeof. discriminantTy = builtinTypes->externType; } - else if (!typeguard->isTypeof && typeguard->type == "vector") - discriminantTy = builtinTypes->neverType; // TODO: figure out a way to deal with this quirky type + else if (typeguard->type == "vector" && !typeguard->isTypeof) + { + // `vector` is defined in EmbeddedBuiltinDefinitions, not as an actual built-in type + auto typeFun = globalScope->lookupType("vector"); + if (typeFun) + discriminantTy = follow(typeFun->type); + } else if (!typeguard->isTypeof) discriminantTy = builtinTypes->neverType; else if (auto typeFun = globalScope->lookupType(typeguard->type); typeFun && typeFun->typeParams.empty() && typeFun->typePackParams.empty()) @@ -3307,6 +3552,19 @@ void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExpr* expr, Type void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprLocal* local, TypeId rhsType) { + if (FFlag::DebugLuauCFG) + { + TypeId assignTy = resolveLHSType(scope, local->location, CFG::LValue{static_cast(local)}); + localTypes.try_insert(assignTy, {}); + localTypes[assignTy].insert(rhsType); + + std::optional annotatedTy = scope->lookup(local->local); + if (annotatedTy) + addConstraint(scope, local->location, SubtypeConstraint{rhsType, *annotatedTy}); + + return; + } + std::optional annotatedTy = scope->lookup(local->local); LUAU_ASSERT(annotatedTy); @@ -3360,10 +3618,14 @@ void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprGlobal* glob if (annotatedTy == follow(rhsType)) return; - // Sketchy: We're specifically looking for BlockedTypes that were - // initially created by ConstraintGenerator::prepopulateGlobalScope. - if (auto bt = get(follow(*annotatedTy)); bt && !bt->getOwner()) - emplaceType(asMutable(*annotatedTy), rhsType); + auto followedAnnotation = follow(*annotatedTy); + if (auto bt = get(followedAnnotation); bt && uninitializedGlobals.contains(global->name)) + { + LUAU_ASSERT(bt->getOwner() == nullptr); + uninitializedGlobals.erase(global->name); + emplaceType(asMutable(followedAnnotation), rhsType); + } + addConstraint(scope, global->location, SubtypeConstraint{rhsType, *annotatedTy}); } @@ -3439,9 +3701,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, TypeIds valuesLowerBound; - std::optional start{std::nullopt}; - if (FFlag::LuauPushTypeConstraintLambdas3) - start = checkpoint(this); + Checkpoint start = checkpoint(this); for (const AstExprTable::Item& item : expr->items) { @@ -3457,14 +3717,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, // -- s should have type `string` here // end // } - TypeId itemTy = check( - scope, - item.value, - /* expectedType */ std::nullopt, - /* forceSingleton */ false, - /* generalize */ !FFlag::LuauPushTypeConstraintLambdas3 - ) - .ty; + TypeId itemTy = check(scope, item.value, /* expectedType */ std::nullopt, /* forceSingleton */ false, /* generalize */ false).ty; if (item.key) { @@ -3492,9 +3745,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, } } - std::optional end{std::nullopt}; - if (FFlag::LuauPushTypeConstraintLambdas3) - end = checkpoint(this); + Checkpoint end = checkpoint(this); if (!indexKeyLowerBound.empty()) { @@ -3539,19 +3790,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, /* expr */ NotNull{expr}, } ); - if (FFlag::LuauPushTypeConstraintLambdas3) - { - LUAU_ASSERT(start && end); - forEachConstraint( - *start, - *end, - this, - [ptc](const ConstraintPtr& c) - { - c->dependencies.emplace_back(ptc.get()); - } - ); - } + + addAllAsReverseDependencies(start, end, this, ptc); } if (FInt::LuauPrimitiveInferenceInTableLimit > 0 && expr->items.size > size_t(FInt::LuauPrimitiveInferenceInTableLimit)) @@ -3562,11 +3802,13 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignature( const ScopePtr& parent, + ClassDeclRecord* enclosingClass, AstExprFunction* fn, std::optional expectedType, std::optional originalName ) { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses || enclosingClass == nullptr); ScopePtr signatureScope = nullptr; ScopePtr bodyScope = nullptr; TypePackId returnType = nullptr; @@ -3639,30 +3881,73 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu genericTypePacks = expectedFunction->genericPacks; } - if (fn->self) + + bool hasExplicitSelf; + bool hasSelf; + + if (FFlag::DebugLuauUserDefinedClasses) { - TypeId selfType = freshType(signatureScope, Polarity::Negative); - argTypes.push_back(selfType); - argNames.emplace_back(FunctionArgument{fn->self->name.value, fn->self->location}); - signatureScope->bindings[fn->self] = Binding{selfType, fn->self->location}; + hasExplicitSelf = enclosingClass != nullptr && fn->args.size > 0 && fn->args.data[0]->name == "self"; + hasSelf = hasExplicitSelf || fn->self != nullptr; + + if (hasSelf) + { + TypeId selfType = nullptr; + if (enclosingClass != nullptr) + selfType = enclosingClass->ty; + else + selfType = freshType(signatureScope, Polarity::Negative); + + AstLocal* selfLocal = fn->self ? fn->self : hasExplicitSelf ? fn->args.data[0] : nullptr; + LUAU_ASSERT(selfLocal); + + argTypes.push_back(selfType); + argNames.emplace_back(FunctionArgument{selfLocal->name.value, selfLocal->location}); - DefId def = dfg->getDef(fn->self); - signatureScope->lvalueTypes[def] = selfType; - updateRValueRefinements(signatureScope, def, selfType); + signatureScope->bindings[selfLocal] = Binding{selfType, selfLocal->location}; + + DefId def = dfg->getDef(selfLocal); + signatureScope->lvalueTypes[def] = selfType; + updateRValueRefinements(signatureScope, def, selfType); + } + } + else + { + if (fn->self) + { + TypeId selfType = freshType(signatureScope, Polarity::Negative); + argTypes.push_back(selfType); + argNames.emplace_back(FunctionArgument{fn->self->name.value, fn->self->location}); + signatureScope->bindings[fn->self] = Binding{selfType, fn->self->location}; + + DefId def = dfg->getDef(fn->self); + signatureScope->lvalueTypes[def] = selfType; + updateRValueRefinements(signatureScope, def, selfType); + } } + for (size_t i = 0; i < fn->args.size; ++i) { + if (FFlag::DebugLuauUserDefinedClasses) + { + if (hasExplicitSelf && i == 0) + { + // It is forbidden to put a type annotation on the self + // parameter of a class method, but we still need to populate + // astResolvedTypes for TC2. + if (AstType* annotation = fn->args.data[0]->annotation) + resolveType(signatureScope, annotation, /* inTypeArguments */ false, /* replaceErrorWithFresh */ true, Polarity::Negative); + continue; + } + } + AstLocal* local = fn->args.data[i]; TypeId argTy = nullptr; if (local->annotation) { - if (FFlag::LuauStorePolarityInline) - argTy = - resolveType(signatureScope, local->annotation, /* inTypeArguments */ false, /* replaceErrorWithFresh*/ true, Polarity::Negative); - else - argTy = resolveType(signatureScope, local->annotation, /* inTypeArguments */ false, /* replaceErrorWithFresh*/ true); + argTy = resolveType(signatureScope, local->annotation, /* inTypeArguments */ false, /* replaceErrorWithFresh*/ true, Polarity::Negative); } else { @@ -3732,19 +4017,10 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu varargPack = follow(varargPack); returnType = follow(returnType); - if (FFlag::LuauDontIncludeVarargWithAnnotation) - { - if (!fn->varargAnnotation) - genericTypePacks.push_back(varargPack); - if (!fn->returnAnnotation) - genericTypePacks.push_back(returnType); - } - else - { + if (!fn->varargAnnotation) genericTypePacks.push_back(varargPack); - if (varargPack != returnType) - genericTypePacks.push_back(returnType); - } + if (!fn->returnAnnotation) + genericTypePacks.push_back(returnType); // If there is both an annotation and an expected type, the annotation wins. // Type checking will sort out any discrepancies later. @@ -3769,7 +4045,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu actualFunction.generics = std::move(genericTypes); actualFunction.genericPacks = std::move(genericTypePacks); actualFunction.argNames = std::move(argNames); - actualFunction.hasSelf = fn->self != nullptr; + actualFunction.hasSelf = FFlag::DebugLuauUserDefinedClasses ? hasSelf : fn->self != nullptr; FunctionDefinition defn; defn.definitionModuleName = module->name; @@ -3782,13 +4058,13 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu LUAU_ASSERT(actualFunctionType); module->astTypes[fn] = actualFunctionType; - if (!FFlag::LuauStorePolarityInline) - inferGenericPolarities_DEPRECATED(arena, NotNull{signatureScope.get()}, actualFunctionType); - if (expectedType && get(*expectedType)) bindFreeType(*expectedType, actualFunctionType); - scopeToFunction[signatureScope.get()] = actualFunctionType; + if (FFlag::DebugLuauCyclicRequireTypeInference) + cgraph->scopeToFunction[signatureScope.get()] = actualFunctionType; + else + scopeToFunction[signatureScope.get()] = actualFunctionType; return { /* signature */ actualFunctionType, @@ -3907,11 +4183,8 @@ TypeId ConstraintGenerator::resolveReferenceType( addConstraint(scope, ty->location, ReduceConstraint{result}); } - if (FFlag::LuauStorePolarityInline) - { - if (auto genericType = getMutable(follow(result))) - genericType->polarity = (genericType->polarity & Polarity::Mixed) | polarity; - } + if (auto genericType = getMutable(follow(result))) + genericType->polarity = (genericType->polarity & Polarity::Mixed) | polarity; return result; } @@ -3939,103 +4212,63 @@ TypeId ConstraintGenerator::resolveTableType(const ScopePtr& scope, AstType* ty, TableType::Props props; std::optional indexer; - if (FFlag::LuauStorePolarityInline) + Polarity p = polarity; + for (const AstTableProp& prop : tab->props) { - Polarity p = polarity; - for (const AstTableProp& prop : tab->props) - { - Property& propRef = props[prop.name.value]; - - // Set the polarity for the inner type - polarity = polarityOfAccess(prop.access, p); + Property& propRef = props[prop.name.value]; - TypeId propTy = resolveType_(scope, prop.type, inTypeArguments); + // Set the polarity for the inner type + polarity = polarityOfAccess(prop.access, p); - propRef.typeLocation = prop.location; + TypeId propTy = resolveType_(scope, prop.type, inTypeArguments); - switch (prop.access) - { - case AstTableAccess::ReadWrite: - propRef.readTy = propTy; - propRef.writeTy = propTy; - break; - case AstTableAccess::Read: - propRef.readTy = propTy; - break; - case AstTableAccess::Write: - propRef.writeTy = propTy; - break; - default: - ice->ice("Unexpected property access " + std::to_string(int(prop.access))); - break; - } - } + propRef.typeLocation = prop.location; - if (AstTableIndexer* astIndexer = tab->indexer) + switch (prop.access) { - if (astIndexer->access == AstTableAccess::Read) - reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"read keyword is illegal here"}); - else if (astIndexer->access == AstTableAccess::Write) - reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"}); - else if (astIndexer->access == AstTableAccess::ReadWrite) - { - polarity = Polarity::Mixed; - indexer = TableIndexer{ - resolveType_(scope, astIndexer->indexType, inTypeArguments), - resolveType_(scope, astIndexer->resultType, inTypeArguments), - }; - } - else - ice->ice("Unexpected property access " + std::to_string(int(astIndexer->access))); + case AstTableAccess::ReadWrite: + propRef.readTy = propTy; + propRef.writeTy = propTy; + break; + case AstTableAccess::Read: + propRef.readTy = propTy; + break; + case AstTableAccess::Write: + propRef.writeTy = propTy; + break; + default: + ice->ice("Unexpected property access " + std::to_string(int(prop.access))); + break; } - - polarity = p; } - else + + if (AstTableIndexer* astIndexer = tab->indexer) { - for (const AstTableProp& prop : tab->props) + if (astIndexer->access == AstTableAccess::Read) { - TypeId propTy = resolveType_(scope, prop.type, inTypeArguments); - - Property& p = props[prop.name.value]; - p.typeLocation = prop.location; - - switch (prop.access) - { - case AstTableAccess::ReadWrite: - p.readTy = propTy; - p.writeTy = propTy; - break; - case AstTableAccess::Read: - p.readTy = propTy; - break; - case AstTableAccess::Write: - p.writeTy = propTy; - break; - default: - ice->ice("Unexpected property access " + std::to_string(int(prop.access))); - break; - } + polarity = p; + indexer = TableIndexer{ + resolveType_(scope, astIndexer->indexType, inTypeArguments), + resolveType_(scope, astIndexer->resultType, inTypeArguments), + /*isReadOnly*/ true + }; } - - if (AstTableIndexer* astIndexer = tab->indexer) + else if (astIndexer->access == AstTableAccess::Write) + reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"}); + else if (astIndexer->access == AstTableAccess::ReadWrite) { - if (astIndexer->access == AstTableAccess::Read) - reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"read keyword is illegal here"}); - else if (astIndexer->access == AstTableAccess::Write) - reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"}); - else if (astIndexer->access == AstTableAccess::ReadWrite) - { - indexer = TableIndexer{ - resolveType(scope, astIndexer->indexType, inTypeArguments), - resolveType(scope, astIndexer->resultType, inTypeArguments), - }; - } - else - ice->ice("Unexpected property access " + std::to_string(int(astIndexer->access))); + polarity = Polarity::Mixed; + indexer = TableIndexer{ + resolveType_(scope, astIndexer->indexType, inTypeArguments), + resolveType_(scope, astIndexer->resultType, inTypeArguments), + }; } + else + ice->ice("Unexpected property access " + std::to_string(int(astIndexer->access))); } + polarity = p; + TypeId tableTy = arena->addType(TableType{props, indexer, scope->level, scope.get(), TableState::Sealed}); TableType* ttv = getMutable(tableTy); @@ -4089,21 +4322,11 @@ TypeId ConstraintGenerator::resolveFunctionType( AstTypePackExplicit tempArgTypes{Location{}, fn->argTypes}; - TypePackId argTypes; - TypePackId returnTypes; - if (FFlag::LuauStorePolarityInline) - { - Polarity p = polarity; - polarity = invert(polarity); - argTypes = resolveTypePack_(signatureScope, &tempArgTypes, inTypeArguments, replaceErrorWithFresh); - polarity = p; - returnTypes = resolveTypePack_(signatureScope, fn->returnTypes, inTypeArguments, replaceErrorWithFresh); - } - else - { - argTypes = resolveTypePack_(signatureScope, &tempArgTypes, inTypeArguments, replaceErrorWithFresh); - returnTypes = resolveTypePack_(signatureScope, fn->returnTypes, inTypeArguments, replaceErrorWithFresh); - } + Polarity p = polarity; + polarity = invert(polarity); + TypePackId argTypes = resolveTypePack_(signatureScope, &tempArgTypes, inTypeArguments, replaceErrorWithFresh); + polarity = p; + TypePackId returnTypes = resolveTypePack_(signatureScope, fn->returnTypes, inTypeArguments, replaceErrorWithFresh); // TODO: FunctionType needs a pointer to the scope so that we know // how to quantify/instantiate it. @@ -4145,18 +4368,9 @@ TypeId ConstraintGenerator::resolveType( Polarity initialPolarity ) { - if (FFlag::LuauStorePolarityInline) - { - // Reset the polarity - polarity = initialPolarity; - return resolveType_(scope, ty, inTypeArguments, replaceErrorWithFresh); - } - else - { - TypeId result = resolveType_(scope, ty, inTypeArguments, replaceErrorWithFresh); - inferGenericPolarities_DEPRECATED(arena, NotNull{scope.get()}, result); - return result; - } + // Reset the polarity + polarity = initialPolarity; + return resolveType_(scope, ty, inTypeArguments, replaceErrorWithFresh); } TypeId ConstraintGenerator::resolveType_(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) @@ -4233,12 +4447,7 @@ TypeId ConstraintGenerator::resolveType_(const ScopePtr& scope, AstType* ty, boo { result = builtinTypes->errorType; if (replaceErrorWithFresh) - { - if (FFlag::LuauStorePolarityInline) - result = freshType(scope, polarity); - else - result = freshType(scope); - } + result = freshType(scope, polarity); } else { @@ -4258,17 +4467,8 @@ TypePackId ConstraintGenerator::resolveTypePack( Polarity initialPolarity ) { - if (FFlag::LuauStorePolarityInline) - { - polarity = initialPolarity; - return resolveTypePack_(scope, tp, inTypeArgument, replaceErrorWithFresh); - } - else - { - TypePackId result = resolveTypePack_(scope, tp, inTypeArgument, replaceErrorWithFresh); - inferGenericPolarities_DEPRECATED(arena, NotNull{scope.get()}, result); - return result; - } + polarity = initialPolarity; + return resolveTypePack_(scope, tp, inTypeArgument, replaceErrorWithFresh); } TypePackId ConstraintGenerator::resolveTypePack_(const ScopePtr& scope, AstTypePack* tp, bool inTypeArgument, bool replaceErrorWithFresh) @@ -4276,7 +4476,7 @@ TypePackId ConstraintGenerator::resolveTypePack_(const ScopePtr& scope, AstTypeP TypePackId result; if (auto expl = tp->as()) { - result = resolveTypePack(scope, expl->typeList, inTypeArgument, replaceErrorWithFresh); + result = resolveTypePack_(scope, expl->typeList, inTypeArgument, replaceErrorWithFresh); } else if (auto var = tp->as()) { @@ -4301,32 +4501,20 @@ TypePackId ConstraintGenerator::resolveTypePack_(const ScopePtr& scope, AstTypeP result = builtinTypes->errorTypePack; } - if (FFlag::LuauStorePolarityInline) + if (auto gtp = getMutable(follow(result))) { - if (auto gtp = getMutable(follow(result))) - { - // The initial polarity is unknown, so we flip that bit off - // by saying that we are at most Mixed, and then add in the - // polarity we're currently processing. - gtp->polarity = (gtp->polarity & Polarity::Mixed) | polarity; - } + // The initial polarity is unknown, so we flip that bit off + // by saying that we are at most Mixed, and then add in the + // polarity we're currently processing. + gtp->polarity = (gtp->polarity & Polarity::Mixed) | polarity; } module->astResolvedTypePacks[tp] = result; return result; } -TypePackId ConstraintGenerator::resolveTypePack( - const ScopePtr& scope, - const AstTypeList& list, - bool inTypeArguments, - bool replaceErrorWithFresh, - Polarity initialPolarity -) +TypePackId ConstraintGenerator::resolveTypePack_(const ScopePtr& scope, const AstTypeList& list, bool inTypeArguments, bool replaceErrorWithFresh) { - if (FFlag::LuauStorePolarityInline) - polarity = initialPolarity; - std::vector head; for (AstType* headTy : list.types) @@ -4340,10 +4528,19 @@ TypePackId ConstraintGenerator::resolveTypePack( tail = resolveTypePack_(scope, list.tailType, inTypeArguments, replaceErrorWithFresh); } - TypePackId result = addTypePack(std::move(head), tail); - if (!FFlag::LuauStorePolarityInline) - inferGenericPolarities_DEPRECATED(arena, NotNull{scope.get()}, result); - return result; + return addTypePack(std::move(head), tail); +} + +TypePackId ConstraintGenerator::resolveTypePack( + const ScopePtr& scope, + const AstTypeList& list, + bool inTypeArguments, + bool replaceErrorWithFresh, + Polarity initialPolarity +) +{ + polarity = initialPolarity; + return resolveTypePack_(scope, list, inTypeArguments, replaceErrorWithFresh); } std::vector> ConstraintGenerator::createGenerics( @@ -4363,10 +4560,7 @@ std::vector> ConstraintGenerator::createG genericTy = it->second; else { - if (FFlag::LuauStorePolarityInline) - genericTy = arena->addType(GenericType{scope.get(), generic->name.value, Polarity::None}); - else - genericTy = arena->addType(GenericType{scope.get(), generic->name.value}); + genericTy = arena->addType(GenericType{scope.get(), generic->name.value, Polarity::None}); scope->parent->typeAliasTypeParameters[generic->name.value] = genericTy; } @@ -4401,10 +4595,7 @@ std::vector> ConstraintGenerator::cre genericTy = it->second; else { - if (FFlag::LuauStorePolarityInline) - genericTy = arena->addTypePack(TypePackVar{GenericTypePack{scope.get(), generic->name.value, Polarity::None}}); - else - genericTy = arena->addTypePack(TypePackVar{GenericTypePack{scope.get(), generic->name.value}}); + genericTy = arena->addTypePack(TypePackVar{GenericTypePack{scope.get(), generic->name.value, Polarity::None}}); scope->parent->typeAliasTypePackParameters[generic->name.value] = genericTy; } @@ -4497,6 +4688,8 @@ struct GlobalPrepopulator : AstVisitor const NotNull arena; const NotNull dfg; + DenseHashSet uninitializedGlobals{{}}; + GlobalPrepopulator(NotNull globalScope, NotNull arena, NotNull dfg) : globalScope(globalScope) , arena(arena) @@ -4527,6 +4720,7 @@ struct GlobalPrepopulator : AstVisitor if (globalScope->bindings.find(g->name) == globalScope->bindings.end()) { TypeId bt = arena->addType(BlockedType{}); + uninitializedGlobals.insert(g->name); globalScope->bindings[g->name] = Binding{bt, g->location}; } } @@ -4540,6 +4734,7 @@ struct GlobalPrepopulator : AstVisitor if (AstExprGlobal* g = function->name->as()) { TypeId bt = arena->addType(BlockedType{}); + uninitializedGlobals.insert(g->name); globalScope->bindings[g->name] = Binding{bt}; } @@ -4562,6 +4757,9 @@ void ConstraintGenerator::prepopulateGlobalScopeForFragmentTypecheck(const Scope // Handle type function globals as well, without preparing a module scope since they have a separate environment GlobalPrepopulator tfgp{NotNull{typeFunctionRuntime->rootScope.get()}, arena, dfg}; program->visit(&tfgp); + + for (auto name : tfgp.uninitializedGlobals) + uninitializedGlobals.insert(name); } void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) @@ -4573,9 +4771,15 @@ void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, As program->visit(&gp); + for (auto name : gp.uninitializedGlobals) + uninitializedGlobals.insert(name); + // Handle type function globals as well, without preparing a module scope since they have a separate environment GlobalPrepopulator tfgp{NotNull{typeFunctionRuntime->rootScope.get()}, arena, dfg}; program->visit(&tfgp); + + for (auto name : tfgp.uninitializedGlobals) + uninitializedGlobals.insert(name); } bool ConstraintGenerator::recordPropertyAssignment(TypeId ty) diff --git a/Analysis/src/ConstraintGraph.cpp b/Analysis/src/ConstraintGraph.cpp new file mode 100644 index 00000000..b5699654 --- /dev/null +++ b/Analysis/src/ConstraintGraph.cpp @@ -0,0 +1,615 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/ConstraintGraph.h" +#include "Luau/TypeUtils.h" + +#include +#include + +LUAU_FASTFLAG(DebugLuauLogSolver) +LUAU_FASTFLAG(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + +namespace Luau +{ + +namespace +{ +[[maybe_unused]] bool canMutate(TypeId ty, NotNull constraint) +{ + if (auto blocked = get(ty)) + { + const Constraint* owner = blocked->getOwner(); + LUAU_ASSERT(owner); + return owner == constraint; + } + + return true; +} + +[[maybe_unused]] static bool canMutate(TypePackId tp, NotNull constraint) +{ + if (auto blocked = get(tp)) + { + Constraint* owner = blocked->owner; + LUAU_ASSERT(owner); + return owner == constraint; + } + + return true; +} + +} // namespace + +size_t HashBlockedConstraintId::operator()(const BlockedConstraintId& bci) const +{ + size_t result = 0; + + if (const TypeId* ty = get_if(&bci)) + result = std::hash()(*ty); + else if (const TypePackId* tp = get_if(&bci)) + result = std::hash()(*tp); + else if (Constraint const* const* c = get_if(&bci)) + result = std::hash()(*c); + else + LUAU_ASSERT(!"Should be unreachable"); + + return result; +} + +ConstraintList::Iterator::Iterator(NotNull cl, size_t index) + : cl{cl} + , index{index} +{ + advanceUntilPresentOrEnd(); +} + +ConstraintList::Iterator& ConstraintList::Iterator::operator++() +{ + if (index < cl->order.size()) + { + index++; + advanceUntilPresentOrEnd(); + } + return *this; +} + +bool ConstraintList::Iterator::operator==(const Iterator& rhs) const +{ + return cl == rhs.cl && index == rhs.index; +} + +bool ConstraintList::Iterator::operator!=(const Iterator& rhs) const +{ + return !(*this == rhs); +} + +ConstraintList::Iterator::value_type ConstraintList::Iterator::operator*() +{ + return cl->order[index]; +} + +void ConstraintList::Iterator::advanceUntilPresentOrEnd() +{ + while (index < cl->order.size() && !cl->contains(cl->order[index])) + index++; + return; +} + +ConstraintGraph::ConstraintGraph(NotNull builtinTypes) + : builtinTypes(builtinTypes) +{ +} + +bool ConstraintList::contains(ConstraintVertex vertex) const +{ + if (auto entry = present.find(vertex)) + return *entry; + return false; +} + +void ConstraintList::insert(ConstraintVertex vertex) +{ + auto [entry, fresh] = present.try_insert(vertex, true); + if (fresh) + { + order.emplace_back(vertex); + entries++; + } + else if (!entry) + { + entry = true; + entries++; + } + // If the entry was *not* fresh and its value was already true, then do + // nothing: the set state has not changed. +} + +void ConstraintList::remove(ConstraintVertex vertex) +{ + if (auto entry = present.find(vertex)) + { + // If the entry is true then we also need to decrement the number of + // entries in the constraint list. + if (*entry) + entries--; + *entry = false; + } +} + +size_t ConstraintList::size() const +{ + return entries; +} + +void ConstraintList::clear() +{ + order.clear(); + present.clear(); + entries = 0; +} + +ConstraintList::Iterator ConstraintList::begin() +{ + return Iterator{NotNull{this}, 0}; +} + +ConstraintList::Iterator ConstraintList::end() +{ + return Iterator{NotNull{this}, order.size()}; +} + +bool ConstraintGraph::addDependencyOf(ConstraintVertex dependency, ConstraintVertex target) +{ + auto deps = findDependencyList(target); + auto reverseDeps = findReverseDependencyList(dependency); + + if (deps->contains(dependency)) + { + // If we are claiming this is not a fresh block, we _better_ be + // tracking the reverse dependency as well! + LUAU_ASSERT(reverseDeps->contains(target)); + return false; + } + + deps->insert(dependency); + reverseDeps->insert(target); + return true; +} + +bool ConstraintGraph::addDependencyOf(Constraint* dependency, Constraint* target) +{ + return addDependencyOf(static_cast(dependency), static_cast(target)); +} + +/** + * Let's say we have nodes A, B, C, and D (where => means "depends on") + * + * A, B, C => D + * + * As part of dispatching D, we need to mint E. This function sets us up such that: + * + * A, B, C => E + * + */ +void ConstraintGraph::inheritBlocks(ConstraintVertex existingVertex, ConstraintVertex newVertex) +{ + auto existingReverseDeps = findReverseDependencyList(existingVertex); + auto newReverseDeps = findReverseDependencyList(newVertex); + + // For each reverse dependency of [existingVertex] ... + for (auto existingRdep : *existingReverseDeps) + { + // ... add it as a reverse dependency of [newVertex] + newReverseDeps->insert(existingRdep); + /// ... and add [newVertex] as a dependency. + auto newDeps = findDependencyList(existingRdep); + newDeps->insert(newVertex); + } +} + +void ConstraintGraph::unblockTypeOrPack(TypeId vertex) +{ + repairTypeReferences(vertex); + clearReverseDependenciesOf(follow(vertex)); +} + +void ConstraintGraph::unblockTypeOrPack(TypePackId vertex) +{ + repairTypeReferences(vertex); + clearReverseDependenciesOf(follow(vertex)); +} + +ConstraintGraph::UnblockedTypes ConstraintGraph::unblockConstraint(NotNull c) +{ + UnblockedTypes result; + + // The reverse dependencies of this constraint should contain all of the types + // and type packs that this constraint may mutate, either as a free type or + // as a blocked type. + auto reverseDeps = findReverseDependencyList(c.get()); + for (auto rdep : *reverseDeps) + { + if (auto ty = rdep.get_if()) + { + result.types.insert(*ty); + auto deps = findDependencyList(*ty); + deps->remove(c.get()); + } + else if (auto tp = rdep.get_if()) + { + result.packs.insert(*tp); + auto deps = findDependencyList(*tp); + deps->remove(c.get()); + } + else if (auto depCons = rdep.get_if()) + { + auto deps = findDependencyList(*depCons); + deps->remove(c.get()); + if (FFlag::DebugLuauLogSolver) + printf("Unblocking count=%d\t%s\n", int(deps->size()), toString(**depCons, {/* exhaustive */ true}).c_str()); + } + else + { + LUAU_ASSERT(!"Unknown constraint graph vertex."); + } + } + + /** + * This whole song and dance is to repair the constraint graph after we + * dispatch a constraint. + * + * We are assuming that, after a constraint has been dispatched, some + * number of mutations have been made to the type graph. Importantly: if a + * type has been mutated, then it was previously a reverse dependency of + * [c]. If that is the case, then we can walk the reverse deps of [c] and + * try to find bound types, shift their references over to their bounds, + * and "repair" the dependency graph without having to track every single + * [bind] call. + * + * This means that any [emplaceType] outside this file is subject to drift, + * but it is safe as long as it occurs while the type being mutated is in + * the reverse dependency set of the constraint being dispatched. + * + * We do this in two steps to ensure that [c] does not exist as a + * dependency of *any* type while repairing references. An alternative + * implementation would be to pass [c] to [repairTypeReferences] and know + * *not* to transfer it as a dependency. + */ + + for (TypeId type : result.types) + repairTypeReferences(type); + + for (TypePackId typePack : result.packs) + repairTypeReferences(typePack); + + return result; +} + +bool ConstraintGraph::hasUnsolvedDependencies(ConstraintVertex vertex) +{ + auto deps = findDependencyList(vertex); + if (!FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + if (auto c = vertex.get_if()) + { + if (auto ptc = (*c)->c.get_if()) + return deps->size() > 1; + } + } + return deps->size() > 0; +} + +bool ConstraintGraph::DEPRECATED_hasStrictlyMoreThanOneDependency(ConstraintVertex vertex) +{ + LUAU_ASSERT(!FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier); + auto deps = findDependencyList(vertex); + return deps->size() > 1; +} + +/** + * For every vertex V and type T, we want to claim that T depends on V: + * - Add a forward edge in [dependencies] from T to V + * - Add a backwards edge in [reverseDependencies] from V to T + * Additionally, the original vertex [originalVertex] should be removed + * from the reverse dependency list if given. + */ +void ConstraintGraph::copyDependenciesToReachableTypes( + std::optional originalVertex, + NotNull sourceDependencies, + TypeIds mutatedTypes, + TypePackIds mutatedTypePacks +) +{ + for (const auto& vertex : *sourceDependencies) + { + // NOTE: Technically we could express this function solely in terms + // of [addDependencyOf], but we save some cycles by fetching the + // reverse dependency list once. + auto vertexReverseDeps = findReverseDependencyList(vertex); + + if (originalVertex) + vertexReverseDeps->remove(*originalVertex); + + for (auto subTarget : mutatedTypes) + { + auto tyDeps = findDependencyList(subTarget); + // Add this vertex to the list of dependencies for this type. + tyDeps->insert(vertex); + // ... and then add the same backwards edge. + vertexReverseDeps->insert(subTarget); + } + + for (auto subPackTarget : mutatedTypePacks) + { + auto tpDeps = findDependencyList(subPackTarget); + // Add this vertex to the list of dependencies for this type. + tpDeps->insert(vertex); + // ... and then add the same backwards edge. + vertexReverseDeps->insert(subPackTarget); + } + } +} + +void ConstraintGraph::clearReverseDependenciesOf(ConstraintVertex vertex) +{ + LUAU_ASSERT(vertex.get_if() == nullptr); + + auto revDeps = findReverseDependencyList(vertex); + + // For all of the reverse dependencies of vertex (vertices that depend on vertex) ... + for (auto rdep : *revDeps) + { + // Remove vertex from the list of dependencies. + // TODO CLI-205496: We should assert that deps contains `vertex` + auto deps = findDependencyList(rdep); + deps->remove(vertex); + } + + // Then clear this set. + revDeps->clear(); +} + +template +void ConstraintGraph::shiftReferences(T source, T target) +{ + static_assert(std::is_same_v || std::is_same_v, "Shift references can only be used with types or type packs."); + if (source == target) + return; + + auto sourceDependencies = findDependencyList(source); + + TypeIds mutatedTypes; + TypePackIds mutatedTypePacks; + ReferenceCountInitializer rci{NotNull{&mutatedTypes}, NotNull{&mutatedTypePacks}}; + rci.traverse(target); + copyDependenciesToReachableTypes(source, sourceDependencies, std::move(mutatedTypes), std::move(mutatedTypePacks)); + + // Types in the constraint graph are always dynamically discovered, so + // when we shift a reference over, we'll remove it from the dependencies + // of our reverse dependencies and then delete the edge from the graph. + clearReverseDependenciesOf(source); +} + +template void ConstraintGraph::shiftReferences(TypePackId source, TypePackId target); +template void ConstraintGraph::shiftReferences(TypeId source, TypeId target); + +template +void ConstraintGraph::repairTypeReferences(T ty) +{ + static_assert(std::is_same_v || std::is_same_v, "Repair type references can only be used with types or type packs."); + + T root = follow(ty); + + // This is a strong guard against a self bound cylic type, but we + // hopefully threw an exception above if this were the case. + DenseHashSet seen{nullptr}; + seen.insert(root); + + while (!seen.contains(ty)) + { + seen.insert(ty); + // This ensures: + // 1. Any constraint that may mutate `vertex` will now signal that it + // mutates `root` + // 2. Any constraint waiting on `vertex` will be unblocked. + shiftReferences(ty, root); + if constexpr (std::is_same_v) + { + if (auto bt = get(ty)) + ty = bt->boundTo; + } + else if constexpr (std::is_same_v) + { + if (auto bt = get(ty)) + ty = bt->boundTo; + } + } +} + +template void ConstraintGraph::repairTypeReferences(TypeId ty); +template void ConstraintGraph::repairTypeReferences(TypePackId ty); + +template +void ConstraintGraph::copyDependenciesOf(T source, T target) +{ + static_assert(std::is_same_v || std::is_same_v, "Copying dependencies can only be used with types or type packs."); + auto sourceDependencies = findDependencyList(source); + TypeIds mutatedTypes; + TypePackIds mutatedTypePacks; + ReferenceCountInitializer rci{NotNull{&mutatedTypes}, NotNull{&mutatedTypePacks}}; + rci.traverse(target); + // We do not want to _delete_ the original vertex, so we pass nullopt here. + copyDependenciesToReachableTypes(std::nullopt, sourceDependencies, std::move(mutatedTypes), std::move(mutatedTypePacks)); +} + +template void ConstraintGraph::copyDependenciesOf(TypeId source, TypeId target); +template void ConstraintGraph::copyDependenciesOf(TypePackId source, TypePackId target); + +NotNull ConstraintGraph::findDependencyList(ConstraintVertex vertex) +{ + if (auto dep = dependencies.find(vertex)) + return NotNull{*dep}; + + auto newlist = NotNull{constraintLists.emplace_back(new ConstraintList()).get()}; + + auto [it, fresh] = dependencies.try_insert(vertex, newlist.get()); + LUAU_ASSERT(fresh); + return NotNull{newlist}; +} + +NotNull ConstraintGraph::findReverseDependencyList(ConstraintVertex vertex) +{ + if (auto rdep = reverseDependencies.find(vertex)) + return NotNull{*rdep}; + + auto newlist = NotNull{constraintLists.emplace_back(new ConstraintList()).get()}; + + auto [it, fresh] = reverseDependencies.try_insert(vertex, newlist.get()); + LUAU_ASSERT(fresh); + return NotNull{newlist}; +} + +namespace +{ +std::string toString(ConstraintVertex vertex) +{ + return Luau::visit( + overloaded{ + [&](TypeId ty) + { + return "Type " + toString(ty, {/* exhaustive */ true}); + }, + [&](TypePackId tp) + { + return "Type pack " + toString(tp, {/* exhaustive */ true}); + }, + [&](const Constraint* c) + { + return "Cons " + toString(*c, {/* exhaustive */ true}); + } + }, + vertex + ); +} +} // namespace + + +std::string dump(ConstraintVertex vertex) +{ + auto out = toString(vertex); + printf("%s\n", out.c_str()); + return out; +} + +void dotEscape(std::ostream& os, const std::string& s) +{ + os << "\""; + for (char c : s) + { + switch (c) + { + case '"': + os << "\\\""; + break; + case '\\': + os << "\\\\"; + break; + case '\n': + os << "\\n"; + break; + case '<': + os << "\\<"; + break; + case '>': + os << "\\>"; + break; + case '{': + os << "\\{"; + break; + case '}': + os << "\\}"; + break; + case '|': + os << "\\|"; + break; + default: + os << c; + break; + } + } + os << "\""; +} + +void ConstraintGraph::dump() +{ + for (auto [v, deps] : dependencies) + { + auto vstr = toString(v); + for (auto d : *deps) + { + dotEscape(std::cout, vstr); + std::cout << " -> "; + dotEscape(std::cout, toString(d)); + std::cout << std::endl; + } + } +} + +void ConstraintGraph::dumpBlocked(NotNull c, ToStringOptions& opts) +{ + printf("Blocked on:\n"); + auto deps = findDependencyList(c.get()); + for (auto dep : *deps) + { + Luau::visit( + overloaded{ + [&](TypeId ty) + { + printf("\tType %s\n", toString(ty, opts).c_str()); + }, + [&](TypePackId tp) + { + printf("\tPack %s\n", toString(tp, opts).c_str()); + }, + [&](const Constraint* c) + { + printf("\tCons %s\n", toString(*c, opts).c_str()); + } + }, + dep + ); + } +} + +void ConstraintGraph::dumpWith(const std::vector>& unsolvedConstraints, ToStringOptions& opts) +{ + // TODO: It might be nice to *also* dump the types here. + printf("constraints:\n"); + for (NotNull c : unsolvedConstraints) + { + auto deps = findDependencyList(c.get()); + printf("\t%zu\t%s\n", deps->size(), toString(*c, opts).c_str()); + + for (auto dep : *deps) + { + Luau::visit( + overloaded{ + [&](TypeId ty) + { + printf("\t\t|\tType %s\n", toString(ty, opts).c_str()); + }, + [&](TypePackId tp) + { + printf("\t\t|\tPack %s\n", toString(tp, opts).c_str()); + }, + [&](const Constraint* c) + { + printf("\t\t|\tCons %s\n", toString(*c, opts).c_str()); + } + }, + dep + ); + } + } +} +} // namespace Luau \ No newline at end of file diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 31b424f7..9d554377 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -4,6 +4,7 @@ #include "Luau/Anyification.h" #include "Luau/ApplyTypeFunction.h" #include "Luau/AstUtils.h" +#include "Luau/BuiltinTypeFunctions.h" #include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/DcrLogger.h" @@ -15,7 +16,7 @@ #include "Luau/IterativeTypeVisitor.h" #include "Luau/Location.h" #include "Luau/ModuleResolver.h" -#include "Luau/OverloadResolution.h" +#include "Luau/OverloadResolver.h" #include "Luau/RecursionCounter.h" #include "Luau/ScopedSeenSet.h" #include "Luau/Simplify.h" @@ -40,18 +41,23 @@ LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500) LUAU_FASTFLAGVARIABLE(DebugLuauAssertOnForcedConstraint) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver) -LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies) LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings) -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSupport) -LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity2) -LUAU_FASTFLAG(LuauPushTypeConstraintLambdas3) -LUAU_FASTFLAG(LuauMarkUnscopedGenericsAsSolved) -LUAU_FASTFLAGVARIABLE(LuauUseFastSubtypeForIndexerWithName) -LUAU_FASTFLAGVARIABLE(LuauUnifyWithSubtyping2) -LUAU_FASTFLAGVARIABLE(LuauDoNotUseApplyTypeFunctionToClone) -LUAU_FASTFLAGVARIABLE(LuauReworkInfiniteTypeFinder) -LUAU_FASTFLAG(LuauRelateHandlesCoincidentTables) -LUAU_FASTFLAG(LuauUnpackRespectsAnnotations) +LUAU_FASTFLAGVARIABLE(LuauFixPropReadsOnMetatableTypes) +LUAU_FASTFLAGVARIABLE(LuauCloneTypeFunctionFromForeignArena) +LUAU_FASTFLAGVARIABLE(LuauAlsoInstantiateInferredArguments) +LUAU_FLAGVERSION(LuauAlsoInstantiateInferredArguments, 2) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAGVARIABLE(LuauRemoveConstraintSolverEmplace) +LUAU_FASTFLAGVARIABLE(LuauInstantiateFunctionTypeBeforePush) +LUAU_FASTFLAGVARIABLE(LuauAvoidCascadingRecursiveConstraintViolationError) +LUAU_FASTFLAGVARIABLE(LuauFixInfiniteTypeRedundantBind) +LUAU_FASTFLAG(LuauBidirectionalInferenceVariadics) +LUAU_FASTFLAG(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) +LUAU_FASTFLAG(DebugLuauCyclicRequireTypeInference) +LUAU_FASTFLAGVARIABLE(LuauRemoveExtraSubtypingInstances) +LUAU_FASTFLAGVARIABLE(LuauIndexingIntoErrorGivesError) +LUAU_FASTFLAG(DebugLuauCyclicRequireTypeInference) +LUAU_FASTFLAGVARIABLE(LuauRelaxConstraintOrderingForFunctionCheck) namespace Luau { @@ -72,22 +78,6 @@ size_t HashSubtypeConstraintRecord::operator()(const SubtypeConstraintRecord& c) static void dump(ConstraintSolver* cs, ToStringOptions& opts); -size_t HashBlockedConstraintId::operator()(const BlockedConstraintId& bci) const -{ - size_t result = 0; - - if (const TypeId* ty = get_if(&bci)) - result = std::hash()(*ty); - else if (const TypePackId* tp = get_if(&bci)) - result = std::hash()(*tp); - else if (Constraint const* const* c = get_if(&bci)) - result = std::hash()(*c); - else - LUAU_ASSERT(!"Should be unreachable"); - - return result; -} - [[maybe_unused]] static void dumpBindings(NotNull scope, ToStringOptions& opts) { for (const auto& [k, v] : scope->bindings) @@ -301,29 +291,50 @@ size_t HashInstantiationSignature::operator()(const InstantiationSignature& sign return hash; } -struct InstantiationQueuer : TypeOnceVisitor +struct InstantiationQueuer : IterativeTypeVisitor { ConstraintSolver* solver; NotNull scope; Location location; + std::shared_ptr moduleName; - explicit InstantiationQueuer(NotNull scope, const Location& location, ConstraintSolver* solver) - : TypeOnceVisitor("InstantiationQueuer", /* skipBoundTypes */ true) + explicit InstantiationQueuer(NotNull scope, const Location& location, ConstraintSolver* solver, std::shared_ptr moduleName) + : IterativeTypeVisitor("InstantiationQueuer", /* skipBoundTypes */ true) , solver(solver) , scope(scope) , location(location) + , moduleName(std::move(moduleName)) { } bool visit(TypeId ty, const PendingExpansionType& petv) override { - solver->pushConstraint(scope, location, TypeAliasExpansionConstraint{ty}); + if (FFlag::DebugLuauCyclicRequireTypeInference) + solver->pushConstraint(scope, location, TypeAliasExpansionConstraint{ty}, moduleName); + else + solver->DEPRECATED_pushConstraint(scope, location, TypeAliasExpansionConstraint{ty}); return false; } bool visit(TypeId ty, const TypeFunctionInstanceType&) override { - solver->pushConstraint(scope, location, ReduceConstraint{ty}); + if (FFlag::LuauAlsoInstantiateInferredArguments) + { + if (!solver->typeFunctionsToFinalize.contains(ty)) + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + solver->typeFunctionsToFinalize[ty] = solver->pushConstraint(scope, location, ReduceConstraint{ty}, moduleName); + else + solver->typeFunctionsToFinalize[ty] = solver->DEPRECATED_pushConstraint(scope, location, ReduceConstraint{ty}); + } + } + else + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + solver->pushConstraint(scope, location, ReduceConstraint{ty}, moduleName); + else + solver->DEPRECATED_pushConstraint(scope, location, ReduceConstraint{ty}); + } return true; } @@ -350,71 +361,63 @@ struct InfiniteTypeFinder : IterativeTypeVisitor bool visit(TypeId ty) override { - if (FFlag::LuauReworkInfiniteTypeFinder) - return !foundInfiniteType; - else - return true; + return !foundInfiniteType; } bool visit(TypeId ty, const PendingExpansionType& petv) override { - if (FFlag::LuauReworkInfiniteTypeFinder) - { - if (foundInfiniteType) - return false; + if (foundInfiniteType) + return false; - const std::optional tf = - petv.prefix ? scope->lookupImportedType(petv.prefix->value, petv.name.value) : scope->lookupType(petv.name.value); + const std::optional tf = + petv.prefix ? scope->lookupImportedType(petv.prefix->value, petv.name.value) : scope->lookupType(petv.name.value); - if (!tf) - return true; + if (!tf) + return true; - // If `tf->type` is different from `signature.fn.type` then we - // have two different type aliases. - if (follow(tf->type) != follow(signature.fn.type)) - return true; + // If `tf->type` is different from `signature.fn.type` then we + // have two different type aliases. + if (follow(tf->type) != follow(signature.fn.type)) + return true; - // We want to check that the arguments to this pending expansion - // type are exactly the generic arguments provided. - for (size_t i = 0; i < std::min(petv.typeArguments.size(), tf->typeParams.size()); ++i) + // We want to check that the arguments to this pending expansion + // type are exactly the generic arguments provided. + for (size_t i = 0; i < std::min(petv.typeArguments.size(), tf->typeParams.size()); ++i) + { + if (FFlag::LuauAvoidCascadingRecursiveConstraintViolationError) { - if (petv.typeArguments[i] != tf->typeParams[i].ty) + auto pendingTypeArg = follow(petv.typeArguments[i]); + auto tfTypeParam = follow(tf->typeParams[i].ty); + if (is(pendingTypeArg) || is(tfTypeParam)) + continue; + + if (pendingTypeArg != tfTypeParam) { foundInfiniteType = true; return false; } } - - // Ditto with packs. - for (size_t i = 0; i < std::min(petv.packArguments.size(), tf->typePackParams.size()); ++i) + else { - if (petv.packArguments[i] != tf->typePackParams[i].tp) + if (petv.typeArguments[i] != tf->typeParams[i].ty) { foundInfiniteType = true; return false; } } - - return false; } - else - { - const std::optional tf = - (petv.prefix) ? scope->lookupImportedType(petv.prefix->value, petv.name.value) : scope->lookupType(petv.name.value); - - if (!tf.has_value()) - return true; - auto [typeArguments, packArguments] = saturateArguments(solver->arena, solver->builtinTypes, *tf, petv.typeArguments, petv.packArguments); - - if (follow(tf->type) == follow(signature.fn.type) && (signature.arguments != typeArguments || signature.packArguments != packArguments)) + // Ditto with packs. + for (size_t i = 0; i < std::min(petv.packArguments.size(), tf->typePackParams.size()); ++i) + { + if (petv.packArguments[i] != tf->typePackParams[i].tp) { foundInfiniteType = true; return false; } - - return true; } + + return false; } }; @@ -428,17 +431,20 @@ ConstraintSolver::ConstraintSolver( DcrLogger* logger, NotNull dfg, TypeCheckLimits limits, - ConstraintSet constraintSet_ + ConstraintSet constraintSet_, + NotNull cgraph, + NotNull subtyping ) : arena(normalizer->arena) , builtinTypes(normalizer->builtinTypes) , normalizer(normalizer) , typeFunctionRuntime(typeFunctionRuntime) , constraintSet(std::move(constraintSet_)) - , constraints(borrowConstraints(constraintSet.constraints)) - , scopeToFunction(&constraintSet.scopeToFunction) + , constraints(borrowConstraints(FFlag::DebugLuauCyclicRequireTypeInference ? cgraph->constraints : constraintSet.constraints)) + , scopeToFunction(FFlag::DebugLuauCyclicRequireTypeInference ? NotNull{&cgraph->scopeToFunction} : NotNull{&constraintSet.scopeToFunction}) , rootScope(constraintSet.rootScope) , module(std::move(module)) + , representativeModuleName(std::make_shared(this->module->name)) , dfg(dfg) , solverConstraintLimit(FInt::LuauSolverConstraintLimit) , moduleResolver(moduleResolver) @@ -446,6 +452,8 @@ ConstraintSolver::ConstraintSolver( , logger(logger) , limits(std::move(limits)) , opts{/*exhaustive*/ true} + , cgraph(cgraph) + , subtyping(subtyping) { initFreeTypeTracking(); } @@ -461,7 +469,9 @@ ConstraintSolver::ConstraintSolver( std::vector requireCycles, DcrLogger* logger, NotNull dfg, - TypeCheckLimits limits + TypeCheckLimits limits, + NotNull cgraph, + NotNull subtyping ) : arena(normalizer->arena) , builtinTypes(normalizer->builtinTypes) @@ -472,6 +482,7 @@ ConstraintSolver::ConstraintSolver( , scopeToFunction(scopeToFunction) , rootScope(rootScope) , module(std::move(module)) + , representativeModuleName(std::make_shared(this->module->name)) , dfg(dfg) , solverConstraintLimit(FInt::LuauSolverConstraintLimit) , moduleResolver(moduleResolver) @@ -479,6 +490,8 @@ ConstraintSolver::ConstraintSolver( , logger(logger) , limits(std::move(limits)) , opts{/*exhaustive*/ true} + , cgraph(cgraph) + , subtyping(subtyping) { initFreeTypeTracking(); } @@ -512,7 +525,10 @@ void ConstraintSolver::run() if (FFlag::DebugLuauLogSolver) { - printf("Starting solver for module %s (%s)\n", module->humanReadableName.c_str(), module->name.c_str()); + if (FFlag::DebugLuauCyclicRequireTypeInference) + printf("Starting solver for module %s\n", representativeModuleName->c_str()); + else + printf("Starting solver for module %s (%s)\n", module->humanReadableName.c_str(), module->name.c_str()); dump(this, opts); printf("Bindings:\n"); dumpBindings(rootScope, opts); @@ -524,28 +540,29 @@ void ConstraintSolver::run() } // Free types that have no constraints at all can be generalized right away. - for (TypeId ty : constraintSet.freeTypes) + // TODO CLI-206649: We can fold constraint set into constraint graph. + TypeIds& freeTypesToProcess = FFlag::DebugLuauCyclicRequireTypeInference ? cgraph->freeTypes : constraintSet.freeTypes; + for (TypeId ty : freeTypesToProcess) { - if (auto it = mutatedFreeTypeToConstraint.find(ty); it == mutatedFreeTypeToConstraint.end() || it->second.empty()) + if (!cgraph->hasUnsolvedDependencies(ty)) generalizeOneType(ty); } - - constraintSet.freeTypes.clear(); + freeTypesToProcess.clear(); auto runSolverPass = [&](bool force) { bool progress = false; + size_t i = 0; while (i < unsolvedConstraints.size()) { NotNull c = unsolvedConstraints[i]; - if (!force && isBlocked(c)) + if (!force && cgraph->hasUnsolvedDependencies(c.get())) { - ++i; + i++; continue; } - if (limits.finishTime && TimeTrace::getClock() > *limits.finishTime) throwTimeLimitError(); if (limits.cancellationToken && limits.cancellationToken->requested()) @@ -557,7 +574,7 @@ void ConstraintSolver::run() break; std::string saveMe = FFlag::DebugLuauLogSolver ? toString(*c, opts) : std::string{}; - StepSnapshot snapshot; + ConstraintStepSnapshot snapshot; if (logger) { @@ -573,46 +590,39 @@ void ConstraintSolver::run() if (success) { - unblock(c); + if (logger) + logger->commitStepSnapshot(snapshot); + + auto unblockResult = cgraph->unblockConstraint(c); + + // We need to handle the logger here. + if (logger) + logger->popBlock(c); + unsolvedConstraints.erase(unsolvedConstraints.begin() + ptrdiff_t(i)); - if (const auto maybeMutated = maybeMutatedFreeTypes.find(c); maybeMutated != maybeMutatedFreeTypes.end()) + for (TypeId ty : unblockResult.types) { - DenseHashSet seen{nullptr}; - for (auto ty : maybeMutated->second) + if (!cgraph->hasUnsolvedDependencies(ty)) { - // There is a high chance that this type has been rebound - // across blocked types, rebound free types, pending - // expansion types, etc, so we need to follow it. - ty = follow(ty); - - if (seen.contains(ty)) - continue; - seen.insert(ty); - - size_t& refCount = unresolvedConstraints[ty]; - if (refCount > 0) - refCount -= 1; - - // We have two constraints that are designed to wait for the - // refCount on a free type to be equal to 1: the - // PrimitiveTypeConstraint and ReduceConstraint. We - // therefore wake any constraint waiting for a free type's - // refcount to be 1 or 0. - if (refCount <= 1) - unblock(ty, Location{}); - - if (refCount == 0) - generalizeOneType(ty); - } - } + std::optional snap; + if (logger) + snap = logger->prepareGeneralizationSnapshot(toString(ty), rootScope, unsolvedConstraints); + generalizeOneType(ty); - if (logger) - { - logger->commitStepSnapshot(snapshot); + if (logger) + { + snap->after = toString(ty); + logger->commitStepSnapshot(std::move(*snap)); + } + + unblock(ty, Location{}); + } } + // TODO CLI-206534: We never eagerly generalize free type + // packs. Maybe we should. if (FFlag::DebugLuauLogSolver) { if (force) @@ -621,22 +631,7 @@ void ConstraintSolver::run() if (force) { - printf("Blocked on:\n"); - - for (const auto& [bci, cv] : blocked) - { - if (end(cv) == std::find(begin(cv), end(cv), c)) - continue; - - if (auto bty = get_if(&bci)) - printf("\tType %s\n", toString(*bty, opts).c_str()); - else if (auto btp = get_if(&bci)) - printf("\tPack %s\n", toString(*btp, opts).c_str()); - else if (auto cc = get_if(&bci)) - printf("\tCons %s\n", toString(**cc, opts).c_str()); - else - LUAU_ASSERT(!"Unreachable??"); - } + cgraph->dumpBlocked(c, opts); } dump(this, opts); @@ -661,7 +656,12 @@ void ConstraintSolver::run() } while (progress); if (!unsolvedConstraints.empty()) - reportError(ConstraintSolvingIncompleteError{}, Location{}); + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(ConstraintSolvingIncompleteError{}, Location{}, *representativeModuleName); + else + DEPRECATED_reportError(ConstraintSolvingIncompleteError{}, Location{}); + } // After we have run all the constraints, type functions should be generalized // At this point, we can try to perform one final simplification to suss out @@ -686,7 +686,7 @@ void ConstraintSolver::finalizeTypeFunctions() TypeId ty = follow(t); if (get(ty)) { - TypeFunctionContext context{NotNull{this}, constraint->scope, NotNull{constraint}}; + TypeFunctionContext context{NotNull{this}, constraint->scope, NotNull{constraint}, subtyping}; FunctionGraphReductionResult result = reduceTypeFunctions(t, constraint->location, NotNull{&context}, true); for (TypeId r : result.reducedTypes) @@ -775,25 +775,47 @@ void ConstraintSolver::initFreeTypeTracking() for (auto c : this->constraints) { unsolvedConstraints.emplace_back(c); + NotNull borrow{c.get()}; - auto maybeMutatedTypesPerConstraint = c->getMaybeMutatedFreeTypes(); - for (auto ty : maybeMutatedTypesPerConstraint) - { - auto [refCount, _] = unresolvedConstraints.try_insert(ty, 0); - refCount += 1; + auto [types, typePacks] = c->getMaybeMutatedTypes(); - auto [it, fresh] = mutatedFreeTypeToConstraint.try_emplace(ty); - it->second.insert(c.get()); + for (auto ty : types) + { + cgraph->addDependencyOf(borrow.get(), ty); + if (FFlag::DebugLuauLogSolver) + printf("Type %s depends on constraint %s\n", toString(ty, opts).c_str(), toString(*c, opts).c_str()); } - maybeMutatedFreeTypes.emplace(c, maybeMutatedTypesPerConstraint); - for (NotNull dep : c->dependencies) + for (auto tp : typePacks) { - block(dep, c); + cgraph->addDependencyOf(borrow.get(), tp); + if (FFlag::DebugLuauLogSolver) + printf("Type pack %s depends on constraint %s\n", toString(tp, opts).c_str(), toString(*c, opts).c_str()); } } } +namespace +{ + +std::optional resolvePrimitiveLiteral(const FreeType& ft) +{ + if (!ft.primitiveType) + return std::nullopt; + + TypeId bindTo = *ft.primitiveType; + LUAU_ASSERT(is(bindTo)); + TypeId upper = follow(ft.upperBound); + + if (upper != bindTo && maybeSingleton(upper)) + return follow(ft.lowerBound); + + return bindTo; +} + +} // namespace + + void ConstraintSolver::generalizeOneType(TypeId ty) { ty = follow(ty); @@ -806,6 +828,19 @@ void ConstraintSolver::generalizeOneType(TypeId ty) if (!freeTy) return; + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + if (auto bindTo = resolvePrimitiveLiteral(*freeTy); bindTo && ty != *bindTo) + { + emplaceType(asMutable(ty), *bindTo); + + if (FFlag::DebugLuauLogSolver) + printf("Eagerly generalized literal %s (now %s)\n", saveme.c_str(), toString(ty, opts).c_str()); + + return; + } + } + TypeId* functionType = scopeToFunction->find(freeTy->scope); if (!functionType) return; @@ -829,19 +864,21 @@ void ConstraintSolver::bind(NotNull constraint, TypeId ty, Typ LUAU_ASSERT(canMutate(ty, constraint)); boundTo = follow(boundTo); - if (get(ty) && ty == boundTo) - { - emplace( - constraint, ty, constraint->scope, builtinTypes->neverType, builtinTypes->unknownType, Polarity::Mixed - ); // FIXME? Is this the right polarity? - - trackInteriorFreeType(constraint->scope, ty); + // This follow shouldn't be needed, but if for some reason we end up + // with a bound type, we want to also follow it when doing this + // occurence check. + if (follow(ty) == boundTo) + { + auto freshTy = freshType(arena, builtinTypes, constraint->scope, Polarity::Mixed); + emplaceType(asMutable(ty), freshTy); + trackInteriorFreeType(constraint->scope, freshTy); + unblock(ty, constraint->location); return; } - shiftReferences(ty, boundTo); emplaceType(asMutable(ty), boundTo); + unblock(ty, constraint->location); } @@ -853,12 +890,24 @@ void ConstraintSolver::bind(NotNull constraint, TypePackId tp, boundTo = follow(boundTo); LUAU_ASSERT(tp != boundTo); - emplaceTypePack(asMutable(tp), boundTo); + if (occursCheck(tp, boundTo) == OccursCheckResult::Fail) + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(InternalError{"Attempted to create a type pack cycle"}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(InternalError{"Attempted to create a type pack cycle"}, constraint->location); + emplaceTypePack(asMutable(tp), builtinTypes->errorTypePack); + } + else + { + emplaceTypePack(asMutable(tp), boundTo); + } + unblock(tp, constraint->location); } template -void ConstraintSolver::emplace(NotNull constraint, TypeId ty, Args&&... args) +void ConstraintSolver::DEPRECATED_emplace(NotNull constraint, TypeId ty, Args&&... args) { static_assert(!std::is_same_v, "cannot use `emplace`! use `bind`"); @@ -870,7 +919,7 @@ void ConstraintSolver::emplace(NotNull constraint, TypeId ty, } template -void ConstraintSolver::emplace(NotNull constraint, TypePackId tp, Args&&... args) +void ConstraintSolver::DEPRECATED_emplace(NotNull constraint, TypePackId tp, Args&&... args) { static_assert(!std::is_same_v, "cannot use `emplace`! use `bind`"); @@ -883,9 +932,8 @@ void ConstraintSolver::emplace(NotNull constraint, TypePackId bool ConstraintSolver::tryDispatch(NotNull constraint, bool force) { - if (!force && isBlocked(constraint)) - return false; + LUAU_ASSERT(force || !cgraph->hasUnsolvedDependencies(constraint.get())); bool success = false; if (auto sc = get(*constraint)) @@ -904,8 +952,10 @@ bool ConstraintSolver::tryDispatch(NotNull constraint, bool fo success = tryDispatch(*fcc, constraint, force); else if (auto fcc = get(*constraint)) success = tryDispatch(*fcc, constraint, force); - else if (auto fcc = get(*constraint)) - success = tryDispatch(*fcc, constraint); + else if (auto fcc = get(*constraint); !FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier && fcc) + { + success = DEPRECATED_tryDispatch(*fcc, constraint); + } else if (auto hpc = get(*constraint)) success = tryDispatch(*hpc, constraint); else if (auto spc = get(*constraint)) @@ -927,10 +977,7 @@ bool ConstraintSolver::tryDispatch(NotNull constraint, bool fo else if (auto pftc = get(*constraint)) success = tryDispatch(*pftc, constraint); else if (auto esgc = get(*constraint)) - { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSupport); success = tryDispatch(*esgc, constraint); - } else if (auto ptc = get(*constraint)) success = tryDispatch(*ptc, constraint, force); else @@ -974,7 +1021,12 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull generalizedTy = generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, c.sourceType); if (!generalizedTy) - reportError(CodeTooComplex{}, constraint->location); + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(CodeTooComplex{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(CodeTooComplex{}, constraint->location); + } if (generalizedTy) { @@ -986,16 +1038,19 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull(follow(generalizedType))) { - if (c.hasDeprecatedAttribute) + if (c.maybeDeprecatedAttr) { fty->isDeprecatedFunction = true; - fty->deprecatedInfo = std::make_shared(c.deprecatedInfo); + fty->deprecatedInfo = std::make_shared(c.maybeDeprecatedAttr->deprecatedInfo()); } } } else { - reportError(CodeTooComplex{}, constraint->location); + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(CodeTooComplex{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(CodeTooComplex{}, constraint->location); bind(constraint, c.generalizedType, builtinTypes->errorType); } @@ -1014,13 +1069,17 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNullpolarity; GeneralizationResult res = generalizeType(arena, builtinTypes, constraint->scope, ty, params); if (res.resourceLimitsExceeded) - reportError(CodeTooComplex{}, constraint->scope->location); // FIXME: We don't have a very good location for this. + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(CodeTooComplex{}, constraint->scope->location, *constraint->moduleName); // FIXME: We don't have a very good location for this. + else + DEPRECATED_reportError(CodeTooComplex{}, constraint->scope->location); // FIXME: We don't have a very good location for this. + } } else if (get(ty)) sealTable(constraint->scope, ty); - if (FFlag::LuauRelateHandlesCoincidentTables) - unblock(ty, constraint->location); + unblock(ty, constraint->location); } } @@ -1199,12 +1258,16 @@ bool ConstraintSolver::tryDispatch(const NameConstraint& c, NotNullscope->invalidTypeAliases[c.name] = constraint->location; + constraint->scope->invalidTypeAliases[c.name] = constraint->location; + if (FFlag::LuauFixInfiniteTypeRedundantBind) + { + if (get(target) || get(target) || get(target)) + bind(constraint, target, builtinTypes->errorType); + } else - constraint->scope->invalidTypeAliasNames_DEPRECATED.insert(c.name); - shiftReferences(target, builtinTypes->errorType); - emplaceType(asMutable(target), builtinTypes->errorType); + { + bind(constraint, target, builtinTypes->errorType); + } return true; } } @@ -1246,12 +1309,14 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul // We do this check here to ensure that we don't bind an alias to itself if (occursCheck(cTarget, result)) { - reportError(OccursCheckFailed{}, constraint->location); + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(OccursCheckFailed{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(OccursCheckFailed{}, constraint->location); bind(constraint, cTarget, builtinTypes->errorType); } else { - shiftReferences(cTarget, result); bind(constraint, cTarget, result); } }; @@ -1261,14 +1326,58 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul if (!tf.has_value()) { - reportError(UnknownSymbol{petv->name.value, UnknownSymbol::Context::Type}, constraint->location); + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(UnknownSymbol{petv->name.value, UnknownSymbol::Context::Type}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(UnknownSymbol{petv->name.value, UnknownSymbol::Context::Type}, constraint->location); bindResult(builtinTypes->errorType); return true; } // Adding ReduceConstraint on type function for the constraint solver - if (get(follow(tf->type))) - pushConstraint(NotNull(constraint->scope.get()), constraint->location, ReduceConstraint{tf->type}); + if (FFlag::LuauCloneTypeFunctionFromForeignArena) + { + if (const TypeFunctionInstanceType* tfit = get(follow(tf->type))) + { + TypeId toReduce = follow(tf->type); + + // If the type function instance belongs to a different arena (e.g. imported + // from another module), we must create a fresh copy in our arena so that + // the reducer can mutate it during reduction. + if (toReduce->owningArena != arena) + { + toReduce = arena->addType(TypeFunctionInstanceType{ + tfit->function, + tfit->typeArguments, + tfit->packArguments, + tfit->userFuncName, + tfit->userFuncData, + }); + + pushConstraint(NotNull(constraint->scope.get()), constraint->location, ReduceConstraint{toReduce}, constraint->moduleName); + + if (tf->typeParams.empty() && tf->typePackParams.empty()) + { + bindResult(toReduce); + return true; + } + } + else + { + pushConstraint(NotNull(constraint->scope.get()), constraint->location, ReduceConstraint{toReduce}, constraint->moduleName); + } + } + } + else + { + if (get(follow(tf->type))) + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + pushConstraint(NotNull(constraint->scope.get()), constraint->location, ReduceConstraint{tf->type}, constraint->moduleName); + else + DEPRECATED_pushConstraint(NotNull(constraint->scope.get()), constraint->location, ReduceConstraint{tf->type}); + } + } // Due to how pending expansion types and TypeFun's are created // If this check passes, we have created a cyclic / corecursive type alias @@ -1277,7 +1386,10 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul TypeId rhs = tf->type; if (occursCheck(lhs, rhs)) { - reportError(OccursCheckFailed{}, constraint->location); + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(OccursCheckFailed{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(OccursCheckFailed{}, constraint->location); bindResult(builtinTypes->errorType); return true; } @@ -1348,12 +1460,8 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul if (itf.foundInfiniteType) { - // TODO (CLI-56761): Report an error. bindResult(builtinTypes->errorType); - if (FFlag::LuauReworkInfiniteTypeFinder) - constraint->scope->invalidTypeAliases[petv->name.value] = constraint->location; - else - reportError(GenericError{"Recursive type being used with different parameters"}, constraint->location); + constraint->scope->invalidTypeAliases[petv->name.value] = constraint->location; return true; } @@ -1390,9 +1498,8 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul // The application is not recursive, so we need to queue up application of // any child type function instantiations within the result in order for it // to be complete. - InstantiationQueuer queuer{constraint->scope, constraint->location, this}; - queuer.traverse(target); - + InstantiationQueuer queuer{constraint->scope, constraint->location, this, constraint->moduleName}; + queuer.run(target); if (target->persistent || target->owningArena != arena) { bindResult(target); @@ -1420,52 +1527,27 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul { if (needsClone) { - if (FFlag::LuauDoNotUseApplyTypeFunctionToClone) + if (get(target)) { - if (get(target)) - { - CloneState cloneState{builtinTypes}; - instantiated = shallowClone(target, *arena.get(), cloneState, true); - MetatableType* mtv = getMutable(instantiated); - mtv->table = shallowClone(mtv->table, *arena.get(), cloneState, true); - ttv = getMutable(mtv->table); - } - else if (get(target)) - { - CloneState cloneState{builtinTypes}; - instantiated = shallowClone(target, *arena.get(), cloneState, true); - ttv = getMutable(instantiated); - } - - target = follow(instantiated); + CloneState cloneState{builtinTypes}; + instantiated = shallowClone(target, *arena.get(), cloneState, true); + MetatableType* mtv = getMutable(instantiated); + mtv->table = shallowClone(mtv->table, *arena.get(), cloneState, true); + ttv = getMutable(mtv->table); } - else + else if (get(target)) { - // Substitution::clone is a shallow clone. If this is a - // metatable type, we want to mutate its table, so we need to - // explicitly clone that table as well. If we don't, we will - // mutate another module's type surface and cause a - // use-after-free. - if (get(target)) - { - instantiated = applyTypeFunction.clone(target); - MetatableType* mtv = getMutable(instantiated); - mtv->table = applyTypeFunction.clone(mtv->table); - ttv = getMutable(mtv->table); - } - else if (get(target)) - { - instantiated = applyTypeFunction.clone(target); - ttv = getMutable(instantiated); - } - - target = follow(instantiated); + CloneState cloneState{builtinTypes}; + instantiated = shallowClone(target, *arena.get(), cloneState, true); + ttv = getMutable(instantiated); } + + target = follow(instantiated); } // This is a new type - redefine the location. ttv->definitionLocation = constraint->location; - ttv->definitionModuleName = module->name; + ttv->definitionModuleName = FFlag::DebugLuauCyclicRequireTypeInference ? *constraint->moduleName : module->name; ttv->instantiatedTypeParams = typeArguments; ttv->instantiatedTypePackParams = packArguments; @@ -1585,12 +1667,9 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNullscope, constraint->location); - } + fn = instantiateFunctionType(c.fn, c.typeArguments, c.typePackArguments, constraint->scope, constraint->location); } fillInDiscriminantTypes(constraint, c.discriminantTypes); @@ -1608,7 +1687,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull uniqueTypes{nullptr}; if (c.callSite) - findUniqueTypes(NotNull{&uniqueTypes}, c.callSite->args, NotNull{&module->astTypes}); + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + findUniqueTypes(NotNull{&uniqueTypes}, c.callSite->args, NotNull{c.astTypes}); + else + findUniqueTypes(NotNull{&uniqueTypes}, c.callSite->args, NotNull{&module->astTypes}); + } TypeId overloadToUse = fn; @@ -1644,13 +1728,11 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNulladdTypePack(TypePack{{fn}, argsPack}); } - if (!usedMagic) - { - emplace(constraint, c.result, constraint->scope, Polarity::Positive); - trackInteriorFreeTypePack(constraint->scope, c.result); - } - TypeId inferredTy = arena->addType(FunctionType{TypeLevel{}, argsPack, c.result}); + TypePackId retTp = arena->freshTypePack(constraint->scope, Polarity::Positive); + trackInteriorFreeTypePack(constraint->scope, retTp); + + TypeId inferredTy = arena->addType(FunctionType{TypeLevel{}, argsPack, retTp}); Unifier2 u2{NotNull{arena}, builtinTypes, constraint->scope, NotNull{&iceReporter}}; @@ -1664,70 +1746,130 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull( { T } ) -> ...T`, we may not end up with any + // bounds for `T`. We will create an indexer on `tbl` but not + // unify it with anything. This is incorrect, and causes + // us to store a resolved overloaded type of + // `( { unknown } ) -> ...unknown`, which errors in type checking. + // + // Our solution for now is, if there are no bounds on any + // generics, we do not store the resolved overload. + bool hasNonTrivialSubstitution = false; + for (auto& [_, ty] : u2.genericSubstitutions) + if (auto ft = get(ty)) + hasNonTrivialSubstitution |= !is(follow(ft->lowerBound)) || !is(follow(ft->upperBound)); + + // If we have generics we can bind *and* + if (auto overloadAsFn = get(overloadToUse); overloadAsFn && hasNonTrivialSubstitution) { - Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; - std::optional subst = instantiate2( - arena, std::move(u2.genericSubstitutions), std::move(u2.genericPackSubstitutions), NotNull{&subtyping}, constraint->scope, result - ); - if (!subst) + CloneState cs{builtinTypes}; + // We want to clone persistent types here, for example if we try to instantiate + // `table.insert` + auto clonedTy = shallowClone(overloadToUse, *arena, cs, true); + auto clonedFn = getMutable(clonedTy); + LUAU_ASSERT(clonedFn); + clonedFn->generics.clear(); + clonedFn->genericPacks.clear(); + if (auto inst = instantiate2( + arena, + // Intentional copy, could be by reference. + std::move(u2.genericSubstitutions), + // Intentional copy, could be by reference. + std::move(u2.genericPackSubstitutions), + FFlag::LuauRemoveExtraSubtypingInstances ? subtyping : NotNull{&subtyping_DEPRECATED}, + constraint->scope, + clonedTy + )) { - reportError(CodeTooComplex{}, constraint->location); - result = builtinTypes->errorTypePack; + auto instantiatedFn = get(inst); + LUAU_ASSERT(instantiatedFn); + overloadToUse = *inst; + retTp = follow(instantiatedFn->retTypes); } else - result = *subst; + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(CodeTooComplex{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(CodeTooComplex{}, constraint->location); + result = builtinTypes->errorTypePack; + } } else { + auto newRetTp = getApproximateReturnTypeForFunctionCall(overloadToUse).value_or(builtinTypes->errorTypePack); - std::optional subst = - instantiate2_DEPRECATED(arena, std::move(u2.genericSubstitutions), std::move(u2.genericPackSubstitutions), result); - if (!subst) - { - reportError(CodeTooComplex{}, constraint->location); - result = builtinTypes->errorTypePack; - } + std::optional subst = instantiate2( + arena, + std::move(u2.genericSubstitutions), + std::move(u2.genericPackSubstitutions), + FFlag::LuauRemoveExtraSubtypingInstances ? subtyping : NotNull{&subtyping_DEPRECATED}, + constraint->scope, + newRetTp + ); + + if (subst) + retTp = *subst; else - result = *subst; + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(CodeTooComplex{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(CodeTooComplex{}, constraint->location); } - - if (c.result != result) - emplaceTypePack(asMutable(c.result), result); } + if (!usedMagic) + bind(constraint, c.result, retTp); + for (const auto& [expanded, additions] : u2.expandedFreeTypes) { for (TypeId addition : additions) upperBoundContributors[expanded].emplace_back(constraint->location, addition); } - if (UnifyResult::Ok == unifyResult && c.callSite) - (*c.astOverloadResolvedTypes)[c.callSite] = inferredTy; - else if (UnifyResult::Ok != unifyResult) + switch (unifyResult) { - switch (unifyResult) + case UnifyResult::Ok: + if (c.callSite) { - case UnifyResult::Ok: - break; - case UnifyResult::TooComplex: - reportError(UnificationTooComplex{}, constraint->location); - break; - case UnifyResult::OccursCheckFailed: - reportError(OccursCheckFailed{}, constraint->location); - break; + // FIXME CLI-192090 + // For now, due to how bidirectional inference of function + // arguments is implemented, magic functions rely on getting + // the "inferred" type here. + (*c.astOverloadResolvedTypes)[c.callSite] = usedMagic ? inferredTy : overloadToUse; } + break; + case UnifyResult::TooComplex: + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(UnificationTooComplex{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(UnificationTooComplex{}, constraint->location); + break; + case UnifyResult::OccursCheckFailed: + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(OccursCheckFailed{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(OccursCheckFailed{}, constraint->location); + break; } - InstantiationQueuer queuer{constraint->scope, constraint->location, this}; - queuer.traverse(overloadToUse); - queuer.traverse(inferredTy); - - // This can potentially contain free types if the return type of - // `inferredTy` is never unified elsewhere. - trackInteriorFreeType(constraint->scope, inferredTy); - - unblock(c.result, constraint->location); + InstantiationQueuer queuer{constraint->scope, constraint->location, this, constraint->moduleName}; + queuer.run(overloadToUse); + if (FFlag::LuauAlsoInstantiateInferredArguments) + queuer.run(argsPack); + queuer.run(result); return true; } @@ -1743,18 +1885,21 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull expectedArgs = flatten(ftv->argTypes).first; - const std::vector argPackHead = flatten(argsPack).first; - - Replacer replacer{arena, std::move(replacements), std::move(replacementPacks)}; - // If this is a self call, the types will have more elements than the AST call. // We don't attempt to perform bidirectional inference on the self type. const size_t typeOffset = c.callSite->self ? 1 : 0; - if (FFlag::LuauPushTypeConstraintLambdas3) + const std::vector expectedArgs = FFlag::LuauBidirectionalInferenceVariadics + ? extendTypePack(*arena, builtinTypes, ftv->argTypes, c.callSite->args.size + typeOffset).head + : flatten(ftv->argTypes).first; + const std::vector argPackHead = flatten(argsPack).first; + + // TODO: Clip with LuauRemoveExtraSubtypingInstances + Subtyping subtyping_DEPRECATED{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; + + for (size_t i = 0; i < c.callSite->args.size && i + typeOffset < expectedArgs.size() && i + typeOffset < argPackHead.size(); ++i) { - Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; + TypeId expectedArgTy = follow(expectedArgs[i + typeOffset]); + AstExpr* expr = unwrapGroup(c.callSite->args.data[i]); - for (size_t i = 0; i < c.callSite->args.size && i + typeOffset < expectedArgs.size() && i + typeOffset < argPackHead.size(); ++i) - { - TypeId expectedArgTy = follow(expectedArgs[i + typeOffset]); - AstExpr* expr = unwrapGroup(c.callSite->args.data[i]); - - PushTypeResult result = pushTypeInto( - c.astTypes, - c.astExpectedTypes, - NotNull{this}, - constraint, - NotNull{&genericTypesAndPacks}, - NotNull{&u2}, - NotNull{&subtyping}, - expectedArgTy, - expr - ); + PushTypeResult result = pushTypeInto( + c.astTypes, + c.astExpectedTypes, + NotNull{this}, + constraint, + NotNull{&genericTypesAndPacks}, + NotNull{&u2}, + FFlag::LuauRemoveExtraSubtypingInstances ? subtyping : NotNull{&subtyping_DEPRECATED}, + expectedArgTy, + expr + ); - // Consider: - // - // local Direction = { Left = 1, Right = 2 } - // type Direction = keyof - // - // local function move(dirs: { Direction }) --[[...]] end - // - // move({ "Left", "Right", "Left", "Right" }) - // - // We need `keyof` to reduce prior to inferring that the - // arguments to `move` must generalize to their lower bounds. This - // is how we ensure that ordering. - if (!force && !result.incompleteTypes.empty()) + // Consider: + // + // local Direction = { Left = 1, Right = 2 } + // type Direction = keyof + // + // local function move(dirs: { Direction }) --[[...]] end + // + // move({ "Left", "Right", "Left", "Right" }) + // + // We need `keyof` to reduce prior to inferring that the + // arguments to `move` must generalize to their lower bounds. This + // is how we ensure that ordering. + if (!force && !result.incompleteTypes.empty()) + { + for (const auto& [newExpectedTy, newTargetTy, newExpr] : result.incompleteTypes) { - for (const auto& [newExpectedTy, newTargetTy, newExpr] : result.incompleteTypes) - { - auto addition = pushConstraint( + NotNull addition = FFlag::DebugLuauCyclicRequireTypeInference + ? pushConstraint( constraint->scope, constraint->location, - PushTypeConstraint{ - newExpectedTy, - newTargetTy, - /* astTypes */ c.astTypes, - /* astExpectedTypes */ c.astExpectedTypes, - /* expr */ NotNull{newExpr}, - } + PushTypeConstraint{newExpectedTy, newTargetTy, c.astTypes, c.astExpectedTypes, NotNull{newExpr}}, + constraint->moduleName + ) + : DEPRECATED_pushConstraint( + constraint->scope, + constraint->location, + PushTypeConstraint{newExpectedTy, newTargetTy, c.astTypes, c.astExpectedTypes, NotNull{newExpr}} ); - inheritBlocks(constraint, addition); - } + inheritBlocks(constraint, addition); } } } - else + + // Consider: + // + // local Direction = { Left = 1, Right = 2 } + // type Direction = keyof + // + // local function move(dirs: { Direction }) --[[...]] end + // + // move({ "Left", "Right", "Left", "Right" }) + // + // We need `keyof` to reduce prior to inferring that the + // arguments to `move` must generalize to their lower bounds. This + // is how we ensure that ordering. + for (auto& c : u2.incompleteSubtypes) { + NotNull addition = FFlag::DebugLuauCyclicRequireTypeInference + ? pushConstraint(constraint->scope, constraint->location, std::move(c), constraint->moduleName) + : DEPRECATED_pushConstraint(constraint->scope, constraint->location, std::move(c)); + inheritBlocks(constraint, addition); + } - for (size_t i = 0; i < c.callSite->args.size && i + typeOffset < expectedArgs.size() && i + typeOffset < argPackHead.size(); ++i) - { - TypeId expectedArgTy = follow(expectedArgs[i + typeOffset]); - const TypeId actualArgTy = follow(argPackHead[i + typeOffset]); - AstExpr* expr = unwrapGroup(c.callSite->args.data[i]); + return true; +} - (*c.astExpectedTypes)[expr] = expectedArgTy; +bool ConstraintSolver::DEPRECATED_tryDispatch(const DEPRECATED_PrimitiveTypeConstraint& c, NotNull constraint) +{ + LUAU_ASSERT(!FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier); + std::optional expectedType = c.expectedType ? std::make_optional(follow(*c.expectedType)) : std::nullopt; + if (expectedType && (isBlocked(*expectedType) || get(*expectedType))) + return block(*expectedType, constraint); - const auto lambdaTy = get(actualArgTy); - const auto expectedLambdaTy = get(expectedArgTy); - const auto lambdaExpr = expr->as(); - - if (expectedLambdaTy && lambdaTy && lambdaExpr) - { - if (containsGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) - continue; - - const std::vector expectedLambdaArgTys = flatten(expectedLambdaTy->argTypes).first; - const std::vector lambdaArgTys = flatten(lambdaTy->argTypes).first; - - for (size_t j = 0; j < expectedLambdaArgTys.size() && j < lambdaArgTys.size() && j < lambdaExpr->args.size; ++j) - { - if (!lambdaExpr->args.data[j]->annotation && get(follow(lambdaArgTys[j]))) - { - shiftReferences(lambdaArgTys[j], expectedLambdaArgTys[j]); - bind(constraint, lambdaArgTys[j], expectedLambdaArgTys[j]); - } - } - } - else if (expr->is() || expr->is() || expr->is() || - expr->is() || expr->is()) - { - if (containsGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) - { - replacer.resetState(TxnLog::empty(), arena); - if (auto res = replacer.substitute(expectedArgTy)) - { - InstantiationQueuer queuer{constraint->scope, constraint->location, this}; - queuer.traverse(*res); - expectedArgTy = *res; - } - } - Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; - PushTypeResult result = pushTypeInto_DEPRECATED( - c.astTypes, c.astExpectedTypes, NotNull{this}, constraint, NotNull{&u2}, NotNull{&subtyping}, expectedArgTy, expr - ); - // Consider: - // - // local Direction = { Left = 1, Right = 2 } - // type Direction = keyof - // - // local function move(dirs: { Direction }) --[[...]] end - // - // move({ "Left", "Right", "Left", "Right" }) - // - // We need `keyof` to reduce prior to inferring that the - // arguments to `move` must generalize to their lower bounds. This - // is how we ensure that ordering. - if (!force && !result.incompleteTypes.empty()) - { - for (const auto& [newExpectedTy, newTargetTy, newExpr] : result.incompleteTypes) - { - auto addition = pushConstraint( - constraint->scope, - constraint->location, - PushTypeConstraint{ - newExpectedTy, - newTargetTy, - /* astTypes */ c.astTypes, - /* astExpectedTypes */ c.astExpectedTypes, - /* expr */ NotNull{newExpr}, - } - ); - inheritBlocks(constraint, addition); - } - } - } - } - } - // Consider: - // - // local Direction = { Left = 1, Right = 2 } - // type Direction = keyof - // - // local function move(dirs: { Direction }) --[[...]] end - // - // move({ "Left", "Right", "Left", "Right" }) - // - // We need `keyof` to reduce prior to inferring that the - // arguments to `move` must generalize to their lower bounds. This - // is how we ensure that ordering. - for (auto& c : u2.incompleteSubtypes) - { - NotNull addition = pushConstraint(constraint->scope, constraint->location, std::move(c)); - inheritBlocks(constraint, addition); - } - - return true; -} - -bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNull constraint) -{ - std::optional expectedType = c.expectedType ? std::make_optional(follow(*c.expectedType)) : std::nullopt; - if (expectedType && (isBlocked(*expectedType) || get(*expectedType))) - return block(*expectedType, constraint); - - const FreeType* freeType = get(follow(c.freeType)); + const FreeType* freeType = get(follow(c.freeType)); // if this is no longer a free type, then we're done. if (!freeType) @@ -1974,12 +2040,11 @@ bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNull 1) + if (cgraph->DEPRECATED_hasStrictlyMoreThanOneDependency(c.freeType)) { block(c.freeType, constraint); return false; } - TypeId bindTo = c.primitiveType; if (freeType->upperBound != c.primitiveType && maybeSingleton(freeType->upperBound)) @@ -1988,7 +2053,6 @@ bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNulllowerBound; auto ty = follow(c.freeType); - shiftReferences(ty, bindTo); bind(constraint, ty, bindTo); return true; @@ -2065,8 +2129,20 @@ bool ConstraintSolver::tryDispatchHasIndexer( else if (auto mt = get(follow(ft->upperBound))) return tryDispatchHasIndexer(recursionDepth, constraint, mt->table, indexType, resultType, seen); - FreeType freeResult{ft->scope, builtinTypes->neverType, builtinTypes->unknownType, Polarity::Mixed}; - emplace(constraint, resultType, freeResult); + if (FFlag::LuauRemoveConstraintSolverEmplace) + { + auto freeResult = freshType(arena, builtinTypes, ft->scope, Polarity::Mixed); + trackInteriorFreeType(ft->scope, freeResult); + bind(constraint, resultType, freeResult); + // We expect `resultType` to be followed later, so just reassign it here. + resultType = freeResult; + } + else + { + FreeType freeResult{ft->scope, builtinTypes->neverType, builtinTypes->unknownType, Polarity::Mixed}; + DEPRECATED_emplace(constraint, resultType, freeResult); + } + TypeId upperBound = arena->addType(TableType{/* props */ {}, TableIndexer{indexType, resultType}, TypeLevel{}, ft->scope, TableState::Unsealed}); @@ -2093,9 +2169,18 @@ bool ConstraintSolver::tryDispatchHasIndexer( { // FIXME this is greedy. - FreeType freeResult{tt->scope, builtinTypes->neverType, builtinTypes->unknownType, Polarity::Mixed}; - emplace(constraint, resultType, freeResult); - trackInteriorFreeType(constraint->scope, resultType); + if (FFlag::LuauRemoveConstraintSolverEmplace) + { + auto freeResult = freshType(arena, builtinTypes, tt->scope, Polarity::Mixed); + trackInteriorFreeType(tt->scope, freeResult); + bind(constraint, resultType, freeResult); + } + else + { + FreeType freeResult{tt->scope, builtinTypes->neverType, builtinTypes->unknownType, Polarity::Mixed}; + DEPRECATED_emplace(constraint, resultType, freeResult); + trackInteriorFreeType(constraint->scope, resultType); + } tt->indexer = TableIndexer{indexType, resultType}; return true; @@ -2119,75 +2204,146 @@ bool ConstraintSolver::tryDispatchHasIndexer( } else if (auto it = get(subjectType)) { - // subjectType <: {[indexType]: resultType} - // - // 'a & ~(false | nil) <: {[indexType]: resultType} - // - // 'a <: {[indexType]: resultType} - // ~(false | nil) <: {[indexType]: resultType} + // Indexing into an intersection of types is roughly akin to overload + // selection: for every type in the intersection where it is well typed + // to index into _that_ type, we construct an intersection of said result + // types. + if (FFlag::LuauRemoveConstraintSolverEmplace) + { + + IntersectionBuilder ib{arena, builtinTypes}; + bool success = false; - Set parts{nullptr}; - for (TypeId part : it) - parts.insert(follow(part)); + for (TypeId part : it) + { + TypeId r = arena->addType(BlockedType{}); + getMutable(r)->setOwner(constraint.get()); + + bool ok = tryDispatchHasIndexer(recursionDepth, constraint, part, indexType, r, seen); + // If we've cut a recursive loop short, skip it. + if (!ok) + continue; - Set results{nullptr}; + r = follow(r); + if (!get(r)) + { + success = true; + ib.add(r); + } + } - for (TypeId part : parts) + // We need to distinguish between the empty case (there + // were no valid indexable types) and the bottom type (one of the + // indexable result types was never). UnionBuilder will opt to + // only record that its seen a top type as an optimization. we + // add a flag to distinguish these cases. + if (success) + bind(constraint, resultType, ib.build()); + else + bind(constraint, resultType, builtinTypes->errorType); + } + else { - TypeId r = arena->addType(BlockedType{}); - getMutable(r)->setOwner(constraint.get()); - bool ok = tryDispatchHasIndexer(recursionDepth, constraint, part, indexType, r, seen); - // If we've cut a recursive loop short, skip it. - if (!ok) - continue; + Set parts{nullptr}; + for (TypeId part : it) + parts.insert(follow(part)); - r = follow(r); - if (!get(r)) - results.insert(r); + Set results{nullptr}; + + for (TypeId part : parts) + { + TypeId r = arena->addType(BlockedType{}); + getMutable(r)->setOwner(constraint.get()); + + bool ok = tryDispatchHasIndexer(recursionDepth, constraint, part, indexType, r, seen); + // If we've cut a recursive loop short, skip it. + if (!ok) + continue; + + r = follow(r); + if (!get(r)) + results.insert(r); + } + + if (0 == results.size()) + bind(constraint, resultType, builtinTypes->errorType); + else if (1 == results.size()) + bind(constraint, resultType, *results.begin()); + else + DEPRECATED_emplace(constraint, resultType, std::vector(results.begin(), results.end())); } - if (0 == results.size()) - bind(constraint, resultType, builtinTypes->errorType); - else if (1 == results.size()) - bind(constraint, resultType, *results.begin()); - else - emplace(constraint, resultType, std::vector(results.begin(), results.end())); return true; } else if (auto ut = get(subjectType)) { - Set parts{nullptr}; - for (TypeId part : ut) - parts.insert(follow(part)); + // Indexing into a union of types means constructing a union of + // results: we don't know _which_ type it could be. + if (FFlag::LuauRemoveConstraintSolverEmplace) + { + UnionBuilder ub{arena, builtinTypes}; + bool success = false; - Set results{nullptr}; + for (TypeId option : ut) + { + TypeId r = arena->addType(BlockedType{}); + getMutable(r)->setOwner(constraint.get()); - for (TypeId part : parts) - { - TypeId r = arena->addType(BlockedType{}); - getMutable(r)->setOwner(constraint.get()); + bool ok = tryDispatchHasIndexer(recursionDepth, constraint, option, indexType, r, seen); + // If we've cut a recursive loop short, skip it. + if (!ok) + continue; - bool ok = tryDispatchHasIndexer(recursionDepth, constraint, part, indexType, r, seen); - // If we've cut a recursive loop short, skip it. - if (!ok) - continue; + r = follow(r); + success = true; + ub.add(r); + } - r = follow(r); - results.insert(r); + // We need to distinguish between the empty case (there + // were no valid indexable types) and the top type (one of the + // indexable result types was unknown). UnionBuilder will opt to + // only record that its seen a top type as an optimization. we + // add a flag to distinguish these cases. + if (success) + bind(constraint, resultType, ub.build()); + else + bind(constraint, resultType, builtinTypes->errorType); } - - if (0 == results.size()) - bind(constraint, resultType, builtinTypes->errorType); - else if (1 == results.size()) + else { - TypeId firstResult = *results.begin(); - shiftReferences(resultType, firstResult); - bind(constraint, resultType, firstResult); + + Set parts{nullptr}; + for (TypeId part : ut) + parts.insert(follow(part)); + + Set results{nullptr}; + + for (TypeId part : parts) + { + TypeId r = arena->addType(BlockedType{}); + getMutable(r)->setOwner(constraint.get()); + + bool ok = tryDispatchHasIndexer(recursionDepth, constraint, part, indexType, r, seen); + // If we've cut a recursive loop short, skip it. + if (!ok) + continue; + + r = follow(r); + results.insert(r); + } + + if (0 == results.size()) + bind(constraint, resultType, builtinTypes->errorType); + else if (1 == results.size()) + { + TypeId firstResult = *results.begin(); + bind(constraint, resultType, firstResult); + } + else + DEPRECATED_emplace(constraint, resultType, std::vector(results.begin(), results.end())); } - else - emplace(constraint, resultType, std::vector(results.begin(), results.end())); return true; } @@ -2245,7 +2401,14 @@ bool ConstraintSolver::tryDispatch(const HasIndexerConstraint& c, NotNull seen{nullptr}; - return tryDispatchHasIndexer(recursionDepth, constraint, subjectType, indexType, c.resultType, seen); + auto result = tryDispatchHasIndexer(recursionDepth, constraint, subjectType, indexType, c.resultType, seen); + + // CLI-205496: This implies that we also need an edge representing + // having an indexer, which is terrifying. + if (result) + unblock(subjectType, Location{}); + + return result; } bool ConstraintSolver::tryDispatch(const AssignPropConstraint& c, NotNull constraint) @@ -2384,8 +2547,7 @@ bool ConstraintSolver::tryDispatch(const AssignPropConstraint& c, NotNullcopyDependenciesOf(lhsType, rhsType); bind(constraint, c.propType, rhsType); Property& newProp = lhsTable->props[propName]; newProp.readTy = rhsType; @@ -2579,16 +2741,9 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull(resultTy)); - LUAU_ASSERT(canMutate(resultTy, constraint)); - } - if (get(resultTy)) { - if (FFlag::LuauUnpackRespectsAnnotations) - LUAU_ASSERT(canMutate(resultTy, constraint)); + LUAU_ASSERT(canMutate(resultTy, constraint)); if (follow(srcTy) == resultTy) { // It is sometimes the case that we find that a blocked type @@ -2597,8 +2752,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNullscope, Polarity::Positive); // FIXME? Is this the right polarity? trackInteriorFreeType(constraint->scope, f); - shiftReferences(resultTy, f); - emplaceType(asMutable(resultTy), f); + bind(constraint, resultTy, f); } else bind(constraint, resultTy, srcTy); @@ -2606,8 +2760,6 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNulllocation); - ++resultIter; ++i; } @@ -2635,7 +2787,7 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNullscope, constraint}; + TypeFunctionContext context{NotNull{this}, constraint->scope, constraint, subtyping}; FunctionGraphReductionResult result = reduceTypeFunctions(ty, constraint->location, NotNull{&context}, force); for (TypeId r : result.reducedTypes) @@ -2647,8 +2799,7 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNulllocation); + unblock(ity, constraint->location); } bool reductionFinished = result.blockedTypes.empty() && result.blockedPacks.empty(); @@ -2663,7 +2814,10 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNullmoduleName); + else + DEPRECATED_reportError(std::move(message)); } // if we're completely dispatching this constraint, we want to record any uninhabited type functions to unblock. @@ -2692,7 +2846,7 @@ bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNullscope, constraint}; + TypeFunctionContext context{NotNull{this}, constraint->scope, constraint, subtyping}; FunctionGraphReductionResult result = reduceTypeFunctions(tp, constraint->location, NotNull{&context}, force); for (TypeId r : result.reducedTypes) @@ -2778,7 +2932,7 @@ struct FindAllUnionMembers : TypeOnceVisitor bool visit(TypeId ty, const TableType& tbl) override { - if (FFlag::LuauRelateHandlesCoincidentTables && tbl.state != TableState::Sealed) + if (tbl.state != TableState::Sealed) blockedTys.insert(ty); else recordedTys.insert(ty); @@ -2826,6 +2980,10 @@ bool ConstraintSolver::tryDispatch(const SimplifyConstraint& c, NotNullscope, constraint->location, result, ty); } emplaceType(asMutable(target), result); + // HACK: This is one of the few spots in the constraint solver where + // we emplace a type that we do not claim we can mutate. Probably means + // this code is smelly and should be reworked. + cgraph->shiftReferences(target, result); return true; } @@ -2842,6 +3000,21 @@ bool ConstraintSolver::tryDispatch(const PushFunctionTypeConstraint& c, NotNull< if (!expectedFn || !fn) return true; + if (FFlag::LuauInstantiateFunctionTypeBeforePush) + { + if (auto instantiated = instantiate(builtinTypes, arena, NotNull{&limits}, constraint->scope, c.expectedFunctionType)) + { + expectedFn = get(*instantiated); + // If we had a function type before, we better have a function type after. + LUAU_ASSERT(expectedFn); + } + else + { + // If instantiate fails, just bail. + return true; + } + } + auto expectedParams = begin(expectedFn->argTypes); auto params = begin(fn->argTypes); @@ -2852,7 +3025,6 @@ bool ConstraintSolver::tryDispatch(const PushFunctionTypeConstraint& c, NotNull< { if (is(follow(*params))) { - shiftReferences(*params, *expectedParams); bind(constraint, *params, *expectedParams); } expectedParams++; @@ -2865,14 +3037,11 @@ bool ConstraintSolver::tryDispatch(const PushFunctionTypeConstraint& c, NotNull< size_t idx = 0; while (idx < c.expr->args.size && expectedParams != end(expectedFn->argTypes) && params != end(fn->argTypes)) { - // If we have an explicitly annotated parameter, a non-free type for - // the parameter, or the expected type contains a generic, bail. - // - Annotations should be respected above all else; - // - a non-free-type is unexpected, so just bail; - // - a generic in the expected type might cause us to leak a generic, so bail. - if (!c.expr->args.data[idx]->annotation && get(*params) && !ContainsAnyGeneric::hasAnyGeneric(*expectedParams)) + // Annotations should be respected above all else, if we see one bail. + // A non-free-type is unexpected, so also bail. + if (!c.expr->args.data[idx]->annotation && get(*params) && + (FFlag::LuauInstantiateFunctionTypeBeforePush || !ContainsAnyGeneric_DEPRECATED::hasAnyGeneric(*expectedParams))) { - shiftReferences(*params, *expectedParams); bind(constraint, *params, *expectedParams); } expectedParams++; @@ -2880,7 +3049,8 @@ bool ConstraintSolver::tryDispatch(const PushFunctionTypeConstraint& c, NotNull< idx++; } - if (!c.expr->returnAnnotation && get(fn->retTypes) && !ContainsAnyGeneric::hasAnyGeneric(expectedFn->retTypes)) + if (!c.expr->returnAnnotation && get(fn->retTypes) && + (FFlag::LuauInstantiateFunctionTypeBeforePush || !ContainsAnyGeneric_DEPRECATED::hasAnyGeneric(expectedFn->retTypes))) bind(constraint, fn->retTypes, expectedFn->retTypes); return true; @@ -2888,8 +3058,6 @@ bool ConstraintSolver::tryDispatch(const PushFunctionTypeConstraint& c, NotNull< bool ConstraintSolver::tryDispatch(const TypeInstantiationConstraint& c, NotNull constraint) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSupport); - if (isBlocked(c.functionType)) return block(c.functionType, constraint); @@ -2924,11 +3092,13 @@ TypeId ConstraintSolver::instantiateFunctionType( const Location& location ) { + functionTypeId = follow(functionTypeId); + // no work to be done if we're not instantiating with anything if (typeArguments.empty() && typePackArguments.empty()) return functionTypeId; - const FunctionType* ft = get(follow(functionTypeId)); + const FunctionType* ft = get(functionTypeId); if (!ft) { return functionTypeId; @@ -2965,37 +3135,36 @@ TypeId ConstraintSolver::instantiateFunctionType( replacementPacks[*typePackParametersIter++] = typePackArgument; } - Replacer r{arena, std::move(replacements), std::move(replacementPacks)}; + Replacer r{arena, NotNull{&replacements}, NotNull{&replacementPacks}}; - std::optional result = r.substitute(functionTypeId); - if (!result) - return builtinTypes->errorType; + CloneState cs{builtinTypes}; + // We clone persistent types here to enable instantiation for generic + // builtins like `table.find`; otherwise, the lines after would + // immediately corrupt the definitions of the original function. + auto clonedFunctionTypeId = shallowClone(functionTypeId, *arena, cs, /* clonePersistentTypes */ true); + FunctionType* ft2 = getMutable(clonedFunctionTypeId); + LUAU_ASSERT(ft != ft2); - FunctionType* ft2 = getMutable(*result); + // We instantiate all generics, replacing any with free types. + ft2->generics.clear(); - // we must remove the portions we successfully instantiated - dropWhile( - ft2->generics, - [](const TypeId& ty) - { - return !is(follow(ty)); - } - ); - dropWhile( - ft2->genericPacks, - [](const TypePackId& ty) - { - return !is(follow(ty)); - } - ); + // However, we only instantiate as many type pack arguments as are given. + if (!ft2->genericPacks.empty() && typePackArguments.size() < ft2->genericPacks.size()) + ft2->genericPacks.erase(ft2->genericPacks.begin(), ft2->genericPacks.begin() + typePackArguments.size()); + else + ft2->genericPacks.clear(); + auto result = r.substitute(clonedFunctionTypeId); + if (!result) + return builtinTypes->errorType; return *result; } bool ConstraintSolver::tryDispatch(const PushTypeConstraint& c, NotNull constraint, bool force) { Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}, &uninhabitedTypeFunctions}; - Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; + // Clip with LuauRemoveExtraSubtypingInstances + Subtyping subtyping_DEPRECATED{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; // NOTE: If we don't do this check up front, we almost immediately start // spawning tons of push type constraints. It's pretty important. @@ -3008,27 +3177,17 @@ bool ConstraintSolver::tryDispatch(const PushTypeConstraint& c, NotNull empty{nullptr}; - PushTypeResult result; - if (FFlag::LuauPushTypeConstraintLambdas3) - { - result = pushTypeInto( - c.astTypes, - c.astExpectedTypes, - NotNull{this}, - NotNull{constraint}, - NotNull{&empty}, - NotNull{&u2}, - NotNull{&subtyping}, - c.expectedType, - c.expr - ); - } - else - { - result = pushTypeInto_DEPRECATED( - c.astTypes, c.astExpectedTypes, NotNull{this}, NotNull{constraint}, NotNull{&u2}, NotNull{&subtyping}, c.expectedType, c.expr - ); - } + PushTypeResult result = pushTypeInto( + c.astTypes, + c.astExpectedTypes, + NotNull{this}, + NotNull{constraint}, + NotNull{&empty}, + NotNull{&u2}, + FFlag::LuauRemoveExtraSubtypingInstances ? subtyping : NotNull{&subtyping_DEPRECATED}, + c.expectedType, + c.expr + ); // If we're forcing this constraint, just early exit: we can continue // inferring the rest of the file, we might just error when we shouldn't. @@ -3037,17 +3196,18 @@ bool ConstraintSolver::tryDispatch(const PushTypeConstraint& c, NotNullscope, - constraint->location, - PushTypeConstraint{ - /* expectedType */ newExpectedTy, - /* targetType */ newTargetTy, - /* astTypes */ c.astTypes, - /* astExpectedTypes */ c.astExpectedTypes, - /* expr */ NotNull{newExpr}, - } - ); + NotNull addition = FFlag::DebugLuauCyclicRequireTypeInference + ? pushConstraint( + constraint->scope, + constraint->location, + PushTypeConstraint{newExpectedTy, newTargetTy, c.astTypes, c.astExpectedTypes, NotNull{newExpr}}, + constraint->moduleName + ) + : DEPRECATED_pushConstraint( + constraint->scope, + constraint->location, + PushTypeConstraint{newExpectedTy, newTargetTy, c.astTypes, c.astExpectedTypes, NotNull{newExpr}} + ); inheritBlocks(constraint, addition); } @@ -3067,7 +3227,10 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl TypeId tableTy = arena->addType(TableType{TableState::Sealed, {}, constraint->scope}); getMutable(tableTy)->indexer = TableIndexer{keyTy, valueTy}; - pushConstraint(constraint->scope, constraint->location, SubtypeConstraint{iteratorTy, tableTy}); + if (FFlag::DebugLuauCyclicRequireTypeInference) + pushConstraint(constraint->scope, constraint->location, SubtypeConstraint{iteratorTy, tableTy}, constraint->moduleName); + else + DEPRECATED_pushConstraint(constraint->scope, constraint->location, SubtypeConstraint{iteratorTy, tableTy}); auto it = begin(c.variables); auto endIt = end(c.variables); @@ -3127,7 +3290,19 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl if (iteratorTable->indexer) { - std::vector expectedVariables{iteratorTable->indexer->indexType, iteratorTable->indexer->indexResultType}; + std::vector expectedVariables; + // Add an intersection ReduceConstraint for the indexer result type to denote it can't be nil + const TypeId intersectionWithNotNil = arena->addTypeFunction( + builtinTypes->typeFunctions->intersectFunc, {iteratorTable->indexer->indexResultType, builtinTypes->notNilType} + ); + + if (FFlag::DebugLuauCyclicRequireTypeInference) + pushConstraint(constraint->scope, constraint->location, ReduceConstraint{intersectionWithNotNil}, constraint->moduleName); + else + DEPRECATED_pushConstraint(constraint->scope, constraint->location, ReduceConstraint{intersectionWithNotNil}); + + expectedVariables = {iteratorTable->indexer->indexType, intersectionWithNotNil}; + while (c.variables.size() >= expectedVariables.size()) expectedVariables.push_back(builtinTypes->errorType); @@ -3180,7 +3355,10 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl } else { - reportError(UnificationTooComplex{}, constraint->location); + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(UnificationTooComplex{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(UnificationTooComplex{}, constraint->location); } } else @@ -3190,7 +3368,10 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl } else { - reportError(UnificationTooComplex{}, constraint->location); + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(UnificationTooComplex{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(UnificationTooComplex{}, constraint->location); } } else if (auto iteratorMetatable = get(iteratorTy)) @@ -3222,7 +3403,9 @@ bool ConstraintSolver::tryDispatchIterableFunction(TypeId nextTy, TypeId tableTy TypePackId variablesPack = arena->addTypePack(BlockedTypePack{}); - auto callConstraint = pushConstraint(constraint->scope, constraint->location, FunctionCallConstraint{nextTy, tableTyPack, variablesPack}); + auto callConstraint = FFlag::DebugLuauCyclicRequireTypeInference + ? pushConstraint(constraint->scope, constraint->location, FunctionCallConstraint{nextTy, tableTyPack, variablesPack}, constraint->moduleName) + : DEPRECATED_pushConstraint(constraint->scope, constraint->location, FunctionCallConstraint{nextTy, tableTyPack, variablesPack}); getMutable(variablesPack)->owner = callConstraint.get(); @@ -3240,7 +3423,9 @@ NotNull ConstraintSolver::unpackAndAssign( NotNull constraint ) { - auto c = pushConstraint(constraint->scope, constraint->location, UnpackConstraint{destTypes, srcTypes}); + auto c = FFlag::DebugLuauCyclicRequireTypeInference + ? pushConstraint(constraint->scope, constraint->location, UnpackConstraint{destTypes, srcTypes}, constraint->moduleName) + : DEPRECATED_pushConstraint(constraint->scope, constraint->location, UnpackConstraint{destTypes, srcTypes}); for (TypeId t : destTypes) { @@ -3284,7 +3469,7 @@ TablePropLookupResult ConstraintSolver::lookupTableProp( if (isBlocked(subjectType)) return {{subjectType}, std::nullopt}; - else if (get(subjectType) || get(subjectType)) + else if (get(subjectType) || get(subjectType) || (FFlag::LuauIndexingIntoErrorGivesError && get(subjectType))) { return {{}, subjectType}; } @@ -3305,24 +3490,16 @@ TablePropLookupResult ConstraintSolver::lookupTableProp( } } - if (FFlag::LuauUseFastSubtypeForIndexerWithName) + if (ttv->indexer) { - if (ttv->indexer) - { - if (isBlocked(ttv->indexer->indexType)) - return {{ttv->indexer->indexType}, std::nullopt, true}; - - // CLI-169235: This is silly but this needs to use the same - // logic as `index<_, _>` - TypeId fauxLiteral = arena->addType(SingletonType{StringSingleton{propName}}); - if (fastIsSubtype(fauxLiteral, ttv->indexer->indexType)) - return {/* blockedTypes */ {}, ttv->indexer->indexResultType, /* isIndex */ true}; - } - } - else - { - if (ttv->indexer && maybeString(ttv->indexer->indexType)) - return {{}, ttv->indexer->indexResultType, /* isIndex = */ true}; + if (isBlocked(ttv->indexer->indexType)) + return {{ttv->indexer->indexType}, std::nullopt, true}; + + // CLI-169235: This is silly but this needs to use the same + // logic as `index<_, _>` + TypeId fauxLiteral = arena->addType(SingletonType{StringSingleton{propName}}); + if (fastIsSubtype(fauxLiteral, ttv->indexer->indexType)) + return {/* blockedTypes */ {}, ttv->indexer->indexResultType, /* isIndex */ true}; } if (ttv->state == TableState::Free) @@ -3364,6 +3541,11 @@ TablePropLookupResult ConstraintSolver::lookupTableProp( if (inConditional) return {{}, builtinTypes->unknownType}; } + else if (auto mt = get(subjectType); FFlag::LuauFixPropReadsOnMetatableTypes && mt && context == ValueContext::LValue) + { + // TODO __newindex: CLI-199848 + return lookupTableProp(constraint, mt->table, propName, context, inConditional, suppressSimplification, seen); + } else if (auto mt = get(subjectType); mt && context == ValueContext::RValue) { auto result = lookupTableProp(constraint, mt->table, propName, context, inConditional, suppressSimplification, seen); @@ -3410,6 +3592,21 @@ TablePropLookupResult ConstraintSolver::lookupTableProp( { if (auto p = lookupExternTypeProp(ct, propName)) return {{}, context == ValueContext::RValue ? p->readTy : p->writeTy}; + + if (FFlag::DebugLuauUserDefinedClasses) + { + if (ct->metatable) + { + if (const TableType* tt = get(*ct->metatable)) + { + // For user-defined classes, the instance metatable holds metamethods (e.g. __add) + // directly in its props rather than under an __index table. + if (auto prop = tt->props.find(propName); prop != tt->props.end()) + return {{}, context == ValueContext::RValue ? prop->second.readTy : prop->second.writeTy}; + } + } + } + if (ct->indexer) { return {{}, ct->indexer->indexResultType, /* isIndex = */ true}; @@ -3551,7 +3748,44 @@ bool ConstraintSolver::unify(NotNull constraint, TID subTy, TI { static_assert(std::is_same_v || std::is_same_v); - if (FFlag::LuauUnifyWithSubtyping2) + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}, &uninhabitedTypeFunctions}; + auto result = u2.unify(subTy, superTy); + + for (auto&& cv : u2.incompleteSubtypes) + if (FFlag::DebugLuauCyclicRequireTypeInference) + inheritBlocks(constraint, pushConstraint(constraint->scope, constraint->location, std::move(cv), constraint->moduleName)); + else + inheritBlocks(constraint, DEPRECATED_pushConstraint(constraint->scope, constraint->location, std::move(cv))); + + for (const auto& [ty, newUpperBounds] : u2.expandedFreeTypes) + { + auto& upperBounds = upperBoundContributors[ty]; + for (auto newUpperBound : newUpperBounds) + upperBounds.emplace_back(constraint->location, newUpperBound); + } + + switch (result) + { + case UnifyResult::OccursCheckFailed: + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(OccursCheckFailed{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(OccursCheckFailed{}, constraint->location); + return false; + case UnifyResult::TooComplex: + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(UnificationTooComplex{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(UnificationTooComplex{}, constraint->location); + return false; + case UnifyResult::Ok: + default: + return true; + } + } + else { Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; SubtypingUnifier stu{arena, builtinTypes, NotNull{&iceReporter}}; @@ -3565,7 +3799,9 @@ bool ConstraintSolver::unify(NotNull constraint, TID subTy, TI for (auto& cv : unifierResult.outstandingConstraints) { - auto newConstraint = pushConstraint(constraint->scope, constraint->location, std::move(cv)); + auto newConstraint = FFlag::DebugLuauCyclicRequireTypeInference + ? pushConstraint(constraint->scope, constraint->location, std::move(cv), constraint->moduleName) + : DEPRECATED_pushConstraint(constraint->scope, constraint->location, std::move(cv)); inheritBlocks(constraint, newConstraint); } @@ -3578,78 +3814,28 @@ bool ConstraintSolver::unify(NotNull constraint, TID subTy, TI switch (unifierResult.unified) { case UnifyResult::OccursCheckFailed: - reportError(OccursCheckFailed{}, constraint->location); + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(OccursCheckFailed{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(OccursCheckFailed{}, constraint->location); return false; case UnifyResult::TooComplex: - reportError(UnificationTooComplex{}, constraint->location); + if (FFlag::DebugLuauCyclicRequireTypeInference) + reportError(UnificationTooComplex{}, constraint->location, *constraint->moduleName); + else + DEPRECATED_reportError(UnificationTooComplex{}, constraint->location); return false; case UnifyResult::Ok: default: return true; } } - else - { - Unifier2 u2{NotNull{arena}, builtinTypes, constraint->scope, NotNull{&iceReporter}, &uninhabitedTypeFunctions}; - - const UnifyResult unifyResult = u2.unify(subTy, superTy); - - for (ConstraintV& c : u2.incompleteSubtypes) - { - NotNull addition = pushConstraint(constraint->scope, constraint->location, std::move(c)); - inheritBlocks(constraint, addition); - } - - if (UnifyResult::Ok == unifyResult) - { - for (const auto& [expanded, additions] : u2.expandedFreeTypes) - { - for (TypeId addition : additions) - upperBoundContributors[expanded].emplace_back(constraint->location, addition); - } - } - else - { - switch (unifyResult) - { - case Luau::UnifyResult::Ok: - break; - case Luau::UnifyResult::OccursCheckFailed: - reportError(OccursCheckFailed{}, constraint->location); - break; - case Luau::UnifyResult::TooComplex: - reportError(UnificationTooComplex{}, constraint->location); - break; - } - return false; - } - - return true; - } - -} - -bool ConstraintSolver::block_(BlockedConstraintId target, NotNull constraint) -{ - // If a set is not present for the target, construct a new DenseHashSet for it, - // else grab the address of the existing set. - auto [iter, inserted] = blocked.try_emplace(target, nullptr); - auto& [key, blockVec] = *iter; - - if (blockVec.find(constraint)) - return false; - - blockVec.insert(constraint); - - size_t& count = blockedConstraints[constraint]; - count += 1; - - return true; } void ConstraintSolver::block(NotNull target, NotNull constraint) { - const bool newBlock = block_(target.get(), constraint); + const bool newBlock = cgraph->addDependencyOf(target.get(), constraint.get()); + if (newBlock) { if (logger) @@ -3662,7 +3848,8 @@ void ConstraintSolver::block(NotNull target, NotNull constraint) { - const bool newBlock = block_(follow(target), constraint); + const bool newBlock = cgraph->addDependencyOf(follow(target), constraint.get()); + if (newBlock) { if (logger) @@ -3677,7 +3864,8 @@ bool ConstraintSolver::block(TypeId target, NotNull constraint bool ConstraintSolver::block(TypePackId target, NotNull constraint) { - const bool newBlock = block_(target, constraint); + const bool newBlock = cgraph->addDependencyOf(follow(target), constraint.get()); + if (newBlock) { if (logger) @@ -3692,89 +3880,7 @@ bool ConstraintSolver::block(TypePackId target, NotNull constr void ConstraintSolver::inheritBlocks(NotNull source, NotNull addition) { - // Anything that is blocked on this constraint must also be blocked on our - // synthesized constraints. - auto blockedIt = blocked.find(source.get()); - if (blockedIt != blocked.end()) - { - for (const Constraint* blockedConstraint : blockedIt->second) - { - block(addition, NotNull{blockedConstraint}); - } - } -} - -struct Blocker : TypeOnceVisitor -{ - NotNull solver; - NotNull constraint; - - bool blocked = false; - - explicit Blocker(NotNull solver, NotNull constraint) - : TypeOnceVisitor("Blocker", /* skipBoundTypes */ true) - , solver(solver) - , constraint(constraint) - { - } - - bool visit(TypeId ty, const PendingExpansionType&) override - { - blocked = true; - solver->block(ty, constraint); - return false; - } - - bool visit(TypeId ty, const ExternType&) override - { - return false; - } -}; - -bool ConstraintSolver::blockOnPendingTypes(TypeId target, NotNull constraint) -{ - Blocker blocker{NotNull{this}, constraint}; - blocker.traverse(target); - return !blocker.blocked; -} - -bool ConstraintSolver::blockOnPendingTypes(TypePackId targetPack, NotNull constraint) -{ - Blocker blocker{NotNull{this}, constraint}; - blocker.traverse(targetPack); - return !blocker.blocked; -} - -void ConstraintSolver::unblock_(BlockedConstraintId progressed) -{ - auto it = blocked.find(progressed); - if (it == blocked.end()) - return; - - // unblocked should contain a value always, because of the above check - for (const Constraint* unblockedConstraint : it->second) - { - auto& count = blockedConstraints[NotNull{unblockedConstraint}]; - if (FFlag::DebugLuauLogSolver) - printf("Unblocking count=%d\t%s\n", int(count), toString(*unblockedConstraint, opts).c_str()); - - // This assertion being hit indicates that `blocked` and - // `blockedConstraints` de-synchronized at some point. This is problematic - // because we rely on this count being correct to skip over blocked - // constraints. - LUAU_ASSERT(count > 0); - count -= 1; - } - - blocked.erase(it); -} - -void ConstraintSolver::unblock(NotNull progressed) -{ - if (logger) - logger->popBlock(progressed); - - return unblock_(progressed.get()); + cgraph->inheritBlocks(source.get(), addition.get()); } void ConstraintSolver::unblock(TypeId ty, Location location) @@ -3791,13 +3897,17 @@ void ConstraintSolver::unblock(TypeId ty, Location location) if (logger) logger->popBlock(progressed); - unblock_(progressed); - if (auto bt = get(progressed)) progressed = bt->boundTo; else break; } + + /** + * WARNING: It is critical that we pass the unfollowed type here: `unblockTypeOrPack` + * repairs all of the references from `ty` to its followed type. + */ + cgraph->unblockTypeOrPack(ty); } void ConstraintSolver::unblock(TypePackId progressed, Location) @@ -3805,33 +3915,36 @@ void ConstraintSolver::unblock(TypePackId progressed, Location) if (logger) logger->popBlock(progressed); - return unblock_(progressed); + return cgraph->unblockTypeOrPack(progressed); } -void ConstraintSolver::unblock(const std::vector& types, Location location) +void ConstraintSolver::reproduceConstraints(NotNull scope, const Location& location, const Substitution& subst, const std::shared_ptr& moduleName) { - for (TypeId t : types) - unblock(t, location); -} + for (auto [_, newTy] : subst.newTypes) + { + if (get(newTy)) + pushConstraint(scope, location, ReduceConstraint{newTy}, moduleName); + } -void ConstraintSolver::unblock(const std::vector& packs, Location location) -{ - for (TypePackId t : packs) - unblock(t, location); + for (auto [_, newPack] : subst.newPacks) + { + if (get(newPack)) + pushConstraint(scope, location, ReducePackConstraint{newPack}, moduleName); + } } -void ConstraintSolver::reproduceConstraints(NotNull scope, const Location& location, const Substitution& subst) +void ConstraintSolver::DEPRECATED_reproduceConstraints(NotNull scope, const Location& location, const Substitution& subst) { for (auto [_, newTy] : subst.newTypes) { if (get(newTy)) - pushConstraint(scope, location, ReduceConstraint{newTy}); + DEPRECATED_pushConstraint(scope, location, ReduceConstraint{newTy}); } for (auto [_, newPack] : subst.newPacks) { if (get(newPack)) - pushConstraint(scope, location, ReducePackConstraint{newPack}); + DEPRECATED_pushConstraint(scope, location, ReducePackConstraint{newPack}); } } @@ -3863,13 +3976,41 @@ bool ConstraintSolver::isBlocked(TypePackId tp) const return nullptr != get(tp); } -bool ConstraintSolver::isBlocked(NotNull constraint) const +NotNull ConstraintSolver::pushConstraint(NotNull scope, const Location& location, ConstraintV cv, std::shared_ptr moduleName) { - auto blockedIt = blockedConstraints.find(constraint); - return blockedIt != blockedConstraints.end() && blockedIt->second > 0; + std::optional scr; + if (auto sc = cv.get_if()) + scr.emplace(SubtypeConstraintRecord{sc->subType, sc->superType, SubtypingVariance::Covariant}); + else if (auto ec = cv.get_if()) + scr.emplace(SubtypeConstraintRecord{ec->assignmentType, ec->resultType, SubtypingVariance::Invariant}); + + if (scr) + { + if (auto f = seenConstraints.find(*scr)) + return NotNull{*f}; + } + + std::unique_ptr c = std::make_unique(scope, location, std::move(cv), std::move(moduleName)); + NotNull borrow = NotNull(c.get()); + + if (scr) + seenConstraints[*scr] = borrow; + + solverConstraints.push_back(std::move(c)); + unsolvedConstraints.emplace_back(borrow); + + if (solverConstraintLimit > 0) + { + --solverConstraintLimit; + + if (solverConstraintLimit == 0) + reportError(CodeTooComplex{}, location, *borrow->moduleName); + } + + return borrow; } -NotNull ConstraintSolver::pushConstraint(NotNull scope, const Location& location, ConstraintV cv) +NotNull ConstraintSolver::DEPRECATED_pushConstraint(NotNull scope, const Location& location, ConstraintV cv) { std::optional scr; if (auto sc = cv.get_if()) @@ -3897,17 +4038,17 @@ NotNull ConstraintSolver::pushConstraint(NotNull scope, const --solverConstraintLimit; if (solverConstraintLimit == 0) - reportError(CodeTooComplex{}, location); + DEPRECATED_reportError(CodeTooComplex{}, location); } return borrow; } -TypeId ConstraintSolver::resolveModule(const ModuleInfo& info, const Location& location) +TypeId ConstraintSolver::DEPRECATED_resolveModule(const ModuleInfo& info, const Location& location) { if (info.name.empty()) { - reportError(UnknownRequire{}, location); + DEPRECATED_reportError(UnknownRequire{}, location); return builtinTypes->errorType; } @@ -3921,14 +4062,14 @@ TypeId ConstraintSolver::resolveModule(const ModuleInfo& info, const Location& l if (!module) { if (!moduleResolver->moduleExists(info.name) && !info.optional) - reportError(UnknownRequire{moduleResolver->getHumanReadableModuleName(info.name)}, location); + DEPRECATED_reportError(UnknownRequire{moduleResolver->getHumanReadableModuleName(info.name)}, location); return builtinTypes->errorType; } if (module->type != SourceCode::Type::Module) { - reportError(IllegalRequire{module->humanReadableName, "Module is not a ModuleScript. It cannot be required."}, location); + DEPRECATED_reportError(IllegalRequire{module->humanReadableName, "Module is not a ModuleScript. It cannot be required."}, location); return builtinTypes->errorType; } @@ -3939,95 +4080,84 @@ TypeId ConstraintSolver::resolveModule(const ModuleInfo& info, const Location& l std::optional moduleType = first(modulePack); if (!moduleType) { - reportError(IllegalRequire{module->humanReadableName, "Module does not return exactly 1 value. It cannot be required."}, location); + DEPRECATED_reportError(IllegalRequire{module->humanReadableName, "Module does not return exactly 1 value. It cannot be required."}, location); return builtinTypes->errorType; } return *moduleType; } -void ConstraintSolver::reportError(TypeErrorData&& data, const Location& location) +TypeId ConstraintSolver::resolveModule(const ModuleInfo& info, const Location& location, const ModuleName& moduleName) { - errors.emplace_back(location, std::move(data)); - errors.back().moduleName = module->name; -} - -void ConstraintSolver::reportError(TypeError e) -{ - errors.emplace_back(std::move(e)); - errors.back().moduleName = module->name; -} - -void ConstraintSolver::shiftReferences(TypeId source, TypeId target) -{ - target = follow(target); - - // if the target isn't a reference counted type, there's nothing to do. - // this stops us from keeping unnecessary counts for e.g. primitive types. - if (!isReferenceCountedType(target)) - return; + if (info.name.empty()) + { + reportError(UnknownRequire{}, location, moduleName); + return builtinTypes->errorType; + } - // This can happen in the _very_ specific case of: - // - // local Tbl = {} - // Tbl.__index = Tbl - // - // This would probably not be required if table type stating worked in - // a reasonable manner. - if (source == target) - return; + for (const auto& [location, path] : requireCycles) + { + if (!path.empty() && path.front() == info.name) + return builtinTypes->anyType; + } - auto sourceRefs = unresolvedConstraints.find(source); - if (sourceRefs) + ModulePtr module = moduleResolver->getModule(info.name); + if (!module) { - // we read out the count before proceeding to avoid hash invalidation issues. - size_t count = *sourceRefs; + if (!moduleResolver->moduleExists(info.name) && !info.optional) + reportError(UnknownRequire{moduleResolver->getHumanReadableModuleName(info.name)}, location, moduleName); - auto [targetRefs, _] = unresolvedConstraints.try_insert(target, 0); - targetRefs += count; + return builtinTypes->errorType; } - // Any constraint that might have mutated source may now mutate target - if (auto it = mutatedFreeTypeToConstraint.find(source); it != mutatedFreeTypeToConstraint.end()) + if (module->type != SourceCode::Type::Module) { - const OrderedSet& constraintsAffectedBySource = it->second; - auto [it2, fresh2] = mutatedFreeTypeToConstraint.try_emplace(target); + reportError(IllegalRequire{module->humanReadableName, "Module is not a ModuleScript. It cannot be required."}, location, moduleName); + return builtinTypes->errorType; + } - OrderedSet& constraintsAffectedByTarget = it2->second; + TypePackId modulePack = module->returnType; + if (get(modulePack)) + return builtinTypes->errorType; - for (const Constraint* constraint : constraintsAffectedBySource) - { - constraintsAffectedByTarget.insert(constraint); - auto [it3, fresh3] = maybeMutatedFreeTypes.try_emplace(NotNull{constraint}, TypeIds{}); - it3->second.insert(target); - } + std::optional moduleType = first(modulePack); + if (!moduleType) + { + reportError(IllegalRequire{module->humanReadableName, "Module does not return exactly 1 value. It cannot be required."}, location, moduleName); + return builtinTypes->errorType; } + + return *moduleType; } -std::optional ConstraintSolver::generalizeFreeType(NotNull scope, TypeId type) +void ConstraintSolver::reportError(TypeErrorData&& data, const Location& location, const ModuleName& errorModule) { - TypeId t = follow(type); - if (get(t)) - { - auto refCount = unresolvedConstraints.find(t); - if (refCount && *refCount > 0) - return {}; + errors.emplace_back(location, std::move(data)); + errors.back().moduleName = errorModule.empty() ? *representativeModuleName : errorModule; +} - // if no reference count is present, then that means the only constraints referring to - // this free type need only for it to be generalized. in principle, this means we could - // have actually never generated the free type in the first place, but we couldn't know - // that until all constraint generation is complete. - } +void ConstraintSolver::DEPRECATED_reportError(TypeErrorData&& data, const Location& location) +{ + errors.emplace_back(location, std::move(data)); + errors.back().moduleName = module->name; +} - return generalize(NotNull{arena}, builtinTypes, scope, generalizedTypes, type); +void ConstraintSolver::DEPRECATED_reportError(TypeError e) +{ + errors.emplace_back(std::move(e)); + errors.back().moduleName = module->name; } -bool ConstraintSolver::hasUnresolvedConstraints(TypeId ty) +void ConstraintSolver::DEPRECATED_reportError(TypeError e, const ModuleName& errorModule) { - if (auto refCount = unresolvedConstraints.find(ty)) - return *refCount > 0; + errors.emplace_back(std::move(e)); + errors.back().moduleName = errorModule.empty() ? *representativeModuleName : errorModule; +} - return false; +bool ConstraintSolver::hasUnresolvedConstraints(TypeId ty) +{ + ty = follow(ty); + return cgraph->hasUnsolvedDependencies(ty); } TypeId ConstraintSolver::simplifyIntersection(NotNull scope, Location location, TypeId left, TypeId right) @@ -4077,12 +4207,18 @@ TypePackId ConstraintSolver::anyifyModuleReturnTypePackGenerics(TypePackId tp) LUAU_NOINLINE void ConstraintSolver::throwTimeLimitError() const { - throw TimeLimitError(module->name); + if (FFlag::DebugLuauCyclicRequireTypeInference) + throw TimeLimitError(*representativeModuleName); + else + throw TimeLimitError(module->name); } LUAU_NOINLINE void ConstraintSolver::throwUserCancelError() const { - throw UserCancelError(module->name); + if (FFlag::DebugLuauCyclicRequireTypeInference) + throw UserCancelError(*representativeModuleName); + else + throw UserCancelError(module->name); } // Instantiate private template implementations for external callers @@ -4102,22 +4238,7 @@ std::vector> borrowConstraints(const std::vector c : cs->unsolvedConstraints) - { - auto it = cs->blockedConstraints.find(c); - int blockCount = it == cs->blockedConstraints.end() ? 0 : int(it->second); - printf("\t%d\t%s\n", blockCount, toString(*c, opts).c_str()); - - if (FFlag::DebugLuauLogSolverIncludeDependencies) - { - for (NotNull dep : c->dependencies) - { - if (std::find(cs->unsolvedConstraints.begin(), cs->unsolvedConstraints.end(), dep) != cs->unsolvedConstraints.end()) - printf("\t\t|\t%s\n", toString(*dep, opts).c_str()); - } - } - } + cs->cgraph->dumpWith(cs->unsolvedConstraints, opts); } } // namespace Luau diff --git a/Analysis/src/ControlFlowGraph.cpp b/Analysis/src/ControlFlowGraph.cpp new file mode 100644 index 00000000..74ff3ce5 --- /dev/null +++ b/Analysis/src/ControlFlowGraph.cpp @@ -0,0 +1,686 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/ControlFlowGraph.h" +#include "Luau/Ast.h" +#include "Luau/AstUtils.h" +#include "Luau/Common.h" + +#include +#include +#include + +LUAU_FASTFLAG(DebugLuauFreezeArena) + +namespace Luau::CFG +{ + +namespace CFGRefinement +{ + +RefinementId RefinementArena::proposition(DefId def, bool sense) +{ + return NotNull{allocator.allocate(Proposition{def, std::nullopt, /*isTypeof*/ false, sense})}; +} + +RefinementId RefinementArena::typeProposition(DefId def, std::optional type, bool isTypeof, bool sense) +{ + return NotNull{allocator.allocate(Proposition{def, std::move(type), isTypeof, sense})}; +} + +RefinementId RefinementArena::conjunction(RefinementId lhs, RefinementId rhs) +{ + return NotNull{allocator.allocate(Conjunction{lhs, rhs})}; +} + +RefinementId RefinementArena::disjunction(RefinementId lhs, RefinementId rhs) +{ + return NotNull{allocator.allocate(Disjunction{lhs, rhs})}; +} + +RefinementId RefinementArena::negation(RefinementId r) +{ + if (auto* conj = get(r)) + return disjunction(negation(conj->lhs), negation(conj->rhs)); + if (auto* disj = get(r)) + return conjunction(negation(disj->lhs), negation(disj->rhs)); + if (auto* neg = get(r)) + return neg->refinement; + + LUAU_ASSERT(get(r)); + return NotNull{allocator.allocate(Negation{r})}; +} + +void RefinementArena::freeze() +{ + allocator.freeze(); +} + +} // namespace CFGRefinement + +Block::Block(BlockKind kind, std::string debugName) + : kind(kind) + , debugName(debugName) +{ +} + +void Block::addSuccessor(Block* target) +{ + successors.emplace_back(target); + target->predecessors.emplace_back(this); +} + +bool Block::containsDefinition(Symbol sym) const +{ + return reachingDefinitions.contains(sym); +} + +Definition* Block::getReachingDefinition(Symbol sym) const +{ + if (auto* v = reachingDefinitions.find(sym)) + return *v; + return nullptr; +} + +void Block::setReachingDefinition(Symbol sym, DefId def) +{ + reachingDefinitions[sym] = def; +} + +const std::vector& Block::getInstructions() const +{ + return instructions; +} + +const std::vector& Block::getPredecessors() const +{ + return predecessors; +} + +const std::vector& Block::getSuccessors() const +{ + return successors; +} + +Block* CFGAllocator::newBlock(BlockKind kind, std::string debugName) +{ + return block.allocate(kind, debugName); +} + +DefId CFGAllocator::newDefinition(Symbol sym, size_t version) +{ + return NotNull{defs.allocate(SymDef{sym, version})}; +} + +void CFGAllocator::freeze() +{ + block.freeze(); + defs.freeze(); + refinementArena.freeze(); + frozen = true; +} + +BlockId ControlFlowGraph::newBlock(BlockKind kind, std::string debugName) +{ + Block* b = allocator->newBlock(kind, debugName); + return blocks.emplace_back(b); +} + +void ControlFlowGraph::computeRPO() +{ + std::vector stack; + DenseHashSet visited{nullptr}; + + stack.push_back(blocks[0]); + while (!stack.empty()) + { + auto& curr = stack.back(); + visited.insert(curr); + bool added = false; + for (auto succ : curr->getSuccessors()) + { + if (!visited.contains(succ)) + { + stack.emplace_back(succ); + added = true; + } + } + if (added) + continue; + + rpoOrder.emplace_back(curr); + stack.pop_back(); + } + + std::reverse(rpoOrder.begin(), rpoOrder.end()); +} + +Definition* ControlFlowGraph::resolve(Definition* def) const +{ + // It's possible that we could do path compression here as an easy optimization. + while (auto* fwd = forwards.find(def)) + def = *fwd; + return def; +} + +Definition* ControlFlowGraph::getUseDef(AstExpr* expr) const +{ + if (auto* def = useDefs.find(expr)) + return resolve(*def); + return nullptr; +} + +Definition* ControlFlowGraph::getLhsDef(const LValue& lv) const +{ + if (auto* def = lhsDefs.find(lv)) + return resolve(*def); + return nullptr; +} + + +CFGBuilder::CFGBuilder(NotNull allocator) + : cfg(std::make_unique(allocator)) + , allocator(allocator) + , currentBlock(cfg->newBlock(BlockKind::Entry, "Entry Block")) +{ + seal(currentBlock); +} + +std::unique_ptr CFGBuilder::makeCFG(NotNull allocator, AstStatBlock* block) +{ + CFGBuilder builder(allocator); + builder.lower(block); + auto cfg = std::move(builder.cfg); + cfg->computeRPO(); + if (FFlag::DebugLuauFreezeArena) + allocator->freeze(); + return cfg; +} + +bool CFGBuilder::isSealed(Block* b) +{ + return sealedBlocks.contains(b); +} + +// The CFGBuilder walks a AstStatBlock and labels definitions in SSA form. +// When we finish walking a block and labelling each def, the block is 'filled' +// Filled blocks are allowed to have successors +// a block is sealed, if no predecessors need to be added +// Because only filled blocks can have successors, predecessors are always filled. +// You should seal a block when you are sure that no more predecessors will be added to it. +// E.g. When lowering a while loop, the condition block might have a back edge from the body of the while loop +// You can only seal this when the body has finished lowering, since that is the last +// predecessor that must be added +void CFGBuilder::seal(Block* b) +{ + auto joinsToFill = incompleteJoins.find(b); + if (joinsToFill != nullptr) + { + for (auto inst : *joinsToFill) + { + // If you're sealing a block and filling the arguments to joins, + // it's possible for other instructions to get recursively re-written, including other joins. + // In this case, we should check if the incomplete instruction is still a join + if (auto join = inst->get_if()) + { + DefId resolved = fillJoinOperands(b, NotNull{inst}, join); + b->setReachingDefinition(resolved->sym, resolved); + } + } + incompleteJoins[b] = DenseHashSet{}; + } + sealedBlocks.insert(b); +} + +void CFGBuilder::lower(AstStat* statement) +{ + if (auto blk = statement->as()) + lower(blk); + else if (auto local = statement->as()) + lower(local); + else if (auto assn = statement->as()) + lower(assn); + else if (auto statIf = statement->as()) + lower(statIf); + else if (auto statWhile = statement->as()) + lower(statWhile); + else if (auto expr = statement->as()) + lower(expr); + else + { + LUAU_ASSERT(!"Unhandled statement"); + } +} + +void CFGBuilder::lower(AstStatExpr* stat) +{ + lowerExpr(stat->expr); +} + +bool CFGBuilder::tryLowerAssertion(AstExprCall* call) +{ + if (call->args.size == 0) + return false; + + auto global = call->func->as(); + if (!global || global->name != "assert") + return false; + + AstExpr* cond = call->args.data[0]; + for (size_t i = 0; i < call->args.size; i++) + { + lowerExpr(call->args.data[i]); + if (i == 0) + { + if (auto ref = resolveCondition(cond)) + emitRefineInstruction(currentBlock, *ref); + } + } + + return true; +} + +void CFGBuilder::lower(AstStatBlock* statement) +{ + for (auto st : statement->body) + lower(st); +} + +void CFGBuilder::lower(AstStatLocal* local) +{ + for (size_t i = 0; i < local->vars.size; i++) + { + AstLocal* loc = local->vars.data[i]; + AstExpr* expr = i < local->values.size ? local->values.data[i] : nullptr; + + if (expr) + lowerExpr(expr); + + Symbol sym(loc); + DefId def = newDefinition(sym); + emit(currentBlock, def, local); + currentBlock->setReachingDefinition(sym, def); + cfg->lhsDefs[LValue{sym}] = def.get(); + } +} + +std::optional extractLValueSymbol(AstExpr* target) +{ + if (auto local = target->as()) + return Symbol(local->local); + else if (auto global = target->as()) + return Symbol(global->name); + + return std::nullopt; +} + +void CFGBuilder::lower(AstStatAssign* assn) +{ + for (size_t i = 0; i < assn->values.size; i++) + lowerExpr(assn->values.data[i]); + + for (size_t i = 0; i < assn->vars.size; i++) + { + AstExpr* target = assn->vars.data[i]; + + if (auto sym = extractLValueSymbol(target)) + { + DefId def = newDefinition(*sym); + emit(currentBlock, def, assn); + currentBlock->setReachingDefinition(*sym, def); + cfg->lhsDefs[LValue{target}] = def.get(); + } + else + { + LUAU_ASSERT(!"Unhandled lvalue type"); + } + } +} + +Block* CFGBuilder::newBlock(BlockKind kind, std::string debugName, Block* pred) +{ + Block* b = cfg->newBlock(kind, debugName); + if (pred) + pred->addSuccessor(b); + return b; +} + +DefId CFGBuilder::newDefinition(Symbol sym) +{ + return allocator->newDefinition(sym, nextVersionIndex(sym)); +} + +std::pair> CFGBuilder::emitJoin(Block* block, Symbol sym) +{ + DefId def = newDefinition(sym); + InstrId jInstr = emit(block, def); + Join* j = jInstr->get_if(); + LUAU_ASSERT(j); + + block->setReachingDefinition(sym, def); + incompleteJoins[block].insert(jInstr); + return {jInstr, NotNull{j}}; +} + +void CFGBuilder::lower(AstStatIf* statIf) +{ + Block* currBlock = currentBlock.get(); + + Block* thenBlock = newBlock(BlockKind::Linear, "then branch", currBlock); + auto ref = resolveCondition(statIf->condition); + if (ref) + emitRefineInstruction(thenBlock, *ref); + + // Then only has one predecessor + seal(thenBlock); + Block* thenExit; + { + BlockScope scope(*this, thenBlock); + lower(statIf->thenbody); + thenExit = currentBlock.get(); + } + + // Else branch (may be nullptr, another AstStatIf for elseif, or a block) + Block* elseBlock = newBlock(BlockKind::Linear, "else branch", currBlock); + Block* elseExit = elseBlock; // If there is an else body, overwrite this + seal(elseBlock); + if (ref) + emitRefineInstruction(elseBlock, allocator->refinementArena.negation(*ref)); + + + if (statIf->elsebody) + { + BlockScope scope(*this, elseBlock); + lower(statIf->elsebody); + elseExit = currentBlock.get(); + } + + // Merge block — all paths converge here + // Predecessors are wired up below once we know which branches reach here. + Block* mergeBlock = newBlock(BlockKind::Linear, "merge"); + thenExit->addSuccessor(mergeBlock); + elseExit->addSuccessor(mergeBlock); + seal(mergeBlock); + currentBlock = NotNull{mergeBlock}; +} + + +void CFGBuilder::lower(AstStatWhile* statWhile) +{ + Block* preLoop = currentBlock.get(); + + // Loop header — receives the back-edge so we don't seal it yet. Resolve the + // condition inside the header's scope so reads of variables mutated in the + // body hit this unsealed block, emit an incomplete Join, and get their + // operands filled in when the header is sealed after the back-edge. + Block* loopHeader = newBlock(BlockKind::Condition, "while-loop condition", preLoop); + std::optional ref; + { + BlockScope scope(*this, loopHeader); + ref = resolveCondition(statWhile->condition); + } + + Block* bodyBlock = newBlock(BlockKind::Linear, "while-loop body", loopHeader); + if (ref) + emitRefineInstruction(bodyBlock, *ref); + seal(bodyBlock); + Block* bodyExit; + { + BlockScope scope(*this, bodyBlock); + lower(statWhile->body); + bodyExit = currentBlock.get(); + } + + // You can seal the loop header now because no predecessors will be added to it. + bodyExit->addSuccessor(loopHeader); + seal(loopHeader); + + Block* exitBlock = newBlock(BlockKind::Linear, "while-loop exit", loopHeader); + if (ref) + emitRefineInstruction(exitBlock, allocator->refinementArena.negation(*ref)); + seal(exitBlock); + currentBlock = NotNull{exitBlock}; +} + +void CFGBuilder::lowerExpr(AstExpr* expr) +{ + if (auto local = expr->as()) + { + lowerExpr(local); + } + else if (auto binop = expr->as()) + { + lowerExpr(binop->left); + lowerExpr(binop->right); + } + else if (auto call = expr->as()) + { + lowerExpr(call); + } + else if (auto group = expr->as()) + { + lowerExpr(group->expr); + } +} + +void CFGBuilder::lowerExpr(AstExprLocal* local) +{ + DefId def = readVariable(currentBlock, Symbol(local->local)); + cfg->useDefs[local] = def; +} + +void CFGBuilder::lowerExpr(AstExprCall* call) +{ + if (tryLowerAssertion(call)) + return; + lowerExpr(call->func); + for (size_t i = 0; i < call->args.size; i++) + lowerExpr(call->args.data[i]); +} + +std::optional CFGBuilder::resolveCondition(AstExpr* condition) +{ + auto& arena = allocator->refinementArena; + + if (auto group = condition->as()) + { + return resolveCondition(group->expr); + } + else if (auto loc = condition->as()) + { + DefId def = readVariable(currentBlock, Symbol(loc->local)); + cfg->useDefs[loc] = def; + return arena.proposition(def, /* sense */ true); + } + else if (auto binop = condition->as()) + { + if (auto tg = matchTypeGuard(binop->op, binop->left, binop->right)) + { + if (auto tgtLocal = tg->target->as()) + { + auto def = readVariable(currentBlock, Symbol(tgtLocal->local)); + cfg->useDefs[tgtLocal] = def; + bool sense = binop->op == AstExprBinary::CompareEq; + return arena.typeProposition(def, tg->type, tg->isTypeof, sense); + } + return std::nullopt; + } + + auto lRef = resolveCondition(binop->left); + auto rRef = resolveCondition(binop->right); + if (binop->op == AstExprBinary::And) + { + // (A and B) truthy => both truthy; a missing side still preserves the other. + if (lRef && rRef) + return arena.conjunction(*lRef, *rRef); + return lRef ? lRef : rRef; + } + else if (binop->op == AstExprBinary::Or) + { + // (A or B) truthy => at least one truthy; an unrefined side means we can't narrow. + if (lRef && rRef) + return arena.disjunction(*lRef, *rRef); + } + } + else if (auto unop = condition->as()) + { + if (unop->op == AstExprUnary::Op::Not) + { + if (auto inner = resolveCondition(unop->expr)) + return arena.negation(*inner); + } + } + + return std::nullopt; +} + +void CFGBuilder::emitRefineInstruction(Block* block, CFGRefinement::RefinementId refinement) +{ + // This function walks the refinement tree and, at every + // Proposition, emits a refinement into the block with a fresh definition. + // This differs only slightly from SSI form, which introduces a virtual `sigma` + // definition as a terminator, which then gets written in subsequent blocks. + // I've chosen to elide this terminator in favor of just emitting the fresh def + refinement + // explicitly into the block. A consequence of this is that this representation will mint a + // empty block with only refinement information, but this just makes it easier to handle phi emission. + visit( + overloaded{ + [&](const CFGRefinement::Proposition& prop) + { + DefId refined = newDefinition(prop.ptr->sym); + emit(block, refined, prop); + block->setReachingDefinition(prop.ptr->sym, refined); + }, + [&](const CFGRefinement::Conjunction& conj) + { + emitRefineInstruction(block, conj.lhs); + emitRefineInstruction(block, conj.rhs); + }, + [&](const CFGRefinement::Negation& neg) + { + // RefinementArena::negation pushes through And/Or via DeMorgan and cancels + // double negation, so the only shape that reaches here is Negation(Proposition). + auto prop = neg.refinement->get_if(); + LUAU_ASSERT(prop != nullptr); + emitRefineInstruction(block, allocator->refinementArena.typeProposition(prop->ptr, prop->type, prop->isTypeof, !prop->sense)); + }, + [&](const CFGRefinement::Disjunction&) + { + // CLI-205330 tracks the work needed to handle Disjunctions (e.g. we need to calculate sets of propositions on individual defs + // For the most part, this would just re-implement the existing refinement calculation logic in constraint generation. + }, + }, + *refinement + ); +} + +DefId CFGBuilder::readVariable(BlockId block, Symbol sym) +{ + if (auto v = block->getReachingDefinition(sym); v != nullptr) + return NotNull{v}; + + if (!isSealed(block)) + { + auto p = emitJoin(block, sym); + return p.second->definition; + } + else if (block->getPredecessors().size() == 1) + { + auto def = readVariable(block->getPredecessors().front(), sym); + block->setReachingDefinition(sym, def); + return def; + } + else + { + auto [inst, join] = emitJoin(block, sym); + block->setReachingDefinition(sym, join->definition); + auto d = fillJoinOperands(block, inst, join); + block->setReachingDefinition(d->sym, d); + return d; + } +} + +DefId CFGBuilder::fillJoinOperands(Block* block, InstrId instr, Join* j) +{ + for (BlockId pred : block->getPredecessors()) + { + auto def = readVariable(pred, j->definition->sym); + j->operands.emplace_back(def); + } + + recordUses(instr); + return trimTrivialJoin(instr, j); +} + + +DefId CFGBuilder::trimTrivialJoin(InstrId inst, Join* j) +{ + LUAU_ASSERT(j); + auto curr = j->definition; + // Tracks the duplicated or unreachable phi nodes + Definition* same = nullptr; + // Phis have two operands (by construction) + for (auto& op : j->operands) + { + if (op == same || op == curr) + continue; + if (same != nullptr) + return curr; + same = op; + } + + Set tmp_; + Set& usingInsts = tmp_; + if (Set* uses = usingInstructions.find(curr)) + usingInsts = *uses; + usingInsts.erase(inst); + + if (same == nullptr) + return curr; + + inst->emplace(); + // The current join is trivial, so we can replace it with the single op that isn't itself + cfg->forwards[curr.get()] = same; + + for (auto& usingInst : usingInsts) + { + if (Join* j = usingInst->get_if()) + trimTrivialJoin(NotNull{usingInst}, j); + } + + return NotNull{same}; +} + +void CFGBuilder::recordUses(InstrId inst) +{ + visit( + overloaded{ + [&](const Declare&) {}, + [&](const Assign&) {}, + [&](const Dead&) {}, + [&](const Join& join) + { + for (auto& op : join.operands) + usingInstructions[op].insert(inst); + }, + [&](const Refine& refine) + { + usingInstructions[refine.toRefine.get()].insert(inst); + }, + }, + *inst.get() + ); +} + +size_t CFGBuilder::nextVersionIndex(Symbol sym) +{ + if (!versionCounter.contains(sym)) + { + versionCounter[sym] = 0; + return 0; + } + + auto ref = versionCounter.find(sym); + *ref += 1; + return *ref; +} + +} // namespace Luau::CFG diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index cd6ecd33..78777cfb 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -8,14 +8,13 @@ #include "Luau/Error.h" #include "Luau/TimeTrace.h" -#include #include LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSyntax) -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSupport) -LUAU_FASTFLAGVARIABLE(LuauCaptureRecursiveCallsForTablesAndGlobals) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAGVARIABLE(LuauDoNotOverwriteAstDefs) +LUAU_FASTFLAGVARIABLE(LuauAvoidTrivialPhis) namespace Luau { @@ -229,18 +228,47 @@ void DataFlowGraphBuilder::join(DfgScope* p, DfgScope* a, DfgScope* b) void DataFlowGraphBuilder::joinBindings(DfgScope* p, const DfgScope& a, const DfgScope& b) { - for (const auto& [sym, def1] : a.bindings) + if (FFlag::LuauAvoidTrivialPhis) { - if (auto def2 = b.bindings.find(sym)) - p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); - else if (auto def2 = p->lookup(sym)) - p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); - } + auto join = [&](auto sym, auto def1, auto def2) + { + // Refinements are keyed on `DefId`s, meaning that allocating + // a trivial phi node like this *breaks* refinements. + if (def1 == def2) + p->bindings[sym] = def1; + else + p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{def2}); + }; - for (const auto& [sym, def1] : b.bindings) + for (const auto& [sym, def1] : a.bindings) + { + if (auto def2 = b.bindings.find(sym)) + join(sym, def1, *def2); + else if (auto def2 = p->lookup(sym)) + join(sym, def1, *def2); + } + + for (const auto& [sym, def1] : b.bindings) + { + if (auto def2 = p->lookup(sym)) + join(sym, def1, *def2); + } + } + else { - if (auto def2 = p->lookup(sym)) - p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + for (const auto& [sym, def1] : a.bindings) + { + if (auto def2 = b.bindings.find(sym)) + p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + else if (auto def2 = p->lookup(sym)) + p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + } + + for (const auto& [sym, def1] : b.bindings) + { + if (auto def2 = p->lookup(sym)) + p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + } } } @@ -336,8 +364,7 @@ DefId DataFlowGraphBuilder::lookup(DefId def, const std::string& key, Location l if (auto it = props->find(key); it != props->end()) return NotNull{it->second}; } - else if (auto phi = get(def); - phi && phi->operands.empty() && (!FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals || current->scopeType == DfgScope::Function)) + else if (auto phi = get(def); phi && phi->operands.empty() && current->scopeType == DfgScope::Function) { DefId result = defArena->freshCell(def->name, location); scope->props[def][key] = result; @@ -434,6 +461,11 @@ ControlFlow DataFlowGraphBuilder::visit(AstStat* s) return visit(d); else if (auto d = s->as()) return visit(d); + else if (auto d = s->as()) + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + return visit(d); + } else if (auto error = s->as()) return visit(error); else @@ -705,70 +737,56 @@ ControlFlow DataFlowGraphBuilder::visit(AstStatFunction* f) // but for bug compatibility, we'll assume the same thing here. visitLValue(f->name, defArena->freshCell(Symbol{}, f->name->location)); - if (FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals) + // This logic is for supporting: + // + // local coolmath = {} + // function coolmath.factorial(n: number) + // if n <= 1 then + // return 1 + // else + // return coolmath.factorial(n - 1) * n + // end + // end + // + // We want to ensure that the `coolmath.factorial` inside the function + // statement uses the ungeneralized function type. Without any + // intervention we would use the version from the captured `coolmath` + // upvalue, which would be the generalized type. That would cause + // the above snippet to _always_ force a constraint, as there is a + // cycle between the generalization constraint of the function and + // the constraints related to resolving the recursive call. We add + // a similar case for global functions, as in: + // + // function walk(n) + // if n.tag == "leaf" then + // print(n.value) + // else + // walk(n.left) + // print(n.value) + // walk(n.right) + // end + // end + // + // NOTE: It is not immediately obvious to me, in DataFlowGraph, if this + // can be extended to any arbitrary assignment, such as: + // + // function foo.bar.baz.bing() + // local _ = foo.bar.baz.bing() + // end + // + // ... hence us only handling the common case of a single property deep. + DfgScope* signatureScope = makeChildScope(DfgScope::Function); + PushScope ps{scopeStack, signatureScope}; + if (auto global = f->name->as()) { - // This logic is for supporting: - // - // local coolmath = {} - // function coolmath.factorial(n: number) - // if n <= 1 then - // return 1 - // else - // return coolmath.factorial(n - 1) * n - // end - // end - // - // We want to ensure that the `coolmath.factorial` inside the function - // statement uses the ungeneralized function type. Without any - // intervention we would use the version from the captured `coolmath` - // upvalue, which would be the generalized type. That would cause - // the above snippet to _always_ force a constraint, as there is a - // cycle between the generalization constraint of the function and - // the constraints related to resolving the recursive call. We add - // a similar case for global functions, as in: - // - // function walk(n) - // if n.tag == "leaf" then - // print(n.value) - // else - // walk(n.left) - // print(n.value) - // walk(n.right) - // end - // end - // - // NOTE: It is not immediately obvious to me, in DataFlowGraph, if this - // can be extended to any arbitrary assignment, such as: - // - // function foo.bar.baz.bing() - // local _ = foo.bar.baz.bing() - // end - // - // ... hence us only handling the common case of a single property deep. - if (auto global = f->name->as()) - { - DfgScope* signatureScope = makeChildScope(DfgScope::Function); - PushScope ps{scopeStack, signatureScope}; - signatureScope->bindings[global->name] = graph.getDef(f->name); - visitFunction(f->func, NotNull{signatureScope}); - } - else if (auto name = f->name->as(); name && name->expr->is()) - { - auto receiver = name->expr->as()->local; - DfgScope* signatureScope = makeChildScope(DfgScope::Function); - PushScope ps{scopeStack, signatureScope}; - signatureScope->props[lookup(receiver, f->func->location)][name->index.value] = graph.getDef(f->name); - visitFunction(f->func, NotNull{signatureScope}); - } - else - { - visitExpr(f->func); - } + signatureScope->bindings[global->name] = graph.getDef(f->name); } - else + else if (auto name = f->name->as(); name && name->expr->is()) { - visitExpr(f->func); + auto receiver = name->expr->as()->local; + signatureScope->props[lookup(receiver, f->func->location)][name->index.value] = graph.getDef(f->name); } + visitFunction(f->func, NotNull{signatureScope}); if (auto local = f->name->as()) { @@ -862,6 +880,37 @@ ControlFlow DataFlowGraphBuilder::visit(AstStatDeclareExternType* d) return ControlFlow::None; } +ControlFlow DataFlowGraphBuilder::visit(AstStatClass* d) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + DefId def = defArena->freshCell(d->name, d->name->location); + + graph.localDefs[d->name] = def; + currentScope()->bindings[d->name->name] = def; + captures[d->name->name].allVersions.push_back(def); + + for (const auto& member : d->members) + { + Luau::visit( + overloaded{ + [&](const AstClassProperty& prop) + { + if (prop.ty) + visitType(prop.ty); + }, + [&](const AstClassMethod& method) + { + visitExpr(method.function); + } + }, + member + ); + } + + + return ControlFlow::None; +} + ControlFlow DataFlowGraphBuilder::visit(AstStatError* error) { DfgScope* unreachable = makeChildScope(); @@ -894,6 +943,8 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExpr* e) return {defArena->freshCell(Symbol{}, c->location), nullptr}; // ok else if (auto c = e->as()) return {defArena->freshCell(Symbol{}, c->location), nullptr}; // ok + else if (auto c = e->as()) + return {defArena->freshCell(Symbol{}, c->location), nullptr}; // ok else if (auto c = e->as()) return {defArena->freshCell(Symbol{}, c->location), nullptr}; // ok else if (auto l = e->as()) @@ -923,10 +974,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExpr* e) else if (auto i = e->as()) return visitExpr(i); else if (auto i = e->as()) - { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); return visitExpr(i); - } else if (auto error = e->as()) return visitExpr(error); else @@ -934,9 +982,24 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExpr* e) }; auto [def, key] = go(); - graph.astDefs[e] = def; - if (key) - graph.astRefinementKeys[e] = key; + + if (FFlag::LuauDoNotOverwriteAstDefs) + { + if (!graph.astDefs.contains(e)) + { + graph.astDefs[e] = def; + LUAU_ASSERT(!graph.astRefinementKeys.contains(e)); + if (key) + graph.astRefinementKeys[e] = key; + } + } + else + { + graph.astDefs[e] = def; + if (key) + graph.astRefinementKeys[e] = key; + } + return {def, key}; } @@ -964,6 +1027,17 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprCall* c) { visitExpr(c->func); + for (const AstTypeOrPack& typeOrPack : c->typeArguments) + { + if (typeOrPack.type) + visitType(typeOrPack.type); + else + { + LUAU_ASSERT(typeOrPack.typePack); + visitTypePack(typeOrPack.typePack); + } + } + for (AstExpr* arg : c->args) visitExpr(arg); @@ -990,9 +1064,23 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprCall* c) scopeStack.push_back(child); auto [def, key] = *result; - graph.astDefs[firstArg] = def; - if (key) - graph.astRefinementKeys[firstArg] = key; + + if (FFlag::LuauDoNotOverwriteAstDefs) + { + if (!graph.astDefs.contains(firstArg)) + { + graph.astDefs[firstArg] = def; + LUAU_ASSERT(!graph.astRefinementKeys.contains(firstArg)); + if (key) + graph.astRefinementKeys[firstArg] = key; + } + } + else + { + graph.astDefs[firstArg] = def; + if (key) + graph.astRefinementKeys[firstArg] = key; + } visitLValue(firstArg, def); } @@ -1082,53 +1170,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprFunction* f) DfgScope* signatureScope = makeChildScope(DfgScope::Function); PushScope ps{scopeStack, signatureScope}; - if (FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals) - { - return visitFunction(f, NotNull{signatureScope}); - } - else - { - - if (AstLocal* self = f->self) - { - // There's no syntax for `self` to have an annotation if using `function t:m()` - LUAU_ASSERT(!self->annotation); - - DefId def = defArena->freshCell(f->debugname, f->location); - graph.localDefs[self] = def; - signatureScope->bindings[self] = def; - captures[self].allVersions.push_back(def); - } - - for (AstLocal* param : f->args) - { - if (param->annotation) - visitType(param->annotation); - - DefId def = defArena->freshCell(param, param->location); - graph.localDefs[param] = def; - signatureScope->bindings[param] = def; - captures[param].allVersions.push_back(def); - } - - if (f->varargAnnotation) - visitTypePack(f->varargAnnotation); - - if (f->returnAnnotation) - visitTypePack(f->returnAnnotation); - - // TODO: function body can be re-entrant, as in mutations that occurs at the end of the function can also be - // visible to the beginning of the function, so statically speaking, the body of the function has an exit point - // that points back to itself, e.g. - // - // local function f() print(f) f = 5 end - // local g = f - // g() --> function: address - // g() --> 5 - visit(f->body); - - return {defArena->freshCell(f->debugname, f->location), nullptr}; - } + return visitFunction(f, NotNull{signatureScope}); } DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprTable* t) @@ -1196,19 +1238,16 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprInterpString* i) DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprInstantiate* i) { - if (FFlag::LuauExplicitTypeInstantiationSupport) + for (const AstTypeOrPack& typeOrPack : i->typeArguments) { - for (const AstTypeOrPack& typeOrPack : i->typeArguments) + if (typeOrPack.type) { - if (typeOrPack.type) - { - visitType(typeOrPack.type); - } - else - { - LUAU_ASSERT(typeOrPack.typePack); - visitTypePack(typeOrPack.typePack); - } + visitType(typeOrPack.type); + } + else + { + LUAU_ASSERT(typeOrPack.typePack); + visitTypePack(typeOrPack.typePack); } } @@ -1245,7 +1284,15 @@ void DataFlowGraphBuilder::visitLValue(AstExpr* e, DefId incomingDef) handle->ice("Unknown AstExpr in DataFlowGraphBuilder::visitLValue"); }; - graph.astDefs[e] = go(); + if (FFlag::LuauDoNotOverwriteAstDefs) + { + if (!graph.astDefs.contains(e)) + graph.astDefs[e] = go(); + } + else + { + graph.astDefs[e] = go(); + } } DefId DataFlowGraphBuilder::visitLValue(AstExprLocal* l, DefId incomingDef) diff --git a/Analysis/src/DcrLogger.cpp b/Analysis/src/DcrLogger.cpp index 8138fec8..b65b48c4 100644 --- a/Analysis/src/DcrLogger.cpp +++ b/Analysis/src/DcrLogger.cpp @@ -169,9 +169,10 @@ void write(JsonEmitter& emitter, const BoundarySnapshot& snapshot) o.finish(); } -void write(JsonEmitter& emitter, const StepSnapshot& snapshot) +void write(JsonEmitter& emitter, const ConstraintStepSnapshot& snapshot) { ObjectEmitter o = emitter.writeObject(); + o.writePair("type", "constraint"); o.writePair("currentConstraint", snapshot.currentConstraint); o.writePair("forced", snapshot.forced); o.writePair("unsolvedConstraints", snapshot.unsolvedConstraints); @@ -180,6 +181,29 @@ void write(JsonEmitter& emitter, const StepSnapshot& snapshot) o.finish(); } +void write(JsonEmitter& emitter, const GeneralizeStepSnapshot& eg) +{ + ObjectEmitter o = emitter.writeObject(); + o.writePair("type", "generalize"); + o.writePair("before", eg.before); + o.writePair("after", eg.after); + o.writePair("unsolvedConstraints", eg.unsolvedConstraints); + o.writePair("rootScope", eg.rootScope); + o.writePair("typeStrings", eg.typeStrings); + o.finish(); +} + +void write(JsonEmitter& emitter, const StepSnapshot& snap) +{ + visit( + [&](const auto& s) + { + write(emitter, s); + }, + snap + ); +} + void write(JsonEmitter& emitter, const TypeSolveLog& log) { ObjectEmitter o = emitter.writeObject(); @@ -400,7 +424,7 @@ void DcrLogger::captureInitialSolverState(const Scope* rootScope, const std::vec captureBoundaryState(solveLog.initialState, rootScope, unsolvedConstraints); } -StepSnapshot DcrLogger::prepareStepSnapshot( +ConstraintStepSnapshot DcrLogger::prepareStepSnapshot( const Scope* rootScope, NotNull current, bool force, @@ -413,7 +437,7 @@ StepSnapshot DcrLogger::prepareStepSnapshot( for (NotNull c : unsolvedConstraints) { constraints[c.get()] = { - toString(*c.get(), opts), + toString(*c, opts), c->location, snapshotBlocks(c), }; @@ -422,7 +446,7 @@ StepSnapshot DcrLogger::prepareStepSnapshot( DenseHashMap typeStrings{nullptr}; snapshotTypeStrings(generationLog.exprTypeLocations, generationLog.annotationTypeLocations, typeStrings, opts); - return StepSnapshot{ + return ConstraintStepSnapshot{ current, force, std::move(constraints), @@ -431,8 +455,42 @@ StepSnapshot DcrLogger::prepareStepSnapshot( }; } +GeneralizeStepSnapshot DcrLogger::prepareGeneralizationSnapshot( + std::string before, + const Scope* rootScope, + const std::vector>& unsolvedConstraints +) +{ + ScopeSnapshot scopeSnapshot = snapshotScope(rootScope, opts); + DenseHashMap constraints{nullptr}; + + for (NotNull c : unsolvedConstraints) + { + constraints[c.get()] = { + toString(*c, opts), + c->location, + snapshotBlocks(c), + }; + } + + DenseHashMap typeStrings{nullptr}; + snapshotTypeStrings(generationLog.exprTypeLocations, generationLog.annotationTypeLocations, typeStrings, opts); + + return GeneralizeStepSnapshot{ + std::move(before), + /*after*/ "", // to be filled in + std::move(constraints), + std::move(scopeSnapshot), + std::move(typeStrings), + }; +} + void DcrLogger::commitStepSnapshot(StepSnapshot snapshot) { + // If the type wasn't changed under generalization, skip this. + if (auto eg = get_if(&snapshot); eg && eg->before == eg->after) + return; + solveLog.stepStates.push_back(std::move(snapshot)); } diff --git a/Analysis/src/DumpCFG.cpp b/Analysis/src/DumpCFG.cpp new file mode 100644 index 00000000..714c0cb0 --- /dev/null +++ b/Analysis/src/DumpCFG.cpp @@ -0,0 +1,417 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/DumpCFG.h" +#include "Luau/Common.h" +#include "Luau/ControlFlowGraph.h" + +#include +#include + +LUAU_FASTFLAGVARIABLE(DebugLuauLogCFG) +LUAU_FASTFLAGVARIABLE(DebugLuauDumpCFGJson) +using namespace Luau::CFG; + +namespace Luau +{ + +static std::string getLocalName(AstLocal* local) +{ + if (local->name.value) + return local->name.value; + return "?"; +} + +static std::string dumpDef(Definition* def) +{ + return def->versionedName(); +} + +// Walks an expression tree, printing locals as their resolved definition versions. +struct ExprPrinter : AstVisitor +{ + NotNull cfg; + std::string result; + + explicit ExprPrinter(NotNull cfg) + : cfg(cfg) + { + } + + bool visit(AstExprLocal* node) override + { + if (Definition* def = cfg->getUseDef(node)) + result += dumpDef(def); + else + result += getLocalName(node->local) + "?"; + return false; + } + + bool visit(AstExprConstantNumber* node) override + { + if (node->parseResult == ConstantNumberParseResult::Ok && node->value == static_cast(node->value)) + result += std::to_string(static_cast(node->value)); + else + result += std::to_string(node->value); + return false; + } + + bool visit(AstExprConstantString* node) override + { + result += "\"" + std::string(node->value.data, node->value.size) + "\""; + return false; + } + + bool visit(AstExprConstantBool* node) override + { + result += node->value ? "true" : "false"; + return false; + } + + bool visit(AstExprConstantNil*) override + { + result += "nil"; + return false; + } + + bool visit(AstExprBinary* node) override + { + node->left->visit(this); + result += " " + toString(node->op) + " "; + node->right->visit(this); + return false; + } + + bool visit(AstExprUnary* node) override + { + result += toString(node->op); + node->expr->visit(this); + return false; + } + + bool visit(AstExpr* node) override + { + result += ""; + return false; + } +}; + +static std::string dumpExpr(AstExpr* expr, NotNull cfg) +{ + ExprPrinter printer(cfg); + expr->visit(&printer); + return printer.result; +} + +static AstExpr* findRhsExpr(Symbol sym, AstStatLocal* source) +{ + if (!sym.local) + return nullptr; + for (size_t i = 0; i < source->vars.size; i++) + { + if (source->vars.data[i] == sym.local && i < source->values.size) + return source->values.data[i]; + } + return nullptr; +} + +static AstExpr* findRhsExpr(Symbol sym, AstStatAssign* source) +{ + for (size_t i = 0; i < source->vars.size; i++) + { + if (i >= source->values.size) + continue; + AstExpr* var = source->vars.data[i]; + if (sym.local) + { + if (auto* exprLocal = var->as()) + { + if (exprLocal->local == sym.local) + return source->values.data[i]; + } + } + else if (sym.global.value) + { + if (auto* exprGlobal = var->as()) + { + if (exprGlobal->name == sym.global) + return source->values.data[i]; + } + } + } + return nullptr; +} + +static std::string dumpInstruction(const Instruction* inst, NotNull cfg) +{ + return Luau::visit( + overloaded{ + [&](const Declare& decl) -> std::string + { + std::string result = "local " + dumpDef(decl.def); + if (AstExpr* rhs = findRhsExpr(decl.def->sym, decl.source)) + result += " = " + dumpExpr(rhs, cfg); + return result; + }, + [&](const Assign& assign) -> std::string + { + std::string result = dumpDef(assign.def); + if (AstExpr* rhs = findRhsExpr(assign.def->sym, assign.source)) + result += " = " + dumpExpr(rhs, cfg); + return result; + }, + [](const Join& join) -> std::string + { + std::string result = dumpDef(join.definition) + " = join("; + for (size_t i = 0; i < join.operands.size(); i++) + { + if (i > 0) + result += ", "; + result += dumpDef(join.operands[i]); + } + result += ")"; + return result; + }, + [](const Refine& flow) -> std::string + { + std::string rhs; + if (flow.type) + { + const char* guard = flow.isTypeof ? "typeof" : "type"; + const char* cmp = flow.sense ? "==" : "~="; + rhs = std::string(guard) + "(" + dumpDef(flow.toRefine) + ") " + cmp + " \"" + *flow.type + "\""; + } + else + { + rhs = dumpDef(flow.toRefine) + " " + (flow.sense ? "truthy" : "falsy"); + } + return dumpDef(flow.definition) + " = refine(" + rhs + ")"; + }, + [](const Dead&) -> std::string + { + return ""; + }, + }, + *inst + ); +} + +static std::string dumpBlock(const Block& block, NotNull cfg) +{ + std::string result; + for (const Instruction* inst : block.getInstructions()) + { + if (inst->get_if()) + continue; + result += " " + dumpInstruction(inst, cfg) + "\n"; + } + return result; +} + +static const char* blockKindName(BlockKind kind) +{ + switch (kind) + { + case BlockKind::Entry: + return "entry"; + case BlockKind::Linear: + return "linear"; + case BlockKind::Condition: + return "condition"; + } + LUAU_ASSERT(!"Unknown BlockKind - you may need to add a case to the stringifier here"); + return "?"; +} + +std::string dumpCFG(const ControlFlowGraph& cfg) +{ + std::stringstream result; + for (size_t i = 0; i < cfg.blocks.size(); i++) + { + const Block* block = cfg.blocks[i]; + result << "Block " << i << " (" << blockKindName(block->kind); + if (!block->debugName.empty()) + result << " \"" << block->debugName << "\""; + result << ")"; + + const std::vector& successors = block->getSuccessors(); + if (!successors.empty()) + { + result << " -> ["; + for (size_t j = 0; j < successors.size(); j++) + { + if (j > 0) + result << ", "; + for (size_t k = 0; k < cfg.blocks.size(); k++) + { + if (cfg.blocks[k] == successors[j]) + { + result << "B" << k; + break; + } + } + } + result << "]"; + } + + result << ":\n"; + result << dumpBlock(*block, NotNull{&cfg}); + } + return result.str(); +} + +static std::string jsonEscape(const std::string& s) +{ + std::string out; + out.reserve(s.size() + 2); + for (char c : s) + { + switch (c) + { + case '"': + out += "\\\""; + break; + case '\\': + out += "\\\\"; + break; + case '\b': + out += "\\b"; + break; + case '\f': + out += "\\f"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + if (static_cast(c) < 0x20) + { + char buf[8]; + snprintf(buf, sizeof(buf), "\\u%04x", static_cast(c)); + out += buf; + } + else + { + out += c; + } + } + } + return out; +} + +static size_t indexOfBlock(const ControlFlowGraph& cfg, BlockId block) +{ + for (size_t i = 0; i < cfg.blocks.size(); i++) + { + if (cfg.blocks[i] == block) + return i; + } + return 0; +} + +std::string dumpCFGJson(const ControlFlowGraph& cfg) +{ + // iongraph requires every "loopheader" block to have exactly one predecessor + // marked "backedge" (asserted in Graph.ts). A loop header is any block whose + // predecessor has a higher index than itself (the back-edge comes from within + // the loop body). Pre-compute both flags plus per-block loopDepth in a single + // pass; loopDepth is the number of loops enclosing the block, and iongraph + // uses it to resolve each block to its innermost loop header. + std::vector isLoopHeader(cfg.blocks.size(), false); + std::vector isBackedge(cfg.blocks.size(), false); + std::vector> backedges; // (loop header index, back-edge source index) + for (size_t i = 0; i < cfg.blocks.size(); i++) + { + for (const BlockId& pred : cfg.blocks[i]->getPredecessors()) + { + size_t predIdx = indexOfBlock(cfg, pred); + if (predIdx > i) + { + isLoopHeader[i] = true; + isBackedge[predIdx] = true; + backedges.emplace_back(i, predIdx); + } + } + } + std::vector loopDepth(cfg.blocks.size(), 0); + for (size_t i = 0; i < cfg.blocks.size(); i++) + { + for (const auto& be : backedges) + { + if (be.first <= i && i <= be.second) + loopDepth[i]++; + } + } + + std::string out = "{\"functions\":[{\"name\":\"cfg\",\"passes\":[{\"name\":\"CFG\",\"mir\":{\"blocks\":["; + + int nextInstrId = 1; + for (size_t i = 0; i < cfg.blocks.size(); i++) + { + const Block* block = cfg.blocks[i]; + if (i > 0) + out += ','; + + out += "{\"number\":" + std::to_string(i); + out += ",\"loopDepth\":" + std::to_string(loopDepth[i]); + + out += ",\"attributes\":["; + bool firstAttr = true; + if (isLoopHeader[i]) + { + out += "\"loopheader\""; + firstAttr = false; + } + if (isBackedge[i]) + { + if (!firstAttr) + out += ','; + out += "\"backedge\""; + } + out += "]"; + + out += ",\"predecessors\":["; + const std::vector& preds = block->getPredecessors(); + for (size_t j = 0; j < preds.size(); j++) + { + if (j > 0) + out += ','; + out += std::to_string(indexOfBlock(cfg, preds[j])); + } + out += "]"; + + out += ",\"successors\":["; + const std::vector& succs = block->getSuccessors(); + for (size_t j = 0; j < succs.size(); j++) + { + if (j > 0) + out += ','; + out += std::to_string(indexOfBlock(cfg, succs[j])); + } + out += "]"; + + out += ",\"instructions\":["; + const std::vector& instructions = block->getInstructions(); + for (size_t j = 0; j < instructions.size(); j++) + { + if (j > 0) + out += ','; + out += "{\"id\":" + std::to_string(nextInstrId++); + out += ",\"opcode\":\"" + jsonEscape(dumpInstruction(instructions[j], NotNull{&cfg})) + "\""; + out += ",\"attributes\":[],\"inputs\":[],\"uses\":[],\"memInputs\":[],\"type\":\"\"}"; + } + out += "]}"; + } + + // Close blocks array and mir object, then add an empty lir (iongraph touches p.lir.blocks unconditionally). + out += "]},\"lir\":{\"blocks\":[]}"; + // Close: pass object, passes array, function object, functions array, root object. + out += "}]}]}"; + return out; +} + +} // namespace Luau diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index 3541ddd2..052bf4e4 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -1,8 +1,11 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/BuiltinDefinitions.h" -LUAU_FASTFLAGVARIABLE(LuauTypeCheckerUdtfRenameClassToExtern) -LUAU_FASTFLAGVARIABLE(LuauMorePermissiveNewtableType) +LUAU_FASTFLAG(LuauIntegerLibrary) +LUAU_FASTFLAG(LuauIntegerType2) +LUAU_FASTFLAG(LuauAllowGlobalDeclarationToBeCalledClass) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAG(LuauUdtfTypeIsSubtypeOf) namespace Luau { @@ -119,6 +122,11 @@ declare math: { pi: number, huge: number, + nan: number, + e: number, + phi: number, + sqrt2: number, + tau: number, randomseed: @checked (seed: number) -> (), random: @checked (number?, number?) -> number, @@ -262,6 +270,41 @@ declare buffer: { writestring: @checked (b: buffer, offset: number, value: string, count: number?) -> (), readbits: @checked (b: buffer, bitOffset: number, bitCount: number) -> number, writebits: @checked (b: buffer, bitOffset: number, bitCount: number, value: number) -> (), + readinteger: @checked (b: buffer, offset: number) -> integer, + writeinteger: @checked (b: buffer, offset: number, value: integer) -> (), +} + +)BUILTIN_SRC"; + +static constexpr const char* kBuiltinDefinitionBufferSrc_NOINTEGER = R"BUILTIN_SRC( +--- Buffer API +declare buffer: { + create: @checked (size: number) -> buffer, + fromstring: @checked (str: string) -> buffer, + tostring: @checked (b: buffer) -> string, + len: @checked (b: buffer) -> number, + copy: @checked (target: buffer, targetOffset: number, source: buffer, sourceOffset: number?, count: number?) -> (), + fill: @checked (b: buffer, offset: number, value: number, count: number?) -> (), + readi8: @checked (b: buffer, offset: number) -> number, + readu8: @checked (b: buffer, offset: number) -> number, + readi16: @checked (b: buffer, offset: number) -> number, + readu16: @checked (b: buffer, offset: number) -> number, + readi32: @checked (b: buffer, offset: number) -> number, + readu32: @checked (b: buffer, offset: number) -> number, + readf32: @checked (b: buffer, offset: number) -> number, + readf64: @checked (b: buffer, offset: number) -> number, + writei8: @checked (b: buffer, offset: number, value: number) -> (), + writeu8: @checked (b: buffer, offset: number, value: number) -> (), + writei16: @checked (b: buffer, offset: number, value: number) -> (), + writeu16: @checked (b: buffer, offset: number, value: number) -> (), + writei32: @checked (b: buffer, offset: number, value: number) -> (), + writeu32: @checked (b: buffer, offset: number, value: number) -> (), + writef32: @checked (b: buffer, offset: number, value: number) -> (), + writef64: @checked (b: buffer, offset: number, value: number) -> (), + readstring: @checked (b: buffer, offset: number, count: number) -> string, + writestring: @checked (b: buffer, offset: number, value: string, count: number?) -> (), + readbits: @checked (b: buffer, bitOffset: number, bitCount: number) -> number, + writebits: @checked (b: buffer, bitOffset: number, bitCount: number, value: number) -> () } )BUILTIN_SRC"; @@ -270,9 +313,9 @@ static const char* const kBuiltinDefinitionVectorSrc = R"BUILTIN_SRC( -- While vector would have been better represented as a built-in primitive type, type solver extern type handling covers most of the properties declare extern type vector with - x: number - y: number - z: number + read x: number + read y: number + read z: number end declare vector: { @@ -297,6 +340,61 @@ declare vector: { )BUILTIN_SRC"; +static const char* const kBuiltinDefinitionIntegerSrc = R"BUILTIN_SRC( + +declare integer: { + create: @checked (x: number) -> integer, + tonumber: @checked (x: integer) -> number, + neg: @checked (value: integer) -> integer, + add: @checked (x: integer, y: integer) -> integer, + sub: @checked (x: integer, y: integer) -> integer, + mul: @checked (x: integer, y: integer) -> integer, + div: @checked (x: integer, y: integer) -> integer, + rem: @checked (x: integer, y: integer) -> integer, + idiv: @checked (x: integer, y: integer) -> integer, + mod: @checked (x: integer, y: integer) -> integer, + udiv: @checked (x: integer, y: integer) -> integer, + urem: @checked (x: integer, y: integer) -> integer, + min: @checked (integer, ...integer) -> integer, + max: @checked (integer, ...integer) -> integer, + band: @checked (...integer) -> integer, + bor: @checked (...integer) -> integer, + bnot: @checked (x: integer) -> integer, + bxor: @checked (...integer) -> integer, + lt: @checked (x: integer, y: integer) -> boolean, + le: @checked (x: integer, y: integer) -> boolean, + ult: @checked (x: integer, y: integer) -> boolean, + ule: @checked (x: integer, y: integer) -> boolean, + gt: @checked (x: integer, y: integer) -> boolean, + ge: @checked (x: integer, y: integer) -> boolean, + ugt: @checked (x: integer, y: integer) -> boolean, + uge: @checked (x: integer, y: integer) -> boolean, + lshift: @checked (x: integer, numBitPositions: integer) -> integer, + rshift: @checked (x: integer, numBitPositions: integer) -> integer, + arshift: @checked (x: integer, numBitPositions: integer) -> integer, + lrotate: @checked (x: integer, numBitPositions: integer) -> integer, + rrotate: @checked (x: integer, numBitPositions: integer) -> integer, + extract: @checked (value: integer, bitPosition: integer, numBits: integer?) -> integer, + replace: @checked (value: integer, replacement: integer, bitPosition: integer, numBits: integer?) -> integer, + clamp: @checked (value: integer, min: integer, max: integer) -> integer, + btest: @checked (...integer) -> boolean, + countrz: @checked (x: integer) -> integer, + countlz: @checked (x: integer) -> integer, + bswap: @checked (x: integer) -> integer, + fromstring: @checked (str: string, base: number?) -> integer, + minsigned: integer, + maxsigned: integer +} + +)BUILTIN_SRC"; + +static const char* kBuiltinDefinitionClassSrc = R"CLASS_SRC( +declare class: { + isinstance: @checked (o: unknown, c: class) -> boolean, + classof: @checked (o: unknown) -> class? +} +)CLASS_SRC"; + std::string getBuiltinDefinitionSource() { std::string result = kBuiltinDefinitionBaseSrc; @@ -308,20 +406,142 @@ std::string getBuiltinDefinitionSource() result += kBuiltinDefinitionTableSrc; result += kBuiltinDefinitionDebugSrc; result += kBuiltinDefinitionUtf8Src; - result += kBuiltinDefinitionBufferSrc; + if (FFlag::LuauIntegerType2 && FFlag::LuauIntegerLibrary) + result += kBuiltinDefinitionBufferSrc; + else + result += kBuiltinDefinitionBufferSrc_NOINTEGER; + result += kBuiltinDefinitionVectorSrc; + if (FFlag::LuauIntegerType2 && FFlag::LuauIntegerLibrary) + { + result += kBuiltinDefinitionIntegerSrc; + } + + if (FFlag::DebugLuauUserDefinedClasses && FFlag::LuauAllowGlobalDeclarationToBeCalledClass) + { + result += kBuiltinDefinitionClassSrc; + } + return result; } // TODO: split into separate tagged unions when the new solver can appropriately handle that. static constexpr const char* kBuiltinDefinitionTypeMethodSrc = R"BUILTIN_SRC( +export type type = { + tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "integer" | "string" | "buffer" | "thread" | + "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "extern" | "generic", + + is: (self: type, arg: string) -> boolean, + issubtypeof: (self: type, arg: type) -> boolean, + + -- for singleton type + value: (self: type) -> (string | boolean | nil), + + -- for negation type + inner: (self: type) -> type, + + -- for union and intersection types + components: (self: type) -> {type}, + + -- for table type + setproperty: (self: type, key: type, value: type?) -> (), + setreadproperty: (self: type, key: type, value: type?) -> (), + setwriteproperty: (self: type, key: type, value: type?) -> (), + readproperty: (self: type, key: type) -> type?, + writeproperty: (self: type, key: type) -> type?, + properties: (self: type) -> { [type]: { read: type?, write: type? } }, + setindexer: (self: type, index: type, result: type) -> (), + setreadindexer: (self: type, index: type, result: type) -> (), + setwriteindexer: (self: type, index: type, result: type) -> (), + indexer: (self: type) -> { index: type, readresult: type, writeresult: type }?, + readindexer: (self: type) -> { index: type, result: type }?, + writeindexer: (self: type) -> { index: type, result: type }?, + setmetatable: (self: type, arg: type) -> (), + metatable: (self: type) -> type?, + + -- for function type + setparameters: (self: type, head: {type}?, tail: type?) -> (), + parameters: (self: type) -> { head: {type}?, tail: type? }, + setreturns: (self: type, head: {type}?, tail: type? ) -> (), + returns: (self: type) -> { head: {type}?, tail: type? }, + setgenerics: (self: type, {type}?) -> (), + generics: (self: type) -> {type}, + + -- for class type + -- 'properties', 'metatable', 'indexer', 'readindexer' and 'writeindexer' are shared with table type + readparent: (self: type) -> type?, + writeparent: (self: type) -> type?, + + -- for generic type + name: (self: type) -> string?, + ispack: (self: type) -> boolean, +} + +)BUILTIN_SRC"; + +static constexpr const char* kBuiltinDefinitionTypeMethodSrc_NOISSUBTYPEOF = R"BUILTIN_SRC( + +export type type = { + tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "integer" | "string" | "buffer" | "thread" | + "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "extern" | "generic", + + is: (self: type, arg: string) -> boolean, + + -- for singleton type + value: (self: type) -> (string | boolean | nil), + + -- for negation type + inner: (self: type) -> type, + + -- for union and intersection types + components: (self: type) -> {type}, + + -- for table type + setproperty: (self: type, key: type, value: type?) -> (), + setreadproperty: (self: type, key: type, value: type?) -> (), + setwriteproperty: (self: type, key: type, value: type?) -> (), + readproperty: (self: type, key: type) -> type?, + writeproperty: (self: type, key: type) -> type?, + properties: (self: type) -> { [type]: { read: type?, write: type? } }, + setindexer: (self: type, index: type, result: type) -> (), + setreadindexer: (self: type, index: type, result: type) -> (), + setwriteindexer: (self: type, index: type, result: type) -> (), + indexer: (self: type) -> { index: type, readresult: type, writeresult: type }?, + readindexer: (self: type) -> { index: type, result: type }?, + writeindexer: (self: type) -> { index: type, result: type }?, + setmetatable: (self: type, arg: type) -> (), + metatable: (self: type) -> type?, + + -- for function type + setparameters: (self: type, head: {type}?, tail: type?) -> (), + parameters: (self: type) -> { head: {type}?, tail: type? }, + setreturns: (self: type, head: {type}?, tail: type? ) -> (), + returns: (self: type) -> { head: {type}?, tail: type? }, + setgenerics: (self: type, {type}?) -> (), + generics: (self: type) -> {type}, + + -- for class type + -- 'properties', 'metatable', 'indexer', 'readindexer' and 'writeindexer' are shared with table type + readparent: (self: type) -> type?, + writeparent: (self: type) -> type?, + + -- for generic type + name: (self: type) -> string?, + ispack: (self: type) -> boolean, +} + +)BUILTIN_SRC"; + +static constexpr const char* kBuiltinDefinitionTypeMethodSrc_NOINTEGER = R"BUILTIN_SRC( + export type type = { tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "buffer" | "thread" | "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "extern" | "generic", is: (self: type, arg: string) -> boolean, + issubtypeof: (self: type, arg: type) -> boolean, -- for singleton type value: (self: type) -> (string | boolean | nil), @@ -372,7 +592,7 @@ static constexpr const char* kBuiltinDefinitionTypeMethodSrc_DEPRECATED = R"BUIL export type type = { tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "buffer" | "thread" | - "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "class" | "generic", + "singleton" | "negation" | "union" | "intersection" | "table" | "function" | "extern" | "generic", is: (self: type, arg: string) -> boolean, @@ -432,6 +652,7 @@ declare types: { string: type, thread: type, buffer: type, + integer: type, singleton: @checked (arg: string | boolean | nil) -> type, optional: @checked (arg: type) -> type, @@ -445,7 +666,7 @@ declare types: { } )BUILTIN_SRC"; -static constexpr const char* kBuiltinDefinitionTypesLibSrc_DEPRECATED = R"BUILTIN_SRC( +static constexpr const char* kBuiltinDefinitionTypesLibSrc_NOINTEGER = R"BUILTIN_SRC( declare types: { unknown: type, @@ -463,26 +684,29 @@ declare types: { negationof: @checked (arg: type) -> type, unionof: @checked (...type) -> type, intersectionof: @checked (...type) -> type, - newtable: @checked (props: {[type]: type} | {[type]: { read: type, write: type } } | nil, indexer: { index: type, readresult: type, writeresult: type }?, metatable: type?) -> type, + newtable: @checked (props: {[type]: type} | {[type]: { read: type?, write: type? } }?, indexer: { index: type, readresult: type, writeresult: type }?, metatable: type?) -> type, newfunction: @checked (parameters: { head: {type}?, tail: type? }?, returns: { head: {type}?, tail: type? }?, generics: {type}?) -> type, copy: @checked (arg: type) -> type, } )BUILTIN_SRC"; - std::string getTypeFunctionDefinitionSource() { std::string result; - if (FFlag::LuauTypeCheckerUdtfRenameClassToExtern) + if (FFlag::LuauUdtfTypeIsSubtypeOf && FFlag::LuauIntegerType2) result += kBuiltinDefinitionTypeMethodSrc; + else if (FFlag::LuauUdtfTypeIsSubtypeOf) + result += kBuiltinDefinitionTypeMethodSrc_NOINTEGER; + else if (FFlag::LuauIntegerType2) + result += kBuiltinDefinitionTypeMethodSrc_NOISSUBTYPEOF; else result += kBuiltinDefinitionTypeMethodSrc_DEPRECATED; - if (FFlag::LuauMorePermissiveNewtableType) + if (FFlag::LuauIntegerType2) result += kBuiltinDefinitionTypesLibSrc; else - result += kBuiltinDefinitionTypesLibSrc_DEPRECATED; + result += kBuiltinDefinitionTypesLibSrc_NOINTEGER; return result; } diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index e0f76ef1..f24d6f7b 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -17,9 +17,8 @@ #include LUAU_FASTINTVARIABLE(LuauIndentTypeMismatchMaxTypeLength, 10) - -LUAU_FASTFLAGVARIABLE(LuauBetterTypeMismatchErrors) -LUAU_FASTFLAG(LuauTypeCheckerUdtfRenameClassToExtern) +LUAU_FASTINTVARIABLE(LuauCyclicSccWarningDisplayLimit, 10) +LUAU_FASTINT(LuauCyclicSccWarningThreshold) static std::string wrongNumberOfArgsString( size_t expectedCount, @@ -116,32 +115,23 @@ struct ErrorConverter std::string given = givenModule ? quote(givenType) + " from " + quote(*givenModule) : quote(givenType); std::string wanted = wantedModule ? quote(wantedType) + " from " + quote(*wantedModule) : quote(wantedType); size_t luauIndentTypeMismatchMaxTypeLength = size_t(FInt::LuauIndentTypeMismatchMaxTypeLength); - if (FFlag::LuauBetterTypeMismatchErrors) + if (get(follow(tm.wantedType))) { - if (get(follow(tm.wantedType))) - { - if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength) - return "Expected this to be unreachable, but got " + given; - return "Expected this to be unreachable, but got\n\t" + given; - } - - if (tm.context == TypeMismatch::InvariantContext) - { - if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength) - return "Expected this to be exactly " + wanted + ", but got " + given; - return "Expected this to be exactly\n\t" + wanted + "\nbut got\n\t" + given; - } - - if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength) - return "Expected this to be " + wanted + ", but got " + given; - return "Expected this to be\n\t" + wanted + "\nbut got\n\t" + given; + if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength) + return "Expected this to be unreachable, but got " + given; + return "Expected this to be unreachable, but got\n\t" + given; } - else + + if (tm.context == TypeMismatch::InvariantContext) { if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength) - return "Type " + given + " could not be converted into " + wanted; - return "Type\n\t" + given + "\ncould not be converted into\n\t" + wanted; + return "Expected this to be exactly " + wanted + ", but got " + given; + return "Expected this to be exactly\n\t" + wanted + "\nbut got\n\t" + given; } + + if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength) + return "Expected this to be " + wanted + ", but got " + given; + return "Expected this to be\n\t" + wanted + "\nbut got\n\t" + given; }; if (givenTypeName == wantedTypeName) @@ -181,10 +171,6 @@ struct ErrorConverter { result += "; " + tm.reason; } - else if (!FFlag::LuauBetterTypeMismatchErrors && tm.context == TypeMismatch::InvariantContext) - { - result += " in an invariant context"; - } return result; } @@ -209,12 +195,7 @@ struct ErrorConverter if (get(t)) return "Key '" + e.key + "' not found in table '" + Luau::toString(t) + "'"; else if (get(t)) - { - if (FFlag::LuauTypeCheckerUdtfRenameClassToExtern) - return "Key '" + e.key + "' not found in external type '" + Luau::toString(t) + "'"; - else - return "Key '" + e.key + "' not found in class '" + Luau::toString(t) + "'"; - } + return "Key '" + e.key + "' not found in external type '" + Luau::toString(t) + "'"; else return "Type '" + Luau::toString(e.table) + "' does not have key '" + e.key + "'"; } @@ -386,12 +367,7 @@ struct ErrorConverter TypeId t = follow(e.table); if (get(t)) - { - if (FFlag::LuauTypeCheckerUdtfRenameClassToExtern) - s += "external type"; - else - s += "class"; - } + s += "external type"; else s += "table"; @@ -509,6 +485,29 @@ struct ErrorConverter return s; } + std::string operator()(const Luau::CyclicModuleGraphTooLarge& e) const + { + std::string s = "This module is part of a cycle of " + std::to_string(e.moduleCount) + " modules that require each other. Consider reducing the number of cyclic dependencies: "; + + size_t cyclicModuleDisplayLimit = std::min(e.moduleCount, static_cast(FInt::LuauCyclicSccWarningDisplayLimit)); + + for (size_t i = 0; i < cyclicModuleDisplayLimit; i++) + { + if (i > 0) + s += ", "; + + if (fileResolver != nullptr) + s += fileResolver->getHumanReadableModuleName(e.members[i]); + else + s += e.members[i]; + } + + if (cyclicModuleDisplayLimit < e.members.size()) + s += ", ..."; + + return s; + } + std::string operator()(const Luau::FunctionExitsWithoutReturning& e) const { return "Not all codepaths in this function return '" + toString(e.expectedReturnType) + "'."; @@ -627,9 +626,7 @@ struct ErrorConverter std::string operator()(const TypePackMismatch& e) const { - std::string ss = FFlag::LuauBetterTypeMismatchErrors - ? "Expected this to be '" + toString(e.wantedTp) + "', but got '" + toString(e.givenTp) + "'" - : "Type pack '" + toString(e.givenTp) + "' could not be converted into '" + toString(e.wantedTp) + "'"; + std::string ss = "Expected this to be '" + toString(e.wantedTp) + "', but got '" + toString(e.givenTp) + "'"; if (!e.reason.empty()) ss += "; " + e.reason; @@ -803,12 +800,14 @@ struct ErrorConverter std::string operator()(const PropertyAccessViolation& e) const { const std::string stringKey = isIdentifier(e.key) ? e.key : "\"" + e.key + "\""; + const std::string kind = getTableType(e.table) ? "table" : "type"; + switch (e.context) { case PropertyAccessViolation::CannotRead: - return "Property " + stringKey + " of table '" + toString(e.table) + "' is write-only"; + return "Property " + stringKey + " of " + kind + " '" + toString(e.table) + "' is write-only"; case PropertyAccessViolation::CannotWrite: - return "Property " + stringKey + " of table '" + toString(e.table) + "' is read-only"; + return "Property " + stringKey + " of " + kind + " '" + toString(e.table) + "' is read-only"; } LUAU_UNREACHABLE(); @@ -837,6 +836,11 @@ struct ErrorConverter return e.message; } + std::string operator()(const BuiltInTypeFunctionError& e) const + { + return toString(e.error); + } + std::string operator()(const ReservedIdentifier& e) const { return e.name + " cannot be used as an identifier for a type function or alias"; @@ -1261,6 +1265,11 @@ bool ModuleHasCyclicDependency::operator==(const ModuleHasCyclicDependency& rhs) return cycle.size() == rhs.cycle.size() && std::equal(cycle.begin(), cycle.end(), rhs.cycle.begin()); } +bool CyclicModuleGraphTooLarge::operator==(const CyclicModuleGraphTooLarge& rhs) const +{ + return moduleCount == rhs.moduleCount; +} + bool IllegalRequire::operator==(const IllegalRequire& rhs) const { return moduleName == rhs.moduleName && reason == rhs.reason; @@ -1378,6 +1387,11 @@ bool UserDefinedTypeFunctionError::operator==(const UserDefinedTypeFunctionError return message == rhs.message; } +bool BuiltInTypeFunctionError::operator==(const BuiltInTypeFunctionError& rhs) const +{ + return error == rhs.error; +} + bool ReservedIdentifier::operator==(const ReservedIdentifier& rhs) const { return name == rhs.name; @@ -1567,6 +1581,9 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState) else if constexpr (std::is_same_v) { } + else if constexpr (std::is_same_v) + { + } else if constexpr (std::is_same_v) { } @@ -1649,6 +1666,9 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState) else if constexpr (std::is_same_v) { } + else if constexpr (std::is_same_v) + { + } else if constexpr (std::is_same_v) { e.rhsType = clone(e.rhsType); diff --git a/Analysis/src/ExpectedTypeVisitor.cpp b/Analysis/src/ExpectedTypeVisitor.cpp index 91a89eb3..409eeb1c 100644 --- a/Analysis/src/ExpectedTypeVisitor.cpp +++ b/Analysis/src/ExpectedTypeVisitor.cpp @@ -8,6 +8,8 @@ #include "Luau/TypeUtils.h" #include "Luau/VisitType.h" +LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceSimplifyTables) + namespace Luau { @@ -15,6 +17,7 @@ ExpectedTypeVisitor::ExpectedTypeVisitor( NotNull> astTypes, NotNull> astExpectedTypes, NotNull> astResolvedTypes, + NotNull> astOverloadResolvedTypes, NotNull arena, NotNull builtinTypes, NotNull rootScope @@ -22,6 +25,7 @@ ExpectedTypeVisitor::ExpectedTypeVisitor( : astTypes(astTypes) , astExpectedTypes(astExpectedTypes) , astResolvedTypes(astResolvedTypes) + , astOverloadResolvedTypes(astOverloadResolvedTypes) , arena(arena) , builtinTypes(builtinTypes) , rootScope(rootScope) @@ -167,7 +171,9 @@ bool ExpectedTypeVisitor::visit(AstExprIndexExpr* expr) bool ExpectedTypeVisitor::visit(AstExprCall* expr) { - auto ty = astTypes->find(expr->func); + TypeId* ty = astOverloadResolvedTypes->find(expr); + if (!ty) + ty = astTypes->find(expr->func); if (!ty) return true; @@ -224,11 +230,21 @@ void ExpectedTypeVisitor::applyExpectedType(TypeId expectedType, const AstExpr* { if (auto exprType = astTypes->find(expr)) { - std::vector parts{begin(utv), end(utv)}; - if (auto tt = extractMatchingTableType(parts, *exprType, builtinTypes)) + if (FFlag::LuauBidirectionalInferenceSimplifyTables) + { + if (auto tt = extractMatchingTableType(utv, *exprType, builtinTypes, arena)) + { + applyExpectedType(*tt, expr); + return; + } + } + else { - applyExpectedType(*tt, expr); - return; + if (auto tt = extractMatchingTableType_DEPRECATED(utv, *exprType, builtinTypes)) + { + applyExpectedType(*tt, expr); + return; + } } } } @@ -283,11 +299,11 @@ void ExpectedTypeVisitor::applyExpectedType(TypeId expectedType, const AstExpr* applyExpectedType(expectedTableType->indexer->indexResultType, item.value); } } - else if (item.kind == AstExprTable::Item::List && expectedTableType->indexer) + else if (item.kind == AstExprTable::Item::Kind::List && expectedTableType->indexer) { applyExpectedType(expectedTableType->indexer->indexResultType, item.value); } - else if (item.kind == AstExprTable::Item::General && expectedTableType->indexer) + else if (item.kind == AstExprTable::Item::Kind::General && expectedTableType->indexer) { applyExpectedType(expectedTableType->indexer->indexResultType, item.value); applyExpectedType(expectedKeyType, item.key); diff --git a/Analysis/src/FragmentAutocomplete.cpp b/Analysis/src/FragmentAutocomplete.cpp index 2c191e4a..b4193500 100644 --- a/Analysis/src/FragmentAutocomplete.cpp +++ b/Analysis/src/FragmentAutocomplete.cpp @@ -30,7 +30,9 @@ LUAU_FASTINT(LuauTypeInferIterationLimit); LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAGVARIABLE(DebugLogFragmentsFromAutocomplete) -LUAU_FASTFLAGVARIABLE(LuauFragmentRequiresCanBeResolvedToAModule) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAG(DebugLuauCyclicRequireTypeInference) +LUAU_FASTFLAGVARIABLE(LuauFragmentACEnableTypeFunctionEvaluation) namespace Luau { @@ -457,6 +459,44 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* st } } } + else if (auto classDecl = stat->as()) + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + // We need to include the class name as part of the + // locals so that within the fragment the class name + // is defined. + localStack.push_back(classDecl->name); + localMap[classDecl->name->name] = classDecl->name; + if (classDecl->location.containsClosed(cursorPos)) + { + AstExprFunction* currentMethod = nullptr; + for (const auto& decl : classDecl->members) + { + // CLI-199277: This looks a little weird, like we might end up + // autocompleting class method arguments in a position like: + // + // class Foobar + // function bazbing(alpha, beta, gamma) + // end + // | -- accidentally include args of bazbing here. + // end + // + if (auto method = decl.get_if()) + { + if (method->function->body->location.begin < cursorPos) + currentMethod = method->function; + } + } + if (currentMethod) + { + for (AstLocal* v : currentMethod->args) + { + localStack.push_back(v); + localMap[v->name] = v; + } + } + } + } } } } @@ -1089,16 +1129,16 @@ FragmentTypeCheckResult typecheckFragment_( ) { LUAU_TIMETRACE_SCOPE("Luau::typecheckFragment_", "FragmentAutocomplete"); - freeze(stale->internalTypes); + freeze(*stale->internalTypes); freeze(stale->interfaceTypes); - ModulePtr incrementalModule = std::make_shared(); + ModulePtr incrementalModule = std::make_shared(std::make_shared()); incrementalModule->name = stale->name; incrementalModule->humanReadableName = "Incremental$" + stale->humanReadableName; - incrementalModule->internalTypes.owningModule = incrementalModule.get(); + incrementalModule->internalTypes->owningModule = incrementalModule.get(); incrementalModule->interfaceTypes.owningModule = incrementalModule.get(); incrementalModule->allocator = std::move(astAllocator); incrementalModule->checkedInNewSolver = true; - unfreeze(incrementalModule->internalTypes); + unfreeze(*incrementalModule->internalTypes); unfreeze(incrementalModule->interfaceTypes); /// Setup typecheck limits @@ -1117,12 +1157,16 @@ FragmentTypeCheckResult typecheckFragment_( unifierState.counters.iterationLimit = limits.unifierIterationLimit.value_or(FInt::LuauTypeInferIterationLimit); /// Initialize the normalizer - Normalizer normalizer{&incrementalModule->internalTypes, frontend.builtinTypes, NotNull{&unifierState}, SolverMode::New}; + Normalizer normalizer{incrementalModule->internalTypes.get(), frontend.builtinTypes, NotNull{&unifierState}, SolverMode::New}; /// User defined type functions runtime TypeFunctionRuntime typeFunctionRuntime(iceHandler, NotNull{&limits}); - typeFunctionRuntime.allowEvaluation = false; + Subtyping subtyping{ + frontend.builtinTypes, NotNull{incrementalModule->internalTypes.get()}, NotNull{&normalizer}, NotNull{&typeFunctionRuntime}, iceHandler + }; + + typeFunctionRuntime.allowEvaluation = FFlag::LuauFragmentACEnableTypeFunctionEvaluation; /// Create a DataFlowGraph just for the surrounding context DataFlowGraph dfg = DataFlowGraphBuilder::build(root, NotNull{&incrementalModule->defArena}, NotNull{&incrementalModule->keyArena}, iceHandler); @@ -1135,162 +1179,14 @@ FragmentTypeCheckResult typecheckFragment_( frontend.requireTrace.erase(name); }}; - if (FFlag::LuauFragmentRequiresCanBeResolvedToAModule) - frontend.requireTrace[incrementalModule->name] = traceRequires(frontend.fileResolver, root, incrementalModule->name, limits); + frontend.requireTrace[incrementalModule->name] = traceRequires(frontend.fileResolver, root, incrementalModule->name, limits); FrontendModuleResolver& resolver = getModuleResolver(frontend, opts); std::shared_ptr freshChildOfNearestScope = std::make_shared(nullptr); - /// Contraint Generator - ConstraintGenerator cg{ - incrementalModule, - NotNull{&normalizer}, - NotNull{&typeFunctionRuntime}, - NotNull{&resolver}, - frontend.builtinTypes, - iceHandler, - freshChildOfNearestScope, - frontend.globals.globalTypeFunctionScope, - nullptr, - nullptr, - NotNull{&dfg}, - {} - }; - - CloneState cloneState{frontend.builtinTypes}; - incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope); - freshChildOfNearestScope->interiorFreeTypes.emplace(); - freshChildOfNearestScope->interiorFreeTypePacks.emplace(); - cg.rootScope = freshChildOfNearestScope.get(); - - // Create module-local scope for the type function environment - ScopePtr localTypeFunctionScope = std::make_shared(cg.typeFunctionScope); - localTypeFunctionScope->location = root->location; - cg.typeFunctionRuntime->rootScope = localTypeFunctionScope; - - reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeStart); - cloneTypesFromFragment( - cloneState, - closestScope.get(), - stale, - NotNull{&incrementalModule->internalTypes}, - NotNull{&dfg}, - frontend.builtinTypes, - root, - freshChildOfNearestScope.get() - ); - reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeEnd); - - cg.visitFragmentRoot(freshChildOfNearestScope, root); - - for (auto p : cg.scopes) - incrementalModule->scopes.emplace_back(std::move(p)); + std::unique_ptr cgraph = std::make_unique(frontend.builtinTypes); - reportWaypoint(reporter, FragmentAutocompleteWaypoint::ConstraintSolverStart); - - /// Initialize the constraint solver and run it - ConstraintSolver cs{ - NotNull{&normalizer}, - NotNull{&typeFunctionRuntime}, - NotNull(cg.rootScope), - borrowConstraints(cg.constraints), - NotNull{&cg.scopeToFunction}, - incrementalModule, - NotNull{&resolver}, - {}, - nullptr, - NotNull{&dfg}, - std::move(limits) - }; - - try - { - cs.run(); - } - catch (const TimeLimitError&) - { - stale->timeout = true; - } - catch (const UserCancelError&) - { - stale->cancelled = true; - } - - reportWaypoint(reporter, FragmentAutocompleteWaypoint::ConstraintSolverEnd); - - ExpectedTypeVisitor etv{ - NotNull{&incrementalModule->astTypes}, - NotNull{&incrementalModule->astExpectedTypes}, - NotNull{&incrementalModule->astResolvedTypes}, - NotNull{&incrementalModule->internalTypes}, - frontend.builtinTypes, - NotNull{freshChildOfNearestScope.get()} - }; - root->visit(&etv); - - // In frontend we would forbid internal types - // because this is just for autocomplete, we don't actually care - // We also don't even need to typecheck - just synthesize types as best as we can - freeze(incrementalModule->internalTypes); - freeze(incrementalModule->interfaceTypes); - freshChildOfNearestScope->parent = closestScope; - return {std::move(incrementalModule), std::move(freshChildOfNearestScope)}; -} - -FragmentTypeCheckResult typecheckFragment__DEPRECATED( - Frontend& frontend, - AstStatBlock* root, - const ModulePtr& stale, - const ScopePtr& closestScope, - const Position& cursorPos, - std::unique_ptr astAllocator, - const FrontendOptions& opts, - IFragmentAutocompleteReporter* reporter -) -{ - LUAU_TIMETRACE_SCOPE("Luau::typecheckFragment_", "FragmentAutocomplete"); - freeze(stale->internalTypes); - freeze(stale->interfaceTypes); - ModulePtr incrementalModule = std::make_shared(); - incrementalModule->name = stale->name; - incrementalModule->humanReadableName = "Incremental$" + stale->humanReadableName; - incrementalModule->internalTypes.owningModule = incrementalModule.get(); - incrementalModule->interfaceTypes.owningModule = incrementalModule.get(); - incrementalModule->allocator = std::move(astAllocator); - incrementalModule->checkedInNewSolver = true; - unfreeze(incrementalModule->internalTypes); - unfreeze(incrementalModule->interfaceTypes); - - /// Setup typecheck limits - TypeCheckLimits limits; - if (opts.moduleTimeLimitSec) - limits.finishTime = TimeTrace::getClock() + *opts.moduleTimeLimitSec; - else - limits.finishTime = std::nullopt; - limits.cancellationToken = opts.cancellationToken; - - /// Icehandler - NotNull iceHandler{&frontend.iceHandler}; - /// Make the shared state for the unifier (recursion + iteration limits) - UnifierSharedState unifierState{iceHandler}; - unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit; - unifierState.counters.iterationLimit = limits.unifierIterationLimit.value_or(FInt::LuauTypeInferIterationLimit); - - /// Initialize the normalizer - Normalizer normalizer{&incrementalModule->internalTypes, frontend.builtinTypes, NotNull{&unifierState}, SolverMode::New}; - - /// User defined type functions runtime - TypeFunctionRuntime typeFunctionRuntime(iceHandler, NotNull{&limits}); - - typeFunctionRuntime.allowEvaluation = false; - - /// Create a DataFlowGraph just for the surrounding context - DataFlowGraph dfg = DataFlowGraphBuilder::build(root, NotNull{&incrementalModule->defArena}, NotNull{&incrementalModule->keyArena}, iceHandler); - reportWaypoint(reporter, FragmentAutocompleteWaypoint::DfgBuildEnd); - - FrontendModuleResolver& resolver = getModuleResolver(frontend, opts); - std::shared_ptr freshChildOfNearestScope = std::make_shared(nullptr); /// Contraint Generator ConstraintGenerator cg{ incrementalModule, @@ -1304,7 +1200,8 @@ FragmentTypeCheckResult typecheckFragment__DEPRECATED( nullptr, nullptr, NotNull{&dfg}, - {} + {}, + NotNull{cgraph.get()}, }; CloneState cloneState{frontend.builtinTypes}; @@ -1323,7 +1220,7 @@ FragmentTypeCheckResult typecheckFragment__DEPRECATED( cloneState, closestScope.get(), stale, - NotNull{&incrementalModule->internalTypes}, + NotNull{incrementalModule->internalTypes.get()}, NotNull{&dfg}, frontend.builtinTypes, root, @@ -1344,14 +1241,16 @@ FragmentTypeCheckResult typecheckFragment__DEPRECATED( NotNull{&normalizer}, NotNull{&typeFunctionRuntime}, NotNull(cg.rootScope), - borrowConstraints(cg.constraints), - NotNull{&cg.scopeToFunction}, + borrowConstraints(FFlag::DebugLuauCyclicRequireTypeInference ? cg.cgraph->constraints : cg.constraints), + NotNull{FFlag::DebugLuauCyclicRequireTypeInference ? &cg.cgraph->scopeToFunction : &cg.scopeToFunction}, incrementalModule, NotNull{&resolver}, {}, nullptr, NotNull{&dfg}, - std::move(limits) + std::move(limits), + NotNull{cgraph.get()}, + NotNull{&subtyping} }; try @@ -1373,22 +1272,24 @@ FragmentTypeCheckResult typecheckFragment__DEPRECATED( NotNull{&incrementalModule->astTypes}, NotNull{&incrementalModule->astExpectedTypes}, NotNull{&incrementalModule->astResolvedTypes}, - NotNull{&incrementalModule->internalTypes}, + NotNull{&incrementalModule->astOverloadResolvedTypes}, + NotNull{incrementalModule->internalTypes.get()}, frontend.builtinTypes, NotNull{freshChildOfNearestScope.get()} }; root->visit(&etv); + // In frontend we would forbid internal types // because this is just for autocomplete, we don't actually care // We also don't even need to typecheck - just synthesize types as best as we can - freeze(incrementalModule->internalTypes); + LUAU_ASSERT(incrementalModule->internalTypes.use_count() == 1); + freeze(*incrementalModule->internalTypes); freeze(incrementalModule->interfaceTypes); freshChildOfNearestScope->parent = closestScope; return {std::move(incrementalModule), std::move(freshChildOfNearestScope)}; } - std::pair typecheckFragment( Frontend& frontend, const ModuleName& moduleName, @@ -1429,11 +1330,7 @@ std::pair typecheckFragment( FrontendOptions frontendOptions = opts.value_or(frontend.options); const ScopePtr& closestScope = findClosestScope(module, parseResult.scopePos); FragmentTypeCheckResult result = - FFlag::LuauFragmentRequiresCanBeResolvedToAModule - ? typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter) - : typecheckFragment__DEPRECATED( - frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter - ); + typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter); result.ancestry = std::move(parseResult.ancestry); reportFragmentString(reporter, tryParse->fragmentToParse); return {FragmentTypeCheckStatus::Success, result}; @@ -1499,11 +1396,11 @@ FragmentAutocompleteResult fragmentAutocomplete( auto globalScope = (opts && opts->forAutocomplete) ? frontend.globalsForAutocomplete.globalScope.get() : frontend.globals.globalScope.get(); if (FFlag::DebugLogFragmentsFromAutocomplete) logLuau("Fragment Autocomplete Source Script", src); - unfreeze(tcResult.incrementalModule->internalTypes); + unfreeze(*tcResult.incrementalModule->internalTypes); auto result = Luau::autocomplete_( tcResult.incrementalModule, frontend.builtinTypes, - &tcResult.incrementalModule->internalTypes, + tcResult.incrementalModule->internalTypes.get(), tcResult.ancestry, globalScope, tcResult.freshScope, @@ -1512,7 +1409,7 @@ FragmentAutocompleteResult fragmentAutocomplete( std::move(callback), isInHotComment ); - freeze(tcResult.incrementalModule->internalTypes); + freeze(*tcResult.incrementalModule->internalTypes); reportWaypoint(reporter, FragmentAutocompleteWaypoint::AutocompleteEnd); return {std::move(tcResult.incrementalModule), tcResult.freshScope.get(), std::move(result)}; } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 23189b67..b2192d3b 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -6,8 +6,12 @@ #include "Luau/Clone.h" #include "Luau/Config.h" #include "Luau/ConstraintGenerator.h" +#include "Luau/ConstraintSet.h" #include "Luau/ConstraintSolver.h" +#include "Luau/ControlFlowGraph.h" #include "Luau/DataFlowGraph.h" +#include "Luau/DenseHash2.h" +#include "Luau/DumpCFG.h" #include "Luau/DcrLogger.h" #include "Luau/ExpectedTypeVisitor.h" #include "Luau/FileResolver.h" @@ -15,11 +19,16 @@ #include "Luau/NotNull.h" #include "Luau/Parser.h" #include "Luau/Scope.h" +#include "Luau/Subtyping.h" #include "Luau/TimeTrace.h" +#include "Luau/Type.h" #include "Luau/TypeArena.h" #include "Luau/TypeCheckLimits.h" +#include "Luau/TypePack.h" +#include "Luau/TypeUtils.h" #include "Luau/TypeChecker2.h" #include "Luau/TypeInfer.h" +#include "Luau/TypeStateMap.h" #include "Luau/VisitType.h" #include @@ -32,8 +41,10 @@ LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauTarjanChildLimit) -LUAU_FASTFLAG(LuauInferInNoCheckMode) +LUAU_FASTINTVARIABLE(LuauCyclicSccWarningThreshold, 4) + LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3) +LUAU_FASTFLAGVARIABLE(LuauFrontendSourceNodeErase) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJsonFile) @@ -41,11 +52,20 @@ LUAU_FASTFLAGVARIABLE(DebugLuauForbidInternalTypes) LUAU_FASTFLAGVARIABLE(DebugLuauForceStrictMode) LUAU_FASTFLAGVARIABLE(DebugLuauForceNonStrictMode) LUAU_FASTFLAGVARIABLE(DebugLuauAlwaysShowConstraintSolvingIncomplete) +LUAU_FASTFLAG(LuauExportValueSyntax) +LUAU_FASTFLAGVARIABLE(LuauExportValueTypecheck) +LUAU_FLAGVERSION(LuauExportValueTypecheck, 2) + +LUAU_FASTFLAGVARIABLE(DebugLuauForceOldSolver) +LUAU_FASTFLAG(DebugLuauCFG) +LUAU_FASTFLAG(DebugLuauLogCFG) +LUAU_FASTFLAG(DebugLuauDumpCFGJson) +LUAU_FASTFLAGVARIABLE(DebugLuauCyclicRequireTypeInference) namespace Luau { -struct BuildQueueItem +struct BuildQueueModuleInfo { ModuleName name; ModuleName humanReadableName; @@ -56,9 +76,21 @@ struct BuildQueueItem Config config; ScopePtr environmentScope; std::vector requireCycles; + + // Result + ModulePtr module; + Frontend::Stats stats; +}; + +struct BuildQueueItem +{ FrontendOptions options; bool recordJsonLog = false; + // Modules that form a cyclic dependency are grouped into a single strongly connected component (SCC) and checked together. + ModuleSCCPtr scc; + std::vector modules; + // Queue state std::vector reverseDeps; int dirtyDependencies = 0; @@ -66,8 +98,6 @@ struct BuildQueueItem // Result std::exception_ptr exception; - ModulePtr module; - Frontend::Stats stats; }; struct BuildQueueWorkState @@ -427,6 +457,19 @@ static TypeCheckLimits makeTypeCheckLimits(const FrontendOptions& options) return limits; } +Frontend::Frontend(SolverMode mode, FileResolver* fileResolver, ConfigResolver* configResolver, FrontendOptions options) + : useNewLuauSolver(mode) + , builtinTypes(NotNull{&builtinTypes_}) + , fileResolver(fileResolver) + , moduleResolver(this) + , moduleResolverForAutocomplete(this) + , globals(builtinTypes, getLuauSolverMode()) + , globalsForAutocomplete(builtinTypes, getLuauSolverMode()) + , configResolver(configResolver) + , options(std::move(options)) +{ +} + Frontend::Frontend(FileResolver* fileResolver, ConfigResolver* configResolver, const FrontendOptions& options) : useNewLuauSolver(FFlag::LuauSolverV2 ? SolverMode::New : SolverMode::Old) , builtinTypes(NotNull{&builtinTypes_}) @@ -509,6 +552,8 @@ CheckResult Frontend::check(const ModuleName& name, std::optional buildQueue; bool cycleDetected = parseGraph(buildQueue, name, makeTypeCheckLimits(frontendOptions), frontendOptions.forAutocomplete); + if (FFlag::DebugLuauCyclicRequireTypeInference) + computeSCCs(buildQueue); DenseHashSet seen{{}}; std::vector buildQueueItems; @@ -517,7 +562,8 @@ CheckResult Frontend::check(const ModuleName& name, std::optionaltimeout) - checkResult.timeoutHits.push_back(item.name); + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + for (const BuildQueueModuleInfo& moduleInfo : item.modules) + { + if (moduleInfo.module->timeout) + checkResult.timeoutHits.push_back(moduleInfo.name); - // If check was manually cancelled, do not return partial results - if (item.module->cancelled) - return {}; + // If check was manually cancelled, do not return partial results + if (moduleInfo.module->cancelled) + return {}; + + checkResult.errors.insert(checkResult.errors.end(), moduleInfo.module->errors.begin(), moduleInfo.module->errors.end()); + + if (moduleInfo.name == name) + checkResult.lintResult = moduleInfo.module->lintResult; + } + } + else + { + const BuildQueueModuleInfo& moduleInfo = item.modules[0]; + + if (moduleInfo.module->timeout) + checkResult.timeoutHits.push_back(moduleInfo.name); - checkResult.errors.insert(checkResult.errors.end(), item.module->errors.begin(), item.module->errors.end()); + // If check was manually cancelled, do not return partial results + if (moduleInfo.module->cancelled) + return {}; + + checkResult.errors.insert(checkResult.errors.end(), moduleInfo.module->errors.begin(), moduleInfo.module->errors.end()); - if (item.name == name) - checkResult.lintResult = item.module->lintResult; + if (moduleInfo.name == name) + checkResult.lintResult = moduleInfo.module->lintResult; + } } return checkResult; @@ -595,6 +663,8 @@ std::vector Frontend::checkQueuedModules( } ); + if (FFlag::DebugLuauCyclicRequireTypeInference) + computeSCCs(queue); addBuildQueueItems(state->buildQueueItems, queue, cycleDetected, seen, frontendOptions); } @@ -606,8 +676,18 @@ std::vector Frontend::checkQueuedModules( for (size_t i = 0; i < state->buildQueueItems.size(); i++) { - BuildQueueItem& item = state->buildQueueItems[i]; - moduleNameToQueue[item.name] = i; + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + for (const BuildQueueModuleInfo& moduleInfo : state->buildQueueItems[i].modules) + { + moduleNameToQueue[moduleInfo.name] = i; + } + } + else + { + BuildQueueItem& item = state->buildQueueItems[i]; + moduleNameToQueue[item.modules[0].name] = i; + } } // Default task execution is single-threaded and immediate @@ -628,15 +708,38 @@ std::vector Frontend::checkQueuedModules( { BuildQueueItem& item = state->buildQueueItems[i]; - for (const ModuleName& dep : item.sourceNode->requireSet) + if (FFlag::DebugLuauCyclicRequireTypeInference) { - if (auto it = sourceNodes.find(dep); it != sourceNodes.end()) + for (const BuildQueueModuleInfo& moduleInfo : item.modules) { - if (it->second->hasDirtyModule(frontendOptions.forAutocomplete)) + for (const ModuleName& dep : moduleInfo.sourceNode->requireSet) { - item.dirtyDependencies++; - - state->buildQueueItems[moduleNameToQueue[dep]].reverseDeps.push_back(i); + if (auto it = sourceNodes.find(dep); it != sourceNodes.end()) + { + if (it->second->hasDirtyModule(frontendOptions.forAutocomplete)) + { + auto queueIt = moduleNameToQueue.find(dep); + if (queueIt != moduleNameToQueue.end() && queueIt->second != i) + { + item.dirtyDependencies++; + state->buildQueueItems[queueIt->second].reverseDeps.push_back(i); + } + } + } + } + } + } + else + { + for (const ModuleName& dep : item.modules[0].sourceNode->requireSet) + { + if (auto it = sourceNodes.find(dep); it != sourceNodes.end()) + { + if (it->second->hasDirtyModule(frontendOptions.forAutocomplete)) + { + item.dirtyDependencies++; + state->buildQueueItems[moduleNameToQueue[dep]].reverseDeps.push_back(i); + } } } } @@ -687,8 +790,25 @@ std::vector Frontend::checkQueuedModules( if (item.exception) itemWithException = i; - if (item.module && item.module->cancelled) - cancelled = true; + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + if (!itemWithException && !cancelled) + { + for (const BuildQueueModuleInfo& moduleInfo : item.modules) + { + if (moduleInfo.module && moduleInfo.module->cancelled) + { + cancelled = true; + break; + } + } + } + } + else + { + if (item.modules[0].module && item.modules[0].module->cancelled) + cancelled = true; + } if (itemWithException || cancelled) break; @@ -750,7 +870,19 @@ std::vector Frontend::checkQueuedModules( checkedModules.reserve(state->buildQueueItems.size()); for (size_t i = 0; i < state->buildQueueItems.size(); i++) - checkedModules.push_back(std::move(state->buildQueueItems[i].name)); + { + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + for (BuildQueueModuleInfo& moduleInfo : state->buildQueueItems[i].modules) + { + checkedModules.push_back(std::move(moduleInfo.name)); + } + } + else + { + checkedModules.push_back(std::move(state->buildQueueItems[i].modules[0].name)); + } + } return checkedModules; } @@ -929,6 +1061,203 @@ bool Frontend::parseGraph( return cyclic; } +static bool moduleHasExports(const SourceModule& sourceModule) +{ + if (!sourceModule.root) + return false; + + for (AstStat* stat : sourceModule.root->body) + { + if (AstStatLocal* local = stat->as()) + { + if (local->isExported) + return true; + } + else if (AstStatLocalFunction* func = stat->as()) + { + if (func->name->isExported) + return true; + } + } + + return false; +} + +// Iterative Tarjan's SCC algorithm over the require graph. +// Returns a list of SCCs that have more than one member or a self-loop. +static std::vector computeTarjanSCCs( + const std::vector& buildQueue, + const std::unordered_map>& sourceNodes +) +{ + const size_t N = buildQueue.size(); + if (N == 0) + return {}; + + DenseHashMap2 nameToVertex; + for (size_t i = 0; i < N; i++) + nameToVertex[buildQueue[i]] = i; + + // Adjacency list: edges[] stores concatenated lists of outgoing edges for each vertex, and edgeStart[] indexes into it + std::vector edges; + std::vector edgeStart(N + 1, 0); + + for (size_t v = 0; v < N; v++) + { + edgeStart[v] = edges.size(); + auto nodeIt = sourceNodes.find(buildQueue[v]); + if (nodeIt != sourceNodes.end()) + { + for (const ModuleName& dep : nodeIt->second->requireSet) + { + if (size_t* idx = nameToVertex.find(dep)) + edges.push_back(*idx); + } + } + } + edgeStart[N] = edges.size(); + + struct TarjanNode + { + int index = -1; // discovery order (-1 = unvisited) + int lowlink = 0; // lowest index reachable from this vertex's DFS subtree + bool onStack = false; // currently on the SCC candidate stack + }; + + // Simulates a recursive call frame: which vertex we're visiting + // and how far through its edge list we've progressed. + struct TarjanFrame + { + size_t vertex; + size_t edgeCursor; // next edge to explore for this vertex (index into edges[]) + }; + + std::vector nodes(N); + std::vector worklist; + std::vector moduleStack; // vertices that may be part of the current SCC + int nextIndex = 0; + + std::vector result; + + for (size_t start = 0; start < N; start++) + { + if (nodes[start].index != -1) + continue; + + // Begin DFS from an unvisited vertex + worklist.push_back(TarjanFrame{start, edgeStart[start]}); + nodes[start].index = nodes[start].lowlink = nextIndex++; + nodes[start].onStack = true; + moduleStack.push_back(start); + + while (!worklist.empty()) + { + TarjanFrame& frame = worklist.back(); + size_t v = frame.vertex; + + if (frame.edgeCursor < edgeStart[v + 1]) + { + // Explore next outgoing edge + size_t w = edges[frame.edgeCursor++]; + + if (nodes[w].index == -1) + { + // Tree edge: w is unvisited, recurse into it + worklist.push_back(TarjanFrame{w, edgeStart[w]}); + nodes[w].index = nodes[w].lowlink = nextIndex++; + nodes[w].onStack = true; + moduleStack.push_back(w); + } + else if (nodes[w].onStack) + { + // Back edge: w is on the current search path, update lowlink + nodes[v].lowlink = std::min(nodes[v].lowlink, nodes[w].index); + } + } + else + { + // All edges explored. If lowlink == index, v is the root of an SCC. + if (nodes[v].lowlink == nodes[v].index) + { + auto scc = std::make_shared(); + size_t w; + do + { + w = moduleStack.back(); + moduleStack.pop_back(); + nodes[w].onStack = false; + scc->members.push_back(buildQueue[w]); + } while (w != v); + + if (scc->members.size() > 1) + { + result.push_back(scc); + } + else if (scc->members.size() == 1) + { + // Single-member SCC is only cyclic if it has a self-loop + const ModuleName& only = scc->members[0]; + auto nodeIt = sourceNodes.find(only); + if (nodeIt != sourceNodes.end() && nodeIt->second->requireSet.contains(only)) + result.push_back(scc); + } + } + + // "Return" from DFS: propagate lowlink to parent frame + worklist.pop_back(); + if (!worklist.empty()) + nodes[worklist.back().vertex].lowlink = std::min(nodes[worklist.back().vertex].lowlink, nodes[v].lowlink); + } + } + } + + return result; +} + +void Frontend::computeSCCs(const std::vector& buildQueue) +{ + LUAU_ASSERT(FFlag::DebugLuauCyclicRequireTypeInference); + + // Clear stale SCC data + for (const ModuleName& name : buildQueue) + { + auto it = sourceNodes.find(name); + if (it != sourceNodes.end()) + it->second->scc.reset(); + } + sccs.clear(); + + std::vector foundSCCs = computeTarjanSCCs(buildQueue, sourceNodes); + + for (const ModuleSCCPtr& scc : foundSCCs) + { + // Only register SCCs where all members use `export` — those have runtime + // placeholder support for cyclic requires. Mixed SCCs with non-export modules + // will crash at runtime, so they go through the old per-module path. + bool allMembersUseExport = true; + for (const ModuleName& member : scc->members) + { + auto it = sourceModules.find(member); + if (it == sourceModules.end() || !it->second || !moduleHasExports(*it->second)) + { + allMembersUseExport = false; + break; + } + } + + if (!allMembersUseExport) + continue; + + for (const ModuleName& member : scc->members) + { + sccs[member] = scc; + auto nodeIt = sourceNodes.find(member); + LUAU_ASSERT(nodeIt != sourceNodes.end()); + nodeIt->second->scc = scc; + } + } +} + void Frontend::addBuildQueueItems( std::vector& items, std::vector& buildQueue, @@ -937,6 +1266,9 @@ void Frontend::addBuildQueueItems( const FrontendOptions& frontendOptions ) { + // Map SCC pointer to item index for grouping SCC members into a single BuildQueueItem + DenseHashMap2 sccToItemIndex; + for (const ModuleName& moduleName : buildQueue) { if (seen.contains(moduleName)) @@ -952,23 +1284,90 @@ void Frontend::addBuildQueueItems( LUAU_ASSERT(sourceModules.count(moduleName)); std::shared_ptr& sourceModule = sourceModules[moduleName]; - BuildQueueItem data{moduleName, fileResolver->getHumanReadableModuleName(moduleName), sourceNode, sourceModule}; + BuildQueueModuleInfo moduleInfo{ + moduleName, + fileResolver->getHumanReadableModuleName(moduleName), + sourceNode, + sourceModule, + }; - data.config = configResolver->getConfig(moduleName, makeTypeCheckLimits(frontendOptions)); - data.environmentScope = getModuleEnvironment(*sourceModule, data.config, frontendOptions.forAutocomplete); - data.recordJsonLog = FFlag::DebugLuauLogSolverToJson; + moduleInfo.config = configResolver->getConfig(moduleName, makeTypeCheckLimits(frontendOptions)); + moduleInfo.environmentScope = getModuleEnvironment(*sourceModule, moduleInfo.config, frontendOptions.forAutocomplete); // in the future we could replace toposort with an algorithm that can flag cyclic nodes by itself // however, for now getRequireCycles isn't expensive in practice on the cases we care about, and long term // all correct programs must be acyclic so this code triggers rarely if (cycleDetected) - data.requireCycles = getRequireCycles(fileResolver, sourceNodes, sourceNode.get()); - - data.options = frontendOptions; + moduleInfo.requireCycles = getRequireCycles(fileResolver, sourceNodes, sourceNode.get()); // This is used by the type checker to replace the resulting type of cyclic modules with any - sourceModule->cyclic = !data.requireCycles.empty(); + sourceModule->cyclic = !moduleInfo.requireCycles.empty(); + + // Check if this module belongs to an SCC that should be grouped + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + if (ModuleSCCPtr* sccPtr = sccs.find(moduleName)) + { + ModuleSCCPtr scc = *sccPtr; + + if (getLuauSolverMode() == SolverMode::New && !FFlag::DebugLuauForceOldSolver) + { + // Create shared arena on first encounter of this SCC + if (!scc->sharedArena) + { + scc->sharedArena = std::make_shared(); + // Register placeholder modules so require() of SCC peers resolves to a BlockedType during constraint generation (later updated to its actual type during constraint solving) + for (const ModuleName& member : scc->members) + { + TypeId placeholderReturnType = scc->sharedArena->addType(BlockedType{}); + TypePackId placeholderPack = scc->sharedArena->addTypePack({placeholderReturnType}); + + ModulePtr placeholderModule = std::make_shared(scc->sharedArena); + placeholderModule->name = member; + placeholderModule->humanReadableName = fileResolver->getHumanReadableModuleName(member); + placeholderModule->type = SourceCode::Type::Module; + placeholderModule->mode = Mode::Strict; + + placeholderModule->returnType = placeholderPack; + + ScopePtr placeholderScope = std::make_shared(builtinTypes->anyTypePack); + placeholderScope->returnType = placeholderPack; + placeholderModule->scopes.emplace_back(Location{}, placeholderScope); + + moduleResolver.setModule(member, std::move(placeholderModule)); + } + } + + moduleInfo.requireCycles.clear(); + + // Add this module to the SCC's BuildQueueItem. + // All SCC members share one item so they're checked together. + if (size_t* existingIdx = sccToItemIndex.find(scc.get())) + { + items[*existingIdx].modules.emplace_back(std::move(moduleInfo)); + } + else + { + BuildQueueItem data; + data.options = frontendOptions; + data.recordJsonLog = FFlag::DebugLuauLogSolverToJson; + data.scc = scc; + data.modules.emplace_back(std::move(moduleInfo)); + + sccToItemIndex[scc.get()] = items.size(); + items.push_back(std::move(data)); + } + continue; + } + } + } + + // Separate BuildQueueItem for non-cyclic module or when SCC not possible + BuildQueueItem data; + data.options = frontendOptions; + data.recordJsonLog = FFlag::DebugLuauLogSolverToJson; + data.modules.emplace_back(std::move(moduleInfo)); items.push_back(std::move(data)); } } @@ -981,11 +1380,297 @@ static void applyInternalLimitScaling(SourceNode& sourceNode, const ModulePtr mo sourceNode.autocompleteLimitsMult = std::min(sourceNode.autocompleteLimitsMult * 2.0, 1.0); } +void Frontend::checkSCCBuildQueueItem(BuildQueueItem& item) +{ + ModuleSCCPtr scc = item.scc; + LUAU_ASSERT(scc->sharedArena); + + TypeCheckLimits typeCheckLimits = makeTypeCheckLimits(item.options); + + UnifierSharedState unifierState{NotNull{&iceHandler}}; + unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit; + unifierState.counters.iterationLimit = typeCheckLimits.unifierIterationLimit.value_or(FInt::LuauTypeInferIterationLimit); + + Normalizer normalizer{scc->sharedArena.get(), builtinTypes, NotNull{&unifierState}, SolverMode::New}; + TypeFunctionRuntime typeFunctionRuntime{NotNull{&iceHandler}, NotNull{&typeCheckLimits}}; + typeFunctionRuntime.allowEvaluation = true; + + // Per-module ConstraintGenerator data for this SCC that needs to be preserved for later use in the ConstraintSolver + struct SCCModuleCGData + { + std::unique_ptr dfg; + std::vector> cgScopes; + }; + std::vector cgData(item.modules.size()); + + // Single shared dependency graph for all modules in this SCC + std::unique_ptr cgraph = std::make_unique(builtinTypes); + + std::vector mergedErrors; + + // Run constraint generation for each module + for (size_t i = 0; i < item.modules.size(); i++) + { + BuildQueueModuleInfo& moduleInfo = item.modules[i]; + const SourceModule& sourceModule = *moduleInfo.sourceModule; + const Config& config = moduleInfo.config; + + Mode mode; + if (FFlag::DebugLuauForceStrictMode) + mode = Mode::Strict; + else if (FFlag::DebugLuauForceNonStrictMode) + mode = Mode::Nonstrict; + else + mode = sourceModule.mode.value_or(config.mode); + + moduleInfo.sourceModule->mode = {mode}; + + // Create module with shared arena + ModulePtr module = std::make_shared(scc->sharedArena); + module->checkedInNewSolver = true; + module->name = sourceModule.name; + module->humanReadableName = sourceModule.humanReadableName; + module->mode = mode; + module->internalTypes->owningModule = module.get(); + module->interfaceTypes.owningModule = module.get(); + module->allocator = sourceModule.allocator; + module->names = sourceModule.names; + module->root = sourceModule.root; + + iceHandler.moduleName = sourceModule.name; + + cgData[i].dfg = std::make_unique( + DataFlowGraphBuilder::build(sourceModule.root, NotNull{&module->defArena}, NotNull{&module->keyArena}, NotNull{&iceHandler}) + ); + + ScopePtr environmentScope = moduleInfo.environmentScope; + + auto prepareModuleScopeWrap = [this](const ModuleName& name, const ScopePtr& scope) + { + if (prepareModuleScope) + prepareModuleScope(name, scope, false); + }; + + ConstraintGenerator cg{ + module, + NotNull{&normalizer}, + NotNull{&typeFunctionRuntime}, + NotNull{&moduleResolver}, + builtinTypes, + NotNull{&iceHandler}, + environmentScope ? environmentScope : globals.globalScope, + globals.globalTypeFunctionScope, + std::move(prepareModuleScopeWrap), + nullptr, // logger + NotNull{cgData[i].dfg.get()}, + moduleInfo.requireCycles, + NotNull{cgraph.get()} + }; + + cg.visitModuleRoot(sourceModule.root); + module->constraintGenerationDidNotComplete = cg.recursionLimitMet; + + cgData[i].cgScopes = std::move(cg.scopes); + + // Bind the placeholder BlockedType to the actual return type so subsequent + // modules in this SCC see real types when they require() this one. + TypePackId actualReturnType = cgData[i].cgScopes[0].second->returnType; + ModulePtr placeholderModule = moduleResolver.getModule(moduleInfo.name); + if (placeholderModule && placeholderModule.get() != module.get()) + { + TypePackId placeholderPack = placeholderModule->returnType; + auto placeholderHead = first(placeholderPack); + + std::optional actualHead; + TypePack headPack = extendTypePack(*scc->sharedArena, builtinTypes, actualReturnType, 1); + if (!headPack.head.empty()) + actualHead = headPack.head[0]; + + if (placeholderHead && actualHead && get(*placeholderHead)) + { + emplaceType(asMutable(*placeholderHead), *actualHead); + } + } + + mergedErrors.insert(mergedErrors.end(), std::make_move_iterator(cg.errors.begin()), std::make_move_iterator(cg.errors.end())); + moduleInfo.module = std::move(module); + } + + LUAU_ASSERT(!cgData.empty() && !cgData[0].cgScopes.empty()); + ScopePtr rootScope = cgData[0].cgScopes[0].second; + + ConstraintSet constraintSet{ + NotNull{rootScope.get()}, + {}, + {}, + DenseHashMap{nullptr}, + {} + }; + + Subtyping subtyping{builtinTypes, NotNull{scc->sharedArena.get()}, NotNull{&normalizer}, NotNull{&typeFunctionRuntime}, NotNull{&iceHandler}}; + + ConstraintSolver cs{ + NotNull{&normalizer}, + NotNull{&typeFunctionRuntime}, + item.modules[0].module, + NotNull{&moduleResolver}, + {}, + nullptr, // logger + NotNull{cgData[0].dfg.get()}, + typeCheckLimits, + std::move(constraintSet), + NotNull{cgraph.get()}, + NotNull{&subtyping} + }; + + try + { + cs.run(); + } + catch (const TimeLimitError&) + { + for (BuildQueueModuleInfo& moduleInfo : item.modules) + moduleInfo.module->timeout = true; + } + catch (const UserCancelError&) + { + for (BuildQueueModuleInfo& moduleInfo : item.modules) + moduleInfo.module->cancelled = true; + } + + // Partition CG + solver errors to the appropriate modules by moduleName + { + DenseHashMap2 nameToModule; + for (const BuildQueueModuleInfo& moduleInfo : item.modules) + nameToModule[moduleInfo.name] = moduleInfo.module; + + for (TypeError& err : mergedErrors) + { + if (ModulePtr* modulePtr = nameToModule.find(err.moduleName)) + (*modulePtr)->errors.emplace_back(std::move(err)); + } + + for (TypeError& err : cs.errors) + { + if (ModulePtr* modulePtr = nameToModule.find(err.moduleName)) + (*modulePtr)->errors.emplace_back(std::move(err)); + } + } + + // Post-solver: run type checking and prepare public interface for each module + for (size_t i = 0; i < item.modules.size(); i++) + { + BuildQueueModuleInfo& moduleInfo = item.modules[i]; + ModulePtr module = moduleInfo.module; + const SourceModule& sourceModule = *moduleInfo.sourceModule; + + module->scopes = std::move(cgData[i].cgScopes); + module->type = sourceModule.type; + + if (module->timeout || module->cancelled) + { + ScopePtr moduleScope = module->getModuleScope(); + moduleScope->returnType = builtinTypes->errorTypePack; + + for (auto& [name, ty] : module->declaredGlobals) + ty = builtinTypes->errorType; + for (auto& [name, tf] : module->exportedTypeBindings) + tf.type = builtinTypes->errorType; + } + else + { + try + { + Mode mode = module->mode; + switch (mode) + { + case Mode::Nonstrict: + Luau::checkNonStrict( + builtinTypes, + NotNull{&typeFunctionRuntime}, + NotNull{&iceHandler}, + NotNull{&unifierState}, + NotNull{cgData[i].dfg.get()}, + NotNull{&typeCheckLimits}, + sourceModule, + module.get() + ); + break; + case Mode::Definition: + // fallthrough + case Mode::Strict: + Luau::check( + builtinTypes, + NotNull{&typeFunctionRuntime}, + NotNull{&unifierState}, + NotNull{&typeCheckLimits}, + nullptr, // logger + sourceModule, + module.get() + ); + break; + case Mode::NoCheck: + break; + } + } + catch (const TimeLimitError&) + { + module->timeout = true; + } + catch (const UserCancelError&) + { + module->cancelled = true; + } + } + + if (FFlag::LuauExportValueSyntax && FFlag::LuauExportValueTypecheck && !module->timeout && !module->cancelled) + synthesizeExportReturn(builtinTypes, NotNull{module.get()}); + + // Clone public interface + unfreeze(module->interfaceTypes); + module->clonePublicInterface(builtinTypes, iceHandler, SolverMode::New); + + if (module->mode == Mode::NoCheck) + { + module->errors.clear(); + continue; + } + + // Add parse errors + ErrorVec parseErrors; + for (const ParseError& pe : sourceModule.parseErrors) + parseErrors.emplace_back(pe.getLocation(), moduleInfo.name, SyntaxError{pe.what()}); + module->errors.insert(module->errors.begin(), parseErrors.begin(), parseErrors.end()); + } + + // Freeze the shared arena after all modules are done + freeze(*scc->sharedArena); + + // Freeze each module's interface types + for (BuildQueueModuleInfo& moduleInfo : item.modules) + freeze(moduleInfo.module->interfaceTypes); + + // Emit a warning on the first SCC member if the cycle is large enough. + if (FInt::LuauCyclicSccWarningThreshold > 0 && scc->members.size() >= static_cast(FInt::LuauCyclicSccWarningThreshold)) + { + item.modules[0].module->errors.emplace_back( + Location{}, item.modules[0].name, CyclicModuleGraphTooLarge{scc->members.size(), scc->members} + ); + } +} + void Frontend::checkBuildQueueItem(BuildQueueItem& item) { - SourceNode& sourceNode = *item.sourceNode; - const SourceModule& sourceModule = *item.sourceModule; - const Config& config = item.config; + if (FFlag::DebugLuauCyclicRequireTypeInference && item.scc && item.modules.size() > 1) + { + checkSCCBuildQueueItem(item); + return; + } + + BuildQueueModuleInfo& moduleInfo = item.modules[0]; + SourceNode& sourceNode = *moduleInfo.sourceNode; + const SourceModule& sourceModule = *moduleInfo.sourceModule; + const Config& config = moduleInfo.config; Mode mode; if (FFlag::DebugLuauForceStrictMode) mode = Mode::Strict; @@ -994,10 +1679,10 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) else mode = sourceModule.mode.value_or(config.mode); - item.sourceModule->mode = {mode}; - ScopePtr environmentScope = item.environmentScope; + moduleInfo.sourceModule->mode = {mode}; + ScopePtr environmentScope = moduleInfo.environmentScope; double timestamp = getTimestamp(); - const std::vector& requireCycles = item.requireCycles; + const std::vector& requireCycles = moduleInfo.requireCycles; TypeCheckLimits typeCheckLimits = makeTypeCheckLimits(item.options); @@ -1027,7 +1712,7 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) environmentScope, /*forAutocomplete*/ true, /*recordJsonLog*/ false, - item.stats, + moduleInfo.stats, std::move(typeCheckLimits) ); @@ -1038,27 +1723,27 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) if (item.options.moduleTimeLimitSec && item.options.applyInternalLimitScaling) applyInternalLimitScaling(sourceNode, moduleForAutocomplete, *item.options.moduleTimeLimitSec); - item.stats.timeCheck += duration; - item.stats.filesStrict += 1; + moduleInfo.stats.timeCheck += duration; + moduleInfo.stats.filesStrict += 1; if (item.options.collectTypeAllocationStats) { - item.stats.typesAllocated += moduleForAutocomplete->internalTypes.types.size(); - item.stats.typePacksAllocated += moduleForAutocomplete->internalTypes.typePacks.size(); - item.stats.boolSingletonsMinted += moduleForAutocomplete->internalTypes.boolSingletonsMinted; - item.stats.strSingletonsMinted += moduleForAutocomplete->internalTypes.strSingletonsMinted; - item.stats.uniqueStrSingletonsMinted += moduleForAutocomplete->internalTypes.uniqueStrSingletonsMinted.size(); + moduleInfo.stats.typesAllocated += moduleForAutocomplete->internalTypes->types.size(); + moduleInfo.stats.typePacksAllocated += moduleForAutocomplete->internalTypes->typePacks.size(); + moduleInfo.stats.boolSingletonsMinted += moduleForAutocomplete->internalTypes->boolSingletonsMinted; + moduleInfo.stats.strSingletonsMinted += moduleForAutocomplete->internalTypes->strSingletonsMinted; + moduleInfo.stats.uniqueStrSingletonsMinted += moduleForAutocomplete->internalTypes->uniqueStrSingletonsMinted.size(); } if (item.options.customModuleCheck) item.options.customModuleCheck(sourceModule, *moduleForAutocomplete); - item.module = moduleForAutocomplete; + moduleInfo.module = moduleForAutocomplete; return; } ModulePtr module = check( - sourceModule, mode, requireCycles, environmentScope, /*forAutocomplete*/ false, item.recordJsonLog, item.stats, std::move(typeCheckLimits) + sourceModule, mode, requireCycles, environmentScope, /*forAutocomplete*/ false, item.recordJsonLog, moduleInfo.stats, std::move(typeCheckLimits) ); double duration = getTimestamp() - timestamp; @@ -1068,17 +1753,17 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) if (item.options.moduleTimeLimitSec && item.options.applyInternalLimitScaling) applyInternalLimitScaling(sourceNode, module, *item.options.moduleTimeLimitSec); - item.stats.timeCheck += duration; - item.stats.filesStrict += (mode == Mode::Strict) ? 1 : 0; - item.stats.filesNonstrict += (mode == Mode::Nonstrict) ? 1 : 0; + moduleInfo.stats.timeCheck += duration; + moduleInfo.stats.filesStrict += (mode == Mode::Strict) ? 1 : 0; + moduleInfo.stats.filesNonstrict += (mode == Mode::Nonstrict) ? 1 : 0; if (item.options.collectTypeAllocationStats) { - item.stats.typesAllocated += module->internalTypes.types.size(); - item.stats.typePacksAllocated += module->internalTypes.typePacks.size(); - item.stats.boolSingletonsMinted += module->internalTypes.boolSingletonsMinted; - item.stats.strSingletonsMinted += module->internalTypes.strSingletonsMinted; - item.stats.uniqueStrSingletonsMinted += module->internalTypes.uniqueStrSingletonsMinted.size(); + moduleInfo.stats.typesAllocated += module->internalTypes->types.size(); + moduleInfo.stats.typePacksAllocated += module->internalTypes->typePacks.size(); + moduleInfo.stats.boolSingletonsMinted += module->internalTypes->boolSingletonsMinted; + moduleInfo.stats.strSingletonsMinted += module->internalTypes->strSingletonsMinted; + moduleInfo.stats.uniqueStrSingletonsMinted += module->internalTypes->uniqueStrSingletonsMinted.size(); } if (item.options.customModuleCheck) @@ -1099,7 +1784,7 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) std::vector warnings = Luau::lint(sourceModule.root, *sourceModule.names, environmentScope, module.get(), sourceModule.hotcomments, lintOptions); - item.stats.timeLint += getTimestamp() - timestamp; + moduleInfo.stats.timeLint += getTimestamp() - timestamp; module->lintResult = classifyLints(warnings, config); } @@ -1112,7 +1797,7 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) copyErrors(module->errors, module->interfaceTypes, builtinTypes); freeze(module->interfaceTypes); - module->internalTypes.clear(); + module->internalTypes->clear(); module->defArena.allocator.clear(); module->keyArena.allocator.clear(); @@ -1134,7 +1819,7 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) { for (const RequireCycle& cyc : requireCycles) { - TypeError te{cyc.location, item.name, ModuleHasCyclicDependency{cyc.path}}; + TypeError te{cyc.location, moduleInfo.name, ModuleHasCyclicDependency{cyc.path}}; module->errors.push_back(te); } @@ -1143,10 +1828,10 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) ErrorVec parseErrors; for (const ParseError& pe : sourceModule.parseErrors) - parseErrors.emplace_back(pe.getLocation(), item.name, SyntaxError{pe.what()}); + parseErrors.emplace_back(pe.getLocation(), moduleInfo.name, SyntaxError{pe.what()}); module->errors.insert(module->errors.begin(), parseErrors.begin(), parseErrors.end()); - item.module = module; + moduleInfo.module = module; } void Frontend::checkBuildQueueItems(std::vector& items) @@ -1155,8 +1840,25 @@ void Frontend::checkBuildQueueItems(std::vector& items) { checkBuildQueueItem(item); - if (item.module && item.module->cancelled) - break; + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + bool cancelled = false; + for (const BuildQueueModuleInfo& moduleInfo : item.modules) + { + if (moduleInfo.module && moduleInfo.module->cancelled) + { + cancelled = true; + break; + } + } + if (cancelled) + break; + } + else + { + if (item.modules[0].module && item.modules[0].module->cancelled) + break; + } recordItemResult(item); } @@ -1167,52 +1869,65 @@ void Frontend::recordItemResult(const BuildQueueItem& item) if (item.exception) std::rethrow_exception(item.exception); - bool replacedModule = false; - if (item.options.forAutocomplete) - { - replacedModule = moduleResolverForAutocomplete.setModule(item.name, item.module); - item.sourceNode->dirtyModuleForAutocomplete = false; - } - else + auto recordModuleInfo = [&](const BuildQueueModuleInfo& moduleInfo) { - replacedModule = moduleResolver.setModule(item.name, item.module); - item.sourceNode->dirtyModule = false; - } + bool replacedModule = false; + if (item.options.forAutocomplete) + { + replacedModule = moduleResolverForAutocomplete.setModule(moduleInfo.name, moduleInfo.module); + moduleInfo.sourceNode->dirtyModuleForAutocomplete = false; + } + else + { + replacedModule = moduleResolver.setModule(moduleInfo.name, moduleInfo.module); + moduleInfo.sourceNode->dirtyModule = false; + } - if (replacedModule) - { - LUAU_TIMETRACE_SCOPE("Frontend::invalidateDependentModules", "Frontend"); - LUAU_TIMETRACE_ARGUMENT("name", item.name.c_str()); - traverseDependents( - item.name, - [forAutocomplete = item.options.forAutocomplete](SourceNode& sourceNode) - { - bool traverseSubtree = !sourceNode.hasInvalidModuleDependency(forAutocomplete); - sourceNode.setInvalidModuleDependency(true, forAutocomplete); - return traverseSubtree; - } - ); - } + if (replacedModule) + { + LUAU_TIMETRACE_SCOPE("Frontend::invalidateDependentModules", "Frontend"); + LUAU_TIMETRACE_ARGUMENT("name", moduleInfo.name.c_str()); + traverseDependents( + moduleInfo.name, + [forAutocomplete = item.options.forAutocomplete](SourceNode& sourceNode) + { + bool traverseSubtree = !sourceNode.hasInvalidModuleDependency(forAutocomplete); + sourceNode.setInvalidModuleDependency(true, forAutocomplete); + return traverseSubtree; + } + ); + } - item.sourceNode->setInvalidModuleDependency(false, item.options.forAutocomplete); + moduleInfo.sourceNode->setInvalidModuleDependency(false, item.options.forAutocomplete); - stats.timeCheck += item.stats.timeCheck; - stats.timeLint += item.stats.timeLint; + stats.timeCheck += moduleInfo.stats.timeCheck; + stats.timeLint += moduleInfo.stats.timeLint; - stats.filesStrict += item.stats.filesStrict; - stats.filesNonstrict += item.stats.filesNonstrict; + stats.filesStrict += moduleInfo.stats.filesStrict; + stats.filesNonstrict += moduleInfo.stats.filesNonstrict; - if (item.options.collectTypeAllocationStats) - { - stats.typesAllocated += item.stats.typesAllocated; - stats.typePacksAllocated += item.stats.typePacksAllocated; + if (item.options.collectTypeAllocationStats) + { + stats.typesAllocated += moduleInfo.stats.typesAllocated; + stats.typePacksAllocated += moduleInfo.stats.typePacksAllocated; - stats.boolSingletonsMinted += item.stats.boolSingletonsMinted; - stats.strSingletonsMinted += item.stats.strSingletonsMinted; - stats.uniqueStrSingletonsMinted += item.stats.uniqueStrSingletonsMinted; - } + stats.boolSingletonsMinted += moduleInfo.stats.boolSingletonsMinted; + stats.strSingletonsMinted += moduleInfo.stats.strSingletonsMinted; + stats.uniqueStrSingletonsMinted += moduleInfo.stats.uniqueStrSingletonsMinted; + } + + stats.dynamicConstraintsCreated += moduleInfo.stats.dynamicConstraintsCreated; + }; - stats.dynamicConstraintsCreated += item.stats.dynamicConstraintsCreated; + if (FFlag::DebugLuauCyclicRequireTypeInference) + { + for (const BuildQueueModuleInfo& moduleInfo : item.modules) + recordModuleInfo(moduleInfo); + } + else + { + recordModuleInfo(item.modules[0]); + } } void Frontend::performQueueItemTask(std::shared_ptr state, size_t itemPos) @@ -1453,14 +2168,14 @@ ModulePtr check( LUAU_TIMETRACE_ARGUMENT("module", sourceModule.name.c_str()); LUAU_TIMETRACE_ARGUMENT("name", sourceModule.humanReadableName.c_str()); - ModulePtr module = std::make_shared(); + ModulePtr module = std::make_shared(std::make_shared()); module->checkedInNewSolver = true; module->name = sourceModule.name; module->humanReadableName = sourceModule.humanReadableName; module->mode = mode; - module->internalTypes.owningModule = module.get(); + module->internalTypes->owningModule = module.get(); module->interfaceTypes.owningModule = module.get(); - module->internalTypes.collectSingletonStats = options.collectTypeAllocationStats; + module->internalTypes->collectSingletonStats = options.collectTypeAllocationStats; module->allocator = sourceModule.allocator; module->names = sourceModule.names; module->root = sourceModule.root; @@ -1484,11 +2199,29 @@ ModulePtr check( unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit; unifierState.counters.iterationLimit = limits.unifierIterationLimit.value_or(FInt::LuauTypeInferIterationLimit); - Normalizer normalizer{&module->internalTypes, builtinTypes, NotNull{&unifierState}, SolverMode::New}; + Normalizer normalizer{module->internalTypes.get(), builtinTypes, NotNull{&unifierState}, SolverMode::New}; TypeFunctionRuntime typeFunctionRuntime{iceHandler, NotNull{&limits}}; typeFunctionRuntime.allowEvaluation = true; + Subtyping subtyping{builtinTypes, NotNull{module->internalTypes.get()}, NotNull{&normalizer}, NotNull{&typeFunctionRuntime}, iceHandler}; + + std::unique_ptr cgraph = std::make_unique(builtinTypes); + + CFG::CFGAllocator cfgAllocator; + std::unique_ptr cfg; + std::unique_ptr state; + if (FFlag::DebugLuauCFG && mode != Mode::Definition) + { + cfg = CFG::CFGBuilder::makeCFG(NotNull{&cfgAllocator}, sourceModule.root); + if (FFlag::DebugLuauLogCFG) + printf("%s", dumpCFG(*cfg).c_str()); + if (FFlag::DebugLuauDumpCFGJson) + printf("%s\n", dumpCFGJson(*cfg).c_str()); + state = std::make_unique(NotNull{module->internalTypes.get()}, NotNull{parentScope.get()}, builtinTypes, NotNull{cfg.get()}); + state->computeTypes(); + } + ConstraintGenerator cg{ module, NotNull{&normalizer}, @@ -1501,7 +2234,9 @@ ModulePtr check( std::move(prepareModuleScope), logger.get(), NotNull{&dfg}, - requireCycles + requireCycles, + NotNull{cgraph.get()}, + FFlag::DebugLuauCFG ? state.get() : nullptr }; ConstraintSet constraintSet = cg.run(sourceModule.root); @@ -1517,9 +2252,12 @@ ModulePtr check( logger.get(), NotNull{&dfg}, limits, - std::move(constraintSet) + std::move(constraintSet), + NotNull{cgraph.get()}, + NotNull{&subtyping} }; + if (options.randomizeConstraintResolutionSeed) cs.randomize(*options.randomizeConstraintResolutionSeed); @@ -1604,6 +2342,9 @@ ModulePtr check( { module->cancelled = true; } + + if (FFlag::LuauExportValueSyntax && FFlag::LuauExportValueTypecheck && !module->timeout && !module->cancelled) + synthesizeExportReturn(builtinTypes, NotNull{module.get()}); } // if the only error we're producing is one about constraint solving being incomplete, we can silence it. @@ -1617,7 +2358,8 @@ ModulePtr check( NotNull{&module->astTypes}, NotNull{&module->astExpectedTypes}, NotNull{&module->astResolvedTypes}, - NotNull{&module->internalTypes}, + NotNull{&module->astOverloadResolvedTypes}, + NotNull{module->internalTypes.get()}, builtinTypes, NotNull{parentScope.get()} }; @@ -1670,7 +2412,7 @@ ModulePtr check( // Notably, we would first need to get to a place where TypeChecker2 is // never in the position of dealing with a FreeType. They should all be // bound to something by the time constraints are solved. - freeze(module->internalTypes); + freeze(*module->internalTypes); freeze(module->interfaceTypes); return module; @@ -1777,7 +2519,30 @@ std::pair Frontend::getSourceNode(const ModuleName& if (!source) { - sourceModules.erase(name); + if (FFlag::LuauFrontendSourceNodeErase) + { + if (auto it = sourceNodes.find(name); it != sourceNodes.end()) + { + // Remove this module from the dependents set of each of its dependencies + for (const ModuleName& dep : it->second->requireSet) + { + if (auto depIt = sourceNodes.find(dep); depIt != sourceNodes.end()) + depIt->second->dependents.erase(name); + } + + sourceNodes.erase(it); + } + + sourceModules.erase(name); + requireTrace.erase(name); + moduleResolver.eraseModule(name); + moduleResolverForAutocomplete.eraseModule(name); + } + else + { + sourceModules.erase(name); + } + return {nullptr, nullptr}; } @@ -1943,6 +2708,13 @@ bool FrontendModuleResolver::setModule(const ModuleName& moduleName, ModulePtr m return replaced; } +void FrontendModuleResolver::eraseModule(const ModuleName& moduleName) +{ + std::scoped_lock lock(moduleMutex); + + modules.erase(moduleName); +} + void FrontendModuleResolver::clearModules() { std::scoped_lock lock(moduleMutex); @@ -2017,6 +2789,33 @@ void Frontend::clear() requireTrace.clear(); } +void Frontend::clearModules(const std::vector& names) +{ + for (const ModuleName& name : names) + markDirty(name); + + for (const ModuleName& name : names) + { + auto it = sourceNodes.find(name); + if (it == sourceNodes.end()) + continue; + + // Remove this module from the dependents set of each of its dependencies + for (const ModuleName& dep : it->second->requireSet) + { + auto depIt = sourceNodes.find(dep); + if (depIt != sourceNodes.end()) + depIt->second->dependents.erase(name); + } + + sourceNodes.erase(it); + sourceModules.erase(name); + requireTrace.erase(name); + moduleResolver.eraseModule(name); + moduleResolverForAutocomplete.eraseModule(name); + } +} + void Frontend::clearBuiltinEnvironments() { environments.clear(); @@ -2040,7 +2839,7 @@ TypeId Frontend::parseType( if (!parseResult.errors.empty()) iceHandler->ice("Frontend::parseType error: " + parseResult.errors.front().getMessage()); - ModulePtr module = std::make_shared(); + ModulePtr module = std::make_shared(std::make_shared()); UnifierSharedState unifierState{iceHandler}; unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit; @@ -2055,6 +2854,8 @@ TypeId Frontend::parseType( DataFlowGraph dfg = DataFlowGraphBuilder::empty(NotNull{&module->defArena}, NotNull{&module->keyArena}); + std::unique_ptr cgraph = std::make_unique(builtinTypes); + ConstraintGenerator cg{ module, NotNull{&normalizer}, @@ -2067,12 +2868,14 @@ TypeId Frontend::parseType( nullptr, nullptr, NotNull{&dfg}, - {} + {}, + NotNull{cgraph.get()}, }; TypeId t = cg.resolveType(globals.globalScope, parseResult.root, false); - if (!cg.constraints.empty()) + bool hasConstraints = FFlag::DebugLuauCyclicRequireTypeInference ? !cg.cgraph->constraints.empty() : !cg.constraints.empty(); + if (hasConstraints) { iceHandler->ice("Not yet implemented: parseType cannot reduce other type aliases"); } diff --git a/Analysis/src/Generalization.cpp b/Analysis/src/Generalization.cpp index 1a04290e..0fc1ab84 100644 --- a/Analysis/src/Generalization.cpp +++ b/Analysis/src/Generalization.cpp @@ -13,10 +13,12 @@ #include "Luau/TypeArena.h" #include "Luau/TypeIds.h" #include "Luau/TypePack.h" +#include "Luau/TypeUtils.h" #include "Luau/VisitType.h" LUAU_FASTINTVARIABLE(LuauGenericCounterMaxDepth, 15) LUAU_FASTINTVARIABLE(LuauGenericCounterMaxSteps, 1500) +LUAU_FASTFLAGVARIABLE(LuauCollapseDirectBoundCycles) namespace Luau { @@ -725,8 +727,203 @@ void removeType(NotNull arena, NotNull builtinTypes, Ty tr.process(haystack); } +struct FreeTypeFinder : TypeOnceVisitor +{ + NotNull arena; + TypeIds freeTys; + + explicit FreeTypeFinder(NotNull arena) + : TypeOnceVisitor("FreeTypeFinder", /*skipBoundTypes*/ true) + , arena(arena) + {} + + bool visit(TypeId ty, const FreeType&) override + { + if (ty->owningArena != arena) + return false; + + freeTys.insert(ty); + return true; + } + + bool visit(TypeId ty, const TableType&) override + { + return false; + } + + bool visit(TypeId ty, const MetatableType&) override + { + return false; + } + + bool visit(TypeId ty, const FunctionType&) override + { + return false; + } + + bool visit(TypeId ty, const ExternType&) override + { + return false; + } +}; + +TypeId getDirectFreeNeighbor(TypeId ty) +{ + ty = follow(ty); + if (get(ty)) + return ty; + return nullptr; +} + +void collapseInvariantFreeType(NotNull arena, TypeId ty) +{ + FreeTypeFinder ftf{arena}; + ftf.traverse(ty); + + for (TypeId t : ftf.freeTys) + { + const FreeType* ft = get(t); + LUAU_ASSERT(ft); + + auto ub = follow(ft->upperBound); + auto lb = follow(ft->lowerBound); + if (ub == lb && ub != t) + emplaceType(asMutable(t), ub); + } +} + + +// Walk direct free-type bounds starting from `startTy`. If a cycle is +// reachable, collapse every member into one representative free type whose +// bounds are the union of every member's external lower bound and the +// intersection of every member's external upper bound. Cycle self-references +// (whether direct or nested in unions/intersections) are stripped from those +// bounds. +// +// A "direct bound" means A.lowerBound or A.upperBound IS another free type +// (not nested inside a union/intersection/table/function). Cycles formed by +// direct bounds (A->B->...->A) are the only cycles this helper detects; +// non-cycling chains are left alone. +// +// Returns true if any types were collapsed. +bool collapseDirectBoundCycleAt(NotNull arena, NotNull builtinTypes, TypeId startTy) +{ + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + collapseInvariantFreeType(arena, startTy); + + startTy = follow(startTy); + + if (!get(startTy)) + return false; + + TypeIds path; + TypeId cur = startTy; + + while (cur) + { + if (path.contains(cur)) + { + // Collect cycle members: everything on the path from `cur` onward. + TypeIds cycleMembers; + bool inCycle = false; + for (TypeId member : path) + { + if (member == cur) + inCycle = true; + if (inCycle) + cycleMembers.insert(member); + } + LUAU_ASSERT(!cycleMembers.empty()); + + // Merge external bounds from every cycle member into the + // representative. Lower bounds union; upper bounds intersect. + // Skip bounds that are entirely cycle self-refs. + UnionBuilder mergedLowers{arena, builtinTypes}; + IntersectionBuilder mergedUppers{arena, builtinTypes}; + + for (TypeId m : cycleMembers) + { + FreeType* ft = getMutable(m); + if (!ft) + continue; + + TypeId lb = follow(ft->lowerBound); + if (!cycleMembers.contains(lb)) + { + for (TypeId other : cycleMembers) + removeType(arena, builtinTypes, lb, other); + lb = follow(lb); + if (!get(lb) && !cycleMembers.contains(lb)) + mergedLowers.add(lb); + } + + TypeId ub = follow(ft->upperBound); + if (!cycleMembers.contains(ub)) + { + for (TypeId other : cycleMembers) + removeType(arena, builtinTypes, ub, other); + ub = follow(ub); + if (!get(ub) && !cycleMembers.contains(ub)) + mergedUppers.add(ub); + } + } + + // Pick the back-edge target (cycleMembers[0] = `cur`) as the + // representative, give it the merged bounds, and bind the rest. + TypeId rep = cycleMembers.front(); + FreeType* repFree = getMutable(rep); + LUAU_ASSERT(repFree); + + repFree->lowerBound = mergedLowers.build(); + repFree->upperBound = mergedUppers.build(); + + auto it = cycleMembers.begin() + 1; + while (it != cycleMembers.end()) + { + emplaceType(asMutable(*it), rep); + ++it; + } + + return true; + } + + path.insert(cur); + + FreeType* ft = getMutable(cur); + if (!ft) + break; + + // Try to follow a direct free-type bound (prefer upper, then lower). + TypeId next = getDirectFreeNeighbor(ft->upperBound); + if (!next || next == cur) + next = getDirectFreeNeighbor(ft->lowerBound); + if (next == cur) + next = nullptr; + + cur = next; + } + + return false; +} + +// Batch pre-pass: collapse direct-bound cycles among the free types in the +// generalization frontier. Iteration order does not matter -- once a cycle +// has been collapsed, subsequent walks from any member terminate immediately +// because the type is no longer free (it has been bound to the rep) or +// because the rep's bounds no longer reference cycle members. +void collapseFreeTypeCycles( + NotNull arena, + NotNull builtinTypes, + const InsertionOrderedMap>& freeTypes +) +{ + for (const auto& [startTy, _] : freeTypes) + collapseDirectBoundCycleAt(arena, builtinTypes, startTy); +} + } // namespace + GeneralizationResult generalizeType( NotNull arena, NotNull builtinTypes, @@ -737,6 +934,27 @@ GeneralizationResult generalizeType( { freeTy = follow(freeTy); + // Collapse any direct-bound cycle this free type participates in before we + // commit to a generalization decision. This handles the per-call + // invocations from ConstraintSolver -- which bypass the batch pre-pass in + // generalize() -- and is a no-op when the cycle has already been collapsed + // by that pre-pass. When this fires, freeTy may be re-bound to the + // representative of the cycle, so we re-follow it. + if (FFlag::LuauCollapseDirectBoundCycles) + { + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + // Run this and unconditionally re-follow. + collapseDirectBoundCycleAt(arena, builtinTypes, freeTy); + freeTy = follow(freeTy); + } + else if (collapseDirectBoundCycleAt(arena, builtinTypes, freeTy)) + freeTy = follow(freeTy); + + if (!get(freeTy)) + return {freeTy, /*wasReplacedByGeneric*/ false}; + } + FreeType* ft = getMutable(freeTy); LUAU_ASSERT(ft); @@ -766,10 +984,24 @@ GeneralizationResult generalizeType( else if (isPositive(params.polarity) && !hasUpperBound) { TypeId lb = follow(ft->lowerBound); - if (FreeType* lowerFree = getMutable(lb); lowerFree && lowerFree->upperBound == freeTy) - lowerFree->upperBound = builtinTypes->unknownType; - else + if (FFlag::LuauCollapseDirectBoundCycles) removeType(arena, builtinTypes, lb, freeTy); + else + { + if (FreeType* lowerFree = getMutable(lb); lowerFree && lowerFree->upperBound == freeTy) + { + // If we are generalizing 'a in: + // + // LO <: 'b <: 'a <: UP + // + // ... we can hold onto the bound UP and forward it to 'b. + TypeId upperBound = follow(ft->upperBound); + removeType(arena, builtinTypes, upperBound, freeTy); + lowerFree->upperBound = follow(upperBound); + } + else + removeType(arena, builtinTypes, lb, freeTy); + } if (follow(lb) != freeTy) emplaceType(asMutable(freeTy), lb); @@ -785,10 +1017,27 @@ GeneralizationResult generalizeType( else { TypeId ub = follow(ft->upperBound); - if (FreeType* upperFree = getMutable(ub); upperFree && upperFree->lowerBound == freeTy) - upperFree->lowerBound = builtinTypes->neverType; - else + // When LuauCollapseDirectBoundCycles is on, the pre-pass + // collapseDirectBoundCycleAt has already collapsed any 2-cycle here, + // so the forwarding branch below would never fire -- skip it. + if (FFlag::LuauCollapseDirectBoundCycles) removeType(arena, builtinTypes, ub, freeTy); + else + { + if (FreeType* upperFree = getMutable(ub); upperFree && upperFree->lowerBound == freeTy) + { + // If we are generalizing 'a in: + // + // LO <: 'a <: 'b <: UP + // + // ... we can hold onto the bound LO and forward it to 'b. + TypeId lowerBound = follow(ft->lowerBound); + removeType(arena, builtinTypes, lowerBound, freeTy); + upperFree->lowerBound = follow(lowerBound); + } + else + removeType(arena, builtinTypes, ub, freeTy); + } if (follow(ub) != freeTy) emplaceType(asMutable(freeTy), ub); @@ -892,16 +1141,65 @@ std::optional generalize( functionTy->genericPacks.push_back(tp); }; - for (const auto& [freeTy, params] : fts.types) + if (FFlag::LuauCollapseDirectBoundCycles && !generalizationTarget) + collapseFreeTypeCycles(arena, builtinTypes, fts.types); + + if (FFlag::LuauCollapseDirectBoundCycles) { - if (!generalizationTarget || freeTy == *generalizationTarget) + auto generalizeJustOne = [&](TypeId freeTy, const auto& params) { + if (!get(follow(freeTy))) + return GeneralizationResult{}; + GeneralizationResult res = generalizeType(arena, builtinTypes, scope, freeTy, params); + if (res.resourceLimitsExceeded) - return std::nullopt; + return res; if (res && res.wasReplacedByGeneric) pushGeneric(*res.result); + + return res; + }; + + if (generalizationTarget) + { + auto it = fts.types.find(*generalizationTarget); + if (it != fts.types.end()) + { + const auto [freeTy, params] = *it; + auto res = generalizeJustOne(freeTy, params); + if (res.resourceLimitsExceeded) + return std::nullopt; + } + } + else + { + for (const auto& [freeTy, params] : fts.types) + { + auto res = generalizeJustOne(freeTy, params); + if (res.resourceLimitsExceeded) + return std::nullopt; + } + } + } + else + { + for (const auto& [freeTy, params] : fts.types) + { + if (!generalizationTarget || freeTy == *generalizationTarget) + { + if (FFlag::LuauCollapseDirectBoundCycles && !get(follow(freeTy))) + continue; + + GeneralizationResult res = generalizeType(arena, builtinTypes, scope, freeTy, params); + + if (res.resourceLimitsExceeded) + return std::nullopt; + + if (res && res.wasReplacedByGeneric) + pushGeneric(*res.result); + } } } diff --git a/Analysis/src/GlobalTypes.cpp b/Analysis/src/GlobalTypes.cpp index 8b205f4c..bd25428e 100644 --- a/Analysis/src/GlobalTypes.cpp +++ b/Analysis/src/GlobalTypes.cpp @@ -2,6 +2,9 @@ #include "Luau/GlobalTypes.h" +LUAU_FASTFLAG(LuauIntegerType2) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) + namespace Luau { @@ -15,12 +18,19 @@ GlobalTypes::GlobalTypes(NotNull builtinTypes, SolverMode mode) globalScope->addBuiltinTypeBinding("any", TypeFun{{}, builtinTypes->anyType}); globalScope->addBuiltinTypeBinding("nil", TypeFun{{}, builtinTypes->nilType}); globalScope->addBuiltinTypeBinding("number", TypeFun{{}, builtinTypes->numberType}); + if (FFlag::LuauIntegerType2) + globalScope->addBuiltinTypeBinding("integer", TypeFun{{}, builtinTypes->integerType}); globalScope->addBuiltinTypeBinding("string", TypeFun{{}, builtinTypes->stringType}); globalScope->addBuiltinTypeBinding("boolean", TypeFun{{}, builtinTypes->booleanType}); globalScope->addBuiltinTypeBinding("thread", TypeFun{{}, builtinTypes->threadType}); globalScope->addBuiltinTypeBinding("buffer", TypeFun{{}, builtinTypes->bufferType}); globalScope->addBuiltinTypeBinding("unknown", TypeFun{{}, builtinTypes->unknownType}); globalScope->addBuiltinTypeBinding("never", TypeFun{{}, builtinTypes->neverType}); + if (FFlag::DebugLuauUserDefinedClasses) + { + globalScope->addBuiltinTypeBinding("object", TypeFun{{}, builtinTypes->objectType}); + globalScope->addBuiltinTypeBinding("class", TypeFun{{}, builtinTypes->classType}); + } unfreeze(*builtinTypes->arena); TypeId stringMetatableTy = makeStringMetatable(builtinTypes, mode); diff --git a/Analysis/src/InferPolarity.cpp b/Analysis/src/InferPolarity.cpp deleted file mode 100644 index cc409337..00000000 --- a/Analysis/src/InferPolarity.cpp +++ /dev/null @@ -1,167 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details - -#include "Luau/DenseHash.h" -#include "Luau/Polarity.h" -#include "Luau/Scope.h" -#include "Luau/VisitType.h" - - -namespace Luau -{ - -struct InferPolarity : TypeVisitor -{ - NotNull arena; - NotNull scope; - - DenseHashMap types{nullptr}; - DenseHashMap packs{nullptr}; - - Polarity polarity = Polarity::Positive; - - explicit InferPolarity(NotNull arena, NotNull scope) - : TypeVisitor("InferPolarity", /* skipBoundTypes */ true) - , arena(arena) - , scope(scope) - { - } - - void flip() - { - polarity = invert(polarity); - } - - bool visit(TypeId ty, const GenericType& gt) override - { - if (ty->owningArena != arena) - return false; - - if (subsumes(scope, gt.scope)) - types[ty] |= polarity; - - return false; - } - - bool visit(TypeId ty, const TableType& tt) override - { - if (ty->owningArena != arena) - return false; - - const Polarity p = polarity; - for (const auto& [name, prop] : tt.props) - { - if (prop.isShared()) - { - polarity = Polarity::Mixed; - traverse(*prop.readTy); - continue; - } - - if (prop.readTy) - { - polarity = p; - traverse(*prop.readTy); - } - - if (prop.writeTy) - { - polarity = invert(p); - traverse(*prop.writeTy); - } - } - - if (tt.indexer) - { - polarity = Polarity::Mixed; - traverse(tt.indexer->indexType); - traverse(tt.indexer->indexResultType); - } - - polarity = p; - - return false; - } - - bool visit(TypeId ty, const FunctionType& ft) override - { - if (ty->owningArena != arena) - return false; - - const Polarity p = polarity; - - polarity = Polarity::Positive; - - // If these types actually occur within the function signature, their - // polarity will be overwritten. If not, we infer that they are phantom - // types. - for (TypeId generic : ft.generics) - { - generic = follow(generic); - const auto gen = get(generic); - if (gen && subsumes(scope, gen->scope)) - types[generic] = Polarity::None; - } - for (TypePackId genericPack : ft.genericPacks) - { - genericPack = follow(genericPack); - const auto gen = get(genericPack); - if (gen && subsumes(scope, gen->scope)) - packs[genericPack] = Polarity::None; - } - - flip(); - traverse(ft.argTypes); - flip(); - traverse(ft.retTypes); - - polarity = p; - - return false; - } - - bool visit(TypeId, const ExternType&) override - { - return false; - } - - bool visit(TypePackId tp, const GenericTypePack& gtp) override - { - packs[tp] |= polarity; - return false; - } -}; - -template -static void inferGenericPolarities_(NotNull arena, NotNull scope, TID ty) -{ - InferPolarity infer{arena, scope}; - infer.traverse(ty); - - for (const auto& [ty, polarity] : infer.types) - { - auto gt = getMutable(ty); - LUAU_ASSERT(gt); - gt->polarity = polarity; - } - - for (const auto& [tp, polarity] : infer.packs) - { - if (tp->owningArena != arena) - continue; - auto gp = getMutable(tp); - LUAU_ASSERT(gp); - gp->polarity = polarity; - } -} - -void inferGenericPolarities_DEPRECATED(NotNull arena, NotNull scope, TypeId ty) -{ - inferGenericPolarities_(arena, scope, ty); -} - -void inferGenericPolarities_DEPRECATED(NotNull arena, NotNull scope, TypePackId tp) -{ - inferGenericPolarities_(arena, scope, tp); -} - -} // namespace Luau diff --git a/Analysis/src/Instantiation.cpp b/Analysis/src/Instantiation.cpp index a6af85aa..8d1ddb55 100644 --- a/Analysis/src/Instantiation.cpp +++ b/Analysis/src/Instantiation.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/Instantiation.h" +#include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/Instantiation2.h" // including for `Replacer` which was stolen since it will be kept in the new solver #include "Luau/ToString.h" @@ -11,6 +12,7 @@ #include LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAGVARIABLE(LuauInstantiationUsesPolarity) namespace Luau { @@ -150,6 +152,7 @@ bool ReplaceGenerics::isDirty(TypePackId tp) TypeId ReplaceGenerics::clean(TypeId ty) { LUAU_ASSERT(isDirty(ty)); + if (const TableType* ttv = log->getMutable(ty)) { TableType clone = TableType{ttv->props, ttv->indexer, level, scope, TableState::Free}; @@ -157,16 +160,8 @@ TypeId ReplaceGenerics::clean(TypeId ty) clone.definitionLocation = ttv->definitionLocation; return addType(std::move(clone)); } - else if (FFlag::LuauSolverV2) - { - TypeId res = freshType(NotNull{arena}, builtinTypes, scope); - getMutable(res)->level = level; - return res; - } else - { return arena->freshType(builtinTypes, scope, level); - } } TypePackId ReplaceGenerics::clean(TypePackId tp) @@ -195,28 +190,46 @@ std::optional instantiate( DenseHashMap replacements{nullptr}; DenseHashMap replacementPacks{nullptr}; - for (TypeId g : ft->generics) - replacements[g] = freshType(arena, builtinTypes, scope); + if (FFlag::LuauInstantiationUsesPolarity) + { + for (TypeId g : ft->generics) + { + if (auto gen = get(follow(g))) + replacements[g] = freshType(arena, builtinTypes, scope, gen->polarity); + } + + for (TypePackId g : ft->genericPacks) + { + if (auto gen = get(follow(g))) + replacementPacks[g] = arena->freshTypePack(scope, gen->polarity); + } + } + else + { + for (TypeId g : ft->generics) + replacements[g] = freshType(arena, builtinTypes, scope); - for (TypePackId g : ft->genericPacks) - replacementPacks[g] = arena->freshTypePack(scope); + for (TypePackId g : ft->genericPacks) + replacementPacks[g] = arena->freshTypePack(scope); + } - Replacer r{arena, std::move(replacements), std::move(replacementPacks)}; + Replacer r{arena, NotNull{&replacements}, NotNull{&replacementPacks}}; if (limits->instantiationChildLimit) r.childLimit = *limits->instantiationChildLimit; - std::optional res = r.substitute(ty); - if (!res) - return res; - - FunctionType* ft2 = getMutable(*res); + CloneState cs{builtinTypes}; + // We clone persistent types here to enable instantiation for generic + // builtins like `table.find`; otherwise, the lines after would + // immediately corrupt the definitions of the original function. + auto clonedFunctionTypeId = shallowClone(ty, *arena, cs, /* clonePersistentTypes */ true); + FunctionType* ft2 = getMutable(clonedFunctionTypeId); LUAU_ASSERT(ft != ft2); ft2->generics.clear(); ft2->genericPacks.clear(); - return res; + return r.substitute(clonedFunctionTypeId); } } // namespace Luau diff --git a/Analysis/src/Instantiation2.cpp b/Analysis/src/Instantiation2.cpp index 614277c8..fc5e2ad7 100644 --- a/Analysis/src/Instantiation2.cpp +++ b/Analysis/src/Instantiation2.cpp @@ -4,12 +4,92 @@ #include "Luau/Scope.h" #include "Luau/Instantiation2.h" -LUAU_FASTFLAGVARIABLE(LuauInstantiationUsesGenericPolarity2) -LUAU_FASTFLAGVARIABLE(LuauInstantiationUsesGenericPolarityFollow) +LUAU_FASTFLAGVARIABLE(LuauHigherOrderGenericInference) + namespace Luau { -bool Instantiation2::ignoreChildren(TypeId ty) +Replacer::Replacer( + NotNull arena, + NotNull> replacements, + NotNull> replacementPacks +) + : Substitution(TxnLog::empty(), arena) + , replacements(replacements) + , replacementPacks(replacementPacks) +{ + LUAU_ASSERT(checkReplacementKeys()); +} + +bool Replacer::isDirty(TypeId ty) +{ + return replacements->contains(ty); +} + +bool Replacer::isDirty(TypePackId tp) +{ + return replacementPacks->contains(tp); +} + +TypeId Replacer::clean(TypeId ty) +{ + const auto res = replacements->find(ty); + LUAU_ASSERT(res); + dontTraverseInto(*res); + return *res; +} + +TypePackId Replacer::clean(TypePackId tp) +{ + const auto res = replacementPacks->find(tp); + LUAU_ASSERT(res); + dontTraverseInto(*res); + return *res; +} + +bool Replacer::ignoreChildren(TypeId ty) +{ + if (get(ty)) + return true; + + if (auto ftv = get(ty)) + { + if (ftv->hasNoFreeOrGenericTypes) + return false; + + // If this function type quantifies over these generics, we don't want substitution to + // go any further into them because it's being shadowed in this case. + for (auto generic : ftv->generics) + if (replacements->contains(generic)) + return true; + + for (auto generic : ftv->genericPacks) + if (replacementPacks->contains(generic)) + return true; + } + + return false; +} + +bool Replacer::checkReplacementKeys() const +{ + for (const auto& [k, _] : *replacements) + { + if (k != follow(k)) + return false; + } + + for (const auto& [k, _] : *replacementPacks) + { + if (k != follow(k)) + return false; + } + + return true; +} + + +bool Instantiation2_DEPRECATED::ignoreChildren(TypeId ty) { if (get(ty)) return true; @@ -33,117 +113,150 @@ bool Instantiation2::ignoreChildren(TypeId ty) return false; } -bool Instantiation2::isDirty(TypeId ty) +bool Instantiation2_DEPRECATED::isDirty(TypeId ty) { return get(ty) && genericSubstitutions.contains(ty); } -bool Instantiation2::isDirty(TypePackId tp) +bool Instantiation2_DEPRECATED::isDirty(TypePackId tp) { return get(tp) && genericPackSubstitutions.contains(tp); } -TypeId Instantiation2::clean(TypeId ty) +TypeId Instantiation2_DEPRECATED::clean(TypeId ty) { - if (FFlag::LuauInstantiationUsesGenericPolarity2) - { - LUAU_ASSERT(subtyping && scope); - auto generic = get(ty); - LUAU_ASSERT(generic); - TypeId substTy = follow(genericSubstitutions[ty]); - const FreeType* ft = get(substTy); - - // violation of the substitution invariant if this is not a free type. - LUAU_ASSERT(ft); - - TypeId res; - if (is(FFlag::LuauInstantiationUsesGenericPolarityFollow ? follow(ft->lowerBound) : ft->lowerBound)) - { - // If the lower bound is never, assume that we can pick the - // upper bound, and that this will provide a reasonable type. - // - // If we have a mixed generic who's free type is totally - // unbound (the upper bound is `unknown` and the lower - // bound is `never`), then we instantiate it to `unknown`. - // This seems ... fine. - res = ft->upperBound; - } - else if (is(FFlag::LuauInstantiationUsesGenericPolarityFollow ? follow(ft->upperBound) : ft->upperBound)) - { - // If the upper bound is unknown, assume we can pick the - // lower bound, and that this will provide a reasonable - // type. - res = ft->lowerBound; - } - else - { - // Imagine that we have some set of bounds on a free type: - // - // Q <: 'a <: Z - // - // If we have a mixed generic, then the upper and lower bounds - // should inform what type to instantiate. In fact, we should - // pick the intersection between the two. If our bounds are - // coherent, then Q <: Z, meaning that Q & Z == Q. - // - // If `Q isSubtype(ft->lowerBound, ft->upperBound, NotNull{scope}); - res = r.isSubtype ? ft->lowerBound : ft->upperBound; - } + LUAU_ASSERT(subtyping && scope); + auto generic = get(ty); + LUAU_ASSERT(generic); + TypeId substTy = follow(genericSubstitutions[ty]); + const FreeType* ft = get(substTy); - // Instantiation should not traverse into the type that we are substituting for. - dontTraverseInto(res); + // violation of the substitution invariant if this is not a free type. + LUAU_ASSERT(ft); - return res; + TypeId res; + if (is(follow(ft->lowerBound))) + { + // If the lower bound is never, assume that we can pick the + // upper bound, and that this will provide a reasonable type. + // + // If we have a mixed generic who's free type is totally + // unbound (the upper bound is `unknown` and the lower + // bound is `never`), then we instantiate it to `unknown`. + // This seems ... fine. + res = ft->upperBound; + } + else if (is(follow(ft->upperBound))) + { + // If the upper bound is unknown, assume we can pick the + // lower bound, and that this will provide a reasonable + // type. + res = ft->lowerBound; } else { + // Imagine that we have some set of bounds on a free type: + // + // Q <: 'a <: Z + // + // If we have a mixed generic, then the upper and lower bounds + // should inform what type to instantiate. In fact, we should + // pick the intersection between the two. If our bounds are + // coherent, then Q <: Z, meaning that Q & Z == Q. + // + // If `Q isSubtype(ft->lowerBound, ft->upperBound, NotNull{scope}); + res = r.isSubtype ? ft->lowerBound : ft->upperBound; + } - TypeId substTy = follow(genericSubstitutions[ty]); - const FreeType* ft = get(substTy); - - // violation of the substitution invariant if this is not a free type. - LUAU_ASSERT(ft); - - // if we didn't learn anything about the lower bound, we pick the upper bound instead. - // we default to the lower bound which represents the most specific type for the free type. - TypeId res = get(ft->lowerBound) ? ft->upperBound : ft->lowerBound; - - // Instantiation should not traverse into the type that we are substituting for. - dontTraverseInto(res); + // Instantiation should not traverse into the type that we are substituting for. + dontTraverseInto(res); - return res; - } + return res; } -TypePackId Instantiation2::clean(TypePackId tp) +TypePackId Instantiation2_DEPRECATED::clean(TypePackId tp) { TypePackId res = genericPackSubstitutions[tp]; dontTraverseInto(res); return res; } -std::optional instantiate2_DEPRECATED( +void resolveGenericSubstitutions( TypeArena* arena, - DenseHashMap genericSubstitutions, - DenseHashMap genericPackSubstitutions, - TypeId ty + DenseHashMap& genericSubstitutions, + DenseHashMap& genericPackSubstitutions, + NotNull subtyping, + NotNull scope ) { - Instantiation2 instantiation{arena, std::move(genericSubstitutions), std::move(genericPackSubstitutions)}; - return instantiation.substitute(ty); -} + // Collect the set of original free type IDs from genericSubstitutions + // before we overwrite the map values. These are the types that may + // appear inside type pack substitutions and need to be resolved. + DenseHashSet originalFreeTypes{nullptr}; + for (auto& [_, v] : genericSubstitutions) + { + TypeId followed = follow(v); + if (get(followed)) + originalFreeTypes.insert(followed); + } -std::optional instantiate2_DEPRECATED( - TypeArena* arena, - DenseHashMap genericSubstitutions, - DenseHashMap genericPackSubstitutions, - TypePackId tp -) -{ - Instantiation2 instantiation{arena, std::move(genericSubstitutions), std::move(genericPackSubstitutions)}; - return instantiation.substitute(tp); + auto pickBound = [&](const FreeType* ft) + { + if (is(follow(ft->lowerBound))) + return ft->upperBound; + else if (is(follow(ft->upperBound))) + return ft->lowerBound; + else + { + auto r = subtyping->isSubtype(ft->lowerBound, ft->upperBound, scope); + return r.isSubtype ? ft->lowerBound : ft->upperBound; + } + }; + + // Resolve each generic type substitution: update the map entry to the + // best concrete type, leaving the original free type node untouched. + // Other parts of the solver may still reference it. + for (auto& [_, ty] : genericSubstitutions) + { + ty = follow(ty); + if (auto ft = get(ty)) + ty = pickBound(ft); + } + + // Resolve free types that appear inside type pack substitutions. + // We build new packs rather than mutating existing ones so that other + // references to the same pack are not affected. + for (auto& [_, packSubst] : genericPackSubstitutions) + { + TypePackId followed = follow(packSubst); + if (auto pack = get(followed)) + { + bool changed = false; + std::vector newHead; + newHead.reserve(pack->head.size()); + auto iter = begin(followed); + auto endIter = end(followed); + + while (iter != endIter) + { + TypeId ty = follow(*iter); + if (auto ft = get(ty); ft && originalFreeTypes.contains(ty)) + { + newHead.push_back(pickBound(ft)); + changed = true; + } + else + newHead.push_back(ty); + + ++iter; + } + + if (changed) + packSubst = arena->addTypePack(TypePack{std::move(newHead), iter.tail()}); + } + } } std::optional instantiate2( @@ -155,7 +268,14 @@ std::optional instantiate2( TypeId ty ) { - Instantiation2 instantiation{arena, std::move(genericSubstitutions), std::move(genericPackSubstitutions), subtyping, scope}; + if (FFlag::LuauHigherOrderGenericInference) + { + resolveGenericSubstitutions(arena, genericSubstitutions, genericPackSubstitutions, subtyping, scope); + Replacer r{NotNull{arena}, NotNull{&genericSubstitutions}, NotNull{&genericPackSubstitutions}}; + return r.substitute(ty); + } + + Instantiation2_DEPRECATED instantiation{arena, std::move(genericSubstitutions), std::move(genericPackSubstitutions), subtyping, scope}; return instantiation.substitute(ty); } @@ -168,7 +288,14 @@ std::optional instantiate2( TypePackId tp ) { - Instantiation2 instantiation{arena, std::move(genericSubstitutions), std::move(genericPackSubstitutions), subtyping, scope}; + if (FFlag::LuauHigherOrderGenericInference) + { + resolveGenericSubstitutions(arena, genericSubstitutions, genericPackSubstitutions, subtyping, scope); + Replacer r{NotNull{arena}, NotNull{&genericSubstitutions}, NotNull{&genericPackSubstitutions}}; + return r.substitute(tp); + } + + Instantiation2_DEPRECATED instantiation{arena, std::move(genericSubstitutions), std::move(genericPackSubstitutions), subtyping, scope}; return instantiation.substitute(tp); } diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 3ffeb9a3..6ff6ab7a 100644 --- a/Analysis/src/IostreamHelpers.cpp +++ b/Analysis/src/IostreamHelpers.cpp @@ -145,6 +145,8 @@ static void errorToString(std::ostream& stream, const T& err) stream << "}"; } + else if constexpr (std::is_same_v) + stream << "CyclicModuleGraphTooLarge { moduleCount = " << err.moduleCount << " }"; else if constexpr (std::is_same_v) stream << "IllegalRequire { " << err.moduleName << ", reason = " << err.reason << " }"; else if constexpr (std::is_same_v) @@ -235,6 +237,8 @@ static void errorToString(std::ostream& stream, const T& err) stream << "UnexpectedTypePackInSubtyping { tp = '" + toString(err.tp) + "' }"; else if constexpr (std::is_same_v) stream << "UserDefinedTypeFunctionError { " << err.message << " }"; + else if constexpr (std::is_same_v) + stream << "BuiltInTypeFunctionError { " << toString(err.error) << " }"; else if constexpr (std::is_same_v) stream << "ReservedIdentifier { " << err.name << " }"; else if constexpr (std::is_same_v) diff --git a/Analysis/src/IterativeTypeVisitor.cpp b/Analysis/src/IterativeTypeVisitor.cpp index 49e7fa04..d72c3843 100644 --- a/Analysis/src/IterativeTypeVisitor.cpp +++ b/Analysis/src/IterativeTypeVisitor.cpp @@ -2,6 +2,7 @@ #include "Luau/IterativeTypeVisitor.h" LUAU_FASTINT(LuauVisitRecursionLimit) +LUAU_FASTFLAG(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) namespace Luau { @@ -287,6 +288,12 @@ void IterativeTypeVisitor::process(TypeId ty) traverse(ftv->lowerBound); traverse(ftv->upperBound); + + if (FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) + { + if (ftv->primitiveType) + traverse(*ftv->primitiveType); + } } } else if (auto gtv = get(ty)) diff --git a/Analysis/src/JsonEmitter.cpp b/Analysis/src/JsonEmitter.cpp index 9c8a7af9..9c41102c 100644 --- a/Analysis/src/JsonEmitter.cpp +++ b/Analysis/src/JsonEmitter.cpp @@ -155,16 +155,36 @@ void write(JsonEmitter& emitter, std::string_view sv) for (char c : sv) { - if (c == '"') + switch (c) + { + case '"': emitter.writeRaw("\\\""); - else if (c == '\\') + break; + case '\\': emitter.writeRaw("\\\\"); - else if (c == '\n') + break; + case '\b': + emitter.writeRaw("\\b"); + break; + case '\f': + emitter.writeRaw("\\f"); + break; + case '\n': emitter.writeRaw("\\n"); - else if (c < ' ') - emitter.writeRaw(format("\\u%04x", c)); - else - emitter.writeRaw(c); + break; + case '\r': + emitter.writeRaw("\\r"); + break; + case '\t': + emitter.writeRaw("\\t"); + break; + default: + if (static_cast(c) < 0x20) + emitter.writeRaw(format("\\u%04x", static_cast(c))); + else + emitter.writeRaw(c); + break; + } } emitter.writeRaw('\"'); diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index f025bd78..2afbb8a7 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -14,11 +14,6 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) -LUAU_FASTFLAG(LuauSolverV2) - -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSyntax) -LUAU_FASTFLAG(LuauAnalysisUsesSolverMode) - namespace Luau { @@ -122,6 +117,7 @@ static bool similar(AstExpr* lhs, AstExpr* rhs) CASE(AstExprConstantNil) return true; CASE(AstExprConstantBool) return le->value == re->value; CASE(AstExprConstantNumber) return le->value == re->value; + CASE(AstExprConstantInteger) return le->value == re->value; CASE(AstExprConstantString) return le->value.size == re->value.size && memcmp(le->value.data, re->value.data, le->value.size) == 0; CASE(AstExprLocal) return le->local == re->local; CASE(AstExprGlobal) return le->name == re->name; @@ -193,7 +189,6 @@ static bool similar(AstExpr* lhs, AstExpr* rhs) } CASE(AstExprInstantiate) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); return similar(le->expr, re->expr); } else @@ -1182,7 +1177,7 @@ class LintUnknownType : AstVisitor { Kind_Unknown, Kind_Primitive, // primitive type supported by VM - boolean/userdata/etc. No differentiation between types of userdata. - Kind_Vector, // 'vector' but only used when type is used + Kind_Vector, // TODO: deprecated and not set, but read in 'visit' Kind_Userdata, // custom userdata type }; @@ -1193,7 +1188,7 @@ class LintUnknownType : AstVisitor return Kind_Primitive; if (name == "vector") - return Kind_Vector; + return Kind_Primitive; if (std::optional maybeTy = context->scope->lookupType(name)) return Kind_Userdata; @@ -1285,7 +1280,7 @@ class LintForRange : AstVisitor Location rangeLocation(node->from->location, node->to->location); // for i=#t,1 do - if (fu && fu->op == AstExprUnary::Len && tc && tc->value == 1.0) + if (fu && fu->op == AstExprUnary::Op::Len && tc && tc->value == 1.0) emitWarning( *context, LintWarning::Code_ForRange, rangeLocation, "For loop should iterate backwards; did you forget to specify -1 as step?" ); @@ -1305,10 +1300,10 @@ class LintForRange : AstVisitor tc->value ); // for i=0,#t do - else if (fc && tu && fc->value == 0.0 && tu->op == AstExprUnary::Len) + else if (fc && tu && fc->value == 0.0 && tu->op == AstExprUnary::Op::Len) emitWarning(*context, LintWarning::Code_ForRange, rangeLocation, "For loop starts at 0, but arrays start at 1"); // for i=#t,0 do - else if (fu && fu->op == AstExprUnary::Len && tc && tc->value == 0.0) + else if (fu && fu->op == AstExprUnary::Op::Len && tc && tc->value == 0.0) emitWarning( *context, LintWarning::Code_ForRange, @@ -1915,7 +1910,7 @@ class LintTableLiteral : AstVisitor int count = 0; for (const AstExprTable::Item& item : node->items) - if (item.kind == AstExprTable::Item::List) + if (item.kind == AstExprTable::Item::Kind::List) count++; DenseHashMap*, int, AstArrayPredicate, AstArrayPredicate> names(nullptr); @@ -1993,69 +1988,8 @@ class LintTableLiteral : AstVisitor Location location; }; - if (FFlag::LuauAnalysisUsesSolverMode && context->module->checkedInNewSolver) - { - DenseHashMap names(AstName{}); - - for (const AstTableProp& item : node->props) - { - Rec* rec = names.find(item.name); - if (!rec) - { - names[item.name] = Rec{item.access, item.location}; - continue; - } - - if (int(rec->access) & int(item.access)) - { - if (rec->access == item.access) - emitWarning( - *context, - LintWarning::Code_TableLiteral, - item.location, - "Table type field '%s' is a duplicate; previously defined at line %d", - item.name.value, - rec->location.begin.line + 1 - ); - else if (rec->access == AstTableAccess::ReadWrite) - emitWarning( - *context, - LintWarning::Code_TableLiteral, - item.location, - "Table type field '%s' is already read-write; previously defined at line %d", - item.name.value, - rec->location.begin.line + 1 - ); - else if (rec->access == AstTableAccess::Read) - emitWarning( - *context, - LintWarning::Code_TableLiteral, - rec->location, - "Table type field '%s' already has a read type defined at line %d", - item.name.value, - rec->location.begin.line + 1 - ); - else if (rec->access == AstTableAccess::Write) - emitWarning( - *context, - LintWarning::Code_TableLiteral, - rec->location, - "Table type field '%s' already has a write type defined at line %d", - item.name.value, - rec->location.begin.line + 1 - ); - else - LUAU_ASSERT(!"Unreachable"); - } - else - rec->access = AstTableAccess(int(rec->access) | int(item.access)); - } - - return true; - } - else if (FFlag::LuauSolverV2) + if (context->module->checkedInNewSolver) { - DenseHashMap names(AstName{}); for (const AstTableProp& item : node->props) @@ -2675,7 +2609,7 @@ class LintTableOperations : AstVisitor bool visit(AstExprUnary* node) override { - if (node->op == AstExprUnary::Len) + if (node->op == AstExprUnary::Op::Len) checkIndexer(node, node->expr, "#"); return true; @@ -2849,7 +2783,7 @@ class LintTableOperations : AstVisitor bool isLength(AstExpr* expr, AstExpr* table) { AstExprUnary* n = expr->as(); - return n && n->op == AstExprUnary::Len && similar(n->expr, table); + return n && n->op == AstExprUnary::Op::Len && similar(n->expr, table); } size_t getReturnCount(TypeId ty) @@ -3245,6 +3179,9 @@ class LintIntegerParsing : AstVisitor "Hexadecimal number literal exceeded available precision and was truncated to 2^64" ); break; + case ConstantNumberParseResult::IntOverflow: + emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, "Integer number literal was clamped because it was out of range"); + break; } return true; @@ -3280,7 +3217,7 @@ class LintComparisonPrecedence : AstVisitor { AstExprUnary* expr = node->as(); - return expr && expr->op == AstExprUnary::Not; + return expr && expr->op == AstExprUnary::Op::Not; } bool visit(AstExprBinary* node) override @@ -3476,7 +3413,7 @@ static void lintComments(LintContext& context, const std::vector& ho { const char* level = hc.content.c_str() + notspace; - if (strcmp(level, "0") && strcmp(level, "1") && strcmp(level, "2")) + if (strcmp(level, "0") != 0 && strcmp(level, "1") != 0 && strcmp(level, "2") != 0) emitWarning( context, LintWarning::Code_CommentDirective, diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index 4a734935..e3dfedd5 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -14,8 +14,8 @@ #include -LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAGVARIABLE(LuauAnalysisUsesSolverMode) +LUAU_FASTFLAGVARIABLE(LuauDoNotExportBrokenTypeFunction) +LUAU_FASTFLAG(LuauCloneTypeFunctionFromForeignArena) namespace Luau { @@ -117,19 +117,9 @@ struct ClonePublicInterface : Substitution { NotNull builtinTypes; NotNull module; - // NOTE: This can be made non-optional after - // LuauUseWorkspacePropToChooseSolver is clipped. - std::optional solverMode{std::nullopt}; + SolverMode solverMode; bool internalTypeEscaped = false; - ClonePublicInterface(const TxnLog* log, NotNull builtinTypes, Module* module) - : Substitution(log, &module->interfaceTypes) - , builtinTypes(builtinTypes) - , module(module) - { - LUAU_ASSERT(module); - } - ClonePublicInterface(const TxnLog* log, NotNull builtinTypes, Module* module, SolverMode solverMode) : Substitution(log, &module->interfaceTypes) , builtinTypes(builtinTypes) @@ -141,19 +131,12 @@ struct ClonePublicInterface : Substitution bool isNewSolver() const { - if (FFlag::LuauAnalysisUsesSolverMode) - { - return solverMode == SolverMode::New; - } - else - { - return FFlag::LuauSolverV2 || solverMode == SolverMode::New; - } + return solverMode == SolverMode::New; } bool isDirty(TypeId ty) override { - if (ty->owningArena == &module->internalTypes) + if (ty->owningArena == module->internalTypes.get()) return true; if (const FunctionType* ftv = get(ty)) @@ -165,12 +148,12 @@ struct ClonePublicInterface : Substitution bool isDirty(TypePackId tp) override { - return tp->owningArena == &module->internalTypes; + return tp->owningArena == module->internalTypes.get(); } bool ignoreChildrenVisit(TypeId ty) override { - if (ty->owningArena != &module->internalTypes) + if (ty->owningArena != module->internalTypes.get()) return true; return false; @@ -178,7 +161,7 @@ struct ClonePublicInterface : Substitution bool ignoreChildrenVisit(TypePackId tp) override { - if (tp->owningArena != &module->internalTypes) + if (tp->owningArena != module->internalTypes.get()) return true; return false; @@ -222,6 +205,16 @@ struct ClonePublicInterface : Substitution { genericty->scope = nullptr; } + else if (FFlag::LuauCloneTypeFunctionFromForeignArena) + { + if (auto tfit = get(ty); tfit && tfit->state == TypeFunctionInstanceState::Stuck) + result = arena->addType(ErrorType{ty}); + } + else if (auto tfit = get(ty); + FFlag::LuauDoNotExportBrokenTypeFunction && tfit && tfit->state != TypeFunctionInstanceState::Solved) + { + result = builtinTypes->errorType; + } } return result; @@ -313,7 +306,8 @@ struct ClonePublicInterface : Substitution Module::~Module() { unfreeze(interfaceTypes); - unfreeze(internalTypes); + if (internalTypes) + unfreeze(*internalTypes); } void Module::clonePublicInterface(NotNull builtinTypes, InternalErrorReporter& ice, SolverMode mode) @@ -378,4 +372,118 @@ ScopePtr Module::getModuleScope() const return scopes.front().second; } +void synthesizeExportReturn(NotNull builtinTypes, NotNull module) +{ + LUAU_ASSERT(module->root); + + ScopePtr moduleScope = module->getModuleScope(); + TableType::Props props; + + auto lookupExportedBindingType = [&](AstLocal* local) -> TypeId + { + NotNull scope = moduleScope->findNarrowestScopeContaining(local->location); + + if (std::optional> binding = scope->lookupEx(Symbol{local})) + return follow(binding->first->typeId); + + return builtinTypes->errorType; + }; + + auto lookupExprType = [&](AstExpr* expr) -> TypeId + { + if (TypeId* ty = module->astTypes.find(expr)) + return follow(*ty); + + return builtinTypes->errorType; + }; + + DenseHashSet exportedLocals{nullptr}; + + for (AstStat* statement : module->root->body) + { + if (AstStatLocal* localStat = statement->as()) + { + if (!localStat->isExported) + continue; + + for (size_t i = 0; i < localStat->vars.size; ++i) + { + AstLocal* local = localStat->vars.data[i]; + exportedLocals.insert(local); + + if (localStat->vars.size != localStat->values.size || i >= localStat->values.size) + { + props[local->name.value] = lookupExportedBindingType(local); + } + else + { + props[local->name.value] = Property::readonly(lookupExprType(localStat->values.data[i])); + } + + props[local->name.value].location = local->location; + } + } + else if (AstStatLocalFunction* localFunction = statement->as()) + { + if (!localFunction->name->isExported) + continue; + + props[localFunction->name->name.value] = Property::readonly(lookupExportedBindingType(localFunction->name)); + props[localFunction->name->name.value].location = localFunction->name->location; + } + else if (AstStatAssign* assign = statement->as()) + { + for (size_t i = 0; i < assign->vars.size; ++i) + { + AstExprLocal* exprLocal = assign->vars.data[i]->as(); + if (!exprLocal || !exportedLocals.contains(exprLocal->local)) + continue; + + if (assign->vars.size != assign->values.size || i >= assign->values.size) + { + props[exprLocal->local->name.value] = lookupExportedBindingType(exprLocal->local); + } + else + { + props[exprLocal->local->name.value] = Property::readonly(lookupExprType(assign->values.data[i])); + } + + props[exprLocal->local->name.value].location = exprLocal->local->location; + } + } + else if (AstStatFunction* funcStat = statement->as()) + { + AstExprLocal* exprLocal = funcStat->name->as(); + if (exprLocal && exportedLocals.contains(exprLocal->local)) + { + props[exprLocal->local->name.value] = Property::readonly(lookupExprType(funcStat->func)); + props[exprLocal->local->name.value].location = exprLocal->local->location; + } + } + else if (FFlag::DebugLuauUserDefinedClasses) + { + if (AstStatClass* classStat = statement->as()) + { + if (!classStat->exported) + continue; + + TypeId ty = builtinTypes->errorType; + if (auto found = moduleScope->lookup(Symbol{classStat->name->name})) + ty = follow(*found); + + props[classStat->name->name.value] = Property::readonly(ty); + props[classStat->name->name.value].location = classStat->name->location; + } + } + } + + if (props.empty()) + return; + + TableType tbl{props, std::nullopt, moduleScope->level, TableState::Sealed}; + tbl.definitionModuleName = module->name; + TypeId exports = module->internalTypes->addType(std::move(tbl)); + moduleScope->returnType = module->internalTypes->addTypePack({exports}); +} + } // namespace Luau diff --git a/Analysis/src/NativeStackGuard.cpp b/Analysis/src/NativeStackGuard.cpp index 3a5f9a41..c0f9c424 100644 --- a/Analysis/src/NativeStackGuard.cpp +++ b/Analysis/src/NativeStackGuard.cpp @@ -4,8 +4,6 @@ #include "Luau/Common.h" #include -LUAU_FASTFLAGVARIABLE(LuauUseNativeStackGuard); - // The minimum number of available bytes in the stack's address space. // If we have less, we want to fail and exit rather than risking an overflow. LUAU_FASTINTVARIABLE(LuauStackGuardThreshold, 1024); @@ -23,15 +21,12 @@ NativeStackGuard::NativeStackGuard() : high(0) , low(0) { - if (!FFlag::LuauUseNativeStackGuard) - return; - GetCurrentThreadStackLimits((PULONG_PTR)&low, (PULONG_PTR)&high); } bool NativeStackGuard::isOk() const { - if (!FFlag::LuauUseNativeStackGuard || FInt::LuauStackGuardThreshold <= 0) + if (FInt::LuauStackGuardThreshold <= 0) return true; const uintptr_t sp = uintptr_t(_AddressOfReturnAddress()); @@ -63,9 +58,6 @@ NativeStackGuard::NativeStackGuard() : high(0) , low(0) { - if (!FFlag::LuauUseNativeStackGuard) - return; - pthread_t self = pthread_self(); char* addr = static_cast(pthread_get_stackaddr_np(self)); size_t size = pthread_get_stacksize_np(self); @@ -76,7 +68,7 @@ NativeStackGuard::NativeStackGuard() bool NativeStackGuard::isOk() const { - if (!FFlag::LuauUseNativeStackGuard || FInt::LuauStackGuardThreshold <= 0) + if (FInt::LuauStackGuardThreshold <= 0) return true; const uintptr_t sp = uintptr_t(__builtin_frame_address(0)); diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 8728b92c..ad7760c5 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -23,6 +23,7 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTINTVARIABLE(LuauNonStrictTypeCheckerRecursionLimit, 300) LUAU_FASTFLAGVARIABLE(LuauAddRecursionCounterToNonStrictTypeChecker) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) namespace Luau { @@ -234,7 +235,7 @@ struct NonStrictTypeChecker if (noTypeFunctionErrors.find(instance)) return instance; - TypeFunctionContext context{arena, builtinTypes, stack.back(), NotNull{&normalizer}, typeFunctionRuntime, ice, limits}; + TypeFunctionContext context{arena, builtinTypes, stack.back(), NotNull{&normalizer}, typeFunctionRuntime, ice, limits, NotNull{&subtyping}}; ErrorVec errors = reduceTypeFunctions(instance, location, NotNull{&context}, true).errors; if (errors.empty()) @@ -301,6 +302,8 @@ struct NonStrictTypeChecker return visit(s); else if (auto s = stat->as()) return visit(s); + else if (auto s = stat->as()) + return visit(s); else if (auto s = stat->as()) return visit(s); else @@ -501,6 +504,21 @@ struct NonStrictTypeChecker return {}; } + NonStrictContext visit(AstStatClass* declClass) + { + for (auto prop : declClass->members) + { + if (auto property = get_if(&prop)) + visit(property->ty); + else if (auto method = get_if(&prop)) + visit(method->function); + else + LUAU_ASSERT(!"Unknown class field"); + } + + return {}; + } + NonStrictContext visit(AstStatError* error) { for (AstStat* stat : error->statements) @@ -530,6 +548,8 @@ struct NonStrictTypeChecker return visit(e); else if (auto e = expr->as()) return visit(e); + else if (auto e = expr->as()) + return visit(e); else if (auto e = expr->as()) return visit(e); else if (auto e = expr->as()) @@ -589,6 +609,11 @@ struct NonStrictTypeChecker return {}; } + NonStrictContext visit(AstExprConstantInteger* expr) + { + return {}; + } + NonStrictContext visit(AstExprConstantString* expr) { return {}; @@ -1200,7 +1225,10 @@ struct NonStrictTypeChecker SubtypingResult r = subtyping.isSubtype(actualType, *contextTy, scope); if (r.normalizationTooComplex) reportError(NormalizationTooComplex{}, fragment->location); - if (r.isSubtype) + // If this subtype test passed and we did not see an error + // suppressing bit, then return this as the type that will + // error at runtime. + if (r.isSubtype && !r.isErrorSuppressing) return {actualType}; } } @@ -1260,7 +1288,7 @@ void checkNonStrict( { LUAU_TIMETRACE_SCOPE("checkNonStrict", "Typechecking"); - NonStrictTypeChecker typeChecker{NotNull{&module->internalTypes}, builtinTypes, typeFunctionRuntime, ice, unifierState, dfg, limits, module}; + NonStrictTypeChecker typeChecker{NotNull{module->internalTypes.get()}, builtinTypes, typeFunctionRuntime, ice, unifierState, dfg, limits, module}; typeChecker.visit(sourceModule.root); unfreeze(module->interfaceTypes); copyErrors(module->errors, module->interfaceTypes, builtinTypes); diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 67e0353c..8ac5a4c6 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -20,8 +20,9 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant) LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINTVARIABLE(LuauNormalizerInitialFuel, 3000) - -LUAU_FASTFLAGVARIABLE(LuauExternTypesNormalizeWithShapes) +LUAU_FASTFLAG(LuauIntegerType2) +LUAU_FASTFLAGVARIABLE(LuauAllowIntersectionOfOneTableWithExtern) +LUAU_FASTFLAGVARIABLE(LuauAlwaysIntersectTablesWithTables) namespace Luau { @@ -142,8 +143,7 @@ void NormalizedExternType::resetToNever() { ordering.clear(); externTypes.clear(); - if (FFlag::LuauExternTypesNormalizeWithShapes) - shapeExtensions.clear(); + shapeExtensions.clear(); } bool NormalizedExternType::isNever() const @@ -175,6 +175,7 @@ NormalizedType::NormalizedType(NotNull builtinTypes) , errors(builtinTypes->neverType) , nils(builtinTypes->neverType) , numbers(builtinTypes->neverType) + , integers(builtinTypes->neverType) , strings{NormalizedStringType::never} , threads(builtinTypes->neverType) , buffers(builtinTypes->neverType) @@ -187,14 +188,23 @@ bool NormalizedType::isUnknown() const return true; // Otherwise, we can still be unknown! - bool hasAllPrimitives = isPrim(booleans, PrimitiveType::Boolean) && isPrim(nils, PrimitiveType::NilType) && isNumber(numbers) && - strings.isString() && isThread(threads) && isBuffer(buffers); + bool hasAllPrimitives; + if (FFlag::LuauIntegerType2) + { + hasAllPrimitives = isPrim(booleans, PrimitiveType::Boolean) && isPrim(nils, PrimitiveType::NilType) && isNumber(numbers) && + strings.isString() && isThread(threads) && isBuffer(buffers) && isInteger(integers); + } + else + { + hasAllPrimitives = isPrim(booleans, PrimitiveType::Boolean) && isPrim(nils, PrimitiveType::NilType) && isNumber(numbers) && + strings.isString() && isThread(threads) && isBuffer(buffers); + } - // Check is class + // Check is extern type bool isTopExternType = false; for (const auto& [t, disj] : externTypes.externTypes) { - if (auto ct = get(t)) + if (get(t)) { if (t == builtinTypes->externType && disj.empty()) { @@ -219,20 +229,32 @@ bool NormalizedType::isUnknown() const bool NormalizedType::isExactlyNumber() const { - return hasNumbers() && !hasTops() && !hasBooleans() && !hasExternTypes() && !hasErrors() && !hasNils() && !hasStrings() && !hasThreads() && - !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars(); + if (FFlag::LuauIntegerType2) + return hasNumbers() && !hasTops() && !hasBooleans() && !hasExternTypes() && !hasErrors() && !hasNils() && !hasStrings() && !hasThreads() && + !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars() && !hasIntegers(); + else + return hasNumbers() && !hasTops() && !hasBooleans() && !hasExternTypes() && !hasErrors() && !hasNils() && !hasStrings() && !hasThreads() && + !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars(); } bool NormalizedType::isSubtypeOfString() const { - return hasStrings() && !hasTops() && !hasBooleans() && !hasExternTypes() && !hasErrors() && !hasNils() && !hasNumbers() && !hasThreads() && - !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars(); + if (FFlag::LuauIntegerType2) + return hasStrings() && !hasTops() && !hasBooleans() && !hasExternTypes() && !hasErrors() && !hasNils() && !hasNumbers() && !hasThreads() && + !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars() && !hasIntegers(); + else + return hasStrings() && !hasTops() && !hasBooleans() && !hasExternTypes() && !hasErrors() && !hasNils() && !hasNumbers() && !hasThreads() && + !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars(); } bool NormalizedType::isSubtypeOfBooleans() const { - return hasBooleans() && !hasTops() && !hasExternTypes() && !hasErrors() && !hasNils() && !hasNumbers() && !hasStrings() && !hasThreads() && - !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars(); + if (FFlag::LuauIntegerType2) + return hasBooleans() && !hasTops() && !hasExternTypes() && !hasErrors() && !hasNils() && !hasNumbers() && !hasStrings() && !hasThreads() && + !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars() && !hasIntegers(); + else + return hasBooleans() && !hasTops() && !hasExternTypes() && !hasErrors() && !hasNils() && !hasNumbers() && !hasStrings() && !hasThreads() && + !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars(); } bool NormalizedType::shouldSuppressErrors() const @@ -284,6 +306,14 @@ bool NormalizedType::hasNumbers() const return !get(numbers); } +bool NormalizedType::hasIntegers() const +{ + if (FFlag::LuauIntegerType2) + return get(integers) == nullptr; + else + return false; +} + bool NormalizedType::hasStrings() const { return !strings.isNever(); @@ -324,8 +354,12 @@ bool NormalizedType::isFalsy() const hasAFalse = !bs->value; } - return (hasAFalse || hasNils()) && (!hasTops() && !hasExternTypes() && !hasErrors() && !hasNumbers() && !hasStrings() && !hasThreads() && - !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars()); + if (FFlag::LuauIntegerType2) + return (hasAFalse || hasNils()) && (!hasTops() && !hasExternTypes() && !hasErrors() && !hasNumbers() && !hasStrings() && !hasThreads() && + !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars() && !hasIntegers()); + else + return (hasAFalse || hasNils()) && (!hasTops() && !hasExternTypes() && !hasErrors() && !hasNumbers() && !hasStrings() && !hasThreads() && + !hasBuffers() && !hasTables() && !hasFunctions() && !hasTyvars()); } bool NormalizedType::isTruthy() const @@ -338,16 +372,30 @@ bool NormalizedType::isNil() const if (!hasNils()) return false; - return !hasTops() && !hasBooleans() && !hasExternTypes() && !hasNumbers() && !hasStrings() && !hasThreads() && !hasBuffers() && !hasTables() && - !hasFunctions() && !hasTyvars(); + if (FFlag::LuauIntegerType2) + return !hasTops() && !hasBooleans() && !hasExternTypes() && !hasNumbers() && !hasStrings() && !hasThreads() && !hasBuffers() && + !hasTables() && !hasFunctions() && !hasTyvars() && !hasIntegers(); + else + return !hasTops() && !hasBooleans() && !hasExternTypes() && !hasNumbers() && !hasStrings() && !hasThreads() && !hasBuffers() && + !hasTables() && !hasFunctions() && !hasTyvars(); } static bool isShallowInhabited(const NormalizedType& norm) { // This test is just a shallow check, for example it returns `true` for `{ p : never }` - return !get(norm.tops) || !get(norm.booleans) || !norm.externTypes.isNever() || !get(norm.errors) || - !get(norm.nils) || !get(norm.numbers) || !norm.strings.isNever() || !get(norm.threads) || - !get(norm.buffers) || !norm.functions.isNever() || !norm.tables.empty() || !norm.tyvars.empty(); + if (FFlag::LuauIntegerType2) + { + return !get(norm.tops) || !get(norm.booleans) || !norm.externTypes.isNever() || !get(norm.errors) || + !get(norm.nils) || !get(norm.numbers) || !norm.strings.isNever() || !get(norm.threads) || + (get(norm.buffers) == nullptr) || !norm.functions.isNever() || !norm.tables.empty() || !norm.tyvars.empty() || + (get(norm.integers) == nullptr); + } + else + { + return !get(norm.tops) || !get(norm.booleans) || !norm.externTypes.isNever() || !get(norm.errors) || + !get(norm.nils) || !get(norm.numbers) || !norm.strings.isNever() || !get(norm.threads) || + !get(norm.buffers) || !norm.functions.isNever() || !norm.tables.empty() || !norm.tyvars.empty(); + } } NormalizationResult Normalizer::isInhabited(const NormalizedType* norm) @@ -372,10 +420,20 @@ NormalizationResult Normalizer::isInhabited(const NormalizedType* norm, Set(norm->tops) || !get(norm->booleans) || !get(norm->errors) || !get(norm->nils) || - !get(norm->numbers) || !get(norm->threads) || !get(norm->buffers) || !norm->externTypes.isNever() || - !norm->strings.isNever() || !norm->functions.isNever()) - return NormalizationResult::True; + if (FFlag::LuauIntegerType2) + { + if (!get(norm->tops) || !get(norm->booleans) || !get(norm->errors) || !get(norm->nils) || + !get(norm->numbers) || !get(norm->threads) || !get(norm->buffers) || !norm->externTypes.isNever() || + !get(norm->integers) || !norm->strings.isNever() || !norm->functions.isNever()) + return NormalizationResult::True; + } + else + { + if (!get(norm->tops) || !get(norm->booleans) || !get(norm->errors) || !get(norm->nils) || + !get(norm->numbers) || !get(norm->threads) || !get(norm->buffers) || !norm->externTypes.isNever() || + !norm->strings.isNever() || !norm->functions.isNever()) + return NormalizationResult::True; + } for (const auto& [_, intersect] : norm->tyvars) { @@ -609,6 +667,16 @@ static bool isNormalizedNumber(TypeId ty) return false; } +static bool isNormalizedInteger(TypeId ty) +{ + if (get(ty)) + return true; + else if (const PrimitiveType* ptv = get(ty)) + return ptv->type == PrimitiveType::Integer; + else + return false; +} + static bool isNormalizedString(const NormalizedStringType& ty) { if (ty.isString()) @@ -772,6 +840,8 @@ static void assertInvariant(const NormalizedType& norm) LUAU_ASSERT(isNormalizedError(norm.errors)); LUAU_ASSERT(isNormalizedNil(norm.nils)); LUAU_ASSERT(isNormalizedNumber(norm.numbers)); + if (FFlag::LuauIntegerType2) + LUAU_ASSERT(isNormalizedInteger(norm.integers)); LUAU_ASSERT(isNormalizedString(norm.strings)); LUAU_ASSERT(isNormalizedThread(norm.threads)); LUAU_ASSERT(isNormalizedBuffer(norm.buffers)); @@ -932,6 +1002,7 @@ void Normalizer::clearNormal(NormalizedType& norm) norm.errors = builtinTypes->neverType; norm.nils = builtinTypes->neverType; norm.numbers = builtinTypes->neverType; + norm.integers = builtinTypes->neverType; norm.strings.resetToNever(); norm.threads = builtinTypes->neverType; norm.buffers = builtinTypes->neverType; @@ -1296,11 +1367,8 @@ void Normalizer::unionExternTypes(NormalizedExternType& heres, const NormalizedE { heres.pushPair(thereTy, thereNegations); - if (FFlag::LuauExternTypesNormalizeWithShapes) - { - for (TypeId shape : theres.shapeExtensions) - heres.shapeExtensions.insert(shape); - } + for (TypeId shape : theres.shapeExtensions) + heres.shapeExtensions.insert(shape); } } } @@ -1666,6 +1734,8 @@ NormalizationResult Normalizer::unionNormals(NormalizedType& here, const Normali here.errors = (get(there.errors) ? here.errors : there.errors); here.nils = (get(there.nils) ? here.nils : there.nils); here.numbers = (get(there.numbers) ? here.numbers : there.numbers); + if (FFlag::LuauIntegerType2) + here.integers = (get(there.integers) ? here.integers : there.integers); unionStrings(here.strings, there.strings); here.threads = (get(there.threads) ? here.threads : there.threads); here.buffers = (get(there.buffers) ? here.buffers : there.buffers); @@ -1821,6 +1891,8 @@ NormalizationResult Normalizer::unionNormalWithTy( here.nils = there; else if (ptv->type == PrimitiveType::Number) here.numbers = there; + else if (FFlag::LuauIntegerType2 && (ptv->type == PrimitiveType::Integer)) + here.integers = there; else if (ptv->type == PrimitiveType::String) here.strings.resetToString(); else if (ptv->type == PrimitiveType::Thread) @@ -1862,6 +1934,10 @@ NormalizationResult Normalizer::unionNormalWithTy( std::optional tn; std::shared_ptr thereNormal = normalize(ntv->ty); + + if (!thereNormal) + return NormalizationResult::False; + tn = negateNormal(*thereNormal); if (!tn) @@ -1952,6 +2028,8 @@ std::optional Normalizer::negateNormal(const NormalizedType& her result.nils = get(here.nils) ? builtinTypes->nilType : builtinTypes->neverType; result.numbers = get(here.numbers) ? builtinTypes->numberType : builtinTypes->neverType; + if (FFlag::LuauIntegerType2) + result.integers = get(here.integers) ? builtinTypes->integerType : builtinTypes->neverType; result.strings = here.strings; result.strings.isCofinite = !result.strings.isCofinite; @@ -2047,6 +2125,9 @@ void Normalizer::subtractPrimitive(NormalizedType& here, TypeId ty) case PrimitiveType::Number: here.numbers = builtinTypes->neverType; break; + case PrimitiveType::Integer: + here.integers = builtinTypes->neverType; + break; case PrimitiveType::String: here.strings.resetToNever(); break; @@ -2347,8 +2428,6 @@ void Normalizer::intersectExternTypesWithExternType(NormalizedExternType& heres, void Normalizer::intersectExternTypesWithShape(NormalizedExternType& heres, TypeId there) { - LUAU_ASSERT(FFlag::LuauExternTypesNormalizeWithShapes); - consumeFuel(); // in this case, we want to take the foreign function types we have here, and we want to intersect a table type into them. @@ -2788,14 +2867,26 @@ std::optional Normalizer::intersectionOfTables(TypeId here, TypeId there if (httv->indexer && tttv->indexer) { - // TODO: What should intersection of indexes be? TypeId index = unionType(httv->indexer->indexType, tttv->indexer->indexType); - TypeId indexResult = intersectionType(httv->indexer->indexResultType, tttv->indexer->indexResultType); + TableIndexer idx{index, {}}; + + if (httv->indexer->isReadOnly && tttv->indexer->isReadOnly) + { + // Both read-only: covariant -> intersect values, keep read-only. + idx.indexResultType = intersectionType(httv->indexer->indexResultType, tttv->indexer->indexResultType); + idx.isReadOnly = true; + } + else + idx.indexResultType = intersectionType(httv->indexer->indexResultType, tttv->indexer->indexResultType); + + bool hereModeMatch = httv->indexer->isReadOnly == idx.isReadOnly; + bool thereModeMatch = tttv->indexer->isReadOnly == idx.isReadOnly; + hereSubThere &= hereModeMatch && (httv->indexer->indexType == index) && (httv->indexer->indexResultType == idx.indexResultType); + thereSubHere &= thereModeMatch && (tttv->indexer->indexType == index) && (tttv->indexer->indexResultType == idx.indexResultType); + if (!result.get()) result = std::make_unique(TableType{state, level, scope}); - result->indexer = {index, indexResult}; - hereSubThere &= (httv->indexer->indexType == index) && (httv->indexer->indexResultType == indexResult); - thereSubHere &= (tttv->indexer->indexType == index) && (tttv->indexer->indexResultType == indexResult); + result->indexer = idx; } else if (httv->indexer) { @@ -3173,6 +3264,8 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor here.errors = (get(there.errors) ? there.errors : here.errors); here.nils = (get(there.nils) ? there.nils : here.nils); here.numbers = (get(there.numbers) ? there.numbers : here.numbers); + if (FFlag::LuauIntegerType2) + here.integers = (get(there.integers) ? there.integers : here.integers); intersectStrings(here.strings, there.strings); here.threads = (get(there.threads) ? there.threads : here.threads); here.buffers = (get(there.buffers) ? there.buffers : here.buffers); @@ -3280,16 +3373,25 @@ NormalizationResult Normalizer::intersectNormalWithTy( TypeIds tables = std::move(here.tables); clearNormal(here); - if (FFlag::LuauExternTypesNormalizeWithShapes) + if (FFlag::LuauAlwaysIntersectTablesWithTables) { - if (externTypes.isNever()) - intersectTablesWithTable(tables, there, seenTablePropPairs, seenSetTypes); - else + // We intersect this table against the table part of the + // normalized type, which may include the top table type. + intersectTablesWithTable(tables, there, seenTablePropPairs, seenSetTypes); + if (!externTypes.isNever()) + { + // If we have extern types present, intersect this table + // as a shape against the extern types of this normalized + // type. intersectExternTypesWithShape(externTypes, there); + } } else { - intersectTablesWithTable(tables, there, seenTablePropPairs, seenSetTypes); + if (externTypes.isNever()) + intersectTablesWithTable(tables, there, seenTablePropPairs, seenSetTypes); + else + intersectExternTypesWithShape(externTypes, there); } here.tables = std::move(tables); @@ -3305,10 +3407,47 @@ NormalizationResult Normalizer::intersectNormalWithTy( } else if (get(there)) { - NormalizedExternType nct = std::move(here.externTypes); - clearNormal(here); - intersectExternTypesWithExternType(nct, there); - here.externTypes = std::move(nct); + if (FFlag::LuauAllowIntersectionOfOneTableWithExtern && useNewLuauSolver()) + { + NormalizedExternType nct = std::move(here.externTypes); + TypeIds tables = std::move(here.tables); + clearNormal(here); + // FIXME CLI-214308: The representation of NormalizedExternType + // does not support an intersection like ... + // + // ExternType & (Tbl1 | Tbl2 | Tbl3) + // + // ... however, we can represent ... + // + // ExternType & Tbl1 + // + // ... so if the table part of this normalized type has a single + // table present, then intersect against *that*. + // + // In the future, we should allow intersections against more than + // one table in a union. + if (tables.size() == 0) + { + intersectExternTypesWithExternType(nct, there); + here.externTypes = std::move(nct); + } + else if (tables.size() == 1) + { + if (nct.isNever()) + nct.pushPair(there, {}); + else + intersectExternTypesWithExternType(nct, there); + intersectExternTypesWithShape(nct, tables.front()); + here.externTypes = std::move(nct); + } + } + else + { + NormalizedExternType nct = std::move(here.externTypes); + clearNormal(here); + intersectExternTypesWithExternType(nct, there); + here.externTypes = std::move(nct); + } } else if (get(there)) { @@ -3321,6 +3460,7 @@ NormalizationResult Normalizer::intersectNormalWithTy( TypeId booleans = here.booleans; TypeId nils = here.nils; TypeId numbers = here.numbers; + TypeId integers = here.integers; NormalizedStringType strings = std::move(here.strings); NormalizedFunctionType functions = std::move(here.functions); TypeId threads = here.threads; @@ -3335,6 +3475,8 @@ NormalizationResult Normalizer::intersectNormalWithTy( here.nils = nils; else if (ptv->type == PrimitiveType::Number) here.numbers = numbers; + else if (FFlag::LuauIntegerType2 && (ptv->type == PrimitiveType::Integer)) + here.integers = integers; else if (ptv->type == PrimitiveType::String) here.strings = std::move(strings); else if (ptv->type == PrimitiveType::Thread) @@ -3499,7 +3641,7 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) { const TypeIds& normNegations = norm.externTypes.externTypes.at(normTy); - if (normNegations.empty() && (!FFlag::LuauExternTypesNormalizeWithShapes || norm.externTypes.shapeExtensions.empty())) + if (normNegations.empty() && norm.externTypes.shapeExtensions.empty()) { parts.push_back(normTy); } @@ -3514,12 +3656,9 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) intersection.push_back(arena->addType(NegationType{negation})); } - if (FFlag::LuauExternTypesNormalizeWithShapes) + for (TypeId shape : norm.externTypes.shapeExtensions) { - for (TypeId shape : norm.externTypes.shapeExtensions) - { - intersection.push_back(shape); - } + intersection.push_back(shape); } parts.push_back(arena->addType(IntersectionType{std::move(intersection)})); @@ -3555,6 +3694,11 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) result.push_back(norm.nils); if (!get(norm.numbers)) result.push_back(norm.numbers); + if (FFlag::LuauIntegerType2) + { + if (get(norm.integers) == nullptr) + result.push_back(norm.integers); + } if (norm.strings.isString()) result.push_back(builtinTypes->stringType); else if (norm.strings.isUnion()) @@ -3628,69 +3772,20 @@ void Normalizer::consumeFuel() } } - bool isSubtype( TypeId subTy, TypeId superTy, - NotNull scope, + NotNull arena, NotNull builtinTypes, - InternalErrorReporter& ice, - SolverMode solverMode -) -{ - UnifierSharedState sharedState{&ice}; - TypeArena arena; - TypeCheckLimits limits; - TypeFunctionRuntime typeFunctionRuntime{ - NotNull{&ice}, NotNull{&limits} - }; // TODO: maybe subtyping checks should not invoke user-defined type function runtime - - Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}, solverMode}; - if (solverMode == SolverMode::New) - { - Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&typeFunctionRuntime}, NotNull{&ice}}; - - return subtyping.isSubtype(subTy, superTy, scope).isSubtype; - } - else - { - Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; - - u.tryUnify(subTy, superTy); - return !u.failure; - } -} - -bool isSubtype( - TypePackId subPack, - TypePackId superPack, NotNull scope, - NotNull builtinTypes, - InternalErrorReporter& ice, - SolverMode solverMode + NotNull normalizer, + NotNull typeFunctionRuntime, + NotNull reporter ) { - UnifierSharedState sharedState{&ice}; - TypeArena arena; - TypeCheckLimits limits; - TypeFunctionRuntime typeFunctionRuntime{ - NotNull{&ice}, NotNull{&limits} - }; // TODO: maybe subtyping checks should not invoke user-defined type function runtime - - Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}, solverMode}; - if (solverMode == SolverMode::New) - { - Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&typeFunctionRuntime}, NotNull{&ice}}; - - return subtyping.isSubtype(subPack, superPack, scope, {}).isSubtype; - } - else - { - Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; - - u.tryUnify(subPack, superPack); - return !u.failure; - } + Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, reporter}; + return subtyping.isSubtype(subTy, superTy, scope).isSubtype; } + } // namespace Luau diff --git a/Analysis/src/OverloadResolution.cpp b/Analysis/src/OverloadResolver.cpp similarity index 60% rename from Analysis/src/OverloadResolution.cpp rename to Analysis/src/OverloadResolver.cpp index 95b09a74..783d5420 100644 --- a/Analysis/src/OverloadResolution.cpp +++ b/Analysis/src/OverloadResolver.cpp @@ -1,5 +1,5 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/OverloadResolution.h" +#include "Luau/OverloadResolver.h" #include "Luau/Common.h" #include "Luau/Instantiation2.h" @@ -12,7 +12,7 @@ #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" -LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity2) +LUAU_FASTFLAG(LuauBidirectionalInferenceSimplifyTables) namespace Luau { @@ -494,7 +494,7 @@ void OverloadResolver::testFunction( return; } - TypeFunctionContext context{arena, builtinTypes, scope, normalizer, typeFunctionRuntime, ice, limits}; + TypeFunctionContext context{arena, builtinTypes, scope, normalizer, typeFunctionRuntime, ice, limits, NotNull{&subtyping}}; FunctionGraphReductionResult reduceResult = reduceTypeFunctions(fnTy, callLoc, NotNull{&context}, /*force=*/true); if (!reduceResult.errors.empty()) { @@ -504,7 +504,9 @@ void OverloadResolver::testFunction( TypeId prospectiveFunction = arena->addType(FunctionType{argsPack, builtinTypes->anyTypePack}); - subtyping.uniqueTypes = uniqueTypes; + if (!FFlag::LuauBidirectionalInferenceSimplifyTables) + subtyping.uniqueTypes = uniqueTypes; + SubtypingResult r = subtyping.isSubtype(fnTy, prospectiveFunction, scope); // Frustratingly, subtyping does not know about error suppression, so this @@ -644,70 +646,6 @@ void OverloadResolver::testFunctionOrCallMetamethod( testFunction(result, fnTy, argsPack, fnLocation, uniqueTypes); } -std::pair OverloadResolver::selectOverload_DEPRECATED( - TypeId ty, - TypePackId argsPack, - NotNull> uniqueTypes, - bool useFreeTypeBounds -) -{ - TypeId t = follow(ty); - - if (const FunctionType* fn = get(t)) - { - if (testFunctionTypeForOverloadSelection(fn, uniqueTypes, argsPack, useFreeTypeBounds)) - return {Analysis::Ok, ty}; - else - return {Analysis::OverloadIsNonviable, ty}; - } - else if (auto it = get(t)) - { - for (TypeId component : it) - { - const FunctionType* fn = get(follow(component)); - // Only consider function overloads with compatible arities - if (!fn || !isArityCompatible(argsPack, fn->argTypes, builtinTypes)) - continue; - - if (testFunctionTypeForOverloadSelection(fn, uniqueTypes, argsPack, useFreeTypeBounds)) - return {Analysis::Ok, component}; - } - } - - return {Analysis::OverloadIsNonviable, ty}; -} - -std::pair OverloadResolver::checkOverload( - TypeId fnTy, - const TypePack* args, - AstExpr* fnLoc, - const std::vector* argExprs, - NotNull> uniqueTypes, - bool callMetamethodOk -) -{ - fnTy = follow(fnTy); - - ErrorVec discard; - if (get(fnTy) || get(fnTy) || get(fnTy)) - return {Ok, {}}; - else if (auto fn = get(fnTy)) - return checkOverload_(fnTy, fn, args, fnLoc, argExprs, uniqueTypes); // Intentionally split to reduce the stack pressure of this function. - else if (auto callMm = findMetatableEntry(builtinTypes, discard, fnTy, "__call", callLoc); callMm && callMetamethodOk) - { - // Calling a metamethod forwards the `fnTy` as self. - TypePack withSelf = *args; - withSelf.head.insert(withSelf.head.begin(), fnTy); - - std::vector withSelfExprs = *argExprs; - withSelfExprs.insert(withSelfExprs.begin(), fnLoc); - - return checkOverload(*callMm, &withSelf, fnLoc, &withSelfExprs, uniqueTypes, /*callMetamethodOk=*/false); - } - else - return {TypeIsNotAFunction, {}}; // Intentionally empty. We can just fabricate the type error later on. -} - void OverloadResolver::maybeEmplaceError( ErrorVec* errors, Location argLocation, @@ -839,384 +777,4 @@ bool OverloadResolver::isArityCompatible(const TypePackId candidate, const TypeP return true; } -bool OverloadResolver::testFunctionTypeForOverloadSelection( - const FunctionType* ftv, - NotNull> uniqueTypes, - TypePackId argsPack, - bool useFreeTypeBounds -) -{ - subtyping.uniqueTypes = uniqueTypes; - std::vector generics; - generics.reserve(ftv->generics.size()); - for (TypeId g : ftv->generics) - { - g = follow(g); - if (get(g)) - generics.emplace_back(g); - } - SubtypingResult r = subtyping.isSubtype(argsPack, ftv->argTypes, scope, generics); - - if (!useFreeTypeBounds && !r.assumedConstraints.empty()) - return false; - - if (r.isSubtype) - return true; - - return false; -} - -std::pair OverloadResolver::checkOverload_( - TypeId fnTy, - const FunctionType* fn, - const TypePack* args, - AstExpr* fnExpr, - const std::vector* argExprs, - NotNull> uniqueTypes -) -{ - TypeFunctionContext context{arena, builtinTypes, scope, normalizer, typeFunctionRuntime, ice, limits}; - FunctionGraphReductionResult result = reduceTypeFunctions(fnTy, callLoc, NotNull{&context}, /*force=*/true); - if (!result.errors.empty()) - return {OverloadIsNonviable, result.errors}; - - ErrorVec argumentErrors; - TypePackId typ = arena->addTypePack(*args); - - TypeId prospectiveFunction = arena->addType(FunctionType{typ, builtinTypes->anyTypePack}); - subtyping.uniqueTypes = uniqueTypes; - SubtypingResult sr = subtyping.isSubtype(fnTy, prospectiveFunction, scope); - - if (sr.isSubtype) - return {Analysis::Ok, {}}; - - if (1 == sr.reasoning.size()) - { - const SubtypingReasoning& reason = *sr.reasoning.begin(); - - const TypePath::Path justArguments{TypePath::PackField::Arguments}; - - if (reason.subPath == justArguments && reason.superPath == justArguments) - { - // If the subtype test failed only due to an arity mismatch, - // it is still possible that this function call is okay. - // Subtype testing does not know anything about optional - // function arguments. - // - // This can only happen if the actual function call has a - // finite set of arguments which is too short for the - // function being called. If all of those unsatisfied - // function arguments are options, then this function call - // is ok. - - const size_t firstUnsatisfiedArgument = args->head.size(); - const auto [requiredHead, requiredTail] = flatten(fn->argTypes); - - bool isVariadic = requiredTail && Luau::isVariadic(*requiredTail); - - // If too many arguments were supplied, this overload - // definitely does not match. - if (args->head.size() > requiredHead.size()) - { - auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes); - - TypeError error{fnExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, isVariadic}}; - - return {Analysis::ArityMismatch, {std::move(error)}}; - } - - // If any of the unsatisfied arguments are not supertypes of - // nil or are `unknown`, then this overload does not match. - for (size_t i = firstUnsatisfiedArgument; i < requiredHead.size(); ++i) - { - if (get(follow(requiredHead[i])) || !subtyping.isSubtype(builtinTypes->nilType, requiredHead[i], scope).isSubtype) - { - auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes); - for (auto arg : fn->argTypes) - if (get(follow(arg))) - minParams += 1; - - TypeError error{fnExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, isVariadic}}; - - return {Analysis::ArityMismatch, {std::move(error)}}; - } - } - - // All unsatisfied arguments are supertypes of nil. This overload is a valid match. - return {Analysis::Ok, {}}; - } - - const bool subPathArgTail = matchesPrefix(Path({TypePath::PackField::Arguments, TypePath::PackField::Tail}), reason.subPath); - const bool superPathArgs = matchesPrefix(Path(TypePath::PackField::Arguments), reason.superPath); - const TypePath::Component& lastSubComponent = reason.subPath.components.back(); - const bool subEndsInGenericPackMapping = get_if(&lastSubComponent) != nullptr; - - // If the function's argument list ends with a generic pack, and - // the subtype test failed because of that, we need to check the - // pack that the generic was mapped to in order to report an - // accurate CountMismatch error. - if (subPathArgTail && superPathArgs && subEndsInGenericPackMapping) - { - const TypePack requiredMappedArgs = traverseForFlattenedPack(fnTy, reason.subPath, builtinTypes, arena); - const std::vector prospectiveHead = flatten(typ).first; - - const size_t requiredHeadSize = requiredMappedArgs.head.size(); - const size_t prospectiveHeadSize = prospectiveHead.size(); - - if (prospectiveHeadSize != requiredHeadSize) - { - TypeError error{ - fnExpr->location, - CountMismatch{ - requiredHeadSize, - requiredMappedArgs.tail.has_value() ? std::nullopt : std::optional{requiredHeadSize}, - prospectiveHeadSize, - CountMismatch::Arg - } - }; - - return {Analysis::ArityMismatch, {std::move(error)}}; - } - } - } - - ErrorVec errors; - - // Translate SubtypingReasonings into TypeErrors that could be reported. - for (const SubtypingReasoning& reason : sr.reasoning) - { - /* The return type of our prospective function is always - * any... so any subtype failures here can only arise from - * argument type mismatches. - */ - - Location argLocation; - if (reason.superPath.components.size() <= 1) - break; - - if (const Luau::TypePath::Index* pathIndexComponent = get_if(&reason.superPath.components.at(1))) - { - size_t nthArgument = pathIndexComponent->index; - // if the nth type argument to the function is less than the number of ast expressions we passed to the function - // we should be able to pull out the location of the argument - // If the nth type argument to the function is out of range of the ast expressions we passed to the function - // e.g. table.pack(functionThatReturnsMultipleArguments(arg1, arg2, ....)), default to the location of the last passed expression - // If we passed no expression arguments to the call, default to the location of the function expression. - argLocation = nthArgument < argExprs->size() ? argExprs->at(nthArgument)->location - : argExprs->size() != 0 ? argExprs->back()->location - : fnExpr->location; - - std::optional failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes, arena); - - std::optional failedSuperTy = traverseForType(prospectiveFunction, reason.superPath, builtinTypes, arena); - - maybeEmplaceError(&errors, argLocation, &reason, failedSubTy, failedSuperTy); - } - else if (reason.superPath.components.size() > 1) - { - // traverseForIndex only has a value if path is of form [...PackSlice, Index] - if (const auto index = - traverseForIndex(TypePath::Path{std::vector(reason.superPath.components.begin() + 1, reason.superPath.components.end())})) - { - if (index < argExprs->size()) - argLocation = argExprs->at(*index)->location; - else if (argExprs->size() != 0) - argLocation = argExprs->back()->location; - else - { - // this should never happen - LUAU_ASSERT(false); - argLocation = fnExpr->location; - } - std::optional failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes, arena); - std::optional failedSuperTy = traverseForType(prospectiveFunction, reason.superPath, builtinTypes, arena); - maybeEmplaceError(&errors, argLocation, &reason, failedSubTy, failedSuperTy); - } - } - - std::optional failedSubPack = traverseForPack(fnTy, reason.subPath, builtinTypes, arena); - - std::optional failedSuperPack = traverseForPack(prospectiveFunction, reason.superPath, builtinTypes, arena); - - if (failedSubPack && failedSuperPack) - { - // If a bug in type inference occurs, we may have a mismatch in the return packs. - // This happens when inference incorrectly leaves the result type of a function free. - // If this happens, we don't want to explode, so we'll use the function's location. - if (argExprs->empty()) - argLocation = fnExpr->location; - else - argLocation = argExprs->at(argExprs->size() - 1)->location; - - // TODO extract location from the SubtypingResult path and argExprs - auto errorSuppression = shouldSuppressErrors(normalizer, *failedSubPack).orElse(shouldSuppressErrors(normalizer, *failedSuperPack)); - if (errorSuppression == ErrorSuppression::Suppress) - break; - - switch (reason.variance) - { - case SubtypingVariance::Covariant: - errors.emplace_back(argLocation, TypePackMismatch{*failedSubPack, *failedSuperPack}); - break; - case SubtypingVariance::Contravariant: - errors.emplace_back(argLocation, TypePackMismatch{*failedSuperPack, *failedSubPack}); - break; - case SubtypingVariance::Invariant: - errors.emplace_back(argLocation, TypePackMismatch{*failedSubPack, *failedSuperPack}); - break; - default: - LUAU_ASSERT(0); - break; - } - } - } - - for (GenericBoundsMismatch& mismatch : sr.genericBoundsMismatches) - errors.emplace_back(fnExpr->location, std::move(mismatch)); - - return {Analysis::OverloadIsNonviable, std::move(errors)}; -} - -size_t OverloadResolver::indexof(Analysis analysis) -{ - switch (analysis) - { - case Ok: - return ok.size(); - case TypeIsNotAFunction: - return nonFunctions.size(); - case ArityMismatch: - return arityMismatches.size(); - case OverloadIsNonviable: - return nonviableOverloads.size(); - } - - ice->ice("Inexhaustive switch in FunctionCallResolver::indexof"); -} - -void OverloadResolver::add(Analysis analysis, TypeId ty, ErrorVec&& errors) -{ - resolution.insert(ty, {analysis, indexof(analysis)}); - - switch (analysis) - { - case Ok: - LUAU_ASSERT(errors.empty()); - ok.push_back(ty); - break; - case TypeIsNotAFunction: - LUAU_ASSERT(errors.empty()); - nonFunctions.push_back(ty); - break; - case ArityMismatch: - arityMismatches.emplace_back(ty, std::move(errors)); - break; - case OverloadIsNonviable: - nonviableOverloads.emplace_back(ty, std::move(errors)); - break; - } -} - -// we wrap calling the overload resolver in a separate function to reduce overall stack pressure in `solveFunctionCall`. -// this limits the lifetime of `OverloadResolver`, a large type, to only as long as it is actually needed. -static std::optional selectOverload( - NotNull builtinTypes, - NotNull arena, - NotNull normalizer, - NotNull typeFunctionRuntime, - NotNull scope, - NotNull iceReporter, - NotNull limits, - const Location& location, - TypeId fn, - TypePackId argsPack -) -{ - auto resolver = std::make_unique(builtinTypes, arena, normalizer, typeFunctionRuntime, scope, iceReporter, limits, location); - - DenseHashSet uniqueTypes{nullptr}; - auto [status, overload] = resolver->selectOverload_DEPRECATED(fn, argsPack, NotNull{&uniqueTypes}, /*useFreeTypeBounds*/ false); - - if (status == OverloadResolver::Analysis::Ok) - return overload; - - if (get(fn) || get(fn)) - return fn; - - return {}; -} - -SolveResult solveFunctionCall_DEPRECATED( - NotNull arena, - NotNull builtinTypes, - NotNull normalizer, - NotNull typeFunctionRuntime, - NotNull iceReporter, - NotNull limits, - NotNull scope, - const Location& location, - TypeId fn, - TypePackId argsPack -) -{ - std::optional overloadToUse = - selectOverload(builtinTypes, arena, normalizer, typeFunctionRuntime, scope, iceReporter, limits, location, fn, argsPack); - if (!overloadToUse) - return {SolveResult::NoMatchingOverload}; - - TypePackId resultPack = arena->freshTypePack(scope); - - TypeId inferredTy = arena->addType(FunctionType{TypeLevel{}, argsPack, resultPack}); - Unifier2 u2{NotNull{arena}, builtinTypes, scope, iceReporter}; - - const UnifyResult unifyResult = u2.unify(*overloadToUse, inferredTy); - - if (!u2.genericSubstitutions.empty() || !u2.genericPackSubstitutions.empty()) - { - if (FFlag::LuauInstantiationUsesGenericPolarity2) - { - Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, iceReporter}; - std::optional subst = instantiate2( - arena, std::move(u2.genericSubstitutions), std::move(u2.genericPackSubstitutions), NotNull{&subtyping}, scope, resultPack - ); - if (!subst) - return {SolveResult::CodeTooComplex}; - else - resultPack = *subst; - } - else - { - auto instantiation = std::make_unique(arena, std::move(u2.genericSubstitutions), std::move(u2.genericPackSubstitutions)); - - std::optional subst = instantiation->substitute(resultPack); - - if (!subst) - return {SolveResult::CodeTooComplex}; - else - resultPack = *subst; - } - } - - switch (unifyResult) - { - case Luau::UnifyResult::Ok: - break; - case Luau::UnifyResult::OccursCheckFailed: - return {SolveResult::CodeTooComplex}; - case Luau::UnifyResult::TooComplex: - return {SolveResult::OccursCheckFailed}; - } - - SolveResult result; - result.result = SolveResult::Ok; - result.typePackId = resultPack; - - LUAU_ASSERT(overloadToUse); - result.overloadToUse = *overloadToUse; - result.inferredTy = inferredTy; - result.expandedFreeTypes = std::move(u2.expandedFreeTypes); - - return result; -} - } // namespace Luau diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index 2894a279..58c4ea88 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -4,8 +4,6 @@ LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAG(LuauReworkInfiniteTypeFinder) - namespace Luau { @@ -241,6 +239,7 @@ void Scope::inheritRefinements(const ScopePtr& childScope) } } + bool Scope::shouldWarnGlobal(std::string name) const { for (const Scope* current = this; current; current = current->parent.get()) @@ -253,7 +252,6 @@ bool Scope::shouldWarnGlobal(std::string name) const std::optional Scope::isInvalidTypeAlias(const std::string& name) const { - LUAU_ASSERT(FFlag::LuauReworkInfiniteTypeFinder); for (auto scope = this; scope; scope = scope->parent.get()) { if (auto loc = scope->invalidTypeAliases.find(name)) @@ -263,18 +261,6 @@ std::optional Scope::isInvalidTypeAlias(const std::string& name) const return std::nullopt; } -bool Scope::isInvalidTypeAliasName_DEPRECATED(const std::string& name) const -{ - LUAU_ASSERT(!FFlag::LuauReworkInfiniteTypeFinder); - for (auto scope = this; scope; scope = scope->parent.get()) - { - if (scope->invalidTypeAliasNames_DEPRECATED.contains(name)) - return true; - } - - return false; -} - NotNull Scope::findNarrowestScopeContaining(Location location) { Scope* bestScope = this; diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index 533ad12a..86155f47 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -16,12 +16,9 @@ #include -LUAU_FASTINT(LuauTypeReductionRecursionLimit) LUAU_FASTFLAG(LuauSolverV2) LUAU_DYNAMIC_FASTINTVARIABLE(LuauSimplificationComplexityLimit, 8) LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeSimplificationIterationLimit, 128) -LUAU_FASTFLAGVARIABLE(LuauUnionOfTablesPreservesReadWrite) -LUAU_FASTFLAGVARIABLE(LuauRelateHandlesCoincidentTables) namespace Luau { @@ -149,92 +146,6 @@ Relation flip(Relation rel) } } -// FIXME: I'm not completely certain that this function is theoretically reasonable. -Relation combine(Relation a, Relation b) -{ - switch (a) - { - case Relation::Disjoint: - switch (b) - { - case Relation::Disjoint: - return Relation::Disjoint; - case Relation::Coincident: - return Relation::Superset; - case Relation::Intersects: - return Relation::Intersects; - case Relation::Subset: - return Relation::Intersects; - case Relation::Superset: - return Relation::Intersects; - } - break; - case Relation::Coincident: - switch (b) - { - case Relation::Disjoint: - return Relation::Coincident; - case Relation::Coincident: - return Relation::Coincident; - case Relation::Intersects: - return Relation::Superset; - case Relation::Subset: - return Relation::Coincident; - case Relation::Superset: - return Relation::Intersects; - } - break; - case Relation::Superset: - switch (b) - { - case Relation::Disjoint: - return Relation::Superset; - case Relation::Coincident: - return Relation::Superset; - case Relation::Intersects: - return Relation::Intersects; - case Relation::Subset: - return Relation::Intersects; - case Relation::Superset: - return Relation::Superset; - } - break; - case Relation::Subset: - switch (b) - { - case Relation::Disjoint: - return Relation::Subset; - case Relation::Coincident: - return Relation::Coincident; - case Relation::Intersects: - return Relation::Intersects; - case Relation::Subset: - return Relation::Subset; - case Relation::Superset: - return Relation::Intersects; - } - break; - case Relation::Intersects: - switch (b) - { - case Relation::Disjoint: - return Relation::Intersects; - case Relation::Coincident: - return Relation::Superset; - case Relation::Intersects: - return Relation::Intersects; - case Relation::Subset: - return Relation::Intersects; - case Relation::Superset: - return Relation::Intersects; - } - break; - } - - LUAU_UNREACHABLE(); - return Relation::Intersects; -} - // Given A & B, what is A & ~B? Relation invert(Relation r) { @@ -252,7 +163,7 @@ Relation invert(Relation r) return Relation::Intersects; } - LUAU_UNREACHABLE(); + LUAU_ASSERT(false); return Relation::Intersects; } @@ -414,7 +325,6 @@ Relation relateTableToProp(const TableType* leftTable, const std::string& propNa // And for good measure, default to intersection. return Relation::Intersects; } - } Relation relateTables(const TableType* leftTable, const TableType* rightTable, SimplifierSeenSet& seen) @@ -491,59 +401,6 @@ Relation relateTables(const TableType* leftTable, const TableType* rightTable, S return Relation::Intersects; return hasSubset ? Relation::Subset : Relation::Coincident; - -} - -Relation relateTables_DEPRECATED(TypeId left, TypeId right, SimplifierSeenSet& seen) -{ - NotNull leftTable{get(left)}; - NotNull rightTable{get(right)}; - LUAU_ASSERT(1 == rightTable->props.size()); - // Disjoint props have nothing in common - // t1 with props p1's cannot appear in t2 and t2 with props p2's cannot appear in t1 - bool foundPropFromLeftInRight = std::any_of( - begin(leftTable->props), - end(leftTable->props), - [&](auto prop) - { - return rightTable->props.count(prop.first) > 0; - } - ); - bool foundPropFromRightInLeft = std::any_of( - begin(rightTable->props), - end(rightTable->props), - [&](auto prop) - { - return leftTable->props.count(prop.first) > 0; - } - ); - - if (!foundPropFromLeftInRight && !foundPropFromRightInLeft && leftTable->props.size() >= 1 && rightTable->props.size() >= 1) - return Relation::Intersects; - - const auto [propName, rightProp] = *begin(rightTable->props); - - auto it = leftTable->props.find(propName); - if (it == leftTable->props.end()) - { - // Every table lacking a property is a supertype of a table having that - // property but the reverse is not true. - return Relation::Superset; - } - - const Property leftProp = it->second; - - if (!leftProp.isShared() || !rightProp.isShared()) - return Relation::Intersects; - - Relation r = relate(*leftProp.readTy, *rightProp.readTy, seen); - if (r == Relation::Coincident && 1 != leftTable->props.size()) - { - // eg {tag: "cat", prop: string} & {tag: "cat"} - return Relation::Subset; - } - else - return r; } // A cheap and approximate subtype test @@ -790,39 +647,7 @@ Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen) if (auto rt = get(right)) { - if (FFlag::LuauRelateHandlesCoincidentTables) - { - return relateTables(lt, rt, seen); - } - else - { - // TODO PROBABLY indexers and metatables. - if (1 == rt->props.size()) - { - Relation r = relateTables_DEPRECATED(left, right, seen); - /* - * A reduction of these intersections is certainly possible, but - * it would require minting new table types. Also, I don't think - * it's super likely for this to arise from a refinement. - * - * Time will tell! - * - * ex we simplify this - * {tag: string} & {tag: "cat"} - * but not this - * {tag: string, prop: number} & {tag: "cat"} - */ - if (lt->props.size() > 1 && r == Relation::Superset) - return Relation::Intersects; - - return r; - } - - if (1 == lt->props.size()) - return flip(relate(right, left, seen)); - - return Relation::Intersects; - } + return relateTables(lt, rt, seen); } if (auto re = get(right)) @@ -1778,83 +1603,55 @@ TypeId TypeSimplifier::union_(TypeId left, TypeId right) if (rightPropName != propName) return arena->addType(UnionType{{left, right}}); - if (FFlag::LuauUnionOfTablesPreservesReadWrite) + // Consider: + // + // { prop: number? } | { prop: string? } + // + // Even though these two tables share a property, we cannot + // simplify this type any further, otherwise we can, say, + // launder a `{ prop: number? }` into a `{ prop: string? }` + // and then write a string to it. + // + // We also elect to not simplify unsealed tables. + if (!leftProp.isReadOnly() || !rightProp.isReadOnly() || lt->state != TableState::Sealed || rt->state != TableState::Sealed) + return arena->addType(UnionType{{left, right}}); + + // At this point, we have two read-only singleton tables, e.g.: + // + // { read prop: number? } | { read prop: string? } + // + // We can relate these two properties and produce a simplified + // version, with some special cases. + + switch (relate(*leftProp.readTy, *rightProp.readTy)) { - // Consider: + case Relation::Coincident: + case Relation::Superset: + // The left property is a superset (or coincident) of the + // right, for example: // - // { prop: number? } | { prop: string? } + // { read prop: number? } | { read prop: number } // - // Even though these two tables share a property, we cannot - // simplify this type any further, otherwise we can, say, - // launder a `{ prop: number? }` into a `{ prop: string? }` - // and then write a string to it. + return left; + case Relation::Subset: + // The left property is a subset of the right, for example: // - // We also elect to not simplify unsealed tables. - if (!leftProp.isReadOnly() || !rightProp.isReadOnly() || lt->state != TableState::Sealed || rt->state != TableState::Sealed) - return arena->addType(UnionType{{left, right}}); - - // At this point, we have two read-only singleton tables, e.g.: + // { read prop: nil } | { read prop: false? } // - // { read prop: number? } | { read prop: string? } + return right; + case Relation::Disjoint: + case Relation::Intersects: + // If we are disjoint *or* there's some overlap, then + // we can create a new read-only singleton table with + // a single property. // - // We can relate these two properties and produce a simplified - // version, with some special cases. - - switch (relate(*leftProp.readTy, *rightProp.readTy)) - { - case Relation::Coincident: - case Relation::Superset: - // The left property is a superset (or coincident) of the - // right, for example: - // - // { read prop: number? } | { read prop: number } - // - return left; - case Relation::Subset: - // The left property is a subset of the right, for example: - // - // { read prop: nil } | { read prop: false? } - // - return right; - case Relation::Disjoint: - case Relation::Intersects: - // If we are disjoint *or* there's some overlap, then - // we can create a new read-only singleton table with - // a single property. - // - // We probably could do something quicker here for disjoint, - // given that the union should just mint a new union type - // anyhow. - TableType result; - result.state = TableState::Sealed; - result.props[propName] = Property::readonly(union_(*leftProp.readTy, *rightProp.readTy)); - return arena->addType(std::move(result)); - } - } - else - { - if (leftProp.readTy && rightProp.readTy) - { - Relation r = relate(*leftProp.readTy, *rightProp.readTy); - - switch (r) - { - case Relation::Disjoint: - { - TableType result; - result.state = TableState::Sealed; - result.props[propName] = union_(*leftProp.readTy, *rightProp.readTy); - return arena->addType(result); - } - case Relation::Superset: - case Relation::Coincident: - return left; - case Relation::Subset: - return right; - default: - break; - } - } + // We probably could do something quicker here for disjoint, + // given that the union should just mint a new union type + // anyhow. + TableType result; + result.state = TableState::Sealed; + result.props[propName] = Property::readonly(union_(*leftProp.readTy, *rightProp.readTy)); + return arena->addType(std::move(result)); } } } diff --git a/Analysis/src/StructuralTypeEquality.cpp b/Analysis/src/StructuralTypeEquality.cpp index 3b9f99e8..f8615ee9 100644 --- a/Analysis/src/StructuralTypeEquality.cpp +++ b/Analysis/src/StructuralTypeEquality.cpp @@ -5,9 +5,6 @@ #include "Luau/Type.h" #include "Luau/TypePack.h" -LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauAnalysisUsesSolverMode) - // Test Types for equivalence // More complex than we'd like because Types can self-reference. @@ -117,6 +114,9 @@ bool areEqual(SeenSet& seen, const TableType& lhs, const TableType& rhs) if (lhs.indexer && rhs.indexer) { + if (lhs.indexer->isReadOnly != rhs.indexer->isReadOnly) + return false; + if (!areEqual(seen, *lhs.indexer->indexType, *rhs.indexer->indexType)) return false; @@ -132,26 +132,24 @@ bool areEqual(SeenSet& seen, const TableType& lhs, const TableType& rhs) if (l->first != r->first) return false; - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) + + if (l->second.readTy && r->second.readTy) { - if (l->second.readTy && r->second.readTy) - { - if (!areEqual(seen, **l->second.readTy, **r->second.readTy)) - return false; - } - else if (l->second.readTy || r->second.readTy) + if (!areEqual(seen, **l->second.readTy, **r->second.readTy)) return false; + } + else if (l->second.readTy || r->second.readTy) + return false; - if (l->second.writeTy && r->second.writeTy) - { - if (!areEqual(seen, **l->second.writeTy, **r->second.writeTy)) - return false; - } - else if (l->second.writeTy || r->second.writeTy) + if (l->second.writeTy && r->second.writeTy) + { + if (!areEqual(seen, **l->second.writeTy, **r->second.writeTy)) return false; } - else if (!areEqual(seen, *l->second.type_DEPRECATED(), *r->second.type_DEPRECATED())) + else if (l->second.writeTy || r->second.writeTy) return false; + + ++l; ++r; } diff --git a/Analysis/src/Substitution.cpp b/Analysis/src/Substitution.cpp index 9ef98454..b9b56e51 100644 --- a/Analysis/src/Substitution.cpp +++ b/Analysis/src/Substitution.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/Substitution.h" +#include "Luau/Ast.h" #include "Luau/Common.h" #include "Luau/TxnLog.h" #include "Luau/Type.h" @@ -10,7 +11,6 @@ LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINTVARIABLE(LuauTarjanPreallocationSize, 256) -LUAU_FASTFLAG(LuauAnalysisUsesSolverMode) namespace Luau { @@ -130,6 +130,8 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log) else if constexpr (std::is_same_v) { ExternType clone{a.name, a.props, a.parent, a.metatable, a.tags, a.userData, a.definitionModuleName, a.definitionLocation, a.indexer}; + if (FFlag::DebugLuauUserDefinedClasses) + clone.relation = a.relation; return dest.addType(std::move(clone)); } else if constexpr (std::is_same_v) @@ -242,20 +244,10 @@ void Tarjan::visitChildren(TypeId ty, int index) { for (const auto& [name, prop] : etv->props) { - if (FFlag::LuauAnalysisUsesSolverMode) - { - if (prop.readTy) - visitChild(prop.readTy); - if (prop.writeTy) - visitChild(prop.writeTy); - } - else if (FFlag::LuauSolverV2) - { + if (prop.readTy) visitChild(prop.readTy); + if (prop.writeTy) visitChild(prop.writeTy); - } - else - visitChild(prop.type_DEPRECATED()); } if (etv->parent) @@ -269,6 +261,23 @@ void Tarjan::visitChildren(TypeId ty, int index) visitChild(etv->indexer->indexType); visitChild(etv->indexer->indexResultType); } + + if (FFlag::DebugLuauUserDefinedClasses && etv->relation) + { + Luau::visit( + overloaded{ + [&](const Obj& obj) + { + visitChild(obj.ty); + }, + [&](const Klass& klass) + { + visitChild(klass.ty); + } + }, + *etv->relation + ); + } } else if (const NegationType* ntv = get(ty)) { @@ -836,15 +845,10 @@ void Substitution::replaceChildren(TypeId ty) { for (auto& [name, prop] : etv->props) { - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) - { - if (prop.readTy) - prop.readTy = replace(prop.readTy); - if (prop.writeTy) - prop.writeTy = replace(prop.writeTy); - } - else - prop.setType(replace(prop.type_DEPRECATED())); + if (prop.readTy) + prop.readTy = replace(prop.readTy); + if (prop.writeTy) + prop.writeTy = replace(prop.writeTy); } if (etv->parent) diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 63c2fff7..1ddc374f 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -22,24 +22,28 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauSubtypingRecursionLimit, 100) LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity) LUAU_FASTINTVARIABLE(LuauSubtypingReasoningLimit, 100) -LUAU_FASTFLAGVARIABLE(LuauMorePreciseErrorSuppression) -LUAU_FASTFLAGVARIABLE(LuauSubtypingPackRecursionLimits) LUAU_FASTFLAGVARIABLE(LuauSubtypingMissingPropertiesAsNil) -LUAU_FASTFLAG(LuauTableFreezeCheckIsSubtype) -LUAU_FASTFLAG(LuauUnifyWithSubtyping2) LUAU_FASTINTVARIABLE(LuauSubtypingIterationLimit, 20000) +LUAU_FASTFLAG(LuauPropertyModifierMismatchErrors) +LUAU_FASTFLAGVARIABLE(LuauSubtypeUnionsTogether) +LUAU_FASTFLAGVARIABLE(LuauDropUnionSubtypeReasoning) +LUAU_FASTFLAGVARIABLE(LuauDontBindOptionalGenericToNil) +LUAU_FASTFLAGVARIABLE(LuauImproveUniqueTableWidthSubtyping) +LUAU_FASTFLAG(LuauBidirectionalInferenceSimplifyTables) namespace Luau { bool SubtypingReasoning::operator==(const SubtypingReasoning& other) const { - return subPath == other.subPath && superPath == other.superPath && variance == other.variance; + return subPath == other.subPath && superPath == other.superPath && variance == other.variance && + isPropertyModifierViolation == other.isPropertyModifierViolation; } size_t SubtypingReasoningHash::operator()(const SubtypingReasoning& r) const { - return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1) ^ (static_cast(r.variance) << 1); + return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1) ^ (static_cast(r.variance) << 1) ^ + (static_cast(r.isPropertyModifierViolation) << 2); } MappedGenericEnvironment::MappedGenericFrame::MappedGenericFrame( @@ -145,7 +149,6 @@ bool MappedGenericEnvironment::bindGeneric(TypePackId genericTp, TypePackId bind } else { - LUAU_ASSERT(!"bindGeneric called on a non-bindable generic type pack"); return false; } } @@ -176,13 +179,6 @@ static void assertReasoningValid(TID subTy, TID superTy, const SubtypingResult& } } -template<> -void assertReasoningValid(TableIndexer, TableIndexer, const SubtypingResult&, NotNull, NotNull) -{ - // This specialization exists so that we can invoke methods like - // isInvariantWith() on a pair of TableIndexers. -} - static SubtypingReasonings mergeReasonings(const SubtypingReasonings& a, const SubtypingReasonings& b) { SubtypingReasonings result{kEmptyReasoning}; @@ -228,22 +224,27 @@ static SubtypingReasonings mergeReasonings(const SubtypingReasonings& a, const S return result; } -SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other, SubtypingSuppressionPolicy policy) +SubtypingResult& SubtypingResult::andAlso(SubtypingResult other, SubtypingSuppressionPolicy policy) { // If the other result is not a subtype, we want to join all of its // reasonings to this one. If this result already has reasonings of its own, // those need to be attributed here whenever this _also_ failed. if (!other.isSubtype) - reasoning = isSubtype ? other.reasoning : mergeReasonings(reasoning, other.reasoning); - - isSubtype &= other.isSubtype; - if (FFlag::LuauMorePreciseErrorSuppression) { - if (policy == SubtypingSuppressionPolicy::All) - isErrorSuppressing &= other.isErrorSuppressing; + if (isSubtype) + reasoning = std::move(other.reasoning); else - isErrorSuppressing |= other.isErrorSuppressing; + // NOTE: This probably doesn't need to be two copies. + reasoning = mergeReasonings(reasoning, other.reasoning); } + + isSubtype &= other.isSubtype; + + if (policy == SubtypingSuppressionPolicy::All) + isErrorSuppressing &= other.isErrorSuppressing; + else + isErrorSuppressing |= other.isErrorSuppressing; + normalizationTooComplex |= other.normalizationTooComplex; isCacheable &= other.isCacheable; errors.insert(errors.end(), other.errors.begin(), other.errors.end()); @@ -253,7 +254,7 @@ SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other, Subtypin return *this; } -SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other) +SubtypingResult& SubtypingResult::orElse(SubtypingResult other) { // If this result is a subtype, we do not join the reasoning lists. If this // result is not a subtype, but the other is a subtype, we want to _clear_ @@ -264,24 +265,20 @@ SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other) if (other.isSubtype) { reasoning.clear(); - // It would be nice to be able to `std::move` this. - assumedConstraints = other.assumedConstraints; + assumedConstraints = std::move(other.assumedConstraints); } else { reasoning = mergeReasonings(reasoning, other.reasoning); - if (FFlag::LuauMorePreciseErrorSuppression) - isErrorSuppressing |= other.isErrorSuppressing; + isErrorSuppressing |= other.isErrorSuppressing; } } - else if (FFlag::LuauUnifyWithSubtyping2 && other.isSubtype) + else if (other.isSubtype) { // If the other result has assumed constraints, we drop ours (given // we represent a failed subtype) and then take the constraints of // the other check. - // - // It would also be nice to `std::move` this. - assumedConstraints = other.assumedConstraints; + assumedConstraints = std::move(other.assumedConstraints); } isSubtype |= other.isSubtype; @@ -368,6 +365,13 @@ SubtypingResult& SubtypingResult::withError(TypeError err) return *this; } +SubtypingResult& SubtypingResult::withPropertyModifierViolation() +{ + for (auto& r : reasoning) + r.isPropertyModifierViolation = true; + return *this; +} + SubtypingResult& SubtypingResult::withAssumedConstraint(ConstraintV constraint) { assumedConstraints.push_back(std::move(constraint)); @@ -382,32 +386,6 @@ SubtypingResult SubtypingResult::negate(const SubtypingResult& result) }; } -SubtypingResult SubtypingResult::all(const std::vector& results) -{ - SubtypingResult acc{true}; - - if (FFlag::LuauMorePreciseErrorSuppression) - { - if (results.empty()) - return acc; - - acc.isErrorSuppressing = true; - } - - for (const SubtypingResult& current : results) - acc.andAlso(current, SubtypingSuppressionPolicy::All); - - return acc; -} - -SubtypingResult SubtypingResult::any(const std::vector& results) -{ - SubtypingResult acc{false}; - for (const SubtypingResult& current : results) - acc.orElse(current); - return acc; -} - struct ApplyMappedGenerics : Substitution { NotNull builtinTypes; @@ -454,51 +432,27 @@ struct ApplyMappedGenerics : Substitution } else if (!upperBound.empty()) { - TypeIds boundsToUse; - + IntersectionBuilder ib{arena, builtinTypes}; for (TypeId ub : upperBound) { - // quick and dirty check to avoid adding generic types + // NOTE: The original implementation skips over generic + // types, but that seems incorrect to me. if (!get(ub)) - boundsToUse.insert(ub); - } - - if (boundsToUse.empty()) - { - // This case happens when we've collected no bounds for the generic we're mapping. - // In this case, unknown vs never is an arbitrary choice: - // ie, does it matter if we map add to add or add in the context of subtyping? - // We choose unknown here, since it's closest to the original behavior. - return builtinTypes->unknownType; + ib.add(ub); } - if (boundsToUse.size() == 1) - return *boundsToUse.begin(); - - return arena->addType(IntersectionType{boundsToUse.take()}); + return ib.build(); } else if (!lowerBound.empty()) { - TypeIds boundsToUse; - + UnionBuilder ub{arena, builtinTypes}; for (TypeId lb : lowerBound) { - // quick and dirty check to avoid adding generic types + // NOTE: The original implementation skips over generic + // types, but that seems incorrect to me. if (!get(lb)) - boundsToUse.insert(lb); + ub.add(lb); } - - if (boundsToUse.empty()) - { - // This case happens when we've collected no bounds for the generic we're mapping. - // In this case, unknown vs never is an arbitrary choice: - // ie, does it matter if we map add to add or add in the context of subtyping? - // We choose unknown here, since it's closest to the original behavior. - return builtinTypes->unknownType; - } - else if (lowerBound.size() == 1) - return *boundsToUse.begin(); - else - return arena->addType(UnionType{boundsToUse.take()}); + return ub.build(); } else { @@ -526,6 +480,7 @@ struct ApplyMappedGenerics : Substitution { for (TypeId g : f->generics) { + g = follow(g); if (const std::vector* bounds = env->mappedGenerics.find(g); bounds && !bounds->empty()) // We don't want to mutate the generics of a function that's being subtyped return true; @@ -725,17 +680,13 @@ SubtypingResult Subtyping::cache(SubtypingEnvironment& env, SubtypingResult resu SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy, NotNull scope) { - UnifierCounters& counters = normalizer->sharedState->counters; - RecursionCounter rc(&counters.recursionCount); - if (DFInt::LuauSubtypingRecursionLimit > 0 && DFInt::LuauSubtypingRecursionLimit < counters.recursionCount) + NonExceptionalRecursionLimiter nerl(&normalizer->sharedState->counters.recursionCount); + if (!nerl.isOk(DFInt::LuauSubtypingRecursionLimit)) return SubtypingResult{false, true}; - if (FFlag::LuauUnifyWithSubtyping2) - { - env.iterationCount++; - if (FInt::LuauSubtypingIterationLimit > 0 && env.iterationCount >= FInt::LuauSubtypingIterationLimit) - return SubtypingResult{false, true}; - } + env.iterationCount++; + if (FInt::LuauSubtypingIterationLimit > 0 && env.iterationCount >= FInt::LuauSubtypingIterationLimit) + return SubtypingResult{false, true}; subTy = follow(subTy); superTy = follow(superTy); @@ -796,27 +747,16 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub ScopedSeenSet ssp{seenTypes, typePair}; - // Within the scope to which a generic belongs, that generic should be - // tested as though it were its upper bounds. We do not yet support bounded - // generics, so the upper bound is always unknown. - if (!FFlag::LuauUnifyWithSubtyping2) - { - if (auto subGeneric = get(subTy); subGeneric && subsumes(subGeneric->scope, scope)) - return isCovariantWith(env, builtinTypes->neverType, superTy, scope); - if (auto superGeneric = get(superTy); superGeneric && subsumes(superGeneric->scope, scope)) - return isCovariantWith(env, subTy, builtinTypes->unknownType, scope); - } - SubtypingResult result; - if (FFlag::LuauUnifyWithSubtyping2 && get2(subTy, superTy)) + if (get2(subTy, superTy)) { // Any two free types are potentially subtypes of one another because // both of them could be narrowed to never. result = {true}; result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); } - else if (auto superFree = get(superTy); FFlag::LuauUnifyWithSubtyping2 && superFree) + else if (auto superFree = get(superTy)) { // FIXME CLI-185582: See comment below with the same ticket number. @@ -833,7 +773,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (result.isSubtype) result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); } - else if (auto subFree = get(subTy); FFlag::LuauUnifyWithSubtyping2 && subFree) + else if (auto subFree = get(subTy)) { // FIXME CLI-185582: When combined with unification via subtyping, this // can allow generics to appear upper bounds, but unless this is used @@ -860,14 +800,17 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else result = {false}; } - else if (FFlag::LuauUnifyWithSubtyping2 && (is(subTy) || is(superTy))) + else if ((is(subTy) || is(superTy))) { result = {true}; result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); } - else if (auto subGeneric = get(subTy); FFlag::LuauUnifyWithSubtyping2 && subGeneric && subsumes(subGeneric->scope, scope)) + // TODO: These branches are entirely incorrect. We should never consider + // generic types to "always" be a sub type or super type of another type + // in a given scope. + else if (auto subGeneric = get(subTy); subGeneric && subsumes(subGeneric->scope, scope)) return isCovariantWith(env, builtinTypes->neverType, superTy, scope); - else if (auto superGeneric = get(superTy); FFlag::LuauUnifyWithSubtyping2 && superGeneric && subsumes(superGeneric->scope, scope)) + else if (auto superGeneric = get(superTy); superGeneric && subsumes(superGeneric->scope, scope)) return isCovariantWith(env, subTy, builtinTypes->unknownType, scope); else if (get(superTy)) result = {true}; @@ -882,8 +825,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub // As per TAPL: A | B <: T iff A <: T && B <: T result = isCovariantWith(env, builtinTypes->unknownType, superTy, scope).andAlso(isCovariantWith(env, builtinTypes->errorType, superTy, scope)); - if (FFlag::LuauMorePreciseErrorSuppression) - result.isErrorSuppressing = true; + result.isErrorSuppressing = true; } else if (get(superTy) && !get(subTy) && !get(subTy)) { @@ -892,13 +834,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub LUAU_ASSERT(!get(subTy)); // TODO: replace with ice. bool errorSuppressing = nullptr != get(subTy); - if (FFlag::LuauMorePreciseErrorSuppression) - { - result.isSubtype = !errorSuppressing; - result.isErrorSuppressing = errorSuppressing; - } - else - result = {!errorSuppressing}; + result.isSubtype = !errorSuppressing; + result.isErrorSuppressing = errorSuppressing; } else if (get(subTy)) result = {true}; @@ -907,8 +844,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (get(subTy)) { result = {true}; - if (FFlag::LuauMorePreciseErrorSuppression) - result.isErrorSuppressing = true; + result.isErrorSuppressing = true; } else if (auto subTypeFunctionInstance = get(subTy)) { @@ -949,6 +885,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub result.isCacheable = false; } } + else if (auto p = get2(subTy, superTy); FFlag::LuauSubtypeUnionsTogether && p) + { + result = isCovariantWith(env, p.first, p.second, scope); + if (!result.isSubtype && !result.normalizationTooComplex) + result = trySemanticSubtyping(env, subTy, superTy, scope, result); + } else if (auto subUnion = get(subTy)) result = isCovariantWith(env, subUnion, superTy, scope); else if (auto superUnion = get(superTy)) @@ -965,55 +907,6 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (!result.isSubtype && !result.normalizationTooComplex) result = trySemanticSubtyping(env, subTy, superTy, scope, result); } - else if (auto pair = get2(subTy, superTy); !FFlag::LuauUnifyWithSubtyping2 && pair) - { - // Any two free types are potentially subtypes of one another because - // both of them could be narrowed to never. - result = {true}; - result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); - } - else if (auto superFree = get(superTy); !FFlag::LuauUnifyWithSubtyping2 && superFree) - { - // Given SubTy <: (LB <: SuperTy <: UB) - // - // If SubTy <: UB, then it is possible that SubTy <: SuperTy. - // If SubTy upperBound, scope); - - if (result.isSubtype) - result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); - } - else if (auto subFree = get(subTy); !FFlag::LuauUnifyWithSubtyping2 && subFree) - { - // Given (LB <: SubTy <: UB) <: SuperTy - // - // If UB <: SuperTy, then it is certainly the case that SubTy <: SuperTy. - // If SuperTy <: UB and LB <: SuperTy, then it is possible that UB will later be narrowed such that SubTy <: SuperTy. - // If LB lowerBound, superTy, scope); - result.isSubtype = r.isSubtype; - result.isErrorSuppressing = r.isErrorSuppressing; - if (r.isSubtype) - result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); - } - else - { - if (isCovariantWith(env, subFree->lowerBound, superTy, scope).isSubtype) - { - result = {true}; - result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); - } - else - result = {false}; - } - } else if (auto p = get2(subTy, superTy)) { // We use `isContravariantWith` here in order to make sure that the @@ -1047,35 +940,29 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub result = isCovariantWith(env, p, scope); else if (auto p = get2(subTy, superTy)) { - const bool forceCovariantTest = uniqueTypes != nullptr && uniqueTypes->contains(subTy); + const bool forceCovariantTest = + FFlag::LuauBidirectionalInferenceSimplifyTables ? false : uniqueTypes != nullptr && uniqueTypes->contains(subTy); result = isCovariantWith(env, p.first, p.second, forceCovariantTest, scope); - if (FFlag::LuauUnifyWithSubtyping2) + if (result.isSubtype && !p.first->indexer && p.second->indexer && p.first->state != TableState::Sealed) { - if (result.isSubtype && !p.first->indexer && p.second->indexer && p.first->state != TableState::Sealed) - { - // FIXME CLI-182960 - // - // Currently, unification is also the mechanism by which unsealed - // tables may receive an indexer. If we've observed that this is - // already a subtype and this is something of the form: - // - // {| ... |} <: { [A]: B ... } - // - // Then add an assumed constraint stating such. - result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); - } + // FIXME CLI-182960 + // + // Currently, unification is also the mechanism by which unsealed + // tables may receive an indexer. If we've observed that this is + // already a subtype and this is something of the form: + // + // {| ... |} <: { [A]: B ... } + // + // Then add an assumed constraint stating such. + result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); } } else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p, scope); else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p, scope); - else if (FFlag::LuauTableFreezeCheckIsSubtype && get2(subTy, superTy)) - { - // When FFlag::LuauTableFreezeCheckIsSubtype is clipped, will update the `if` to follow the same pattern of `auto p = get2<...>` as above. - auto p = get2(subTy, superTy); + else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p, scope); - } else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p, scope); else if (auto p = get2(subTy, superTy)) @@ -1105,16 +992,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub */ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp, NotNull scope) { - UnifierCounters& counters = normalizer->sharedState->counters; - std::optional rc; - - if (FFlag::LuauSubtypingPackRecursionLimits) - { - rc.emplace(&counters.recursionCount); - - if (DFInt::LuauSubtypingRecursionLimit > 0 && counters.recursionCount > DFInt::LuauSubtypingRecursionLimit) - return SubtypingResult{false, true}; - } + NonExceptionalRecursionLimiter nerl{&normalizer->sharedState->counters.recursionCount}; + if (!nerl.isOk(DFInt::LuauSubtypingRecursionLimit)) + return SubtypingResult{false, true}; subTp = follow(subTp); superTp = follow(superTp); @@ -1129,8 +1009,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId const size_t headSize = std::min(subHead.size(), superHead.size()); - std::vector results; - results.reserve(std::max(subHead.size(), superHead.size()) + 1); + // SubtypingResult is pretty heavy, we keep it as a pointer for stack pressure reasons. + std::unique_ptr result = std::make_unique(); + result->isSubtype = true; if (subTp == superTp) return {true}; @@ -1138,9 +1019,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId // Match head types pairwise for (size_t i = 0; i < headSize; ++i) - results.push_back( - isCovariantWith(env, subHead[i], superHead[i], scope).withBothComponent(TypePath::Index{i, TypePath::Index::Variant::Pack}) - ); + result->andAlso(isCovariantWith(env, subHead[i], superHead[i], scope).withBothComponent(TypePath::Index{i, TypePath::Index::Variant::Pack})); // Handle mismatched head sizes @@ -1148,23 +1027,23 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { if (subTail) { - std::optional sr = isSubTailCovariantWith(env, results, subTp, *subTail, superTp, headSize, superHead, superTail, scope); - if (sr) - return *sr; + auto earlyExit = isSubTailCovariantWith(env, *result, subTp, *subTail, superTp, headSize, superHead, superTail, scope); + if (earlyExit == EarlyExit::Yes) + return *result; } else { - results.push_back({false}); - return SubtypingResult::all(results); + result->andAlso({false}); + return *result; } } else if (subHead.size() > superHead.size()) { if (superTail) { - std::optional sr = isCovariantWithSuperTail(env, results, subTp, headSize, subHead, subTail, superTp, *superTail, scope); - if (sr) - return *sr; + auto earlyExit = isCovariantWithSuperTail(env, *result, subTp, headSize, subHead, subTail, superTp, *superTail, scope); + if (earlyExit == EarlyExit::Yes) + return *result; } else return {false}; @@ -1176,29 +1055,29 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { if (auto p = get2(*subTail, *superTail)) { - results.push_back(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); + result->andAlso(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); } else if (auto p = get2(*subTail, *superTail)) { - results.push_back(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); + result->andAlso(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); } else if (auto p = get2(*subTail, *superTail)) { - results.push_back(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); + result->andAlso(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); } else if (auto p = get2(*subTail, *superTail)) { - results.push_back(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); + result->andAlso(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); } - else if (FFlag::LuauUnifyWithSubtyping2 && (is(*subTail) || is(*superTail))) + else if ((is(*subTail) || is(*superTail))) { - results.push_back( + result->andAlso( SubtypingResult{true}.withBothComponent(TypePath::PackField::Tail).withAssumedConstraint(PackSubtypeConstraint{*subTail, *superTail}) ); } else if (get(*subTail) || get(*superTail)) // error type is fine on either side - results.push_back(SubtypingResult{true}.withBothComponent(TypePath::PackField::Tail)); + result->andAlso(SubtypingResult{true}.withBothComponent(TypePath::PackField::Tail)); else if (get(*subTail) || get(*superTail)) { // This seems incorrect in the event that the heads don't match ... @@ -1222,7 +1101,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { return isTailCovariantWithTail(env, scope, *subTail, g, Nothing{}); } - else if (FFlag::LuauUnifyWithSubtyping2 && is(*subTail)) + else if (is(*subTail)) { // This is the case where: // 1. Both the `superTp` and `subTp` have the same number of types in the head @@ -1254,15 +1133,15 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId } else if (auto g = get(*superTail)) { - results.push_back(isTailCovariantWithTail(env, scope, Nothing{}, *superTail, g)); + result->andAlso(isTailCovariantWithTail(env, scope, Nothing{}, *superTail, g)); } - else if (FFlag::LuauUnifyWithSubtyping2 && is(*superTail)) + else if (is(*superTail)) { // This is the case where: // 1. Both the `superTp` and `subTp` have the same number of types in the head // 2. `subTp` does not have a tail // 3. `superTp` has a free tail - results.push_back( + result->andAlso( SubtypingResult{true} .withBothComponent(TypePath::PackField::Tail) .withAssumedConstraint(PackSubtypeConstraint{builtinTypes->emptyTypePack, *superTail}) @@ -1274,11 +1153,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId .withError({scope->location, UnexpectedTypePackInSubtyping{*superTail}}); } - SubtypingResult result = SubtypingResult::all(results); - - assertReasoningValid(subTp, superTp, result, builtinTypes, arena); + assertReasoningValid(subTp, superTp, *result, builtinTypes, arena); - return result; + return *result; } /* Check the tail of the subtype pack against a slice of the finite part of the @@ -1296,9 +1173,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId * SubtypingResult, it should be considered to be the result for the entire pack * subtyping relation. It is not necessary to further check the tails. */ -std::optional Subtyping::isSubTailCovariantWith( +Subtyping::EarlyExit Subtyping::isSubTailCovariantWith( SubtypingEnvironment& env, - std::vector& outputResults, + SubtypingResult& outputResult, TypePackId subTp, TypePackId subTail, TypePackId superTp, @@ -1311,10 +1188,10 @@ std::optional Subtyping::isSubTailCovariantWith( if (auto vt = get(subTail)) { for (size_t i = superHeadStartIndex; i < superHead.size(); ++i) - outputResults.push_back(isCovariantWith(env, vt->ty, superHead[i], scope) - .withSubPath(TypePath::PathBuilder().tail().variadic().build()) - .withSuperComponent(TypePath::Index{i, TypePath::Index::Variant::Pack})); - return std::nullopt; + outputResult.andAlso(isCovariantWith(env, vt->ty, superHead[i], scope) + .withSubPath(TypePath::PathBuilder().tail().variadic().build()) + .withSuperComponent(TypePath::Index{i, TypePath::Index::Variant::Pack})); + return EarlyExit::No; } else if (get(subTail)) { @@ -1352,28 +1229,33 @@ std::optional Subtyping::isSubTailCovariantWith( } } - outputResults.push_back(result); - return SubtypingResult::all(outputResults); + outputResult.andAlso(result); + return EarlyExit::Yes; } else if (get(subTail)) - return SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail); - else if (FFlag::LuauUnifyWithSubtyping2 && get(subTail)) + { + outputResult = SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail); + return EarlyExit::Yes; + } + else if (get(subTail)) { TypePackId superTailPack = sliceTypePack(superHeadStartIndex, superTp, superHead, superTail, builtinTypes, arena); - outputResults.push_back( + outputResult.andAlso( SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail).withAssumedConstraint(PackSubtypeConstraint{subTail, superTailPack}) ); - return SubtypingResult::all(outputResults); + return EarlyExit::Yes; } else - return SubtypingResult{false} - .withSubComponent(TypePath::PackField::Tail) - .withError({scope->location, UnexpectedTypePackInSubtyping{subTail}}); + { + outputResult = + SubtypingResult{false}.withSubComponent(TypePath::PackField::Tail).withError({scope->location, UnexpectedTypePackInSubtyping{subTail}}); + return EarlyExit::Yes; + } } -std::optional Subtyping::isCovariantWithSuperTail( +Subtyping::EarlyExit Subtyping::isCovariantWithSuperTail( SubtypingEnvironment& env, - std::vector& results, + SubtypingResult& outputResult, TypePackId subTp, size_t subHeadStartIndex, const std::vector& subHead, @@ -1386,10 +1268,10 @@ std::optional Subtyping::isCovariantWithSuperTail( if (auto vt = get(superTail)) { for (size_t i = subHeadStartIndex; i < subHead.size(); ++i) - results.push_back(isCovariantWith(env, subHead[i], vt->ty, scope) - .withSubComponent(TypePath::Index{i, TypePath::Index::Variant::Pack}) - .withSuperPath(TypePath::PathBuilder().tail().variadic().build())); - return std::nullopt; + outputResult.andAlso(isCovariantWith(env, subHead[i], vt->ty, scope) + .withSubComponent(TypePath::Index{i, TypePath::Index::Variant::Pack}) + .withSuperPath(TypePath::PathBuilder().tail().variadic().build())); + return EarlyExit::No; } else if (get(superTail)) { @@ -1426,23 +1308,29 @@ std::optional Subtyping::isCovariantWithSuperTail( } } - results.push_back(result); - return SubtypingResult::all(results); + outputResult.andAlso(result); + return EarlyExit::Yes; } else if (get(superTail)) - return SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail); - else if (FFlag::LuauUnifyWithSubtyping2 && is(superTail)) + { + outputResult = SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail); + return EarlyExit::Yes; + } + else if (is(superTail)) { TypePackId subTailPack = sliceTypePack(subHeadStartIndex, subTp, subHead, subTail, builtinTypes, arena); - results.push_back( + outputResult.andAlso( SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail).withAssumedConstraint({PackSubtypeConstraint{subTailPack, superTail}}) ); - return SubtypingResult::all(results); + return EarlyExit::Yes; } else - return SubtypingResult{false} - .withSuperComponent(TypePath::PackField::Tail) - .withError({scope->location, UnexpectedTypePackInSubtyping{superTail}}); + { + outputResult = SubtypingResult{false} + .withSuperComponent(TypePath::PackField::Tail) + .withError({scope->location, UnexpectedTypePackInSubtyping{superTail}}); + return EarlyExit::Yes; + } } SubtypingResult Subtyping::isTailCovariantWithTail( @@ -1733,6 +1621,17 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub SubtypingResult result{false}; + if (FFlag::LuauDontBindOptionalGenericToNil) + { + // First pass: If the union already includes subTy, stop. Do not + // attempt to bind any generics. + for (TypeId ty : superUnion) + { + if (follow(ty) == subTy) + return {true}; + } + } + size_t index = 0; for (TypeId ty : superUnion) { @@ -1742,64 +1641,114 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub return SubtypingResult{false, /* normalizationTooComplex */ true}; if (next.isSubtype) - return FFlag::LuauUnifyWithSubtyping2 ? next : SubtypingResult{true}; + return next; - if (FFlag::LuauMorePreciseErrorSuppression) + result.andAlso(next.withSuperComponent(TypePath::Index{index, TypePath::Index::Variant::Union})); + ++index; + } + + if (FFlag::LuauDropUnionSubtypeReasoning) + { + LUAU_ASSERT(!result.isSubtype); + result.reasoning.clear(); + } + + return result; +} + +LUAU_NOINLINE +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, const UnionType* superUnion, NotNull scope) +{ + LUAU_ASSERT(FFlag::LuauSubtypeUnionsTogether); + // A | B | C <: D | E | F + // + // ... when all of A, B and C are subtypes of D | E | F. However, we can + // optimize this (and avoid some correctness issues) by skipping any + // options in the union subtype that are present in the union super type. + // A trivial example is: + // + // -- We can skip the `nil` part of the subtype. + // T? <: U? iff T <: U + // + // NOTE: The correct way to do this would be to unconditionally + // semantically subtype unions. + std::unique_ptr result = std::make_unique(); + result->isSubtype = true; + + TypeIds superUnionOptions; + superUnionOptions.reserve(superUnion->options.size()); + superUnionOptions.insert(begin(superUnion), end(superUnion)); + + for (TypeId ty : superUnionOptions) + LUAU_ASSERT(ty == follow(ty)); + + size_t subIndex = 0; + + for (TypeId ty : subUnion) + { + if (!superUnionOptions.contains(ty)) { - result.andAlso(next.withSuperComponent(TypePath::Index{index, TypePath::Index::Variant::Union})); - ++index; + result->andAlso(isCovariantWith(env, ty, superUnion, scope).withSubComponent(TypePath::Index{subIndex, TypePath::Index::Variant::Union})); + if (result->normalizationTooComplex) + return SubtypingResult{false, /* normalizationTooComplex */ true}; } + + subIndex++; } - return result; + return *result; } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, TypeId superTy, NotNull scope) { // As per TAPL: A | B <: T iff A <: T && B <: T - std::vector subtypings; + // Keep in the heap for stack pressure reasons. + std::unique_ptr result = std::make_unique(); + result->isSubtype = true; size_t i = 0; for (TypeId ty : subUnion) { - subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Union})); + result->andAlso(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Union})); - if (subtypings.back().normalizationTooComplex) + if (result->normalizationTooComplex) return SubtypingResult{false, /* normalizationTooComplex */ true}; } - return SubtypingResult::all(subtypings); + return *result; } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const IntersectionType* superIntersection, NotNull scope) { // As per TAPL: T <: A & B iff T <: A && T <: B - std::vector subtypings; + std::unique_ptr result = std::make_unique(); + result->isSubtype = true; size_t i = 0; for (TypeId ty : superIntersection) { - subtypings.push_back(isCovariantWith(env, subTy, ty, scope).withSuperComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection})); + result->andAlso(isCovariantWith(env, subTy, ty, scope).withSuperComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection})); - if (subtypings.back().normalizationTooComplex) + if (result->normalizationTooComplex) return SubtypingResult{false, /* normalizationTooComplex */ true}; } - return SubtypingResult::all(subtypings); + return *result; } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const IntersectionType* subIntersection, TypeId superTy, NotNull scope) { // As per TAPL: A & B <: T iff A <: T || B <: T - std::vector subtypings; + std::unique_ptr result = std::make_unique(); + result->isSubtype = false; size_t i = 0; for (TypeId ty : subIntersection) { - subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection})); + result->orElse(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection})); - if (subtypings.back().normalizationTooComplex) + if (result->normalizationTooComplex) return SubtypingResult{false, /* normalizationTooComplex */ true}; } - return SubtypingResult::any(subtypings); + return *result; } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NegationType* subNegation, TypeId superTy, NotNull scope) @@ -1830,39 +1779,35 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega { // ¬(A ∪ B) ~ ¬A ∩ ¬B // follow intersection rules: A & B <: T iff A <: T && B <: T - std::vector subtypings; + result = {true}; for (TypeId ty : u) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy, scope).withSubComponent(TypePath::TypeField::Negated)); + result.andAlso(isCovariantWith(env, negatedPart->ty, superTy, scope).withSubComponent(TypePath::TypeField::Negated)); else { NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy, scope)); + result.andAlso(isCovariantWith(env, &negatedTmp, superTy, scope)); } } - - result = SubtypingResult::all(subtypings); } else if (auto i = get(negatedTy)) { // ¬(A ∩ B) ~ ¬A ∪ ¬B // follow union rules: A | B <: T iff A <: T || B <: T - std::vector subtypings; + result = {false}; for (TypeId ty : i) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy, scope).withSubComponent(TypePath::TypeField::Negated)); + result.orElse(isCovariantWith(env, negatedPart->ty, superTy, scope).withSubComponent(TypePath::TypeField::Negated)); else { NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy, scope)); + result.orElse(isCovariantWith(env, &negatedTmp, superTy, scope)); } } - - result = SubtypingResult::any(subtypings); } else if (is(negatedTy)) { @@ -1897,45 +1842,42 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type else if (is(negatedTy)) { // ¬any ~ any - result = FFlag::LuauUnifyWithSubtyping2 ? isCovariantWith(env, subTy, negatedTy, scope) : isSubtype(subTy, negatedTy, scope); + result = isCovariantWith(env, subTy, negatedTy, scope); } else if (auto u = get(negatedTy)) { // ¬(A ∪ B) ~ ¬A ∩ ¬B // follow intersection rules: A & B <: T iff A <: T && B <: T std::vector subtypings; + result = {true}; for (TypeId ty : u) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(env, subTy, negatedPart->ty, scope)); + result.andAlso(isCovariantWith(env, subTy, negatedPart->ty, scope)); else { NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(env, subTy, &negatedTmp, scope)); + result.andAlso(isCovariantWith(env, subTy, &negatedTmp, scope)); } } - - return SubtypingResult::all(subtypings); } else if (auto i = get(negatedTy)) { // ¬(A ∩ B) ~ ¬A ∪ ¬B // follow union rules: A | B <: T iff A <: T || B <: T - std::vector subtypings; + result = {false}; for (TypeId ty : i) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(env, subTy, negatedPart->ty, scope)); + result.orElse(isCovariantWith(env, subTy, negatedPart->ty, scope)); else { NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(env, subTy, &negatedTmp, scope)); + result.orElse(isCovariantWith(env, subTy, &negatedTmp, scope)); } } - - return SubtypingResult::any(subtypings); } else if (auto p = get2(subTy, negatedTy)) { @@ -2031,6 +1973,9 @@ SubtypingResult Subtyping::isCovariantWith( { SubtypingResult result{true}; + // Either this flag is off or `forceCovariantTest` is false. + LUAU_ASSERT(!FFlag::LuauBidirectionalInferenceSimplifyTables || !forceCovariantTest); + if (subTable->props.empty() && !subTable->indexer && subTable->state == TableState::Sealed && superTable->indexer) { // While it is certainly the case that {} props) { - std::vector results; + // If the sub table has the property with the specific name: then + // check whether the two are invariant subtypes (the below overload + // will do an invariant check). if (auto subIter = subTable->props.find(name); subIter != subTable->props.end()) - results.push_back(isCovariantWith(env, subIter->second, superProp, name, forceCovariantTest, scope)); - else if (subTable->indexer) { - if (isCovariantWith(env, builtinTypes->stringType, subTable->indexer->indexType, scope).isSubtype) + record(isCovariantWith(env, subIter->second, superProp, name, forceCovariantTest, scope)); + } + // Otherwise, if the sub table has an indexer where the key type is a + // super type of string, then use that as the "property" type, e.g. + // as in: + // + // { [string]: number } <: { foo: number } + // + else if (subTable->indexer && isCovariantWith(env, builtinTypes->stringType, subTable->indexer->indexType, scope).isSubtype) + { + if (superProp.isShared()) { - if (superProp.isShared()) + if (subTable->indexer->isReadOnly) + // A read-only indexer cannot satisfy a read-write property requirement. + record( + SubtypingResult{false}.withSubComponent(TypePath::TypeField::IndexResult).withSuperComponent(TypePath::Property::read(name)) + ); + else + record(isInvariantWith(env, subTable->indexer->indexResultType, *superProp.readTy, scope) + .withSubComponent(TypePath::TypeField::IndexResult) + .withSuperComponent(TypePath::Property::read(name))); + } + else + { + if (superProp.readTy) { - results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, *superProp.readTy, scope) - .withSubComponent(TypePath::TypeField::IndexResult) - .withSuperComponent(TypePath::Property::read(name))); + record(isCovariantWith(env, subTable->indexer->indexResultType, *superProp.readTy, scope) + .withSubComponent(TypePath::TypeField::IndexResult) + .withSuperComponent(TypePath::Property::read(name))); } - else + if (superProp.writeTy) { - if (superProp.readTy) - results.push_back(isCovariantWith(env, subTable->indexer->indexResultType, *superProp.readTy, scope) - .withSubComponent(TypePath::TypeField::IndexResult) - .withSuperComponent(TypePath::Property::read(name))); - if (superProp.writeTy) - results.push_back(isContravariantWith(env, subTable->indexer->indexResultType, *superProp.writeTy, scope) - .withSubComponent(TypePath::TypeField::IndexResult) - .withSuperComponent(TypePath::Property::write(name))); + if (subTable->indexer->isReadOnly) + record( + SubtypingResult{false} + .withSubComponent(TypePath::TypeField::IndexResult) + .withSuperComponent(TypePath::Property::write(name)) + ); + else + record(isContravariantWith(env, subTable->indexer->indexResultType, *superProp.writeTy, scope) + .withSubComponent(TypePath::TypeField::IndexResult) + .withSuperComponent(TypePath::Property::write(name))); } } } else if (FFlag::LuauSubtypingMissingPropertiesAsNil) { - SubtypingResult result = isCovariantWith(env, Property::readonly(builtinTypes->nilType), superProp, name, forceCovariantTest, scope); - // We must ignore the actual reasoning from here because the subtype doesn't have a property to traverse into later. - // If there is a type error, we want to point at this spot as being responsible for it! - result.reasoning.clear(); - results.push_back(result); - } - - if (results.empty()) - return SubtypingResult{false}; - - if (FFlag::LuauMorePreciseErrorSuppression) - { - bool isSubtype = true; - for (const SubtypingResult& sr : results) - isSubtype &= sr.isSubtype; + SubtypingResult result; - // If the first failed subtype test is a suppressing failure, then - // we set the suppression bit in case there are no subsequent - // non-suppressing failures. - // - // If we at any point encounter a non-suppressing failure, then this - // whole subtype test is a non-suppressing failure. - if (result.isSubtype && !isSubtype) + if (FFlag::LuauImproveUniqueTableWidthSubtyping) { - for (const SubtypingResult& sr : results) - result.andAlso(sr, SubtypingSuppressionPolicy::Any); + if (forceCovariantTest) + result = isCovariantWith(env, Property::rw(builtinTypes->nilType), superProp, name, forceCovariantTest, scope); + else + result = isCovariantWith(env, Property::readonly(builtinTypes->nilType), superProp, name, forceCovariantTest, scope); } else { - for (const SubtypingResult& sr : results) - result.andAlso(sr, SubtypingSuppressionPolicy::All); + result = isCovariantWith(env, Property::readonly(builtinTypes->nilType), superProp, name, forceCovariantTest, scope); } + + // We must ignore the actual reasoning from here because the subtype doesn't have a property to traverse into later. + // If there is a type error, we want to point at this spot as being responsible for it! + result.reasoning.clear(); + record(std::move(result)); } + // If the subtable doesn't have a string indexer and the required + // property does not exist, we can exit early. else { - result.andAlso(SubtypingResult::all(results)); + return SubtypingResult{false}; } } if (superTable->indexer) { if (subTable->indexer) - result.andAlso(isInvariantWith(env, *subTable->indexer, *superTable->indexer, scope)); + { + // We say covariant here, but the implementation of + // isCovariantWith() properly handles variance of the index + // result type. + record(isCovariantWith(env, *subTable->indexer, *superTable->indexer, scope)); + } else if (subTable->state != TableState::Sealed) { // As above, we assume that {| |} <: {T} because the unsealed table @@ -2125,6 +2115,7 @@ SubtypingResult Subtyping::isCovariantWith( return {false}; } + result.isErrorSuppressing = hasErrorSuppression && shouldSuppressErrors; return result; } @@ -2216,7 +2207,6 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Meta if (prop.readTy && fauxSubTable.props.find(name) == fauxSubTable.props.end()) fauxSubTable.props[name] = Property::readonly(*prop.readTy); } - return isCovariantWith(env, &fauxSubTable, superTable, /* forceCovariantTest */ false, scope); } else @@ -2233,8 +2223,6 @@ SubtypingResult Subtyping::isCovariantWith( NotNull scope ) { - LUAU_ASSERT(FFlag::LuauTableFreezeCheckIsSubtype); - // Metatable types can be subtypes of primitive table types if their table component is a subtype of table. if (superPrim->type == PrimitiveType::Table) { @@ -2388,11 +2376,21 @@ SubtypingResult Subtyping::isCovariantWith( if (*subFunction->argTypes == *superFunction->argTypes && *subFunction->retTypes == *superFunction->retTypes) { - if (superFunction->generics.size() != subFunction->generics.size()) + // It's fine to upcast a function with generics to a function without, for example: + // + // local f: ({number}) -> number = (nil :: ({T}) -> T) + // + // ... or even ... + // + // local f: () -> () = (nil :: () -> ()) + // + // Intuitively: a generic function should always be a subtype of its instantiations. + if (superFunction->generics.size() != subFunction->generics.size() && !superFunction->generics.empty()) result.andAlso({false}).withError( TypeError{scope->location, GenericTypeCountMismatch{superFunction->generics.size(), subFunction->generics.size()}} ); - if (superFunction->genericPacks.size() != subFunction->genericPacks.size()) + + if (superFunction->genericPacks.size() != subFunction->genericPacks.size() && !superFunction->genericPacks.empty()) result.andAlso({false}).withError( TypeError{scope->location, GenericTypePackCountMismatch{superFunction->genericPacks.size(), subFunction->genericPacks.size()}} ); @@ -2449,8 +2447,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Prim LUAU_ASSERT(*it->second.readTy); if (auto stringTable = get(*it->second.readTy)) + { result.orElse(isCovariantWith(env, stringTable, superTable, /*forceCovariantTest*/ false, scope) .withSubPath(TypePath::PathBuilder().mt().readProp("__index").build())); + } } } } @@ -2484,8 +2484,10 @@ SubtypingResult Subtyping::isCovariantWith( LUAU_ASSERT(*it->second.readTy); if (auto stringTable = get(*it->second.readTy)) + { result.orElse(isCovariantWith(env, stringTable, superTable, /*forceCovariantTest*/ false, scope) .withSubPath(TypePath::PathBuilder().mt().readProp("__index").build())); + } } } } @@ -2500,11 +2502,21 @@ SubtypingResult Subtyping::isCovariantWith( NotNull scope ) { - return isInvariantWith(env, subIndexer.indexType, superIndexer.indexType, scope) - .withBothComponent(TypePath::TypeField::IndexLookup) - .andAlso( - isInvariantWith(env, subIndexer.indexResultType, superIndexer.indexResultType, scope).withBothComponent(TypePath::TypeField::IndexResult) - ); + SubtypingResult result{false}; + if (subIndexer.isReadOnly && !superIndexer.isReadOnly) + return result.withBothComponent(TypePath::TypeField::IndexResult); + + result = isInvariantWith(env, subIndexer.indexType, superIndexer.indexType, scope).withBothComponent(TypePath::TypeField::IndexLookup); + + // Value-type variance: read-only super → covariant; read-write super → invariant. + if (superIndexer.isReadOnly) + result.andAlso(isCovariantWith(env, subIndexer.indexResultType, superIndexer.indexResultType, scope) + .withBothComponent(TypePath::TypeField::IndexResult)); + else + result.andAlso(isInvariantWith(env, subIndexer.indexResultType, superIndexer.indexResultType, scope) + .withBothComponent(TypePath::TypeField::IndexResult)); + + return result; } SubtypingResult Subtyping::isCovariantWith( @@ -2535,9 +2547,19 @@ SubtypingResult Subtyping::isCovariantWith( if (superProp.isReadWrite()) { if (subProp.isReadOnly()) - res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::read(name))); + { + if (FFlag::LuauPropertyModifierMismatchErrors) + res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::read(name)).withPropertyModifierViolation()); + else + res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::read(name))); + } else if (subProp.isWriteOnly()) - res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::write(name))); + { + if (FFlag::LuauPropertyModifierMismatchErrors) + res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::write(name)).withPropertyModifierViolation()); + else + res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::write(name))); + } } } @@ -2697,21 +2719,25 @@ SubtypingResult Subtyping::isCovariantWith( SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeIds& subTypes, const TypeIds& superTypes, NotNull scope) { - std::vector results; + auto result = std::make_unique(); + result->isSubtype = true; for (TypeId subTy : subTypes) { - results.emplace_back(); + auto innerResult = std::make_unique(); + for (TypeId superTy : superTypes) { - results.back().orElse(isCovariantWith(env, subTy, superTy, scope)); + innerResult->orElse(isCovariantWith(env, subTy, superTy, scope)); - if (results.back().normalizationTooComplex) + if (innerResult->normalizationTooComplex) return SubtypingResult{false, /* normalizationTooComplex */ true}; } + + result->andAlso(*innerResult); } - return SubtypingResult::all(results); + return *result; } SubtypingResult Subtyping::isCovariantWith( @@ -2814,7 +2840,8 @@ TypeId Subtyping::makeAggregateType(const Container& container, TypeId orElse) std::pair Subtyping::handleTypeFunctionReductionResult(const TypeFunctionInstanceType* functionInstance, NotNull scope) { - TypeFunctionContext context{arena, builtinTypes, scope, normalizer, typeFunctionRuntime, iceReporter, NotNull{&limits}}; + TypeFunctionContext context{arena, builtinTypes, scope, normalizer, typeFunctionRuntime, iceReporter, NotNull{&limits}, NotNull{this}}; + TypeId function = arena->addType(*functionInstance); FunctionGraphReductionResult result = reduceTypeFunctions(function, {}, NotNull{&context}, true); ErrorVec errors; @@ -2863,51 +2890,31 @@ SubtypingResult Subtyping::checkGenericBounds( const auto& [lb, ub] = bounds; - TypeIds lbTypes; + UnionBuilder aggregateLowerBound{arena, builtinTypes}; + aggregateLowerBound.reserve(lb.size()); for (TypeId t : lb) { - t = follow(t); - if (const auto mappedBounds = env.mappedGenerics.find(t)) - { - if (mappedBounds->empty()) // If the generic is no longer in scope, we don't have any info about it - continue; - - auto& [lowerBound, upperBound] = mappedBounds->back(); - // We're populating the lower bounds, so we prioritize the upper bounds of a mapped generic - if (!upperBound.empty()) - lbTypes.insert(upperBound.begin(), upperBound.end()); - else if (!lowerBound.empty()) - lbTypes.insert(lowerBound.begin(), lowerBound.end()); - else - lbTypes.insert(builtinTypes->unknownType); - } - else - lbTypes.insert(t); + if (const auto mappedBounds = env.mappedGenerics.find(t); mappedBounds && mappedBounds->empty()) + continue; + aggregateLowerBound.add(t); } + TypeId lowerBound = aggregateLowerBound.build(); - TypeIds ubTypes; + IntersectionBuilder aggregateUpperBound{arena, builtinTypes}; + aggregateUpperBound.reserve(ub.size()); for (TypeId t : ub) { - t = follow(t); - if (const auto mappedBounds = env.mappedGenerics.find(t)) - { - if (mappedBounds->empty()) // If the generic is no longer in scope, we don't have any info about it - continue; - - auto& [lowerBound, upperBound] = mappedBounds->back(); - // We're populating the upper bounds, so we prioritize the lower bounds of a mapped generic - if (!lowerBound.empty()) - ubTypes.insert(lowerBound.begin(), lowerBound.end()); - else if (!upperBound.empty()) - ubTypes.insert(upperBound.begin(), upperBound.end()); - else - ubTypes.insert(builtinTypes->unknownType); - } - else - ubTypes.insert(t); + if (const auto mappedBounds = env.mappedGenerics.find(t); mappedBounds && mappedBounds->empty()) + continue; + aggregateUpperBound.add(t); } - TypeId lowerBound = makeAggregateType(lbTypes.take(), builtinTypes->neverType); - TypeId upperBound = makeAggregateType(ubTypes.take(), builtinTypes->unknownType); + TypeId upperBound = aggregateUpperBound.build(); + + if (auto substLowerBound = env.applyMappedGenerics(builtinTypes, arena, lowerBound, iceReporter)) + lowerBound = *substLowerBound; + + if (auto substUpperBound = env.applyMappedGenerics(builtinTypes, arena, upperBound, iceReporter)) + upperBound = *substUpperBound; std::shared_ptr nt = normalizer->normalize(upperBound); // we say that the result is true if normalization failed because complex types are likely to be inhabited. diff --git a/Analysis/src/SubtypingUnifier.cpp b/Analysis/src/SubtypingUnifier.cpp index 09c22a86..2306462a 100644 --- a/Analysis/src/SubtypingUnifier.cpp +++ b/Analysis/src/SubtypingUnifier.cpp @@ -8,8 +8,6 @@ #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" -LUAU_FASTFLAG(LuauUnifyWithSubtyping2) - namespace Luau { @@ -30,7 +28,10 @@ bool SubtypingUnifier::canBeUnified(TypeId ty) const return is(ty) || isBlocked(ty); } -SubtypingUnifier::Result SubtypingUnifier::dispatchConstraints(NotNull constraint, std::vector assumedConstraints) const +SubtypingUnifier::Result SubtypingUnifier::dispatchConstraints( + NotNull constraint, + std::vector assumedConstraints +) const { UnifyResult unifierRes = UnifyResult::Ok; // NOTE: You *could* potentially reuse the input vector, but this seems @@ -48,34 +49,6 @@ SubtypingUnifier::Result SubtypingUnifier::dispatchConstraints(NotNull(needle)) - return OccursCheckResult::Pass; - - if (!getMutable(needle)) - reporter->ice("Expected needle pack to be free"); - - while (!getMutable(haystack)) - { - if (needle == haystack) - return OccursCheckResult::Fail; - - if (auto a = get(haystack); a && a->tail) - { - haystack = follow(*a->tail); - continue; - } - - break; - } - - return OccursCheckResult::Pass; -} - std::pair SubtypingUnifier::dispatchOneConstraint( NotNull constraint, @@ -141,7 +114,6 @@ std::pair SubtypingUnifier::dispatchOneConstraint( emplaceTypePack(asMutable(superTp), builtinTypes->errorTypePack); return {UnifyResult::OccursCheckFailed, true}; } - emplaceTypePack(asMutable(superTp), subTp); return {UnifyResult::Ok, true}; } diff --git a/Analysis/src/TableLiteralInference.cpp b/Analysis/src/TableLiteralInference.cpp index a13f5bdb..8f963e9a 100644 --- a/Analysis/src/TableLiteralInference.cpp +++ b/Analysis/src/TableLiteralInference.cpp @@ -6,6 +6,7 @@ #include "Luau/Common.h" #include "Luau/ConstraintSolver.h" #include "Luau/HashUtil.h" +#include "Luau/IterativeTypeVisitor.h" #include "Luau/Simplify.h" #include "Luau/Subtyping.h" #include "Luau/Type.h" @@ -13,9 +14,10 @@ #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" -LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintLambdas3) -LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintStripNilFromFunction) -LUAU_FASTFLAGVARIABLE(LuauPushTypeUnifyConstantHandling) +LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceVariadics) +LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceBetterLambdaHandling) +LUAU_FASTFLAG(LuauBidirectionalInferenceSimplifyTables) +LUAU_FASTFLAG(LuauRelaxConstraintOrderingForFunctionCheck) namespace Luau { @@ -23,6 +25,96 @@ namespace Luau namespace { +struct FindFunctionTypeIn : IterativeTypeVisitor +{ + int numberOfLambdaParameters; + const FunctionType* candidate = nullptr; + bool ambiguous = false; + + explicit FindFunctionTypeIn(int numberOfLambdaParameters) + : IterativeTypeVisitor("FindFunctionTypeIn", true, true) + , numberOfLambdaParameters(numberOfLambdaParameters) + { + } + + bool visit(TypeId) override + { + return false; + } + + bool visit(TypeId, const UnionType&) override + { + return true; + } + + bool visit(TypeId, const IntersectionType&) override + { + return true; + } + + bool visit(TypeId ty, const FunctionType& ftv) override + { + // This logic is a little clowny. + // + // For bidirectional inference we're trying to _guess_ what the user + // is intending so that we can give decent results. For functions, we + // will error if the user doesn't provide exactly the correct number of + // arguments. However, consider: + // + // local f: (ReallyComplexTableType, boolean) -> () = function (tbl) + // tbl.| + // end + // + // ... the user would probably prefer to have autocomplete here while + // they're writing the function, even if we'll eventually error. Or, + // the user may be in nonstrict mode. + // + // On top of that we have to do a bunch of `int` casting here. + if (FFlag::LuauBidirectionalInferenceBetterLambdaHandling) + { + if (candidate == nullptr) + { + candidate = get(ty); + ambiguous = false; + return false; + } + + int candidateDistance = std::abs(int(size(candidate->argTypes)) - numberOfLambdaParameters); + int thisDistance = std::abs(int(size(ftv.argTypes)) - numberOfLambdaParameters); + + if (thisDistance < candidateDistance) + { + candidate = get(ty); + ambiguous = false; + } + else if (thisDistance == candidateDistance) + { + ambiguous = true; + } + } + else + { + if (candidate == nullptr || + std::abs(int(size(candidate->argTypes)) - numberOfLambdaParameters) > std::abs(int(size(ftv.argTypes)) - numberOfLambdaParameters)) + { + candidate = get(ty); + return false; + } + } + return false; + } +}; + +/** + * Is this an expression that we can check has an expected type, for now this + * is limited to literals (including lambdas, "function literals"), groups + * (parenthesized expressions), and if-else expressions. + */ +bool isCheckableExpr(const AstExpr* expr) +{ + return isLiteral(expr) || expr->is() || expr->is(); +} + struct BidirectionalTypePusher { @@ -31,7 +123,7 @@ struct BidirectionalTypePusher NotNull solver; NotNull constraint; - DenseHashSet* genericTypesAndPacks; + NotNull> genericTypesAndPacks; NotNull unifier; NotNull subtyping; @@ -52,25 +144,7 @@ struct BidirectionalTypePusher , astExpectedTypes{astExpectedTypes} , solver{solver} , constraint{constraint} - , genericTypesAndPacks{genericTypesAndPacks.get()} - , unifier{unifier} - , subtyping{subtyping} - { - } - - BidirectionalTypePusher( - NotNull> astTypes, - NotNull> astExpectedTypes, - NotNull solver, - NotNull constraint, - NotNull unifier, - NotNull subtyping - ) - : astTypes{astTypes} - , astExpectedTypes{astExpectedTypes} - , solver{solver} - , constraint{constraint} - , genericTypesAndPacks{nullptr} + , genericTypesAndPacks{genericTypesAndPacks} , unifier{unifier} , subtyping{subtyping} { @@ -78,20 +152,12 @@ struct BidirectionalTypePusher TypeId pushType(TypeId expectedType, const AstExpr* expr) { - if (FFlag::LuauPushTypeConstraintLambdas3) - { - (*astExpectedTypes)[expr] = expectedType; - // We may not have a type here if this is the last argument - // passed to a function call: this is potentially expected - // behavior. - if (!astTypes->contains(expr)) - return solver->builtinTypes->anyType; - } - else if (!astTypes->contains(expr)) - { - LUAU_ASSERT(false); - return solver->builtinTypes->errorType; - } + (*astExpectedTypes)[expr] = expectedType; + // We may not have a type here if this is the last argument + // passed to a function call: this is potentially expected + // behavior. + if (!astTypes->contains(expr)) + return solver->builtinTypes->anyType; TypeId exprType = *astTypes->find(expr); @@ -102,6 +168,14 @@ struct BidirectionalTypePusher expectedType = follow(expectedType); exprType = follow(exprType); + if (FFlag::LuauRelaxConstraintOrderingForFunctionCheck && !isCheckableExpr(expr)) + { + // NOTE: For now we aren't using the result of this function, so + // just return the original expression type. + return exprType; + } + + // NOTE: We cannot block on free types here, as that trivially means // any recursive function would have a cycle, consider: // @@ -127,9 +201,6 @@ struct BidirectionalTypePusher if (is(expectedType)) return exprType; - if (!FFlag::LuauPushTypeConstraintLambdas3) - (*astExpectedTypes)[expr] = expectedType; - if (auto group = expr->as()) { pushType(expectedType, group->expr); @@ -143,193 +214,84 @@ struct BidirectionalTypePusher return exprType; } - if (!isLiteral(expr)) - // NOTE: For now we aren't using the result of this function, so - // just return the original expression type. - return exprType; + if (!FFlag::LuauRelaxConstraintOrderingForFunctionCheck) + { + if (!isLiteral(expr)) + // NOTE: For now we aren't using the result of this function, so + // just return the original expression type. + return exprType; + } - if (FFlag::LuauPushTypeUnifyConstantHandling) + + if (expr->is() || expr->is() || expr->is() || + expr->is()) { - if (expr->is() || expr->is() || expr->is() || - expr->is()) + if (auto ft = get(exprType)) { - if (auto ft = get(exprType)) + if (maybeSingleton(expectedType) && maybeSingleton(ft->lowerBound)) { - if (maybeSingleton(expectedType) && maybeSingleton(ft->lowerBound)) - { - // If we see a pattern like: - // - // local function foo(my_enum: "foo" | "bar" | T) -> T - // return my_enum - // end - // local var = foo("meow") - // - // ... where we are attempting to push a singleton onto any string - // literal, and the lower bound is still a singleton, then snap - // to said lower bound. - solver->bind(constraint, exprType, ft->lowerBound); - return exprType; - } + // If we see a pattern like: + // + // local function foo(my_enum: "foo" | "bar" | T) -> T + // return my_enum + // end + // local var = foo("meow") + // + // ... where we are attempting to push a singleton onto any string + // literal, and the lower bound is still a singleton, then snap + // to said lower bound. + solver->bind(constraint, exprType, ft->lowerBound); + return exprType; + } - // if the upper bound is a subtype of the expected type, we can push the expected type in - Relation upperBoundRelation = relate(ft->upperBound, expectedType); - if (upperBoundRelation == Relation::Subset || upperBoundRelation == Relation::Coincident) - { - solver->bind(constraint, exprType, expectedType); - return exprType; - } + // if the upper bound is a subtype of the expected type, we can push the expected type in + Relation upperBoundRelation = relate(ft->upperBound, expectedType); + if (upperBoundRelation == Relation::Subset || upperBoundRelation == Relation::Coincident) + { + solver->bind(constraint, exprType, expectedType); + return exprType; + } - // likewise, if the lower bound is a subtype, we can force the expected type in - // if this is the case and the previous relation failed, it means that the primitive type - // constraint was going to have to select the lower bound for this type anyway. - Relation lowerBoundRelation = relate(ft->lowerBound, expectedType); - if (lowerBoundRelation == Relation::Subset || lowerBoundRelation == Relation::Coincident) - { - solver->bind(constraint, exprType, expectedType); - return exprType; - } + // likewise, if the lower bound is a subtype, we can force the expected type in + // if this is the case and the previous relation failed, it means that the primitive type + // constraint was going to have to select the lower bound for this type anyway. + Relation lowerBoundRelation = relate(ft->lowerBound, expectedType); + if (lowerBoundRelation == Relation::Subset || lowerBoundRelation == Relation::Coincident) + { + solver->bind(constraint, exprType, expectedType); + return exprType; } } } - else + + if (auto exprLambda = expr->as()) { - if (expr->is()) - { - auto ft = get(exprType); - if (ft && get(ft->lowerBound) && fastIsSubtype(solver->builtinTypes->stringType, ft->upperBound) && - fastIsSubtype(ft->lowerBound, solver->builtinTypes->stringType)) - { - if (maybeSingleton(expectedType) && maybeSingleton(ft->lowerBound)) - { - // If we see a pattern like: - // - // local function foo(my_enum: "foo" | "bar" | T) -> T - // return my_enum - // end - // local var = foo("meow") - // - // ... where we are attempting to push a singleton onto any string - // literal, and the lower bound is still a singleton, then snap - // to said lower bound. - if (FFlag::LuauPushTypeConstraintLambdas3) - { - solver->bind(constraint, exprType, ft->lowerBound); - } - else - { - emplaceType(asMutable(exprType), ft->lowerBound); - solver->unblock(exprType, expr->location); - } - return exprType; - } + const auto lambdaTy = get(exprType); - // if the upper bound is a subtype of the expected type, we can push the expected type in - Relation upperBoundRelation = relate(ft->upperBound, expectedType); - if (upperBoundRelation == Relation::Subset || upperBoundRelation == Relation::Coincident) - { - if (FFlag::LuauPushTypeConstraintLambdas3) - { - solver->bind(constraint, exprType, expectedType); - } - else - { - emplaceType(asMutable(exprType), expectedType); - solver->unblock(exprType, expr->location); - } - return exprType; - } + FindFunctionTypeIn ffti{int(exprLambda->args.size)}; + ffti.run(expectedType); + const FunctionType* expectedLambdaTy = ffti.candidate; - // likewise, if the lower bound is a subtype, we can force the expected type in - // if this is the case and the previous relation failed, it means that the primitive type - // constraint was going to have to select the lower bound for this type anyway. - Relation lowerBoundRelation = relate(ft->lowerBound, expectedType); - if (lowerBoundRelation == Relation::Subset || lowerBoundRelation == Relation::Coincident) - { - if (FFlag::LuauPushTypeConstraintLambdas3) - { - solver->bind(constraint, exprType, expectedType); - } - else - { - emplaceType(asMutable(exprType), expectedType); - solver->unblock(exprType, expr->location); - } - return exprType; - } - } - } - else if (expr->is()) + if (lambdaTy && expectedLambdaTy) { - auto ft = get(exprType); - if (ft && get(ft->lowerBound) && fastIsSubtype(solver->builtinTypes->booleanType, ft->upperBound) && - fastIsSubtype(ft->lowerBound, solver->builtinTypes->booleanType)) + if (FFlag::LuauBidirectionalInferenceVariadics) { - // if the upper bound is a subtype of the expected type, we can push the expected type in - Relation upperBoundRelation = relate(ft->upperBound, expectedType); - if (upperBoundRelation == Relation::Subset || upperBoundRelation == Relation::Coincident) - { - if (FFlag::LuauPushTypeConstraintLambdas3) - { - solver->bind(constraint, exprType, expectedType); - } - else - { - emplaceType(asMutable(exprType), expectedType); - solver->unblock(exprType, expr->location); - } - return exprType; - } + const auto& [lambdaArgTys, _lambdaTail] = flatten(lambdaTy->argTypes); + const auto& [expectedLambdaArgTys, _expectedLambdaTail] = + extendTypePack(*solver->arena, solver->builtinTypes, expectedLambdaTy->argTypes, exprLambda->args.size); - // likewise, if the lower bound is a subtype, we can force the expected type in - // if this is the case and the previous relation failed, it means that the primitive type - // constraint was going to have to select the lower bound for this type anyway. - Relation lowerBoundRelation = relate(ft->lowerBound, expectedType); - if (lowerBoundRelation == Relation::Subset || lowerBoundRelation == Relation::Coincident) + auto limit = std::min({lambdaArgTys.size(), expectedLambdaArgTys.size(), exprLambda->args.size}); + for (size_t argIndex = 0; argIndex < limit; argIndex++) { - if (FFlag::LuauPushTypeConstraintLambdas3) - { - solver->bind(constraint, exprType, expectedType); - } - else - { - emplaceType(asMutable(exprType), expectedType); - solver->unblock(exprType, expr->location); - } - return exprType; + if (!exprLambda->args.data[argIndex]->annotation && get(follow(lambdaArgTys[argIndex])) && + !containsGeneric(expectedLambdaArgTys[argIndex], NotNull{genericTypesAndPacks})) + solver->bind(NotNull{constraint}, lambdaArgTys[argIndex], expectedLambdaArgTys[argIndex]); } - } - } - if (expr->is() || expr->is() || expr->is() || - expr->is()) - { - if (auto ft = get(exprType); ft && fastIsSubtype(ft->upperBound, expectedType)) - { - emplaceType(asMutable(exprType), expectedType); - solver->unblock(exprType, expr->location); - return exprType; } - - Relation r = relate(exprType, expectedType); - if (r == Relation::Coincident || r == Relation::Subset) - return expectedType; - - return exprType; - } - } - - - if (FFlag::LuauPushTypeConstraintLambdas3) - { - LUAU_ASSERT(genericTypesAndPacks); - if (auto exprLambda = expr->as()) - { - const auto lambdaTy = get(exprType); - const auto expectedLambdaTy = FFlag::LuauPushTypeConstraintStripNilFromFunction - ? get(stripNil(solver->builtinTypes, *solver->arena, expectedType)) - : get(expectedType); - if (lambdaTy && expectedLambdaTy) + else { + const auto& [lambdaArgTys, _lambdaTail] = flatten(lambdaTy->argTypes); const auto& [expectedLambdaArgTys, _expectedLambdaTail] = flatten(expectedLambdaTy->argTypes); @@ -340,21 +302,25 @@ struct BidirectionalTypePusher !containsGeneric(expectedLambdaArgTys[argIndex], NotNull{genericTypesAndPacks})) solver->bind(NotNull{constraint}, lambdaArgTys[argIndex], expectedLambdaArgTys[argIndex]); } + } + if (FFlag::LuauBidirectionalInferenceBetterLambdaHandling) + { + // When multiple union arms have the same arg count, it's + // ambiguous. Don't bind the return type so the solver can infer + // it from the body. + if (!ffti.ambiguous && !exprLambda->returnAnnotation && get(follow(lambdaTy->retTypes)) && + !containsGeneric(expectedLambdaTy->retTypes, NotNull{genericTypesAndPacks})) + solver->bind(NotNull{constraint}, lambdaTy->retTypes, expectedLambdaTy->retTypes); + } + else + { if (!exprLambda->returnAnnotation && get(follow(lambdaTy->retTypes)) && !containsGeneric(expectedLambdaTy->retTypes, NotNull{genericTypesAndPacks})) solver->bind(NotNull{constraint}, lambdaTy->retTypes, expectedLambdaTy->retTypes); } } } - else - { - if (expr->is()) - { - // TODO: Push argument / return types into the lambda. - return exprType; - } - } // TODO: CLI-169235: This probably ought to use the same logic as @@ -367,12 +333,16 @@ struct BidirectionalTypePusher { if (auto utv = get(expectedType)) { - std::vector parts{begin(utv), end(utv)}; - - std::optional tt = extractMatchingTableType(parts, exprType, solver->builtinTypes); - - if (tt) - (void)pushType(*tt, expr); + if (FFlag::LuauBidirectionalInferenceSimplifyTables) + { + if (auto tt = extractMatchingTableType(utv, exprType, solver->builtinTypes, solver->arena)) + (void)pushType(*tt, expr); + } + else + { + if (auto tt = extractMatchingTableType_DEPRECATED(utv, exprType, solver->builtinTypes)) + (void)pushType(*tt, expr); + } } else if (auto itv = get(expectedType)) { @@ -429,7 +399,7 @@ struct BidirectionalTypePusher // // NOTE: We also do nothing for write properties. } - else if (item.kind == AstExprTable::Item::List) + else if (item.kind == AstExprTable::Item::Kind::List) { if (expectedTableTy->indexer) { @@ -437,7 +407,7 @@ struct BidirectionalTypePusher (void)pushType(expectedTableTy->indexer->indexResultType, item.value); } } - else if (item.kind == AstExprTable::Item::General) + else if (item.kind == AstExprTable::Item::Kind::General) { // We have { ..., [blocked] : somePropExpr, ...} @@ -461,22 +431,6 @@ struct BidirectionalTypePusher }; } // namespace -PushTypeResult pushTypeInto_DEPRECATED( - NotNull> astTypes, - NotNull> astExpectedTypes, - NotNull solver, - NotNull constraint, - NotNull unifier, - NotNull subtyping, - TypeId expectedType, - const AstExpr* expr -) -{ - BidirectionalTypePusher btp{astTypes, astExpectedTypes, solver, constraint, unifier, subtyping}; - (void)btp.pushType(expectedType, expr); - return {std::move(btp.incompleteInferences)}; -} - PushTypeResult pushTypeInto( NotNull> astTypes, NotNull> astExpectedTypes, diff --git a/Analysis/src/ToDot.cpp b/Analysis/src/ToDot.cpp index 74aafd9c..04a14b79 100644 --- a/Analysis/src/ToDot.cpp +++ b/Analysis/src/ToDot.cpp @@ -10,9 +10,6 @@ #include #include -LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauAnalysisUsesSolverMode) - namespace Luau { @@ -38,7 +35,7 @@ struct StateDot bool canDuplicatePrimitive(TypeId ty); void visitChildren(TypeId ty, int index); - void visitChildren(TypePackId ty, int index); + void visitChildren(TypePackId tp, int index); void visitChild(TypeId ty, int parentIndex, const char* linkName = nullptr); void visitChild(TypePackId tp, int parentIndex, const char* linkName = nullptr); @@ -272,22 +269,11 @@ void StateDot::visitChildren(TypeId ty, int index) finishNodeLabel(ty); finishNode(); - if (FFlag::LuauAnalysisUsesSolverMode) - { - if (t.lowerBound && !get(t.lowerBound)) - visitChild(t.lowerBound, index, "[lowerBound]"); - - if (t.upperBound && !get(t.upperBound)) - visitChild(t.upperBound, index, "[upperBound]"); - } - else if (FFlag::LuauSolverV2) - { - if (!get(t.lowerBound)) - visitChild(t.lowerBound, index, "[lowerBound]"); + if (t.lowerBound && !get(t.lowerBound)) + visitChild(t.lowerBound, index, "[lowerBound]"); - if (!get(t.upperBound)) - visitChild(t.upperBound, index, "[upperBound]"); - } + if (t.upperBound && !get(t.upperBound)) + visitChild(t.upperBound, index, "[upperBound]"); } else if constexpr (std::is_same_v) { diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index bbfb2596..fc88b6a9 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -18,10 +18,8 @@ #include #include -LUAU_FASTFLAGVARIABLE(LuauEnableDenseTableAlias) -LUAU_FASTFLAGVARIABLE(LuauToStringDecomposition) - LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauIntegerType2) /* * Enables increasing levels of verbosity for Luau type names when stringifying. @@ -42,8 +40,6 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINTVARIABLE(DebugLuauVerboseTypeNames, 0) LUAU_FASTFLAGVARIABLE(DebugLuauToStringNoLexicalSort) -LUAU_FASTFLAGVARIABLE(LuauToStringIgnoresSyntheticName) - namespace Luau { @@ -184,8 +180,7 @@ struct StringifierState , result(result) , exhaustive(opts.exhaustive) { - if (FFlag::LuauToStringIgnoresSyntheticName) - ignoreSyntheticName = opts.ignoreSyntheticName; + ignoreSyntheticName = opts.ignoreSyntheticName; for (const auto& [_, v] : opts.nameMap.types) usedNames.insert(v); @@ -309,8 +304,6 @@ struct StringifierState void emitAndRecordSpan(const std::string& s, TypeId ty) { - LUAU_ASSERT(FFlag::LuauToStringDecomposition); - size_t startPos = result.name.length(); emit(s); size_t endPos = result.name.length(); @@ -617,6 +610,13 @@ struct TypeStringifier case PrimitiveType::Table: state.emit("table"); return; + case PrimitiveType::Integer: + if (FFlag::LuauIntegerType2) + { + state.emit("integer"); + return; + } + [[fallthrough]]; default: LUAU_ASSERT(!"Unknown primitive type"); throw InternalCompilerError("Unknown primitive type " + std::to_string(ptv.type)); @@ -714,12 +714,9 @@ struct TypeStringifier if (ttv.boundTo) return stringify(*ttv.boundTo); - bool showName = !state.exhaustive; - if (FFlag::LuauEnableDenseTableAlias) - { - // if hide table alias expansions are enabled and there is a name found for the table, use it - showName = !state.exhaustive || state.opts.hideTableAliasExpansions; - } + // if hide table alias expansions are enabled and there is a name found for the table, use it + bool showName = !state.exhaustive || state.opts.hideTableAliasExpansions; + if (showName) { if (ttv.name) @@ -739,40 +736,18 @@ struct TypeStringifier } } - if (FFlag::LuauToStringDecomposition) - state.emitAndRecordSpan(*ttv.name, ty); - else - state.emit(*ttv.name); + state.emitAndRecordSpan(*ttv.name, ty); stringify(ttv.instantiatedTypeParams, ttv.instantiatedTypePackParams); return; } } - if (FFlag::LuauToStringIgnoresSyntheticName) - { - if (!state.exhaustive && !state.ignoreSyntheticName) - { - if (ttv.syntheticName) - { - state.result.invalid = true; - if (FFlag::LuauToStringDecomposition) - state.emitAndRecordSpan(*ttv.syntheticName, ty); - else - state.emit(*ttv.syntheticName); - stringify(ttv.instantiatedTypeParams, ttv.instantiatedTypePackParams); - return; - } - } - } - else if (!state.exhaustive) + if (!state.exhaustive && !state.ignoreSyntheticName) { if (ttv.syntheticName) { state.result.invalid = true; - if (FFlag::LuauToStringDecomposition) - state.emitAndRecordSpan(*ttv.syntheticName, ty); - else - state.emit(*ttv.syntheticName); + state.emitAndRecordSpan(*ttv.syntheticName, ty); stringify(ttv.instantiatedTypeParams, ttv.instantiatedTypePackParams); return; } @@ -814,6 +789,8 @@ struct TypeStringifier if (ttv.indexer && ttv.props.empty() && isNumber(ttv.indexer->indexType)) { state.emit("{"); + if (ttv.indexer->isReadOnly) + state.emit("read "); stringify(ttv.indexer->indexResultType); state.emit("}"); @@ -828,6 +805,8 @@ struct TypeStringifier if (ttv.indexer) { state.newline(); + if (ttv.indexer->isReadOnly) + state.emit("read "); state.emit("["); stringify(ttv.indexer->indexType); state.emit("]: "); @@ -878,10 +857,7 @@ struct TypeStringifier state.result.invalid = true; if (!state.exhaustive && mtv.syntheticName) { - if (FFlag::LuauToStringDecomposition) - state.emitAndRecordSpan(*mtv.syntheticName, ty); - else - state.emit(*mtv.syntheticName); + state.emitAndRecordSpan(*mtv.syntheticName, ty); return; } @@ -895,10 +871,7 @@ struct TypeStringifier void operator()(TypeId ty, const ExternType& etv) { - if (FFlag::LuauToStringDecomposition) - state.emitAndRecordSpan(etv.name, ty); - else - state.emit(etv.name); + state.emitAndRecordSpan(etv.name, ty); } void operator()(TypeId, const AnyType&) @@ -925,184 +898,101 @@ struct TypeStringifier bool optional = false; bool hasNonNilDisjunct = false; - if (FFlag::LuauToStringDecomposition) + std::vector results = {}; + size_t resultsLength = 0; + bool lengthLimitHit = false; + + for (auto el : &uv) { - std::vector results = {}; - size_t resultsLength = 0; - bool lengthLimitHit = false; + el = follow(el); - for (auto el : &uv) + if (state.opts.useQuestionMarks && isNil(el)) { - el = follow(el); - - if (state.opts.useQuestionMarks && isNil(el)) - { - optional = true; - continue; - } - else - { - hasNonNilDisjunct = true; - } - - std::string saved = std::move(state.result.name); - size_t savedSpansSize = state.result.typeSpans.size(); - - bool needParens = !state.cycleNames.contains(el) && (get(el) != nullptr || get(el) != nullptr); + optional = true; + continue; + } + else + { + hasNonNilDisjunct = true; + } - if (needParens) - state.emit("("); + std::string saved = std::move(state.result.name); + size_t savedSpansSize = state.result.typeSpans.size(); - stringify(el); + bool needParens = !state.cycleNames.contains(el) && (get(el) != nullptr || get(el) != nullptr); - if (needParens) - state.emit(")"); + if (needParens) + state.emit("("); - ElementResult elem; - elem.str = std::move(state.result.name); + stringify(el); - for (size_t i = savedSpansSize; i < state.result.typeSpans.size(); ++i) - elem.spans.push_back(state.result.typeSpans[i]); - state.result.typeSpans.resize(savedSpansSize); + if (needParens) + state.emit(")"); - resultsLength += elem.str.length(); - results.push_back(std::move(elem)); + ElementResult elem; + elem.str = std::move(state.result.name); - state.result.name = std::move(saved); + for (size_t i = savedSpansSize; i < state.result.typeSpans.size(); ++i) + elem.spans.push_back(state.result.typeSpans[i]); + state.result.typeSpans.resize(savedSpansSize); - lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; + resultsLength += elem.str.length(); + results.push_back(std::move(elem)); - if (lengthLimitHit) - break; - } + state.result.name = std::move(saved); - state.unsee(&uv); + lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; - if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) - std::sort( - results.begin(), - results.end(), - [](const ElementResult& a, const ElementResult& b) - { - return a.str < b.str; - } - ); + if (lengthLimitHit) + break; + } - if (optional && results.size() > 1) - state.emit("("); + state.unsee(&uv); - bool first = true; - bool shouldPlaceOnNewlines = results.size() > state.opts.compositeTypesSingleLineLimit; - for (ElementResult& elem : results) - { - if (!first) + if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) + std::sort( + results.begin(), + results.end(), + [](const ElementResult& a, const ElementResult& b) { - if (shouldPlaceOnNewlines) - state.newline(); - else - state.emit(" "); - state.emit("| "); + return a.str < b.str; } + ); - size_t basePos = state.result.name.length(); - state.emit(elem.str); - for (const auto& [start, end, ty] : elem.spans) - state.result.typeSpans.emplace_back(ToStringSpan{basePos + start, basePos + end, ty}); - - first = false; - } - - if (optional) - { - const char* s = "?"; - if (results.size() > 1) - s = ")?"; - - if (!hasNonNilDisjunct) - s = "nil"; + if (optional && results.size() > 1) + state.emit("("); - state.emit(s); - } - } - else + bool first = true; + bool shouldPlaceOnNewlines = results.size() > state.opts.compositeTypesSingleLineLimit; + for (ElementResult& elem : results) { - std::vector results = {}; - size_t resultsLength = 0; - bool lengthLimitHit = false; - - for (auto el : &uv) + if (!first) { - el = follow(el); - - if (state.opts.useQuestionMarks && isNil(el)) - { - optional = true; - continue; - } + if (shouldPlaceOnNewlines) + state.newline(); else - { - hasNonNilDisjunct = true; - } - - std::string saved = std::move(state.result.name); - - bool needParens = !state.cycleNames.contains(el) && - (get(el) || get(el)); // NOLINT(readability-implicit-bool-conversion) - - if (needParens) - state.emit("("); - - stringify(el); - - if (needParens) - state.emit(")"); - - resultsLength += state.result.name.length(); - results.push_back(std::move(state.result.name)); - - state.result.name = std::move(saved); - - lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; - - if (lengthLimitHit) - break; + state.emit(" "); + state.emit("| "); } - state.unsee(&uv); + size_t basePos = state.result.name.length(); + state.emit(elem.str); + for (const auto& [start, end, ty] : elem.spans) + state.result.typeSpans.emplace_back(ToStringSpan{basePos + start, basePos + end, ty}); - if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) - std::sort(results.begin(), results.end()); - - if (optional && results.size() > 1) - state.emit("("); - - bool first = true; - bool shouldPlaceOnNewlines = results.size() > state.opts.compositeTypesSingleLineLimit; - for (std::string& ss : results) - { - if (!first) - { - if (shouldPlaceOnNewlines) - state.newline(); - else - state.emit(" "); - state.emit("| "); - } - state.emit(ss); - first = false; - } + first = false; + } - if (optional) - { - const char* s = "?"; - if (results.size() > 1) - s = ")?"; + if (optional) + { + const char* s = "?"; + if (results.size() > 1) + s = ")?"; - if (!hasNonNilDisjunct) - s = "nil"; + if (!hasNonNilDisjunct) + s = "nil"; - state.emit(s); - } + state.emit(s); } } @@ -1115,134 +1005,76 @@ struct TypeStringifier return; } - if (FFlag::LuauToStringDecomposition) - { - std::vector results = {}; - size_t resultsLength = 0; - bool lengthLimitHit = false; + std::vector results = {}; + size_t resultsLength = 0; + bool lengthLimitHit = false; - for (auto el : uv.parts) - { - el = follow(el); - - std::string saved = std::move(state.result.name); - size_t savedSpansSize = state.result.typeSpans.size(); + for (auto el : uv.parts) + { + el = follow(el); - bool needParens = !state.cycleNames.contains(el) && (get(el) != nullptr || get(el) != nullptr); + std::string saved = std::move(state.result.name); + size_t savedSpansSize = state.result.typeSpans.size(); - if (needParens) - state.emit("("); + bool needParens = !state.cycleNames.contains(el) && (get(el) != nullptr || get(el) != nullptr); - stringify(el); + if (needParens) + state.emit("("); - if (needParens) - state.emit(")"); + stringify(el); - ElementResult elem; - elem.str = std::move(state.result.name); + if (needParens) + state.emit(")"); - for (size_t i = savedSpansSize; i < state.result.typeSpans.size(); ++i) - elem.spans.push_back(state.result.typeSpans[i]); - state.result.typeSpans.resize(savedSpansSize); + ElementResult elem; + elem.str = std::move(state.result.name); - resultsLength += elem.str.length(); - results.push_back(std::move(elem)); + for (size_t i = savedSpansSize; i < state.result.typeSpans.size(); ++i) + elem.spans.push_back(state.result.typeSpans[i]); + state.result.typeSpans.resize(savedSpansSize); - state.result.name = std::move(saved); + resultsLength += elem.str.length(); + results.push_back(std::move(elem)); - lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; + state.result.name = std::move(saved); - if (lengthLimitHit) - break; - } + lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; - state.unsee(&uv); + if (lengthLimitHit) + break; + } - if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) - std::sort( - results.begin(), - results.end(), - [](const ElementResult& a, const ElementResult& b) - { - return a.str < b.str; - } - ); + state.unsee(&uv); - bool first = true; - bool shouldPlaceOnNewlines = results.size() > state.opts.compositeTypesSingleLineLimit || isOverloadedFunction(ty); - for (ElementResult& elem : results) - { - if (!first) + if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) + std::sort( + results.begin(), + results.end(), + [](const ElementResult& a, const ElementResult& b) { - if (shouldPlaceOnNewlines) - state.newline(); - else - state.emit(" "); - state.emit("& "); + return a.str < b.str; } + ); - size_t basePos = state.result.name.length(); - state.emit(elem.str); - for (const auto& [start, end, spanTy] : elem.spans) - state.result.typeSpans.emplace_back(ToStringSpan{basePos + start, basePos + end, spanTy}); - - first = false; - } - } - else + bool first = true; + bool shouldPlaceOnNewlines = results.size() > state.opts.compositeTypesSingleLineLimit || isOverloadedFunction(ty); + for (ElementResult& elem : results) { - std::vector results = {}; - size_t resultsLength = 0; - bool lengthLimitHit = false; - - for (auto el : uv.parts) + if (!first) { - el = follow(el); - - std::string saved = std::move(state.result.name); - - bool needParens = - !state.cycleNames.contains(el) && (get(el) || get(el)); // NOLINT(readability-implicit-bool-conversion) - - if (needParens) - state.emit("("); - - stringify(el); - - if (needParens) - state.emit(")"); - - resultsLength += state.result.name.length(); - results.push_back(std::move(state.result.name)); - - state.result.name = std::move(saved); - - lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; - - if (lengthLimitHit) - break; + if (shouldPlaceOnNewlines) + state.newline(); + else + state.emit(" "); + state.emit("& "); } - state.unsee(&uv); - - if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) - std::sort(results.begin(), results.end()); + size_t basePos = state.result.name.length(); + state.emit(elem.str); + for (const auto& [start, end, spanTy] : elem.spans) + state.result.typeSpans.emplace_back(ToStringSpan{basePos + start, basePos + end, spanTy}); - bool first = true; - bool shouldPlaceOnNewlines = results.size() > state.opts.compositeTypesSingleLineLimit || isOverloadedFunction(ty); - for (std::string& ss : results) - { - if (!first) - { - if (shouldPlaceOnNewlines) - state.newline(); - else - state.emit(" "); - state.emit("& "); - } - state.emit(ss); - first = false; - } + first = false; } } @@ -1623,8 +1455,6 @@ static void tableTypeToStringDetailed( TypeStringifier& tvs ) { - LUAU_ASSERT(FFlag::LuauToStringIgnoresSyntheticName); - if (ignoreSyntheticName == IgnoreSyntheticName::No && ttv->syntheticName) result.invalid = true; @@ -1650,37 +1480,6 @@ static void tableTypeToStringDetailed( tvs.stringify(ttv->instantiatedTypeParams, ttv->instantiatedTypePackParams); } -static void tableTypeToStringDetailed_DEPRECATED( - const TableType* ttv, - const IgnoreSyntheticName ignoreSyntheticName, - ToStringResult& result, - const std::shared_ptr& scope, - const std::string_view nameToUse, - TypeStringifier& tvs -) -{ - LUAU_ASSERT(FFlag::LuauToStringIgnoresSyntheticName); - - if (ignoreSyntheticName == IgnoreSyntheticName::No && ttv->syntheticName) - result.invalid = true; - - // If scope is provided, add module name and check visibility - if (ttv->name && scope) - { - auto [success, moduleName] = canUseTypeNameInScope(scope, *ttv->name); - - if (!success) - result.invalid = true; - - if (moduleName) - result.name = format("%s.", moduleName->c_str()); - } - - result.name += nameToUse; - - tvs.stringify(ttv->instantiatedTypeParams, ttv->instantiatedTypePackParams); -} - ToStringResult toStringDetailed(TypeId ty, ToStringOptions& opts) { /* @@ -1705,73 +1504,25 @@ ToStringResult toStringDetailed(TypeId ty, ToStringOptions& opts) if (!opts.exhaustive) { - if (FFlag::LuauToStringIgnoresSyntheticName) + if (state.ignoreSyntheticName) { - if (state.ignoreSyntheticName) - { - if (auto ttv = get(ty); ttv && ttv->name) - { - if (FFlag::LuauToStringDecomposition) - tableTypeToStringDetailed(ty, ttv, IgnoreSyntheticName::Yes, result, opts.scope, *ttv->name, tvs); - else - tableTypeToStringDetailed_DEPRECATED(ttv, IgnoreSyntheticName::Yes, result, opts.scope, *ttv->name, tvs); - - return result; - } - } - else if (auto ttv = get(ty); ttv && (ttv->name || ttv->syntheticName)) + if (auto ttv = get(ty); ttv && ttv->name) { - if (FFlag::LuauToStringDecomposition) - tableTypeToStringDetailed( - ty, ttv, IgnoreSyntheticName::No, result, opts.scope, ttv->name ? *ttv->name : *ttv->syntheticName, tvs - ); - else - tableTypeToStringDetailed_DEPRECATED( - ttv, IgnoreSyntheticName::No, result, opts.scope, ttv->name ? *ttv->name : *ttv->syntheticName, tvs - ); + tableTypeToStringDetailed(ty, ttv, IgnoreSyntheticName::Yes, result, opts.scope, *ttv->name, tvs); return result; } - else if (auto mtv = get(ty); mtv && mtv->syntheticName) - { - result.invalid = true; - result.name = *mtv->syntheticName; - return result; - } } else if (auto ttv = get(ty); ttv && (ttv->name || ttv->syntheticName)) { - if (ttv->syntheticName) - result.invalid = true; - - // If scope is provided, add module name and check visibility - if (ttv->name && opts.scope) - { - auto [success, moduleName] = canUseTypeNameInScope(opts.scope, *ttv->name); - - if (!success) - result.invalid = true; - - if (moduleName) - result.name = format("%s.", moduleName->c_str()); - } - - if (FFlag::LuauToStringDecomposition) - state.emitAndRecordSpan(ttv->name ? *ttv->name : *ttv->syntheticName, ty); - else - result.name += ttv->name ? *ttv->name : *ttv->syntheticName; - - tvs.stringify(ttv->instantiatedTypeParams, ttv->instantiatedTypePackParams); + tableTypeToStringDetailed(ty, ttv, IgnoreSyntheticName::No, result, opts.scope, ttv->name ? *ttv->name : *ttv->syntheticName, tvs); return result; } else if (auto mtv = get(ty); mtv && mtv->syntheticName) { result.invalid = true; - if (FFlag::LuauToStringDecomposition) - state.emitAndRecordSpan(*mtv->syntheticName, ty); - else - result.name = *mtv->syntheticName; + result.name = *mtv->syntheticName; return result; } } @@ -2258,7 +2009,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) { return "function_check " + tos(c.fn) + " " + tos(c.argsPack); } - else if constexpr (std::is_same_v) + else if constexpr (std::is_same_v) { if (c.expectedType) return "prim " + tos(c.freeType) + "[expected: " + tos(*c.expectedType) + "] as " + tos(c.primitiveType); diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index e0c8579a..04b40a37 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -25,13 +25,10 @@ LUAU_FASTFLAG(DebugLuauFreezeArena) -LUAU_FASTFLAG(LuauSolverV2) - LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500) LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAG(LuauAnalysisUsesSolverMode) namespace Luau { @@ -193,6 +190,11 @@ bool isNumber(TypeId ty) return isPrim(ty, PrimitiveType::Number); } +bool isInteger(TypeId ty) +{ + return isPrim(ty, PrimitiveType::Integer); +} + // Returns true when ty is a subtype of string bool isString(TypeId ty) { @@ -844,12 +846,15 @@ BuiltinTypes::BuiltinTypes() , typeFunctions(std::make_unique()) , nilType(arena->addType(Type{PrimitiveType{PrimitiveType::NilType}, /*persistent*/ true})) , numberType(arena->addType(Type{PrimitiveType{PrimitiveType::Number}, /*persistent*/ true})) + , integerType(arena->addType(Type{PrimitiveType{PrimitiveType::Integer}, /*persistent*/ true})) , stringType(arena->addType(Type{PrimitiveType{PrimitiveType::String}, /*persistent*/ true})) , booleanType(arena->addType(Type{PrimitiveType{PrimitiveType::Boolean}, /*persistent*/ true})) , threadType(arena->addType(Type{PrimitiveType{PrimitiveType::Thread}, /*persistent*/ true})) , bufferType(arena->addType(Type{PrimitiveType{PrimitiveType::Buffer}, /*persistent*/ true})) , functionType(arena->addType(Type{PrimitiveType{PrimitiveType::Function}, /*persistent*/ true})) , externType(arena->addType(Type{ExternType{"userdata", {}, std::nullopt, std::nullopt, {}, {}, {}, {}}, /*persistent*/ true})) + , objectType(arena->addType(Type{ExternType{"object", {}, std::nullopt, std::nullopt, {}, {}, {}, {}}, /*persistent*/ true})) + , classType(arena->addType(Type{ExternType{"class", {}, std::nullopt, std::nullopt, {}, {}, {}, {}}, /*persistent*/ true})) , tableType(arena->addType(Type{PrimitiveType{PrimitiveType::Table}, /*persistent*/ true})) , emptyTableType(arena->addType(Type{TableType{TableState::Sealed, TypeLevel{}, nullptr}, /*persistent*/ true})) , trueType(arena->addType(Type{SingletonType{BooleanSingleton{true}}, /*persistent*/ true})) @@ -923,18 +928,12 @@ void persist(TypeId ty) for (const auto& [_name, prop] : ttv->props) { - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) - { - if (prop.readTy) - queue.push_back(*prop.readTy); - if (prop.writeTy) - queue.push_back(*prop.writeTy); - } - else - queue.push_back(prop.type_DEPRECATED()); + if (prop.readTy) + queue.push_back(*prop.readTy); + if (prop.writeTy) + queue.push_back(*prop.writeTy); } - if (ttv->indexer) { queue.push_back(ttv->indexer->indexType); @@ -945,15 +944,11 @@ void persist(TypeId ty) { for (const auto& [_name, prop] : etv->props) { - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) - { - if (prop.readTy) - queue.push_back(*prop.readTy); - if (prop.writeTy) - queue.push_back(*prop.writeTy); - } - else - queue.push_back(prop.type_DEPRECATED()); + + if (prop.readTy) + queue.push_back(*prop.readTy); + if (prop.writeTy) + queue.push_back(*prop.writeTy); } } else if (auto utv = get(t)) diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index 5fff15df..ae300700 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -100,6 +100,8 @@ class TypeRehydrationVisitor return allocator->alloc(Location(), std::nullopt, AstName("boolean"), std::nullopt, Location()); case PrimitiveType::Number: return allocator->alloc(Location(), std::nullopt, AstName("number"), std::nullopt, Location()); + case PrimitiveType::Integer: + return allocator->alloc(Location(), std::nullopt, AstName("integer"), std::nullopt, Location()); case PrimitiveType::String: return allocator->alloc(Location(), std::nullopt, AstName("string"), std::nullopt, Location()); case PrimitiveType::Thread: diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index d0531393..9d4d423d 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -12,7 +12,7 @@ #include "Luau/Instantiation.h" #include "Luau/Metamethods.h" #include "Luau/Normalize.h" -#include "Luau/OverloadResolution.h" +#include "Luau/OverloadResolver.h" #include "Luau/Subtyping.h" #include "Luau/TimeTrace.h" #include "Luau/ToString.h" @@ -34,15 +34,12 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSyntax) -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSupport) - -LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) -LUAU_FASTFLAG(LuauMorePreciseErrorSuppression) -LUAU_FASTFLAG(LuauReworkInfiniteTypeFinder) -LUAU_FASTFLAG(LuauExternTypesNormalizeWithShapes) -LUAU_FASTFLAGVARIABLE(LuauCheckForInWithSubtyping3) LUAU_FASTFLAGVARIABLE(LuauCheckFunctionStatementTypes) +LUAU_FASTFLAGVARIABLE(LuauPropertyModifierMismatchErrors) +LUAU_FASTFLAG(LuauImproveUniqueTableWidthSubtyping) +LUAU_FASTFLAG(LuauBidirectionalInferenceSimplifyTables) + +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) namespace Luau { @@ -318,8 +315,8 @@ TypeChecker2::TypeChecker2( , ice(unifierState->iceHandler) , sourceModule(sourceModule) , module(module) - , normalizer{&module->internalTypes, builtinTypes, unifierState, SolverMode::New, /* cacheInhabitance */ true} - , _subtyping{builtinTypes, NotNull{&module->internalTypes}, NotNull{&normalizer}, typeFunctionRuntime, NotNull{unifierState->iceHandler}} + , normalizer{module->internalTypes.get(), builtinTypes, unifierState, SolverMode::New, /* cacheInhabitance */ true} + , _subtyping{builtinTypes, NotNull{module->internalTypes.get()}, NotNull{&normalizer}, typeFunctionRuntime, NotNull{unifierState->iceHandler}} , subtyping(&_subtyping) { } @@ -497,7 +494,9 @@ TypeId TypeChecker2::checkForTypeFunctionInhabitance(TypeId instance, Location l return instance; seenTypeFunctionInstances.insert(instance); - TypeFunctionContext context{NotNull{&module->internalTypes}, builtinTypes, stack.back(), NotNull{&normalizer}, typeFunctionRuntime, ice, limits}; + TypeFunctionContext context{ + NotNull{module->internalTypes.get()}, builtinTypes, stack.back(), NotNull{&normalizer}, typeFunctionRuntime, ice, limits, subtyping + }; ErrorVec errors = reduceTypeFunctions(instance, location, NotNull{&context}, true).errors; if (!isErrorSuppressing(location, instance)) @@ -670,6 +669,8 @@ void TypeChecker2::visit(AstStat* stat) return visit(s); else if (auto s = stat->as()) return visit(s); + else if (auto s = stat->as()) + return visit(s); else if (auto s = stat->as()) return visit(s); else @@ -722,7 +723,7 @@ void TypeChecker2::visit(AstStatReturn* ret) return; } - auto [head, _] = extendTypePack(module->internalTypes, builtinTypes, expectedRetType, ret->list.size); + auto [head, _] = extendTypePack(*module->internalTypes, builtinTypes, expectedRetType, ret->list.size); bool isSubtype = true; std::vector actualHead; std::optional actualTail; @@ -771,7 +772,7 @@ void TypeChecker2::visit(AstStatReturn* ret) // we double error. if (isSubtype) { - auto reconstructedRetType = module->internalTypes.addTypePack(TypePack{std::move(actualHead), std::move(actualTail)}); + auto reconstructedRetType = module->internalTypes->addTypePack(TypePack{std::move(actualHead), std::move(actualTail)}); testIsSubtype(reconstructedRetType, expectedRetType, ret->location); } @@ -814,7 +815,7 @@ void TypeChecker2::visit(AstStatLocal* local) TypePackId valuePack = lookupPack(value); TypePack valueTypes; if (i < local->vars.size) - valueTypes = extendTypePack(module->internalTypes, builtinTypes, valuePack, local->vars.size - i); + valueTypes = extendTypePack(*module->internalTypes, builtinTypes, valuePack, local->vars.size - i); Location errorLocation; for (size_t j = i; j < local->vars.size; ++j) @@ -898,7 +899,7 @@ void TypeChecker2::visit(AstStatForIn* forInStatement) return; NotNull scope = stack.back(); - TypeArena& arena = module->internalTypes; + TypeArena& arena = *module->internalTypes; std::vector variableTypes; for (AstLocal* var : forInStatement->vars) @@ -981,7 +982,7 @@ void TypeChecker2::visit(AstStatForIn* forInStatement) else reportError(GenericError{"next() does not return enough values"}, forInStatement->values.data[0]->location); - return; + return; } // nextFn is going to be invoked with (arrayTy, startIndexTy) @@ -1013,7 +1014,7 @@ void TypeChecker2::visit(AstStatForIn* forInStatement) else reportError(CountMismatch{2, std::nullopt, firstIterationArgCount, CountMismatch::Arg}, forInStatement->values.data[0]->location); - return; + return; } else if (actualArgCount < minCount) { @@ -1022,7 +1023,7 @@ void TypeChecker2::visit(AstStatForIn* forInStatement) else reportError(CountMismatch{2, std::nullopt, firstIterationArgCount, CountMismatch::Arg}, forInStatement->values.data[0]->location); - return; + return; } const TypeId iterFunc = follow(iterTys[0]); @@ -1308,16 +1309,8 @@ void TypeChecker2::visit(AstStatTypeAlias* stat) if (const Scope* scope = findInnermostScope(stat->location)) { - if (FFlag::LuauReworkInfiniteTypeFinder) - { - if (auto loc = scope->isInvalidTypeAlias(stat->name.value)) - reportError(RecursiveRestraintViolation{}, *loc); - } - else - { - if (scope->isInvalidTypeAliasName_DEPRECATED(stat->name.value)) - reportError(RecursiveRestraintViolation{}, stat->location); - } + if (auto loc = scope->isInvalidTypeAlias(stat->name.value)) + reportError(RecursiveRestraintViolation{}, *loc); } visitGenerics(stat->generics, stat->genericPacks); @@ -1355,6 +1348,26 @@ void TypeChecker2::visit(AstStatDeclareExternType* stat) visit(prop.ty); } +void TypeChecker2::visit(AstStatClass* stat) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + + for (const auto& member : stat->members) + { + if (const auto* prop = member.get_if()) + { + if (prop->ty) + visit(prop->ty); + } + else if (const auto* method = member.get_if()) + { + visit(method->function); + } + else + LUAU_ASSERT(!"Unknown class member!"); + } +} + void TypeChecker2::visit(AstStatError* stat) { for (AstExpr* expr : stat->expressions) @@ -1376,6 +1389,8 @@ void TypeChecker2::visit(AstExpr* expr, ValueContext context) return visit(e); else if (auto e = expr->as()) return visit(e); + else if (auto e = expr->as()) + return visit(e); else if (auto e = expr->as()) return visit(e); else if (auto e = expr->as()) @@ -1406,10 +1421,7 @@ void TypeChecker2::visit(AstExpr* expr, ValueContext context) else if (auto e = expr->as()) return visit(e); else if (auto e = expr->as()) - { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); return visit(e); - } else if (auto e = expr->as()) return visit(e); else if (auto e = expr->as()) @@ -1444,8 +1456,7 @@ void TypeChecker2::visit(AstExprConstantBool* expr) NotNull scope{findInnermostScope(expr->location)}; SubtypingResult r = subtyping->isSubtype(bestType, inferredType, scope); - bool suppress = FFlag::LuauMorePreciseErrorSuppression ? r.isErrorSuppressing : isErrorSuppressing(expr->location, inferredType); - if (!suppress) + if (!r.isErrorSuppressing) { if (!r.isSubtype) reportError(TypeMismatch{inferredType, bestType}, expr->location); @@ -1467,11 +1478,23 @@ void TypeChecker2::visit(AstExprConstantNumber* expr) #endif } +void TypeChecker2::visit(AstExprConstantInteger* expr) +{ +#if defined(LUAU_ENABLE_ASSERT) + const TypeId bestType = builtinTypes->integerType; + const TypeId inferredType = lookupType(expr); + NotNull scope{findInnermostScope(expr->location)}; + + const SubtypingResult r = subtyping->isSubtype(bestType, inferredType, scope); + LUAU_ASSERT(r.isSubtype || isErrorSuppressing(expr->location, inferredType)); +#endif +} + void TypeChecker2::visit(AstExprConstantString* expr) { // strings use specialized inference logic for singleton typeArguments, which can lead to real type errors here. - const TypeId bestType = module->internalTypes.addType(SingletonType{StringSingleton{std::string{expr->value.data, expr->value.size}}}); + const TypeId bestType = module->internalTypes->addType(SingletonType{StringSingleton{std::string{expr->value.data, expr->value.size}}}); const TypeId inferredType = lookupType(expr); NotNull scope{findInnermostScope(expr->location)}; @@ -1513,7 +1536,7 @@ void TypeChecker2::visit(AstExprVarargs* expr) // TODO! } -static void reportAvailableOverloads(ErrorVec& errors, Location location, const std::vector& overloads) +static void reportAvailableOverloads(ErrorVec& errors, Location location, const ModuleName& moduleName, const std::vector& overloads) { if (overloads.empty()) return; @@ -1532,7 +1555,7 @@ static void reportAvailableOverloads(ErrorVec& errors, Location location, const s << toString(overloads[i]); } - errors.emplace_back(location, ExtraInformation{s.str()}); + errors.emplace_back(location, moduleName, ExtraInformation{s.str()}); } void TypeChecker2::visitCall(AstExprCall* call) @@ -1567,12 +1590,9 @@ void TypeChecker2::visitCall(AstExprCall* call) return; } - if (FFlag::LuauExplicitTypeInstantiationSupport) + if (call->typeArguments.size) { - if (call->typeArguments.size) - { - checkTypeInstantiation(call, fnTy, call->location, call->typeArguments); - } + checkTypeInstantiation(call, fnTy, call->location, call->typeArguments); } if (selectedOverloadTy) @@ -1581,22 +1601,12 @@ void TypeChecker2::visitCall(AstExprCall* call) if (result.isSubtype) fnTy = follow(*selectedOverloadTy); - if (FFlag::LuauMorePreciseErrorSuppression) - { - if (result.isErrorSuppressing) - { - for (auto& e : result.errors) - e.location = call->location; - } - } - else + if (result.isErrorSuppressing) { - if (!isErrorSuppressing(call->location, *selectedOverloadTy)) - { - for (auto& e : result.errors) - e.location = call->location; - } + for (auto& e : result.errors) + e.location = call->location; } + reportErrors(std::move(result.errors)); if (result.normalizationTooComplex) { @@ -1624,7 +1634,7 @@ void TypeChecker2::visitCall(AstExprCall* call) { size_t selfOffset = call->self ? 1 : 0; - std::vector paramsHead = extendTypePack(module->internalTypes, builtinTypes, fty->argTypes, call->args.size + selfOffset).head; + std::vector paramsHead = extendTypePack(*module->internalTypes, builtinTypes, fty->argTypes, call->args.size + selfOffset).head; for (size_t idx = 0; idx < call->args.size; ++idx) { @@ -1678,7 +1688,7 @@ void TypeChecker2::visitCall(AstExprCall* call) } } - TypePackId argsTp = module->internalTypes.addTypePack(args); + TypePackId argsTp = module->internalTypes->addTypePack(args); if (auto ftv = get(follow(*originalCallTy))) { if (ftv->magic) @@ -1691,7 +1701,7 @@ void TypeChecker2::visitCall(AstExprCall* call) OverloadResolver resolver{ builtinTypes, - NotNull{&module->internalTypes}, + NotNull{module->internalTypes.get()}, NotNull{&normalizer}, typeFunctionRuntime, NotNull{stack.back()}, @@ -1702,7 +1712,7 @@ void TypeChecker2::visitCall(AstExprCall* call) DenseHashSet uniqueTypes{nullptr}; findUniqueTypes(NotNull{&uniqueTypes}, argExprs, NotNull{&module->astTypes}); - TypePackId argsPack = module->internalTypes.addTypePack(args); + TypePackId argsPack = module->internalTypes->addTypePack(args); const OverloadResolution result2 = resolver.resolveOverload(fnTy, argsPack, call->func->location, NotNull{&uniqueTypes}, false); if (!result2.potentialOverloads.empty()) @@ -1765,7 +1775,7 @@ void TypeChecker2::visitCall(AstExprCall* call) if (!overloadsToReport.empty()) { reportError(MultipleNonviableOverloads{argHead.size()}, call->location); - reportAvailableOverloads(module->errors, call->location, overloadsToReport); + reportAvailableOverloads(module->errors, call->location, module->name, overloadsToReport); } return; @@ -1794,7 +1804,7 @@ void TypeChecker2::visitCall(AstExprCall* call) std::stringstream ss; ss << "No overload for function accepts " << argHead.size() << " arguments."; reportError(GenericError{ss.str()}, call->func->location); - reportAvailableOverloads(module->errors, call->func->location, result2.arityMismatches); + reportAvailableOverloads(module->errors, call->func->location, module->name, result2.arityMismatches); return; } @@ -1808,7 +1818,6 @@ void TypeChecker2::visitCall(AstExprCall* call) reportError(CannotCallNonFunction{fnTy}, call->func->location); return; } - } void TypeChecker2::visit(AstExprCall* call) @@ -1858,7 +1867,7 @@ std::optional TypeChecker2::tryStripUnionFromNil(TypeId ty) const if (result.empty()) return std::nullopt; - return result.size() == 1 ? result[0] : module->internalTypes.addType(UnionType{std::move(result)}); + return result.size() == 1 ? result[0] : module->internalTypes->addType(UnionType{std::move(result)}); } return std::nullopt; @@ -1944,7 +1953,11 @@ void TypeChecker2::visit(AstExprIndexExpr* indexExpr, ValueContext context) if (auto tt = get(exprType)) { if (tt->indexer) + { testIsSubtype(indexType, tt->indexer->indexType, indexExpr->index->location); + if (context == ValueContext::LValue && tt->indexer->isReadOnly) + reportError(PropertyAccessViolation{exprType, "indexer", PropertyAccessViolation::CannotWrite}, indexExpr->location); + } else reportError(CannotExtendTable{exprType, CannotExtendTable::Indexer, "indexer??"}, indexExpr->location); } @@ -2179,10 +2192,10 @@ void TypeChecker2::visit(AstExprUnary* expr) return; } - TypePackId expectedArgs = module->internalTypes.addTypePack({operandType}); - TypePackId expectedRet = module->internalTypes.addTypePack({resultType}); + TypePackId expectedArgs = module->internalTypes->addTypePack({operandType}); + TypePackId expectedRet = module->internalTypes->addTypePack({resultType}); - TypeId expectedFunction = module->internalTypes.addType(FunctionType{expectedArgs, expectedRet}); + TypeId expectedFunction = module->internalTypes->addType(FunctionType{expectedArgs, expectedRet}); bool success = testIsSubtype(*mm, expectedFunction, expr->location); if (!success) @@ -2265,6 +2278,12 @@ static bool isOkToCompare( return false; }; +static bool isComparisonOp(AstExprBinary::Op op) +{ + return op == AstExprBinary::CompareNe || op == AstExprBinary::CompareEq || op == AstExprBinary::CompareGe || op == AstExprBinary::CompareGt || + op == AstExprBinary::CompareLe || op == AstExprBinary::CompareLt; +} + TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) { std::optional inContext; @@ -2272,13 +2291,17 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) expr->op != AstExprBinary::CompareNe) inContext.emplace(&typeContext, TypeContext::Default); + // In compound assignments, the left side is both read-from and written-to, so we have to visit it in both contexts. + if (overrideKey && overrideKey->is()) + visit(expr->left, ValueContext::LValue); + visit(expr->left, ValueContext::RValue); visit(expr->right, ValueContext::RValue); NotNull scope = stack.back(); bool isEquality = expr->op == AstExprBinary::Op::CompareEq || expr->op == AstExprBinary::Op::CompareNe; - bool isComparison = expr->op >= AstExprBinary::Op::CompareEq && expr->op <= AstExprBinary::Op::CompareGe; + bool isComparison = isComparisonOp(expr->op); bool isLogical = expr->op == AstExprBinary::Op::And || expr->op == AstExprBinary::Op::Or; TypeId leftType = follow(lookupType(expr->left)); @@ -2293,7 +2316,7 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) if (expr->op == AstExprBinary::Op::Or) { - leftType = stripNil(builtinTypes, module->internalTypes, leftType); + leftType = stripNil(builtinTypes, *module->internalTypes, leftType); } std::shared_ptr normLeft = normalizer.normalize(leftType); @@ -2324,16 +2347,21 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) } NormalizationResult typesHaveIntersection = normalizer.isIntersectionInhabited(leftType, rightType); + if (isEquality || isComparison) { - // As a special exception, we allow anything to be compared to nil. if (!isOkToCompare(normalizer, typesHaveIntersection, normLeft, normRight)) { reportError(CannotCompareUnrelatedTypes{leftType, rightType, expr->op}, expr->location); return builtinTypes->errorType; } - } + auto eitherExprIsNil = (normLeft && normLeft->isNil()) || (normRight && normRight->isNil()); + + // For equality operations, if either operand is nil, we should allow this comparison through + if (isEquality && eitherExprIsNil) + return builtinTypes->booleanType; + } if (auto it = kBinaryOpMetamethods.find(expr->op); it != kBinaryOpMetamethods.end()) { std::optional leftMt = getMetatable(leftType, builtinTypes); @@ -2419,25 +2447,25 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) // swapped argument ordering. if (expr->op == AstExprBinary::Op::CompareGe || expr->op == AstExprBinary::Op::CompareGt) { - expectedArgs = module->internalTypes.addTypePack({rightType, leftType}); + expectedArgs = module->internalTypes->addTypePack({rightType, leftType}); } else { - expectedArgs = module->internalTypes.addTypePack({leftType, rightType}); + expectedArgs = module->internalTypes->addTypePack({leftType, rightType}); } TypePackId expectedRets; if (expr->op == AstExprBinary::CompareEq || expr->op == AstExprBinary::CompareNe || expr->op == AstExprBinary::CompareGe || expr->op == AstExprBinary::CompareGt || expr->op == AstExprBinary::Op::CompareLe || expr->op == AstExprBinary::Op::CompareLt) { - expectedRets = module->internalTypes.addTypePack({builtinTypes->booleanType}); + expectedRets = module->internalTypes->addTypePack({builtinTypes->booleanType}); } else { - expectedRets = module->internalTypes.addTypePack({module->internalTypes.freshType(builtinTypes, scope, TypeLevel{})}); + expectedRets = module->internalTypes->addTypePack({module->internalTypes->freshType(builtinTypes, scope, TypeLevel{})}); } - TypeId expectedTy = module->internalTypes.addType(FunctionType(expectedArgs, expectedRets)); + TypeId expectedTy = module->internalTypes->addType(FunctionType(expectedArgs, expectedRets)); testIsSubtype(follow(*mm), expectedTy, expr->location); @@ -2544,7 +2572,7 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) return builtinTypes->numberType; case AstExprBinary::Op::Concat: { - const TypeId numberOrString = module->internalTypes.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}}); + const TypeId numberOrString = module->internalTypes->addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}}); testIsSubtype(leftType, numberOrString, expr->left->location); testIsSubtype(rightType, numberOrString, expr->right->location); return builtinTypes->stringType; @@ -2657,15 +2685,13 @@ void TypeChecker2::visit(AstExprIfElse* expr) void TypeChecker2::visit(AstExprInstantiate* explicitTypeInstantiation) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); visit(explicitTypeInstantiation->expr, ValueContext::RValue); - if (FFlag::LuauExplicitTypeInstantiationSupport) - checkTypeInstantiation( - explicitTypeInstantiation->expr, - lookupType(explicitTypeInstantiation->expr), - explicitTypeInstantiation->location, - explicitTypeInstantiation->typeArguments - ); + checkTypeInstantiation( + explicitTypeInstantiation->expr, + lookupType(explicitTypeInstantiation->expr), + explicitTypeInstantiation->location, + explicitTypeInstantiation->typeArguments + ); } void TypeChecker2::visit(AstExprInterpString* interpString) @@ -2691,8 +2717,8 @@ TypeId TypeChecker2::flattenPack(TypePackId pack) return *fst; else if (auto ftp = get(pack)) { - TypeId result = module->internalTypes.freshType(builtinTypes, ftp->scope); - TypePackId freeTail = module->internalTypes.addTypePack(FreeTypePack{ftp->scope}); + TypeId result = module->internalTypes->freshType(builtinTypes, ftp->scope); + TypePackId freeTail = module->internalTypes->addTypePack(FreeTypePack{ftp->scope}); TypePack* resultPack = emplaceTypePack(asMutable(pack)); resultPack->head.assign(1, result); @@ -3020,9 +3046,7 @@ Reasonings TypeChecker2::explainReasonings_(TID subTy, TID superTy, Location loc if (!subLeafTy && !superLeafTy && !subLeafTp && !superLeafTp) { - reportError( - InternalError{"Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack."}, location - ); + reportError(InternalError{"Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack."}, location); return {}; } @@ -3048,12 +3072,32 @@ Reasonings TypeChecker2::explainReasonings_(TID subTy, TID superTy, Location loc std::stringstream reason; - if (FFlag::LuauBetterTypeMismatchErrors && reasoning.subPath == reasoning.superPath) + if (FFlag::LuauPropertyModifierMismatchErrors && reasoning.isPropertyModifierViolation) + { + // The leaf types at the end of the paths are the same type, so a + // plain "X is not a subtype of X" message would be misleading. + // Instead, explain that the mismatch is about the property modifier. + std::string propName = "a property"; + bool isReadOnly = true; + auto last = reasoning.subPath.last(); + LUAU_ASSERT(last && get_if(&*last)); + if (last) + { + if (auto* prop = get_if(&*last)) + { + propName = "`" + prop->name + "`"; + isReadOnly = prop->isRead; + } + } + + if (isReadOnly) + reason << propName << " is a read-only property in the latter type, but the former type requires a read-write property"; + else + reason << propName << " is a write-only property in the latter type, but the former type requires a read-write property"; + } + else if (reasoning.subPath == reasoning.superPath) reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "` in the latter type and `" << superLeafAsString << "` in the former type, and " << baseReason; - else if (reasoning.subPath == reasoning.superPath) - reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "` in the former type and `" << superLeafAsString - << "` in the latter type, and " << baseReason; else if (!reasoning.subPath.empty() && !reasoning.superPath.empty()) reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "` and " << toStringHuman(reasoning.superPath) << "`" << superLeafAsString << "`, and " << baseReason; @@ -3090,20 +3134,18 @@ Reasonings TypeChecker2::explainReasonings(TypePackId subTp, TypePackId superTp, void TypeChecker2::explainError(TypeId subTy, TypeId superTy, Location location, const SubtypingResult& result) { - if (FFlag::LuauMorePreciseErrorSuppression && result.isErrorSuppressing) + if (result.isErrorSuppressing) return; - else + + switch (shouldSuppressErrors(NotNull{&normalizer}, subTy).orElse(shouldSuppressErrors(NotNull{&normalizer}, superTy))) { - switch (shouldSuppressErrors(NotNull{&normalizer}, subTy).orElse(shouldSuppressErrors(NotNull{&normalizer}, superTy))) - { - case ErrorSuppression::Suppress: - return; - case ErrorSuppression::NormalizationFailed: - reportError(NormalizationTooComplex{}, location); - break; - case ErrorSuppression::DoNotSuppress: - break; - } + case ErrorSuppression::Suppress: + return; + case ErrorSuppression::NormalizationFailed: + reportError(NormalizationTooComplex{}, location); + break; + case ErrorSuppression::DoNotSuppress: + break; } Reasonings reasonings = explainReasonings(subTy, superTy, location, result); @@ -3114,20 +3156,18 @@ void TypeChecker2::explainError(TypeId subTy, TypeId superTy, Location location, void TypeChecker2::explainError(TypePackId subTy, TypePackId superTy, Location location, const SubtypingResult& result) { - if (FFlag::LuauMorePreciseErrorSuppression && result.isErrorSuppressing) + if (result.isErrorSuppressing) return; - else + + switch (shouldSuppressErrors(NotNull{&normalizer}, subTy).orElse(shouldSuppressErrors(NotNull{&normalizer}, superTy))) { - switch (shouldSuppressErrors(NotNull{&normalizer}, subTy).orElse(shouldSuppressErrors(NotNull{&normalizer}, superTy))) - { - case ErrorSuppression::Suppress: - return; - case ErrorSuppression::NormalizationFailed: - reportError(NormalizationTooComplex{}, location); - break; - case ErrorSuppression::DoNotSuppress: - break; - } + case ErrorSuppression::Suppress: + return; + case ErrorSuppression::NormalizationFailed: + reportError(NormalizationTooComplex{}, location); + break; + case ErrorSuppression::DoNotSuppress: + break; } Reasonings reasonings = explainReasonings(subTy, superTy, location, result); @@ -3140,7 +3180,34 @@ bool TypeChecker2::testLiteralOrAstTypeIsSubtype(AstExpr* expr, TypeId expectedT { NotNull scope{findInnermostScope(expr->location)}; auto exprTy = lookupType(expr); - SubtypingResult r = subtyping->isSubtype(exprTy, expectedType, scope); + + SubtypingResult r; + + if (FFlag::LuauImproveUniqueTableWidthSubtyping && !FFlag::LuauBidirectionalInferenceSimplifyTables) + { + DenseHashSet uniqueTypes{nullptr}; + findUniqueTypes(NotNull{&uniqueTypes}, std::vector{expr}, NotNull{&module->astTypes}); + + // We create a separate `Subtyping` instance here because, in this + // particular context, we have knowledge that any table literals are + // unique references to their types. Because we know that no other + // references to those values can exist, we can safely test those table + // types covariantly. + // + // These same TypeIds must _not_ be considered to be unique references + // if they occur in any other context, and so we need to separate the + // caches. + + Subtyping st{builtinTypes, NotNull{module->internalTypes.get()}, NotNull{&normalizer}, typeFunctionRuntime, ice}; + st.uniqueTypes = &uniqueTypes; + + r = st.isSubtype(exprTy, expectedType, scope); + } + else + { + r = subtyping->isSubtype(exprTy, expectedType, scope); + } + if (r.isSubtype) return true; @@ -3166,7 +3233,7 @@ bool TypeChecker2::testPotentialLiteralIsSubtype(AstExpr* expr, TypeId expectedT { // In this case: `{ ... } or { ... }` is literal _enough_ that // we should do this covariant check. - auto relaxedExpectedLhs = module->internalTypes.addType(UnionType{{builtinTypes->falsyType, expectedType}}); + auto relaxedExpectedLhs = module->internalTypes->addType(UnionType{{builtinTypes->falsyType, expectedType}}); bool passes = testPotentialLiteralIsSubtype(binExpr->left, relaxedExpectedLhs); passes &= testPotentialLiteralIsSubtype(binExpr->right, expectedType); return passes; @@ -3189,10 +3256,16 @@ bool TypeChecker2::testPotentialLiteralIsSubtype(AstExpr* expr, TypeId expectedT { if (auto utv = get(expectedType)) { - std::vector parts{begin(utv), end(utv)}; - std::optional tt = extractMatchingTableType(parts, exprType, builtinTypes); - if (tt) - return testPotentialLiteralIsSubtype(expr, *tt); + if (FFlag::LuauBidirectionalInferenceSimplifyTables) + { + if (auto tt = extractMatchingTableType(utv, exprType, builtinTypes, NotNull{module->internalTypes.get()})) + return testLiteralOrAstTypeIsSubtype(expr, *tt); + } + else + { + if (auto tt = extractMatchingTableType_DEPRECATED(utv, exprType, builtinTypes)) + return testLiteralOrAstTypeIsSubtype(expr, *tt); + } } if (auto itv = get(expectedType)) @@ -3201,7 +3274,7 @@ bool TypeChecker2::testPotentialLiteralIsSubtype(AstExpr* expr, TypeId expectedT // construct it and use it as the input to this algorithm. TypeIds parts; parts.insert(begin(itv), end(itv)); - TypeId simplified = simplifyIntersection(builtinTypes, NotNull{&module->internalTypes}, std::move(parts)).result; + TypeId simplified = simplifyIntersection(builtinTypes, NotNull{module->internalTypes.get()}, std::move(parts)).result; if (is(simplified)) return testPotentialLiteralIsSubtype(expr, simplified); } @@ -3245,7 +3318,7 @@ bool TypeChecker2::testPotentialLiteralIsSubtype(AstExpr* expr, TypeId expectedT { module->astExpectedTypes[item.key] = expectedTableType->indexer->indexType; module->astExpectedTypes[item.value] = expectedTableType->indexer->indexResultType; - auto inferredKeyType = module->internalTypes.addType(SingletonType{StringSingleton{keyStr}}); + auto inferredKeyType = module->internalTypes->addType(SingletonType{StringSingleton{keyStr}}); isSubtype &= testIsSubtype(inferredKeyType, expectedTableType->indexer->indexType, item.key->location); isSubtype &= testPotentialLiteralIsSubtype(item.value, expectedTableType->indexer->indexResultType); } @@ -3263,7 +3336,7 @@ bool TypeChecker2::testPotentialLiteralIsSubtype(AstExpr* expr, TypeId expectedT } } } - else if (item.kind == AstExprTable::Item::List) + else if (item.kind == AstExprTable::Item::Kind::List) { if (!isArrayLike) { @@ -3277,7 +3350,7 @@ bool TypeChecker2::testPotentialLiteralIsSubtype(AstExpr* expr, TypeId expectedT isSubtype &= testPotentialLiteralIsSubtype(item.value, expectedTableType->indexer->indexResultType); } } - else if (item.kind == AstExprTable::Item::General && expectedTableType->indexer) + else if (item.kind == AstExprTable::Item::Kind::General && expectedTableType->indexer) { module->astExpectedTypes[item.key] = expectedTableType->indexer->indexType; module->astExpectedTypes[item.value] = expectedTableType->indexer->indexResultType; @@ -3305,22 +3378,11 @@ bool TypeChecker2::testIsSubtype(TypeId subTy, TypeId superTy, Location location NotNull scope{findInnermostScope(location)}; SubtypingResult r = subtyping->isSubtype(subTy, superTy, scope); - if (FFlag::LuauMorePreciseErrorSuppression) - { - if (r.isErrorSuppressing) - return r.isSubtype; + if (r.isErrorSuppressing) + return r.isSubtype; - for (auto& e : r.errors) - e.location = location; - } - else - { - if (!isErrorSuppressing(location, subTy)) - { - for (auto& e : r.errors) - e.location = location; - } - } + for (auto& e : r.errors) + e.location = location; reportErrors(std::move(r.errors)); if (r.normalizationTooComplex) @@ -3471,9 +3533,22 @@ PropertyTypes TypeChecker2::lookupProp( if (normValid) fetch(norm->booleans); + if (FFlag::DebugLuauUserDefinedClasses) + { + if (normValid) + { + for (const auto& partTy : norm->externTypes.ordering) + { + fetch(partTy); + if (!normValid) + break; + } + } + } + // TODO: the subsequent code here is basically proof that this broader approach to doing indexing isn't quite right. // we _should_ be leveraging one unified implementation of indexing here, shared with e.g. the `index` type function. - if (normValid && FFlag::LuauExternTypesNormalizeWithShapes) + if (normValid) { // each individual extern type consists of a collection of extern types in a normal form, and a collection of table types describing the // shapes further. extern types and tables are both open to extension in general, and therefore, we need to consider the possibility that a @@ -3578,7 +3653,7 @@ PropertyTypes TypeChecker2::lookupProp( { std::vector parts; parts.insert(parts.end(), norm->functions.parts.begin(), norm->functions.parts.end()); - fetch(module->internalTypes.addType(IntersectionType{std::move(parts)})); + fetch(module->internalTypes->addType(IntersectionType{std::move(parts)})); } } @@ -3589,7 +3664,7 @@ PropertyTypes TypeChecker2::lookupProp( if (get(intersect->tops)) { TypeId ty = normalizer.typeFromNormal(*intersect); - fetch(module->internalTypes.addType(IntersectionType{{tyvar, ty}})); + fetch(module->internalTypes->addType(IntersectionType{{tyvar, ty}})); } else fetch(follow(tyvar)); @@ -3633,17 +3708,24 @@ void TypeChecker2::checkIndexTypeFromType( // because extern typeArguments come into being with full knowledge of their // shape. We instead want to report the unknown property error of // the `else` branch. - else if (context == ValueContext::LValue && !get(tableTy)) + else if (context == ValueContext::LValue) { const auto lvPropTypes = lookupProp(norm.get(), prop, ValueContext::RValue, location, astIndexExprType, dummy); if (lvPropTypes.foundOneProp() && lvPropTypes.noneMissingProp()) reportError(PropertyAccessViolation{tableTy, prop, PropertyAccessViolation::CannotWrite}, location); else if (get(tableTy) || get(tableTy)) reportError(NotATable{tableTy}, location); + else if (auto et = get(tableTy)) + { + if (et->indexer) + reportError(UnknownProperty{tableTy, prop}, location); + else + reportError(PropertyAccessViolation{tableTy, prop, PropertyAccessViolation::CannotWrite}, location); + } else reportError(CannotExtendTable{tableTy, CannotExtendTable::Property, prop}, location); } - else if (context == ValueContext::RValue && !get(tableTy)) + else if (context == ValueContext::RValue) { const auto rvPropTypes = lookupProp(norm.get(), prop, ValueContext::LValue, location, astIndexExprType, dummy); if (rvPropTypes.foundOneProp() && rvPropTypes.noneMissingProp()) @@ -3691,9 +3773,15 @@ PropertyType TypeChecker2::hasIndexTypeFromType( if (tt->indexer) { TypeId indexType = follow(tt->indexer->indexType); - TypeId givenType = module->internalTypes.addType(SingletonType{StringSingleton{prop}}); - if (isSubtype(givenType, indexType, NotNull{module->getModuleScope().get()}, builtinTypes, *ice, SolverMode::New)) + TypeId givenType = module->internalTypes->addType(SingletonType{StringSingleton{prop}}); + bool keyMatches = subtyping->isSubtype(givenType, indexType, NotNull{module->getModuleScope().get()}).isSubtype; + + if (keyMatches) + { + if (context == ValueContext::LValue && tt->indexer->isReadOnly) + return {NormalizationResult::False, {}}; return {NormalizationResult::True, {tt->indexer->indexResultType}}; + } } return {NormalizationResult::False, {builtinTypes->unknownType}}; @@ -3705,12 +3793,37 @@ PropertyType TypeChecker2::hasIndexTypeFromType( // is compatible with the indexer's indexType // Construct the intersection and test inhabitedness! if (auto property = lookupExternTypeProp(cls, prop)) - return {NormalizationResult::True, context == ValueContext::LValue ? property->writeTy : property->readTy}; + { + if ((context == ValueContext::LValue && !property->writeTy) || (context == ValueContext::RValue && !property->readTy)) + return {NormalizationResult::False, {}}; + else + return {NormalizationResult::True, context == ValueContext::LValue ? property->writeTy : property->readTy}; + } if (cls->indexer) { - TypeId inhabitedTestType = module->internalTypes.addType(IntersectionType{{cls->indexer->indexType, astIndexExprType}}); + TypeId inhabitedTestType = module->internalTypes->addType(IntersectionType{{cls->indexer->indexType, astIndexExprType}}); return {normalizer.isInhabited(inhabitedTestType), {cls->indexer->indexResultType}}; } + + if (FFlag::DebugLuauUserDefinedClasses) + { + if (cls->metatable) + { + // For user-defined classes, the object metatable holds metamethods (e.g. __add) + // directly in its props rather than under an __index table. + if (const TableType* mtt = get(follow(*cls->metatable))) + { + if (auto mtProp = mtt->props.find(prop); mtProp != mtt->props.end()) + { + if ((context == ValueContext::LValue && !mtProp->second.writeTy) || + (context == ValueContext::RValue && !mtProp->second.readTy)) + return {NormalizationResult::False, {}}; + return {NormalizationResult::True, context == ValueContext::LValue ? mtProp->second.writeTy : mtProp->second.readTy}; + } + } + } + } + return {NormalizationResult::False, {}}; } else if (const UnionType* utv = get(ty)) @@ -3736,9 +3849,9 @@ PropertyType TypeChecker2::hasIndexTypeFromType( TypeId propTy; if (context == ValueContext::LValue) - propTy = module->internalTypes.addType(IntersectionType{std::move(parts)}); + propTy = module->internalTypes->addType(IntersectionType{std::move(parts)}); else - propTy = module->internalTypes.addType(UnionType{std::move(parts)}); + propTy = module->internalTypes->addType(UnionType{std::move(parts)}); return {NormalizationResult::True, propTy}; } @@ -3767,7 +3880,7 @@ void TypeChecker2::suggestAnnotations(AstExprFunction* expr, TypeId ty) VecDeque workList; DenseHashSet seen{nullptr}; - TypeFunctionReductionGuesser guesser{NotNull{&module->internalTypes}, builtinTypes, NotNull{&normalizer}}; + TypeFunctionReductionGuesser guesser{NotNull{module->internalTypes.get()}, builtinTypes, NotNull{&normalizer}}; for (TypeId retTy : inferredFtv->retTypes) workList.push_back(retTy); @@ -3808,8 +3921,6 @@ void TypeChecker2::checkTypeInstantiation( const AstArray& typeArguments ) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSupport); - const FunctionType* ftv = get(follow(fnType)); if (!ftv) { diff --git a/Analysis/src/TypeFunction.cpp b/Analysis/src/TypeFunction.cpp index 52131938..8ea25694 100644 --- a/Analysis/src/TypeFunction.cpp +++ b/Analysis/src/TypeFunction.cpp @@ -7,7 +7,7 @@ #include "Luau/DenseHash.h" #include "Luau/Normalize.h" #include "Luau/NotNull.h" -#include "Luau/OverloadResolution.h" +#include "Luau/OverloadResolver.h" #include "Luau/Subtyping.h" #include "Luau/ToString.h" #include "Luau/TxnLog.h" @@ -32,8 +32,6 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyApplicationCartesianProductLimit, 5'0 LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1); LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies) -LUAU_FASTFLAGVARIABLE(LuauMarkUnscopedGenericsAsSolved) -LUAU_FASTFLAGVARIABLE(LuauUserTypeFunctionsNoUninhabitedError) namespace Luau { @@ -326,6 +324,8 @@ struct TypeFunctionReducer template void replace(T subject, T replacement) { + static_assert(std::is_same_v || std::is_same_v, "Can only replace types or type packs"); + if (subject->owningArena != ctx->arena.get()) { result.errors.emplace_back(location, InternalError{"Attempting to modify a type function instance from another arena"}); @@ -381,17 +381,15 @@ struct TypeFunctionReducer if (reduction.result) { replace(subject, *reduction.result); - for (auto ty : reduction.freshTypes) + for (auto ty : ctx->freshInstances) { - if constexpr (std::is_same_v) - queuedTys.push_back(ty); - else if constexpr (std::is_same_v) - queuedTps.push_back(ty); + queuedTys.push_back(ty); + if (ctx->solver) + ctx->pushConstraint(ReduceConstraint{ty}); } } else { - LUAU_ASSERT(reduction.freshTypes.empty()); irreducible.insert(subject); if (reduction.error.has_value()) @@ -419,16 +417,11 @@ struct TypeFunctionReducer if constexpr (std::is_same_v) { - if (FFlag::LuauUserTypeFunctionsNoUninhabitedError) + if (const TypeFunctionInstanceType* tf = get(subject)) { - if (const TypeFunctionInstanceType* tf = get(subject)) - { - if (tf->function != &ctx->builtins->typeFunctions->userFunc) - result.errors.emplace_back(location, UninhabitedTypeFunction{subject}); - } + if (tf->function != &ctx->builtins->typeFunctions->userFunc) + result.errors.emplace_back(location, UninhabitedTypeFunction{subject}); } - else - result.errors.emplace_back(location, UninhabitedTypeFunction{subject}); } else if constexpr (std::is_same_v) result.errors.emplace_back(location, UninhabitedTypePackFunction{subject}); @@ -455,6 +448,8 @@ struct TypeFunctionReducer else LUAU_ASSERT(!"Unreachable"); } + + ctx->freshInstances.clear(); } bool done() const @@ -593,11 +588,8 @@ struct TypeFunctionReducer // Let the caller know this type will not become reducible result.irreducibleTypes.insert(subject); - if (FFlag::LuauMarkUnscopedGenericsAsSolved) - { - if (getState(subject) == TypeFunctionInstanceState::Unsolved) - setState(subject, TypeFunctionInstanceState::Solved); - } + if (getState(subject) == TypeFunctionInstanceState::Unsolved) + setState(subject, TypeFunctionInstanceState::Solved); if (FFlag::DebugLuauLogTypeFamilies) printf("Irreducible due to an unscoped generic type\n"); diff --git a/Analysis/src/TypeFunctionError.cpp b/Analysis/src/TypeFunctionError.cpp new file mode 100644 index 00000000..577068cb --- /dev/null +++ b/Analysis/src/TypeFunctionError.cpp @@ -0,0 +1,74 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/TypeFunctionError.h" + +#include "Luau/StringUtils.h" +#include "Luau/ToString.h" + +namespace Luau +{ + +bool UnsupportedType::operator==(const UnsupportedType& rhs) const +{ + return type == rhs.type; +} + +bool UnsupportedTypePack::operator==(const UnsupportedTypePack& rhs) const +{ + return pack == rhs.pack; +} + +bool RuntimeError::operator==(const RuntimeError& rhs) const +{ + return message == rhs.message; +} + +bool FailedToCompile::operator==(const FailedToCompile& rhs) const +{ + return functionName == rhs.functionName && compileError == rhs.compileError; +} + +bool TypeFunctionMissing::operator==(const TypeFunctionMissing& rhs) const +{ + return functionName == rhs.functionName; +} + +bool TypeFunctionError::operator==(const TypeFunctionError& rhs) const +{ + return location == rhs.location && moduleName == rhs.moduleName && data == rhs.data; +} + +struct TypeFunctionErrorConverter +{ + std::string operator()(const UnsupportedType& e) const + { + return format("Type functions do not currently support types of the form '%s'", toString(e.type).c_str()); + } + + std::string operator()(const UnsupportedTypePack& e) const + { + return format("Type functions do not currently support types of the form '%s'", toString(e.pack).c_str()); + } + + std::string operator()(const RuntimeError& e) const + { + return e.message; + } + + std::string operator()(const FailedToCompile& e) const + { + return format("'%s' type function failed to compile with error message: %s", e.functionName.c_str(), e.compileError.c_str()); + } + + std::string operator()(const TypeFunctionMissing& e) const + { + return format("Could not find '%s' type function in the global scope", e.functionName.c_str()); + } +}; + +std::string toString(const TypeFunctionError& error) +{ + TypeFunctionErrorConverter converter; + return visit(converter, error.data); +} + +} // namespace Luau diff --git a/Analysis/src/TypeFunctionRuntime.cpp b/Analysis/src/TypeFunctionRuntime.cpp index 578851de..6fb72931 100644 --- a/Analysis/src/TypeFunctionRuntime.cpp +++ b/Analysis/src/TypeFunctionRuntime.cpp @@ -13,6 +13,8 @@ #include "Luau/Type.h" #include "Luau/TypeFunction.h" #include "Luau/TypeFunctionRuntimeBuilder.h" +#include "Luau/RecursionCounter.h" +#include "Luau/ToString.h" #include "lua.h" #include "lualib.h" @@ -22,11 +24,16 @@ #include LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit) -LUAU_FASTFLAG(LuauTypeCheckerUdtfRenameClassToExtern) +LUAU_FASTFLAG(LuauIntegerType2) -LUAU_FASTFLAGVARIABLE(LuauUnionofIntersectionofFlattens) LUAU_FASTFLAGVARIABLE(LuauTypeFunctionSupportsFrozen) -LUAU_FASTFLAGVARIABLE(LuauUdtfReserveStack) +LUAU_FASTFLAGVARIABLE(LuauTypeFunctionStructuredErrors) +LUAU_FASTFLAGVARIABLE(LuauTypeFunctionSerializeArgNames) +LUAU_FASTFLAGVARIABLE(LuauUdtfTypeIsSubtypeOf) +LUAU_FASTFLAGVARIABLE(LuauTypeFunctionTableIndexerIsReadOnly) +LUAU_FASTFLAGVARIABLE(LuauUdtfCreateSingletonFixErrorMessage) +LUAU_FASTFLAGVARIABLE(LuauUdtfTypeUseTaggedMetatable) +LUAU_FASTFLAGVARIABLE(LuauUdtfTypeToStringMetamethod) namespace Luau { @@ -52,7 +59,7 @@ TypeFunctionRuntime::TypeFunctionRuntime(NotNull ice, Not TypeFunctionRuntime::~TypeFunctionRuntime() {} -std::optional TypeFunctionRuntime::registerFunction(AstStatTypeFunction* function) +std::optional TypeFunctionRuntime::registerFunction_DEPRECATED(AstStatTypeFunction* function) { // If evaluation is disabled, we do not generate additional error messages if (!allowEvaluation) @@ -116,6 +123,92 @@ std::optional TypeFunctionRuntime::registerFunction(AstStatTypeFunc lua_setreadonly(L, -1, true); lua_pop(L, 1); + // Load bytecode into Luau state + if (auto error = checkResultForError_DEPRECATED(L, name.value, luau_load(L, name.value, bytecode.data(), bytecode.size(), 0))) + return error; + + // Execute the global function which should return our user-defined type function + if (auto error = checkResultForError_DEPRECATED(L, name.value, lua_resume(L, nullptr, 0))) + return error; + + if (!lua_isfunction(L, -1)) + { + lua_pop(L, 1); + return format("Could not find '%s' type function in the global scope", name.value); + } + + // Store resulting function in the registry + lua_pushlightuserdata(global, function); + lua_xmove(L, global, 1); + lua_settable(global, LUA_REGISTRYINDEX); + + return std::nullopt; +} + +std::optional TypeFunctionRuntime::registerFunction(AstStatTypeFunction* function) +{ + // If evaluation is disabled, we do not generate additional error messages + if (!allowEvaluation) + return std::nullopt; + + // Do not evaluate type functions with parse errors inside + if (function->hasErrors) + return std::nullopt; + + prepareState(); + + lua_State* global = state.get(); + + // Fetch to check if function is already registered + lua_pushlightuserdata(global, function); + lua_gettable(global, LUA_REGISTRYINDEX); + + if (!lua_isnil(global, -1)) + { + lua_pop(global, 1); + return std::nullopt; + } + + lua_pop(global, 1); + + AstName name = function->name; + + // Construct ParseResult containing the type function + Allocator allocator; + AstNameTable names(allocator); + + AstExpr* exprFunction = function->body; + AstArray exprReturns{&exprFunction, 1}; + AstStatReturn stmtReturn{Location{}, exprReturns}; + AstStat* stmtArray[] = {&stmtReturn}; + AstArray stmts{stmtArray, 1}; + AstStatBlock exec{Location{}, stmts}; + ParseResult parseResult{&exec, 1, {}, {}, {}, CstNodeMap{nullptr}}; + + BytecodeBuilder builder; + try + { + compileOrThrow(builder, parseResult, names); + } + catch (CompileError& e) + { + return TypeFunctionError{Location{}, FailedToCompile{name.value, e.what()}}; + } + + std::string bytecode = builder.getBytecode(); + + // Separate sandboxed thread for individual execution and private globals + lua_State* L = lua_newthread(global); + LuauTempThreadPopper popper(global); + + // Create individual environment for the type function + luaL_sandboxthread(L); + + // Do not allow global writes to that environment + lua_pushvalue(L, LUA_GLOBALSINDEX); + lua_setreadonly(L, -1, true); + lua_pop(L, 1); + // Load bytecode into Luau state if (auto error = checkResultForError(L, name.value, luau_load(L, name.value, bytecode.data(), bytecode.size(), 0))) return error; @@ -127,7 +220,7 @@ std::optional TypeFunctionRuntime::registerFunction(AstStatTypeFunc if (!lua_isfunction(L, -1)) { lua_pop(L, 1); - return format("Could not find '%s' type function in the global scope", name.value); + return TypeFunctionError{Location{}, TypeFunctionMissing{name.value}}; } // Store resulting function in the registry @@ -182,7 +275,7 @@ void* typeFunctionAlloc(void* ud, void* ptr, size_t osize, size_t nsize) } } -std::optional checkResultForError(lua_State* L, const char* typeFunctionName, int luaResult) +std::optional checkResultForError_DEPRECATED(lua_State* L, const char* typeFunctionName, int luaResult) { switch (luaResult) { @@ -202,6 +295,31 @@ std::optional checkResultForError(lua_State* L, const char* typeFun } } +std::optional checkResultForError(lua_State* L, const char* typeFunctionName, int luaResult) +{ + switch (luaResult) + { + case LUA_OK: + return std::nullopt; + case LUA_YIELD: + case LUA_BREAK: + return TypeFunctionError{Location{}, RuntimeError{format("'%s' type function errored: unexpected yield or break", typeFunctionName)}}; + default: + if (!lua_gettop(L)) + return TypeFunctionError{Location{}, RuntimeError{format("'%s' type function errored unexpectedly", typeFunctionName)}}; + + if (lua_isstring(L, -1)) + return TypeFunctionError{ + Location{}, RuntimeError{format("'%s' type function errored at runtime: %s", typeFunctionName, lua_tostring(L, -1))} + }; + + return TypeFunctionError{ + Location{}, + RuntimeError{format("'%s' type function errored at runtime: raised an error of type %s", typeFunctionName, lua_typename(L, -1))} + }; + } +} + TypeFunctionRuntime* getTypeFunctionRuntime(lua_State* L) { return static_cast(lua_getthreaddata(lua_mainthread(L))); @@ -221,31 +339,46 @@ TypeFunctionTypePackVar* allocateTypeFunctionTypePack(lua_State* L, TypeFunction void pushType(lua_State* L, TypeFunctionTypeId type) { - if (FFlag::LuauUdtfReserveStack) - luaL_checkstack(L, 2, "allocating type"); + luaL_checkstack(L, 2, "allocating type"); - TypeFunctionTypeId* ptr = static_cast(lua_newuserdatatagged(L, sizeof(TypeFunctionTypeId), kTypeUserdataTag)); - *ptr = type; + if (FFlag::LuauUdtfTypeUseTaggedMetatable) + { + TypeFunctionTypeId* ptr = static_cast(lua_newuserdatataggedwithmetatable(L, sizeof(TypeFunctionTypeId), kTypeUserdataTag)); + *ptr = type; + } + else + { + TypeFunctionTypeId* ptr = static_cast(lua_newuserdatatagged(L, sizeof(TypeFunctionTypeId), kTypeUserdataTag)); + *ptr = type; - // set the new userdata's metatable to type metatable - luaL_getmetatable(L, "type"); - lua_setmetatable(L, -2); + // set the new userdata's metatable to type metatable + luaL_getmetatable(L, "type"); + lua_setmetatable(L, -2); + } } // Pushes a new type userdata onto the stack void allocTypeUserData(lua_State* L, TypeFunctionTypeVariant type, bool frozen) { - if (FFlag::LuauUdtfReserveStack) - luaL_checkstack(L, 2, "allocating type"); + luaL_checkstack(L, 2, "allocating type"); // allocate a new type userdata - TypeFunctionTypeId* ptr = static_cast(lua_newuserdatatagged(L, sizeof(TypeFunctionTypeId), kTypeUserdataTag)); - *ptr = allocateTypeFunctionType(L, std::move(type)); - const_cast(*ptr)->frozen = frozen; + if (FFlag::LuauUdtfTypeUseTaggedMetatable) + { + TypeFunctionTypeId* ptr = static_cast(lua_newuserdatataggedwithmetatable(L, sizeof(TypeFunctionTypeId), kTypeUserdataTag)); + *ptr = allocateTypeFunctionType(L, std::move(type)); + const_cast(*ptr)->frozen = frozen; + } + else + { + TypeFunctionTypeId* ptr = static_cast(lua_newuserdatatagged(L, sizeof(TypeFunctionTypeId), kTypeUserdataTag)); + *ptr = allocateTypeFunctionType(L, std::move(type)); + const_cast(*ptr)->frozen = frozen; - // set the new userdata's metatable to type metatable - luaL_getmetatable(L, "type"); - lua_setmetatable(L, -2); + // set the new userdata's metatable to type metatable + luaL_getmetatable(L, "type"); + lua_setmetatable(L, -2); + } } void deallocTypeUserData(lua_State* L, void* data) @@ -255,7 +388,7 @@ void deallocTypeUserData(lua_State* L, void* data) bool isTypeUserData(lua_State* L, int idx) { - if (!lua_isuserdata(L, idx)) + if (!FFlag::LuauUdtfTypeUseTaggedMetatable && !lua_isuserdata(L, idx)) return false; return lua_touserdatatagged(L, idx, kTypeUserdataTag) != nullptr; @@ -263,10 +396,17 @@ bool isTypeUserData(lua_State* L, int idx) TypeFunctionTypeId getTypeUserData(lua_State* L, int idx) { - if (auto typ = static_cast(lua_touserdatatagged(L, idx, kTypeUserdataTag))) - return *typ; + if (FFlag::LuauUdtfTypeUseTaggedMetatable) + { + return *static_cast(luaL_checkudatatagged(L, idx, kTypeUserdataTag)); + } + else + { + if (auto typ = static_cast(lua_touserdatatagged(L, idx, kTypeUserdataTag))) + return *typ; - luaL_typeerrorL(L, idx, "type"); + luaL_typeerrorL(L, idx, "type"); + } } std::optional optionalTypeUserData(lua_State* L, int idx) @@ -286,6 +426,8 @@ static std::string getTag(lua_State* L, TypeFunctionTypeId ty) return "boolean"; else if (auto n = get(ty); n && n->type == TypeFunctionPrimitiveType::Type::Number) return "number"; + else if (auto n = get(ty); n && (FFlag::LuauIntegerType2 && (n->type == TypeFunctionPrimitiveType::Type::Integer))) + return "integer"; else if (auto s = get(ty); s && s->type == TypeFunctionPrimitiveType::Type::String) return "string"; else if (auto s = get(ty); s && s->type == TypeFunctionPrimitiveType::Type::Thread) @@ -311,16 +453,11 @@ static std::string getTag(lua_State* L, TypeFunctionTypeId ty) else if (get(ty)) return "function"; else if (get(ty)) - { - if (FFlag::LuauTypeCheckerUdtfRenameClassToExtern) - return "extern"; - else - return "class"; - } + return "extern"; else if (get(ty)) return "generic"; - LUAU_UNREACHABLE(); + LUAU_ASSERT(!"Unsupported type in getTag"); luaL_error(L, "VM encountered unexpected type variant when determining tag"); } @@ -422,7 +559,10 @@ static int createSingleton(lua_State* L) return 1; } - luaL_error(L, "types.singleton: can't create singleton from `%s` type", lua_typename(L, 1)); + if (FFlag::LuauUdtfCreateSingletonFixErrorMessage) + luaL_error(L, "types.singleton: can't create a singleton from a %s", luaL_typename(L, 1)); + else + luaL_error(L, "types.singleton: can't create singleton from `%s` type", lua_typename(L, 1)); } // Luau: `types.generic(name: string, ispack: boolean?) -> type @@ -512,40 +652,26 @@ static int createUnion(lua_State* L) { // get the number of arguments for union int argSize = lua_gettop(L); - if (!FFlag::LuauUnionofIntersectionofFlattens && argSize < 2) - luaL_error(L, "types.unionof: expected at least 2 types to union, but got %d", argSize); std::vector components; components.reserve(argSize); for (int i = 1; i <= argSize; i++) { - if (FFlag::LuauUnionofIntersectionofFlattens) - { - TypeFunctionTypeId component = getTypeUserData(L, i); + TypeFunctionTypeId component = getTypeUserData(L, i); - if (auto unionComponent = get(component)) - components.insert(components.end(), unionComponent->components.begin(), unionComponent->components.end()); - else if (get(component)) - continue; - else - components.push_back(component); - } + if (auto unionComponent = get(component)) + components.insert(components.end(), unionComponent->components.begin(), unionComponent->components.end()); + else if (get(component)) + continue; else - { - components.push_back(getTypeUserData(L, i)); - } + components.push_back(component); } - if (FFlag::LuauUnionofIntersectionofFlattens) - { - if (components.size() == 0) - allocTypeUserData(L, TypeFunctionNeverType{}); - else if (components.size() == 1) - pushType(L, components[0]); - else - allocTypeUserData(L, TypeFunctionUnionType{std::move(components)}); - } + if (components.size() == 0) + allocTypeUserData(L, TypeFunctionNeverType{}); + else if (components.size() == 1) + pushType(L, components[0]); else allocTypeUserData(L, TypeFunctionUnionType{std::move(components)}); @@ -558,40 +684,26 @@ static int createIntersection(lua_State* L) { // get the number of arguments for intersection int argSize = lua_gettop(L); - if (!FFlag::LuauUnionofIntersectionofFlattens && argSize < 2) - luaL_error(L, "types.intersectionof: expected at least 2 types to intersection, but got %d", argSize); std::vector components; components.reserve(argSize); for (int i = 1; i <= argSize; i++) { - if (FFlag::LuauUnionofIntersectionofFlattens) - { - TypeFunctionTypeId component = getTypeUserData(L, i); + TypeFunctionTypeId component = getTypeUserData(L, i); - if (auto intersectionComponent = get(component)) - components.insert(components.end(), intersectionComponent->components.begin(), intersectionComponent->components.end()); - else if (get(component)) - continue; - else - components.push_back(component); - } + if (auto intersectionComponent = get(component)) + components.insert(components.end(), intersectionComponent->components.begin(), intersectionComponent->components.end()); + else if (get(component)) + continue; else - { - components.push_back(getTypeUserData(L, i)); - } + components.push_back(component); } - if (FFlag::LuauUnionofIntersectionofFlattens) - { - if (components.size() == 0) - allocTypeUserData(L, TypeFunctionUnknownType{}); - else if (components.size() == 1) - pushType(L, components[0]); - else - allocTypeUserData(L, TypeFunctionIntersectionType{std::move(components)}); - } + if (components.size() == 0) + allocTypeUserData(L, TypeFunctionUnknownType{}); + else if (components.size() == 1) + pushType(L, components[0]); else allocTypeUserData(L, TypeFunctionIntersectionType{std::move(components)}); @@ -1028,7 +1140,9 @@ static int setTableMetatable(lua_State* L) TypeFunctionTypeId arg = getTypeUserData(L, 2); if (!get(arg)) - luaL_error(L, "type.setmetatable: expected the argument to be a table, but got %s instead", getTag(L, self).c_str()); + { + luaL_error(L, "type.setmetatable: expected the argument to be a table, but got %s instead", getTag(L, arg).c_str()); + } tftt->metatable = arg; @@ -1332,8 +1446,9 @@ static int setFunctionGenerics(lua_State* L) luaL_error(L, "type.setgenerics: cannot be called to mutate a frozen type, use `types.copy` to make a copy"); int argumentCount = lua_gettop(L); - if (argumentCount > 3) - luaL_error(L, "type.setgenerics: expected 3 arguments, but got %d", argumentCount); + + if (argumentCount > 2) + luaL_error(L, "type.setgenerics: expected 2 arguments, but got %d", argumentCount); auto [genericTypes, genericPacks] = getGenerics(L, 2, "types.setgenerics"); @@ -1713,6 +1828,34 @@ static int checkTag(lua_State* L) return 1; } +// Luau `self:issubtypeof(arg: type) -> boolean` +// Returns true if self is a subtype of the given type +static int isSubtypeOf(lua_State* L) +{ + int argumentCount = lua_gettop(L); + if (argumentCount != 2) + luaL_error(L, "type.issubtypeof: expected 2 arguments, but got %d", argumentCount); + + TypeFunctionTypeId self = getTypeUserData(L, 1); + TypeFunctionTypeId arg = getTypeUserData(L, 2); + + TypeFunctionRuntimeBuilderState* runtimeBuilder = Luau::getTypeFunctionRuntime(L)->runtimeBuilder; + NotNull ctx = runtimeBuilder->ctx; + + TypeId subTy = Luau::deserialize(self, runtimeBuilder); + if (FFlag::LuauTypeFunctionStructuredErrors ? !runtimeBuilder->errors.empty() : !runtimeBuilder->errors_DEPRECATED.empty()) + luaL_error(L, "failed to deserialize the self type"); + + TypeId superTy = Luau::deserialize(arg, runtimeBuilder); + if (FFlag::LuauTypeFunctionStructuredErrors ? !runtimeBuilder->errors.empty() : !runtimeBuilder->errors_DEPRECATED.empty()) + luaL_error(L, "failed to deserialize the argument type"); + + SubtypingResult result = ctx->subtyping->isSubtype(subTy, superTy, ctx->scope); + + lua_pushboolean(L, static_cast(result.isSubtype)); + return 1; +} + TypeFunctionTypeId deepClone(NotNull runtime, TypeFunctionTypeId ty); // Forward declaration // Luau: `types.copy(arg: type) -> type` @@ -1726,6 +1869,10 @@ static int deepCopy(lua_State* L) TypeFunctionTypeId arg = getTypeUserData(L, 1); TypeFunctionTypeId copy = deepClone(NotNull{getTypeFunctionRuntime(L)}, arg); + + if (!copy) + luaL_error(L, "types.copy: complexity limit reached during type copy"); + allocTypeUserData(L, copy->type); return 1; } @@ -1745,6 +1892,23 @@ static int isEqualToType(lua_State* L) return 1; } +// Luau: `tostring(self) -> string`, +// or other cases where the `__tostring` metamethod is invoked +static int typeToString(lua_State* L) +{ + TypeFunctionTypeId self = getTypeUserData(L, 1); + + TypeFunctionRuntimeBuilderState* runtimeBuilder = Luau::getTypeFunctionRuntime(L)->runtimeBuilder; + TypeId selfTy = Luau::deserialize(self, runtimeBuilder); + if (FFlag::LuauTypeFunctionStructuredErrors ? !runtimeBuilder->errors.empty() : !runtimeBuilder->errors_DEPRECATED.empty()) + luaL_error(L, "failed to deserialize the self type"); + + std::string asString = Luau::toString(selfTy); + + lua_pushlstring(L, asString.data(), asString.size()); + return 1; +} + void registerTypesLibrary(lua_State* L) { luaL_Reg fields[] = { @@ -1843,10 +2007,6 @@ void registerTypeUserData(lua_State* L) {"readparent", getReadParent}, {"writeparent", getWriteParent}, - // Function type methods (cont.) - {"setgenerics", setFunctionGenerics}, - {"generics", getFunctionGenerics}, - // Generic type methods {"name", getGenericName}, {"ispack", getGenericIsPack}, @@ -1867,15 +2027,33 @@ void registerTypeUserData(lua_State* L) lua_pushcfunction(L, isEqualToType, "__eq"); lua_setfield(L, -2, "__eq"); + if (FFlag::LuauUdtfTypeToStringMetamethod) + { + lua_pushcfunction(L, typeToString, "__tostring"); + lua_setfield(L, -2, "__tostring"); + } + // Indexing will be a dynamic function because some type fields are dynamic lua_newtable(L); luaL_register(L, nullptr, typeUserdataMethods); + + if (FFlag::LuauUdtfTypeIsSubtypeOf) + { + lua_pushcfunction(L, isSubtypeOf, "issubtypeof"); + lua_setfield(L, -2, "issubtypeof"); + } + lua_setreadonly(L, -1, true); lua_pushcclosure(L, typeUserdataIndex, "__index", 1); lua_setfield(L, -2, "__index"); lua_setreadonly(L, -1, true); - lua_pop(L, 1); + + if (FFlag::LuauUdtfTypeUseTaggedMetatable) + // Sets up the metatable for the type userdata. + lua_setuserdatametatable(L, kTypeUserdataTag); + else + lua_pop(L, 1); // Sets up a destructor for the type userdata. lua_setuserdatadtor(L, kTypeUserdataTag, deallocTypeUserData); @@ -1885,7 +2063,6 @@ void registerTypeUserData(lua_State* L) static int unsupportedFunction(lua_State* L) { luaL_errorL(L, "this function is not supported in type functions"); - return 0; } static int print(lua_State* L) @@ -1966,26 +2143,30 @@ void resetTypeFunctionState(lua_State* L) /* * Below are helper methods for __eq - * Same as one from Type.cpp */ -using SeenSet = std::set>; -bool areEqual(SeenSet& seen, const TypeFunctionType& lhs, const TypeFunctionType& rhs); -bool areEqual(SeenSet& seen, const TypeFunctionTypePackVar& lhs, const TypeFunctionTypePackVar& rhs); +struct AreEqualState +{ + std::set> seen; + int recursionCount = 0; +}; -bool seenSetContains(SeenSet& seen, const void* lhs, const void* rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionType& lhs, const TypeFunctionType& rhs); +bool areEqual(AreEqualState& seen, const TypeFunctionTypePackVar& lhs, const TypeFunctionTypePackVar& rhs); + +bool seenSetContains(AreEqualState& seen, const void* lhs, const void* rhs) { if (lhs == rhs) return true; auto p = std::make_pair(lhs, rhs); - if (seen.find(p) != seen.end()) + if (seen.seen.find(p) != seen.seen.end()) return true; - seen.insert(p); + seen.seen.insert(p); return false; } -bool areEqual(SeenSet& seen, const TypeFunctionSingletonType& lhs, const TypeFunctionSingletonType& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionSingletonType& lhs, const TypeFunctionSingletonType& rhs) { if (seenSetContains(seen, &lhs, &rhs)) return true; @@ -2007,7 +2188,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionSingletonType& lhs, const TypeFun return false; } -bool areEqual(SeenSet& seen, const TypeFunctionUnionType& lhs, const TypeFunctionUnionType& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionUnionType& lhs, const TypeFunctionUnionType& rhs) { if (seenSetContains(seen, &lhs, &rhs)) return true; @@ -2029,7 +2210,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionUnionType& lhs, const TypeFunctio return true; } -bool areEqual(SeenSet& seen, const TypeFunctionIntersectionType& lhs, const TypeFunctionIntersectionType& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionIntersectionType& lhs, const TypeFunctionIntersectionType& rhs) { if (seenSetContains(seen, &lhs, &rhs)) return true; @@ -2051,7 +2232,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionIntersectionType& lhs, const Type return true; } -bool areEqual(SeenSet& seen, const TypeFunctionNegationType& lhs, const TypeFunctionNegationType& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionNegationType& lhs, const TypeFunctionNegationType& rhs) { if (seenSetContains(seen, &lhs, &rhs)) return true; @@ -2059,7 +2240,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionNegationType& lhs, const TypeFunc return areEqual(seen, *lhs.type, *rhs.type); } -bool areEqual(SeenSet& seen, const TypeFunctionTableType& lhs, const TypeFunctionTableType& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionTableType& lhs, const TypeFunctionTableType& rhs) { if (seenSetContains(seen, &lhs, &rhs)) return true; @@ -2103,7 +2284,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionTableType& lhs, const TypeFunctio return true; } -bool areEqual(SeenSet& seen, const TypeFunctionFunctionType& lhs, const TypeFunctionFunctionType& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionFunctionType& lhs, const TypeFunctionFunctionType& rhs) { if (seenSetContains(seen, &lhs, &rhs)) return true; @@ -2147,7 +2328,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionFunctionType& lhs, const TypeFunc return true; } -bool areEqual(SeenSet& seen, const TypeFunctionExternType& lhs, const TypeFunctionExternType& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionExternType& lhs, const TypeFunctionExternType& rhs) { if (seenSetContains(seen, &lhs, &rhs)) return true; @@ -2155,8 +2336,9 @@ bool areEqual(SeenSet& seen, const TypeFunctionExternType& lhs, const TypeFuncti return lhs.externTy == rhs.externTy; } -bool areEqual(SeenSet& seen, const TypeFunctionType& lhs, const TypeFunctionType& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionType& lhs, const TypeFunctionType& rhs) { + RecursionLimiter _ra("areEqual", &seen.recursionCount, 100); if (lhs.type.index() != rhs.type.index()) return false; @@ -2236,7 +2418,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionType& lhs, const TypeFunctionType return false; } -bool areEqual(SeenSet& seen, const TypeFunctionTypePack& lhs, const TypeFunctionTypePack& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionTypePack& lhs, const TypeFunctionTypePack& rhs) { if (lhs.head.size() != rhs.head.size()) return false; @@ -2255,7 +2437,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionTypePack& lhs, const TypeFunction return true; } -bool areEqual(SeenSet& seen, const TypeFunctionVariadicTypePack& lhs, const TypeFunctionVariadicTypePack& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionVariadicTypePack& lhs, const TypeFunctionVariadicTypePack& rhs) { if (seenSetContains(seen, &lhs, &rhs)) return true; @@ -2263,7 +2445,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionVariadicTypePack& lhs, const Type return areEqual(seen, *lhs.type, *rhs.type); } -bool areEqual(SeenSet& seen, const TypeFunctionTypePackVar& lhs, const TypeFunctionTypePackVar& rhs) +bool areEqual(AreEqualState& seen, const TypeFunctionTypePackVar& lhs, const TypeFunctionTypePackVar& rhs) { { const TypeFunctionTypePack* lb = get(&lhs); @@ -2291,17 +2473,16 @@ bool areEqual(SeenSet& seen, const TypeFunctionTypePackVar& lhs, const TypeFunct bool TypeFunctionType::operator==(const TypeFunctionType& rhs) const { - SeenSet seen; + AreEqualState seen; return areEqual(seen, *this, rhs); } bool TypeFunctionTypePackVar::operator==(const TypeFunctionTypePackVar& rhs) const { - SeenSet seen; + AreEqualState seen; return areEqual(seen, *this, rhs); } - TypeFunctionProperty TypeFunctionProperty::readonly(TypeFunctionTypeId ty) { TypeFunctionProperty p; @@ -2469,6 +2650,9 @@ class TypeFunctionCloner case TypeFunctionPrimitiveType::Number: target = typeFunctionRuntime->typeArena.allocate(TypeFunctionPrimitiveType(TypeFunctionPrimitiveType::Number)); break; + case TypeFunctionPrimitiveType::Integer: + target = typeFunctionRuntime->typeArena.allocate(TypeFunctionPrimitiveType(TypeFunctionPrimitiveType::Integer)); + break; case TypeFunctionPrimitiveType::String: target = typeFunctionRuntime->typeArena.allocate(TypeFunctionPrimitiveType(TypeFunctionPrimitiveType::String)); break; @@ -2653,7 +2837,13 @@ class TypeFunctionCloner } if (t1->indexer.has_value()) - t2->indexer = TypeFunctionTableIndexer(shallowClone(t1->indexer->keyType), shallowClone(t1->indexer->valueType)); + { + t2->indexer = TypeFunctionTableIndexer( + shallowClone(t1->indexer->keyType), + shallowClone(t1->indexer->valueType), + FFlag::LuauTypeFunctionTableIndexerIsReadOnly ? t1->indexer->isReadOnly : false + ); + } if (t1->metatable.has_value()) t2->metatable = shallowClone(*t1->metatable); @@ -2671,6 +2861,8 @@ class TypeFunctionCloner f2->argTypes = shallowClone(f1->argTypes); f2->retTypes = shallowClone(f1->retTypes); + if (FFlag::LuauTypeFunctionSerializeArgNames) + f2->argNames = f1->argNames; } void cloneChildren(TypeFunctionExternType* c1, TypeFunctionExternType* c2) diff --git a/Analysis/src/TypeFunctionRuntimeBuilder.cpp b/Analysis/src/TypeFunctionRuntimeBuilder.cpp index 1bb6a725..444124eb 100644 --- a/Analysis/src/TypeFunctionRuntimeBuilder.cpp +++ b/Analysis/src/TypeFunctionRuntimeBuilder.cpp @@ -20,7 +20,9 @@ // currently, controls serialization, deserialization, and `type.copy` LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFunctionSerdeIterationLimit, 100'000); -LUAU_FASTFLAGVARIABLE(LuauTypeFunctionDeserializationShouldNotCrashOnGenericPacks) +LUAU_FASTFLAG(LuauTypeFunctionStructuredErrors) +LUAU_FASTFLAG(LuauTypeFunctionSerializeArgNames) +LUAU_FASTFLAG(LuauTypeFunctionTableIndexerIsReadOnly) namespace Luau { @@ -63,7 +65,7 @@ class TypeFunctionSerializer shallowSerialize(ty); run(); - if (hasExceededIterationLimit() || state->errors.size() != 0) + if (hasExceededIterationLimit() || hasErrors()) return nullptr; return find(ty).value_or(nullptr); @@ -74,13 +76,20 @@ class TypeFunctionSerializer shallowSerialize(tp); run(); - if (hasExceededIterationLimit() || state->errors.size() != 0) + if (hasExceededIterationLimit() || hasErrors()) return nullptr; return find(tp).value_or(nullptr); } private: + bool hasErrors() const + { + if (FFlag::LuauTypeFunctionStructuredErrors) + return !state->errors.empty(); + return state->errors_DEPRECATED.size() != 0; + } + bool hasExceededIterationLimit() const { if (DFInt::LuauTypeFunctionSerdeIterationLimit == 0) @@ -95,7 +104,7 @@ class TypeFunctionSerializer { ++steps; - if (hasExceededIterationLimit() || state->errors.size() != 0) + if (hasExceededIterationLimit() || hasErrors()) break; auto [ty, tfti] = queue.back(); @@ -156,6 +165,9 @@ class TypeFunctionSerializer case PrimitiveType::Number: target = typeFunctionRuntime->typeArena.allocate(TypeFunctionPrimitiveType(TypeFunctionPrimitiveType::Number)); break; + case PrimitiveType::Integer: + target = typeFunctionRuntime->typeArena.allocate(TypeFunctionPrimitiveType(TypeFunctionPrimitiveType::Integer)); + break; case PrimitiveType::String: target = typeFunctionRuntime->typeArena.allocate(TypeFunctionPrimitiveType(TypeFunctionPrimitiveType::String)); break; @@ -168,10 +180,12 @@ class TypeFunctionSerializer case PrimitiveType::Function: case PrimitiveType::Table: default: - { - std::string error = format("Argument of primitive type %s is not currently serializable by type functions", toString(ty).c_str()); - state->errors.push_back(error); - } + if (FFlag::LuauTypeFunctionStructuredErrors) + state->errors.emplace_back(Location{}, UnsupportedType{ty}); + else + state->errors_DEPRECATED.push_back( + format("Argument of primitive type %s is not currently serializable by type functions", toString(ty).c_str()) + ); } } else if (get(ty)) @@ -188,8 +202,12 @@ class TypeFunctionSerializer target = typeFunctionRuntime->typeArena.allocate(TypeFunctionSingletonType{TypeFunctionStringSingleton{ss->value}}); else { - std::string error = format("Argument of singleton type %s is not currently serializable by type functions", toString(ty).c_str()); - state->errors.push_back(error); + if (FFlag::LuauTypeFunctionStructuredErrors) + state->errors.emplace_back(Location{}, UnsupportedType{ty}); + else + state->errors_DEPRECATED.push_back( + format("Argument of singleton type %s is not currently serializable by type functions", toString(ty).c_str()) + ); } } else if (get(ty)) @@ -224,8 +242,12 @@ class TypeFunctionSerializer } else { - std::string error = format("Argument of type %s is not currently serializable by type functions", toString(ty).c_str()); - state->errors.push_back(error); + if (FFlag::LuauTypeFunctionStructuredErrors) + state->errors.emplace_back(Location{}, UnsupportedType{ty}); + else + state->errors_DEPRECATED.push_back( + format("Argument of type %s is not currently serializable by type functions", toString(ty).c_str()) + ); } types[ty] = target; @@ -257,8 +279,12 @@ class TypeFunctionSerializer } else { - std::string error = format("Argument of type pack %s is not currently serializable by type functions", toString(tp).c_str()); - state->errors.push_back(error); + if (FFlag::LuauTypeFunctionStructuredErrors) + state->errors.emplace_back(Location{}, UnsupportedTypePack{tp}); + else + state->errors_DEPRECATED.push_back( + format("Argument of type pack %s is not currently serializable by type functions", toString(tp).c_str()) + ); } packs[tp] = target; @@ -295,9 +321,14 @@ class TypeFunctionSerializer else if (auto [g1, g2] = std::tuple{get(ty), getMutable(tfti)}; g1 && g2) serializeChildren(g1, g2); else - { // Either this or ty and tfti do not represent the same type - std::string error = format("Argument of type %s is not currently serializable by type functions", toString(ty).c_str()); - state->errors.push_back(error); + { + // Either this or ty and tfti do not represent the same type + if (FFlag::LuauTypeFunctionStructuredErrors) + state->errors.emplace_back(Location{}, UnsupportedType{ty}); + else + state->errors_DEPRECATED.push_back( + format("Argument of type %s is not currently serializable by type functions", toString(ty).c_str()) + ); } } @@ -310,9 +341,12 @@ class TypeFunctionSerializer else if (auto [gPack1, gPack2] = std::tuple{get(tp), getMutable(tftp)}; gPack1 && gPack2) serializeChildren(gPack1, gPack2); else - { // Either this or ty and tfti do not represent the same type - std::string error = format("Argument of type pack %s is not currently serializable by type functions", toString(tp).c_str()); - state->errors.push_back(error); + { + // Either this or tp and tftp do not represent the same type + if (FFlag::LuauTypeFunctionStructuredErrors) + state->errors.emplace_back(Location{}, UnsupportedTypePack{tp}); + else + state->errors_DEPRECATED.push_back(format("Type functions do not currently support types of the form '%s'", toString(tp).c_str())); } } @@ -384,7 +418,13 @@ class TypeFunctionSerializer } if (t1->indexer) - t2->indexer = TypeFunctionTableIndexer(shallowSerialize(t1->indexer->indexType), shallowSerialize(t1->indexer->indexResultType)); + { + t2->indexer = TypeFunctionTableIndexer( + shallowSerialize(t1->indexer->indexType), + shallowSerialize(t1->indexer->indexResultType), + FFlag::LuauTypeFunctionTableIndexerIsReadOnly ? t1->indexer->isReadOnly : false + ); + } } void serializeChildren(const MetatableType* m1, TypeFunctionTableType* m2) @@ -408,6 +448,18 @@ class TypeFunctionSerializer f2->argTypes = shallowSerialize(f1->argTypes); f2->retTypes = shallowSerialize(f1->retTypes); + + if (FFlag::LuauTypeFunctionSerializeArgNames) + { + f2->argNames.reserve(f1->argNames.size()); + for (const auto& argName : f1->argNames) + { + if (argName) + f2->argNames.emplace_back(argName->name); + else + f2->argNames.emplace_back(); + } + } } void serializeChildren(const ExternType* c1, TypeFunctionExternType* c2) @@ -550,7 +602,7 @@ class TypeFunctionDeserializer shallowDeserialize(ty); run(); - if (hasExceededIterationLimit() || state->errors.size() != 0) + if (hasExceededIterationLimit() || hasErrors()) { TypeId error = state->ctx->builtins->errorType; types[ty] = error; @@ -565,7 +617,7 @@ class TypeFunctionDeserializer shallowDeserialize(tp); run(); - if (hasExceededIterationLimit() || state->errors.size() != 0) + if (hasExceededIterationLimit() || hasErrors()) { TypePackId error = state->ctx->builtins->errorTypePack; packs[tp] = error; @@ -584,13 +636,28 @@ class TypeFunctionDeserializer return steps + queue.size() >= size_t(DFInt::LuauTypeFunctionSerdeIterationLimit); } + bool hasErrors() const + { + if (FFlag::LuauTypeFunctionStructuredErrors) + return !state->errors.empty(); + return state->errors_DEPRECATED.size() != 0; + } + + void pushRuntimeError(std::string message) + { + if (FFlag::LuauTypeFunctionStructuredErrors) + state->errors.emplace_back(Location{}, RuntimeError{std::move(message)}); + else + state->errors_DEPRECATED.push_back(std::move(message)); + } + void run() { while (!queue.empty()) { ++steps; - if (hasExceededIterationLimit() || state->errors.size() != 0) + if (hasExceededIterationLimit() || hasErrors()) break; auto [tfti, ty] = queue.back(); @@ -599,7 +666,7 @@ class TypeFunctionDeserializer deserializeChildren(tfti, ty); // If we have completed working on all children of a function, remove the generic parameters from scope - if (!functionScopes.empty() && queue.size() == functionScopes.back().oldQueueSize && state->errors.empty()) + if (!functionScopes.empty() && queue.size() == functionScopes.back().oldQueueSize && !hasErrors()) { closeFunctionScope(functionScopes.back().function); functionScopes.pop_back(); @@ -671,6 +738,9 @@ class TypeFunctionDeserializer case TypeFunctionPrimitiveType::Type::Number: target = state->ctx->builtins->numberType; break; + case TypeFunctionPrimitiveType::Type::Integer: + target = state->ctx->builtins->integerType; + break; case TypeFunctionPrimitiveType::Type::String: target = state->ctx->builtins->stringType; break; @@ -725,7 +795,7 @@ class TypeFunctionDeserializer { if (g->isPack) { - state->errors.push_back(format("Generic type pack '%s...' cannot be placed in a type position", g->name.c_str())); + pushRuntimeError(format("Generic type pack '%s...' cannot be placed in a type position", g->name.c_str())); return nullptr; } else @@ -741,7 +811,7 @@ class TypeFunctionDeserializer if (it == genericTypes.rend()) { - state->errors.push_back(format("Generic type '%s' is not in a scope of the active generic function", g->name.c_str())); + pushRuntimeError(format("Generic type '%s' is not in a scope of the active generic function", g->name.c_str())); return nullptr; } @@ -784,7 +854,7 @@ class TypeFunctionDeserializer if (it == genericPacks.rend()) { - state->errors.push_back(format("Generic type pack '%s...' is not in a scope of the active generic function", gPack->name.c_str())); + pushRuntimeError(format("Generic type pack '%s...' is not in a scope of the active generic function", gPack->name.c_str())); return nullptr; } @@ -912,7 +982,13 @@ class TypeFunctionDeserializer } if (t2->indexer.has_value()) - t1->indexer = TableIndexer(shallowDeserialize(t2->indexer->keyType), shallowDeserialize(t2->indexer->valueType)); + { + t1->indexer = TableIndexer( + shallowDeserialize(t2->indexer->keyType), + shallowDeserialize(t2->indexer->valueType), + FFlag::LuauTypeFunctionTableIndexerIsReadOnly ? t2->indexer->isReadOnly : false + ); + } } void deserializeChildren(TypeFunctionTableType* m2, MetatableType* m1) @@ -933,9 +1009,9 @@ class TypeFunctionDeserializer for (auto ty : f2->generics) { auto gty = get(ty); - if (FFlag::LuauTypeFunctionDeserializationShouldNotCrashOnGenericPacks && (!gty || gty->isPack)) + if (!gty || gty->isPack) { - state->errors.emplace_back("Encountered unexpected generic"); + pushRuntimeError("Encountered unexpected generic"); return; } else @@ -946,7 +1022,7 @@ class TypeFunctionDeserializer // Duplicates are not allowed if (genericNames.find(nameKey) != genericNames.end()) { - state->errors.push_back(format("Duplicate type parameter '%s'", gty->name.c_str())); + pushRuntimeError(format("Duplicate type parameter '%s'", gty->name.c_str())); return; } @@ -959,9 +1035,9 @@ class TypeFunctionDeserializer for (auto tp : f2->genericPacks) { auto gtp = get(tp); - if (FFlag::LuauTypeFunctionDeserializationShouldNotCrashOnGenericPacks && !gtp) + if (!gtp) { - state->errors.emplace_back("Encountered unexpected generic type pack"); + pushRuntimeError("Encountered unexpected generic type pack"); return; } else @@ -972,7 +1048,7 @@ class TypeFunctionDeserializer // Duplicates are not allowed if (genericNames.find(nameKey) != genericNames.end()) { - state->errors.push_back(format("Duplicate type parameter '%s'", gtp->name.c_str())); + pushRuntimeError(format("Duplicate type parameter '%s'", gtp->name.c_str())); return; } @@ -996,6 +1072,18 @@ class TypeFunctionDeserializer if (f2->retTypes) f1->retTypes = shallowDeserialize(f2->retTypes); + + if (FFlag::LuauTypeFunctionSerializeArgNames) + { + f1->argNames.reserve(f2->argNames.size()); + for (const auto& name : f2->argNames) + { + if (name) + f1->argNames.emplace_back(FunctionArgument{*name, {}}); + else + f1->argNames.emplace_back(); + } + } } void deserializeChildren(TypeFunctionExternType* c2, ExternType* c1) diff --git a/Analysis/src/TypeIds.cpp b/Analysis/src/TypeIds.cpp index 56666cd7..14dd53fe 100644 --- a/Analysis/src/TypeIds.cpp +++ b/Analysis/src/TypeIds.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/Type.h" +#include "Luau/TypePack.h" #include "Luau/TypeIds.h" namespace Luau @@ -111,6 +112,11 @@ size_t TypeIds::count(TypeId ty) const return (val && *val) ? 1 : 0; } +bool TypeIds::contains(TypeId ty) const +{ + return 0 != count(ty); +} + void TypeIds::retain(const TypeIds& tys) { for (auto it = begin(); it != end();) @@ -179,5 +185,4 @@ void TypeIds::reserve(size_t n) order.reserve(n); } - } // namespace Luau diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index dd9ee668..8529feba 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -2,7 +2,6 @@ #include "Luau/TypeInfer.h" #include "Luau/ApplyTypeFunction.h" -#include "Luau/Cancellation.h" #include "Luau/Common.h" #include "Luau/Instantiation.h" #include "Luau/ModuleResolver.h" @@ -29,10 +28,11 @@ LUAU_FASTINTVARIABLE(LuauTypeInferTypePackLoopLimit, 5000) LUAU_FASTINTVARIABLE(LuauCheckRecursionLimit, 300) LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500) LUAU_FASTFLAG(LuauKnowsTheDataModel3) -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSyntax) -LUAU_FASTFLAGVARIABLE(LuauExplicitTypeInstantiationSupport) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification) LUAU_FASTFLAG(LuauInstantiateInSubtyping) +LUAU_FASTFLAG(LuauExportValueSyntax) +LUAU_FASTFLAG(LuauExportValueTypecheck) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) namespace Luau { @@ -212,6 +212,7 @@ TypeChecker::TypeChecker(const ScopePtr& globalScope, ModuleResolver* resolver, , reusableInstantiation(TxnLog::empty(), nullptr, builtinTypes, {}, nullptr) , nilType(builtinTypes->nilType) , numberType(builtinTypes->numberType) + , integerType(builtinTypes->integerType) , stringType(builtinTypes->stringType) , booleanType(builtinTypes->booleanType) , threadType(builtinTypes->threadType) @@ -245,10 +246,10 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo LUAU_TIMETRACE_ARGUMENT("module", module.name.c_str()); LUAU_TIMETRACE_ARGUMENT("name", module.humanReadableName.c_str()); - currentModule.reset(new Module); + currentModule = std::make_shared(std::make_shared()); currentModule->name = module.name; currentModule->humanReadableName = module.humanReadableName; - currentModule->internalTypes.owningModule = currentModule.get(); + currentModule->internalTypes->owningModule = currentModule.get(); currentModule->interfaceTypes.owningModule = currentModule.get(); currentModule->type = module.type; currentModule->allocator = module.allocator; @@ -256,7 +257,7 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo currentModule->root = module.root; iceHandler->moduleName = module.name; - normalizer.arena = ¤tModule->internalTypes; + normalizer.arena = currentModule->internalTypes.get(); unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit; unifierState.counters.iterationLimit = unifierIterationLimit ? *unifierIterationLimit : FInt::LuauTypeInferIterationLimit; @@ -286,6 +287,9 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo currentModule->cancelled = true; } + if (FFlag::LuauExportValueSyntax && FFlag::LuauExportValueTypecheck && !currentModule->timeout && !currentModule->cancelled) + synthesizeExportReturn(builtinTypes, NotNull{currentModule.get()}); + if (get(follow(moduleScope->returnType))) moduleScope->returnType = addTypePack(TypePack{{}, std::nullopt}); else @@ -304,7 +308,7 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo currentModule->clonePublicInterface(builtinTypes, *iceHandler, SolverMode::Old); - freeze(currentModule->internalTypes); + freeze(*currentModule->internalTypes); freeze(currentModule->interfaceTypes); // Clear unifier cache since it's keyed off internal typeArguments that get deallocated @@ -397,6 +401,11 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStat& program) return ControlFlow::None; } + else if (FFlag::DebugLuauUserDefinedClasses && program.is()) + { + reportError(program.as()->name->location, GenericError{"class keyword is illegal here"}); + return ControlFlow::None; + } else ice("Unknown AstStat"); } @@ -841,7 +850,7 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatReturn& retur } } - Demoter demoter{¤tModule->internalTypes, builtinTypes}; + Demoter demoter{currentModule->internalTypes.get(), builtinTypes}; demoter.demote(expectedTypes); TypePackId retPack = checkExprList(scope, return_.location, return_.list, false, {}, expectedTypes).type; @@ -869,12 +878,12 @@ ErrorVec TypeChecker::tryUnify_(Id subTy, Id superTy, const ScopePtr& scope, con Unifier state = mkUnifier(scope, location); if (FFlag::DebugLuauFreezeDuringUnification) - freeze(currentModule->internalTypes); + freeze(*currentModule->internalTypes); state.tryUnify(subTy, superTy); if (FFlag::DebugLuauFreezeDuringUnification) - unfreeze(currentModule->internalTypes); + unfreeze(*currentModule->internalTypes); if (state.errors.empty()) state.log.commit(); @@ -1445,7 +1454,7 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& checkFunctionBody(funScope, ty, *function.func); - InplaceDemoter demoter{funScope->level, ¤tModule->internalTypes}; + InplaceDemoter demoter{funScope->level, currentModule->internalTypes.get()}; demoter.traverse(ty); if (ttv && ttv->state != TableState::Sealed) @@ -1529,7 +1538,7 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& ty { // If the table is already named and we want to rename the type function, we have to bind new alias to a copy // Additionally, we can't modify typeArguments that come from other modules - if (ttv->name || follow(ty)->owningArena != ¤tModule->internalTypes) + if (ttv->name || follow(ty)->owningArena != currentModule->internalTypes.get()) { bool sameTys = std::equal( ttv->instantiatedTypeParams.begin(), @@ -1586,7 +1595,7 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& ty else if (auto mtv = getMutable(follow(ty))) { // We can't modify typeArguments that come from other modules - if (follow(ty)->owningArena == ¤tModule->internalTypes) + if (follow(ty)->owningArena == currentModule->internalTypes.get()) mtv->syntheticName = name; } @@ -1895,6 +1904,8 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp } else if (expr.is()) result = WithPredicate{numberType}; + else if (expr.is()) + result = WithPredicate{integerType}; else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) @@ -1924,10 +1935,7 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) - { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); result = checkExpr(scope, *a); - } else ice("Unhandled AstExpr?"); @@ -2315,7 +2323,7 @@ TypeId TypeChecker::checkExprTable( auto [keyType, valueType] = fieldTypes[i]; - if (item.kind == AstExprTable::Item::List) + if (item.kind == AstExprTable::Item::Kind::List) { if (expectedTable && !indexer) indexer = expectedTable->indexer; @@ -2328,7 +2336,7 @@ TypeId TypeChecker::checkExprTable( else indexer = TableIndexer{numberType, anyIfNonstrict(valueType)}; } - else if (item.kind == AstExprTable::Item::Record || item.kind == AstExprTable::Item::General) + else if (item.kind == AstExprTable::Item::Kind::Record || item.kind == AstExprTable::Item::Kind::General) { if (auto key = k->as()) { @@ -2426,12 +2434,12 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp std::optional expectedResultType; bool isIndexedItem = false; - if (item.kind == AstExprTable::Item::List) + if (item.kind == AstExprTable::Item::Kind::List) { expectedResultType = expectedIndexResultType; isIndexedItem = true; } - else if (item.kind == AstExprTable::Item::Record || item.kind == AstExprTable::Item::General) + else if (item.kind == AstExprTable::Item::Kind::Record || item.kind == AstExprTable::Item::Kind::General) { if (auto key = item.key->as()) { @@ -2488,9 +2496,9 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp switch (expr.op) { - case AstExprUnary::Not: + case AstExprUnary::Op::Not: return {booleanType, {NotPredicate{std::move(result.predicates)}}}; - case AstExprUnary::Minus: + case AstExprUnary::Op::Minus: { const bool operandIsAny = get(operandType) || get(operandType) || get(operandType); @@ -2529,7 +2537,7 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp reportErrors(tryUnify(operandType, numberType, scope, expr.location)); return WithPredicate{numberType}; } - case AstExprUnary::Len: + case AstExprUnary::Op::Len: { tablify(operandType); @@ -2780,7 +2788,7 @@ TypeId TypeChecker::checkRelationalOperation( // // eg it is okay to compare string? == number? because the two typeArguments // have nil in common, but string == number is not allowed. - std::optional eqTestResult = areEqComparable(NotNull{¤tModule->internalTypes}, NotNull{&normalizer}, lhsType, rhsType); + std::optional eqTestResult = areEqComparable(NotNull{currentModule->internalTypes.get()}, NotNull{&normalizer}, lhsType, rhsType); if (!eqTestResult) { reportErrorCodeTooComplex(expr.location); @@ -3280,9 +3288,6 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprInstantiate& explicitTypeInstantiation) { - if (!FFlag::LuauExplicitTypeInstantiationSupport) - return WithPredicate{errorRecoveryType(scope)}; - WithPredicate baseType = checkExpr(scope, *explicitTypeInstantiation.expr); return WithPredicate{instantiateTypeParameters( @@ -4482,9 +4487,7 @@ WithPredicate TypeChecker::checkExprPackHelper(const ScopePtr& scope functionType = *propTy; actualFunctionType = instantiate( scope, - FFlag::LuauExplicitTypeInstantiationSupport && expr.typeArguments.size - ? instantiateTypeParameters(scope, functionType, expr.typeArguments, expr.func, expr.location) - : functionType, + expr.typeArguments.size ? instantiateTypeParameters(scope, functionType, expr.typeArguments, expr.func, expr.location) : functionType, expr.func->location ); } @@ -4633,7 +4636,7 @@ std::vector> TypeChecker::getExpectedTypesForCall(const st } } - Demoter demoter{¤tModule->internalTypes, builtinTypes}; + Demoter demoter{currentModule->internalTypes.get(), builtinTypes}; demoter.demote(expectedTypes); return expectedTypes; @@ -4960,7 +4963,7 @@ WithPredicate TypeChecker::checkExprList( const Location& location, const AstArray& exprs, bool substituteFreeForNil, - const std::vector& instantiateGenerics, + const std::vector& annotatedTypeArguments, const std::vector>& expectedTypes ) { @@ -5031,7 +5034,7 @@ WithPredicate TypeChecker::checkExprList( if (!FFlag::LuauInstantiateInSubtyping) { - if (instantiateGenerics.size() > i && instantiateGenerics[i]) + if (annotatedTypeArguments.size() > i && annotatedTypeArguments[i]) actualType = instantiate(scope, actualType, expr->location); } @@ -5254,7 +5257,7 @@ TypeId TypeChecker::instantiate(const ScopePtr& scope, TypeId ty, Location locat std::optional instantiated; - reusableInstantiation.resetState(log, ¤tModule->internalTypes, builtinTypes, scope->level, /*scope*/ nullptr); + reusableInstantiation.resetState(log, currentModule->internalTypes.get(), builtinTypes, scope->level, /*scope*/ nullptr); if (instantiationChildLimit) reusableInstantiation.childLimit = *instantiationChildLimit; @@ -5272,7 +5275,7 @@ TypeId TypeChecker::instantiate(const ScopePtr& scope, TypeId ty, Location locat TypeId TypeChecker::anyify(const ScopePtr& scope, TypeId ty, Location location) { - Anyification anyification{¤tModule->internalTypes, scope, builtinTypes, iceHandler, anyType, anyTypePack}; + Anyification anyification{currentModule->internalTypes.get(), scope, builtinTypes, iceHandler, anyType, anyTypePack}; std::optional any = anyification.substitute(ty); if (anyification.normalizationTooComplex) reportError(location, NormalizationTooComplex{}); @@ -5287,7 +5290,7 @@ TypeId TypeChecker::anyify(const ScopePtr& scope, TypeId ty, Location location) TypePackId TypeChecker::anyify(const ScopePtr& scope, TypePackId ty, Location location) { - Anyification anyification{¤tModule->internalTypes, scope, builtinTypes, iceHandler, anyType, anyTypePack}; + Anyification anyification{currentModule->internalTypes.get(), scope, builtinTypes, iceHandler, anyType, anyTypePack}; std::optional any = anyification.substitute(ty); if (any.has_value()) return *any; @@ -5494,7 +5497,7 @@ TypeId TypeChecker::freshType(const ScopePtr& scope) TypeId TypeChecker::freshType(TypeLevel level) { - return currentModule->internalTypes.freshType(builtinTypes, level); + return currentModule->internalTypes->freshType(builtinTypes, level); } TypeId TypeChecker::singletonType(bool value) @@ -5505,7 +5508,7 @@ TypeId TypeChecker::singletonType(bool value) TypeId TypeChecker::singletonType(std::string value) { // TODO: cache singleton typeArguments - return currentModule->internalTypes.addType(Type(SingletonType(StringSingleton{std::move(value)}))); + return currentModule->internalTypes->addType(Type(SingletonType(StringSingleton{std::move(value)}))); } TypeId TypeChecker::errorRecoveryType(const ScopePtr& scope) @@ -5574,12 +5577,12 @@ std::pair, bool> TypeChecker::pickTypesFromSense(TypeId ty TypeId TypeChecker::addTV(Type&& tv) { - return currentModule->internalTypes.addType(std::move(tv)); + return currentModule->internalTypes->addType(std::move(tv)); } TypePackId TypeChecker::addTypePack(TypePackVar&& tv) { - return currentModule->internalTypes.addTypePack(std::move(tv)); + return currentModule->internalTypes->addTypePack(std::move(tv)); } TypePackId TypeChecker::addTypePack(TypePack&& tp) @@ -5750,7 +5753,7 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno if (notEnoughParameters && hasDefaultParameters) { // 'applyTypeFunction' is used to substitute default typeArguments that reference previous generic typeArguments - ApplyTypeFunction applyTypeFunction{¤tModule->internalTypes}; + ApplyTypeFunction applyTypeFunction{currentModule->internalTypes.get()}; for (size_t i = 0; i < typesProvided; ++i) applyTypeFunction.typeArguments[tf->typeParams[i].ty] = typeParams[i]; @@ -6066,7 +6069,7 @@ TypeId TypeChecker::instantiateTypeFun( if (tf.typeParams.empty() && tf.typePackParams.empty()) return tf.type; - ApplyTypeFunction applyTypeFunction{¤tModule->internalTypes}; + ApplyTypeFunction applyTypeFunction{currentModule->internalTypes.get()}; for (size_t i = 0; i < tf.typeParams.size(); ++i) applyTypeFunction.typeArguments[tf.typeParams[i].ty] = typeParams[i]; @@ -6539,6 +6542,8 @@ void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, RefinementMap& r return refine(isString, stringType); else if (typeguardP.kind == "number") return refine(isNumber, numberType); + else if (typeguardP.kind == "integer") + return refine(isInteger, integerType); else if (typeguardP.kind == "boolean") return refine(isBoolean, booleanType); else if (typeguardP.kind == "thread") diff --git a/Analysis/src/TypePack.cpp b/Analysis/src/TypePack.cpp index 86798544..1f0b0715 100644 --- a/Analysis/src/TypePack.cpp +++ b/Analysis/src/TypePack.cpp @@ -201,6 +201,14 @@ const TypeId& TypePackIterator::operator*() return tp->head[currentIndex]; } +std::optional TypePackIterator::tryGetHead() const +{ + if (currentIndex == 0) + return currentTypePack; + else + return std::nullopt; +} + std::optional TypePackIterator::tail() { LUAU_ASSERT(!tp); diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index 7560e73b..28c40a21 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -16,8 +16,6 @@ #include #include -LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauAnalysisUsesSolverMode) LUAU_FASTFLAG(LuauSubtypingMissingPropertiesAsNil) // Maximum number of steps to follow when traversing a path. May not always @@ -380,11 +378,7 @@ struct TraversalState if (prop) { - std::optional maybeType; - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) - maybeType = property.isRead ? prop->readTy : prop->writeTy; - else - maybeType = prop->type_DEPRECATED(); + std::optional maybeType = property.isRead ? prop->readTy : prop->writeTy; if (maybeType) { @@ -656,13 +650,10 @@ std::string toString(const TypePath::Path& path, bool prefixDot) if constexpr (std::is_same_v) { result << '['; - if (FFlag::LuauAnalysisUsesSolverMode || FFlag::LuauSolverV2) - { - if (c.isRead) - result << "read "; - else - result << "write "; - } + if (c.isRead) + result << "read "; + else + result << "write "; result << '"' << c.name << '"' << ']'; } diff --git a/Analysis/src/TypeStateMap.cpp b/Analysis/src/TypeStateMap.cpp new file mode 100644 index 00000000..2ef5acf5 --- /dev/null +++ b/Analysis/src/TypeStateMap.cpp @@ -0,0 +1,217 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/TypeStateMap.h" + +#include "Luau/Common.h" +#include "Luau/Constraint.h" +#include "Luau/ControlFlowGraph.h" +#include "Luau/Type.h" +#include "Luau/TypeArena.h" +#include "Luau/BuiltinTypeFunctions.h" +#include "Luau/TypeUtils.h" +#include + + +LUAU_FASTINTVARIABLE(LuauMaxCFGDataflowIterations, 2); + +namespace Luau::CFG +{ + +TypeStateMap::TypeStateMap(NotNull arena, NotNull globalScope, NotNull builtinTypes, NotNull g) + : arena(arena) + , builtinTypes(builtinTypes) + , g(g) + , globalScope(globalScope) +{ +} + +TypeId TypeStateMap::getRHSType(AstExpr* expr) +{ + if (Definition* def = g->getUseDef(expr)) + return getType(def); + return builtinTypes->errorType; +} + +TypeId TypeStateMap::getLHSType(const LValue& lv) const +{ + if (Definition* def = g->getLhsDef(lv)) + return getType(def); + return builtinTypes->anyType; +} + +std::optional TypeStateMap::getOptionalConstraint(TypeId ty) const +{ + if (auto cv = typesRequiringConstraint.find(ty)) + return {*cv}; + return std::nullopt; +} + +TypeId TypeStateMap::getType(Definition* def) const +{ + if (auto ty = defTypes.find(def)) + return *ty; + else + { + LUAU_ASSERT(!"Couldn't find type for definition - you probably forgot to allocate a type during CFG traversal"); + return builtinTypes->errorType; + } +} + +void TypeStateMap::computeTypes() +{ + for (int iter = 0; iter < FInt::LuauMaxCFGDataflowIterations; iter++) + { + for (Block* blk : g->rpo()) + { + for (auto inst : blk->getInstructions()) + handleInstruction(inst); + } + } +} + +void TypeStateMap::handleInstruction(InstrId id) +{ + visit( + overloaded{ + [&](const Dead&) {}, + [&](const Declare& decl) + { + if (defTypes.find(decl.def.get())) + return; + defTypes[decl.def.get()] = arena->addType(BlockedType{}); + }, + [&](const Assign& assign) + { + if (defTypes.find(assign.def.get())) + return; + defTypes[assign.def.get()] = arena->addType(BlockedType{}); + }, + [&](const Refine& refine) + { + if (defTypes.find(refine.definition)) + return; + if (auto ty = defTypes.find(refine.toRefine)) + { + auto dt = getDiscriminantOf(refine); + + TypeId result = arena->addTypeFunction(builtinTypes->typeFunctions->refineFunc, {*ty, dt}, {}); + defTypes[refine.definition] = result; + typesRequiringConstraint[result] = ReduceConstraint{result}; + } + else + { + fprintf(stderr, "Refine: could not find type for def '%s'\n", refine.toRefine->versionedName().c_str()); + LUAU_ASSERT(false); + } + }, + [&](const Join& join) + { + auto existingTy = defTypes.find(join.definition.get()); + // We resolved a type for this join instruction on the first pass so there is nothing to do here + if (existingTy && !get(*existingTy)) + return; + + // At this point, we know either existingType is nil (first pass) or existing type is a blocked type + std::vector operands; + bool missingOp = false; + for (const auto& op : join.operands) + { + Definition* resolved = g->resolve(op); + if (auto opTy = defTypes.find(resolved)) + operands.emplace_back(*opTy); + else + { + missingOp = true; + break; + } + } + + if (missingOp) + { + defTypes[join.definition.get()] = arena->addType(BlockedType{}); + return; + } + + auto finalizeType = [&]() + { + auto ub = UnionBuilder{arena, builtinTypes}; + ub.reserve(operands.size()); + for (auto& opTy : operands) + ub.add(opTy); + return ub.build(); + }; + + auto result = finalizeType(); + + if (existingTy) + emplaceType(asMutable(*existingTy), result); + else + defTypes[join.definition.get()] = result; + + auto typeToConstrain = existingTy ? *existingTy : result; + typesRequiringConstraint[typeToConstrain] = SimplifyConstraint{typeToConstrain}; + } + }, + *id.get() + ); +} + +TypeId TypeStateMap::getDiscriminantOf(const Refine& refine) +{ + if (!refine.type.has_value()) + return refine.sense ? builtinTypes->truthyType : builtinTypes->falsyType; + + LUAU_ASSERT(refine.type.has_value()); + const std::string& name = *refine.type; + + TypeId discriminantTy = builtinTypes->neverType; + if (name == "nil") + discriminantTy = builtinTypes->nilType; + else if (name == "string") + discriminantTy = builtinTypes->stringType; + else if (name == "number") + discriminantTy = builtinTypes->numberType; + else if (name == "integer") + discriminantTy = builtinTypes->integerType; + else if (name == "boolean") + discriminantTy = builtinTypes->booleanType; + else if (name == "thread") + discriminantTy = builtinTypes->threadType; + else if (name == "buffer") + discriminantTy = builtinTypes->bufferType; + else if (name == "table") + discriminantTy = builtinTypes->tableType; + else if (name == "function") + discriminantTy = builtinTypes->functionType; + else if (name == "userdata") + { + // typeof("userdata") collapses to the extern-type root; the precise + // class hierarchy is irrelevant for the purposes of refinement. + discriminantTy = builtinTypes->externType; + } + else if (auto typeFun = globalScope->lookupType(name); typeFun && typeFun->typeParams.empty() && typeFun->typePackParams.empty()) + { + TypeId ty = follow(typeFun->type); + + // Only accept the root of an extern-type chain (or anything tagged as a + // typeof root). Anything else stays `never` and produces an empty + // refinement. + if (auto etv = get(ty); etv && (etv->parent == builtinTypes->externType || hasTag(ty, kTypeofRootTag))) + discriminantTy = ty; + } + + // sense=false flips the proposition: the branch is taken when the def is + // *not* of `discriminantTy`, so the discriminant we feed to refine<...> is + // the negation. + if (!refine.sense) + { + if (auto nt = get(discriminantTy)) + discriminantTy = nt->ty; + else + discriminantTy = arena->addType(NegationType{discriminantTy}); + } + + + return discriminantTy; +} + +} // namespace Luau::CFG diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index f497ea5e..485e2eb3 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -13,10 +13,6 @@ #include -LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauContainsAnyGenericDoesntTraverseIntoExtern) -LUAU_FASTFLAG(LuauAnalysisUsesSolverMode) - namespace Luau { @@ -139,25 +135,8 @@ std::optional findTablePropertyRespectingMeta( const auto& fit = itt->props.find(name); if (fit != itt->props.end()) { - // This is only used in the old solver? - if (FFlag::LuauAnalysisUsesSolverMode) - { - if (useNewSolver) - { - switch (context) - { - case ValueContext::RValue: - return fit->second.readTy; - case ValueContext::LValue: - return fit->second.writeTy; - } - } - else - { - return fit->second.readTy; - } - } - else if (FFlag::LuauSolverV2) + + if (useNewSolver) { switch (context) { @@ -168,7 +147,9 @@ std::optional findTablePropertyRespectingMeta( } } else - return fit->second.type_DEPRECATED(); + { + return fit->second.readTy; + } } } else if (const auto& itf = get(index)) @@ -527,7 +508,7 @@ class BlockedTypeInLiteralVisitor : public AstVisitor NotNull> toBlock_; }; -std::vector findBlockedArgTypesIn(AstExprCall* expr, NotNull> astTypes) +std::vector findBlockedArgTypesIn_DEPRECATED(AstExprCall* expr, NotNull> astTypes) { std::vector toBlock; BlockedTypeInLiteralVisitor v{astTypes, NotNull{&toBlock}}; @@ -581,88 +562,214 @@ bool fastIsSubtype(TypeId subTy, TypeId superTy) return r == Relation::Coincident || r == Relation::Superset; } -std::optional extractMatchingTableType(std::vector& tables, TypeId exprType, NotNull builtinTypes) +/** + * There is a tension with how we encode tables and how we _want_ them to be + * typechecked. The classic example is: + * + * local tbl: { x: number? } = { x = 42 } + * + * Obviously, this _should_ work, but the type checker would tell us correctly + * that `{ x: number } extractMatchingTableType_DEPRECATED(const UnionType* expectedUnion, TypeId exprType, NotNull builtinTypes) { - if (tables.empty()) - return std::nullopt; - const TableType* exprTable = get(follow(exprType)); if (!exprTable) return std::nullopt; - size_t tableCount = 0; - std::optional firstTable; - - for (TypeId ty : tables) + // Try to filter out tables based on property names, for example + // if we are considering the type ... + // + // { foo: number, bar: string } | { foo: number, baz: boolean } + // + // ... and the table in question looks like ... + // + // { baz = true } + // + // ... the user probably intends the second definition. + TypeIds potentialTables; + + for (TypeId ty : expectedUnion) { - ty = follow(ty); if (auto tt = get(ty)) { - // If the expected table has a key whose type is a string or boolean - // singleton and the corresponding exprType property does not match, - // then skip this table. - - if (!firstTable) - firstTable = ty; - ++tableCount; - + bool isDisjoint = false; + // NOTE: We iterate over the expected properties for structural subtyping reasons, + // consider: + // + // local t: { foo: number? } = { + // foo = 42, + // -- 10,000 properties not shown. + // } + // + // Those 10k properties do not matter here. for (const auto& [name, expectedProp] : tt->props) { + // If the property from the expected type is not in the + // expression, skip it. + auto propInTableExpr = exprTable->props.find(name); + if (propInTableExpr == exprTable->props.end()) + continue; + + // Also, if the expected type does not have a read component, skip this. if (!expectedProp.readTy) continue; - const TypeId expectedType = follow(*expectedProp.readTy); + const auto& [_, exprProp] = *propInTableExpr; - auto st = get(expectedType); - if (!st) + // If the expression property doesn't have a read type, then + // we cannot reasonably check this against the read type of + // the expected property. + if (!exprProp.readTy) + { + // Also assert here: we should never encounter an inferred + // write-only type from an expression. + LUAU_ASSERT(!"Unexpected write-only property inside table literal."); continue; + } - auto it = exprTable->props.find(name); - if (it == exprTable->props.end()) - continue; + const TypeId expectedPropType = follow(*expectedProp.readTy); + const TypeId exprPropType = follow(*exprProp.readTy); - const auto& [_name, exprProp] = *it; + if (relate(expectedPropType, exprPropType) == Relation::Disjoint) + { + isDisjoint = true; + break; + } - if (!exprProp.readTy) + auto ft = get(exprPropType); + if (ft && relate(ft->lowerBound, expectedPropType) == Relation::Disjoint) + { + isDisjoint = true; + break; + } + } + + if (!isDisjoint) + potentialTables.insert(ty); + } + } + + if (potentialTables.size() == 1) + return {*potentialTables.begin()}; + + return std::nullopt; +} + +std::optional extractMatchingTableType( + const UnionType* expectedUnion, + TypeId exprType, + NotNull builtinTypes, + NotNull arena +) +{ + const TableType* exprTable = get(follow(exprType)); + if (!exprTable) + return std::nullopt; + + // Try to filter out tables based on property names, for example + // if we are considering the type ... + // + // { foo: number, bar: string } | { foo: number, baz: boolean } + // + // ... and the table in question looks like ... + // + // { baz = true } + // + // ... the user probably intends the second definition. + TypeIds potentialTables; + + for (TypeId ty : expectedUnion) + { + // NOTE: This probably should just be replaced with normalization. + if (auto itv = get(ty)) + { + TypeIds parts; + parts.insert(begin(itv), end(itv)); + ty = simplifyIntersection(builtinTypes, arena, std::move(parts)).result; + } + + if (auto tt = get(ty)) + { + bool isDisjoint = false; + // NOTE: We iterate over the expected properties for structural subtyping reasons, + // consider: + // + // local t: { foo: number? } = { + // foo = 42, + // -- 10,000 properties not shown. + // } + // + // Those 10k properties do not matter here. + for (const auto& [name, expectedProp] : tt->props) + { + // If the property from the expected type is not in the + // expression, skip it. + auto propInTableExpr = exprTable->props.find(name); + if (propInTableExpr == exprTable->props.end()) continue; - const TypeId propType = follow(*exprProp.readTy); + // Also, if the expected type does not have a read component, skip this. + if (!expectedProp.readTy) + continue; - const FreeType* ft = get(propType); + const auto& [_, exprProp] = *propInTableExpr; - if (ft && get(ft->lowerBound)) + // If the expression property doesn't have a read type, then + // we cannot reasonably check this against the read type of + // the expected property. + if (!exprProp.readTy) { - if (fastIsSubtype(builtinTypes->booleanType, ft->upperBound) && fastIsSubtype(expectedType, builtinTypes->booleanType)) - { - return ty; - } + // Also assert here: we should never encounter an inferred + // write-only type from an expression. + LUAU_ASSERT(!"Unexpected write-only property inside table literal."); + continue; + } - if (fastIsSubtype(builtinTypes->stringType, ft->upperBound) && fastIsSubtype(expectedType, ft->lowerBound)) - { - return ty; - } + const TypeId expectedPropType = follow(*expectedProp.readTy); + const TypeId exprPropType = follow(*exprProp.readTy); + + if (relate(expectedPropType, exprPropType) == Relation::Disjoint) + { + isDisjoint = true; + break; } - if (fastIsSubtype(propType, expectedType)) - return ty; + auto ft = get(exprPropType); + if (ft && relate(ft->lowerBound, expectedPropType) == Relation::Disjoint) + { + isDisjoint = true; + break; + } } + + if (!isDisjoint) + potentialTables.insert(ty); } } - if (tableCount == 1) - { - LUAU_ASSERT(firstTable); - return firstTable; - } + if (potentialTables.size() == 1) + return {*potentialTables.begin()}; return std::nullopt; } bool isRecord(const AstExprTable::Item& item) { - if (item.kind == AstExprTable::Item::Record) + if (item.kind == AstExprTable::Item::Kind::Record) return true; - else if (item.kind == AstExprTable::Item::General && item.key->is()) + else if (item.kind == AstExprTable::Item::Kind::General && item.key->is()) return true; else return false; @@ -854,38 +961,38 @@ TypeId addUnion(NotNull arena, NotNull builtinTypes, st return ub.build(); } -ContainsAnyGeneric::ContainsAnyGeneric() +ContainsAnyGeneric_DEPRECATED::ContainsAnyGeneric_DEPRECATED() : TypeOnceVisitor("ContainsAnyGeneric", /* skipBoundTypes */ true) { } -bool ContainsAnyGeneric::visit(TypeId ty, const ExternType&) +bool ContainsAnyGeneric_DEPRECATED::visit(TypeId ty, const ExternType&) { - return !FFlag::LuauContainsAnyGenericDoesntTraverseIntoExtern; + return false; } -bool ContainsAnyGeneric::visit(TypeId ty) +bool ContainsAnyGeneric_DEPRECATED::visit(TypeId ty) { found = found || is(ty); return !found; } -bool ContainsAnyGeneric::visit(TypePackId ty) +bool ContainsAnyGeneric_DEPRECATED::visit(TypePackId ty) { found = found || is(follow(ty)); return !found; } -bool ContainsAnyGeneric::hasAnyGeneric(TypeId ty) +bool ContainsAnyGeneric_DEPRECATED::hasAnyGeneric(TypeId ty) { - ContainsAnyGeneric cg; + ContainsAnyGeneric_DEPRECATED cg; cg.traverse(ty); return cg.found; } -bool ContainsAnyGeneric::hasAnyGeneric(TypePackId tp) +bool ContainsAnyGeneric_DEPRECATED::hasAnyGeneric(TypePackId tp) { - ContainsAnyGeneric cg; + ContainsAnyGeneric_DEPRECATED cg; cg.traverse(tp); return cg.found; } @@ -949,5 +1056,55 @@ bool isBlocked(TypeId ty) return is(ty); } +std::optional getApproximateReturnTypeForFunctionCall(TypeId ty, DenseHashSet& seen) +{ + ty = follow(ty); + if (seen.contains(ty)) + return std::nullopt; + + seen.insert(ty); + + if (auto ftv = get(ty)) + return {ftv->retTypes}; + + if (auto utv = get(ty); utv && begin(utv) != end(utv)) + return getApproximateReturnTypeForFunctionCall(*begin(utv), seen); + + return std::nullopt; +} + +std::optional getApproximateReturnTypeForFunctionCall(TypeId ty) +{ + DenseHashSet seen{nullptr}; + return getApproximateReturnTypeForFunctionCall(ty, seen); +} + +OccursCheckResult occursCheck(TypePackId needle, TypePackId haystack) +{ + needle = follow(needle); + haystack = follow(haystack); + + LUAU_ASSERT((is(needle))); + + if (is(needle)) + return OccursCheckResult::Pass; + + while (!get(haystack)) + { + if (needle == haystack) + return OccursCheckResult::Fail; + + if (auto a = get(haystack); a && a->tail) + { + haystack = follow(*a->tail); + continue; + } + + break; + } + + return OccursCheckResult::Pass; +} + } // namespace Luau diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index 6f37f1ef..51ce41a9 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -15,14 +15,9 @@ #include LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) -LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping) LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping) -LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauFixIndexerSubtypingOrdering) -LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) -LUAU_FASTFLAGVARIABLE(LuauUnifierRecursionOnRestart) -LUAU_FASTFLAGVARIABLE(LuauUnifierDoesntReferToNewSolver) namespace Luau { @@ -1497,10 +1492,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal auto mkFreshType = [this](Scope* scope, TypeLevel level) { - if (FFlag::LuauSolverV2 || FFlag::LuauUnifierDoesntReferToNewSolver) - return freshType(NotNull{types}, builtinTypes, scope); - else - return types->freshType(builtinTypes, scope, level); + return freshType(NotNull{types}, builtinTypes, scope); }; const TypePackId emptyTp = types->addTypePack(TypePack{{}, std::nullopt}); @@ -1972,18 +1964,7 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection, // If one of the types stopped being a table altogether, we need to restart from the top if ((superTy != superTyNew || activeSubTy != subTyNew) && errors.empty()) - { - if (FFlag::LuauUnifierRecursionOnRestart) - { - RecursionLimiter _ra("Unifier::tryUnifyTables", &sharedState.counters.recursionCount, sharedState.counters.recursionLimit); - tryUnify(subTy, superTy, false, isIntersection); - return; - } - else - { - return tryUnify(subTy, superTy, false, isIntersection); - } - } + return tryUnify(subTy, superTy, false, isIntersection); // Otherwise, restart only the table unification TableType* newSuperTable = log.getMutable(superTyNew); @@ -2062,18 +2043,7 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection, // If one of the types stopped being a table altogether, we need to restart from the top if ((superTy != superTyNew || activeSubTy != subTyNew) && errors.empty()) - { - if (FFlag::LuauUnifierRecursionOnRestart) - { - RecursionLimiter _ra("Unifier::tryUnifyTables", &sharedState.counters.recursionCount, sharedState.counters.recursionLimit); - tryUnify(subTy, superTy, false, isIntersection); - return; - } - else - { - return tryUnify(subTy, superTy, false, isIntersection); - } - } + return tryUnify(subTy, superTy, false, isIntersection); // Recursive unification can change the txn log, and invalidate the old // table. If we detect that this has happened, we start over, with the updated @@ -2196,8 +2166,7 @@ void Unifier::tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed) auto fail = [&](std::optional e) { - std::string reason = FFlag::LuauBetterTypeMismatchErrors ? "The given type's metatable does not satisfy the requirements." - : "The former's metatable does not satisfy the requirements."; + std::string reason = "The given type's metatable does not satisfy the requirements."; if (e) reportError(location, TypeMismatch{osuperTy, osubTy, std::move(reason), std::move(e), mismatchContext()}); else diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 9a39f669..aac58fb3 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -3,6 +3,7 @@ #include "Luau/Unifier2.h" #include "Luau/Instantiation.h" +#include "Luau/Instantiation2.h" #include "Luau/Scope.h" #include "Luau/Simplify.h" #include "Luau/Type.h" @@ -23,7 +24,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_DYNAMIC_FASTINTVARIABLE(LuauUnifierRecursionLimit, 100) LUAU_FASTFLAGVARIABLE(LuauLimitUnificationRecursion) -LUAU_FASTFLAGVARIABLE(LuauUnifier2HandleMismatchedPacks) +LUAU_FASTFLAG(LuauHigherOrderGenericInference) namespace Luau { @@ -199,7 +200,7 @@ UnifyResult Unifier2::unify_(TypeId subTy, TypeId superTy) if (superFree) { - superFree->lowerBound = mkUnion(superFree->lowerBound, subTy); + superFree->lowerBound = mkUnion(superFree->lowerBound, instantiateWithBoundTypes(subTy)); } if (subFree) @@ -236,6 +237,7 @@ UnifyResult Unifier2::unify_(TypeId subTy, TypeId superTy) else if (subNever && superFn) { // If `never` is the subtype, then we can propagate that inward. + UnifyResult argResult = unify_(superFn->argTypes, builtinTypes->neverTypePack); UnifyResult retResult = unify_(builtinTypes->neverTypePack, superFn->retTypes); return argResult & retResult; @@ -298,6 +300,18 @@ UnifyResult Unifier2::unify_(TypeId subTy, TypeId superTy) return UnifyResult::Ok; } +template TypeId Unifier2::instantiateWithBoundTypes(TypeId ty); +template TypePackId Unifier2::instantiateWithBoundTypes(TypePackId ty); + +template +TID Unifier2::instantiateWithBoundTypes(TID ty) +{ + Replacer r{arena, NotNull{&genericSubstitutions}, NotNull{&genericPackSubstitutions}}; + if (auto newTy = r.substitute(ty)) + return *newTy; + return ty; +} + // If superTy is a function and subTy already has a // potentially-compatible function in its upper bound, we assume that // the function is not overloaded and attempt to combine superTy into @@ -309,8 +323,9 @@ UnifyResult Unifier2::unifyFreeWithType(TypeId subTy, TypeId superTy) auto doDefault = [&]() { - subFree->upperBound = mkIntersection(subFree->upperBound, superTy); - expandedFreeTypes[subTy].push_back(superTy); + auto newSuperTy = instantiateWithBoundTypes(superTy); + subFree->upperBound = mkIntersection(subFree->upperBound, newSuperTy); + expandedFreeTypes[subTy].push_back(newSuperTy); return UnifyResult::Ok; }; @@ -319,6 +334,36 @@ UnifyResult Unifier2::unifyFreeWithType(TypeId subTy, TypeId superTy) if (get(upperBound)) return unify_(subFree->upperBound, superTy); + // When superTy is a union or intersection, propagate subTy as a lower bound into any + // free-type members. Without this, `freeA <: 'T | nil` (or `freeA <: 'T & C`) never + // constrains 'T, because the FreeType path intercepts before structural dispatch. + // Members may be GenericTypes that map to FreeTypes via genericSubstitutions. + auto propagateToFreeMembers = [&](auto memberRange) + { + for (TypeId member : memberRange) + { + TypeId m = follow(member); + if (auto subst = genericSubstitutions.find(m)) + m = follow(*subst); + if (FreeType* memberFree = getMutable(m)) + { + memberFree->lowerBound = mkUnion(memberFree->lowerBound, instantiateWithBoundTypes(subTy)); + } + } + }; + + if (const UnionType* superUnion = get(superTy)) + { + propagateToFreeMembers(superUnion->options); + return doDefault(); + } + + if (const IntersectionType* superIntersection = get(superTy)) + { + propagateToFreeMembers(superIntersection->parts); + return doDefault(); + } + const FunctionType* superFunction = get(superTy); if (!superFunction) return doDefault(); @@ -364,9 +409,11 @@ UnifyResult Unifier2::unify_(TypeId subTy, const FunctionType* superFn) if (shouldInstantiate) { + for (TypeId generic : subFn->generics) { - const GenericType* gen = get(follow(generic)); + generic = follow(generic); + const GenericType* gen = get(generic); if (gen) genericSubstitutions[generic] = freshType(scope, gen->polarity); } @@ -492,7 +539,6 @@ UnifyResult Unifier2::unify_(TableType* subTable, const TableType* superTable) superTypePackParamsIter != superTable->instantiatedTypePackParams.end()) { result &= unify_(*subTypePackParamsIter, *superTypePackParamsIter); - subTypePackParamsIter++; superTypePackParamsIter++; } @@ -616,8 +662,6 @@ UnifyResult Unifier2::unify_(const AnyType*, const MetatableType* superMetatable return unify_(builtinTypes->anyType, superMetatable->table); } -// FIXME? This should probably return an ErrorVec or an optional -// rather than a boolean to signal an occurs check failure. UnifyResult Unifier2::unify_(TypePackId subTp, TypePackId superTp) { if (FInt::LuauTypeInferIterationLimit > 0 && iterationCount >= FInt::LuauTypeInferIterationLimit) @@ -639,12 +683,6 @@ UnifyResult Unifier2::unify_(TypePackId subTp, TypePackId superTp) subTp = follow(subTp); superTp = follow(superTp); - if (auto subGen = genericPackSubstitutions.find(subTp)) - return unify_(*subGen, superTp); - - if (auto superGen = genericPackSubstitutions.find(superTp)) - return unify_(subTp, *superGen); - if (seenTypePackPairings.contains({subTp, superTp})) return UnifyResult::Ok; seenTypePackPairings.insert({subTp, superTp}); @@ -652,229 +690,225 @@ UnifyResult Unifier2::unify_(TypePackId subTp, TypePackId superTp) if (subTp == superTp) return UnifyResult::Ok; - if (isIrresolvable(subTp) || isIrresolvable(superTp)) + auto emplaceFreeTypePack = [this](TypePackId target, TypePackId boundTo) { - if (uninhabitedTypeFunctions && (uninhabitedTypeFunctions->contains(subTp) || uninhabitedTypeFunctions->contains(superTp))) - return UnifyResult::Ok; + LUAU_ASSERT(is(target)); - incompleteSubtypes.emplace_back(PackSubtypeConstraint{subTp, superTp}); - return UnifyResult::Ok; - } + boundTo = instantiateWithBoundTypes(boundTo); - const FreeTypePack* subFree = get(subTp); - const FreeTypePack* superFree = get(superTp); - - if (subFree) - { - DenseHashSet seen{nullptr}; - if (OccursCheckResult::Fail == occursCheck(seen, subTp, superTp)) + if (occursCheck(target, boundTo) == OccursCheckResult::Fail) { - emplaceTypePack(asMutable(subTp), builtinTypes->errorTypePack); + emplaceTypePack(asMutable(target), builtinTypes->errorTypePack); return UnifyResult::OccursCheckFailed; } - - emplaceTypePack(asMutable(subTp), superTp); + emplaceTypePack(asMutable(target), boundTo); return UnifyResult::Ok; - } + }; - if (superFree) + // FIXME: CLI-188000: If we are _directly_ given a free type, we must + // eagerly emplace it. Otherwise, later, we may generalize the underlying + // free types incorrectly. + if (is(subTp)) + return emplaceFreeTypePack(subTp, superTp); + + if (is(superTp)) + return emplaceFreeTypePack(superTp, subTp); + + /* If the passed iterator points at the head of a type pack, return that. If + * not, allocate a fresh type pack starting at the position of the iterator. + */ + auto makeTail = [this](TypePackIterator iter, TypePackIterator endIter) { - DenseHashSet seen{nullptr}; - if (OccursCheckResult::Fail == occursCheck(seen, superTp, subTp)) + std::optional newSuper = iter.tryGetHead(); + if (newSuper) + return *newSuper; + + std::vector newHead; + while (iter != endIter) { - emplaceTypePack(asMutable(superTp), builtinTypes->errorTypePack); - return UnifyResult::OccursCheckFailed; + newHead.push_back(*iter); + ++iter; } - emplaceTypePack(asMutable(superTp), subTp); - return UnifyResult::Ok; - } + return arena->addTypePack(std::move(newHead), iter.tail()); + }; - size_t maxLength = std::max(flatten(subTp).first.size(), flatten(superTp).first.size()); + /* If either type pack is blocked, record a constraint so that the solver + * can get back to it later. Else unify. + */ + auto deferOrUnify = [this](TypePackId subTp, TypePackId superTp) + { + if (isIrresolvable(subTp) || isIrresolvable(superTp)) + { + if (uninhabitedTypeFunctions != nullptr && (uninhabitedTypeFunctions->contains(subTp) || uninhabitedTypeFunctions->contains(superTp))) + return UnifyResult::Ok; - auto [subTypes, subTail] = extendTypePack(*arena, builtinTypes, subTp, maxLength); - auto [superTypes, superTail] = extendTypePack(*arena, builtinTypes, superTp, maxLength); + incompleteSubtypes.emplace_back(PackSubtypeConstraint{subTp, superTp}); + return UnifyResult::Ok; + } + else + return unify_(subTp, superTp); + }; - // right-pad the subpack with nils if `superPack` is larger since that's what a function call does - if (subTypes.size() < maxLength) - subTypes.resize(maxLength, builtinTypes->nilType); + auto maybeReplaceTail = [this](std::optional maybeTp) + { + if (!maybeTp) + return builtinTypes->emptyTypePack; - if (FFlag::LuauUnifier2HandleMismatchedPacks) + auto tp = follow(*maybeTp); + if (auto replacement = genericPackSubstitutions.find(tp)) + return follow(*replacement); + return tp; + }; + + if (FFlag::LuauHigherOrderGenericInference) { - for (size_t i = 0; i < std::min(subTypes.size(), superTypes.size()); ++i) - unify_(subTypes[i], superTypes[i]); + auto subIter = begin(subTp); + const auto subEnd = end(subTp); + auto superIter = begin(superTp); + const auto superEnd = end(superTp); + + while (subIter != subEnd && superIter != superEnd) + { + unify_(*subIter, *superIter); + ++subIter; + ++superIter; + } - if (subTypes.size() < maxLength && subTail) + // If we have hit the end of one OR the other iter, and if that ended + // iter points at a variadic pack, expand it out. Note that, if both + // packs have variadic tails, we do not expand. + if (subIter == subEnd && superIter != superEnd && subIter.tail()) { - TypePackId superTypesSlice = arena->addTypePack( - TypePack{ - std::vector(superTypes.begin() + subTypes.size(), superTypes.end()), - superTail, + if (auto vtp = get(follow(*subIter.tail()))) + { + while (superIter != superEnd) + { + unify_(vtp->ty, *superIter); + ++superIter; } - ); - return unify_(*subTail, superTypesSlice); + } } - else if (superTypes.size() < maxLength && superTail) + if (superIter == superEnd && subIter != subEnd && superIter.tail()) { - TypePackId subTypesSlice = arena->addTypePack( - TypePack{ - std::vector(subTypes.begin() + superTypes.size(), subTypes.end()), - subTail, + if (auto vtp = get(follow(*superIter.tail()))) + { + while (subIter != subEnd) + { + unify_(*subIter, vtp->ty); + ++subIter; } - ); - return unify_(subTypesSlice, *superTail); + } } - // These assertions are meant to ensure we haven't missed a case. - LUAU_ASSERT( - // If the heads are evenly matched, then we just check the tails. - subTypes.size() == superTypes.size() || - // If neither type has a tail, alls good. - (!subTail && !superTail) || - // If the sub pack has a tail, more types in its head, and the - // super pack has no tail, alls good. - (subTail && !superTail && subTypes.size() > superTypes.size()) || - // ... and the other way 'round for the super pack. - (!subTail && superTail && subTypes.size() < superTypes.size()) - ); - if (subTail && superTail) - return unify_(*subTail, *superTail); - else if (subTail) - return unify_(*subTail, builtinTypes->emptyTypePack); - else if (superTail) - return unify(builtinTypes->emptyTypePack, *superTail); - - return UnifyResult::Ok; - } - else - { - if (subTypes.size() < maxLength || superTypes.size() < maxLength) - return UnifyResult::Ok; - - for (size_t i = 0; i < maxLength; ++i) - unify_(subTypes[i], superTypes[i]); - if (subTail && superTail) + if (subIter == subEnd && superIter == superEnd) { - TypePackId followedSubTail = follow(*subTail); - TypePackId followedSuperTail = follow(*superTail); + auto subTail = subIter.tail(); + auto superTail = superIter.tail(); - if (get(followedSubTail) || get(followedSuperTail)) - return unify_(followedSubTail, followedSuperTail); + if (!subTail && !superTail) + return UnifyResult::Ok; + + return deferOrUnify(maybeReplaceTail(subTail), maybeReplaceTail(superTail)); } - else if (subTail) + else if (subIter == subEnd) { - TypePackId followedSubTail = follow(*subTail); - if (get(followedSubTail)) - emplaceTypePack(asMutable(followedSubTail), builtinTypes->emptyTypePack); + LUAU_ASSERT(superIter != superEnd); + TypePackId newSub = maybeReplaceTail(subIter.tail()); + TypePackId newSuper = makeTail(superIter, superEnd); + + return deferOrUnify(newSub, newSuper); } - else if (superTail) + else if (superIter == superEnd) { - TypePackId followedSuperTail = follow(*superTail); - if (get(followedSuperTail)) - emplaceTypePack(asMutable(followedSuperTail), builtinTypes->emptyTypePack); + LUAU_ASSERT(subIter != subEnd); + TypePackId newSub = makeTail(subIter, subEnd); + TypePackId newSuper = maybeReplaceTail(superIter.tail()); + return deferOrUnify(newSub, newSuper); } + LUAU_ASSERT(!"Unreachable"); return UnifyResult::Ok; } -} -TypeId Unifier2::mkUnion(TypeId left, TypeId right) -{ - left = follow(left); - right = follow(right); - - return simplifyUnion(builtinTypes, arena, left, right).result; -} - -TypeId Unifier2::mkIntersection(TypeId left, TypeId right) -{ - left = follow(left); - right = follow(right); + size_t maxLength = std::max(std::distance(begin(subTp), end(subTp)), std::distance(begin(superTp), end(superTp))); - return simplifyIntersection(builtinTypes, arena, left, right).result; -} + auto [subTypes, subTail] = extendTypePack(*arena, builtinTypes, subTp, maxLength); + auto [superTypes, superTail] = extendTypePack(*arena, builtinTypes, superTp, maxLength); -OccursCheckResult Unifier2::occursCheck(DenseHashSet& seen, TypeId needle, TypeId haystack) -{ - RecursionLimiter _ra("Unifier2::occursCheck", &recursionCount, recursionLimit); + auto limit = std::min(subTypes.size(), superTypes.size()); + for (size_t i = 0; i < limit; ++i) + unify_(subTypes[i], superTypes[i]); - OccursCheckResult occurrence = OccursCheckResult::Pass; + // At this point it should be the case that either: + // - `subTypes` now has all of its types unified, and we are down to its tail + // - `superTypes` now has all of its types unified, and we are down to its tail - auto check = [&](TypeId ty) + if (!subTail && !superTail) { - if (occursCheck(seen, needle, ty) == OccursCheckResult::Fail) - occurrence = OccursCheckResult::Fail; - }; - - needle = follow(needle); - haystack = follow(haystack); - - if (seen.find(haystack)) - return OccursCheckResult::Pass; - - seen.insert(haystack); - - if (get(needle)) - return OccursCheckResult::Pass; - - if (!get(needle)) - ice->ice("Expected needle to be free"); - - if (needle == haystack) - return OccursCheckResult::Fail; + // If both types are missing a tail, we've done all we can. + return UnifyResult::Ok; + } - if (auto haystackFree = get(haystack)) + // It should be the case that exclusively one of these packs can be reduced + // to their tail for the rest of the function. + if (limit < subTypes.size()) { - check(haystackFree->lowerBound); - check(haystackFree->upperBound); + LUAU_ASSERT(limit == superTypes.size()); + // If we have extra subtypes left over, construct a new type pack + std::vector newSubHead{subTypes.begin() + superTypes.size(), subTypes.end()}; + subTp = arena->addTypePack(TypePack{std::move(newSubHead), subTail}); + superTp = maybeReplaceTail(superTail); } - else if (auto ut = get(haystack)) + else if (limit < superTypes.size()) { - for (TypeId ty : ut->options) - check(ty); + LUAU_ASSERT(limit == subTypes.size() && limit < superTypes.size()); + // If we have extra subtypes left over, construct a new type pack + std::vector newSuperHead{superTypes.begin() + subTypes.size(), superTypes.end()}; + superTp = arena->addTypePack(TypePack{std::move(newSuperHead), superTail}); + subTp = maybeReplaceTail(subTail); } - else if (auto it = get(haystack)) + else { - for (TypeId ty : it->parts) - check(ty); + subTp = maybeReplaceTail(subTail); + superTp = maybeReplaceTail(superTail); } - return occurrence; -} - -OccursCheckResult Unifier2::occursCheck(DenseHashSet& seen, TypePackId needle, TypePackId haystack) -{ - needle = follow(needle); - haystack = follow(haystack); + if (isIrresolvable(subTp) || isIrresolvable(superTp)) + { + if (uninhabitedTypeFunctions != nullptr && (uninhabitedTypeFunctions->contains(subTp) || uninhabitedTypeFunctions->contains(superTp))) + return UnifyResult::Ok; - if (seen.find(haystack)) - return OccursCheckResult::Pass; + incompleteSubtypes.emplace_back(PackSubtypeConstraint{subTp, superTp}); + return UnifyResult::Ok; + } - seen.insert(haystack); + // ... after doing all of our replacements, we may also need to check for + // free types again. - if (getMutable(needle)) - return OccursCheckResult::Pass; + if (is(subTp)) + return emplaceFreeTypePack(subTp, superTp); - if (!getMutable(needle)) - ice->ice("Expected needle pack to be free"); + if (is(superTp)) + return emplaceFreeTypePack(superTp, subTp); - RecursionLimiter _ra("Unifier2::occursCheck", &recursionCount, recursionLimit); + return UnifyResult::Ok; +} - while (!getMutable(haystack)) - { - if (needle == haystack) - return OccursCheckResult::Fail; +TypeId Unifier2::mkUnion(TypeId left, TypeId right) +{ + left = follow(left); + right = follow(right); - if (auto a = get(haystack); a && a->tail) - { - haystack = follow(*a->tail); - continue; - } + return simplifyUnion(builtinTypes, arena, left, right).result; +} - break; - } +TypeId Unifier2::mkIntersection(TypeId left, TypeId right) +{ + left = follow(left); + right = follow(right); - return OccursCheckResult::Pass; + return simplifyIntersection(builtinTypes, arena, left, right).result; } TypeId Unifier2::freshType(NotNull scope, Polarity polarity) diff --git a/Analysis/src/UserDefinedTypeFunction.cpp b/Analysis/src/UserDefinedTypeFunction.cpp index 7501a7d5..8003ebe2 100644 --- a/Analysis/src/UserDefinedTypeFunction.cpp +++ b/Analysis/src/UserDefinedTypeFunction.cpp @@ -6,6 +6,7 @@ #include "Luau/Normalize.h" #include "Luau/StringUtils.h" #include "Luau/TimeTrace.h" +#include "Luau/TypeFunctionError.h" #include "Luau/UserDefinedTypeFunction.h" #include "Luau/VisitType.h" @@ -13,6 +14,7 @@ #include "lualib.h" LUAU_FASTFLAG(LuauTypeFunctionSupportsFrozen) +LUAU_FASTFLAG(LuauTypeFunctionStructuredErrors) namespace Luau { @@ -110,7 +112,7 @@ static int evaluateTypeAliasCall(lua_State* L) TypeFunctionTypeId tfty = getTypeUserData(L, i + 1); TypeId ty = deserialize(tfty, runtimeBuilder); - if (!runtimeBuilder->errors.empty()) + if (FFlag::LuauTypeFunctionStructuredErrors ? !runtimeBuilder->errors.empty() : !runtimeBuilder->errors_DEPRECATED.empty()) luaL_error(L, "failed to deserialize type at argument %d", i + 1); rawTypeArguments.push_back(ty); @@ -177,15 +179,26 @@ static int evaluateTypeAliasCall(lua_State* L) TypeFunctionTypeId serializedTy = serialize(follow(target), runtimeBuilder); + if (FFlag::LuauTypeFunctionStructuredErrors) + { + if (!runtimeBuilder->errors.empty()) + luaL_error(L, "%s", toString(runtimeBuilder->errors.front()).c_str()); + } + else + { + if (!runtimeBuilder->errors_DEPRECATED.empty()) + luaL_error(L, "%s", runtimeBuilder->errors_DEPRECATED.front().c_str()); + } + + if (!serializedTy) + luaL_error(L, "Complexity limit reached when passing a type to a type alias"); + if (FFlag::LuauTypeFunctionSupportsFrozen) { FreezeTypeFunctionTypes freezer{}; freezer.run(serializedTy); } - if (!runtimeBuilder->errors.empty()) - luaL_error(L, "%s", runtimeBuilder->errors.front().c_str()); - allocTypeUserData(L, serializedTy->type, /* frozen */ true); return 1; } @@ -198,6 +211,7 @@ TypeFunctionReductionResult userDefinedTypeFunction( ) { auto typeFunction = getMutable(instance); + LUAU_ASSERT(typeFunction); if (typeFunction->userFuncData.owner.expired()) { @@ -237,7 +251,10 @@ TypeFunctionReductionResult userDefinedTypeFunction( if (definition.first->hasErrors) return {ctx->builtins->errorType, Reduction::MaybeOk, {}, {}}; - if (std::optional error = ctx->typeFunctionRuntime->registerFunction(definition.first)) + bool registrationFailed = FFlag::LuauTypeFunctionStructuredErrors + ? ctx->typeFunctionRuntime->registerFunction(definition.first).has_value() + : ctx->typeFunctionRuntime->registerFunction_DEPRECATED(definition.first).has_value(); + if (registrationFailed) { // Failure to register at this point means that original definition had to error out and should not have been present in the // environment @@ -312,15 +329,16 @@ TypeFunctionReductionResult userDefinedTypeFunction( TypeFunctionTypeId serializedTy = serialize(ty, runtimeBuilder.get()); - if (FFlag::LuauTypeFunctionSupportsFrozen) - { - FreezeTypeFunctionTypes freezer{}; - freezer.run(serializedTy); - } - // Only register aliases that are representable in type environment - if (runtimeBuilder->errors.empty()) + if (serializedTy && + (FFlag::LuauTypeFunctionStructuredErrors ? runtimeBuilder->errors.empty() : runtimeBuilder->errors_DEPRECATED.empty())) { + if (FFlag::LuauTypeFunctionSupportsFrozen) + { + FreezeTypeFunctionTypes freezer{}; + freezer.run(serializedTy); + } + allocTypeUserData(L, serializedTy->type, /* frozen */ true); lua_setfield(L, -2, name.c_str()); } @@ -358,9 +376,21 @@ TypeFunctionReductionResult userDefinedTypeFunction( LUAU_ASSERT(!isPending(ty, ctx->solver)); TypeFunctionTypeId serializedTy = serialize(ty, runtimeBuilder.get()); + // Check if there were any errors while serializing - if (runtimeBuilder->errors.size() != 0) - return {std::nullopt, Reduction::Erroneous, {}, {}, runtimeBuilder->errors.front()}; + if (FFlag::LuauTypeFunctionStructuredErrors) + { + if (!runtimeBuilder->errors.empty()) + return {std::nullopt, Reduction::Erroneous, {}, {}, toString(runtimeBuilder->errors.front())}; + } + else + { + if (runtimeBuilder->errors_DEPRECATED.size() != 0) + return {std::nullopt, Reduction::Erroneous, {}, {}, runtimeBuilder->errors_DEPRECATED.front()}; + } + + if (!serializedTy) + return {std::nullopt, Reduction::Erroneous, {}, {}, "Complexity limit reached when passing a type to a type function"}; allocTypeUserData(L, serializedTy->type); } @@ -378,8 +408,16 @@ TypeFunctionReductionResult userDefinedTypeFunction( ctx->typeFunctionRuntime->messages.clear(); - if (auto error = checkResultForError(L, name.value, lua_pcall(L, int(typeParams.size()), 1, 0))) - return {std::nullopt, Reduction::Erroneous, {}, {}, std::move(error), ctx->typeFunctionRuntime->messages}; + if (FFlag::LuauTypeFunctionStructuredErrors) + { + if (auto error = checkResultForError(L, name.value, lua_pcall(L, int(typeParams.size()), 1, 0))) + return {std::nullopt, Reduction::Erroneous, {}, {}, toString(*error), ctx->typeFunctionRuntime->messages}; + } + else + { + if (auto error = checkResultForError_DEPRECATED(L, name.value, lua_pcall(L, int(typeParams.size()), 1, 0))) + return {std::nullopt, Reduction::Erroneous, {}, {}, std::move(error), ctx->typeFunctionRuntime->messages}; + } // If the return value is not a type userdata, return with error message if (!isTypeUserData(L, 1)) @@ -396,16 +434,32 @@ TypeFunctionReductionResult userDefinedTypeFunction( TypeFunctionTypeId retTypeFunctionTypeId = getTypeUserData(L, 1); - // No errors should be present here since we should've returned already if any were raised during serialization. - LUAU_ASSERT(runtimeBuilder->errors.size() == 0); + if (FFlag::LuauTypeFunctionStructuredErrors) + { + // No errors should be present here since we should've returned already if any were raised during serialization. + LUAU_ASSERT(runtimeBuilder->errors.empty()); + + TypeId retTypeId = deserialize(retTypeFunctionTypeId, runtimeBuilder.get()); + + // At least 1 error occurred while deserializing + if (!runtimeBuilder->errors.empty()) + return {std::nullopt, Reduction::Erroneous, {}, {}, toString(runtimeBuilder->errors.front()), ctx->typeFunctionRuntime->messages}; - TypeId retTypeId = deserialize(retTypeFunctionTypeId, runtimeBuilder.get()); + return {retTypeId, Reduction::MaybeOk, {}, {}, std::nullopt, ctx->typeFunctionRuntime->messages}; + } + else + { + // No errors should be present here since we should've returned already if any were raised during serialization. + LUAU_ASSERT(runtimeBuilder->errors_DEPRECATED.size() == 0); - // At least 1 error occurred while deserializing - if (runtimeBuilder->errors.size() > 0) - return {std::nullopt, Reduction::Erroneous, {}, {}, runtimeBuilder->errors.front(), ctx->typeFunctionRuntime->messages}; + TypeId retTypeId = deserialize(retTypeFunctionTypeId, runtimeBuilder.get()); - return {retTypeId, Reduction::MaybeOk, {}, {}, std::nullopt, ctx->typeFunctionRuntime->messages}; + // At least 1 error occurred while deserializing + if (runtimeBuilder->errors_DEPRECATED.size() > 0) + return {std::nullopt, Reduction::Erroneous, {}, {}, runtimeBuilder->errors_DEPRECATED.front(), ctx->typeFunctionRuntime->messages}; + + return {retTypeId, Reduction::MaybeOk, {}, {}, std::nullopt, ctx->typeFunctionRuntime->messages}; + } } } // namespace Luau diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index 65481857..d858f9c0 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -1,7 +1,9 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/Common.h" #include "Luau/Location.h" +#include "Luau/Variant.h" #include #include @@ -11,6 +13,8 @@ #include #include +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) + namespace Luau { @@ -70,15 +74,27 @@ struct AstLocal AstLocal* shadow; size_t functionDepth; size_t loopDepth; + bool isConst; + // exported is only a property set after construction + bool isExported = false; AstType* annotation; - AstLocal(const AstName& name, const Location& location, AstLocal* shadow, size_t functionDepth, size_t loopDepth, AstType* annotation) + AstLocal( + const AstName& name, + const Location& location, + AstLocal* shadow, + size_t functionDepth, + size_t loopDepth, + AstType* annotation, + bool isConst = false + ) : name(name) , location(location) , shadow(shadow) , functionDepth(functionDepth) , loopDepth(loopDepth) + , isConst(isConst) , annotation(annotation) { } @@ -197,11 +213,12 @@ class AstAttr : public AstNode public: LUAU_RTTI(AstAttr) - enum Type + enum class Type { Checked, Native, Deprecated, + DebugNoinline, Unknown }; @@ -327,6 +344,7 @@ enum class ConstantNumberParseResult Malformed, BinOverflow, HexOverflow, + IntOverflow, }; class AstExprConstantNumber : public AstExpr @@ -342,12 +360,24 @@ class AstExprConstantNumber : public AstExpr ConstantNumberParseResult parseResult; }; +class AstExprConstantInteger : public AstExpr +{ +public: + LUAU_RTTI(AstExprConstantInteger) + + AstExprConstantInteger(const Location& location, int64_t value, ConstantNumberParseResult parseResult = ConstantNumberParseResult::Ok); + + void visit(AstVisitor* visitor) override; + + int64_t value; + ConstantNumberParseResult parseResult; +}; class AstExprConstantString : public AstExpr { public: LUAU_RTTI(AstExprConstantString) - enum QuoteStyle + enum class QuoteStyle { // A string created using double quotes or an interpolated string, // as in: @@ -533,7 +563,7 @@ class AstExprTable : public AstExpr struct Item { - enum Kind + enum class Kind { List, // foo, in which case key is a nullptr Record, // foo=bar, in which case key is a AstExprConstantString @@ -560,7 +590,7 @@ class AstExprUnary : public AstExpr public: LUAU_RTTI(AstExprUnary) - enum Op + enum class Op { Not, Minus, @@ -666,7 +696,7 @@ class AstExprInstantiate : public AstExpr public: LUAU_RTTI(AstExprInstantiate) - AstExprInstantiate(const Location& location, AstExpr* expr, AstArray typePack); + AstExprInstantiate(const Location& location, AstExpr* expr, AstArray types); void visit(AstVisitor* visitor) override; @@ -808,7 +838,8 @@ class AstStatLocal : public AstStat const Location& location, const AstArray& vars, const AstArray& values, - const std::optional& equalsSignLocation + const std::optional& equalsSignLocation, + bool isConst = false ); void visit(AstVisitor* visitor) override; @@ -816,6 +847,11 @@ class AstStatLocal : public AstStat AstArray vars; AstArray values; + bool isConst = false; + bool isExported = false; + + // if the StatLocal is being exported, this is the location of `const` or `local` + std::optional keywordLocation; std::optional equalsSignLocation; }; @@ -921,12 +957,15 @@ class AstStatLocalFunction : public AstStat public: LUAU_RTTI(AstStatLocalFunction) - AstStatLocalFunction(const Location& location, AstLocal* name, AstExprFunction* func); + AstStatLocalFunction(const Location& location, AstLocal* name, AstExprFunction* func, bool isConst, Position constKeywordBegin); void visit(AstVisitor* visitor) override; AstLocal* name; AstExprFunction* func; + bool isConst; + // Position of the `const` keyword; Position::missing() when isConst is false. + Position constKeywordBegin; }; class AstStatTypeAlias : public AstStat @@ -1041,6 +1080,13 @@ class AstStatDeclareFunction : public AstStat AstTypePack* retTypes; }; +enum class AstTableAccess +{ + Read = 0b01, + Write = 0b10, + ReadWrite = 0b11, +}; + struct AstDeclaredExternTypeProperty { AstName name; @@ -1048,13 +1094,42 @@ struct AstDeclaredExternTypeProperty AstType* ty = nullptr; bool isMethod = false; Location location; + AstTableAccess access = AstTableAccess::ReadWrite; }; -enum class AstTableAccess +struct AstClassProperty { - Read = 0b01, - Write = 0b10, - ReadWrite = 0b11, + Location qualifierLocation; + AstName name; + Location nameLocation; + std::optional typeColonLocation = std::nullopt; + AstType* ty = nullptr; +}; + +struct AstClassMethod +{ + std::optional qualifierLocation; + Location keywordLocation; + AstName functionName; + Location nameLocation; + AstExprFunction* function; +}; + +using AstClassMember = Variant; + +class AstStatClass : public AstStat +{ +public: + LUAU_RTTI(AstStatClass) + + AstLocal* name; + AstExpr* super; + AstArray members; + bool exported; + + AstStatClass(const Location& location, AstLocal* name, AstExpr* super, AstArray members, bool exported); + + void visit(AstVisitor* visitor) override; }; struct AstTableIndexer @@ -1115,7 +1190,8 @@ class AstTypeReference : public AstType std::optional prefixLocation, const Location& nameLocation, bool hasParameterList = false, - const AstArray& parameters = {} + const AstArray& parameters = {}, + AstLocal* prefixLocal = nullptr ); void visit(AstVisitor* visitor) override; @@ -1123,6 +1199,7 @@ class AstTypeReference : public AstType bool hasParameterList; std::optional prefix; std::optional prefixLocation; + AstLocal* prefixLocal = nullptr; AstName name; Location nameLocation; AstArray parameters; @@ -1402,6 +1479,10 @@ class AstVisitor { return visit(static_cast(node)); } + virtual bool visit(class AstExprConstantInteger* node) + { + return visit(static_cast(node)); + } virtual bool visit(class AstExprConstantString* node) { return visit(static_cast(node)); @@ -1548,6 +1629,11 @@ class AstVisitor { return visit(static_cast(node)); } + virtual bool visit(class AstStatClass* node) + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + return visit(static_cast(node)); + } virtual bool visit(class AstStatDeclareExternType* node) { return visit(static_cast(node)); diff --git a/Ast/include/Luau/Cst.h b/Ast/include/Luau/Cst.h index d5ba16e1..e1cfd200 100644 --- a/Ast/include/Luau/Cst.h +++ b/Ast/include/Luau/Cst.h @@ -51,6 +51,49 @@ class CstNode const int classIndex; }; +class CstAttr : public CstNode +{ +public: + LUAU_CST_RTTI(CstAttr) + + explicit CstAttr(bool hasAt); + + bool hasAt; // false when inside an attribute list, ie @[native checked] +}; + +class CstParametrizedAttr : public CstNode +{ +public: + LUAU_CST_RTTI(CstParametrizedAttr) + + explicit CstParametrizedAttr(Position openParenPosition, Position closeParenPosition, AstArray argsCommaPositions); + + Position openParenPosition; // for `@x(args)` form + Position closeParenPosition; + + // Commas inside the `(a, b, c)` arg list + AstArray argsCommaPositions; +}; + +struct CstAttrList +{ + explicit CstAttrList(Position atBracketPosition, Position closeBracketPosition, AstArray commaPositions); + + Position atBracketPosition; + Position closeBracketPosition; + AstArray commaPositions; +}; + +class CstExprGroup : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprGroup) + + explicit CstExprGroup(Position closePosition); + + Position closePosition; +}; + class CstExprConstantNumber : public CstNode { public: @@ -61,12 +104,22 @@ class CstExprConstantNumber : public CstNode AstArray value; }; +class CstExprConstantInteger : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprConstantInteger) + + explicit CstExprConstantInteger(const AstArray& value); + + AstArray value; +}; + class CstExprConstantString : public CstNode { public: - LUAU_CST_RTTI(CstExprConstantNumber) + LUAU_CST_RTTI(CstExprConstantString) - enum QuoteStyle + enum class QuoteStyle { QuotedSingle, QuotedDouble, @@ -84,13 +137,13 @@ class CstExprConstantString : public CstNode // Shared between the expression and call nodes struct CstTypeInstantiation { - Position leftArrow1Position = {0, 0}; - Position leftArrow2Position = {0, 0}; + Position leftArrow1Position = Position::missing(); + Position leftArrow2Position = Position::missing(); AstArray commaPositions = {}; - Position rightArrow1Position = {0, 0}; - Position rightArrow2Position = {0, 0}; + Position rightArrow1Position = Position::missing(); + Position rightArrow2Position = Position::missing(); }; class CstExprCall : public CstNode @@ -98,10 +151,10 @@ class CstExprCall : public CstNode public: LUAU_CST_RTTI(CstExprCall) - CstExprCall(std::optional openParens, std::optional closeParens, AstArray commaPositions); + CstExprCall(Position openParens, Position closeParens, AstArray commaPositions); - std::optional openParens; - std::optional closeParens; + Position openParens; + Position closeParens; AstArray commaPositions; CstTypeInstantiation* explicitTypes = nullptr; }; @@ -124,14 +177,15 @@ class CstExprFunction : public CstNode CstExprFunction(); - Position functionKeywordPosition{0, 0}; - Position openGenericsPosition{0, 0}; + AstArray attrLists = {}; + Position functionKeywordPosition = Position::missing(); + Position openGenericsPosition = Position::missing(); AstArray genericsCommaPositions; - Position closeGenericsPosition{0, 0}; + Position closeGenericsPosition = Position::missing(); AstArray argsAnnotationColonPositions; AstArray argsCommaPositions; - Position varargAnnotationColonPosition{0, 0}; - Position returnSpecifierPosition{0, 0}; + Position varargAnnotationColonPosition = Position::missing(); + Position returnSpecifierPosition = Position::missing(); }; class CstExprTable : public CstNode @@ -139,19 +193,20 @@ class CstExprTable : public CstNode public: LUAU_CST_RTTI(CstExprTable) - enum Separator + enum class Separator { Comma, Semicolon, + Missing }; struct Item { - std::optional indexerOpenPosition; // '[', only if Kind == General - std::optional indexerClosePosition; // ']', only if Kind == General - std::optional equalsPosition; // only if Kind != List - std::optional separator; // may be missing for last Item - std::optional separatorPosition; + Position indexerOpenPosition; // '[', only if Kind == General + Position indexerClosePosition; // ']', only if Kind == General + Position equalsPosition; // only if Kind != List + Separator separator; // may be missing for last Item + Position separatorPosition; }; explicit CstExprTable(const AstArray& items); @@ -224,17 +279,6 @@ class CstStatDo : public CstNode Position endPosition; }; -// Clip with FFlag::LuauCstStatBlock -class CstStatDo_DEPRECATED : public CstNode -{ -public: - LUAU_CST_RTTI(CstStatDo_DEPRECATED) - - explicit CstStatDo_DEPRECATED(Position endPosition); - - Position endPosition; -}; - class CstStatRepeat : public CstNode { public: @@ -272,12 +316,12 @@ class CstStatFor : public CstNode public: LUAU_CST_RTTI(CstStatFor) - CstStatFor(Position annotationColonPosition, Position equalsPosition, Position endCommaPosition, std::optional stepCommaPosition); + CstStatFor(Position annotationColonPosition, Position equalsPosition, Position endCommaPosition, Position stepCommaPosition); Position annotationColonPosition; Position equalsPosition; Position endCommaPosition; - std::optional stepCommaPosition; + Position stepCommaPosition; }; class CstStatForIn : public CstNode @@ -320,7 +364,9 @@ class CstStatFunction : public CstNode LUAU_CST_RTTI(CstStatFunction) explicit CstStatFunction(Position functionKeywordPosition); + explicit CstStatFunction(AstArray attrLists, Position functionKeywordPosition); + AstArray attrLists; Position functionKeywordPosition; }; @@ -330,7 +376,9 @@ class CstStatLocalFunction : public CstNode LUAU_CST_RTTI(CstStatLocalFunction) explicit CstStatLocalFunction(Position localKeywordPosition, Position functionKeywordPosition); + explicit CstStatLocalFunction(AstArray attrLists, Position localKeywordPosition, Position functionKeywordPosition); + AstArray attrLists; Position localKeywordPosition; Position functionKeywordPosition; }; @@ -340,9 +388,9 @@ class CstGenericType : public CstNode public: LUAU_CST_RTTI(CstGenericType) - CstGenericType(std::optional defaultEqualsPosition); + CstGenericType(Position defaultEqualsPosition); - std::optional defaultEqualsPosition; + Position defaultEqualsPosition; }; class CstGenericTypePack : public CstNode @@ -350,10 +398,10 @@ class CstGenericTypePack : public CstNode public: LUAU_CST_RTTI(CstGenericTypePack) - CstGenericTypePack(Position ellipsisPosition, std::optional defaultEqualsPosition); + CstGenericTypePack(Position ellipsisPosition, Position defaultEqualsPosition); Position ellipsisPosition; - std::optional defaultEqualsPosition; + Position defaultEqualsPosition; }; class CstStatTypeAlias : public CstNode @@ -393,13 +441,13 @@ class CstTypeReference : public CstNode LUAU_CST_RTTI(CstTypeReference) CstTypeReference( - std::optional prefixPointPosition, + Position prefixPointPosition, Position openParametersPosition, AstArray parametersCommaPositions, Position closeParametersPosition ); - std::optional prefixPointPosition; + Position prefixPointPosition; Position openParametersPosition; AstArray parametersCommaPositions; Position closeParametersPosition; @@ -412,7 +460,7 @@ class CstTypeTable : public CstNode struct Item { - enum struct Kind + enum class Kind { Indexer, Property, @@ -423,8 +471,8 @@ class CstTypeTable : public CstNode Position indexerOpenPosition; // '[', only if Kind != Property Position indexerClosePosition; // ']' only if Kind != Property Position colonPosition; - std::optional separator; // may be missing for last Item - std::optional separatorPosition; + CstExprTable::Separator separator; // may be missing for last Item + Position separatorPosition; CstExprConstantString* stringInfo = nullptr; // only if Kind == StringProperty Position stringPosition{0, 0}; // only if Kind == StringProperty @@ -446,7 +494,7 @@ class CstTypeFunction : public CstNode AstArray genericsCommaPositions, Position closeGenericsPosition, Position openArgsPosition, - AstArray> argumentNameColonPositions, + AstArray argumentNameColonPositions, AstArray argumentsCommaPositions, Position closeArgsPosition, Position returnArrowPosition @@ -456,7 +504,7 @@ class CstTypeFunction : public CstNode AstArray genericsCommaPositions; Position closeGenericsPosition; Position openArgsPosition; - AstArray> argumentNameColonPositions; + AstArray argumentNameColonPositions; AstArray argumentsCommaPositions; Position closeArgsPosition; Position returnArrowPosition; @@ -478,9 +526,9 @@ class CstTypeUnion : public CstNode public: LUAU_CST_RTTI(CstTypeUnion) - CstTypeUnion(std::optional leadingPosition, AstArray separatorPositions); + CstTypeUnion(Position leadingPosition, AstArray separatorPositions); - std::optional leadingPosition; + Position leadingPosition; AstArray separatorPositions; }; @@ -489,9 +537,9 @@ class CstTypeIntersection : public CstNode public: LUAU_CST_RTTI(CstTypeIntersection) - explicit CstTypeIntersection(std::optional leadingPosition, AstArray separatorPositions); + explicit CstTypeIntersection(Position leadingPosition, AstArray separatorPositions); - std::optional leadingPosition; + Position leadingPosition; AstArray separatorPositions; }; @@ -507,6 +555,16 @@ class CstTypeSingletonString : public CstNode unsigned int blockDepth; }; +class CstTypeGroup : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeGroup) + + CstTypeGroup(Position closePosition); + + Position closePosition; +}; + class CstTypePackExplicit : public CstNode { public: @@ -515,7 +573,6 @@ class CstTypePackExplicit : public CstNode explicit CstTypePackExplicit(); explicit CstTypePackExplicit(Position openParenthesesPosition, Position closeParenthesesPosition, AstArray commaPositions); - bool hasParentheses; Position openParenthesesPosition; Position closeParenthesesPosition; AstArray commaPositions; diff --git a/Ast/include/Luau/Location.h b/Ast/include/Luau/Location.h index 95d4c78a..31e90aef 100644 --- a/Ast/include/Luau/Location.h +++ b/Ast/include/Luau/Location.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 + namespace Luau { @@ -14,6 +16,11 @@ struct Position { } + static Position missing() + { + return {UINT_MAX, UINT_MAX}; + } + bool operator==(const Position& rhs) const { return this->column == rhs.column && this->line == rhs.line; @@ -47,6 +54,11 @@ struct Position } void shift(const Position& start, const Position& oldEnd, const Position& newEnd); + + bool hasValue() const + { + return line != UINT_MAX || column != UINT_MAX; + } }; struct Location diff --git a/Ast/include/Luau/Parser.h b/Ast/include/Luau/Parser.h index 010be4a8..8591de74 100644 --- a/Ast/include/Luau/Parser.h +++ b/Ast/include/Luau/Parser.h @@ -5,7 +5,6 @@ #include "Luau/Lexer.h" #include "Luau/ParseOptions.h" #include "Luau/ParseResult.h" -#include "Luau/StringUtils.h" #include "Luau/DenseHash.h" #include "Luau/Common.h" #include "Luau/Cst.h" @@ -146,7 +145,7 @@ class Parser AstExpr* parseFunctionName(bool& hasself, AstName& debugname); // function funcname funcbody - LUAU_FORCEINLINE AstStat* parseFunctionStat(const AstArray& attributes = {nullptr, 0}); + LUAU_FORCEINLINE AstStatFunction* parseFunctionStat(const AstArray& attributes, TempVector* cstAttrLists = nullptr); std::optional validateAttribute( Location loc, @@ -155,11 +154,20 @@ class Parser const AstArray& args ); - // attribute ::= '@' NAME + Location getAttributeStartLocation( + const AstArray& attributes, + const TempVector* cstAttrLists, + const Location& defaultLocation + ); + + // attrlist = '@[' parattr {',' parattr} ']' + void parseAttrList(TempVector& attributes, TempVector* cstAttrLists); + + // attribute ::= '@' NAME | attrlist void parseAttribute(TempVector& attribute); // attributes ::= {attribute} - AstArray parseAttributes(); + AstArray parseAttributes(TempVector* cstAttrLists = nullptr); // attributes local function Name funcbody // attributes function funcname funcbody @@ -169,7 +177,13 @@ class Parser // local function Name funcbody | // local namelist [`=' explist] - AstStat* parseLocal(const AstArray& attributes); + AstStat* parseLocal( + const Location start, + const Position keywordPosition, + const AstArray& attributes, + bool isConst, + TempVector* cstAttrLists = nullptr + ); // return [explist] AstStat* parseReturn(); @@ -177,6 +191,8 @@ class Parser // type Name `=' Type AstStat* parseTypeAlias(const Location& start, bool exported, Position typeKeywordPosition); + AstStat* parseClassStat(const Location& start, bool exported); + // type function Name ... end AstStat* parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition); @@ -191,6 +207,13 @@ class Parser // varlist `=' explist AstStat* parseAssignment(AstExpr* initial); + AstStat* parseExportValue( + const Location& start, + const Position keywordPosition, + const AstArray& attributes, + TempVector* cstAttrLists = nullptr + ); + // var [`+=' | `-=' | `*=' | `/=' | `%=' | `^=' | `..='] exp AstStat* parseCompoundAssignment(AstExpr* initial, AstExprBinary::Op op); @@ -203,14 +226,16 @@ class Parser const Lexeme& matchFunction, const AstName& debugname, const Name* localName, - const AstArray& attributes + const AstArray& attributes, + const bool isConst = false, + TempVector* cstAttrLists = nullptr ); // explist ::= {exp `,'} exp void parseExprList(TempVector& result, TempVector* commaPositions = nullptr); // binding ::= Name [`:` Type] - Binding parseBinding(); + Binding parseBinding(bool isConst = false); AstArray extractAnnotationColonPositions(const TempVector& bindings); // bindinglist ::= (binding | `...') {`,' bindinglist} @@ -220,7 +245,8 @@ class Parser bool allowDot3 = false, AstArray* commaPositions = nullptr, Position* initialCommaPosition = nullptr, - Position* varargAnnotationColonPosition = nullptr + Position* varargAnnotationColonPosition = nullptr, + bool isConst = false ); AstType* parseOptionalType(); @@ -241,7 +267,7 @@ class Parser TempVector& result, TempVector>& resultNames, TempVector* commaPositions = nullptr, - TempVector>* nameColonPositions = nullptr + TempVector* nameColonPositions = nullptr ); AstTypePack* parseOptionalReturnType(Position* returnSpecifierPosition = nullptr); @@ -303,6 +329,7 @@ class Parser // primaryexp -> prefixexp { `.' NAME | `[' exp `]' | TypeInstantiation | `:' NAME [TypeInstantiation] funcargs | funcargs } AstExpr* parsePrimaryExpr(bool asStatement); + AstExpr* parseIndexExpr(Position start, AstExpr* expr); AstExpr* parseMethodCall(Position start, AstExpr* expr); // asexp -> simpleexp [`::' Type] @@ -311,11 +338,13 @@ class Parser // simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | [attributes] FUNCTION body | primaryexp AstExpr* parseSimpleExpr(); - std::tuple, Location, Location> parseCallList(TempVector* commaPositions); + AstExpr* parseAttributedFunction(const Location& start); + + std::tuple, Location, Location> parseCallList(TempVector* commaPositions, Position* closeParenPosition = nullptr); // args ::= `(' [explist] `)' | tableconstructor | String AstExpr* parseFunctionArgs(AstExpr* func, bool self); - std::optional tableSeparator(); + CstExprTable::Separator tableSeparator(); // tableconstructor ::= `{' [fieldlist] `}' // fieldlist ::= field {fieldsep field} [fieldsep] @@ -334,6 +363,8 @@ class Parser AstExpr* parseExplicitTypeInstantiationExpr(Position start, AstExpr& basedOnExpr); + AstExpr* parseClassRefExpr(); + // Name std::optional parseNameOpt(const char* context = nullptr); Name parseName(const char* context = nullptr); @@ -418,6 +449,9 @@ class Parser ... ) LUAU_PRINTF_ATTR(5, 6); AstExprError* reportExprError(const Location& location, const AstArray& expressions, const char* format, ...) LUAU_PRINTF_ATTR(4, 5); + AstStatClass* getMatchingClass(AstExpr* expr); + bool isExprLValue(AstExpr* expr); + AstExprError* reportLValueError(AstExpr* expr); AstTypeError* reportTypeError(const Location& location, const AstArray& types, const char* format, ...) LUAU_PRINTF_ATTR(4, 5); // `parseErrorLocation` is associated with the parser error // `astErrorLocation` is associated with the AstTypeError created @@ -476,11 +510,13 @@ class Parser Name name; AstType* annotation; Position colonPosition; + bool isConst; - explicit Binding(const Name& name, AstType* annotation = nullptr, Position colonPosition = {0, 0}) + explicit Binding(const Name& name, AstType* annotation = nullptr, Position colonPosition = {0, 0}, bool isConst = false) : name(name) , annotation(annotation) , colonPosition(colonPosition) + , isConst(isConst) { } }; @@ -509,11 +545,15 @@ class Parser DenseHashMap localMap; std::vector localStack; + DenseHashMap classesWithinModule{{}}; std::vector parseErrors; std::vector matchRecoveryStopOnToken; + DenseHashMap declaredExportBindings; + bool hasModuleReturn = false; + std::vector scratchAttr; std::vector scratchStat; std::vector> scratchString; @@ -529,6 +569,7 @@ class Parser std::vector scratchType; std::vector scratchTypeOrPack; std::vector scratchDeclaredClassProps; + std::vector scratchClassDeclarations; std::vector scratchItem; std::vector scratchCstItem; std::vector scratchArgName; @@ -536,7 +577,8 @@ class Parser std::vector scratchGenericTypePacks; std::vector> scratchOptArgName; std::vector scratchPosition; - std::vector> scratchOptPosition; + std::vector scratchPosition2; + std::vector scratchCstAttrList; std::string scratchData; CstNodeMap cstNodeMap; diff --git a/Ast/include/Luau/PrettyPrinter.h b/Ast/include/Luau/PrettyPrinter.h index 6d69bb59..423c61e1 100644 --- a/Ast/include/Luau/PrettyPrinter.h +++ b/Ast/include/Luau/PrettyPrinter.h @@ -23,11 +23,12 @@ std::string toString(AstNode* node); void dump(AstNode* node); // Never fails on a well-formed AST -std::string prettyPrint(AstStatBlock& ast); +std::string prettyPrint(AstStatBlock& block); +std::string prettyPrint(AstStatBlock& block, const CstNodeMap& cstNodeMap); std::string prettyPrintWithTypes(AstStatBlock& block); std::string prettyPrintWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap); -// Only fails when parsing fails -PrettyPrintResult prettyPrint(std::string_view source, ParseOptions options = ParseOptions{}, bool withTypes = false); +// Only fails when parsing fails and we're not ignoring parse errors. +PrettyPrintResult prettyPrint(std::string_view source, ParseOptions options = ParseOptions{}, bool withTypes = false, bool ignoreParseErrors = false); } // namespace Luau diff --git a/Ast/src/Ast.cpp b/Ast/src/Ast.cpp index 9760cdc7..367e010b 100644 --- a/Ast/src/Ast.cpp +++ b/Ast/src/Ast.cpp @@ -4,8 +4,6 @@ #include "Luau/Common.h" -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSyntax) - namespace Luau { @@ -36,8 +34,6 @@ static void visitTypeList(AstVisitor* visitor, const AstTypeList& list) static void visitTypeOrPackArray(AstVisitor* visitor, const AstArray& arrayOfTypeOrPack) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); - for (const AstTypeOrPack& param : arrayOfTypeOrPack) { if (param.type) @@ -175,6 +171,18 @@ void AstExprConstantNumber::visit(AstVisitor* visitor) visitor->visit(this); } +AstExprConstantInteger::AstExprConstantInteger(const Location& location, int64_t value, ConstantNumberParseResult parseResult) + : AstExpr(ClassIndex(), location) + , value(value) + , parseResult(parseResult) +{ +} + +void AstExprConstantInteger::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + AstExprConstantString::AstExprConstantString(const Location& location, const AstArray& value, QuoteStyle quoteStyle) : AstExpr(ClassIndex(), location) , value(value) @@ -240,7 +248,6 @@ AstExprCall::AstExprCall( , self(self) , argLocation(argLocation) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax || explicitTypes.size == 0); } void AstExprCall::visit(AstVisitor* visitor) @@ -416,11 +423,11 @@ std::string toString(AstExprUnary::Op op) { switch (op) { - case AstExprUnary::Minus: + case AstExprUnary::Op::Minus: return "-"; - case AstExprUnary::Not: + case AstExprUnary::Op::Not: return "not"; - case AstExprUnary::Len: + case AstExprUnary::Op::Len: return "#"; default: LUAU_ASSERT(false); @@ -551,13 +558,10 @@ AstExprInstantiate::AstExprInstantiate(const Location& location, AstExpr* expr, , expr(expr) , typeArguments(types) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); } void AstExprInstantiate::visit(AstVisitor* visitor) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); - if (visitor->visit(this)) { expr->visit(visitor); @@ -706,11 +710,13 @@ AstStatLocal::AstStatLocal( const Location& location, const AstArray& vars, const AstArray& values, - const std::optional& equalsSignLocation + const std::optional& equalsSignLocation, + bool isConst ) : AstStat(ClassIndex(), location) , vars(vars) , values(values) + , isConst(isConst) , equalsSignLocation(equalsSignLocation) { } @@ -858,10 +864,12 @@ void AstStatFunction::visit(AstVisitor* visitor) } } -AstStatLocalFunction::AstStatLocalFunction(const Location& location, AstLocal* name, AstExprFunction* func) +AstStatLocalFunction::AstStatLocalFunction(const Location& location, AstLocal* name, AstExprFunction* func, bool isConst, Position constKeywordBegin) : AstStat(ClassIndex(), location) , name(name) , func(func) + , isConst(isConst) + , constKeywordBegin(constKeywordBegin) { } @@ -971,6 +979,44 @@ AstStatDeclareFunction::AstStatDeclareFunction( { } +AstStatClass::AstStatClass(const Location& location, AstLocal* name, AstExpr* super, AstArray members, bool exported) + : AstStat(ClassIndex(), location) + , name(name) + , super(super) + , members(members) + , exported(exported) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); +} + +void AstStatClass::visit(AstVisitor* visitor) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + if (visitor->visit(this)) + { + if (super) + super->visit(visitor); + + for (const auto& member : members) + { + Luau::visit( + overloaded{ + [&](const AstClassProperty& prop) + { + if (prop.ty) + prop.ty->visit(visitor); + }, + [&](const AstClassMethod& method) + { + method.function->visit(visitor); + } + }, + member + ); + } + } +} + AstStatDeclareFunction::AstStatDeclareFunction( const Location& location, const AstArray& attributes, @@ -1084,12 +1130,14 @@ AstTypeReference::AstTypeReference( std::optional prefixLocation, const Location& nameLocation, bool hasParameterList, - const AstArray& parameters + const AstArray& parameters, + AstLocal* prefixLocal ) : AstType(ClassIndex(), location) , hasParameterList(hasParameterList) , prefix(prefix) , prefixLocation(prefixLocation) + , prefixLocal(prefixLocal) , name(name) , nameLocation(nameLocation) , parameters(parameters) @@ -1100,20 +1148,7 @@ void AstTypeReference::visit(AstVisitor* visitor) { if (visitor->visit(this)) { - if (FFlag::LuauExplicitTypeInstantiationSyntax) - { - visitTypeOrPackArray(visitor, parameters); - } - else - { - for (const AstTypeOrPack& param : parameters) - { - if (param.type) - param.type->visit(visitor); - else - param.typePack->visit(visitor); - } - } + visitTypeOrPackArray(visitor, parameters); } } diff --git a/Ast/src/Cst.cpp b/Ast/src/Cst.cpp index f425735c..51ae6b2a 100644 --- a/Ast/src/Cst.cpp +++ b/Ast/src/Cst.cpp @@ -3,13 +3,38 @@ #include "Luau/Cst.h" #include "Luau/Common.h" -LUAU_FASTFLAG(LuauCstStatDoWithStatsStart) - namespace Luau { int gCstRttiIndex = 0; +CstAttr::CstAttr(bool hasAt) + : CstNode(CstClassIndex()) + , hasAt(hasAt) +{ +} + +CstParametrizedAttr::CstParametrizedAttr(Position openParenPosition, Position closeParenPosition, AstArray argsCommaPositions) + : CstNode(CstClassIndex()) + , openParenPosition(openParenPosition) + , closeParenPosition(closeParenPosition) + , argsCommaPositions(argsCommaPositions) +{ +} + +CstAttrList::CstAttrList(Position atBracketPosition, Position closeBracketPosition, AstArray commaPositions) + : atBracketPosition(atBracketPosition) + , closeBracketPosition(closeBracketPosition) + , commaPositions(commaPositions) +{ +} + +CstExprGroup::CstExprGroup(Position closePosition) + : CstNode(CstClassIndex()) + , closePosition(closePosition) +{ +} + CstExprConstantNumber::CstExprConstantNumber(const AstArray& value) : CstNode(CstClassIndex()) , value(value) @@ -25,7 +50,13 @@ CstExprConstantString::CstExprConstantString(AstArray sourceString, QuoteS LUAU_ASSERT(blockDepth == 0 || quoteStyle == QuoteStyle::QuotedRaw); } -CstExprCall::CstExprCall(std::optional openParens, std::optional closeParens, AstArray commaPositions) +CstExprConstantInteger::CstExprConstantInteger(const AstArray& value) + : CstNode(CstClassIndex()) + , value(value) +{ +} + +CstExprCall::CstExprCall(Position openParens, Position closeParens, AstArray commaPositions) : CstNode(CstClassIndex()) , openParens(openParens) , closeParens(closeParens) @@ -89,14 +120,6 @@ CstStatDo::CstStatDo(Position statsStartPosition, Position endPosition) , statsStartPosition(statsStartPosition) , endPosition(endPosition) { - LUAU_ASSERT(FFlag::LuauCstStatDoWithStatsStart); -} - -CstStatDo_DEPRECATED::CstStatDo_DEPRECATED(Position endPosition) - : CstNode(CstClassIndex()) - , endPosition(endPosition) -{ - LUAU_ASSERT(!FFlag::LuauCstStatDoWithStatsStart); } CstStatRepeat::CstStatRepeat(Position untilPosition) @@ -123,12 +146,7 @@ CstStatLocal::CstStatLocal( { } -CstStatFor::CstStatFor( - Position annotationColonPosition, - Position equalsPosition, - Position endCommaPosition, - std::optional stepCommaPosition -) +CstStatFor::CstStatFor(Position annotationColonPosition, Position equalsPosition, Position endCommaPosition, Position stepCommaPosition) : CstNode(CstClassIndex()) , annotationColonPosition(annotationColonPosition) , equalsPosition(equalsPosition) @@ -165,24 +183,41 @@ CstStatCompoundAssign::CstStatCompoundAssign(Position opPosition) CstStatFunction::CstStatFunction(Position functionKeywordPosition) : CstNode(CstClassIndex()) + , attrLists({}) + , functionKeywordPosition(functionKeywordPosition) +{ +} + +CstStatFunction::CstStatFunction(AstArray attrLists, Position functionKeywordPosition) + : CstNode(CstClassIndex()) + , attrLists(attrLists) , functionKeywordPosition(functionKeywordPosition) { } CstStatLocalFunction::CstStatLocalFunction(Position localKeywordPosition, Position functionKeywordPosition) : CstNode(CstClassIndex()) + , attrLists({}) , localKeywordPosition(localKeywordPosition) , functionKeywordPosition(functionKeywordPosition) { } -CstGenericType::CstGenericType(std::optional defaultEqualsPosition) +CstStatLocalFunction::CstStatLocalFunction(AstArray attrLists, Position localKeywordPosition, Position functionKeywordPosition) + : CstNode(CstClassIndex()) + , attrLists(attrLists) + , localKeywordPosition(localKeywordPosition) + , functionKeywordPosition(functionKeywordPosition) +{ +} + +CstGenericType::CstGenericType(Position defaultEqualsPosition) : CstNode(CstClassIndex()) , defaultEqualsPosition(defaultEqualsPosition) { } -CstGenericTypePack::CstGenericTypePack(Position ellipsisPosition, std::optional defaultEqualsPosition) +CstGenericTypePack::CstGenericTypePack(Position ellipsisPosition, Position defaultEqualsPosition) : CstNode(CstClassIndex()) , ellipsisPosition(ellipsisPosition) , defaultEqualsPosition(defaultEqualsPosition) @@ -213,7 +248,7 @@ CstStatTypeFunction::CstStatTypeFunction(Position typeKeywordPosition, Position } CstTypeReference::CstTypeReference( - std::optional prefixPointPosition, + Position prefixPointPosition, Position openParametersPosition, AstArray parametersCommaPositions, Position closeParametersPosition @@ -238,7 +273,7 @@ CstTypeFunction::CstTypeFunction( AstArray genericsCommaPositions, Position closeGenericsPosition, Position openArgsPosition, - AstArray> argumentNameColonPositions, + AstArray argumentNameColonPositions, AstArray argumentsCommaPositions, Position closeArgsPosition, Position returnArrowPosition @@ -262,14 +297,14 @@ CstTypeTypeof::CstTypeTypeof(Position openPosition, Position closePosition) { } -CstTypeUnion::CstTypeUnion(std::optional leadingPosition, AstArray separatorPositions) +CstTypeUnion::CstTypeUnion(Position leadingPosition, AstArray separatorPositions) : CstNode(CstClassIndex()) , leadingPosition(leadingPosition) , separatorPositions(separatorPositions) { } -CstTypeIntersection::CstTypeIntersection(std::optional leadingPosition, AstArray separatorPositions) +CstTypeIntersection::CstTypeIntersection(Position leadingPosition, AstArray separatorPositions) : CstNode(CstClassIndex()) , leadingPosition(leadingPosition) , separatorPositions(separatorPositions) @@ -282,21 +317,25 @@ CstTypeSingletonString::CstTypeSingletonString(AstArray sourceString, CstE , quoteStyle(quoteStyle) , blockDepth(blockDepth) { - LUAU_ASSERT(quoteStyle != CstExprConstantString::QuotedInterp); + LUAU_ASSERT(quoteStyle != CstExprConstantString::QuoteStyle::QuotedInterp); +} + +CstTypeGroup::CstTypeGroup(Position closePosition) + : CstNode(CstClassIndex()) + , closePosition(closePosition) +{ } CstTypePackExplicit::CstTypePackExplicit() : CstNode(CstClassIndex()) - , hasParentheses(false) - , openParenthesesPosition(Position{0, 0}) - , closeParenthesesPosition(Position{0, 0}) + , openParenthesesPosition(Position::missing()) + , closeParenthesesPosition(Position::missing()) , commaPositions({}) { } CstTypePackExplicit::CstTypePackExplicit(Position openParenthesesPosition, Position closeParenthesesPosition, AstArray commaPositions) : CstNode(CstClassIndex()) - , hasParentheses(true) , openParenthesesPosition(openParenthesesPosition) , closeParenthesesPosition(closeParenthesesPosition) , commaPositions(commaPositions) diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 797b83ba..80a1d264 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.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/Parser.h" +#include "Luau/Ast.h" #include "Luau/Common.h" #include "Luau/TimeTrace.h" @@ -9,6 +10,7 @@ #include #include #include +#include LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) LUAU_FASTINTVARIABLE(LuauTypeLengthLimit, 1000) @@ -19,9 +21,17 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) // See docs/SyntaxChanges.md for an explanation. LUAU_FASTFLAGVARIABLE(LuauSolverV2) LUAU_DYNAMIC_FASTFLAGVARIABLE(DebugLuauReportReturnTypeVariadicWithTypeSuffix, false) -LUAU_FASTFLAGVARIABLE(LuauExplicitTypeInstantiationSyntax) -LUAU_FASTFLAGVARIABLE(LuauCstStatDoWithStatsStart) -LUAU_FASTFLAGVARIABLE(DesugaredArrayTypeReferenceIsEmpty) +LUAU_FASTFLAGVARIABLE(LuauIntegerType2) +LUAU_FASTFLAGVARIABLE(LuauExportValueSyntax) +LUAU_FLAGVERSION(LuauExportValueSyntax, 4) + +LUAU_FASTFLAGVARIABLE(DebugLuauNoInline) +LUAU_FASTFLAGVARIABLE(DebugLuauUserDefinedClasses) +LUAU_FASTFLAGVARIABLE(LuauAllowGlobalDeclarationToBeCalledClass) +LUAU_FASTFLAGVARIABLE(LuauDisallowExternClassInTypeDefinitions) +LUAU_FASTFLAGVARIABLE(LuauStoreConstKeywordBegin) +LUAU_FASTFLAGVARIABLE(LuauTrackPrefixLocal) +LUAU_FASTFLAGVARIABLE(LuauNoDuplicateBinaryPrefix) // Clip with DebugLuauReportReturnTypeVariadicWithTypeSuffix bool luau_telemetry_parsed_return_type_variadic_with_type_suffix = false; @@ -90,6 +100,10 @@ AttributeEntry kAttributeEntries[] = { {nullptr, AstAttr::Type::Checked, {}} }; +std::pair&> kDebugAttributeEntries[] = { + {{"debugnoinline", AstAttr::Type::DebugNoinline, {}}, FFlag::DebugLuauNoInline}, +}; + ParseError::ParseError(const Location& location, std::string message) : location(location) , message(std::move(message)) @@ -300,6 +314,7 @@ Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Alloc , recursionCounter(0) , endMismatchSuspect(Lexeme(Location(), Lexeme::Eof)) , localMap(AstName()) + , declaredExportBindings(AstName()) , cstNodeMap(nullptr) { Function top; @@ -441,7 +456,10 @@ AstStat* Parser::parseStat() case Lexeme::ReservedFunction: return parseFunctionStat(AstArray({nullptr, 0})); case Lexeme::ReservedLocal: - return parseLocal(AstArray({nullptr, 0})); + { + Location start = lexer.current().location; + return parseLocal(start, start.begin, {nullptr, 0}, false); + } case Lexeme::ReservedReturn: return parseReturn(); case Lexeme::ReservedBreak: @@ -474,16 +492,52 @@ AstStat* Parser::parseStat() if (ident == "type") return parseTypeAlias(expr->location, /* exported= */ false, expr->location.begin); - if (ident == "export" && lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "type") + if (FFlag::DebugLuauUserDefinedClasses && ident == "class") + return parseClassStat(start, /*exported*/ false); + + if (ident == "export") { - Position typeKeywordPosition = lexer.current().location.begin; - nextLexeme(); - return parseTypeAlias(expr->location, /* exported= */ true, typeKeywordPosition); + if (FFlag::LuauExportValueSyntax) + { + Lexeme current = lexer.current(); + + if (current.type == Lexeme::ReservedLocal || current.type == Lexeme::ReservedFunction || + (current.type == Lexeme::Name && AstName(current.name) == "const") || + ((FFlag::DebugLuauUserDefinedClasses && current.type == Lexeme::Name) && AstName(current.name) == "class")) + { + return parseExportValue(expr->location, expr->location.begin, AstArray({nullptr, 0})); + } + else if (current.type == Lexeme::Name && AstName(current.name) == "type") + { + Position typeKeywordPosition = current.location.begin; + nextLexeme(); + return parseTypeAlias(expr->location, /* exported= */ true, typeKeywordPosition); + } + } + // TODO: remove with LuauExportValueSyntax + else if (FFlag::DebugLuauUserDefinedClasses && AstName(lexer.current().name) == "class") + { + nextLexeme(); + return parseClassStat(start, /*exported*/ true); + } + else + { + if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "type") + { + Position typeKeywordPosition = lexer.current().location.begin; + nextLexeme(); + return parseTypeAlias(expr->location, /* exported= */ true, typeKeywordPosition); + } + } } + if (ident == "continue") return parseContinue(expr->location); + if (ident == "const") + return parseLocal(expr->location, expr->location.begin, AstArray({nullptr, 0}), true); + if (options.allowDeclarationSyntax) { if (ident == "declare") @@ -600,9 +654,9 @@ AstStat* Parser::parseRepeat() functionStack.back().loopDepth--; - Position untilPosition = lexer.current().location.begin; bool hasUntil = expectMatchEndAndConsume(Lexeme::ReservedUntil, matchRepeat); body->hasEnd = hasUntil; + Position untilPosition = hasUntil ? lexer.previousLocation().begin : Position::missing(); AstExpr* cond = parseExpr(); @@ -622,43 +676,21 @@ AstStat* Parser::parseDo() Lexeme matchDo = lexer.current(); nextLexeme(); // do - if (FFlag::LuauCstStatDoWithStatsStart) - { - std::optional statsStart = options.storeCstData ? std::optional{lexer.current().location} : std::nullopt; - - AstStatBlock* body = parseBlock(); - - body->location.begin = start.begin; - - Location endLocation = lexer.current().location; - body->hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); - if (body->hasEnd) - body->location.end = endLocation.end; + Position statsStart = lexer.current().location.begin; - if (options.storeCstData) - { - LUAU_ASSERT(statsStart); - cstNodeMap[body] = allocator.alloc(statsStart->begin, endLocation.begin); - } - - return body; - } - else - { - AstStatBlock* body = parseBlock(); + AstStatBlock* body = parseBlock(); - body->location.begin = start.begin; + body->location.begin = start.begin; - Location endLocation = lexer.current().location; - body->hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); - if (body->hasEnd) - body->location.end = endLocation.end; + Location endLocation = lexer.current().location; + body->hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); + if (body->hasEnd) + body->location.end = endLocation.end; - if (options.storeCstData) - cstNodeMap[body] = allocator.alloc(endLocation.begin); + if (options.storeCstData) + cstNodeMap[body] = allocator.alloc(statsStart, body->hasEnd ? endLocation.begin : Position::missing()); - return body; - } + return body; } // break @@ -702,12 +734,12 @@ AstStat* Parser::parseFor() AstExpr* from = parseExpr(); - Position endCommaPosition = lexer.current().location.begin; - expectAndConsume(',', "index range"); + bool hasEndComma = expectAndConsume(',', "index range"); + Position endCommaPosition = hasEndComma ? lexer.previousLocation().begin : Position::missing(); AstExpr* to = parseExpr(); - std::optional stepCommaPosition = std::nullopt; + Position stepCommaPosition = Position::missing(); AstExpr* step = nullptr; if (lexer.current().type == ',') @@ -854,13 +886,28 @@ AstExpr* Parser::parseFunctionName(bool& hasself, AstName& debugname) return expr; } -// function funcname funcbody -AstStat* Parser::parseFunctionStat(const AstArray& attributes) +AstStatClass* Parser::getMatchingClass(AstExpr* expr) { - Location start = lexer.current().location; + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + if (AstExprGlobal* g = expr->as()) + { + if (AstStatClass** classDecl = classesWithinModule.find(g->name)) + return *classDecl; + } + return nullptr; +} - if (attributes.size > 0) - start = attributes.data[0]->location; +bool Parser::isExprLValue(AstExpr* expr) +{ + return (expr->is() && !expr->as()->local->isConst) || + (expr->is() && !(FFlag::DebugLuauUserDefinedClasses && getMatchingClass(expr) != nullptr)) || + expr->is() || expr->is(); +} + +// function funcname funcbody +AstStatFunction* Parser::parseFunctionStat(const AstArray& attributes, TempVector* cstAttrLists) +{ + Location start = getAttributeStartLocation(attributes, cstAttrLists, lexer.current().location); Lexeme matchFunction = lexer.current(); nextLexeme(); @@ -869,6 +916,12 @@ AstStat* Parser::parseFunctionStat(const AstArray& attributes) AstName debugname; AstExpr* expr = parseFunctionName(hasself, debugname); + if (!isExprLValue(expr)) + { + expr = FFlag::LuauExportValueSyntax ? reportLValueError(expr) + : reportExprError(expr->location, copy({expr}), "Assigned expression must be a variable or a field"); + } + matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; AstExprFunction* body = parseFunctionBody(hasself, matchFunction, debugname, nullptr, attributes).first; @@ -877,7 +930,9 @@ AstStat* Parser::parseFunctionStat(const AstArray& attributes) AstStatFunction* node = allocator.alloc(Location(start, body->location), expr, body); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(matchFunction.location.begin); + cstNodeMap[node] = cstAttrLists ? allocator.alloc(copy(*cstAttrLists), matchFunction.location.begin) + : allocator.alloc(matchFunction.location.begin); + return node; } @@ -902,6 +957,16 @@ std::optional Parser::validateAttribute( } } + for (const auto& [attributeEntry, fflagBool] : kDebugAttributeEntries) + { + if (fflagBool && strcmp(attributeName, attributeEntry.name) == 0) + { + type = attributeEntry.type; + argsValidator = attributeEntry.argsValidator; + break; + } + } + if (!type) { if (strlen(attributeName) == 0) @@ -930,88 +995,127 @@ std::optional Parser::validateAttribute( return type; } -// attribute ::= '@' NAME -void Parser::parseAttribute(TempVector& attributes) +// attrlist = '@[' parattr {',' parattr} ']' +void Parser::parseAttrList(TempVector& attributes, TempVector* cstAttrLists) { - AstArray empty; + Lexeme open = lexer.current(); - LUAU_ASSERT(lexer.current().type == Lexeme::Type::Attribute || lexer.current().type == Lexeme::Type::AttributeOpen); + LUAU_ASSERT(open.type == Lexeme::Type::AttributeOpen); - if (lexer.current().type == Lexeme::Type::Attribute) - { - Location loc = lexer.current().location; - - const char* name = lexer.current().name; - std::optional type = validateAttribute(loc, name, attributes, empty); + nextLexeme(); - nextLexeme(); + AstArray empty; + TempVector commaPositions(scratchPosition); - attributes.push_back(allocator.alloc(loc, type.value_or(AstAttr::Type::Unknown), empty, AstName(name))); - } - else + if (lexer.current().type != ']') { - Lexeme open = lexer.current(); - nextLexeme(); - - if (lexer.current().type != ']') + while (true) { - while (true) + Name name = parseName("attribute name"); + + Location nameLoc = name.location; + const char* attrName = name.name.value; + + Lexeme argOpen = lexer.current(); + Lexeme::Type argOpenType = argOpen.type; + + if (argOpenType == Lexeme::RawString || argOpenType == Lexeme::QuotedString || argOpenType == '{' || argOpenType == '(') { - Name name = parseName("attribute name"); + Position openParenPosition = argOpenType == '(' ? argOpen.location.begin : Position::missing(); + TempVector argCommaPositions(scratchPosition2); + Position closeParenPosition = Position::missing(); - Location nameLoc = name.location; - const char* attrName = name.name.value; + auto [args, argsLocation, _exprLocation] = + options.storeCstData ? parseCallList(&argCommaPositions, &closeParenPosition) : parseCallList(nullptr, nullptr); - if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString || lexer.current().type == '{' || - lexer.current().type == '(') + for (const AstExpr* arg : args) { + if (!isConstantLiteral(arg) && !isLiteralTable(arg)) + report(argsLocation, "Only literals can be passed as arguments for attributes"); + } - auto [args, argsLocation, _exprLocation] = parseCallList(nullptr); + std::optional type = validateAttribute(nameLoc, attrName, attributes, args); - for (const AstExpr* arg : args) - { - if (!isConstantLiteral(arg) && !isLiteralTable(arg)) - report(argsLocation, "Only literals can be passed as arguments for attributes"); - } + AstAttr* node = + allocator.alloc(Location(nameLoc, argsLocation), type.value_or(AstAttr::Type::Unknown), args, AstName(attrName)); - std::optional type = validateAttribute(nameLoc, attrName, attributes, args); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(openParenPosition, closeParenPosition, copy(argCommaPositions)); - attributes.push_back( - allocator.alloc(Location(nameLoc, argsLocation), type.value_or(AstAttr::Type::Unknown), args, AstName(attrName)) - ); - } - else - { - std::optional type = validateAttribute(nameLoc, attrName, attributes, empty); - attributes.push_back(allocator.alloc(nameLoc, type.value_or(AstAttr::Type::Unknown), empty, AstName(attrName))); - } + attributes.push_back(node); + } + else + { + std::optional type = validateAttribute(nameLoc, attrName, attributes, empty); - if (lexer.current().type == ',') - { - nextLexeme(); - } - else - { - break; - } + AstAttr* node = allocator.alloc(nameLoc, type.value_or(AstAttr::Type::Unknown), empty, AstName(attrName)); + + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(/* hasAt */ false); + + attributes.push_back(node); } - } - else - { - report(Location(open.location, lexer.current().location), "Attribute list cannot be empty"); - // autocomplete expects at least one unknown attribute. - attributes.push_back( - allocator.alloc(Location(open.location, lexer.current().location), AstAttr::Type::Unknown, empty, nameError) - ); + const Lexeme& current = lexer.current(); + if (current.type == ',') + { + if (options.storeCstData) + commaPositions.push_back(current.location.begin); + + nextLexeme(); + } + else + { + break; + } } + } + else + { + report(Location(open.location, lexer.current().location), "Attribute list cannot be empty"); + + // autocomplete expects at least one unknown attribute. + AstAttr* node = allocator.alloc(Location(open.location, lexer.current().location), AstAttr::Type::Unknown, empty, nameError); + + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(/* hasAt */ false); + + attributes.push_back(node); + } + + bool closingBracketFound = expectMatchAndConsume(']', open); - expectMatchAndConsume(']', open); + if (options.storeCstData) + { + LUAU_ASSERT(cstAttrLists); + cstAttrLists->push_back(allocator.alloc( + open.location.begin, closingBracketFound ? lexer.previousLocation().begin : Position::missing(), copy(commaPositions) + )); } } +// attribute ::= '@' NAME +void Parser::parseAttribute(TempVector& attributes) +{ + AstArray empty; + + LUAU_ASSERT(lexer.current().type == Lexeme::Type::Attribute); + + Location loc = lexer.current().location; + + const char* name = lexer.current().name; + std::optional type = validateAttribute(loc, name, attributes, empty); + + nextLexeme(); + + AstAttr* node = allocator.alloc(loc, type.value_or(AstAttr::Type::Unknown), empty, AstName(name)); + attributes.push_back(node); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(/* hasAt */ true); +} + // attributes ::= {attribute} -AstArray Parser::parseAttributes() +AstArray Parser::parseAttributes(TempVector* cstAttrLists) { Lexeme::Type type = lexer.current().type; @@ -1020,56 +1124,123 @@ AstArray Parser::parseAttributes() TempVector attributes(scratchAttr); while (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) - parseAttribute(attributes); + { + if (lexer.current().type == Lexeme::Type::Attribute) + parseAttribute(attributes); + else + parseAttrList(attributes, cstAttrLists); + } return copy(attributes); } +Location Parser::getAttributeStartLocation( + const AstArray& attributes, + const TempVector* cstAttrLists, + const Location& defaultLocation +) +{ + if (attributes.size > 0) + { + if (cstAttrLists && cstAttrLists->size() > 0) + { + Location firstAttrLocation = attributes.data[0]->location; + const Position atBracketPosition = (*cstAttrLists)[0]->atBracketPosition; + + if (firstAttrLocation.begin < atBracketPosition) + return firstAttrLocation; + else + return Location(atBracketPosition, atBracketPosition); + } + else + return attributes.data[0]->location; + } + else if (cstAttrLists && cstAttrLists->size() > 0) + { + const Position atBracketPosition = (*cstAttrLists)[0]->atBracketPosition; + return Location(atBracketPosition, atBracketPosition); + } + else + return defaultLocation; +} + // attributes local function Name funcbody // attributes function funcname funcbody // attributes `declare function' Name`(' [parlist] `)' [`:` Type] // declare Name '{' Name ':' attributes `(' [parlist] `)' [`:` Type] '}' AstStat* Parser::parseAttributeStat() { - AstArray attributes = parseAttributes(); + const Location startLocation = lexer.current().location; + + AstArray attributes; + TempVector cstAttrLists(scratchCstAttrList); + attributes = parseAttributes(&cstAttrLists); Lexeme::Type type = lexer.current().type; switch (type) { case Lexeme::Type::ReservedFunction: - return parseFunctionStat(attributes); + return parseFunctionStat(attributes, &cstAttrLists); case Lexeme::Type::ReservedLocal: - return parseLocal(attributes); + return parseLocal( + getAttributeStartLocation(attributes, &cstAttrLists, startLocation), lexer.current().location.begin, attributes, false, &cstAttrLists + ); case Lexeme::Type::Name: + { + if (FFlag::LuauExportValueSyntax && AstName(lexer.current().name) == "export") + { + Location keywordLoc = lexer.current().location; + nextLexeme(); + return parseExportValue(getAttributeStartLocation(attributes, &cstAttrLists, startLocation), keywordLoc.begin, attributes, &cstAttrLists); + } + + if (strcmp("const", lexer.current().data) == 0) + { + Location keywordLoc = lexer.current().location; + nextLexeme(); + return parseLocal(getAttributeStartLocation(attributes, &cstAttrLists, startLocation), keywordLoc.begin, attributes, true, &cstAttrLists); + } if (options.allowDeclarationSyntax && !strcmp("declare", lexer.current().data)) { AstExpr* expr = parsePrimaryExpr(/* asStatement= */ true); return parseDeclaration(expr->location, attributes); } + } [[fallthrough]]; default: return reportStatError( lexer.current().location, {}, {}, - "Expected 'function', 'local function', 'declare function' or a function type declaration after attribute, but got %s instead", + "Expected 'function', 'local function', 'const function', 'declare function' or a function type declaration after attribute, but got " + "%s instead", lexer.current().toString().c_str() ); } } -// local function Name funcbody | -// local bindinglist [`=' explist] -AstStat* Parser::parseLocal(const AstArray& attributes) +bool isEnoughValues(TempVector& values, size_t expected) { - Location start = lexer.current().location; - - if (attributes.size > 0) - start = attributes.data[0]->location; + if (values.size() > 0) + { + AstExpr* last = values.back(); + if (last->is() || last->is()) + return true; + } + return values.size() == expected; +} - Position localKeywordPosition = lexer.current().location.begin; - nextLexeme(); // local +AstStat* Parser::parseLocal( + const Location start, + const Position keywordPosition, + const AstArray& attributes, + bool isConst, + TempVector* cstAttrLists +) +{ + if (!isConst) + nextLexeme(); // local if (lexer.current().type == Lexeme::ReservedFunction) { @@ -1086,15 +1257,21 @@ AstStat* Parser::parseLocal(const AstArray& attributes) matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; - auto [body, var] = parseFunctionBody(false, matchFunction, name.name, &name, attributes); + auto [body, var] = parseFunctionBody(false, matchFunction, name.name, &name, attributes, isConst); matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; Location location{start.begin, body->location.end}; - AstStatLocalFunction* node = allocator.alloc(location, var, body); + AstStatLocalFunction* node = allocator.alloc( + location, var, body, isConst, isConst && FFlag::LuauStoreConstKeywordBegin ? keywordPosition : Position::missing() + ); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(localKeywordPosition, functionKeywordPosition); + { + cstNodeMap[node] = cstAttrLists != nullptr + ? allocator.alloc(copy(*cstAttrLists), keywordPosition, functionKeywordPosition) + : allocator.alloc(keywordPosition, functionKeywordPosition); + } return node; } else @@ -1115,9 +1292,9 @@ AstStat* Parser::parseLocal(const AstArray& attributes) TempVector names(scratchBinding); AstArray varsCommaPositions; if (options.storeCstData) - parseBindingList(names, false, &varsCommaPositions); + parseBindingList(names, false, &varsCommaPositions, nullptr, nullptr, isConst); else - parseBindingList(names); + parseBindingList(names, false, nullptr, nullptr, nullptr, isConst); matchRecoveryStopOnToken['=']--; @@ -1142,12 +1319,24 @@ AstStat* Parser::parseLocal(const AstArray& attributes) Location end = values.empty() ? lexer.previousLocation() : values.back()->location; - AstStatLocal* node = allocator.alloc(Location(start, end), copy(vars), copy(values), equalsSignLocation); + AstStatLocal* node = allocator.alloc(Location(start, end), copy(vars), copy(values), equalsSignLocation, isConst); if (options.storeCstData) { cstNodeMap[node] = allocator.alloc(extractAnnotationColonPositions(names), varsCommaPositions, copy(valuesCommaPositions)); } + // It is a syntax error when a const declaration *definitely* does + // not have enough values, for example: + // + // const foo + // const bar, baz = 42 + // + // Both error as there's probably user error (`foo` and `baz` can + // only ever be `nil`). We report an error but return the + // declaration as-is, as it's still reasonable syntactically. + if (isConst && !isEnoughValues(values, vars.size())) + report(node->location, "Missing initializer in const declaration"); + return node; } } @@ -1170,6 +1359,15 @@ AstStat* Parser::parseReturn() AstStatReturn* node = allocator.alloc(Location(start, end), copy(list)); if (options.storeCstData) cstNodeMap[node] = allocator.alloc(copy(commaPositions)); + + if (FFlag::LuauExportValueSyntax && functionStack.size() == 1) + { + if (!declaredExportBindings.empty()) + report(node->location, "Exporting values is not compatible with top-level return (export/return conflict)"); + + hasModuleReturn = true; + } + return node; } @@ -1190,17 +1388,17 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t if (!name) name = Name(nameError, lexer.current().location); - Position genericsOpenPosition{0, 0}; + Position genericsOpenPosition = Position::missing(); AstArray genericsCommaPositions; - Position genericsClosePosition{0, 0}; + Position genericsClosePosition = Position::missing(); auto [generics, genericPacks] = options.storeCstData ? parseGenericTypeList( /* withDefaultValues= */ true, &genericsOpenPosition, &genericsCommaPositions, &genericsClosePosition ) : parseGenericTypeList(/* withDefaultValues= */ true); - Position equalsPosition = lexer.current().location.begin; - expectAndConsume('=', "type alias"); + bool equalsFound = expectAndConsume('=', "type alias"); + Position equalsPosition = equalsFound ? lexer.previousLocation().begin : Position::missing(); AstType* type = parseType(); @@ -1213,6 +1411,217 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t return node; } +namespace +{ + +const std::unordered_set ALLOWED_METAMETHODS{ + "__call", + "__concat", + "__unm", + "__add", + "__sub", + "__mul", + "__div", + "__mod", + "__pow", + "__tostring", + "__eq", + "__lt", + "__le", + "__iter", + "__len", + "__idiv", +}; + +const std::unordered_set EXPLICITLY_DISALLOWED_METAMETHODS{ + "__index", + "__newindex", + "__mode", + "__metatable", + "__type", +}; + +} // namespace + +// classStatement ::= `class` Name classProps `end` +// classProps ::= classProp [classProps] +// classProp ::= name [: classQualifier* type] +LUAU_NOINLINE AstStat* Parser::parseClassStat(const Location& start, bool exported) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + std::optional name = parseNameOpt("type name"); + + // Use error name if the name is missing + if (!name) + name = Name(nameError, lexer.current().location); + + AstLocal* nameLocal = + allocator.alloc(name->name, name->location, nullptr, functionStack.size() - 1, functionStack.back().loopDepth, nullptr, true); + + AstExpr* super = nullptr; + if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "extends") + { + nextLexeme(); + + super = parseClassRefExpr(); + } + + TempVector declarations(scratchClassDeclarations); + + // TODO: This does not seem particularly performant, but we need to + // establish the invariant that properties and methods share a + // namespace, so writing something like: + // + // class Foo + // public x + // function x() end + // end + // + // ... must fail. This gets the job done but maybe we can do something + // slightly more performant here (e.g.: a "scratch" set). + DenseHashSet classMemberNamespace{{}}; + + while (lexer.current().type != Lexeme::ReservedEnd && lexer.current().type != Lexeme::Eof) + { + std::optional qualifierLocation; + if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "public") + { + qualifierLocation = lexer.current().location; + nextLexeme(); + } + + // If we saw a qualifier _and_ the current token is not `function`, + // assume this is a property. + if (qualifierLocation && lexer.current().type != Lexeme::ReservedFunction) + { + std::optional propName = parseNameOpt("class property name"); + if (!propName) + continue; + + AstType* propType = nullptr; + std::optional typeColonLocation; + + if (lexer.current().type == ':') + { + typeColonLocation = lexer.current().location; + nextLexeme(); + propType = parseType(); + } + + if (strncmp(propName->name.value, "__", 2) == 0) + report(propName->location, "Class properties cannot start with '__'"); + + if (classMemberNamespace.contains(propName->name)) + { + report(propName->location, "Duplicate class member '%s'", propName->name.value); + } + else + { + classMemberNamespace.insert(propName->name); + + // Either both of these are present or neither are. + LUAU_ASSERT((bool)propType == (bool)typeColonLocation); + declarations.push_back( + AstClassProperty{ + *qualifierLocation, + propName->name, + propName->location, + typeColonLocation, + propType, + } + ); + } + } + else if (lexer.current().type == Lexeme::ReservedFunction) + { + auto matchFunction = lexer.current(); + nextLexeme(); + + Name name = parseName("method name"); + // This is a little funky as we pass in a debug name but not a + // local name. The reason is that // in the declaration: + // + // class Student + // public name + // function print(self) + // print(`Hello, I'm a student and my name is {self.name}`) + // end + // end + // + // ... `print` inside `Student.print` is the _global_ print, and not + // the class' print. That would be `self.print`. + matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; + + auto [body, _] = parseFunctionBody(false, matchFunction, name.name, nullptr, {}); + + matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; + + if (body->args.size > 0 && body->args.data[0]->name == "self" && body->args.data[0]->annotation != nullptr) + report(body->args.data[0]->annotation->location, "The 'self' parameter cannot have a type annotation"); + + if (strncmp(name.name.value, "__", 2) == 0) + { + if (EXPLICITLY_DISALLOWED_METAMETHODS.count(name.name.value) > 0) + report(name.location, "Classes cannot define '%s' as a metamethod", name.name.value); + else if (ALLOWED_METAMETHODS.count(name.name.value) == 0) + report(name.location, "Cannot use '%s' as a method name: names starting with '__' are reserved", name.name.value); + } + + // TODO CLI-200853: We should support attributes, we do not need + // to support them prior to the full launch. + if (classMemberNamespace.contains(name.name)) + { + report(name.location, "Duplicate class member '%s'", name.name.value); + } + else + { + classMemberNamespace.insert(name.name); + + declarations.push_back( + AstClassMethod{ + qualifierLocation, + matchFunction.location, + name.name, + name.location, + body, + } + ); + } + } + else + { + report(lexer.current().location, "Only class properties and functions can be declared within a class"); + nextLexeme(); // skip the unexpected token to avoid an infinite loop + } + } + + // TODO: We should use `expectMatchEndAndConsume`. It is difficult as we + // are treating "class" as a contextual keyword (and we must as we also) + // plan to add a `class` library. + Location end = lexer.current().location; + expectAndConsume(Lexeme::ReservedEnd, "class"); + Location location{start, end}; + + // We only allow classes at the top level: we can make use of the + // recursion counter to check this, though it's a little clowny. + if (recursionCounter > 1) + report(nameLocal->location, "Cannot declare class '%s' inside another statement or expression", nameLocal->name.value); + + AstStatClass* cls = allocator.alloc(location, nameLocal, super, copy(declarations), exported); + if (classesWithinModule.contains(nameLocal->name)) + { + return reportStatError( + nameLocal->location, + {}, + copy({static_cast(cls)}), + "A class named '%s' has already been declared in this module", + nameLocal->name.value + ); + } + classesWithinModule[nameLocal->name] = cls; + return cls; +} + // type function Name `(' arglist `)' `=' funcbody `end' AstStat* Parser::parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition) { @@ -1381,12 +1790,21 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArray props(scratchDeclaredClassProps); AstTableIndexer* indexer = nullptr; @@ -1494,6 +1910,27 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArray propName = parseNameOpt("property name"); @@ -1503,7 +1940,9 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArrayname, propName->location, propType, false, Location(propStart, lexer.previousLocation())} + AstDeclaredExternTypeProperty{ + propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation()), access + } ); } } @@ -1524,18 +1963,39 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArrayis() && expr->as()->local->isConst) + { + AstExprLocal* local = expr->as(); + return reportExprError(expr->location, copy({expr}), "Variable '%s' is constant and may not be reassigned", local->local->name.value); + } + if (FFlag::DebugLuauUserDefinedClasses) + { + if (AstStatClass* classStat = getMatchingClass(expr)) + { + return reportExprError( + expr->location, + copy({expr}), + "'%s' refers to a class and cannot be used as a variable name (defined on line %d)", + classStat->name->name.value, + classStat->location.begin.line + 1 + ); + } + } -static bool isExprLValue(AstExpr* expr) -{ - return expr->is() || expr->is() || expr->is() || expr->is(); + return reportExprError(expr->location, copy({expr}), "Assigned expression must be a variable or a field"); } // varlist `=' explist AstStat* Parser::parseAssignment(AstExpr* initial) { if (!isExprLValue(initial)) - initial = reportExprError(initial->location, copy({initial}), "Assigned expression must be a variable or a field"); + initial = FFlag::LuauExportValueSyntax + ? reportLValueError(initial) + : reportExprError(initial->location, copy({initial}), "Assigned expression must be a variable or a field"); TempVector vars(scratchExpr); TempVector varsCommaPositions(scratchPosition); @@ -1550,29 +2010,149 @@ AstStat* Parser::parseAssignment(AstExpr* initial) AstExpr* expr = parsePrimaryExpr(/* asStatement= */ true); if (!isExprLValue(expr)) - expr = reportExprError(expr->location, copy({expr}), "Assigned expression must be a variable or a field"); + expr = FFlag::LuauExportValueSyntax ? reportLValueError(expr) + : reportExprError(expr->location, copy({expr}), "Assigned expression must be a variable or a field"); vars.push_back(expr); } - Position equalsPosition = lexer.current().location.begin; - expectAndConsume('=', "assignment"); + bool equalsFound = expectAndConsume('=', "assignment"); + Position equalsPosition = equalsFound ? lexer.previousLocation().begin : Position::missing(); TempVector values(scratchExprAux); TempVector valuesCommaPositions(scratchPosition); parseExprList(values, options.storeCstData ? &valuesCommaPositions : nullptr); AstStatAssign* node = allocator.alloc(Location(initial->location, values.back()->location), copy(vars), copy(values)); - cstNodeMap[node] = allocator.alloc(copy(varsCommaPositions), equalsPosition, copy(valuesCommaPositions)); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(copy(varsCommaPositions), equalsPosition, copy(valuesCommaPositions)); return node; } +AstStat* Parser::parseExportValue( + const Location& start, + const Position keywordPosition, + const AstArray& attributes, + TempVector* cstAttrLists +) +{ + if (functionStack.size() != 1 || recursionCounter != 1) + report(start, "'export' may only be applied to top-level statements"); + + if (hasModuleReturn) + report(start, "Exporting values is not compatible with top-level return (export/return conflict)"); + + auto checkDuplicateExport = [&](AstName name, const Location& location) -> bool + { + if (declaredExportBindings.find(name)) + return false; + + declaredExportBindings[name] = location; + return true; + }; + + auto exportLocalStat = [&](AstStat* stat, const Location& keywordLocation) -> AstStat* + { + if (AstStatLocal* localStat = stat->as()) + { + localStat->isExported = true; + + for (AstLocal* local : localStat->vars) + { + if (!checkDuplicateExport(local->name, local->location)) + { + report(local->location, "Duplicate exported identifier '%s'", local->name.value); + continue; + } + + local->isExported = true; + } + + localStat->keywordLocation = keywordLocation; + } + else + LUAU_ASSERT(!"Expected export local/const to parse as AstStatLocal"); + + return stat; + }; + + if (attributes.size != 0 && lexer.current().type != Lexeme::ReservedFunction) + { + report( + lexer.current().location, + "Expected 'function' after export declaration with attribute, but got %s instead", + lexer.current().toString().c_str() + ); + } + + if (lexer.current().type == Lexeme::ReservedLocal) + { + Location localKeywordLocation = lexer.current().location; + + if (lexer.lookahead().type == Lexeme::ReservedFunction) + { + report(start, "'export' must be followed by an identifier or 'function'; try removing 'local'"); + // still parse the function for error recovery + return parseLocal(start, localKeywordLocation.begin, {nullptr, 0}, true); + } + + return exportLocalStat(parseLocal(start, keywordPosition, {nullptr, 0}, false), localKeywordLocation); + } + else if (lexer.current().type == Lexeme::ReservedFunction) + { + auto funcStat = parseLocal(start, keywordPosition, attributes, true, cstAttrLists); + if (!funcStat->is()) + // parseLocal returned a parse error + return funcStat; + + auto func = funcStat->as(); + + if (!checkDuplicateExport(func->name->name, func->name->location)) + report(func->name->location, "Duplicate exported identifier '%s'", func->name->name.value); + + func->name->isExported = true; + func->name->isConst = true; + return func; + } + else if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "const") + { + Location constKeywordLocation = lexer.current().location; + nextLexeme(); + + if (lexer.current().type == Lexeme::ReservedFunction) + { + report(start, "'export' must be followed by an identifier or 'function'"); + // still parse the function for error recovery + return parseLocal(start, constKeywordLocation.begin, {nullptr, 0}, true); + } + + return exportLocalStat(parseLocal(start, constKeywordLocation.begin, {nullptr, 0}, true), constKeywordLocation); + } + else if (FFlag::DebugLuauUserDefinedClasses && lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "class") + { + nextLexeme(); + auto stat = parseClassStat(start, /*exported*/ true); + if (auto classStat = stat->as()) + { + if (!checkDuplicateExport(classStat->name->name, classStat->name->location)) + report(classStat->name->location, "Duplicate exported class '%s'", classStat->name->name.value); + + classStat->name->isExported = true; + } + return stat; + } + + return reportStatError(start, {}, {}, "'export' must be followed by an identifier or 'function'"); +} + // var [`+=' | `-=' | `*=' | `/=' | `%=' | `^=' | `..='] exp AstStat* Parser::parseCompoundAssignment(AstExpr* initial, AstExprBinary::Op op) { if (!isExprLValue(initial)) { - initial = reportExprError(initial->location, copy({initial}), "Assigned expression must be a variable or a field"); + initial = FFlag::LuauExportValueSyntax + ? reportLValueError(initial) + : reportExprError(initial->location, copy({initial}), "Assigned expression must be a variable or a field"); } Position opPosition = lexer.current().location.begin; @@ -1608,7 +2188,9 @@ std::pair Parser::parseFunctionBody( const Lexeme& matchFunction, const AstName& debugname, const Name* localName, - const AstArray& attributes + const AstArray& attributes, + const bool isConst, + TempVector* cstAttrLists ) { Location start = matchFunction.location; @@ -1618,6 +2200,9 @@ std::pair Parser::parseFunctionBody( auto* cstNode = options.storeCstData ? allocator.alloc() : nullptr; + if (cstNode && cstAttrLists) + cstNode->attrLists = copy(*cstAttrLists); + auto [generics, genericPacks] = cstNode ? parseGenericTypeList( @@ -1669,7 +2254,9 @@ std::pair Parser::parseFunctionBody( AstLocal* funLocal = nullptr; if (localName) - funLocal = pushLocal(Binding(*localName, nullptr)); + { + funLocal = pushLocal(Binding(*localName, nullptr, {0, 0}, isConst)); + } unsigned int localsBegin = saveLocals(); @@ -1738,7 +2325,7 @@ void Parser::parseExprList(TempVector& result, TempVector* c } } -Parser::Binding Parser::parseBinding() +Parser::Binding Parser::parseBinding(bool isConst) { std::optional name = parseNameOpt("variable name"); @@ -1746,13 +2333,13 @@ Parser::Binding Parser::parseBinding() if (!name) name = Name(nameError, lexer.current().location); - Position colonPosition = lexer.current().location.begin; + Position colonPosition = lexer.current().type == ':' ? lexer.current().location.begin : Position::missing(); AstType* annotation = parseOptionalType(); if (options.storeCstData) - return Binding(*name, annotation, colonPosition); + return Binding(*name, annotation, colonPosition, isConst); else - return Binding(*name, annotation); + return Binding(*name, annotation, Position::missing(), isConst); } AstArray Parser::extractAnnotationColonPositions(const TempVector& bindings) @@ -1769,7 +2356,8 @@ LUAU_NOINLINE std::tuple Parser::parseBindingList( bool allowDot3, AstArray* commaPositions, Position* initialCommaPosition, - Position* varargAnnotationColonPosition + Position* varargAnnotationColonPosition, + bool isConst ) { TempVector localCommaPositions(scratchPosition); @@ -1800,7 +2388,7 @@ LUAU_NOINLINE std::tuple Parser::parseBindingList( return {true, varargLocation, tailAnnotation}; } - result.push_back(parseBinding()); + result.push_back(parseBinding(isConst)); if (lexer.current().type != ',') break; @@ -1831,7 +2419,7 @@ AstTypePack* Parser::parseTypeList( TempVector& result, TempVector>& resultNames, TempVector* commaPositions, - TempVector>* nameColonPositions + TempVector* nameColonPositions ) { while (true) @@ -1847,7 +2435,7 @@ AstTypePack* Parser::parseTypeList( if (nameColonPositions) { while (nameColonPositions->size() < result.size()) - nameColonPositions->push_back({}); + nameColonPositions->push_back(Position::missing()); } resultNames.push_back(AstArgumentName{AstName(lexer.current().name), lexer.current().location}); @@ -1862,7 +2450,7 @@ AstTypePack* Parser::parseTypeList( // If we have a type with named arguments, provide elements for all types resultNames.push_back({}); if (nameColonPositions) - nameColonPositions->push_back({}); + nameColonPositions->push_back(Position::missing()); } result.push_back(parseType()); @@ -1946,7 +2534,7 @@ AstTypePack* Parser::parseReturnType() TempVector result(scratchType); TempVector> resultNames(scratchOptArgName); TempVector commaPositions(scratchPosition); - TempVector> nameColonPositions(scratchOptPosition); + TempVector nameColonPositions(scratchPosition2); AstTypePack* varargAnnotation = nullptr; // possibly () -> ReturnType @@ -1959,8 +2547,8 @@ AstTypePack* Parser::parseReturnType() } const Location location{begin.location, lexer.current().location}; - Position closeParenthesesPosition = lexer.current().location.begin; - expectMatchAndConsume(')', begin, true); + bool closeParenFound = expectMatchAndConsume(')', begin, true); + Position closeParenthesesPosition = closeParenFound ? lexer.previousLocation().begin : Position::missing(); matchRecoveryStopOnToken[Lexeme::SkinnyArrow]--; @@ -1970,7 +2558,18 @@ AstTypePack* Parser::parseReturnType() if (result.size() == 1) { // TODO(CLI-140667): stop parsing type suffix when varargAnnotation != nullptr - this should be a parse error - AstType* inner = varargAnnotation == nullptr ? allocator.alloc(location, result[0]) : result[0]; + AstType* inner = nullptr; + + if (varargAnnotation == nullptr) + { + inner = allocator.alloc(location, result[0]); + + if (options.storeCstData) + cstNodeMap[inner] = allocator.alloc(closeParenFound ? closeParenthesesPosition : Position::missing()); + } + else + inner = result[0]; + AstType* returnType = parseTypeSuffix(inner, begin.location); if (DFFlag::DebugLuauReportReturnTypeVariadicWithTypeSuffix && varargAnnotation != nullptr && @@ -2000,9 +2599,9 @@ AstTypePack* Parser::parseReturnType() if (options.storeCstData && tail->is()) { cstNodeMap[tail] = allocator.alloc( - Position{0, 0}, + Position::missing(), AstArray{}, - Position{0, 0}, + Position::missing(), location.begin, copy(nameColonPositions), copy(commaPositions), @@ -2025,15 +2624,15 @@ std::pair Parser::extractString switch (lexer.current().type) { case Lexeme::QuotedString: - style = - lexer.current().getQuoteStyle() == Lexeme::QuoteStyle::Double ? CstExprConstantString::QuotedDouble : CstExprConstantString::QuotedSingle; + style = lexer.current().getQuoteStyle() == Lexeme::QuoteStyle::Double ? CstExprConstantString::QuoteStyle::QuotedDouble + : CstExprConstantString::QuoteStyle::QuotedSingle; break; case Lexeme::InterpStringSimple: - style = CstExprConstantString::QuotedInterp; + style = CstExprConstantString::QuoteStyle::QuotedInterp; break; case Lexeme::RawString: { - style = CstExprConstantString::QuotedRaw; + style = CstExprConstantString::QuoteStyle::QuotedRaw; blockDepth = lexer.current().getBlockDepth(); break; } @@ -2049,11 +2648,11 @@ Parser::TableIndexerResult Parser::parseTableIndexer(AstTableAccess access, std: { AstType* index = parseType(); - Position indexerClosePosition = lexer.current().location.begin; - expectMatchAndConsume(']', begin); + bool indexerCloseFound = expectMatchAndConsume(']', begin); + Position indexerClosePosition = indexerCloseFound ? lexer.previousLocation().begin : Position::missing(); - Position colonPosition = lexer.current().location.begin; - expectAndConsume(':', "table field"); + bool colonFound = expectAndConsume(':', "table field"); + Position colonPosition = colonFound ? lexer.previousLocation().begin : Position::missing(); AstType* result = parseType(); @@ -2121,10 +2720,10 @@ AstType* Parser::parseTableType(bool inDeclarationContext) AstArray sourceString; std::optional> chars = parseCharArray(options.storeCstData ? &sourceString : nullptr); - Position indexerClosePosition = lexer.current().location.begin; - expectMatchAndConsume(']', begin); - Position colonPosition = lexer.current().location.begin; - expectAndConsume(':', "table field"); + bool closingBracketFound = expectMatchAndConsume(']', begin); + Position indexerClosePosition = closingBracketFound ? lexer.previousLocation().begin : Position::missing(); + bool colonFound = expectAndConsume(':', "table field"); + Position colonPosition = colonFound ? lexer.previousLocation().begin : Position::missing(); AstType* type = parseType(); @@ -2135,18 +2734,21 @@ AstType* Parser::parseTableType(bool inDeclarationContext) { props.push_back(AstTableProp{AstName(chars->data), begin.location, type, access, accessLocation}); if (options.storeCstData) + { + CstExprTable::Separator separator = tableSeparator(); cstItems.push_back( CstTypeTable::Item{ CstTypeTable::Item::Kind::StringProperty, begin.location.begin, indexerClosePosition, colonPosition, - tableSeparator(), - lexer.current().location.begin, + separator, + separator != CstExprTable::Separator::Missing ? lexer.current().location.begin : Position::missing(), allocator.alloc(sourceString, style, blockDepth), stringPosition } ); + } } else report(begin.location, "String literal contains malformed escape sequence or \\0"); @@ -2167,16 +2769,19 @@ AstType* Parser::parseTableType(bool inDeclarationContext) auto tableIndexerResult = parseTableIndexer(access, accessLocation, begin); indexer = tableIndexerResult.node; if (options.storeCstData) + { + CstExprTable::Separator separator = tableSeparator(); cstItems.push_back( CstTypeTable::Item{ CstTypeTable::Item::Kind::Indexer, tableIndexerResult.indexerOpenPosition, tableIndexerResult.indexerClosePosition, tableIndexerResult.colonPosition, - tableSeparator(), - lexer.current().location.begin, + separator, + separator != CstExprTable::Separator::Missing ? lexer.current().location.begin : Position::missing(), } ); + } } } } @@ -2186,18 +2791,9 @@ AstType* Parser::parseTableType(bool inDeclarationContext) // array-like table type: {T} desugars into {[number]: T} isArray = true; - if (FFlag::DesugaredArrayTypeReferenceIsEmpty) - { - Location nullTypeLocation = Location(start.begin, 0); - AstType* index = allocator.alloc(nullTypeLocation, std::nullopt, nameNumber, std::nullopt, nullTypeLocation); - indexer = allocator.alloc(AstTableIndexer{index, type, type->location, access, accessLocation}); - } - else - { - AstType* index = allocator.alloc(type->location, std::nullopt, nameNumber, std::nullopt, type->location); - indexer = allocator.alloc(AstTableIndexer{index, type, type->location, access, accessLocation}); - } - + Location nullTypeLocation = Location(start.begin, 0); + AstType* index = allocator.alloc(nullTypeLocation, std::nullopt, nameNumber, std::nullopt, nullTypeLocation); + indexer = allocator.alloc(AstTableIndexer{index, type, type->location, access, accessLocation}); break; } else @@ -2207,23 +2803,26 @@ AstType* Parser::parseTableType(bool inDeclarationContext) if (!name) break; - Position colonPosition = lexer.current().location.begin; - expectAndConsume(':', "table field"); + bool colonFound = expectAndConsume(':', "table field"); + Position colonPosition = colonFound ? lexer.previousLocation().begin : Position::missing(); AstType* type = parseType(inDeclarationContext); props.push_back(AstTableProp{name->name, name->location, type, access, accessLocation}); if (options.storeCstData) + { + CstExprTable::Separator separator = tableSeparator(); cstItems.push_back( CstTypeTable::Item{ CstTypeTable::Item::Kind::Property, - Position{0, 0}, - Position{0, 0}, + Position::missing(), + Position::missing(), colonPosition, - tableSeparator(), - lexer.current().location.begin + separator, + separator != CstExprTable::Separator::Missing ? lexer.current().location.begin : Position::missing(), } ); + } } if (lexer.current().type == ',' || lexer.current().type == ';') @@ -2258,9 +2857,9 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray Lexeme begin = lexer.current(); - Position genericsOpenPosition{0, 0}; + Position genericsOpenPosition = Position::missing(); AstArray genericsCommaPositions; - Position genericsClosePosition{0, 0}; + Position genericsClosePosition = Position::missing(); auto [generics, genericPacks] = options.storeCstData ? parseGenericTypeList( /* withDefaultValues= */ false, &genericsOpenPosition, &genericsCommaPositions, &genericsClosePosition @@ -2269,14 +2868,14 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray Lexeme parameterStart = lexer.current(); - expectAndConsume('(', "function parameters"); + bool openArgsFound = expectAndConsume('(', "function parameters"); matchRecoveryStopOnToken[Lexeme::SkinnyArrow]++; TempVector params(scratchType); TempVector> names(scratchOptArgName); - TempVector> nameColonPositions(scratchOptPosition); - TempVector argCommaPositions(scratchPosition); + TempVector nameColonPositions(scratchPosition); + TempVector argCommaPositions(scratchPosition2); AstTypePack* varargAnnotation = nullptr; if (lexer.current().type != ')') @@ -2288,7 +2887,7 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray } Location closeArgsLocation = lexer.current().location; - expectMatchAndConsume(')', parameterStart, true); + bool closeArgsFound = expectMatchAndConsume(')', parameterStart, true); matchRecoveryStopOnToken[Lexeme::SkinnyArrow]--; @@ -2306,13 +2905,21 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray { AstTypePackExplicit* node = allocator.alloc(begin.location, AstTypeList{paramTypes, nullptr}); if (options.storeCstData) - cstNodeMap[node] = - allocator.alloc(parameterStart.location.begin, closeArgsLocation.begin, copy(argCommaPositions)); + cstNodeMap[node] = allocator.alloc( + openArgsFound ? parameterStart.location.begin : Position::missing(), + closeArgsFound ? closeArgsLocation.begin : Position::missing(), + copy(argCommaPositions) + ); return {{}, node}; } else { - return {allocator.alloc(Location(parameterStart.location, closeArgsLocation), params[0]), {}}; + AstTypeGroup* node = allocator.alloc(Location(parameterStart.location, closeArgsLocation), params[0]); + + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(closeArgsFound ? closeArgsLocation.begin : Position::missing()); + + return {node, {}}; } } @@ -2320,7 +2927,11 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray { AstTypePackExplicit* node = allocator.alloc(begin.location, AstTypeList{paramTypes, varargAnnotation}); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(parameterStart.location.begin, closeArgsLocation.begin, copy(argCommaPositions)); + cstNodeMap[node] = allocator.alloc( + openArgsFound ? parameterStart.location.begin : Position::missing(), + closeArgsFound ? closeArgsLocation.begin : Position::missing(), + copy(argCommaPositions) + ); return {{}, node}; } @@ -2334,10 +2945,10 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray genericsOpenPosition, genericsCommaPositions, genericsClosePosition, - parameterStart.location.begin, + openArgsFound ? parameterStart.location.begin : Position::missing(), copy(nameColonPositions), copy(argCommaPositions), - closeArgsLocation.begin, + closeArgsFound ? closeArgsLocation.begin : Position::missing(), returnArrowPosition ); } @@ -2397,7 +3008,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) { TempVector parts(scratchType); TempVector separatorPositions(scratchPosition); - std::optional leadingPosition = std::nullopt; + Position leadingPosition = Position::missing(); if (type != nullptr) parts.push_back(type); @@ -2426,7 +3037,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) if (options.storeCstData) { - if (type == nullptr && !leadingPosition.has_value()) + if (type == nullptr && !leadingPosition.hasValue()) leadingPosition = separatorPosition; else separatorPositions.push_back(separatorPosition); @@ -2456,7 +3067,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) if (options.storeCstData) { - if (type == nullptr && !leadingPosition.has_value()) + if (type == nullptr && !leadingPosition.hasValue()) leadingPosition = separatorPosition; else separatorPositions.push_back(separatorPosition); @@ -2619,8 +3230,9 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) else if (lexer.current().type == Lexeme::Name) { std::optional prefix; - std::optional prefixPointPosition; + Position prefixPointPosition = Position::missing(); std::optional prefixLocation; + AstLocal* prefixLocal = nullptr; Name name = parseName("type name"); if (lexer.current().type == '.') @@ -2630,7 +3242,14 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) prefix = name.name; prefixLocation = name.location; - name = parseIndexName("field name", *prefixPointPosition); + + if (FFlag::LuauTrackPrefixLocal) + { + AstLocal* const* prefixLocalValue = localMap.find(name.name); + prefixLocal = (prefixLocalValue && *prefixLocalValue) ? *prefixLocalValue : nullptr; + } + + name = parseIndexName("field name", prefixPointPosition); } else if (lexer.current().type == Lexeme::Dot3) { @@ -2640,25 +3259,29 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) else if (name.name == "typeof") { Lexeme typeofBegin = lexer.current(); - expectAndConsume('(', "typeof type"); + bool openParenFound = expectAndConsume('(', "typeof type"); AstExpr* expr = parseExpr(); Location end = lexer.current().location; - expectMatchAndConsume(')', typeofBegin); + bool closeParenFound = expectMatchAndConsume(')', typeofBegin); AstTypeTypeof* node = allocator.alloc(Location(start, end), expr); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(typeofBegin.location.begin, end.begin); + { + cstNodeMap[node] = allocator.alloc( + openParenFound ? typeofBegin.location.begin : Position::missing(), closeParenFound ? end.begin : Position::missing() + ); + } return {node, {}}; } bool hasParameters = false; AstArray parameters{}; - Position parametersOpeningPosition{0, 0}; + Position parametersOpeningPosition = Position::missing(); TempVector parametersCommaPositions(scratchPosition); - Position parametersClosingPosition{0, 0}; + Position parametersClosingPosition = Position::missing(); if (lexer.current().type == '<') { @@ -2671,8 +3294,9 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) Location end = lexer.previousLocation(); - AstTypeReference* node = - allocator.alloc(Location(start, end), prefix, name.name, prefixLocation, name.location, hasParameters, parameters); + AstTypeReference* node = allocator.alloc( + Location(start, end), prefix, name.name, prefixLocation, name.location, hasParameters, parameters, prefixLocal + ); if (options.storeCstData) cstNodeMap[node] = allocator.alloc( prefixPointPosition, parametersOpeningPosition, copy(parametersCommaPositions), parametersClosingPosition @@ -2767,11 +3391,11 @@ AstTypePack* Parser::parseTypePack() std::optional Parser::parseUnaryOp(const Lexeme& l) { if (l.type == Lexeme::ReservedNot) - return AstExprUnary::Not; + return AstExprUnary::Op::Not; else if (l.type == '-') - return AstExprUnary::Minus; + return AstExprUnary::Op::Minus; else if (l.type == '#') - return AstExprUnary::Len; + return AstExprUnary::Op::Len; else return std::nullopt; } @@ -2850,7 +3474,7 @@ std::optional Parser::checkUnaryConfusables() if (curr.type == '!') { report(start, "Unexpected '!'; did you mean 'not'?"); - return AstExprUnary::Not; + return AstExprUnary::Op::Not; } return {}; @@ -3016,6 +3640,8 @@ AstExpr* Parser::parsePrefixExpr() Position end = lexer.current().location.end; + bool closeParenFound = false; + if (lexer.current().type != ')') { const char* suggestion = (lexer.current().type == '=') ? "; did you mean to use '{' when defining a table?" : nullptr; @@ -3026,10 +3652,17 @@ AstExpr* Parser::parsePrefixExpr() } else { + closeParenFound = true; + nextLexeme(); } - return allocator.alloc(Location(start, end), expr); + AstExpr* exprGroup = allocator.alloc(Location(start, end), expr); + + if (options.storeCstData) + cstNodeMap[exprGroup] = allocator.alloc(closeParenFound ? lexer.previousLocation().begin : Position::missing()); + + return exprGroup; } else { @@ -3059,19 +3692,7 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement) } else if (lexer.current().type == '[') { - MatchLexeme matchBracket = lexer.current(); - nextLexeme(); - - AstExpr* index = parseExpr(); - - Position closeBracketPosition = lexer.current().location.begin; - Position end = lexer.current().location.end; - - expectMatchAndConsume(']', matchBracket); - - expr = allocator.alloc(Location(start, end), expr, index); - if (options.storeCstData) - cstNodeMap[expr] = allocator.alloc(matchBracket.position, closeBracketPosition); + expr = parseIndexExpr(start, expr); } else if (lexer.current().type == ':') { @@ -3092,7 +3713,7 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement) { expr = parseFunctionArgs(expr, false); } - else if (FFlag::LuauExplicitTypeInstantiationSyntax && lexer.current().type == '<' && lexer.lookahead().type == '<') + else if (lexer.current().type == '<' && lexer.lookahead().type == '<') { expr = parseExplicitTypeInstantiationExpr(start, *expr); } @@ -3110,6 +3731,25 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement) return expr; } +LUAU_NOINLINE AstExpr* Parser::parseIndexExpr(Position start, AstExpr* expr) +{ + MatchLexeme matchBracket = lexer.current(); + nextLexeme(); + + AstExpr* index = parseExpr(); + + Position closeBracketPosition = lexer.current().location.begin; + Position end = lexer.current().location.end; + + bool closingBracketFound = expectMatchAndConsume(']', matchBracket); + + expr = allocator.alloc(Location(start, end), expr, index); + if (options.storeCstData) + cstNodeMap[expr] = allocator.alloc(matchBracket.position, closingBracketFound ? closeBracketPosition : Position::missing()); + + return expr; +} + AstExpr* Parser::parseMethodCall(Position start, AstExpr* expr) { Position opPosition = lexer.current().location.begin; @@ -3118,38 +3758,31 @@ AstExpr* Parser::parseMethodCall(Position start, AstExpr* expr) Name index = parseIndexName("method name", opPosition); AstExpr* func = allocator.alloc(Location(start, index.location.end), expr, index.name, index.location, opPosition, ':'); - if (FFlag::LuauExplicitTypeInstantiationSyntax) - { - AstArray typeArguments; - CstTypeInstantiation* cstTypeArguments = options.storeCstData ? allocator.alloc() : nullptr; + AstArray typeArguments; + CstTypeInstantiation* cstTypeArguments = options.storeCstData ? allocator.alloc() : nullptr; - if (lexer.current().type == '<' && lexer.lookahead().type == '<') - { - typeArguments = parseTypeInstantiationExpr(cstTypeArguments); - } + if (lexer.current().type == '<' && lexer.lookahead().type == '<') + { + typeArguments = parseTypeInstantiationExpr(cstTypeArguments); + } - expr = parseFunctionArgs(func, true); + expr = parseFunctionArgs(func, true); - if (options.storeCstData) + if (options.storeCstData) + { + CstNode** cstNode = cstNodeMap.find(expr); + if (cstNode) { - CstNode** cstNode = cstNodeMap.find(expr); - if (cstNode) - { - CstExprCall* exprCall = (*cstNode)->as(); - LUAU_ASSERT(exprCall); - exprCall->explicitTypes = cstTypeArguments; - } + CstExprCall* exprCall = (*cstNode)->as(); + LUAU_ASSERT(exprCall); + exprCall->explicitTypes = cstTypeArguments; } - - // If we have an AstExprCall, fill in the type arguments - if (auto call = expr->as(); call && typeArguments.size > 0) - call->typeArguments = typeArguments; - } - else - { - expr = parseFunctionArgs(func, true); } + // If we have an AstExprCall, fill in the type arguments + if (auto call = expr->as(); call && typeArguments.size > 0) + call->typeArguments = typeArguments; + return expr; } @@ -3161,17 +3794,12 @@ AstExpr* Parser::parseAssertionExpr() if (lexer.current().type == Lexeme::DoubleColon) { - CstExprTypeAssertion* cstNode = nullptr; - if (options.storeCstData) - { - Position opPosition = lexer.current().location.begin; - cstNode = allocator.alloc(opPosition); - } + Position opPosition = lexer.current().location.begin; nextLexeme(); AstType* annotation = parseType(); AstExprTypeAssertion* node = allocator.alloc(Location(start, annotation->location), expr, annotation); if (options.storeCstData) - cstNodeMap[node] = cstNode; + cstNodeMap[node] = allocator.alloc(opPosition); return node; } else @@ -3186,6 +3814,14 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data, { LUAU_ASSERT(base == 2 || base == 16); + if (FFlag::LuauNoDuplicateBinaryPrefix) + { + // Some libc implementations accept an optional 0b prefix for base-2 parsing. + // Binary literals have already had their leading 0b stripped by us. + if (base == 2 && data[0] == '0' && (data[1] == 'b' || data[1] == 'B')) + return ConstantNumberParseResult::Malformed; + } + char* end = nullptr; unsigned long long value = strtoull(data, &end, base); @@ -3211,6 +3847,61 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data, return ConstantNumberParseResult::Ok; } +static ConstantNumberParseResult parseInteger64(int64_t& result, const char* data, int base) +{ + LUAU_ASSERT(base == 2 || base == 10 || base == 16); + + char* end = nullptr; + + if (base == 10) + { + result = strtoll(data, &end, 10); + + if (end == data || *end != 'i' || end[1] != '\0') + return ConstantNumberParseResult::Malformed; + + if (((result == LLONG_MIN) || (result == LLONG_MAX)) && (errno == ERANGE)) + { + // 'errno' might have been set before we called 'strtoll', but we don't want the overhead of resetting a TLS variable on each call + // so we only reset it when we get a result that might be an out-of-range error and parse again to make sure + errno = 0; + result = strtoll(data, &end, 10); + + if (errno == ERANGE) + return ConstantNumberParseResult::IntOverflow; + } + } + else + { + if (FFlag::LuauNoDuplicateBinaryPrefix) + { + if (base == 2 && data[0] == '0' && (data[1] == 'b' || data[1] == 'B')) + return ConstantNumberParseResult::Malformed; + } + + // hex and binary literals represent bit patterns covering the full uint64 range + unsigned long long u = strtoull(data, &end, base); + + if (end == data || *end != 'i' || end[1] != '\0') + return ConstantNumberParseResult::Malformed; + + if ((u == ULLONG_MAX) && (errno == ERANGE)) + { + // 'errno' might have been set before we called 'strtoull', but we don't want the overhead of resetting a TLS variable on each call + // so we only reset it when we get a result that might be an out-of-range error and parse again to make sure + errno = 0; + u = strtoull(data, &end, base); + + if (errno == ERANGE) + return base == 2 ? ConstantNumberParseResult::BinOverflow : ConstantNumberParseResult::HexOverflow; + } + + result = (int64_t)u; + } + + return ConstantNumberParseResult::Ok; +} + static ConstantNumberParseResult parseDouble(double& result, const char* data) { // binary literal @@ -3244,23 +3935,33 @@ static ConstantNumberParseResult parseDouble(double& result, const char* data) return ConstantNumberParseResult::Ok; } +// LUAU_NOINLINE is used to limit the stack cost of parseSimpleExpr which is on the recursive expression-parsing path +LUAU_NOINLINE AstExpr* Parser::parseAttributedFunction(const Location& start) +{ + AstArray attributes{nullptr, 0}; + TempVector cstAttrLists(scratchCstAttrList); + + attributes = parseAttributes(&cstAttrLists); + + if (lexer.current().type != Lexeme::ReservedFunction) + { + return reportExprError(start, {}, "Expected 'function' declaration after attribute, but got %s instead", lexer.current().toString().c_str()); + } + + Lexeme matchFunction = lexer.current(); + nextLexeme(); + + return parseFunctionBody(false, matchFunction, AstName(), nullptr, attributes, false, &cstAttrLists).first; +} + // simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | [attributes] FUNCTION body | primaryexp AstExpr* Parser::parseSimpleExpr() { Location start = lexer.current().location; - AstArray attributes{nullptr, 0}; - if (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) { - attributes = parseAttributes(); - - if (lexer.current().type != Lexeme::ReservedFunction) - { - return reportExprError( - start, {}, "Expected 'function' declaration after attribute, but got %s instead", lexer.current().toString().c_str() - ); - } + return parseAttributedFunction(start); } if (lexer.current().type == Lexeme::ReservedNil) @@ -3286,7 +3987,7 @@ AstExpr* Parser::parseSimpleExpr() Lexeme matchFunction = lexer.current(); nextLexeme(); - return parseFunctionBody(false, matchFunction, AstName(), nullptr, attributes).first; + return parseFunctionBody(false, matchFunction, AstName(), nullptr, AstArray{nullptr, 0}, false, nullptr).first; } else if (lexer.current().type == Lexeme::Number) { @@ -3340,7 +4041,7 @@ AstExpr* Parser::parseSimpleExpr() } } -std::tuple, Location, Location> Parser::parseCallList(TempVector* commaPositions) +std::tuple, Location, Location> Parser::parseCallList(TempVector* commaPositions, Position* closeParenPosition) { LUAU_ASSERT( lexer.current().type == '(' || lexer.current().type == '{' || lexer.current().type == Lexeme::RawString || @@ -3361,7 +4062,9 @@ std::tuple, Location, Location> Parser::parseCallList(TempVec Location end = lexer.current().location; Position argEnd = end.end; - expectMatchAndConsume(')', matchParen); + bool closeParenFound = expectMatchAndConsume(')', matchParen); + if (closeParenPosition && closeParenFound) + *closeParenPosition = end.begin; return {copy(args), Location(argStart, argEnd), Location(matchParen.position, lexer.previousLocation().begin)}; } @@ -3402,13 +4105,15 @@ AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self) Location end = lexer.current().location; Position argEnd = end.end; - expectMatchAndConsume(')', matchParen); + bool closingParenFound = expectMatchAndConsume(')', matchParen); AstExprCall* node = allocator.alloc( Location(func->location, end), func, copy(args), self, AstArray{}, Location(argStart, argEnd) ); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(matchParen.position, lexer.previousLocation().begin, copy(commaPositions)); + cstNodeMap[node] = allocator.alloc( + matchParen.position, closingParenFound ? lexer.previousLocation().begin : Position::missing(), copy(commaPositions) + ); return node; } else if (lexer.current().type == '{') @@ -3421,7 +4126,7 @@ AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self) Location(func->location, expr->location), func, copy(&expr, 1), self, AstArray{}, Location(argStart, argEnd) ); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(std::nullopt, std::nullopt, AstArray{nullptr, 0}); + cstNodeMap[node] = allocator.alloc(Position::missing(), Position::missing(), AstArray{nullptr, 0}); return node; } else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString) @@ -3433,7 +4138,7 @@ AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self) Location(func->location, expr->location), func, copy(&expr, 1), self, AstArray{}, argLocation ); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(std::nullopt, std::nullopt, AstArray{nullptr, 0}); + cstNodeMap[node] = allocator.alloc(Position::missing(), Position::missing(), AstArray{nullptr, 0}); return node; } else @@ -3468,14 +4173,14 @@ LUAU_NOINLINE void Parser::reportAmbiguousCallError() ); } -std::optional Parser::tableSeparator() +CstExprTable::Separator Parser::tableSeparator() { if (lexer.current().type == ',') - return CstExprTable::Comma; + return CstExprTable::Separator::Comma; else if (lexer.current().type == ';') - return CstExprTable::Semicolon; + return CstExprTable::Separator::Semicolon; else - return std::nullopt; + return CstExprTable::Separator::Missing; } // tableconstructor ::= `{' [fieldlist] `}' @@ -3491,12 +4196,9 @@ AstExpr* Parser::parseTableConstructor() MatchLexeme matchBrace = lexer.current(); expectAndConsume('{', "table literal"); - unsigned lastElementIndent = 0; while (lexer.current().type != '}') { - lastElementIndent = lexer.current().location.begin.column; - if (lexer.current().type == '[') { Position indexerOpenPosition = lexer.current().location.begin; @@ -3505,17 +4207,26 @@ AstExpr* Parser::parseTableConstructor() AstExpr* key = parseExpr(); - Position indexerClosePosition = lexer.current().location.begin; - expectMatchAndConsume(']', matchLocationBracket); + bool closingBracketFound = expectMatchAndConsume(']', matchLocationBracket); + Position indexerClosePosition = closingBracketFound ? lexer.previousLocation().begin : Position::missing(); - Position equalsPosition = lexer.current().location.begin; - expectAndConsume('=', "table field"); + bool equalsFound = expectAndConsume('=', "table field"); + Position equalsPosition = equalsFound ? lexer.previousLocation().begin : Position::missing(); AstExpr* value = parseExpr(); - items.push_back({AstExprTable::Item::General, key, value}); + items.push_back({AstExprTable::Item::Kind::General, key, value}); if (options.storeCstData) - cstItems.push_back({indexerOpenPosition, indexerClosePosition, equalsPosition, tableSeparator(), lexer.current().location.begin}); + { + CstExprTable::Separator separator = tableSeparator(); + cstItems.push_back( + {indexerOpenPosition, + indexerClosePosition, + equalsPosition, + separator, + separator == CstExprTable::Separator::Missing ? Position::missing() : lexer.current().location.begin} + ); + } } else if (lexer.current().type == Lexeme::Name && lexer.lookahead().type == '=') { @@ -3528,30 +4239,48 @@ AstExpr* Parser::parseTableConstructor() nameString.data = const_cast(name.name.value); nameString.size = strlen(name.name.value); - AstExpr* key = allocator.alloc(name.location, nameString, AstExprConstantString::Unquoted); + AstExpr* key = allocator.alloc(name.location, nameString, AstExprConstantString::QuoteStyle::Unquoted); AstExpr* value = parseExpr(); if (AstExprFunction* func = value->as()) func->debugname = name.name; - items.push_back({AstExprTable::Item::Record, key, value}); + items.push_back({AstExprTable::Item::Kind::Record, key, value}); if (options.storeCstData) - cstItems.push_back({std::nullopt, std::nullopt, equalsPosition, tableSeparator(), lexer.current().location.begin}); + { + CstExprTable::Separator separator = tableSeparator(); + cstItems.push_back( + {Position::missing(), + Position::missing(), + equalsPosition, + separator, + separator == CstExprTable::Separator::Missing ? Position::missing() : lexer.current().location.begin} + ); + } } else { AstExpr* expr = parseExpr(); - items.push_back({AstExprTable::Item::List, nullptr, expr}); + items.push_back({AstExprTable::Item::Kind::List, nullptr, expr}); if (options.storeCstData) - cstItems.push_back({std::nullopt, std::nullopt, std::nullopt, tableSeparator(), lexer.current().location.begin}); + { + CstExprTable::Separator separator = tableSeparator(); + cstItems.push_back( + {Position::missing(), + Position::missing(), + Position::missing(), + separator, + separator == CstExprTable::Separator::Missing ? Position::missing() : lexer.current().location.begin} + ); + } } if (lexer.current().type == ',' || lexer.current().type == ';') { nextLexeme(); } - else if ((lexer.current().type == '[' || lexer.current().type == Lexeme::Name) && lexer.current().location.begin.column == lastElementIndent) + else if (lexer.current().type == '[' || lexer.current().type == Lexeme::Name) { report(lexer.current().location, "Expected ',' after table constructor element"); } @@ -3581,8 +4310,8 @@ AstExpr* Parser::parseIfElseExpr() AstExpr* condition = parseExpr(); - Position thenPosition = lexer.current().location.begin; bool hasThen = expectAndConsume(Lexeme::ReservedThen, "if then else expression"); + Position thenPosition = hasThen ? lexer.previousLocation().begin : Position::missing(); AstExpr* trueExpr = parseExpr(); AstExpr* falseExpr = nullptr; @@ -3691,11 +4420,14 @@ std::pair, AstArray> Parser::pars { seenPack = true; - Position ellipsisPosition = lexer.current().location.begin; + Position ellipsisPosition = Position::missing(); if (lexer.current().type != Lexeme::Dot3) report(lexer.current().location, "Generic types come before generic type packs"); else + { + ellipsisPosition = lexer.current().location.begin; nextLexeme(); + } if (withDefaultValues && lexer.current().type == '=') { @@ -3732,7 +4464,7 @@ std::pair, AstArray> Parser::pars AstGenericTypePack* node = allocator.alloc(nameLocation, name, nullptr); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(ellipsisPosition, std::nullopt); + cstNodeMap[node] = allocator.alloc(ellipsisPosition, Position::missing()); namePacks.push_back(node); } } @@ -3758,7 +4490,7 @@ std::pair, AstArray> Parser::pars AstGenericType* node = allocator.alloc(nameLocation, name, nullptr); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(std::nullopt); + cstNodeMap[node] = allocator.alloc(Position::missing()); names.push_back(node); } } @@ -3779,9 +4511,9 @@ std::pair, AstArray> Parser::pars break; } - if (closePosition) - *closePosition = lexer.current().location.begin; - expectMatchAndConsume('>', begin); + bool closingBracketFound = expectMatchAndConsume('>', begin); + if (closePosition && closingBracketFound) + *closePosition = lexer.previousLocation().begin; } if (commaPositions) @@ -3845,9 +4577,25 @@ AstArray Parser::parseTypeParams(Position* openingPosition, TempV // (&, |, ?), then assume that this was actually a // parenthesized type. auto parenthesizedType = explicitTypePack->typeList.types.data[0]; - parameters.push_back( - {parseTypeSuffix(allocator.alloc(parenthesizedType->location, parenthesizedType), begin), {}} - ); + + AstTypeGroup* typeGroup = allocator.alloc(parenthesizedType->location, parenthesizedType); + + if (options.storeCstData) + { + CstNode** cstNode = cstNodeMap.find(explicitTypePack); + + LUAU_ASSERT(cstNode && *cstNode); + if (cstNode && *cstNode) + { + CstTypePackExplicit* cstExplicitTypePack = (*cstNode)->as(); + LUAU_ASSERT(cstExplicitTypePack); + + if (cstExplicitTypePack) + cstNodeMap[typeGroup] = allocator.alloc(cstExplicitTypePack->closeParenthesesPosition); + } + } + + parameters.push_back({parseTypeSuffix(typeGroup, begin), {}}); } else { @@ -3888,9 +4636,9 @@ AstArray Parser::parseTypeParams(Position* openingPosition, TempV break; } - if (closingPosition) - *closingPosition = lexer.current().location.begin; - expectMatchAndConsume('>', begin); + bool closingBracketFound = expectMatchAndConsume('>', begin); + if (closingPosition && closingBracketFound) + *closingPosition = lexer.previousLocation().begin; } return copy(parameters); @@ -3934,10 +4682,10 @@ AstExpr* Parser::parseString() { case Lexeme::QuotedString: case Lexeme::InterpStringSimple: - style = AstExprConstantString::QuotedSimple; + style = AstExprConstantString::QuoteStyle::QuotedSimple; break; case Lexeme::RawString: - style = AstExprConstantString::QuotedRaw; + style = AstExprConstantString::QuoteStyle::QuotedRaw; break; default: LUAU_ASSERT(false && "Invalid string type"); @@ -4099,10 +4847,35 @@ LUAU_NOINLINE AstExpr* Parser::parseExplicitTypeInstantiationExpr(Position start return expr; } -AstArray Parser::parseTypeInstantiationExpr(CstTypeInstantiation* cstNodeOut, Location* endLocationOut) +// classrefexp -> NAME { `.' NAME | `[' exp `]' } +AstExpr* Parser::parseClassRefExpr() { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); + Position start = lexer.current().location.begin; + AstExpr* name = parseNameExpr("class reference expression"); + + const Lexeme& dotOrBracket = lexer.current(); + if (dotOrBracket.type == '.') + { + nextLexeme(); + const Position& dotPosition = dotOrBracket.location.begin; + Parser::Name index = parseIndexName("class reference expression", dotPosition); + + return allocator.alloc(Location(start, index.location.end), name, index.name, index.location, dotPosition, '.'); + } + else if (dotOrBracket.type == '[') + { + nextLexeme(); + AstExpr* key = parseExpr(); + expectAndConsume(']', "class reference expression"); + return allocator.alloc(Location(start, lexer.previousLocation().end), name, key); + } + else + return name; +} + +AstArray Parser::parseTypeInstantiationExpr(CstTypeInstantiation* cstNodeOut, Location* endLocationOut) +{ LUAU_ASSERT(lexer.current().type == '<' && lexer.lookahead().type == '<'); if (cstNodeOut) @@ -4156,17 +4929,44 @@ AstExpr* Parser::parseNumber() scratchData.erase(std::remove(scratchData.begin(), scratchData.end(), '_'), scratchData.end()); } - double value = 0; - ConstantNumberParseResult result = parseDouble(value, scratchData.c_str()); - nextLexeme(); + if (FFlag::LuauIntegerType2 && (scratchData.back() == 'i')) + { + int64_t value = 0; + ConstantNumberParseResult result; + if ((strncmp(scratchData.c_str(), "0x", 2) == 0) || (strncmp(scratchData.c_str(), "0X", 2) == 0)) + result = parseInteger64(value, scratchData.c_str(), 16); // pass in '0x' prefix, it's handled by strtoll + else if ((strncmp(scratchData.c_str(), "0b", 2) == 0) || (strncmp(scratchData.c_str(), "0B", 2) == 0)) + result = parseInteger64(value, scratchData.c_str() + 2, 2); + else + result = parseInteger64(value, scratchData.c_str(), 10); - if (result == ConstantNumberParseResult::Malformed) - return reportExprError(start, {}, "Malformed number"); + nextLexeme(); - AstExprConstantNumber* node = allocator.alloc(start, value, result); - if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(sourceData); - return node; + if (result == ConstantNumberParseResult::Malformed) + return reportExprError(start, {}, "Malformed integer"); + + if (result != ConstantNumberParseResult::Ok) + return reportExprError(start, {}, "Integer overflow"); + + AstExprConstantInteger* node = allocator.alloc(start, value, result); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(sourceData); + return node; + } + else + { + double value = 0; + ConstantNumberParseResult result = parseDouble(value, scratchData.c_str()); + nextLexeme(); + + if (result == ConstantNumberParseResult::Malformed) + return reportExprError(start, {}, "Malformed number"); + + AstExprConstantNumber* node = allocator.alloc(start, value, result); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(sourceData); + return node; + } } AstLocal* Parser::pushLocal(const Binding& binding) @@ -4175,7 +4975,7 @@ AstLocal* Parser::pushLocal(const Binding& binding) AstLocal*& local = localMap[name.name]; local = allocator.alloc( - name.name, name.location, /* shadow= */ local, functionStack.size() - 1, functionStack.back().loopDepth, binding.annotation + name.name, name.location, /* shadow= */ local, functionStack.size() - 1, functionStack.back().loopDepth, binding.annotation, binding.isConst ); localStack.push_back(local); diff --git a/Ast/src/PrettyPrinter.cpp b/Ast/src/PrettyPrinter.cpp index f95fe1ab..1d23d52b 100644 --- a/Ast/src/PrettyPrinter.cpp +++ b/Ast/src/PrettyPrinter.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/PrettyPrinter.h" +#include "Luau/Cst.h" #include "Luau/Parser.h" #include "Luau/StringUtils.h" #include "Luau/Common.h" @@ -9,8 +10,8 @@ #include #include -LUAU_FASTFLAG(LuauExplicitTypeInstantiationSyntax) -LUAU_FASTFLAG(LuauCstStatDoWithStatsStart) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAG(LuauExportValueSyntax) namespace { @@ -190,7 +191,7 @@ struct StringWriter : Writer void sourceString(std::string_view s, CstExprConstantString::QuoteStyle quoteStyle, unsigned int blockDepth) override { - if (quoteStyle == CstExprConstantString::QuotedRaw) + if (quoteStyle == CstExprConstantString::QuoteStyle::QuotedRaw) { auto blocks = std::string(blockDepth, '='); write('['); @@ -208,13 +209,13 @@ struct StringWriter : Writer char quote = '"'; switch (quoteStyle) { - case CstExprConstantString::QuotedDouble: + case CstExprConstantString::QuoteStyle::QuotedDouble: quote = '"'; break; - case CstExprConstantString::QuotedSingle: + case CstExprConstantString::QuoteStyle::QuotedSingle: quote = '\''; break; - case CstExprConstantString::QuotedInterp: + case CstExprConstantString::QuoteStyle::QuotedInterp: quote = '`'; break; default: @@ -261,7 +262,7 @@ class CommaSeparatorInserter class ArgNameInserter { public: - ArgNameInserter(Writer& w, AstArray> names, AstArray> colonPositions) + ArgNameInserter(Writer& w, const AstArray>& names, const AstArray& colonPositions) : writer(w) , names(names) , colonPositions(colonPositions) @@ -278,10 +279,7 @@ class ArgNameInserter writer.advance(name->second.begin); writer.identifier(name->first.value); if (idx < colonPositions.size) - { - LUAU_ASSERT(colonPositions.data[idx].has_value()); - writer.advance(*colonPositions.data[idx]); - } + writer.advance(colonPositions.data[idx]); writer.symbol(":"); } } @@ -290,8 +288,8 @@ class ArgNameInserter private: Writer& writer; - AstArray> names; - AstArray> colonPositions; + const AstArray>& names; + const AstArray& colonPositions; size_t idx = 0; }; @@ -315,6 +313,18 @@ struct Printer return nullptr; } + // If pos has a value, advances to it and writes s. Otherwise does nothing. + void maybeAdvanceAndWrite(const Position& pos, std::string_view s, bool alwaysWrite = false) + { + if (pos.hasValue()) + { + advance(pos); + writer.write(s); + } + else if (alwaysWrite) + writer.write(s); + } + void visualize(const AstLocal& local, Position colonPosition) { advance(local.location.begin); @@ -322,13 +332,12 @@ struct Printer writer.identifier(local.name.value); if (writeTypes && local.annotation) { - advance(colonPosition); - writer.symbol(":"); + maybeAdvanceAndWrite(colonPosition, ":", true); visualizeTypeAnnotation(*local.annotation); } } - void visualizeTypePackAnnotation(AstTypePack& annotation, bool forVarArg, bool unconditionallyParenthesize = true) + void visualizeTypePackAnnotation(AstTypePack& annotation, bool forVarArg, bool unconditionallyParenthesize = true, bool forFunctionReturn = false) { advance(annotation.location.begin); if (const AstTypePackVariadic* variadicTp = annotation.as()) @@ -351,11 +360,18 @@ struct Printer if (const auto cstNode = lookupCstNode(explicitTp)) visualizeTypeList( explicitTp->typeList, - cstNode->hasParentheses, + /* unconditionallyParenthesize */ false, cstNode->openParenthesesPosition, cstNode->closeParenthesesPosition, cstNode->commaPositions ); + else if (forFunctionReturn) + { + size_t packSize = explicitTp->typeList.types.size + (explicitTp->typeList.tailType != nullptr ? 1 : 0); + visualizeTypeList( + explicitTp->typeList, /* unconditionallyParenthesize */ forFunctionReturn ? packSize != 1 : unconditionallyParenthesize + ); + } else visualizeTypeList(explicitTp->typeList, unconditionallyParenthesize); } @@ -368,32 +384,25 @@ struct Printer void visualizeNamedTypeList( const AstTypeList& list, bool unconditionallyParenthesize, - std::optional openParenthesesPosition, - std::optional closeParenthesesPosition, - AstArray commaPositions, - AstArray> argNames, - AstArray> argNamesColonPositions + Position openParenthesesPosition, + Position closeParenthesesPosition, + const AstArray& commaPositions, + const AstArray>& argNames, + const AstArray& argNamesColonPositions ) { size_t typeCount = list.types.size + (list.tailType != nullptr ? 1 : 0); if (typeCount == 0) { - if (openParenthesesPosition) - advance(*openParenthesesPosition); - writer.symbol("("); - if (closeParenthesesPosition) - advance(*closeParenthesesPosition); - writer.symbol(")"); + maybeAdvanceAndWrite(openParenthesesPosition, "(", unconditionallyParenthesize); + + maybeAdvanceAndWrite(closeParenthesesPosition, ")", unconditionallyParenthesize); } else if (typeCount == 1) { bool shouldParenthesize = unconditionallyParenthesize && (list.types.size == 0 || !list.types.data[0]->is()); - if (shouldParenthesize) - { - if (openParenthesesPosition) - advance(*openParenthesesPosition); - writer.symbol("("); - } + // bool shouldParenthesize = unconditionallyParenthesize && list.types.size != 1; // don't parenthesize singleton type packs + maybeAdvanceAndWrite(openParenthesesPosition, "(", shouldParenthesize); ArgNameInserter(writer, argNames, argNamesColonPositions)(); @@ -407,18 +416,11 @@ struct Printer visualizeTypeAnnotation(*list.types.data[0]); } - if (shouldParenthesize) - { - if (closeParenthesesPosition) - advance(*closeParenthesesPosition); - writer.symbol(")"); - } + maybeAdvanceAndWrite(closeParenthesesPosition, ")", shouldParenthesize); } else { - if (openParenthesesPosition) - advance(*openParenthesesPosition); - writer.symbol("("); + maybeAdvanceAndWrite(openParenthesesPosition, "(", unconditionallyParenthesize); CommaSeparatorInserter comma(writer, commaPositions.size > 0 ? commaPositions.begin() : nullptr); ArgNameInserter argName(writer, argNames, argNamesColonPositions); @@ -435,17 +437,15 @@ struct Printer visualizeTypePackAnnotation(*list.tailType, false); } - if (closeParenthesesPosition) - advance(*closeParenthesesPosition); - writer.symbol(")"); + maybeAdvanceAndWrite(closeParenthesesPosition, ")", unconditionallyParenthesize); } } void visualizeTypeList( const AstTypeList& list, bool unconditionallyParenthesize, - std::optional openParenthesesPosition = std::nullopt, - std::optional closeParenthesesPosition = std::nullopt, + Position openParenthesesPosition = Position::missing(), + Position closeParenthesesPosition = Position::missing(), AstArray commaPositions = {} ) { @@ -467,9 +467,16 @@ struct Printer if (const auto& a = expr.as()) { writer.symbol("("); + visualize(*a->expr); - advanceBefore(a->location.end, 1); - writer.symbol(")"); + + if (const auto cstNode = lookupCstNode(a)) + maybeAdvanceAndWrite(cstNode->closePosition, ")"); + else + { + advanceBefore(a->location.end, 1); + writer.symbol(")"); + } } else if (expr.is()) { @@ -512,6 +519,28 @@ struct Printer } } } + else if (const auto& a = expr.as()) + { + if (const auto cstNode = lookupCstNode(a)) + { + writer.literal(std::string_view(cstNode->value.data, cstNode->value.size)); + } + else + { + if (a->value >= 0) + { + char buffer[100]; + size_t len = snprintf(buffer, sizeof(buffer), "%lldi", (long long)a->value); + writer.literal(std::string_view{buffer, len}); + } + else + { + char buffer[100]; + size_t len = snprintf(buffer, sizeof(buffer), "0x%llxi", (unsigned long long)a->value); + writer.literal(std::string_view{buffer, len}); + } + } + } else if (const auto& a = expr.as()) { if (const auto cstNode = lookupCstNode(a)) @@ -541,26 +570,15 @@ struct Printer const auto cstNode = lookupCstNode(a); - if (FFlag::LuauExplicitTypeInstantiationSyntax) + if (writeTypes && (a->typeArguments.size > 0 || (cstNode && cstNode->explicitTypes))) { - if (writeTypes && (a->typeArguments.size > 0 || (cstNode && cstNode->explicitTypes))) - { - visualizeExplicitTypeInstantiation(a->typeArguments, cstNode && cstNode->explicitTypes ? cstNode->explicitTypes : nullptr); - } + visualizeExplicitTypeInstantiation(a->typeArguments, cstNode && cstNode->explicitTypes ? cstNode->explicitTypes : nullptr); } if (cstNode) - { - if (cstNode->openParens) - { - advance(*cstNode->openParens); - writer.symbol("("); - } - } + maybeAdvanceAndWrite(cstNode->openParens, "("); else - { writer.symbol("("); - } CommaSeparatorInserter comma(writer, cstNode ? cstNode->commaPositions.begin() : nullptr); for (const auto& arg : a->args) @@ -570,17 +588,9 @@ struct Printer } if (cstNode) - { - if (cstNode->closeParens) - { - advance(*cstNode->closeParens); - writer.symbol(")"); - } - } + maybeAdvanceAndWrite(cstNode->closeParens, ")"); else - { writer.symbol(")"); - } } else if (const auto& a = expr.as()) { @@ -593,21 +603,35 @@ struct Printer else if (const auto& a = expr.as()) { const auto cstNode = lookupCstNode(a); + visualize(*a->expr); + if (cstNode) - advance(cstNode->openBracketPosition); - writer.symbol("["); + maybeAdvanceAndWrite(cstNode->openBracketPosition, "["); + else + writer.symbol("["); + visualize(*a->index); + if (cstNode) - advance(cstNode->closeBracketPosition); - writer.symbol("]"); + maybeAdvanceAndWrite(cstNode->closeBracketPosition, "]"); + else + writer.symbol("]"); } else if (const auto& a = expr.as()) { - for (const auto& attribute : a->attributes) - visualizeAttribute(*attribute); - if (const auto cstNode = lookupCstNode(a)) - advance(cstNode->functionKeywordPosition); + if (const CstExprFunction* cstNode = lookupCstNode(a)) + { + visualizeAttributes(a->attributes, &cstNode->attrLists); + if (cstNode->functionKeywordPosition.hasValue()) + advance(cstNode->functionKeywordPosition); + } + else + { + for (const auto& attribute : a->attributes) + visualizeAttribute(*attribute); + } + writer.keyword("function"); visualizeFunctionBody(*a); } @@ -636,36 +660,46 @@ struct Printer switch (item.kind) { - case AstExprTable::Item::List: + case AstExprTable::Item::Kind::List: break; - case AstExprTable::Item::Record: + case AstExprTable::Item::Kind::Record: { const auto& value = item.key->as()->value; + advance(item.key->location.begin); + writer.identifier(std::string_view(value.data, value.size)); + if (cstItem) - advance(*cstItem->equalsPosition); + advance(cstItem->equalsPosition); else writer.maybeSpace(item.value->location.begin, 1); writer.symbol("="); } break; - case AstExprTable::Item::General: + case AstExprTable::Item::Kind::General: { if (cstItem) - advance(*cstItem->indexerOpenPosition); - writer.symbol("["); - visualize(*item.key); - if (cstItem) - advance(*cstItem->indexerClosePosition); - writer.symbol("]"); - if (cstItem) - advance(*cstItem->equalsPosition); + { + LUAU_ASSERT(cstItem->indexerOpenPosition.hasValue()); + maybeAdvanceAndWrite(cstItem->indexerOpenPosition, "[", true); + + visualize(*item.key); + + maybeAdvanceAndWrite(cstItem->indexerClosePosition, "]"); + + maybeAdvanceAndWrite(cstItem->equalsPosition, "="); + } else + { + writer.symbol("["); + visualize(*item.key); + writer.symbol("]"); writer.maybeSpace(item.value->location.begin, 1); - writer.symbol("="); + writer.symbol("="); + } } break; @@ -678,14 +712,10 @@ struct Printer if (cstItem) { - if (cstItem->separator) + if (cstItem->separator != CstExprTable::Separator::Missing) { - LUAU_ASSERT(cstItem->separatorPosition); - advance(*cstItem->separatorPosition); - if (cstItem->separator == CstExprTable::Comma) - writer.symbol(","); - else if (cstItem->separator == CstExprTable::Semicolon) - writer.symbol(";"); + LUAU_ASSERT(cstItem->separatorPosition.hasValue()); + maybeAdvanceAndWrite(cstItem->separatorPosition, cstItem->separator == CstExprTable::Separator::Comma ? "," : ";", true); } cstItem++; } @@ -707,13 +737,13 @@ struct Printer switch (a->op) { - case AstExprUnary::Not: + case AstExprUnary::Op::Not: writer.keyword("not"); break; - case AstExprUnary::Minus: + case AstExprUnary::Op::Minus: writer.symbol("-"); break; - case AstExprUnary::Len: + case AstExprUnary::Op::Len: writer.symbol("#"); break; } @@ -831,8 +861,6 @@ struct Printer } else if (const auto& a = expr.as()) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); - visualize(*a->expr); if (writeTypes) @@ -859,6 +887,7 @@ struct Printer void advance(const Position& newPos) { + LUAU_ASSERT(newPos.hasValue()); writer.advance(newPos); } @@ -876,44 +905,24 @@ struct Printer if (const auto& block = program.as()) { - if (FFlag::LuauCstStatDoWithStatsStart) + if (const auto cstNode = lookupCstNode(block)) { - if (const auto cstNode = lookupCstNode(block)) - { - writer.keyword("do"); - - advance(cstNode->statsStartPosition); + writer.keyword("do"); - for (const auto& s : block->body) - visualize(*s); + advance(cstNode->statsStartPosition); - advance(cstNode->endPosition); - writer.keyword("end"); - } - else - { - for (const auto& s : block->body) - visualize(*s); + for (const auto& s : block->body) + visualize(*s); - writer.advance(block->location.end); - writeEnd(program.location); - } + maybeAdvanceAndWrite(cstNode->endPosition, "end"); } else { - writer.keyword("do"); for (const auto& s : block->body) visualize(*s); - if (const auto cstNode = lookupCstNode(block)) - { - advance(cstNode->endPosition); - writer.keyword("end"); - } - else - { - writer.advance(block->location.end); - writeEnd(program.location); - } + + writer.advance(block->location.end); + writeEnd(program.location); } } else if (const auto& a = program.as()) @@ -937,10 +946,12 @@ struct Printer writer.keyword("repeat"); visualizeBlock(*a->body); if (const auto cstNode = lookupCstNode(a)) - writer.advance(cstNode->untilPosition); + maybeAdvanceAndWrite(cstNode->untilPosition, "until"); else + { advanceBefore(a->condition->location.begin, 6); - writer.keyword("until"); + writer.keyword("until"); + } visualize(*a->condition); } else if (program.is()) @@ -967,8 +978,23 @@ struct Printer else if (const auto& a = program.as()) { const auto cstNode = lookupCstNode(a); + if (FFlag::LuauExportValueSyntax && a->isExported) + { + writer.keyword("export"); - writer.keyword("local"); + if (a->keywordLocation.has_value()) + advance(a->keywordLocation->begin); + + writer.keyword(a->isConst ? "const" : "local"); + } + else if (a->isConst) + { + writer.keyword("const"); + } + else + { + writer.keyword("local"); + } CommaSeparatorInserter varComma(writer, cstNode ? cstNode->varsCommaPositions.begin() : nullptr); for (size_t i = 0; i < a->vars.size; i++) @@ -1003,21 +1029,27 @@ struct Printer writer.keyword("for"); - visualize(*a->var, cstNode ? cstNode->annotationColonPosition : Position{0, 0}); + visualize(*a->var, cstNode ? cstNode->annotationColonPosition : Position::missing()); if (cstNode) advance(cstNode->equalsPosition); writer.symbol("="); + visualize(*a->from); + if (cstNode) - advance(cstNode->endCommaPosition); - writer.symbol(","); + maybeAdvanceAndWrite(cstNode->endCommaPosition, ","); + else + writer.symbol(","); + visualize(*a->to); + if (a->step) { - if (cstNode && cstNode->stepCommaPosition) - advance(*cstNode->stepCommaPosition); + if (cstNode) + advance(cstNode->stepCommaPosition); writer.symbol(","); + visualize(*a->step); } advance(a->doLocation.begin); @@ -1077,10 +1109,12 @@ struct Printer } if (cstNode) - advance(cstNode->equalsPosition); + maybeAdvanceAndWrite(cstNode->equalsPosition, "="); else + { writer.space(); - writer.symbol("="); + writer.symbol("="); + } CommaSeparatorInserter valueComma(writer, cstNode ? cstNode->valuesCommaPositions.begin() : nullptr); for (const auto& value : a->values) @@ -1148,25 +1182,44 @@ struct Printer } else if (const auto& a = program.as()) { - for (const auto& attribute : a->func->attributes) - visualizeAttribute(*attribute); - if (const auto cstNode = lookupCstNode(a)) + if (const CstStatFunction* cstNode = lookupCstNode(a)) + { + visualizeAttributes(a->func->attributes, &cstNode->attrLists); advance(cstNode->functionKeywordPosition); + } + else + visualizeAttributes(a->func->attributes, nullptr); writer.keyword("function"); visualize(*a->name); visualizeFunctionBody(*a->func); } else if (const auto& a = program.as()) { - for (const auto& attribute : a->func->attributes) - visualizeAttribute(*attribute); - const auto cstNode = lookupCstNode(a); + if (cstNode) + visualizeAttributes(a->func->attributes, &cstNode->attrLists); + else + { + for (const auto& attribute : a->func->attributes) + visualizeAttribute(*attribute); + } + if (cstNode) advance(cstNode->localKeywordPosition); - writer.keyword("local"); + if (FFlag::LuauExportValueSyntax && a->name->isExported) + { + writer.keyword("export"); + } + else if (a->name->isConst) + { + writer.keyword("const"); + } + else + { + writer.keyword("local"); + } if (cstNode) advance(cstNode->functionKeywordPosition); @@ -1209,15 +1262,11 @@ struct Printer if (o->defaultValue) { - const auto* genericTypeCstNode = lookupCstNode(o); - - if (genericTypeCstNode) - { - LUAU_ASSERT(genericTypeCstNode->defaultEqualsPosition.has_value()); - advance(*genericTypeCstNode->defaultEqualsPosition); - } + if (const CstGenericType* genericTypeCstNode = lookupCstNode(o)) + advance(genericTypeCstNode->defaultEqualsPosition); else writer.maybeSpace(o->defaultValue->location.begin, 2); + writer.symbol("="); visualizeTypeAnnotation(*o->defaultValue); } @@ -1232,16 +1281,14 @@ struct Printer writer.advance(o->location.begin); writer.identifier(o->name.value); if (genericTypePackCstNode) - advance(genericTypePackCstNode->ellipsisPosition); - writer.symbol("..."); + maybeAdvanceAndWrite(genericTypePackCstNode->ellipsisPosition, "..."); + else + writer.symbol("..."); if (o->defaultValue) { if (cstNode) - { - LUAU_ASSERT(genericTypePackCstNode->defaultEqualsPosition.has_value()); - advance(*genericTypePackCstNode->defaultEqualsPosition); - } + advance(genericTypePackCstNode->defaultEqualsPosition); else writer.maybeSpace(o->defaultValue->location.begin, 2); writer.symbol("="); @@ -1250,14 +1297,18 @@ struct Printer } if (cstNode) - advance(cstNode->genericsClosePosition); - writer.symbol(">"); + maybeAdvanceAndWrite(cstNode->genericsClosePosition, ">"); + else + writer.symbol(">"); } if (cstNode) - advance(cstNode->equalsPosition); + maybeAdvanceAndWrite(cstNode->equalsPosition, "="); else + { writer.maybeSpace(a->type->location.begin, 2); - writer.symbol("="); + writer.symbol("="); + } + visualizeTypeAnnotation(*a->type); } } @@ -1310,6 +1361,57 @@ struct Printer writer.symbol(":"); visualizeTypeAnnotation(*a->type); } + else if (const auto& c = program.as(); c && FFlag::DebugLuauUserDefinedClasses) + { + writer.keyword("class"); + writer.advance(c->name->location.begin); + writer.identifier(c->name->name.value); + if (c->super) + { + writer.keyword("extends"); + visualize(*c->super); + } + + for (const auto& member : c->members) + { + visit( + overloaded{ + [&](const AstClassProperty& prop) + { + writer.advance(prop.qualifierLocation.begin); + writer.keyword("public"); + writer.advance(prop.nameLocation.begin); + writer.identifier(prop.name.value); + if (writeTypes && prop.ty) + { + LUAU_ASSERT(prop.typeColonLocation.has_value()); + writer.advance(prop.typeColonLocation->begin); + writer.symbol(":"); + visualizeTypeAnnotation(*prop.ty); + } + }, + [&](const AstClassMethod& method) + { + if (method.qualifierLocation) + { + writer.advance(method.qualifierLocation->begin); + writer.keyword("public"); + } + writer.advance(method.keywordLocation.begin); + writer.keyword("function"); + writer.advance(method.nameLocation.begin); + writer.identifier(method.functionName.value); + visualizeFunctionBody(*method.function); + } + }, + member + ); + } + + writer.newline(); + writer.keyword("end"); + writer.newline(); + } else { LUAU_ASSERT(!"Unknown AstStat"); @@ -1329,9 +1431,12 @@ struct Printer if (func.generics.size > 0 || func.genericPacks.size > 0) { CommaSeparatorInserter comma(writer, cstNode ? cstNode->genericsCommaPositions.begin() : nullptr); + if (cstNode) - advance(cstNode->openGenericsPosition); - writer.symbol("<"); + maybeAdvanceAndWrite(cstNode->openGenericsPosition, "<"); + else + writer.symbol("<"); + for (const auto& o : func.generics) { comma(); @@ -1339,6 +1444,7 @@ struct Printer writer.advance(o->location.begin); writer.identifier(o->name.value); } + for (const auto& o : func.genericPacks) { comma(); @@ -1349,9 +1455,11 @@ struct Printer advance(genericTypePackCstNode->ellipsisPosition); writer.symbol("..."); } + if (cstNode) - advance(cstNode->closeGenericsPosition); - writer.symbol(">"); + maybeAdvanceAndWrite(cstNode->closeGenericsPosition, ">"); + else + writer.symbol(">"); } if (func.argLocation) @@ -1372,9 +1480,11 @@ struct Printer if (cstNode) { LUAU_ASSERT(cstNode->argsAnnotationColonPositions.size > i); - advance(cstNode->argsAnnotationColonPositions.data[i]); + maybeAdvanceAndWrite(cstNode->argsAnnotationColonPositions.data[i], ":"); } - writer.symbol(":"); + else + writer.symbol(":"); + visualizeTypeAnnotation(*local->annotation); } } @@ -1388,11 +1498,10 @@ struct Printer if (func.varargAnnotation) { if (cstNode) - { - LUAU_ASSERT(cstNode->varargAnnotationColonPosition != Position({0, 0})); - advance(cstNode->varargAnnotationColonPosition); - } - writer.symbol(":"); + maybeAdvanceAndWrite(cstNode->varargAnnotationColonPosition, ":"); + else + writer.symbol(":"); + visualizeTypePackAnnotation(*func.varargAnnotation, true); } } @@ -1404,12 +1513,14 @@ struct Printer if (writeTypes && func.returnAnnotation != nullptr) { if (cstNode) - advance(cstNode->returnSpecifierPosition); - writer.symbol(":"); + maybeAdvanceAndWrite(cstNode->returnSpecifierPosition, ":"); + else + writer.symbol(":"); if (!cstNode) writer.space(); - visualizeTypePackAnnotation(*func.returnAnnotation, false, false); + + visualizeTypePackAnnotation(*func.returnAnnotation, false, false, true); } visualizeBlock(*func.body); @@ -1470,8 +1581,10 @@ struct Printer visualize(*elseif.condition); if (cstNode) - advance(cstNode->thenPosition); - writer.keyword("then"); + maybeAdvanceAndWrite(cstNode->thenPosition, "then"); + else + writer.keyword("then"); + visualize(*elseif.trueExpr); if (elseif.falseExpr) @@ -1494,20 +1607,85 @@ struct Printer void visualizeAttribute(AstAttr& attribute) { advance(attribute.location.begin); - switch (attribute.type) - { - case AstAttr::Checked: - writer.keyword("@checked"); - break; - case AstAttr::Native: - writer.keyword("@native"); - break; - case AstAttr::Deprecated: - writer.keyword("@deprecated"); - break; - case AstAttr::Unknown: - writer.keyword("@" + std::string{attribute.name.value}); - break; + if (const CstAttr* cstNode = lookupCstNode(&attribute)) + { + if (cstNode->hasAt) + writer.symbol("@"); + writer.identifier(attribute.name.value); + } + else if (const CstParametrizedAttr* cstParamNode = lookupCstNode(&attribute)) + { + writer.identifier(attribute.name.value); + + maybeAdvanceAndWrite(cstParamNode->openParenPosition, "("); + + const size_t commaPositionSize = cstParamNode->argsCommaPositions.size; + + for (size_t i = 0; i < attribute.args.size; ++i) + { + visualize(*attribute.args.data[i]); + if (i < commaPositionSize) + maybeAdvanceAndWrite(cstParamNode->argsCommaPositions.data[i], ","); + } + + maybeAdvanceAndWrite(cstParamNode->closeParenPosition, ")"); + } + else + { + writer.symbol("@"); + writer.identifier(attribute.name.value); + } + } + + void visualizeAttributes(const AstArray& attributes, const AstArray* attrLists) + { + if (attrLists == nullptr) + { + for (const auto& attribute : attributes) + visualizeAttribute(*attribute); + return; + } + + auto currentAttribute = attributes.begin(); + auto currentAttrList = attrLists->begin(); + + const auto attributesEnd = attributes.end(); + const auto attrListsEnd = attrLists->end(); + + while (currentAttribute != attributesEnd || currentAttrList != attrListsEnd) + { + if (currentAttrList == attrListsEnd || (*currentAttribute)->location.begin < (*currentAttrList)->atBracketPosition) + { + visualizeAttribute(**currentAttribute); + ++currentAttribute; + } + else + { + const CstAttrList* cstAttrList = *currentAttrList; + // Start of attribute list + advance(cstAttrList->atBracketPosition); + writer.symbol("@["); + + for (const Position& commaPosition : cstAttrList->commaPositions) + { + LUAU_ASSERT(currentAttribute != attributesEnd); + LUAU_ASSERT((*currentAttribute)->location.begin < commaPosition); + + visualizeAttribute(**currentAttribute); + ++currentAttribute; + + advance(commaPosition); + writer.symbol(","); + } + + LUAU_ASSERT(currentAttribute != attributesEnd); + visualizeAttribute(**currentAttribute); + ++currentAttribute; + + maybeAdvanceAndWrite(cstAttrList->closeBracketPosition, "]"); + + ++currentAttrList; + } } } @@ -1522,7 +1700,7 @@ struct Printer { writer.write(a->prefix->value); if (cstNode) - advance(*cstNode->prefixPointPosition); + advance(cstNode->prefixPointPosition); writer.symbol("."); } @@ -1531,9 +1709,11 @@ struct Printer if (a->parameters.size > 0 || a->hasParameterList) { CommaSeparatorInserter comma(writer, cstNode ? cstNode->parametersCommaPositions.begin() : nullptr); + if (cstNode) advance(cstNode->openParametersPosition); writer.symbol("<"); + for (auto o : a->parameters) { comma(); @@ -1543,9 +1723,11 @@ struct Printer else visualizeTypePackAnnotation(*o.typePack, false); } + if (cstNode) - advance(cstNode->closeParametersPosition); - writer.symbol(">"); + maybeAdvanceAndWrite(cstNode->closeParametersPosition, ">"); + else + writer.symbol(">"); } } else if (const auto& a = typeAnnotation.as()) @@ -1558,6 +1740,7 @@ struct Printer if (cstNode) advance(cstNode->openGenericsPosition); writer.symbol("<"); + for (const auto& o : a->generics) { comma(); @@ -1565,6 +1748,7 @@ struct Printer writer.advance(o->location.begin); writer.identifier(o->name.value); } + for (const auto& o : a->genericPacks) { comma(); @@ -1575,27 +1759,28 @@ struct Printer advance(genericTypePackCstNode->ellipsisPosition); writer.symbol("..."); } + if (cstNode) - advance(cstNode->closeGenericsPosition); - writer.symbol(">"); + maybeAdvanceAndWrite(cstNode->closeGenericsPosition, ">"); + else + writer.symbol(">"); } - { - visualizeNamedTypeList( - a->argTypes, - true, - cstNode ? std::make_optional(cstNode->openArgsPosition) : std::nullopt, - cstNode ? std::make_optional(cstNode->closeArgsPosition) : std::nullopt, - cstNode ? cstNode->argumentsCommaPositions : Luau::AstArray{}, - a->argNames, - cstNode ? cstNode->argumentNameColonPositions : Luau::AstArray>{} - ); - } + visualizeNamedTypeList( + a->argTypes, + cstNode == nullptr, + cstNode ? cstNode->openArgsPosition : Position::missing(), + cstNode ? cstNode->closeArgsPosition : Position::missing(), + cstNode ? cstNode->argumentsCommaPositions : Luau::AstArray{}, + a->argNames, + cstNode ? cstNode->argumentNameColonPositions : Luau::AstArray{} + ); if (cstNode) advance(cstNode->returnArrowPosition); writer.symbol("->"); - visualizeTypePackAnnotation(*a->returnTypes, false); + + visualizeTypePackAnnotation(*a->returnTypes, false, cstNode == nullptr); } else if (const auto& a = typeAnnotation.as()) { @@ -1621,9 +1806,8 @@ struct Printer { const AstTableProp* prop = a->props.begin(); - for (size_t i = 0; i < cstNode->items.size; ++i) + for (const CstTypeTable::Item& item : cstNode->items) { - CstTypeTable::Item item = cstNode->items.data[i]; // we store indexer as part of items to preserve property ordering if (item.kind == CstTypeTable::Item::Kind::Indexer) { @@ -1638,21 +1822,19 @@ struct Printer advance(item.indexerOpenPosition); writer.symbol("["); + visualizeTypeAnnotation(*a->indexer->indexType); - advance(item.indexerClosePosition); - writer.symbol("]"); - advance(item.colonPosition); - writer.symbol(":"); + + maybeAdvanceAndWrite(item.indexerClosePosition, "]"); + + maybeAdvanceAndWrite(item.colonPosition, ":"); + visualizeTypeAnnotation(*a->indexer->resultType); - if (item.separator) + if (item.separator != CstExprTable::Separator::Missing) { - LUAU_ASSERT(item.separatorPosition); - advance(*item.separatorPosition); - if (item.separator == CstExprTable::Comma) - writer.symbol(","); - else if (item.separator == CstExprTable::Semicolon) - writer.symbol(";"); + LUAU_ASSERT(item.separatorPosition.hasValue()); + maybeAdvanceAndWrite(item.separatorPosition, item.separator == CstExprTable::Separator::Comma ? "," : ";", true); } } else @@ -1666,16 +1848,17 @@ struct Printer if (item.kind == CstTypeTable::Item::Kind::StringProperty) { - advance(item.indexerOpenPosition); - writer.symbol("["); + LUAU_ASSERT(item.indexerOpenPosition.hasValue()); + maybeAdvanceAndWrite(item.indexerOpenPosition, "["); + advance(item.stringPosition); writer.sourceString( std::string_view(item.stringInfo->sourceString.data, item.stringInfo->sourceString.size), item.stringInfo->quoteStyle, item.stringInfo->blockDepth ); - advance(item.indexerClosePosition); - writer.symbol("]"); + + maybeAdvanceAndWrite(item.indexerClosePosition, "]"); } else { @@ -1683,18 +1866,14 @@ struct Printer writer.identifier(prop->name.value); } - advance(item.colonPosition); - writer.symbol(":"); + maybeAdvanceAndWrite(item.colonPosition, ":"); + visualizeTypeAnnotation(*prop->type); - if (item.separator) + if (item.separator != CstExprTable::Separator::Missing) { - LUAU_ASSERT(item.separatorPosition); - advance(*item.separatorPosition); - if (item.separator == CstExprTable::Comma) - writer.symbol(","); - else if (item.separator == CstExprTable::Semicolon) - writer.symbol(";"); + LUAU_ASSERT(item.separatorPosition.hasValue()); + maybeAdvanceAndWrite(item.separatorPosition, item.separator == CstExprTable::Separator::Comma ? "," : ";", true); } ++prop; @@ -1744,15 +1923,20 @@ struct Printer } else if (auto a = typeAnnotation.as()) { - const auto cstNode = lookupCstNode(a); writer.keyword("typeof"); - if (cstNode) - advance(cstNode->openPosition); - writer.symbol("("); - visualize(*a->expr); - if (cstNode) - advance(cstNode->closePosition); - writer.symbol(")"); + + if (const CstTypeTypeof* cstNode = lookupCstNode(a)) + { + maybeAdvanceAndWrite(cstNode->openPosition, "("); + visualize(*a->expr); + maybeAdvanceAndWrite(cstNode->closePosition, ")"); + } + else + { + writer.symbol("("); + visualize(*a->expr); + writer.symbol(")"); + } } else if (const auto& a = typeAnnotation.as()) { @@ -1786,11 +1970,8 @@ struct Printer } } - if (cstNode && cstNode->leadingPosition) - { - advance(*cstNode->leadingPosition); - writer.symbol("|"); - } + if (cstNode) + maybeAdvanceAndWrite(cstNode->leadingPosition, "|"); size_t separatorIndex = 0; for (size_t i = 0; i < a->types.size; ++i) @@ -1829,12 +2010,8 @@ struct Printer { const auto cstNode = lookupCstNode(a); - // If the sizes are equal, we know there is a leading & token - if (cstNode && cstNode->leadingPosition) - { - advance(*cstNode->leadingPosition); - writer.symbol("&"); - } + if (cstNode) + maybeAdvanceAndWrite(cstNode->leadingPosition, "&"); for (size_t i = 0; i < a->types.size; ++i) { @@ -1861,9 +2038,16 @@ struct Printer else if (const auto& a = typeAnnotation.as()) { writer.symbol("("); + visualizeTypeAnnotation(*a->type); - advanceBefore(a->location.end, 1); - writer.symbol(")"); + + if (const CstTypeGroup* cstNode = lookupCstNode(a)) + maybeAdvanceAndWrite(cstNode->closePosition, ")"); + else + { + advanceBefore(a->location.end, 1); + writer.symbol(")"); + } } else if (const auto& a = typeAnnotation.as()) { @@ -1892,19 +2076,15 @@ struct Printer void visualizeExplicitTypeInstantiation(const AstArray& typeArguments, const CstTypeInstantiation* cstNode) { - LUAU_ASSERT(FFlag::LuauExplicitTypeInstantiationSyntax); - if (cstNode) - { - advance(cstNode->leftArrow1Position); - } - writer.symbol("<"); + maybeAdvanceAndWrite(cstNode->leftArrow1Position, std::string_view("<")); + else + writer.symbol("<"); if (cstNode) - { - advance(cstNode->leftArrow2Position); - } - writer.symbol("<"); + maybeAdvanceAndWrite(cstNode->leftArrow2Position, std::string_view("<")); + else + writer.symbol("<"); CommaSeparatorInserter comma(writer, cstNode ? cstNode->commaPositions.begin() : nullptr); for (const auto& typeOrPack : typeArguments) @@ -1923,16 +2103,14 @@ struct Printer } if (cstNode) - { - advance(cstNode->rightArrow1Position); - } - writer.symbol(">"); + maybeAdvanceAndWrite(cstNode->rightArrow1Position, ">"); + else + writer.symbol(">"); if (cstNode) - { - advance(cstNode->rightArrow2Position); - } - writer.symbol(">"); + maybeAdvanceAndWrite(cstNode->rightArrow2Position, ">"); + else + writer.symbol(">"); } }; @@ -1966,6 +2144,11 @@ std::string prettyPrint(AstStatBlock& block, const CstNodeMap& cstNodeMap) return writer.str(); } +std::string prettyPrint(AstStatBlock& block) +{ + return prettyPrint(block, CstNodeMap{nullptr}); +} + std::string prettyPrintWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap) { StringWriter writer; @@ -1980,7 +2163,7 @@ std::string prettyPrintWithTypes(AstStatBlock& block) return prettyPrintWithTypes(block, CstNodeMap{nullptr}); } -PrettyPrintResult prettyPrint(std::string_view source, ParseOptions options, bool withTypes) +PrettyPrintResult prettyPrint(std::string_view source, ParseOptions options, bool withTypes, bool ignoreParseErrors) { options.storeCstData = true; @@ -1988,7 +2171,7 @@ PrettyPrintResult prettyPrint(std::string_view source, ParseOptions options, boo auto names = AstNameTable{allocator}; ParseResult parseResult = Parser::parse(source.data(), source.size(), names, allocator, std::move(options)); - if (!parseResult.errors.empty()) + if (!parseResult.errors.empty() && !ignoreParseErrors) { // PrettyPrintResult keeps track of only a single error const ParseError& error = parseResult.errors.front(); diff --git a/Compiler/include/Luau/BytecodeBuilder.h b/Bytecode/include/Luau/BytecodeBuilder.h similarity index 77% rename from Compiler/include/Luau/BytecodeBuilder.h rename to Bytecode/include/Luau/BytecodeBuilder.h index f3ee89e8..93720255 100644 --- a/Compiler/include/Luau/BytecodeBuilder.h +++ b/Bytecode/include/Luau/BytecodeBuilder.h @@ -41,15 +41,27 @@ class BytecodeBuilder static const unsigned int kMaxLength = 32; int32_t keys[kMaxLength]; + // constants are indices that correspond to the proto constant table + // if a key does not have an associated constant to fill in, it has a sentinel value of -1 + int32_t constants[kMaxLength]; unsigned int length = 0; + bool hasConstants = false; bool operator==(const TableShape& other) const; }; + struct ClassShape + { + int32_t className; + std::vector propertyNames; + std::vector methodNames; + }; + BytecodeBuilder(BytecodeEncoder* encoder = 0); + virtual ~BytecodeBuilder() = default; uint32_t beginFunction(uint8_t numparams, bool isvararg = false); - void endFunction(uint8_t maxstacksize, uint8_t numupvalues, uint8_t flags = 0); + void endFunction(uint8_t maxstacksize, uint8_t numupvalues, uint8_t flags = 0, uint64_t cost = 0); void setMainFunction(uint32_t fid); @@ -57,13 +69,18 @@ class BytecodeBuilder int32_t addConstantBoolean(bool value); int32_t addConstantInteger(int32_t value); int32_t addConstantNumber(double value); - int32_t addConstantVector(float x, float y, float z, float w); + int32_t addConstantInteger(int64_t value); + int32_t addConstantVectorf(float x, float y, float z, float w); + int32_t addConstantVectord(double x, double y, double z, double w); int32_t addConstantString(StringRef value); int32_t addImport(uint32_t iid); int32_t addConstantTable(const TableShape& shape); int32_t addConstantClosure(uint32_t fid); + uint32_t addFbSlot(LuauFeedbackType t); + int16_t addChildFunction(uint32_t fid); + int32_t addClassShape(ClassShape shape); void emitABC(LuauOpcode op, uint8_t a, uint8_t b, uint8_t c); void emitAD(LuauOpcode op, uint8_t a, int16_t d); @@ -77,8 +94,10 @@ class BytecodeBuilder [[nodiscard]] bool patchJumpD(size_t jumpLabel, size_t targetLabel); [[nodiscard]] bool patchSkipC(size_t jumpLabel, size_t targetLabel); + void patchAux(size_t targetAux, int32_t newValue); + void foldJumps(); - void expandJumps(); + std::vector expandJumps(); void setFunctionTypeInfo(std::string value); void pushLocalTypeInfo(LuauBytecodeType type, uint8_t reg, uint32_t startpc, uint32_t endpc); @@ -109,6 +128,7 @@ class BytecodeBuilder Dump_Locals = 1 << 3, Dump_Remarks = 1 << 4, Dump_Types = 1 << 5, + Dump_Constants = 1 << 6, }; void setDumpFlags(uint32_t flags) @@ -135,8 +155,25 @@ class BytecodeBuilder std::string dumpSourceRemarks() const; std::string dumpTypeInfo() const; + std::string getFunctionData(uint32_t id) + { + return functions[id].data; + } + + uint32_t getFunctionCount() const + { + return static_cast(functions.size()); + } + + std::vector getStringTable(); + void annotateInstruction(std::string& result, uint32_t fid, uint32_t instpos) const; + void clearStringTable() + { + stringTable.clear(); + } + static uint32_t getImportId(int32_t id0); static uint32_t getImportId(int32_t id0, int32_t id1); static uint32_t getImportId(int32_t id0, int32_t id1, int32_t id2); @@ -150,7 +187,7 @@ class BytecodeBuilder static uint8_t getVersion(); static uint8_t getTypeEncodingVersion(); -private: +protected: struct Constant { enum Type @@ -158,26 +195,29 @@ class BytecodeBuilder Type_Nil, Type_Boolean, Type_Number, - Type_Vector, + Type_Integer, + Type_Vectorf, + Type_Vectord, Type_String, Type_Import, Type_Table, Type_Closure, - // ServerLua: added by us for constant integer support. - Type_Integer, + Type_ClassShape, }; Type type; union { bool valueBoolean; - int32_t valueInteger; double valueNumber; - float valueVector[4]; + int64_t valueInteger64; + float valueVectorf[4]; + double valueVectord[4]; unsigned int valueString; // index into string table uint32_t valueImport; // 10-10-10-2 encoded import id uint32_t valueTable; // index into tableShapes[] uint32_t valueClosure; // index of function in global list + uint32_t valueClassShape; // index into classShapes[] }; }; @@ -185,13 +225,16 @@ class BytecodeBuilder { Constant::Type type; // Note: this stores value* from Constant; when type is Type_Number, this stores the same bits as double does but in uint64_t. - // For Type_Vector, x and y are stored in 'value' and z and w are stored in 'extra'. + // For Type_Vectorf, x and y are stored in 'value' and z and w are stored in 'extra1'. + // For Type_Vectord, x is stored in 'value', y, z and w are stored in 'extra1/2/3' accordingly. uint64_t value; - uint64_t extra = 0; + uint64_t extra1 = 0; + uint64_t extra2 = 0; + uint64_t extra3 = 0; bool operator==(const ConstantKey& key) const { - return type == key.type && value == key.value && extra == key.extra; + return type == key.type && value == key.value && extra1 == key.extra1 && extra2 == key.extra2 && extra3 == key.extra3; } }; @@ -282,6 +325,9 @@ class BytecodeBuilder std::vector jumps; std::vector tableShapes; + std::vector classShapes; + + std::vector fbSlots; bool hasLongJumps = false; @@ -319,16 +365,24 @@ class BytecodeBuilder void validate() const; void validateInstructions() const; void validateVariadic() const; + virtual void validateConst(int32_t cid) const; + virtual void validateConst(int32_t cid, Constant::Type constType) const; + virtual uint8_t validateProto(int32_t pid) const; + virtual uint8_t validateClosure(int32_t cid) const; void tagYieldPoints(); std::string dumpCurrentFunction(std::vector& dumpinstoffs) const; - void dumpConstant(std::string& result, int k) const; - void dumpInstruction(const uint32_t* opcode, std::string& output, int targetLabel) const; + virtual void dumpConstant(std::string& result, int k, bool detailed) const; + void dumpInstruction(const uint32_t* opcode, std::string& result, int targetLabel) const; + + int calcLinesSpan() const; + void fillBaselineInfo(int span, int* baseline, size_t baselineSize) const; - void writeFunction(std::string& ss, uint32_t id, uint8_t flags); + void writeFunction(std::string& ss, uint32_t id, uint8_t flags, uint64_t cost); void writeLineInfo(std::string& ss) const; void writeStringTable(std::string& ss) const; + void writeClassShape(std::string& ss, const ClassShape& cs) const; int32_t addConstant(const ConstantKey& key, const Constant& value); // ServerLua: we need this to be public! @@ -336,6 +390,7 @@ class BytecodeBuilder unsigned int addStringTableEntry(StringRef value); const char* tryGetUserdataTypeName(LuauBytecodeType type) const; + void clearState(); }; } // namespace Luau diff --git a/Bytecode/include/Luau/BytecodeCallInliner.h b/Bytecode/include/Luau/BytecodeCallInliner.h new file mode 100644 index 00000000..945e48ca --- /dev/null +++ b/Bytecode/include/Luau/BytecodeCallInliner.h @@ -0,0 +1,819 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeOps.h" +#include "Luau/DenseHash.h" + +#include +#include +#include + +namespace Luau +{ +namespace Bytecode +{ + +// Inliner limit is conservatively lower to avoid instructions like namecall that effectively use R(A+1) and CALL/RETURN interpreting 255 as -1 +constexpr uint8_t kMaxInlinerCombinedStackSize = 250; + +template +struct CallInliner +{ + BcFunction& caller; + BcFunction& target; + BcCallFB call; + std::vector callParams; + Reg targetReg; + uint32_t callerFbVecSize; + + uint32_t callerBlocksSizeBeforeInline = 0; + uint32_t callerInstSizeBeforeInline = 0; + uint32_t callerVmConstSizeBeforeInline = 0; + uint32_t callerProtoSizeBeforeInline = 0; + uint32_t callerUpValSizeBeforeInline = 0; + + std::vector returnOps; + std::unordered_set callProjections; + std::unordered_map, BcOpHash> varArgMoves; + // memoizes target-phi -> caller-phi so a target phi referenced both in a block's phi list and as + // another phi's operand maps to a single caller phi. Without this, the operand reference would get + // its own unanchored duplicate that SCCP never visits (it only visits phis listed in a block) + DenseHashMap mappedPhis{BcOp()}; + + CallInliner(BcFunction& caller, BcFunction& target, BcOp callOp, uint32_t callerFbVecSize) + : caller(caller) + , target(target) + , call(caller.template as>(callOp)) + , callParams(call.params()) + , targetReg(call.getOutReg()) + , callerFbVecSize(callerFbVecSize) + { + } + + bool hasEdge(const BcEdges& edges, BcBlockEdgeKind kind) + { + for (auto& e : edges) + if (e.kind == kind) + return true; + return false; + } + + void addSuccessor(BcRef from, BcRef to, BcBlockEdgeKind kind) + { + LUAU_ASSERT( + kind != BcBlockEdgeKind::Fallthrough || + (!hasEdge(from->successors, BcBlockEdgeKind::Fallthrough) && !hasEdge(to->predecessors, BcBlockEdgeKind::Fallthrough)) + ); + from->successors.push_back({kind, to.op}); + to->predecessors.push_back({kind, from.op}); + } + + std::pair, BcRef> splitBlockOnOp(BcOp splitOp) + { + // This function splits a block with an instruction in 3 parts: + // prevBlock - retains all ops before splitOp. it is original block to keep all existing + // references to block unaffected. Fallstrhough to insnsBlock. + // insnBlock - freashly created block containing only splitOp. Fallstrhough to nextBlock. + // nextBlock - optionally created if there are any instruction after splitOp. If new block is created + // all successor/predcessor relations are migrated to the new block. + BcRef insn = caller.inst(splitOp); + LUAU_ASSERT(insn->block.kind == BcOpKind::Block); + BcRef prevBlock = caller.block(insn->block); + LUAU_ASSERT(std::find(prevBlock->ops.begin(), prevBlock->ops.end(), splitOp) != prevBlock->ops.end()); + BcRef insnBlock = caller.block(caller.addBlock()); + insnBlock->sortkey = prevBlock->sortkey; + insnBlock->chainkey = prevBlock->chainkey + 1; + + BcRef nextBlock = caller.block(caller.addBlock()); + nextBlock->sortkey = insnBlock->sortkey; + nextBlock->chainkey = insnBlock->chainkey + 1; + while (prevBlock->ops.back() != splitOp) + { + BcRef insn = caller.inst(prevBlock->ops.back()); + prevBlock->ops.pop_back(); + nextBlock->ops.push_front(insn.op); + insn->block = nextBlock.op; + } + // Make all successors to point to next block. + for (BcBlockEdge& e : prevBlock->successors) + { + BcRef succ = caller.block(e.target); + for (BcBlockEdge& pred : succ->predecessors) + if (pred.target == prevBlock.op) + pred.target = nextBlock.op; + } + nextBlock->successors = prevBlock->successors; + prevBlock->successors.clear(); + + addSuccessor(prevBlock, insnBlock, BcBlockEdgeKind::Fallthrough); + addSuccessor(insnBlock, nextBlock, BcBlockEdgeKind::Fallthrough); + + LUAU_ASSERT(prevBlock->ops.back() == splitOp); + prevBlock->ops.pop_back(); + insnBlock->ops.push_back(splitOp); + caller.instOp(splitOp).block = insnBlock.op; + + return {prevBlock, nextBlock}; + } + + BcOp replaceNamecall(BcNamecall namecall, BcRef& prevBlock) + { + // move LOP_NAMECALL to call block + namecall.prependTo(call->block); + prevBlock->ops.pop_back(); + LUAU_ASSERT(targetReg == namecall.getOutReg()); + Reg tableReg = namecall.getOutReg() + 1; + + // and replace it with LOP_MOVE + LOP_GETTABLEKS + BcMove move = BcMove::create(caller); + move.setSrc(namecall.Table()); + move.setOutReg(tableReg); + move.appendTo(prevBlock.op); + + // GETTABLEKS can clobber original source register of NAMECALL and put a function closure there + // But MOVE target already has a table at this point. + namecall.setTable(move.op()); + + BcGetTableKS getTableKS = BcGetTableKS::create(caller); + getTableKS.setSource(move.op()); + getTableKS.setHint(namecall.Hint()); + getTableKS.setKey(namecall.Key().op.index); + getTableKS.setOutReg(targetReg); + getTableKS.appendTo(prevBlock.op); + + return getTableKS.op(); + } + + void appendCmpProto(BcRef& prevBlock, BcOp targetOp, uint32_t targetProtoId) + { + BcCmpProto cmpProto = BcCmpProto::create(caller); + cmpProto.setClosure(targetOp); + cmpProto.setProtoId(targetProtoId); + cmpProto.setFallback(call->block); + cmpProto.appendTo(prevBlock.op); + addSuccessor(prevBlock, caller.block(call->block), BcBlockEdgeKind::Branch); + } + + void allocateBlocks() + { + callerBlocksSizeBeforeInline = uint32_t(caller.blocks.size()); + caller.blocks.resize(callerBlocksSizeBeforeInline + target.blocks.size()); + } + + BcOp mapBlockOp(BcOp targetBlock) + { + LUAU_ASSERT(targetBlock.kind == BcOpKind::Block); + return BcOp{BcOpKind::Block, callerBlocksSizeBeforeInline + targetBlock.index}; + } + + void allocateInstructions() + { + callerInstSizeBeforeInline = uint32_t(caller.instructions.size()); + caller.instructions.resize(callerInstSizeBeforeInline + target.instructions.size()); + } + + BcOp mapInstOp(BcOp targetInst) + { + LUAU_ASSERT(targetInst.kind == BcOpKind::Inst); + return BcOp{BcOpKind::Inst, callerInstSizeBeforeInline + targetInst.index}; + } + + void allocateVmConsts() + { + callerVmConstSizeBeforeInline = uint32_t(caller.constants.size()); + caller.constants.reserve(callerVmConstSizeBeforeInline + target.constants.size()); + for (auto& c : target.constants) + caller.constants.push_back(c); + } + + BcOp mapVmConstOp(BcOp targetVmConst) + { + LUAU_ASSERT(targetVmConst.kind == BcOpKind::VmConst); + return BcOp{BcOpKind::VmConst, callerVmConstSizeBeforeInline + targetVmConst.index}; + } + + void allocateProtos() + { + callerProtoSizeBeforeInline = uint32_t(caller.protos.size()); + caller.protos.resize(callerProtoSizeBeforeInline + target.protos.size()); + } + + BcOp mapProtoOp(BcOp targetProtoOp) + { + LUAU_ASSERT(targetProtoOp.kind == BcOpKind::VmProto); + return BcOp{BcOpKind::VmProto, callerProtoSizeBeforeInline + targetProtoOp.index}; + } + + void allocateUpValues() + { + callerUpValSizeBeforeInline = caller.nups; + caller.nups += target.nups; + } + + BcOp mapUpValueOp(BcOp targetUpval) + { + LUAU_ASSERT(targetUpval.kind == BcOpKind::VmUpvalue); + return BcOp{BcOpKind::VmUpvalue, callerUpValSizeBeforeInline + targetUpval.index}; + } + + void findTargetCallProjections() + { + for (uint32_t i = 0; i < caller.projections.size(); i++) + { + BcProj& proj = caller.projections[i]; + if (proj.op == call.op()) + { + BcOp projOp{BcOpKind::Proj, i}; + if (callProjections.count(projOp) > 0) + continue; + returnOps.resize(proj.index + 1); + BcOp phiOp = caller.addPhi(); + BcRef phi = caller.phi(phiOp); + caller.addUse(phi, projOp); + callProjections.insert(projOp); + returnOps[proj.index] = phiOp; + } + } + } + + void setReturnOp(uint32_t idx, BcOp op) + { + if (idx >= returnOps.size()) + returnOps.resize(idx + 1); + + if (returnOps[idx].kind == BcOpKind::None) + returnOps[idx] = op; + else + { + if (returnOps[idx].kind != BcOpKind::Phi) + { + BcOp phiOp = caller.addPhi(); + BcRef phi = caller.phi(phiOp); + caller.addUse(phi, returnOps[idx]); + returnOps[idx] = phiOp; + } + else + { + BcRef phi = caller.phi(returnOps[idx]); + bool exists = false; + for (auto phiOp : phi->ops) + if (phiOp == op) + { + exists = true; + break; + } + + if (!exists) + caller.addUse(phi, op); + } + } + } + + bool replaceReturn(BcRef& nextBlock, BcOp callerBlockOp, BcOp targetReturnOp) + { + BcRef callerBlock = caller.block(callerBlockOp); + BcReturn ret = target.template as>(targetReturnOp); + if (ret.ReturnCount() < 0) + return false; + std::vector values = ret.values(); + uint32_t i = 0; + for (; i < values.size(); i++) + { + BcMove move = BcMove::create(caller); + move.setSrc(mapToCallerOp(values[i])); + move.setOutReg(targetReg + i); + move.appendTo(callerBlockOp); + setReturnOp(i, move.op()); + } + int callRes = call.ReturnCount(); + LUAU_ASSERT(callRes >= 0); + for (; i < static_cast(callRes); i++) + { + BcLoadNil loadNil = BcLoadNil::create(caller); + loadNil.setOutReg(targetReg + i); + loadNil.appendTo(callerBlockOp); + setReturnOp(i, loadNil.op()); + } + + callerBlock->successors.push_back({BcBlockEdgeKind::Fallthrough, nextBlock.op}); + nextBlock->predecessors.push_back({BcBlockEdgeKind::Fallthrough, callerBlockOp}); + return true; + } + + void replaceGetVarArg(BcOp callerBlockOp, BcOp targetGetVarArgsOp) + { + BcGetVarArgs getVarArgs = target.template as>(targetGetVarArgsOp); + int count = getVarArgs.ValuesCount(); + if (count < 0) + count = std::max(0, int(callParams.size() - target.numparams)); + std::vector moves; + for (int i = 0; i < count; i++) + { + if (static_cast(target.numparams + i) < callParams.size()) + { + BcMove move = BcMove::create(caller); + move.setSrc(callParams[target.numparams + i]); + move.setOutReg(mapToCallerReg(getVarArgs.startReg() + i)); + move.appendTo(callerBlockOp); + moves.push_back(move.op()); + } + else + { + BcLoadNil loadNil = BcLoadNil::create(caller); + loadNil.setOutReg(mapToCallerReg(getVarArgs.startReg() + i)); + loadNil.appendTo(callerBlockOp); + moves.push_back(loadNil.op()); + } + } + varArgMoves[targetGetVarArgsOp] = moves; + } + + BcOp getVarArgParam(BcGetVarArgs& getVarArgs, uint32_t idx) + { + LUAU_ASSERT(varArgMoves.count(getVarArgs.op()) > 0 && idx < varArgMoves[getVarArgs.op()].size()); + return varArgMoves[getVarArgs.op()][idx]; + } + + bool migrateBlocks(BcRef& nextBlock) + { + BcRef callBlock = caller.block(call->block); + uint32_t insnBlockSortKey = callBlock->sortkey; + uint32_t insnBlockChainKey = callBlock->chainkey; + uint32_t maxChainKey = 0; + for (uint32_t i = 0; i < target.blocks.size(); i++) + { + BcBlock& targetBlock = target.blocks[i]; + BcBlock& callerBlock = caller.blocks[callerBlocksSizeBeforeInline + i]; + BcOp callerBlockOp = BcOp{BcOpKind::Block, callerBlocksSizeBeforeInline + i}; + + if (i == target.exitBlock.index) + { + // it is old exit block + callerBlock.sortkey = kBlockNoStartPc; + callerBlock.flags |= BcBlockFlag::Dead; + continue; + } + callerBlock.sortkey = insnBlockSortKey; + callerBlock.chainkey = insnBlockChainKey + targetBlock.sortkey; + maxChainKey = std::max(callerBlock.chainkey, maxChainKey); + for (auto& e : targetBlock.successors) + if (e.target != target.exitBlock) + callerBlock.successors.push_back({e.kind, mapBlockOp(e.target)}); + + for (auto& e : targetBlock.predecessors) + callerBlock.predecessors.push_back({e.kind, mapBlockOp(e.target)}); + + for (auto phiOp : targetBlock.phis) + { + BcOp callerPhiOp = mapToCallerOp(phiOp); + callerBlock.phis.push_back(callerPhiOp); + } + + for (auto op : targetBlock.ops) + { + BcInst& inst = target.instOp(op); + if (inst.op == LOP_GETVARARGS) + { + replaceGetVarArg(callerBlockOp, op); + } + else if (inst.op == LOP_RETURN) + { + if (!replaceReturn(nextBlock, callerBlockOp, op)) + return false; + } + else + { + BcOp callerInstOp = mapInstOp(op); + callerBlock.appendInstruction(callerInstOp); + caller.instOp(callerInstOp).block = callerBlockOp; + } + } + } + callBlock->chainkey = maxChainKey + 1; + nextBlock->chainkey = maxChainKey + 2; + return true; + } + + BcOp mapToCallerOp(BcOp targetOp) + { + switch (targetOp.kind) + { + case BcOpKind::Inst: + return mapInstOp(targetOp); + case BcOpKind::Block: + return mapBlockOp(targetOp); + case BcOpKind::Imm: + { + caller.immediates.push_back(target.immOp(targetOp)); + return BcOp{BcOpKind::Imm, static_cast(caller.immediates.size() - 1)}; + } + case BcOpKind::Phi: + { + // memoize before recursing so a phi that (transitively) references itself, as loop-carried + // phis do, resolves to the same caller phi instead of recursing forever + if (auto it = mappedPhis.find(targetOp); it != nullptr) + return *it; + + BcOp callerPhiOp = caller.addPhi(); + mappedPhis[targetOp] = callerPhiOp; + BcRef targetPhi = target.phi(targetOp); + for (uint32_t i = 0; i < targetPhi->ops.size(); i++) + { + BcOp targetPhiOp = targetPhi->ops[i]; + BcOp mapped = mapToCallerOp(targetPhiOp); + BcRef callerPhi = caller.phi(callerPhiOp); + caller.addUse(callerPhi, mapped); + } + return callerPhiOp; + } + case BcOpKind::Proj: + { + BcRef proj = target.proj(targetOp); + if (target.is_vararg) + { + BcRef inst = target.inst(proj->op); + if (inst->op == LOP_GETVARARGS) + { + BcGetVarArgs getVarArgs = BcGetVarArgs::from(target, inst); + LUAU_ASSERT(getVarArgs.ValuesCount() >= 0); + return getVarArgParam(getVarArgs, proj->index); + } + } + return caller.addProj(mapToCallerOp(proj->op), proj->index); + } + case BcOpKind::VmReg: + if (targetOp.index < target.numparams) + { + // it is an argument for target. we can find it in the call's inputs + LUAU_ASSERT(targetOp.index < callParams.size()); + return callParams[targetOp.index]; + } + else + { + return BcOp{BcOpKind::VmReg, static_cast(mapToCallerReg(targetOp.index))}; + } + case BcOpKind::VmConst: + return mapVmConstOp(targetOp); + case BcOpKind::VmProto: + return mapProtoOp(targetOp); + case BcOpKind::VmUpvalue: + return mapUpValueOp(targetOp); + default: + return targetOp; + } + } + + Reg mapToCallerReg(Reg reg) + { + return targetReg + 1 + (target.is_vararg ? static_cast(callParams.size()) : 0) + reg; + } + + bool isMultiConsumer(BcFunction& graph, BcRef& inst) + { + switch (inst->op) + { + case LOP_SETLIST: + return BcSetList::from(graph, inst).Count() < 0; + case LOP_RETURN: + return BcReturn::from(graph, inst).ReturnCount() < 0; + case LOP_CALLFB: + return BcCallFB::from(graph, inst).ParamCount() < 0; + case LOP_CALL: + return BcCall::from(graph, inst).ParamCount() < 0; + default: + return false; + } + } + + void makeFixedConsumer(BcFunction& graph, BcRef& inst) + { + switch (inst->op) + { + case LOP_SETLIST: + { + auto setList = BcSetList::from(graph, inst); + setList.setCount(static_cast(setList.params().size())); + break; + } + case LOP_RETURN: + { + auto ret = BcReturn::from(graph, inst); + ret.setReturnCount(static_cast(ret.values().size())); + break; + } + case LOP_CALLFB: + { + auto callFb = BcCallFB::from(graph, inst); + callFb.setParamCount(static_cast(callFb.params().size())); + break; + } + case LOP_CALL: + { + auto call = BcCall::from(graph, inst); + call.setParamCount(static_cast(call.params().size())); + break; + } + default: + LUAU_UNREACHABLE(); + } + } + + bool isGetVarArg(BcOp targetOp) + { + if (targetOp.kind != BcOpKind::Inst) + return false; + BcRef inst = target.inst(targetOp); + return inst->op == LOP_GETVARARGS; + } + + void migrateInstructions() + { + for (uint32_t i = 0; i < target.instructions.size(); i++) + { + BcOp targetInsnOp{BcOpKind::Inst, i}; + BcOp callerInsnOp{BcOpKind::Inst, callerInstSizeBeforeInline + i}; + BcRef targetInst = target.inst(targetInsnOp); + BcRef callerInst = caller.inst(callerInsnOp); + + if (targetInst->op == LOP_RETURN || targetInst->op == LOP_GETVARARGS) + continue; + + callerInst->op = targetInst->op; + callerInst->block = mapBlockOp(targetInst->block); + callerInst->line = call->line; + + if (target.is_vararg && isMultiConsumer(target, targetInst) && isGetVarArg(targetInst->ops.back())) + { + for (BcOp inp : targetInst->ops) + { + if (inp != targetInst->ops.back()) + caller.addUse(callerInst, mapToCallerOp(inp)); + else + { + LUAU_ASSERT(varArgMoves.count(inp) > 0); + std::vector& moves = varArgMoves[inp]; + for (BcOp move : moves) + caller.addUse(callerInst, move); + } + } + makeFixedConsumer(caller, callerInst); + } + else + { + for (BcOp inp : targetInst->ops) + caller.addUse(callerInst, mapToCallerOp(inp)); + } + if (auto it = target.regs.find(targetInsnOp); it != target.regs.end()) + caller.regs[callerInsnOp] = mapToCallerReg(it->second); + // Instructions with special migration handling. + switch (callerInst->op) + { + case LOP_CALLFB: + { + // Feedback slots are concatenated in optimized version: caller's slots + target's slots. + // So all target's slot should be increased by caller's slots count. + BcCallFB fbcall = BcCallFB::from(caller, callerInst); + + // do not migrate sealed fbcalls + if (fbcall.FbSlot() != -1) + fbcall.setFbSlot(fbcall.FbSlot() + callerFbVecSize); + + break; + } + default: + break; + } + } + } + + void replaceCallUsagesInOps(BcOp consumer, BcOps& ops) + { + // It is safe to assume the call instruction is always referred as a projection, + // because inlining of only fixed return size calls are supported and parsers + // always emits projections for fixed return calls. + // If multireturn calls inlining will be supported in the future, it should account + // for bare call replacements as well. + for (BcOp& op : ops) + if (auto it = callProjections.find(op); it != callProjections.end()) + { + BcProj& proj = caller.projOp(*it); + LUAU_ASSERT(proj.index < returnOps.size()); + // the projection operand is rewritten in place, so the `ops` edge already exists; + // only the return def's reverse `uses` edge needs to be recorded + op = returnOps[proj.index]; + caller.recordUse(op, consumer); + } + } + + void replaceCallUsagesWithReturnPhis() + { + for (uint32_t i = 0; i < callerInstSizeBeforeInline; i++) + replaceCallUsagesInOps(BcOp{BcOpKind::Inst, i}, caller.instructions[i].ops); + + for (uint32_t i = 0; i < caller.phis.size(); i++) + if (std::find(returnOps.begin(), returnOps.end(), BcOp{BcOpKind::Phi, i}) == returnOps.end()) + replaceCallUsagesInOps(BcOp{BcOpKind::Phi, i}, caller.phis[i].ops); + } + + void dropPrepVarArgsInInlinedPath() + { + BcRef inlinedEntryBlock = caller.block(mapBlockOp(target.entryBlock)); + if (inlinedEntryBlock->ops.size() > 0 && caller.instOp(inlinedEntryBlock->ops.front()).op == LOP_PREPVARARGS) + inlinedEntryBlock->ops.pop_front(); + } + + void allocateGraphEntitiesForTarget() + { + allocateBlocks(); + allocateInstructions(); + allocateVmConsts(); + allocateProtos(); + allocateUpValues(); + } + + void setFallthrough(BcEdges& edges, BcOp entry) + { + for (auto& e : edges) + if (e.kind == BcBlockEdgeKind::Fallthrough) + { + e.target = entry; + return; + } + edges.push_back({BcBlockEdgeKind::Fallthrough, entry}); + } + + void fillUnderCallArguments() + { + if (callParams.size() >= target.numparams) + return; + + BcOp inlineEntryBlock = mapBlockOp(target.entryBlock); + size_t callParamSize = callParams.size(); + callParams.resize(target.numparams); + for (Reg param = target.numparams; param > callParamSize; param--) + { + BcLoadNil loadNil = BcLoadNil::create(caller); + loadNil.setOutReg(targetReg + param); + loadNil.prependTo(inlineEntryBlock); + callParams[param - 1] = loadNil.op(); + } + } + + bool inlineTarget(uint32_t targetProtoId) + { + LUAU_ASSERT(validate()); + uint32_t newMaxStackSize = static_cast(caller.maxstacksize) + static_cast(target.maxstacksize); + + if (target.is_vararg) + newMaxStackSize += uint32_t(callParams.size()); + + if (newMaxStackSize >= kMaxInlinerCombinedStackSize) + return false; + + if (call.ParamCount() < 0 || call.ReturnCount() < 0) + return false; + + // inlining of upvalues is not supported yet + if (target.nups > 0) + return false; + + caller.maxstacksize = newMaxStackSize; + + auto [prevBlock, nextBlock] = splitBlockOnOp(call.op()); + + BcOp targetOp = call.Target(); + if (prevBlock->ops.size() > 0) + { + auto lastInst = caller.inst(prevBlock->ops.back()); + if (lastInst->op == LOP_NAMECALL) + targetOp = replaceNamecall(caller.template as>(lastInst.op), prevBlock); + } + + appendCmpProto(prevBlock, targetOp, targetProtoId); + + // Seal FB slot of inlined call. + call.setFbSlot(-1); + + allocateGraphEntitiesForTarget(); + + fillUnderCallArguments(); + + findTargetCallProjections(); + + if (!migrateBlocks(nextBlock)) + return false; + + BcRef callerInlinedEntry = caller.block(mapBlockOp(target.entryBlock)); + + // Remove prevBlock fallthrough to call block from its predecessors + BcEdges& insnPreds = caller.block(call->block)->predecessors; + insnPreds.resize( + unsigned( + std::remove_if( + insnPreds.begin(), + insnPreds.end(), + [prevBlock = prevBlock](BcBlockEdge& p) + { + return p.kind == BcBlockEdgeKind::Fallthrough && p.target == prevBlock.op; + } + ) - + insnPreds.begin() + ) + ); + + setFallthrough(prevBlock->successors, callerInlinedEntry.op); + setFallthrough(callerInlinedEntry->predecessors, prevBlock.op); + + migrateInstructions(); + + replaceCallUsagesWithReturnPhis(); + + for (BcOp retOp : returnOps) + if (retOp.kind == BcOpKind::Phi) + nextBlock->phis.push_back(retOp); + + dropPrepVarArgsInInlinedPath(); + + LUAU_ASSERT(validate()); + + return true; + } + + bool validate() const + { + if (!validateCfg()) + return false; + if (!validatePhis()) + return false; + return true; + } + + bool validatePhis() const + { + for (BcPhi& phi : caller.phis) + for (BcOp op : phi.ops) + LUAU_ASSERT(op.kind == BcOpKind::Inst || op.kind == BcOpKind::VmReg || op.kind == BcOpKind::Proj || op.kind == BcOpKind::Phi); + return true; + } + + bool validateCfg() const + { + auto validateEdges = [&](uint32_t from, const BcEdges& edges, const BcEdges BcBlock::* mirrorDir) -> bool + { + for (const BcBlockEdge& edge : edges) + { + // In-range + if (edge.target.kind != BcOpKind::Block || edge.target.index >= caller.blocks.size()) + return false; + + const BcBlock& other = caller.blocks[edge.target.index]; + + // Alive + if ((other.flags & BcBlockFlag::Dead) != 0) + return false; + + // Outgoing edge is mirrored in the target + const BcEdges& mirror = other.*mirrorDir; + if (std::find_if( + mirror.begin(), + mirror.end(), + [&](const BcBlockEdge& e) + { + return e.kind == edge.kind && e.target.kind == BcOpKind::Block && e.target.index == from; + } + ) == mirror.end()) + return false; + } + + return true; + }; + + for (uint32_t i = 0; i < caller.blocks.size(); i++) + { + const BcBlock& block = caller.blocks[i]; + + // Skip dead blocks as they might be in inconsistent state + if ((block.flags & BcBlockFlag::Dead) != 0) + continue; + + if (!validateEdges(i, block.successors, &BcBlock::predecessors)) + return false; + + if (!validateEdges(i, block.predecessors, &BcBlock::successors)) + return false; + } + + return true; + } +}; + +template +bool inlineCall(BcFunction& caller, BcFunction& target, BcOp callOp, uint32_t targetProtoId, uint32_t callerFbVecSize = 0) +{ + CallInliner inliner(caller, target, callOp, callerFbVecSize); + return inliner.inlineTarget(targetProtoId); +} + +} // namespace Bytecode +} // namespace Luau diff --git a/Bytecode/include/Luau/BytecodeGraph.h b/Bytecode/include/Luau/BytecodeGraph.h new file mode 100644 index 00000000..c27b782e --- /dev/null +++ b/Bytecode/include/Luau/BytecodeGraph.h @@ -0,0 +1,640 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Bytecode.h" +#include "Luau/BytecodeBuilder.h" +#include "Luau/SmallVector.h" + +#include +#include +#include +#include +#include + +#include +#include + +namespace Luau +{ +namespace Bytecode +{ + +using Instruction = uint32_t; +using Reg = uint8_t; + +enum class BcOpKind : uint32_t +{ + None, + + // To reference a immediate value + Imm, + + // To reference a result of a previous instruction + Inst, + + // To reference a basic block in control flow + Block, + + // Phi operand + Phi, + + // Projection of multireturn call or variadic arguments + Proj, + + // To reference a VM register + VmReg, + + // To reference a VM constant + VmConst, + + // To reference a VM upvalue + VmUpvalue, + + // To reference a VM upvalue + VmProto, +}; + +struct BcOp +{ + BcOpKind kind : 4; + uint32_t index : 28; + + BcOp() + : kind(BcOpKind::None) + , index(0) + { + } + + BcOp(BcOpKind kind, uint32_t index) + : kind(kind) + , index(index) + { + } + + bool operator==(const BcOp& rhs) const + { + return kind == rhs.kind && index == rhs.index; + } + + bool operator!=(const BcOp& rhs) const + { + return !(*this == rhs); + } +}; + +static_assert(sizeof(BcOp) == 4); + +struct BcOpHash +{ + size_t operator()(const BcOp& p) const + { + size_t res = 0; + memcpy(&res, &p, sizeof(p)); + return res; + } +}; + +using RegMap = std::unordered_map; + +enum class BcImmKind : uint8_t +{ + Boolean, + Int, + Import +}; + +struct BcImm +{ + BcImmKind kind; + + union + { + bool valueBoolean; + int32_t valueInt; + uint32_t valueImport; + }; + + bool operator==(const BcImm& rhs) const + { + if (kind == BcImmKind::Boolean && rhs.kind == BcImmKind::Boolean) + return valueBoolean == rhs.valueBoolean; + else if (kind == BcImmKind::Int && rhs.kind == BcImmKind::Int) + return valueInt == rhs.valueInt; + else if (kind == BcImmKind::Import && rhs.kind == BcImmKind::Import) + return valueImport == rhs.valueImport; + else + return false; + } + + bool operator!=(const BcImm& rhs) const + { + return !(*this == rhs); + } +}; + +enum class BcVmConstKind : uint8_t +{ + Nil, + Boolean, + Number, + Vectorf, + Vectord, + String, + Import, + Table, + Closure, + Integer, + ClassShape +}; + +struct BcVmConst +{ + BcVmConstKind kind; + + union + { + bool valueBoolean; + double valueNumber; + float valueVectorf[4]; + double valueVectord[4]; + std::string_view valueString; + uint32_t valueImport; + uint32_t valueTable; + uint32_t valueClosure; + int64_t valueInteger; + uint32_t valueClassShape; + }; + + BcVmConst() + : kind(BcVmConstKind::Nil) + , valueBoolean(0) + { + } + + bool operator==(const BcVmConst& rhs) const + { + if (kind != rhs.kind) + return false; + + switch (kind) + { + case BcVmConstKind::Nil: + return true; + + case BcVmConstKind::Boolean: + return valueBoolean == rhs.valueBoolean; + + case BcVmConstKind::Number: + return valueNumber == rhs.valueNumber; + + case BcVmConstKind::Vectorf: + return valueVectorf[0] == rhs.valueVectorf[0] && valueVectorf[1] == rhs.valueVectorf[1] && valueVectorf[2] == rhs.valueVectorf[2] && + valueVectorf[3] == rhs.valueVectorf[3]; + + case BcVmConstKind::Vectord: + return valueVectord[0] == rhs.valueVectord[0] && valueVectord[1] == rhs.valueVectord[1] && valueVectord[2] == rhs.valueVectord[2] && + valueVectord[3] == rhs.valueVectord[3]; + + case BcVmConstKind::String: + return valueString == rhs.valueString; + + case BcVmConstKind::Import: + return valueImport == rhs.valueImport; + + case BcVmConstKind::Table: + return valueTable == rhs.valueTable; + + case BcVmConstKind::Closure: + return valueClosure == rhs.valueClosure; + + case BcVmConstKind::Integer: + return valueInteger == rhs.valueInteger; + + case BcVmConstKind::ClassShape: + return valueClassShape == rhs.valueClassShape; + + default: + LUAU_ASSERT(!"Unhandled BcVmConstKind"); + return false; + } + return false; + } + + bool operator!=(const BcVmConst& rhs) const + { + return !(*this == rhs); + } +}; + +using BcOps = SmallVector; + +struct BcInst +{ + LuauOpcode op; + BcOp block; + + // Operands + BcOps ops; + std::vector uses; + + uint32_t lastUse = 0; + uint32_t useCount = 0; + uint32_t line = 0; +}; + +// When IrInst operands are used, current instruction index is often required to track lifetime +inline constexpr uint32_t kInvalidInstIdx = ~0u; + +struct BcInstHash +{ + static const uint32_t m = 0x5bd1e995; + static const int r = 24; + + static uint32_t mix(uint32_t h, uint32_t k) + { + // MurmurHash2 step + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + return h; + } + + static uint32_t mix(uint32_t h, BcOp op) + { + static_assert(sizeof(op) == sizeof(uint32_t)); + uint32_t k; + memcpy(&k, &op, sizeof(op)); + + return mix(h, k); + } + + size_t operator()(const BcInst& key) const + { + // MurmurHash2 unrolled + uint32_t h = 25; + + h = mix(h, uint32_t(key.op)); + for (size_t i = 0; i < 7; i++) + h = mix(h, i < uint32_t(key.ops.size()) ? key.ops[i] : BcOp{}); + + // MurmurHash2 tail + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return h; + } +}; + +struct BcInstEq +{ + bool operator()(const BcInst& a, const BcInst& b) const + { + if (a.op != b.op || a.ops.size() != b.ops.size()) + return false; + for (size_t i = 0; i < a.ops.size(); i++) + if (a.ops[i] != b.ops[i]) + return false; + return true; + } +}; + +inline constexpr uint32_t kBlockNoStartPc = ~0u; + +enum BcBlockEdgeKind +{ + Branch, + Fallthrough, + Loop +}; + +struct BcBlockEdge +{ + BcBlockEdgeKind kind; + BcOp target; +}; + +using BcEdges = SmallVector; + +enum BcBlockFlag +{ + Dead = 1 << 0 +}; + +struct BcBlock +{ + uint8_t flags = 0; + uint32_t useCount = 0; + + std::list phis; + std::list ops; + BcEdges successors; + BcEdges predecessors; + + uint32_t sortkey = ~0u; + uint32_t chainkey = 0; + + // Bytecode PC position at which the block was generated + uint32_t startpc = kBlockNoStartPc; + + void appendInstruction(BcOp inst) + { + LUAU_ASSERT(inst.kind == BcOpKind::Inst); + ops.push_back(inst); + } +}; + +struct BcPhi +{ + BcOps ops; + std::vector uses; +}; + +struct BcProj +{ + BcOp op; + uint32_t index; +}; + +struct TypedLocal +{ + LuauBytecodeType type; + uint8_t reg; + uint32_t startpc; + uint32_t endpc; +}; + +struct DebugLocal +{ + std::string_view varname; + uint8_t reg; + uint32_t startpc; + uint32_t endpc; +}; + +template +struct BcRef +{ + std::vector& vec; + BcOp op; + + T* operator->() + { + LUAU_ASSERT(op.index < vec.size()); + return &vec[op.index]; + } + + T& operator*() + { + LUAU_ASSERT(op.index < vec.size()); + return vec[op.index]; + } +}; + +template +struct BcFunction +{ + uint8_t maxstacksize; + uint8_t numparams; + uint8_t nups; + bool is_vararg; + uint8_t flags; + + std::vector blocks; + std::vector instructions; + std::vector constants; + std::vector immediates; + std::vector phis; + std::vector projections; + std::vector tableShapes; + std::vector classShapes; + + BcOp entryBlock; + BcOp exitBlock; + + std::string typeInfo; + std::vector upvalueTypes; + std::vector localTypes; + std::vector protos; + + std::string debugname; + uint32_t linedefined; + std::vector upvalueNames; + std::vector locals; + + RegMap regs; + + BcOp addBlock() + { + blocks.emplace_back(BcBlock{}); + return BcOp{BcOpKind::Block, static_cast(blocks.size() - 1)}; + } + + BcOp addInst() + { + instructions.emplace_back(BcInst{}); + return BcOp{BcOpKind::Inst, static_cast(instructions.size() - 1)}; + } + + BcOp addPhi() + { + phis.emplace_back(BcPhi{}); + return BcOp{BcOpKind::Phi, static_cast(phis.size() - 1)}; + } + + BcOp addProj(BcOp op, uint32_t index) + { + projections.emplace_back(BcProj{op, index}); + return BcOp{BcOpKind::Proj, static_cast(projections.size() - 1)}; + } + + BcBlock& blockOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Block); + return blocks[op.index]; + } + + BcInst& instOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Inst); + return instructions[op.index]; + } + + BcInst* asInstOp(BcOp op) + { + if (op.kind == BcOpKind::Inst) + return &instructions[op.index]; + + return nullptr; + } + + BcImm& immOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Imm); + return immediates[op.index]; + } + + VmConst& constOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::VmConst); + return constants[op.index]; + } + + BcPhi& phiOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Phi); + return phis[op.index]; + } + + BcProj& projOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Proj); + return projections[op.index]; + } + + uint32_t getBlockIndex(const BcBlock& block) const + { + // Can only be called with blocks from our vector + LUAU_ASSERT(&block >= blocks.data() && &block <= blocks.data() + blocks.size()); + return uint32_t(&block - blocks.data()); + } + + uint32_t getInstIndex(const BcInst& inst) const + { + // Can only be called with instructions from our vector + LUAU_ASSERT(&inst >= instructions.data() && &inst <= instructions.data() + instructions.size()); + return uint32_t(&inst - instructions.data()); + } + + BcOp addImm(BcImmKind kind) + { + BcImm imm{kind}; + imm.valueInt = 0; + immediates.emplace_back(imm); + return BcOp{BcOpKind::Imm, static_cast(immediates.size() - 1)}; + } + + BcOp addImm(const BcImm& imm) + { + immediates.emplace_back(imm); + return BcOp{BcOpKind::Imm, static_cast(immediates.size() - 1)}; + } + + BcOp addConst(const VmConst& value) + { + constants.emplace_back(value); + return BcOp{BcOpKind::VmConst, static_cast(constants.size() - 1)}; + } + + BcRef block(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Block); + return {blocks, op}; + } + + BcRef inst(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Inst); + return {instructions, op}; + } + + template + T as(BcOp op) + { + BcRef insn = inst(op); + LUAU_ASSERT(insn->op == T::opcode); + + return T{*this, insn}; + } + + BcRef imm(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Imm); + return {immediates, op}; + } + + BcRef phi(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Phi); + return {phis, op}; + } + + BcRef proj(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Proj); + return {projections, op}; + } + + BcRef vmConst(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::VmConst); + return {constants, op}; + } + + void recordUse(BcOp usedOp, BcOp user) + { + if (usedOp.kind == BcOpKind::Inst) + this->instOp(usedOp).uses.push_back(user); + else if (usedOp.kind == BcOpKind::Phi) + this->phiOp(usedOp).uses.push_back(user); + } + + void addUse(BcRef instUser, BcOp usedOp) + { + instUser->ops.push_back(usedOp); + recordUse(usedOp, instUser.op); + } + + void addUse(BcRef phiUser, BcOp usedOp) + { + phiUser->ops.push_back(usedOp); + recordUse(usedOp, phiUser.op); + } + + void eraseUse(BcOp userOp, BcOp usedOp) + { + if (usedOp.kind == BcOpKind::Inst) + { + BcRef usedInst = inst(usedOp); + usedInst->uses.erase(std::remove(usedInst->uses.begin(), usedInst->uses.end(), userOp), usedInst->uses.end()); + } + else if (usedOp.kind == BcOpKind::Phi) + { + BcRef usedPhi = phi(usedOp); + usedPhi->uses.erase(std::remove(usedPhi->uses.begin(), usedPhi->uses.end(), userOp), usedPhi->uses.end()); + } + } + + void eraseOp(BcOp op) + { + BcRef instRef = inst(op); + BcRef blockRef = block(instRef->block); + blockRef->ops.erase(std::remove(blockRef->ops.begin(), blockRef->ops.end(), op), blockRef->ops.end()); + } + + // replace the instruction's operands while keeping def->use links consistent + void setOps(BcOp op, BcRef inst, std::initializer_list newOps) + { + for (BcOp oldOp : inst->ops) + eraseUse(op, oldOp); + inst->ops.clear(); + for (BcOp newOp : newOps) + { + inst->ops.push_back(newOp); + recordUse(newOp, op); + } + }; +}; + +using CompTimeBcFunction = BcFunction; + +std::optional fromFunctionBytecode(std::string bytecode, std::vector& strings); +std::string toFunctionBytecode(CompTimeBcFunction& fn); +std::string toFunctionBytecode(BytecodeBuilder& bcb, CompTimeBcFunction& fn); + +} // namespace Bytecode +} // namespace Luau diff --git a/Bytecode/include/Luau/BytecodeOps.h b/Bytecode/include/Luau/BytecodeOps.h new file mode 100644 index 00000000..03d413cc --- /dev/null +++ b/Bytecode/include/Luau/BytecodeOps.h @@ -0,0 +1,336 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" + +#include + +namespace Luau +{ +namespace Bytecode +{ + +template +struct BcInstHelper +{ + BcFunction& graph; + BcRef inst; + + static T create(BcFunction& graph) + { + BcOp op = graph.addInst(); + BcRef inst = graph.inst(op); + inst->op = T::opcode; + return {graph, inst}; + } + + static T from(BcFunction& graph, BcRef& inst) + { + LUAU_ASSERT(inst->op == T::opcode); + return {graph, inst}; + } + + BcInst* operator->() + { + return &inst.vec[inst.op.index]; + } + + BcOp op() + { + return inst.op; + } + + void prependTo(BcOp block) + { + inst->block = block; + graph.blockOp(block).ops.push_front(inst.op); + } + + void appendTo(BcOp block) + { + inst->block = block; + graph.blockOp(block).appendInstruction(inst.op); + } + + void insertBefore(BcRef op) + { + inst->block = op->block; + BcRef block = graph.block(op->block); + auto it = std::find(block->ops.begin(), block->ops.end(), op.op); + LUAU_ASSERT(it != block->ops.end()); + block->ops.insert(it, inst.op); + } + + void setOutReg(Reg out) + { + graph.regs[inst.op] = out; + } + + Reg getOutReg() + { + auto it = graph.regs.find(inst.op); + LUAU_ASSERT(it != graph.regs.end()); + return it->second; + } + +protected: + int intImmInput(uint32_t inputIdx) + { + LUAU_ASSERT(inputIdx < inst->ops.size()); + BcImm& imm = graph.immOp(inst->ops[inputIdx]); + return imm.valueInt; + } + + void setImmInput(uint32_t inputIdx, int value) + { + if (getBcOp(inputIdx).kind == BcOpKind::None) + inst->ops[inputIdx] = graph.addImm(BcImmKind::Int); + BcImm& imm = graph.immOp(inst->ops[inputIdx]); + LUAU_ASSERT(imm.kind == BcImmKind::Int); + imm.valueInt = value; + } + + BcOp getBcOp(uint32_t inputIdx) + { + if (inputIdx >= inst->ops.size()) + inst->ops.resize(inputIdx + 1); + return inst->ops[inputIdx]; + } + + void setBcOp(uint32_t inputIdx, BcOp op) + { + if (inputIdx >= inst->ops.size()) + inst->ops.resize(inputIdx + 1); + inst->ops[inputIdx] = op; + + if (op.kind == BcOpKind::Inst) + { + BcRef opInst = graph.inst(op); + if (std::find(opInst->uses.begin(), opInst->uses.end(), inst.op) == opInst->uses.end()) + { + opInst->uses.push_back(inst.op); + } + } + else if (op.kind == BcOpKind::Phi) + { + BcRef opPhi = graph.phi(op); + if (std::find(opPhi->uses.begin(), opPhi->uses.end(), inst.op) == opPhi->uses.end()) + { + opPhi->uses.push_back(inst.op); + } + } + } + + BcRef getVmConst(uint32_t inputIdx) + { + BcOp constOp = getBcOp(inputIdx); + LUAU_ASSERT(constOp.kind == BcOpKind::VmConst); + return graph.vmConst(constOp); + } + + void setVmConst(uint32_t inputIdx, uint32_t cid) + { + LUAU_ASSERT(cid < graph.constants.size()); + setBcOp(inputIdx, BcOp{BcOpKind::VmConst, cid}); + } + + BcRef getBlock(uint32_t inputIdx) + { + BcOp blockOp = getBcOp(inputIdx); + LUAU_ASSERT(blockOp.kind == BcOpKind::Block); + return graph.block(blockOp); + } + + std::vector sliceInputs(uint32_t startFrom) + { + BcOps& ops = this->inst->ops; + std::vector result; + result.reserve(ops.size() - startFrom); + for (uint32_t i = startFrom; i < ops.size(); i++) + result.push_back(ops[i]); + return result; + } +}; + +#define INT_IMM(name, idx) \ + static const uint32_t k##name = idx; \ + int name() \ + { \ + return this->intImmInput(idx); \ + } \ + void set##name(int value) \ + { \ + this->setImmInput(idx, value); \ + } + +#define BC_OP(name, idx) \ + static const uint32_t k##name = idx; \ + BcOp name() \ + { \ + return this->getBcOp(idx); \ + } \ + void set##name(BcOp value) \ + { \ + this->setBcOp(idx, value); \ + } + +#define VM_CONST(name, idx) \ + static const uint32_t k##name = idx; \ + BcRef name() \ + { \ + return this->getVmConst(idx); \ + } \ + void set##name(uint32_t cid) \ + { \ + this->setVmConst(idx, cid); \ + } + +#define JUMP_TO(name, idx) \ + static const uint32_t k##name = idx; \ + BcRef name() \ + { \ + return this->getBlock(idx); \ + } \ + void set##name(BcOp block) \ + { \ + this->setBcOp(idx, block); \ + } + +template +struct BcMove : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_MOVE; + BC_OP(Src, 0) +}; + +template +struct BcCall : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_CALL; + INT_IMM(ParamCount, 0) + INT_IMM(ReturnCount, 1) + BC_OP(Target, 2) + + static const uint32_t kParamStartInput = 3; + std::vector params() + { + return this->sliceInputs(kParamStartInput); + } +}; + +template +struct BcCallFB : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_CALLFB; + INT_IMM(ParamCount, 0) + INT_IMM(ReturnCount, 1) + INT_IMM(FbSlot, 2) + BC_OP(Target, 3) + + static const uint32_t kParamStartInput = 4; + std::vector params() + { + return this->sliceInputs(kParamStartInput); + } +}; + +template +struct BcReturn : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_RETURN; + INT_IMM(ReturnCount, 0) + + static const uint32_t kValuesStartInput = 1; + std::vector values() + { + if (ReturnCount() == 0) + return {}; + return this->sliceInputs(kValuesStartInput); + } +}; + +template +struct BcLoadNil : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_LOADNIL; +}; + +template +struct BcNamecall : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_NAMECALL; + BC_OP(Table, 0) + INT_IMM(Hint, 1) + VM_CONST(Key, 2) +}; + +template +struct BcGetTableKS : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_GETTABLEKS; + BC_OP(Source, 0) + INT_IMM(Hint, 1) + VM_CONST(Key, 2) +}; + +template +struct BcGetVarArgs : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_GETVARARGS; + static const uint32_t kStartRegInput = 0; + INT_IMM(ValuesCount, 1) + + Reg startReg() + { + return this->inst->ops[kStartRegInput].index; + } +}; + +template +struct BcJump : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_JUMP; + JUMP_TO(Target, 0) +}; + +template +struct BcCmpProto : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_CMPPROTO; + + BC_OP(Closure, 0) + INT_IMM(ProtoId, 1) + JUMP_TO(Fallback, 2) +}; + +template +struct BcSetList : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_SETLIST; + INT_IMM(StartIndex, 0) + INT_IMM(Count, 1) + static const uint32_t kParamStartInput = 2; + std::vector params() + { + return this->sliceInputs(kParamStartInput); + } +}; + +template +struct BcGetImport : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_GETIMPORT; + VM_CONST(Import, 0) + INT_IMM(PathLength, 1) + static const uint32_t kPathStartInput = 2; + std::vector importPath() + { + return this->sliceInputs(kPathStartInput); + } +}; + +#undef INT_IMM +#undef BC_OP + +} // namespace Bytecode +} // namespace Luau diff --git a/Bytecode/include/Luau/BytecodeValidation.h b/Bytecode/include/Luau/BytecodeValidation.h new file mode 100644 index 00000000..c76df096 --- /dev/null +++ b/Bytecode/include/Luau/BytecodeValidation.h @@ -0,0 +1,71 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" + +#include + +#include + +namespace Luau +{ + +namespace Bytecode +{ + +template +inline std::vector& usesOf(BcFunction& fn, const BcOp& def) +{ + if (def.kind == BcOpKind::Inst) + return fn.instOp(def).uses; + else + { + LUAU_ASSERT(def.kind == BcOpKind::Phi); + return fn.phiOp(def).uses; + } +} + +template +inline int countUses(BcFunction& fn, const BcOp& def, const BcOp& consumer) +{ + std::vector& uses = usesOf(fn, def); + return static_cast(std::count(uses.begin(), uses.end(), consumer)); +} + +template +inline bool hasUse(BcFunction& fn, const BcOp& def, const BcOp& consumer) +{ + return countUses(fn, def, consumer) > 0; +} + +// for every operand of every inst/phi in a live block, the referenced def must have that consumer in its `uses` +template +bool verifyUseConsistency(BcFunction& fn) +{ + auto checkOperand = [&](const BcOp& consumer, BcOp operand) + { + if (operand.kind != BcOpKind::Inst && operand.kind != BcOpKind::Phi) + return true; + return hasUse(fn, operand, consumer); + }; + + bool result = true; + for (BcBlock& block : fn.blocks) + { + if ((block.flags & BcBlockFlag::Dead) != 0) + continue; + + for (const BcOp& phiOp : block.phis) + for (const BcOp& operand : fn.phiOp(phiOp).ops) + result &= checkOperand(phiOp, operand); + + for (const BcOp& instOp : block.ops) + for (const BcOp& operand : fn.instOp(instOp).ops) + result &= checkOperand(instOp, operand); + } + return result; +} + +} // namespace Bytecode + +} // namespace Luau diff --git a/Bytecode/include/Luau/Sccp.h b/Bytecode/include/Luau/Sccp.h new file mode 100644 index 00000000..37ff5a90 --- /dev/null +++ b/Bytecode/include/Luau/Sccp.h @@ -0,0 +1,930 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Bytecode.h" +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeUtils.h" +#include "Luau/BytecodeValidation.h" +#include "Luau/VecDeque.h" +#include "Luau/DenseHash2.h" + +#include +#include +#include + +namespace Luau +{ +namespace Bytecode +{ + +// SCCP is generic over the constant representation +// Each instantiation inherits VmConstOps with the operations the pass needs to evaluate constants +struct VmConstOps +{ + virtual std::optional evaluate(const BcOp& lhsOp, const BcOp& rhsOp, LuauOpcode op) const = 0; + virtual bool falsey(const BcOp& falseyOp) const = 0; + + // standard three way comparison: -1 if lhsOp < rhsOp, 0 if lhsOp == rhsOp, 1 if lhsOp > rhsOp + virtual int cmp(const BcOp& lhsOp, const BcOp& rhsOp) const = 0; + virtual int cmp(const BcOp& lhsOp, const BcImm& rhs) const = 0; + + virtual BcOp makeNil() const = 0; + virtual BcImm makeImm(bool value) const = 0; + virtual BcImm makeImm(int32_t value) const = 0; + + // true if the VmConst supports ordering comparisons (number, integer, string) + virtual bool isOrderable(const BcOp& vmConstOp) const = 0; + virtual bool kindEquals(const BcOp& lhsOp, const BcOp& rhsOp) const = 0; + + // returns std::nullopt if the comparison is not supported + // rhsOp may either be a VmConst or an Imm, lhs only VmConst + virtual std::optional eq(const BcOp& lhsOp, const BcOp& rhsOp) const = 0; + virtual std::optional eq(const BcOp& lhsOp, bool rhs) const = 0; + virtual std::optional eq(const BcOp& lhsOp, int32_t rhs) const = 0; + + // only true for LUA_TNUMBER + virtual bool isArithmeticConstant(const BcOp& vmConstOp) const = 0; + + virtual double asNumber(const BcOp& vmConstOp) const = 0; + + virtual BcRef asImm(BcOp op) const = 0; + + VmConstOps() = default; + virtual ~VmConstOps() = default; + VmConstOps(const VmConstOps&) = default; + VmConstOps(VmConstOps&&) = delete; + VmConstOps& operator=(const VmConstOps&) = default; + VmConstOps& operator=(VmConstOps&&) = delete; +}; + +struct BcVmConstImpl : public VmConstOps +{ + std::optional evaluate(const BcOp& lhsOp, const BcOp& rhsOp, LuauOpcode op) const override; + bool falsey(const BcOp& falseyOp) const override; + + int cmp(const BcOp& lhsOp, const BcOp& rhsOp) const override; + int cmp(const BcOp& lhsOp, const BcImm& rhs) const override; + + BcOp makeNil() const override; + BcImm makeImm(bool value) const override; + BcImm makeImm(int32_t value) const override; + BcRef asImm(BcOp op) const override; + + bool isOrderable(const BcOp& vmConstOp) const override; + bool kindEquals(const BcOp& lhsOp, const BcOp& rhsOp) const override; + + std::optional eq(const BcOp& lhsOp, const BcOp& rhsOp) const override; + std::optional eq(const BcOp& lhsOp, bool rhs) const override; + std::optional eq(const BcOp& lhsOp, int32_t rhs) const override; + + bool isArithmeticConstant(const BcOp& vmConstOp) const override; + + double asNumber(const BcOp& vmConstOp) const override; + + explicit BcVmConstImpl(BcFunction& func) + : VmConstOps() + , func(func) + { + } + + BcFunction& func; +}; + + +enum class Constness +{ + Undetermined, // lattice top + NotAConstant, // lattice bottom + VmConstant, + ImmConstant, +}; + +struct ConstnessLattice +{ + Constness kind = Constness::Undetermined; + std::optional vmConst = std::nullopt; + std::optional immConst = std::nullopt; + + ConstnessLattice() = default; + + ConstnessLattice(Constness kind, BcOp bcOp) + : kind(kind) + , vmConst(bcOp) + { + LUAU_ASSERT(kind == Constness::VmConstant); + } + + ConstnessLattice(Constness kind, BcImm imm) + : kind(kind) + , vmConst(std::nullopt) + , immConst(imm) + { + LUAU_ASSERT(kind == Constness::ImmConstant); + } + + explicit ConstnessLattice(Constness kind) + : kind(kind) + , vmConst(std::nullopt) + , immConst(std::nullopt) + { + } + + ConstnessLattice merge(const ConstnessLattice& other) const + { + // Undetermined is lattice top: meeting with it yields the other operand + if (kind == Constness::Undetermined) + return other; + if (other.kind == Constness::Undetermined) + return *this; + // two equal constants meet to themselves; anything else falls to bottom + if (*this == other) + return *this; + return ConstnessLattice(Constness::NotAConstant); + } + + bool operator==(const ConstnessLattice& other) const + { + if (kind != other.kind) + return false; + if (kind == Constness::ImmConstant) + return immConst == other.immConst; + if (kind == Constness::VmConstant) + return vmConst == other.vmConst; + return true; + } + + bool operator!=(const ConstnessLattice& other) const + { + return !(*this == other); + } +}; + +enum class ConditionState +{ + AlwaysFalse, + AlwaysTrue, + Unknown, +}; + +struct JumpTarget +{ + bool dead = false; + BcOp blockOp; + ConditionState condition = ConditionState::Unknown; +}; + +using OpConstness = DenseHashMap2; + +struct SccpState +{ + OpConstness opConstness; + + ConstnessLattice operandLattice(const BcOp& op) + { + if (op.kind == BcOpKind::Proj || op.kind == BcOpKind::VmReg || op.kind == BcOpKind::VmUpvalue) + return ConstnessLattice(Constness::NotAConstant); + return opConstness[op]; + } + + // an unresolved condition is bottom if any operand is bottom, else top + Constness unknownConditionConstness(std::initializer_list ops) + { + for (const BcOp& op : ops) + { + ConstnessLattice lat = operandLattice(op); + if (lat.kind == Constness::NotAConstant) + return Constness::NotAConstant; + } + return Constness::Undetermined; + } +}; + +class SccpInterpreter +{ +public: + explicit SccpInterpreter(VmConstOps* impl, SccpState* state) + : impl(impl) + , state(state) + { + } + + ConditionState evaluateCondition(const BcOp& op); + ConditionState evaluateComparisonCondition(LuauOpcode op, const BcOp& lhs, const BcOp& rhs); + ConditionState evaluateXeqkCondition(BcRef inst); + + ConstnessLattice evaluateArith(LuauOpcode op, BcRef instRepr); + ConstnessLattice evaluate(LuauOpcode op, BcRef instRepr); + + +private: + VmConstOps* impl; + SccpState* state; +}; + +template +struct Sccp +{ + BcFunction& func; + VmConstOps* impl; + + SccpState state; + SccpInterpreter interpreter; + + // this maps a block (index) to its predecessors that it was reached from + // if a block is not in this map, it is unreachable + DenseHashMap2> blockUses; + + VecDeque flowWorklist; + DenseHashSet2 flowWorklistSet; + + // when a def's lattice value changes, its uses must be re-evaluated + VecDeque ssaWorklist; + + explicit Sccp(BcFunction& func, VmConstOps* impl) + : func(func) + , impl(impl) + , interpreter(impl, &state) + { + } + + ConstnessLattice makeBoolImm(bool value) + { + BcImm imm{}; + imm.kind = BcImmKind::Boolean; + imm.valueBoolean = value; + return ConstnessLattice(Constness::ImmConstant, imm); + } + + std::optional getFallthrough(BcRef block) + { + std::optional fallthrough; + for (auto& succOp : block->successors) + { + if (succOp.kind == BcBlockEdgeKind::Fallthrough) + { + if (fallthrough) + { + LUAU_ASSERT(!"Multiple fallthroughs"); + return std::nullopt; + } + + fallthrough = succOp.target; + } + } + + return fallthrough; + } + + // builds the target/fallthrough pair for a two-way branch + // targetTakenOnTrue says which edge the jump takes when the condition holds; the untaken edge is marked dead when the condition resolves + // a cond of Unknown leaves both edges live, which also covers branches whose condition the pass never folds (loops, CMPPROTO) + std::vector conditionalTargets(BcRef inst, const BcOp& target, ConditionState cond, bool targetTakenOnTrue) + { + LUAU_ASSERT(target.kind == BcOpKind::Block); + std::optional fallthrough = getFallthrough(func.block(inst->block)); + LUAU_ASSERT(fallthrough); + + bool targetDead = false; + bool fallthroughDead = false; + if (cond == ConditionState::AlwaysTrue) + { + targetDead = !targetTakenOnTrue; + fallthroughDead = targetTakenOnTrue; + } + else if (cond == ConditionState::AlwaysFalse) + { + targetDead = targetTakenOnTrue; + fallthroughDead = !targetTakenOnTrue; + } + + return {{targetDead, target, cond}, {fallthroughDead, *fallthrough, cond}}; + } + + // when the condition is a known constant, the untaken path is marked dead and each target carries its resolved condition + std::vector jumpTargets(BcRef inst) + { + switch (inst->op) + { + case LOP_JUMP: + case LOP_JUMPBACK: + { + const BcOp& targetOp = inst->ops[0]; + LUAU_ASSERT(targetOp.kind == BcOpKind::Block); + return {{false, targetOp, ConditionState::AlwaysTrue}}; + } + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + { + ConditionState cond = interpreter.evaluateCondition(inst->ops[0]); + return conditionalTargets(inst, inst->ops[1], cond, inst->op == LOP_JUMPIF); + } + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + { + ConditionState cond = interpreter.evaluateComparisonCondition(inst->op, inst->ops[0], inst->ops[1]); + bool negated = (inst->op == LOP_JUMPIFNOTEQ || inst->op == LOP_JUMPIFNOTLE || inst->op == LOP_JUMPIFNOTLT); + return conditionalTargets(inst, inst->ops[2], cond, !negated); + } + case LOP_JUMPXEQKNIL: + case LOP_JUMPXEQKB: + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + { + ConditionState cond = interpreter.evaluateXeqkCondition(inst); + bool negated = func.immOp(inst->ops[1]).valueBoolean; + return conditionalTargets(inst, inst->ops[2], cond, !negated); + } + case LOP_FORNPREP: + case LOP_FORNLOOP: + case LOP_FORGPREP: + case LOP_FORGPREP_NEXT: + case LOP_FORGPREP_INEXT: + return conditionalTargets(inst, inst->ops[3], ConditionState::Unknown, true); + case LOP_FORGLOOP: + // FORGLOOP has two leading imm operands FORGPREP* lack, so its target is ops[5] + return conditionalTargets(inst, inst->ops[5], ConditionState::Unknown, true); + case LOP_CMPPROTO: + return conditionalTargets(inst, inst->ops[2], ConditionState::Unknown, true); + case LOP_JUMPX: + LUAU_ASSERT(!"Should have never parsed this"); + [[fallthrough]]; + default: + return {}; + } + } + + // for each operand, evaluate and merge the lattice with the phi lattice + void visitPhi(BcRef phi) + { + LUAU_ASSERT(phi->ops.size() > 0); + + ConstnessLattice fold; + + for (size_t i = 0; i < phi->ops.size(); i++) + { + BcOp op = phi->ops[i]; + + ConstnessLattice lattice = state.operandLattice(op); + fold = lattice.merge(fold); + } + + const ConstnessLattice& prevLattice = state.opConstness[phi.op]; + if (fold != prevLattice) + { + for (BcOp use : phi->uses) + ssaWorklist.push_back(use); + } + state.opConstness[phi.op] = fold; + } + + // evaluate an instruction, comparing against the previous lattice value, and inserting all uses into the SSA worklist if it changed + void visitInst(BcRef inst) + { + // CAPTURE REF can be mutated externally via SETUPVAL + // the SSA graph doesn't model that alias, so mark the source non-constant to avoid folding a stale value + if (inst->op == LOP_CAPTURE && inst->ops.size() >= 2) + { + const BcOp& captureTypeOp = inst->ops[0]; + LUAU_ASSERT(captureTypeOp.kind == BcOpKind::Imm); + const BcImm& captureImm = func.immOp(captureTypeOp); + if (captureImm.kind == BcImmKind::Int && captureImm.valueInt == LCT_REF) + { + const BcOp& srcOp = inst->ops[1]; + ConstnessLattice prev = state.opConstness[srcOp]; + if (prev.kind != Constness::NotAConstant && (srcOp.kind == BcOpKind::Inst || srcOp.kind == BcOpKind::Phi)) + { + state.opConstness[srcOp] = ConstnessLattice(Constness::NotAConstant); + + + for (BcOp use : usesOf(func, srcOp)) + ssaWorklist.push_back(use); + } + } + } + + ConstnessLattice lattice = interpreter.evaluate(inst->op, inst); + const ConstnessLattice& prevLattice = state.opConstness[inst.op]; + + ConstnessLattice newVal = lattice.merge(prevLattice); + if (newVal != prevLattice) + { + for (BcOp use : inst->uses) + ssaWorklist.push_back(use); + } + + for (const JumpTarget& target : jumpTargets(inst)) + { + uint32_t blockIdx = func.getBlockIndex(func.blockOp(target.blockOp)); + if (!target.dead) + { + blockUses[blockIdx].insert(inst->block); + if (!flowWorklistSet.find(target.blockOp)) + flowWorklist.push_back(target.blockOp); + } + } + + state.opConstness[inst.op] = newVal; + } + + void propagate() + { + BcRef entryBlock = func.block(func.entryBlock); + BcRef exitBlock = func.block(func.exitBlock); + // the entry block is always live + blockUses[func.getBlockIndex(*entryBlock)].insert(entryBlock.op); + // the exit block is always live per serialization requirements + blockUses[func.getBlockIndex(*exitBlock)].insert(exitBlock.op); + + flowWorklist.push_back(func.entryBlock); + while (!flowWorklist.empty() || !ssaWorklist.empty()) + { + while (!flowWorklist.empty()) + { + BcOp blockOp = flowWorklist.front(); + BcRef block = func.block(blockOp); + + flowWorklist.pop_front(); + if (flowWorklistSet.contains(block.op)) + continue; + + for (BcOp& phiOp : block->phis) + { + LUAU_ASSERT(phiOp.kind == BcOpKind::Phi); + visitPhi(func.phi(phiOp)); + } + + for (BcOp& op : block->ops) + { + LUAU_ASSERT(op.kind == BcOpKind::Inst); + visitInst(func.inst(op)); + } + + bool blockEndsWithBranch = false; + if (!block->ops.empty() && block->ops.back().kind == BcOpKind::Inst) + blockEndsWithBranch = !jumpTargets(func.inst(block->ops.back())).empty(); + + for (BcBlockEdge succEdge : block->successors) + { + if (succEdge.kind == BcBlockEdgeKind::Fallthrough) + { + uint32_t succIdx = func.getBlockIndex(func.blockOp(succEdge.target)); + // If this block ends with a branch and the fallthrough wasn't already added by visitInst, skip it + if (blockEndsWithBranch && !blockUses[succIdx].contains(block.op)) + continue; + + blockUses[succIdx].insert(block.op); + if (!flowWorklistSet.contains(succEdge.target)) + flowWorklist.push_back(succEdge.target); + } + } + flowWorklistSet.insert(block.op); + } + + while (!ssaWorklist.empty()) + { + + BcOp op = ssaWorklist.front(); + ssaWorklist.pop_front(); + + if (op.kind == BcOpKind::Inst) + visitInst(func.inst(op)); + else if (op.kind == BcOpKind::Phi) + visitPhi(func.phi(op)); + } + } + } + + BcOp makeConstantOp(const ConstnessLattice& lattice) + { + if (lattice.kind == Constness::VmConstant) + return func.addConst(lattice.vmConst.value()); + else if (lattice.kind == Constness::ImmConstant) + return func.addImm(lattice.immConst.value()); + + LUAU_ASSERT(!"makeConstantOp called on non-constant lattice value"); + return BcOp{}; + } + + void replaceOperand(BcRef inst, BcOp oldOp, BcOp newOp) + { + for (BcOp& op : inst->ops) + { + if (op == oldOp) + op = newOp; + } + } + + void replacePhiOperand(BcRef phi, BcOp oldOp, BcOp newOp) + { + for (BcOp& op : phi->ops) + { + if (op == oldOp) + op = newOp; + } + } + + bool isLoadInst(BcOp op) + { + if (op.kind != BcOpKind::Inst) + return false; + LuauOpcode opcode = func.inst(op)->op; + return opcode == LOP_LOADK || opcode == LOP_LOADKX || opcode == LOP_LOADN || opcode == LOP_LOADB || opcode == LOP_LOADNIL; + } + + // rewrite a folded instruction in-place to a load instruction + // op identity is preserved so all existing uses remain valid + void rewriteToLoad(BcOp op, const ConstnessLattice& lattice) + { + BcRef inst = func.inst(op); + for (BcOp& usedOp : inst->ops) + { + func.eraseUse(op, usedOp); + } + + inst->ops.clear(); + if (lattice.kind == Constness::VmConstant) + { + inst->op = LOP_LOADK; + BcOp constOp = lattice.vmConst.value(); + inst->ops.push_back(constOp); + } + else + { + LUAU_ASSERT(lattice.kind == Constness::ImmConstant); + const BcImm& imm = lattice.immConst.value(); + inst->op = (imm.kind == BcImmKind::Boolean) ? LOP_LOADB : LOP_LOADN; + inst->ops.push_back(func.addImm(imm)); + } + } + + void removeDeadEdges(BcRef inst) + { + std::vector targets = jumpTargets(inst); + + BcRef block = func.block(inst->block); + + BcOp liveTarget{}; + bool hasLive = false; + + for (const JumpTarget& target : targets) + { + if (target.dead) + { + BcEdges& succs = block->successors; + unsigned writeIdx = 0; + for (unsigned i = 0; i < succs.size(); i++) + { + if (!(succs[i].target == target.blockOp)) + succs[writeIdx++] = succs[i]; + } + succs.resize(writeIdx); + } + else + { + liveTarget = target.blockOp; + hasLive = true; + } + } + + if (!hasLive) + return; + + // ensure the live target has a fallthrough edge + bool hasFallthrough = false; + for (unsigned i = 0; i < block->successors.size(); i++) + { + if (block->successors[i].target == liveTarget) + { + block->successors[i].kind = BcBlockEdgeKind::Fallthrough; + hasFallthrough = true; + break; + } + } + + if (!hasFallthrough) + block->successors.push_back({BcBlockEdgeKind::Fallthrough, liveTarget}); + } + + void replaceUses() + { + for (auto& [op, lattice] : state.opConstness) + { + if (lattice.kind != Constness::ImmConstant && lattice.kind != Constness::VmConstant) + continue; + if (op.kind != BcOpKind::Inst) + continue; + if (isLoadInst(op)) + continue; + + BcRef inst = func.inst(op); + LUAU_ASSERT(inst->op != LOP_JUMPX); + + // isJumpD is safe to use here because JUMPX is not parsed by the GraphParser + if (isJumpD(inst->op)) + { + removeDeadEdges(inst); + func.eraseOp(op); + } + else + rewriteToLoad(op, lattice); + } + } + + void simplifyPhis() + { + for (BcOp blockOp : flowWorklistSet) + { + BcRef block = func.block(blockOp); + + for (auto it = block->phis.begin(); it != block->phis.end();) + { + BcOp op = *it; + LUAU_ASSERT(op.kind == BcOpKind::Phi); + + BcRef phi = func.phi(op); + if (phi->ops.empty()) + { + ++it; + continue; + } + + BcOp unique = phi->ops[0]; + bool allSame = true; + for (size_t i = 1; i < phi->ops.size(); i++) + { + if (phi->ops[i] != unique) + { + allSame = false; + break; + } + } + + if (!allSame) + { + ++it; + continue; + } + + for (BcOp use : usesOf(func, op)) + { + if (use.kind == BcOpKind::Inst) + replaceOperand(func.inst(use), op, unique); + else if (use.kind == BcOpKind::Phi) + replacePhiOperand(func.phi(use), op, unique); + + usesOf(func, unique).push_back(use); + } + phi->uses.clear(); + it = block->phis.erase(it); + } + } + } + + void updateBlockUses() + { + // mark dead blocks by forward reachability from entry, not by blockUses + // (which can miss blocks depending on worklist ordering) + DenseHashSet2 reachable; + std::vector worklist; + + uint32_t entryIdx = func.getBlockIndex(*func.block(func.entryBlock)); + uint32_t exitIdx = func.getBlockIndex(*func.block(func.exitBlock)); + reachable.insert(entryIdx); + reachable.insert(exitIdx); + worklist.push_back(entryIdx); + + while (!worklist.empty()) + { + uint32_t idx = worklist.back(); + worklist.pop_back(); + BcBlock& blk = func.blocks[idx]; + for (const BcBlockEdge& edge : blk.successors) + { + uint32_t succIdx = func.getBlockIndex(func.blockOp(edge.target)); + if (!reachable.contains(succIdx)) + { + reachable.insert(succIdx); + worklist.push_back(succIdx); + } + } + } + + for (BcBlock& block : func.blocks) + { + uint32_t blockidx = func.getBlockIndex(block); + block.useCount = static_cast(blockUses[blockidx].size()); + if (!reachable.contains(blockidx)) + block.flags |= BcBlockFlag::Dead; + } + } + + static std::optional arithToKOpcode(LuauOpcode op) + { + switch (op) + { + case LOP_ADD: + return LOP_ADDK; + case LOP_SUB: + return LOP_SUBK; + case LOP_MUL: + return LOP_MULK; + case LOP_DIV: + return LOP_DIVK; + case LOP_MOD: + return LOP_MODK; + case LOP_POW: + return LOP_POWK; + default: + return std::nullopt; + } + } + + // pure value producers can be removed once nothing references their result + bool isPureProducer(LuauOpcode op) const + { + switch (op) + { + case LOP_LOADK: + case LOP_LOADKX: + case LOP_LOADN: + case LOP_LOADB: + case LOP_LOADNIL: + case LOP_GETUPVAL: + return true; + default: + return false; + } + } + + std::optional registerOf(BcOp op) + { + if (op.kind == BcOpKind::VmReg) + return static_cast(op.index); + if (auto it = func.regs.find(op); it != func.regs.end()) + return it->second; + return std::nullopt; + } + + void eraseDeadProducer(BcOp op) + { + if (op.kind != BcOpKind::Inst) + return; + BcRef inst = func.inst(op); + if (!isPureProducer(inst->op)) + return; + if (!inst->uses.empty()) + return; + func.eraseOp(op); + } + + // values that are known constant used in arithmetic operations can be turned into their KR or RK variants + // sub and div have both KR and RK + void arithToK() + { + for (BcBlock& block : func.blocks) + { + uint32_t blockidx = func.getBlockIndex(block); + if (blockUses[blockidx].empty()) + continue; + + std::vector toErase; + toErase.reserve(block.ops.size()); + + for (auto bcOpIt = block.ops.begin(); bcOpIt != block.ops.end(); ++bcOpIt) + { + BcOp& op = *bcOpIt; + + BcRef inst = func.inst(op); + + std::optional kOpcode = arithToKOpcode(inst->op); + if (!kOpcode || inst->ops.size() != 2) + continue; + + BcOp lhs = inst->ops[0]; + BcOp rhs = inst->ops[1]; + // we can safely assume that, at most, one of these can be a VmConstant + // if they both were constant, the arith would have been folded + // TODO: ImmConstant? + ConstnessLattice lhsLat = state.operandLattice(lhs); + ConstnessLattice rhsLat = state.operandLattice(rhs); + + BcOp nonConstantOp; + ConstnessLattice constantK; + bool rk = false; + + auto isConstNumber = [&](const ConstnessLattice& lat) -> bool + { + return lat.kind == Constness::VmConstant && lat.vmConst && impl->isArithmeticConstant(lat.vmConst.value()); + }; + + if (isConstNumber(rhsLat) && lhsLat.kind == Constness::NotAConstant) + { + // can fold this to a K variant + nonConstantOp = lhs; + constantK = rhsLat; + } + else if (isConstNumber(lhsLat) && rhsLat.kind == Constness::NotAConstant) + { + if (inst->op == LOP_ADD || inst->op == LOP_MUL || inst->op == LOP_SUB || inst->op == LOP_DIV) + { + // LOP_ADD and LOP_MUL are commutative + // LOP_SUB and LOP_DIV can emit the RK variant + + nonConstantOp = rhs; + constantK = lhsLat; + + if (inst->op == LOP_SUB) + { + kOpcode = LOP_SUBRK; + rk = true; + } + else if (inst->op == LOP_DIV) + { + kOpcode = LOP_DIVRK; + rk = true; + } + } + } + else + { + continue; + } + + BcOp prevConstOperand = (nonConstantOp == lhs) ? rhs : lhs; + + // we can do some potential folding here now that we know one operand is constant + // for instance, adds of zero, muls of zero or 1, pows of zero or 1, etc + double valueNumber = impl->asNumber(constantK.vmConst.value()); + if (valueNumber == 0) + { + if (inst->op == LOP_ADD || inst->op == LOP_SUB) + { + inst->op = LOP_MOVE; + func.setOps(op, inst, {nonConstantOp}); + } + else if (inst->op == LOP_MUL) + { + inst->op = LOP_LOADN; + BcImm imm{BcImmKind::Int}; + imm.valueInt = 0; + func.setOps(op, inst, {func.addImm(imm)}); + } + else if (inst->op == LOP_POW) + { + inst->op = LOP_LOADN; + BcImm imm{BcImmKind::Int}; + imm.valueInt = 1; + func.setOps(op, inst, {func.addImm(imm)}); + } + } + else if (valueNumber == 1) + { + if (inst->op == LOP_MUL || inst->op == LOP_POW || inst->op == LOP_DIV) + { + inst->op = LOP_MOVE; + func.setOps(op, inst, {nonConstantOp}); + } + } + else + { + inst->op = *kOpcode; + if (!rk) + func.setOps(op, inst, {nonConstantOp, constantK.vmConst.value()}); + else + // SUBRK and DIVRK expect B as the constant table index + func.setOps(op, inst, {constantK.vmConst.value(), nonConstantOp}); + } + + toErase.push_back(prevConstOperand); + } + + for (BcOp op : toErase) + { + eraseDeadProducer(op); + } + } + } + + void rewrite() + { + arithToK(); + replaceUses(); + simplifyPhis(); + updateBlockUses(); + } +}; + +template +void foldConstants(BcFunction& func, VmConstOps& impl) +{ + Sccp sccp(func, &impl); + sccp.propagate(); + sccp.rewrite(); +} + +} // namespace Bytecode +} // namespace Luau diff --git a/Compiler/src/BytecodeBuilder.cpp b/Bytecode/src/BytecodeBuilder.cpp similarity index 74% rename from Compiler/src/BytecodeBuilder.cpp rename to Bytecode/src/BytecodeBuilder.cpp index 111cdcac..a51fe24c 100644 --- a/Compiler/src/BytecodeBuilder.cpp +++ b/Bytecode/src/BytecodeBuilder.cpp @@ -6,8 +6,16 @@ #include #include +#include -LUAU_FASTFLAGVARIABLE(LuauCompileCorrectLocalPc) +LUAU_FASTFLAG(LuauIntegerType2) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAG(LuauEmitCallFeedback) +LUAU_FASTFLAGVARIABLE(LuauVirtualBcBuilder) +LUAU_FASTFLAGVARIABLE(LuauBytecodeCostModel) +LUAU_FLAGVERSION(LuauBytecodeCostModel, 2) +LUAU_FASTFLAGVARIABLE(LuauCompileEmitVectorDouble) +LUAU_FLAGVERSION(LuauCompileEmitVectorDouble, 2) namespace Luau { @@ -32,6 +40,13 @@ static int log2(int v) return r; } +static int ceillog2(int v) +{ + LUAU_ASSERT(v > 0); + + return v == 1 ? 0 : log2(v - 1) + 1; +} + static void writeByte(std::string& ss, unsigned char value) { ss.append(reinterpret_cast(&value), sizeof(value)); @@ -52,7 +67,7 @@ static void writeDouble(std::string& ss, double value) ss.append(reinterpret_cast(&value), sizeof(value)); } -static void writeVarInt(std::string& ss, unsigned int value) +static void writeVarInt(std::string& ss, uint64_t value) { do { @@ -61,89 +76,21 @@ static void writeVarInt(std::string& ss, unsigned int value) } while (value); } -inline bool isJumpD(LuauOpcode op) +bool BytecodeBuilder::StringRef::operator==(const StringRef& other) const { - switch (op) - { - case LOP_JUMP: - case LOP_JUMPIF: - case LOP_JUMPIFNOT: - case LOP_JUMPIFEQ: - case LOP_JUMPIFLE: - case LOP_JUMPIFLT: - case LOP_JUMPIFNOTEQ: - case LOP_JUMPIFNOTLE: - case LOP_JUMPIFNOTLT: - case LOP_FORNPREP: - case LOP_FORNLOOP: - case LOP_FORGPREP: - case LOP_FORGLOOP: - case LOP_FORGPREP_INEXT: - case LOP_FORGPREP_NEXT: - case LOP_JUMPBACK: - case LOP_JUMPXEQKNIL: - case LOP_JUMPXEQKB: - case LOP_JUMPXEQKN: - case LOP_JUMPXEQKS: - return true; - - default: - return false; - } + return (data && other.data) ? (length == other.length && memcmp(data, other.data, length) == 0) : (data == other.data); } -inline bool isSkipC(LuauOpcode op) +bool BytecodeBuilder::TableShape::operator==(const TableShape& other) const { - switch (op) - { - case LOP_LOADB: - return true; + bool equal = length == other.length && memcmp(keys, other.keys, length * sizeof(keys[0])) == 0 && hasConstants == other.hasConstants; - default: - return false; - } -} - -inline bool isFastCall(LuauOpcode op) -{ - switch (op) + if (hasConstants) { - case LOP_FASTCALL: - case LOP_FASTCALL1: - case LOP_FASTCALL2: - case LOP_FASTCALL2K: - case LOP_FASTCALL3: - return true; - - default: - return false; + equal = equal && memcmp(constants, other.constants, length * sizeof(constants[0])) == 0; } -} - -static int getJumpTarget(uint32_t insn, uint32_t pc) -{ - LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); - - if (isJumpD(op)) - return int(pc + LUAU_INSN_D(insn) + 1); - else if (isFastCall(op)) - return int(pc + LUAU_INSN_C(insn) + 2); - else if (isSkipC(op) && LUAU_INSN_C(insn)) - return int(pc + LUAU_INSN_C(insn) + 1); - else if (op == LOP_JUMPX) - return int(pc + LUAU_INSN_E(insn) + 1); - else - return -1; -} -bool BytecodeBuilder::StringRef::operator==(const StringRef& other) const -{ - return (data && other.data) ? (length == other.length && memcmp(data, other.data, length) == 0) : (data == other.data); -} - -bool BytecodeBuilder::TableShape::operator==(const TableShape& other) const -{ - return length == other.length && memcmp(keys, other.keys, length * sizeof(keys[0])) == 0; + return equal; } size_t BytecodeBuilder::StringRefHash::operator()(const StringRef& v) const @@ -153,10 +100,10 @@ size_t BytecodeBuilder::StringRefHash::operator()(const StringRef& v) const size_t BytecodeBuilder::ConstantKeyHash::operator()(const ConstantKey& key) const { - if (key.type == Constant::Type_Vector) + if (key.type == Constant::Type_Vectorf) { uint32_t i[4]; - static_assert(sizeof(key.value) + sizeof(key.extra) == sizeof(i), "Expecting vector to have four 32-bit components"); + static_assert(sizeof(key.value) + sizeof(key.extra1) == sizeof(i), "Expecting vector to have four 32-bit components"); memcpy(i, &key.value, sizeof(i)); // scramble bits to make sure that integer coordinates have entropy in lower bits @@ -170,6 +117,26 @@ size_t BytecodeBuilder::ConstantKeyHash::operator()(const ConstantKey& key) cons return size_t(h); } + else if (key.type == Constant::Type_Vectord) + { + uint64_t i[4]; + static_assert( + sizeof(key.value) + sizeof(key.extra1) + sizeof(key.extra2) + sizeof(key.extra3) == sizeof(i), + "Expecting vector to have four 64-bit components" + ); + memcpy(i, &key.value, sizeof(i)); + + // scramble bits to make sure that integer coordinates have entropy in lower bits + i[0] ^= i[0] >> 32; + i[1] ^= i[1] >> 32; + i[2] ^= i[2] >> 32; + i[3] ^= i[3] >> 32; + + // Optimized Spatial Hashing for Collision Detection of Deformable Objects + uint32_t h = uint32_t(i[0] * 73856093) ^ uint32_t(i[1] * 19349663) ^ uint32_t(i[2] * 83492791) ^ uint32_t(i[3] * 39916801); + + return size_t(h); + } else { // finalizer from MurmurHash64B @@ -201,6 +168,12 @@ size_t BytecodeBuilder::TableShapeHash::operator()(const TableShape& v) const { hash ^= v.keys[i]; hash *= 16777619; + + if (v.hasConstants) + { + hash ^= v.constants[i]; + hash *= 16777619; + } } return hash; @@ -243,7 +216,31 @@ uint32_t BytecodeBuilder::beginFunction(uint8_t numparams, bool isvararg) return id; } -void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues, uint8_t flags) +void BytecodeBuilder::clearState() +{ + insns.clear(); + lines.clear(); + constants.clear(); + protos.clear(); + jumps.clear(); + fbSlots.clear(); + tableShapes.clear(); + + debugLocals.clear(); + debugUpvals.clear(); + + typedLocals.clear(); + typedUpvals.clear(); + + constantMap.clear(); + tableShapeMap.clear(); + protoMap.clear(); + + debugRemarks.clear(); + debugRemarkBuffer.clear(); +} + +void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues, uint8_t flags, uint64_t cost) { LUAU_ASSERT(currentFunction != ~0u); @@ -269,30 +266,38 @@ void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues, uin if (encoder) encoder->encode(insns.data(), insns.size()); - writeFunction(func.data, currentFunction, flags); + writeFunction(func.data, currentFunction, flags, cost); currentFunction = ~0u; totalInstructionCount += insns.size(); - insns.clear(); - lines.clear(); - constants.clear(); - protos.clear(); - jumps.clear(); - tableShapes.clear(); - - debugLocals.clear(); - debugUpvals.clear(); - - typedLocals.clear(); - typedUpvals.clear(); - - constantMap.clear(); - tableShapeMap.clear(); - protoMap.clear(); - - debugRemarks.clear(); - debugRemarkBuffer.clear(); + if (FFlag::LuauVirtualBcBuilder) + { + clearState(); + } + else + { + insns.clear(); + lines.clear(); + constants.clear(); + protos.clear(); + jumps.clear(); + fbSlots.clear(); + tableShapes.clear(); + + debugLocals.clear(); + debugUpvals.clear(); + + typedLocals.clear(); + typedUpvals.clear(); + + constantMap.clear(); + tableShapeMap.clear(); + protoMap.clear(); + + debugRemarks.clear(); + debugRemarkBuffer.clear(); + } } void BytecodeBuilder::setMainFunction(uint32_t fid) @@ -363,13 +368,7 @@ int32_t BytecodeBuilder::addConstantBoolean(bool value) int32_t BytecodeBuilder::addConstantInteger(int32_t value) { - Constant c = {Constant::Type_Integer}; - c.valueInteger = value; - - ConstantKey k = {Constant::Type_Integer, 0}; - // plop this into the uint64_t - memcpy(&k.value, &value, sizeof(value)); - return addConstant(k, c); + return addConstantInteger((int64_t)value); } int32_t BytecodeBuilder::addConstantNumber(double value) @@ -384,22 +383,55 @@ int32_t BytecodeBuilder::addConstantNumber(double value) return addConstant(k, c); } -int32_t BytecodeBuilder::addConstantVector(float x, float y, float z, float w) +int32_t BytecodeBuilder::addConstantInteger(int64_t value) { - Constant c = {Constant::Type_Vector}; - c.valueVector[0] = x; - c.valueVector[1] = y; - c.valueVector[2] = z; - c.valueVector[3] = w; + Constant c = {Constant::Type_Integer}; + c.valueInteger64 = value; - ConstantKey k = {Constant::Type_Vector}; + ConstantKey k = {Constant::Type_Integer}; + static_assert(sizeof(k.value) == sizeof(value), "Expecting integer to be 64-bit"); + memcpy(&k.value, &value, sizeof(value)); + + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addConstantVectorf(float x, float y, float z, float w) +{ + Constant c = {Constant::Type_Vectorf}; + c.valueVectorf[0] = x; + c.valueVectorf[1] = y; + c.valueVectorf[2] = z; + c.valueVectorf[3] = w; + + ConstantKey k = {Constant::Type_Vectorf}; static_assert( - sizeof(k.value) == sizeof(x) + sizeof(y) && sizeof(k.extra) == sizeof(z) + sizeof(w), "Expecting vector to have four 32-bit components" + sizeof(k.value) == sizeof(x) + sizeof(y) && sizeof(k.extra1) == sizeof(z) + sizeof(w), "Expecting vector to have four 32-bit components" ); memcpy(&k.value, &x, sizeof(x)); memcpy((char*)&k.value + sizeof(x), &y, sizeof(y)); - memcpy(&k.extra, &z, sizeof(z)); - memcpy((char*)&k.extra + sizeof(z), &w, sizeof(w)); + memcpy(&k.extra1, &z, sizeof(z)); + memcpy((char*)&k.extra1 + sizeof(z), &w, sizeof(w)); + + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addConstantVectord(double x, double y, double z, double w) +{ + Constant c = {Constant::Type_Vectord}; + c.valueVectord[0] = x; + c.valueVectord[1] = y; + c.valueVectord[2] = z; + c.valueVectord[3] = w; + + ConstantKey k = {Constant::Type_Vectord}; + static_assert( + sizeof(k.value) == sizeof(x) && sizeof(k.extra1) == sizeof(y) && sizeof(k.extra2) == sizeof(z) && sizeof(k.extra3) == sizeof(w), + "Expecting vector to have four 64-bit components" + ); + memcpy(&k.value, &x, sizeof(x)); + memcpy(&k.extra1, &y, sizeof(y)); + memcpy(&k.extra2, &z, sizeof(z)); + memcpy(&k.extra3, &w, sizeof(w)); return addConstant(k, c); } @@ -456,6 +488,13 @@ int32_t BytecodeBuilder::addConstantClosure(uint32_t fid) return addConstant(k, c); } +uint32_t BytecodeBuilder::addFbSlot(LuauFeedbackType t) +{ + LUAU_ASSERT(t == LuauFeedbackType::LFT_CALLTARGET); + fbSlots.push_back(uint32_t(getInstructionCount())); + return uint32_t(fbSlots.size() - 1); +} + int16_t BytecodeBuilder::addChildFunction(uint32_t fid) { if (int16_t* cache = protoMap.find(fid)) @@ -472,6 +511,24 @@ int16_t BytecodeBuilder::addChildFunction(uint32_t fid) return int16_t(id); } +int32_t BytecodeBuilder::addClassShape(ClassShape shape) +{ + uint32_t id = uint32_t(constants.size()); + + if (id >= kMaxConstantCount) + return -1; + + Constant c = {Constant::Type_ClassShape}; + + c.valueClassShape = uint32_t(classShapes.size()); + + classShapes.emplace_back(std::move(shape)); + + constants.push_back(c); + + return int32_t(id); +} + void BytecodeBuilder::emitABC(LuauOpcode op, uint8_t a, uint8_t b, uint8_t c) { uint32_t insn = uint32_t(op) | (a << 8) | (b << 16) | (c << 24); @@ -569,6 +626,12 @@ bool BytecodeBuilder::patchSkipC(size_t jumpLabel, size_t targetLabel) return true; } +void BytecodeBuilder::patchAux(size_t targetAux, int32_t newValue) +{ + LUAU_ASSERT(targetAux < insns.size()); + insns[targetAux] = newValue; +} + void BytecodeBuilder::setFunctionTypeInfo(std::string value) { functions[currentFunction].typeinfo = std::move(value); @@ -708,7 +771,7 @@ void BytecodeBuilder::finalize() // assemble final bytecode blob uint8_t version = getVersion(); - LUAU_ASSERT(version >= LBC_VERSION_MIN && version <= LBC_VERSION_MAX); + LUAU_ASSERT((version >= LBC_VERSION_MIN && version <= LBC_VERSION_MAX) || version == LBC_VERSION_CLASSES); bytecode = char(version); @@ -736,13 +799,17 @@ void BytecodeBuilder::finalize() writeVarInt(bytecode, uint32_t(functions.size())); for (const Function& func : functions) + { + if (FFlag::LuauBytecodeCostModel || FFlag::LuauCompileEmitVectorDouble || FFlag::DebugLuauUserDefinedClasses) + writeVarInt(bytecode, func.data.size()); bytecode += func.data; + } LUAU_ASSERT(mainFunction < functions.size()); writeVarInt(bytecode, mainFunction); } -void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags) +void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags, uint64_t cost) { LUAU_ASSERT(id < functions.size()); const Function& func = functions[id]; @@ -807,23 +874,50 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags) writeByte(ss, c.valueBoolean); break; - // ServerLua: This is for us, it creates a lightuserdata. - case Constant::Type_Integer: - writeByte(ss, LBC_CONSTANT_INTEGER); - writeInt(ss, c.valueInteger); - break; - case Constant::Type_Number: writeByte(ss, LBC_CONSTANT_NUMBER); writeDouble(ss, c.valueNumber); break; - case Constant::Type_Vector: + case Constant::Type_Integer: + writeByte(ss, LBC_CONSTANT_INTEGER); + if (c.valueInteger64 < 0) + { + writeByte(ss, 1); + writeVarInt(ss, ~(uint64_t)c.valueInteger64 + 1); + } + else + { + writeByte(ss, 0); + writeVarInt(ss, c.valueInteger64); + } + break; + + case Constant::Type_Vectorf: writeByte(ss, LBC_CONSTANT_VECTOR); - writeFloat(ss, c.valueVector[0]); - writeFloat(ss, c.valueVector[1]); - writeFloat(ss, c.valueVector[2]); - writeFloat(ss, c.valueVector[3]); + writeFloat(ss, c.valueVectorf[0]); + writeFloat(ss, c.valueVectorf[1]); + writeFloat(ss, c.valueVectorf[2]); + writeFloat(ss, c.valueVectorf[3]); + break; + + case Constant::Type_Vectord: + if (FFlag::LuauCompileEmitVectorDouble) + { + writeByte(ss, LBC_CONSTANT_VECTORD); + writeDouble(ss, c.valueVectord[0]); + writeDouble(ss, c.valueVectord[1]); + writeDouble(ss, c.valueVectord[2]); + writeDouble(ss, c.valueVectord[3]); + } + else + { + writeByte(ss, LBC_CONSTANT_VECTOR); + writeFloat(ss, float(c.valueVectord[0])); + writeFloat(ss, float(c.valueVectord[1])); + writeFloat(ss, float(c.valueVectord[2])); + writeFloat(ss, float(c.valueVectord[3])); + } break; case Constant::Type_String: @@ -839,10 +933,23 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags) case Constant::Type_Table: { const TableShape& shape = tableShapes[c.valueTable]; - writeByte(ss, LBC_CONSTANT_TABLE); - writeVarInt(ss, uint32_t(shape.length)); - for (unsigned int i = 0; i < shape.length; ++i) - writeVarInt(ss, shape.keys[i]); + if (shape.hasConstants) + { + writeByte(ss, LBC_CONSTANT_TABLE_WITH_CONSTANTS); + writeVarInt(ss, uint32_t(shape.length)); + for (unsigned int i = 0; i < shape.length; ++i) + { + writeVarInt(ss, shape.keys[i]); + writeInt(ss, shape.constants[i]); + } + } + else + { + writeByte(ss, LBC_CONSTANT_TABLE); + writeVarInt(ss, uint32_t(shape.length)); + for (unsigned int i = 0; i < shape.length; ++i) + writeVarInt(ss, shape.keys[i]); + } break; } @@ -851,6 +958,12 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags) writeVarInt(ss, c.valueClosure); break; + case Constant::Type_ClassShape: + writeByte(ss, LBC_CONSTANT_CLASS_SHAPE); + writeClassShape(ss, classShapes[c.valueClassShape]); + break; + + default: LUAU_ASSERT(!"Unsupported constant type"); } @@ -920,9 +1033,40 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags) { writeVarInt(ss, yield_point); } + + if (FFlag::LuauEmitCallFeedback) + { + // Feedback Slots + writeVarInt(ss, fbSlots.size()); + for (uint32_t pc : fbSlots) + { + writeByte(ss, LFT_CALLTARGET); + writeVarInt(ss, pc); + } + } + else if (FFlag::LuauBytecodeCostModel || FFlag::LuauCompileEmitVectorDouble || FFlag::DebugLuauUserDefinedClasses) + { + writeVarInt(ss, 0); // Empty feedback vector + } + + if ((FFlag::LuauBytecodeCostModel || FFlag::LuauCompileEmitVectorDouble || FFlag::DebugLuauUserDefinedClasses) && (flags & LPF_INLINABLE) != 0) + { + writeVarInt(ss, cost); + } } -void BytecodeBuilder::writeLineInfo(std::string& ss) const +void BytecodeBuilder::writeClassShape(std::string& ss, const ClassShape& cs) const +{ + writeVarInt(ss, cs.className); + writeVarInt(ss, cs.propertyNames.size()); + writeVarInt(ss, cs.methodNames.size()); + for (const auto propName : cs.propertyNames) + writeVarInt(ss, propName); + for (const auto methodName : cs.methodNames) + writeVarInt(ss, methodName); +} + +int BytecodeBuilder::calcLinesSpan() const { LUAU_ASSERT(!lines.empty()); @@ -954,6 +1098,63 @@ void BytecodeBuilder::writeLineInfo(std::string& ss) const span = 1 << log2(int(next - offset)); } } + return span; +} + +void BytecodeBuilder::fillBaselineInfo(int span, int* baseline, size_t baselineSize) const +{ + for (size_t offset = 0; offset < lines.size(); offset += span) + { + size_t next = offset; + + int min = lines[offset]; + + for (; next < lines.size() && next < offset + span; ++next) + min = std::min(min, lines[next]); + + baseline[offset / span] = min; + } +} + +void BytecodeBuilder::writeLineInfo(std::string& ss) const +{ + LUAU_ASSERT(!lines.empty()); + + // this function encodes lines inside each span as a 8-bit delta to span baseline + // span is always a power of two; depending on the line info input, it may need to be as low as 1 + int span = 1 << 24; + + // first pass: determine span length + if (FFlag::LuauVirtualBcBuilder) + { + span = calcLinesSpan(); + } + else + { + for (size_t offset = 0; offset < lines.size(); offset += span) + { + size_t next = offset; + + int min = lines[offset]; + int max = lines[offset]; + + for (; next < lines.size() && next < offset + span; ++next) + { + min = std::min(min, lines[next]); + max = std::max(max, lines[next]); + + if (max - min > 255) + break; + } + + if (next < lines.size() && next - offset < size_t(span)) + { + // since not all lines in the range fit in 8b delta, we need to shrink the span + // next iteration will need to reprocess some lines again since span changed + span = 1 << log2(int(next - offset)); + } + } + } // second pass: compute span base int baselineOne = 0; @@ -968,16 +1169,23 @@ void BytecodeBuilder::writeLineInfo(std::string& ss) const baseline = baselineScratch.data(); } - for (size_t offset = 0; offset < lines.size(); offset += span) + if (FFlag::LuauVirtualBcBuilder) { - size_t next = offset; + fillBaselineInfo(span, baseline, baselineSize); + } + else + { + for (size_t offset = 0; offset < lines.size(); offset += span) + { + size_t next = offset; - int min = lines[offset]; + int min = lines[offset]; - for (; next < lines.size() && next < offset + span; ++next) - min = std::min(min, lines[next]); + for (; next < lines.size() && next < offset + span; ++next) + min = std::min(min, lines[next]); - baseline[offset / span] = min; + baseline[offset / span] = min; + } } // third pass: write resulting data @@ -1115,10 +1323,10 @@ void BytecodeBuilder::foldJumps() } } -void BytecodeBuilder::expandJumps() +std::vector BytecodeBuilder::expandJumps() { if (!hasLongJumps) - return; + return {}; // we have some jump instructions that couldn't be patched which means their offset didn't fit into 16 bits // our strategy for replacing instructions is as follows: instead of @@ -1162,7 +1370,7 @@ void BytecodeBuilder::expandJumps() for (size_t i = 0; i < insns.size();) { uint8_t op = LUAU_INSN_OP(insns[i]); - LUAU_ASSERT(op < LOP__COUNT || (op > LOP_LSL__START && op < LOP_LSL__END)); + LUAU_ASSERT(op < LOP__COUNT || (op >= LOP_LSL__START && op <= LOP_LSL__END)); if (currentJump < jumps.size() && jumps[currentJump].source == i) { @@ -1247,31 +1455,29 @@ void BytecodeBuilder::expandJumps() insns.swap(newinsns); lines.swap(newlines); - if (FFlag::LuauCompileCorrectLocalPc) + for (DebugLocal& debugLocal : debugLocals) { - for (DebugLocal& debugLocal : debugLocals) - { - // endpc is exclusive, to get the right remapping, we need to remap the location before the end - if (debugLocal.startpc != debugLocal.endpc) - debugLocal.endpc = remap[debugLocal.endpc - 1] + 1; - else - debugLocal.endpc = remap[debugLocal.endpc]; - - debugLocal.startpc = remap[debugLocal.startpc]; + // endpc is exclusive, to get the right remapping, we need to remap the location before the end + if (debugLocal.startpc != debugLocal.endpc) + debugLocal.endpc = remap[debugLocal.endpc - 1] + 1; + else + debugLocal.endpc = remap[debugLocal.endpc]; - } + debugLocal.startpc = remap[debugLocal.startpc]; + } - for (TypedLocal& typedLocal : typedLocals) - { - // endpc is exclusive, to get the right remapping, we need to remap the location before the end - if (typedLocal.startpc != typedLocal.endpc) - typedLocal.endpc = remap[typedLocal.endpc - 1] + 1; - else - typedLocal.endpc = remap[typedLocal.endpc]; + for (TypedLocal& typedLocal : typedLocals) + { + // endpc is exclusive, to get the right remapping, we need to remap the location before the end + if (typedLocal.startpc != typedLocal.endpc) + typedLocal.endpc = remap[typedLocal.endpc - 1] + 1; + else + typedLocal.endpc = remap[typedLocal.endpc]; - typedLocal.startpc = remap[typedLocal.startpc]; - } + typedLocal.startpc = remap[typedLocal.startpc]; } + + return remap; } std::string BytecodeBuilder::getError(const std::string& message) @@ -1286,6 +1492,17 @@ std::string BytecodeBuilder::getError(const std::string& message) uint8_t BytecodeBuilder::getVersion() { + if (FFlag::DebugLuauUserDefinedClasses) + return LBC_VERSION_CLASSES; + + if (FFlag::LuauCompileEmitVectorDouble) + return 13; + if (FFlag::LuauBytecodeCostModel) + return 12; + + if (FFlag::LuauEmitCallFeedback) + return 11; + return LBC_VERSION_TARGET; } @@ -1294,6 +1511,32 @@ uint8_t BytecodeBuilder::getTypeEncodingVersion() return LBC_TYPE_VERSION_TARGET; } +// Virtual functions have to be defined even if LUAU_ASSERTENABLED is off. + +void BytecodeBuilder::validateConst(int32_t cid) const +{ + LUAU_ASSERT(unsigned(cid) < constants.size()); +} + +void BytecodeBuilder::validateConst(int32_t cid, Constant::Type constType) const +{ + LUAU_ASSERT(unsigned(cid) < constants.size() && constants[cid].type == constType); +} + +uint8_t BytecodeBuilder::validateProto(int32_t pid) const +{ + LUAU_ASSERT(unsigned(pid) < protos.size()); + LUAU_ASSERT(protos[pid] < functions.size()); + return functions[protos[pid]].numupvalues; +} + +uint8_t BytecodeBuilder::validateClosure(int32_t cid) const +{ + unsigned int proto = constants[cid].valueClosure; + LUAU_ASSERT(proto < functions.size()); + return functions[proto].numupvalues; +} + #ifdef LUAU_ASSERTENABLED void BytecodeBuilder::validate() const { @@ -1306,8 +1549,10 @@ void BytecodeBuilder::validateInstructions() const #define VREG(v) LUAU_ASSERT(unsigned(v) < func.maxstacksize) #define VREGRANGE(v, count) LUAU_ASSERT(unsigned(v + (count < 0 ? 0 : count)) <= func.maxstacksize) #define VUPVAL(v) LUAU_ASSERT(unsigned(v) < func.numupvalues) -#define VCONST(v, kind) LUAU_ASSERT(unsigned(v) < constants.size() && constants[v].type == Constant::Type_##kind) -#define VCONSTANY(v) LUAU_ASSERT(unsigned(v) < constants.size()) +#define VCONST(v, kind) \ + FFlag::LuauVirtualBcBuilder ? validateConst(v, Constant::Type_##kind) \ + : LUAU_ASSERT(unsigned(v) < constants.size() && constants[v].type == Constant::Type_##kind) +#define VCONSTANY(v) FFlag::LuauVirtualBcBuilder ? validateConst(v) : LUAU_ASSERT(unsigned(v) < constants.size()) #define VJUMP(v) LUAU_ASSERT(size_t(i + 1 + v) < insns.size() && insnvalid[i + 1 + v]) LUAU_ASSERT(currentFunction != ~0u); @@ -1415,9 +1660,17 @@ void BytecodeBuilder::validateInstructions() const case LOP_NEWCLOSURE: { VREG(LUAU_INSN_A(insn)); - LUAU_ASSERT(unsigned(LUAU_INSN_D(insn)) < protos.size()); - LUAU_ASSERT(protos[LUAU_INSN_D(insn)] < functions.size()); - unsigned int numupvalues = functions[protos[LUAU_INSN_D(insn)]].numupvalues; + unsigned int numupvalues; + if (FFlag::LuauVirtualBcBuilder) + { + numupvalues = validateProto(LUAU_INSN_D(insn)); + } + else + { + LUAU_ASSERT(unsigned(LUAU_INSN_D(insn)) < protos.size()); + LUAU_ASSERT(protos[LUAU_INSN_D(insn)] < functions.size()); + numupvalues = functions[protos[LUAU_INSN_D(insn)]].numupvalues; + } for (unsigned int j = 0; j < numupvalues; ++j) { @@ -1432,10 +1685,11 @@ void BytecodeBuilder::validateInstructions() const VREG(LUAU_INSN_A(insn)); VREG(LUAU_INSN_B(insn)); VCONST(insns[i + 1], String); - LUAU_ASSERT(LUAU_INSN_OP(insns[i + 2]) == LOP_CALL); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 2]) == LOP_CALLFB || LUAU_INSN_OP(insns[i + 2]) == LOP_CALL); break; case LOP_CALL: + case LOP_CALLFB: { int nparams = LUAU_INSN_B(insn) - 1; int nresults = LUAU_INSN_C(insn) - 1; @@ -1607,9 +1861,17 @@ void BytecodeBuilder::validateInstructions() const { VREG(LUAU_INSN_A(insn)); VCONST(LUAU_INSN_D(insn), Closure); - unsigned int proto = constants[LUAU_INSN_D(insn)].valueClosure; - LUAU_ASSERT(proto < functions.size()); - unsigned int numupvalues = functions[proto].numupvalues; + unsigned int numupvalues; + if (FFlag::LuauVirtualBcBuilder) + { + numupvalues = validateClosure(LUAU_INSN_D(insn)); + } + else + { + unsigned int proto = constants[LUAU_INSN_D(insn)].valueClosure; + LUAU_ASSERT(proto < functions.size()); + numupvalues = functions[proto].numupvalues; + } for (unsigned int j = 0; j < numupvalues; ++j) { @@ -1710,6 +1972,43 @@ void BytecodeBuilder::validateInstructions() const } break; + case LOP_NEWCLASSMEMBER: + VREG(LUAU_INSN_A(insn)); + LUAU_ASSERT(LUAU_INSN_B(insn) == 0); + VREG(LUAU_INSN_C(insn)); + VCONST(insns[i + 1], String); + break; + + case LOP_GETUDATAKS: + case LOP_SETUDATAKS: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VCONST(LUAU_INSN_AUX_KV16(insns[i + 1]), String); + break; + + case LOP_NAMECALLUDATA: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VCONST(LUAU_INSN_AUX_KV16(insns[i + 1]), String); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 2]) == LOP_CALL); + break; + + case LOP_CMPPROTO: + VREG(LUAU_INSN_A(insn)); + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_NEWCLASS: + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + VREG(LUAU_INSN_A(insn)); + uint8_t super = LUAU_INSN_B(insn); + LUAU_ASSERT(super == 0xff || (unsigned(super) < func.maxstacksize)); + LUAU_ASSERT(LUAU_INSN_C(insn) == 0); + VCONST(insns[i + 1], ClassShape); + } + break; + default: LUAU_ASSERT(!"Unsupported opcode"); } @@ -1776,8 +2075,9 @@ void BytecodeBuilder::validateVariadic() const LUAU_ASSERT(!insntargets[i]); } - if (op == LOP_CALL) + if (op == LOP_CALL || op == LOP_CALLFB) { + LUAU_ASSERT(FFlag::LuauEmitCallFeedback || op != LOP_CALLFB); // note: calls may end one variadic sequence and start a new one if (LUAU_INSN_B(insn) == 0) @@ -1880,6 +2180,18 @@ void BytecodeBuilder::tagYieldPoints() // Add a yield just past the call so we can handle that. func.yieldpoints.insert(i + 1); } + else if (op == LOP_CALLFB) + { + // same as LOP_CALL, but the word at i + 1 is the feedback + // slot, so "just past the call" is i + 2 + func.yieldpoints.insert(i + 2); + } + else if (op == LOP_FORGLOOP) + { + // luaD_performcally suspends the caller with savedpc at the + // aux word when the iterator yields (see LUA_CALLINFO_OPYIELD) + func.yieldpoints.insert(i + 1); + } } i += getOpLength(op); LUAU_ASSERT(i <= insns.size()); @@ -1897,7 +2209,7 @@ static bool printableStringConstant(const char* str, size_t len) return true; } -void BytecodeBuilder::dumpConstant(std::string& result, int k) const +void BytecodeBuilder::dumpConstant(std::string& result, int k, bool detailed) const { LUAU_ASSERT(unsigned(k) < constants.size()); const Constant& data = constants[k]; @@ -1910,19 +2222,45 @@ void BytecodeBuilder::dumpConstant(std::string& result, int k) const case Constant::Type_Boolean: formatAppend(result, "%s", data.valueBoolean ? "true" : "false"); break; - // ServerLua: added by us for integer constant support - case Constant::Type_Integer: - formatAppend(result, "%d", data.valueInteger); - break; case Constant::Type_Number: formatAppend(result, "%.17g", data.valueNumber); break; - case Constant::Type_Vector: + case Constant::Type_Integer: + formatAppend(result, "%lld", (long long)(int64_t)data.valueInteger64); + break; + case Constant::Type_Vectorf: // 3-vectors is the most common configuration, so truncate to three components if possible - if (data.valueVector[3] == 0.0) - formatAppend(result, "%.9g, %.9g, %.9g", data.valueVector[0], data.valueVector[1], data.valueVector[2]); + if (data.valueVectorf[3] == 0.0f) + formatAppend(result, "%.9g, %.9g, %.9g", data.valueVectorf[0], data.valueVectorf[1], data.valueVectorf[2]); else - formatAppend(result, "%.9g, %.9g, %.9g, %.9g", data.valueVector[0], data.valueVector[1], data.valueVector[2], data.valueVector[3]); + formatAppend(result, "%.9g, %.9g, %.9g, %.9g", data.valueVectorf[0], data.valueVectorf[1], data.valueVectorf[2], data.valueVectorf[3]); + break; + case Constant::Type_Vectord: + if (FFlag::LuauCompileEmitVectorDouble) + { + // 3-vectors is the most common configuration, so truncate to three components if possible + if (data.valueVectord[3] == 0.0) + formatAppend(result, "%.17g, %.17g, %.17g", data.valueVectord[0], data.valueVectord[1], data.valueVectord[2]); + else + formatAppend( + result, "%.17g, %.17g, %.17g, %.17g", data.valueVectord[0], data.valueVectord[1], data.valueVectord[2], data.valueVectord[3] + ); + } + else + { + // 3-vectors is the most common configuration, so truncate to three components if possible + if (data.valueVectord[3] == 0.0f) + formatAppend(result, "%.9g, %.9g, %.9g", float(data.valueVectord[0]), float(data.valueVectord[1]), float(data.valueVectord[2])); + else + formatAppend( + result, + "%.9g, %.9g, %.9g, %.9g", + float(data.valueVectord[0]), + float(data.valueVectord[1]), + float(data.valueVectord[2]), + float(data.valueVectord[3]) + ); + } break; case Constant::Type_String: { @@ -1988,7 +2326,63 @@ void BytecodeBuilder::dumpConstant(std::string& result, int k) const break; } case Constant::Type_Table: - formatAppend(result, "{...}"); + if (detailed) + { + const TableShape& shape = tableShapes[data.valueTable]; + + unsigned sizenode = shape.length > 0 ? (1u << ceillog2(int(shape.length))) : 0; + unsigned mask = sizenode > 0 ? (sizenode - 1) : 0; + + // Compute slot for each key and detect collisions + std::vector slots; + slots.resize(shape.length, 0); + + // Track first key index to claim the slot + std::vector slotOwner; + slotOwner.resize(sizenode, ~0u); + + for (unsigned i = 0; i < shape.length; ++i) + { + const Constant& keyConst = constants[shape.keys[i]]; + LUAU_ASSERT(keyConst.type == Constant::Type_String); + LUAU_ASSERT(keyConst.valueString != 0u); + const StringRef& str = debugStrings[keyConst.valueString - 1]; + + slots[i] = getStringHash(str) & mask; + + if (slotOwner[slots[i]] == ~0u) + slotOwner[slots[i]] = i; + } + + formatAppend(result, "{"); + + for (unsigned i = 0; i < shape.length; ++i) + { + if (i > 0) + formatAppend(result, ", "); + + formatAppend(result, "["); + dumpConstant(result, shape.keys[i], false); + formatAppend(result, "]"); + + if (shape.hasConstants && shape.constants[i] != -1) + { + formatAppend(result, " = "); + dumpConstant(result, shape.constants[i], false); + } + + formatAppend(result, " #%u", slots[i]); + + if (slotOwner[slots[i]] != i) + formatAppend(result, " (conflict)"); + } + + formatAppend(result, "} sizenode=%u", sizenode); + } + else + { + formatAppend(result, "{...}"); + } break; case Constant::Type_Closure: { @@ -1998,6 +2392,17 @@ void BytecodeBuilder::dumpConstant(std::string& result, int k) const formatAppend(result, "'%s'", func.dumpname.c_str()); break; } + case Constant::Type_ClassShape: + { + const ClassShape& cs = classShapes[data.valueClassShape]; + const Constant& className = constants[cs.className]; + LUAU_ASSERT(className.type == Constant::Type_String && className.valueString <= debugStrings.size()); + const StringRef& str = debugStrings[className.valueString - 1]; + // This should always be printable, in fact this should always be a + // valid Luau identifier! + LUAU_ASSERT(printableStringConstant(str.data, str.length)); + formatAppend(result, "class %.*s (props: %zu, methods: %zu)", int(str.length), str.data, cs.propertyNames.size(), cs.methodNames.size()); + } } } @@ -2024,7 +2429,7 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_LOADK: formatAppend(result, "LOADK R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_D(insn)); - dumpConstant(result, LUAU_INSN_D(insn)); + dumpConstant(result, LUAU_INSN_D(insn), false); result.append("]\n"); break; @@ -2034,14 +2439,14 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_GETGLOBAL: formatAppend(result, "GETGLOBAL R%d K%d [", LUAU_INSN_A(insn), *code); - dumpConstant(result, *code); + dumpConstant(result, *code, false); result.append("]\n"); code++; break; case LOP_SETGLOBAL: formatAppend(result, "SETGLOBAL R%d K%d [", LUAU_INSN_A(insn), *code); - dumpConstant(result, *code); + dumpConstant(result, *code, false); result.append("]\n"); code++; break; @@ -2060,7 +2465,7 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_GETIMPORT: formatAppend(result, "GETIMPORT R%d %d [", LUAU_INSN_A(insn), LUAU_INSN_D(insn)); - dumpConstant(result, LUAU_INSN_D(insn)); + dumpConstant(result, LUAU_INSN_D(insn), false); result.append("]\n"); code++; // AUX break; @@ -2075,14 +2480,14 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_GETTABLEKS: formatAppend(result, "GETTABLEKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code); - dumpConstant(result, *code); + dumpConstant(result, *code, false); result.append("]\n"); code++; break; case LOP_SETTABLEKS: formatAppend(result, "SETTABLEKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code); - dumpConstant(result, *code); + dumpConstant(result, *code, false); result.append("]\n"); code++; break; @@ -2101,7 +2506,7 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_NAMECALL: formatAppend(result, "NAMECALL R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code); - dumpConstant(result, *code); + dumpConstant(result, *code, false); result.append("]\n"); code++; break; @@ -2110,6 +2515,11 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, formatAppend(result, "CALL R%d %d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1, LUAU_INSN_C(insn) - 1); break; + case LOP_CALLFB: + formatAppend(result, "CALLFB R%d %d %d [%d]\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1, LUAU_INSN_C(insn) - 1, static_cast(*code)); + code++; + break; + case LOP_RETURN: formatAppend(result, "RETURN R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1); break; @@ -2180,55 +2590,55 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_ADDK: formatAppend(result, "ADDK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); - dumpConstant(result, LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); result.append("]\n"); break; case LOP_SUBK: formatAppend(result, "SUBK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); - dumpConstant(result, LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); result.append("]\n"); break; case LOP_MULK: formatAppend(result, "MULK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); - dumpConstant(result, LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); result.append("]\n"); break; case LOP_DIVK: formatAppend(result, "DIVK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); - dumpConstant(result, LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); result.append("]\n"); break; case LOP_IDIVK: formatAppend(result, "IDIVK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); - dumpConstant(result, LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); result.append("]\n"); break; case LOP_MODK: formatAppend(result, "MODK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); - dumpConstant(result, LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); result.append("]\n"); break; case LOP_POWK: formatAppend(result, "POWK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); - dumpConstant(result, LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); result.append("]\n"); break; case LOP_SUBRK: formatAppend(result, "SUBRK R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); - dumpConstant(result, LUAU_INSN_B(insn)); + dumpConstant(result, LUAU_INSN_B(insn), false); formatAppend(result, "] R%d\n", LUAU_INSN_C(insn)); break; case LOP_DIVRK: formatAppend(result, "DIVRK R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); - dumpConstant(result, LUAU_INSN_B(insn)); + dumpConstant(result, LUAU_INSN_B(insn), false); formatAppend(result, "] R%d\n", LUAU_INSN_C(insn)); break; @@ -2242,13 +2652,13 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_ANDK: formatAppend(result, "ANDK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); - dumpConstant(result, LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); result.append("]\n"); break; case LOP_ORK: formatAppend(result, "ORK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); - dumpConstant(result, LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); result.append("]\n"); break; @@ -2311,7 +2721,7 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_DUPCLOSURE: formatAppend(result, "DUPCLOSURE R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_D(insn)); - dumpConstant(result, LUAU_INSN_D(insn)); + dumpConstant(result, LUAU_INSN_D(insn), false); result.append("]\n"); break; @@ -2325,7 +2735,7 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_LOADKX: formatAppend(result, "LOADKX R%d K%d [", LUAU_INSN_A(insn), *code); - dumpConstant(result, *code); + dumpConstant(result, *code, false); result.append("]\n"); code++; break; @@ -2349,7 +2759,7 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_FASTCALL2K: formatAppend(result, "FASTCALL2K %d R%d K%d L%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code, targetLabel); - dumpConstant(result, *code); + dumpConstant(result, *code, false); result.append("]\n"); code++; break; @@ -2397,14 +2807,53 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, case LOP_JUMPXEQKN: formatAppend(result, "JUMPXEQKN R%d K%d L%d%s [", LUAU_INSN_A(insn), *code & 0xffffff, targetLabel, *code >> 31 ? " NOT" : ""); - dumpConstant(result, *code & 0xffffff); + dumpConstant(result, *code & 0xffffff, false); result.append("]\n"); code++; break; case LOP_JUMPXEQKS: formatAppend(result, "JUMPXEQKS R%d K%d L%d%s [", LUAU_INSN_A(insn), *code & 0xffffff, targetLabel, *code >> 31 ? " NOT" : ""); - dumpConstant(result, *code & 0xffffff); + dumpConstant(result, *code & 0xffffff, false); + result.append("]\n"); + code++; + break; + + case LOP_GETUDATAKS: + formatAppend(result, "GETUDATAKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_AUX_KV16(*code)); + dumpConstant(result, LUAU_INSN_AUX_KV16(*code), false); + result.append("]\n"); + code++; + break; + + case LOP_SETUDATAKS: + formatAppend(result, "SETUDATAKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_AUX_KV16(*code)); + dumpConstant(result, LUAU_INSN_AUX_KV16(*code), false); + result.append("]\n"); + code++; + break; + + case LOP_NAMECALLUDATA: + formatAppend(result, "NAMECALLUDATA R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_AUX_KV16(*code)); + dumpConstant(result, LUAU_INSN_AUX_KV16(*code), false); + result.append("]\n"); + code++; + break; + + case LOP_NEWCLASSMEMBER: + formatAppend(result, "NEWCLASSMEMBER R%d R%d [", LUAU_INSN_A(insn), LUAU_INSN_C(insn)); + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + case LOP_CMPPROTO: + formatAppend(result, "CMPPROTO R%d #%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel); + break; + + case LOP_NEWCLASS: + formatAppend(result, "NEWCLASS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code); + dumpConstant(result, *code, false); result.append("]\n"); code++; break; @@ -2423,11 +2872,10 @@ static const char* getBaseTypeString(uint8_t type) return "nil"; case LBC_TYPE_BOOLEAN: return "boolean"; - // ServerLua: added by us for integer constant support - case LBC_TYPE_INTEGER: - return "integer"; case LBC_TYPE_NUMBER: return "number"; + case LBC_TYPE_INTEGER: + return "integer"; case LBC_TYPE_STRING: return "string"; case LBC_TYPE_TABLE: @@ -2452,7 +2900,7 @@ static const char* getBaseTypeString(uint8_t type) std::string BytecodeBuilder::dumpCurrentFunction(std::vector& dumpinstoffs) const { - if ((dumpFlags & Dump_Code) == 0) + if ((dumpFlags & (Dump_Code | Dump_Constants)) == 0) return std::string(); int lastLine = -1; @@ -2533,82 +2981,95 @@ std::string BytecodeBuilder::dumpCurrentFunction(std::vector& dumpinstoffs) } } - std::vector labels(insns.size(), -1); - - // annotate valid jump targets with 0 - for (size_t i = 0; i < insns.size();) + if (dumpFlags & Dump_Constants) { - int target = getJumpTarget(insns[i], uint32_t(i)); - - if (target >= 0) + for (size_t i = 0; i < constants.size(); ++i) { - LUAU_ASSERT(size_t(target) < insns.size()); - labels[target] = 0; + formatAppend(result, "K%d: ", int(i)); + dumpConstant(result, int(i), true); + formatAppend(result, "\n"); } - - i += getOpLength(LuauOpcode(LUAU_INSN_OP(insns[i]))); - LUAU_ASSERT(i <= insns.size()); } - int nextLabel = 0; + if (dumpFlags & Dump_Code) + { + std::vector labels(insns.size(), -1); - // compute label ids (sequential integers for all jump targets) - for (size_t i = 0; i < labels.size(); ++i) - if (labels[i] == 0) - labels[i] = nextLabel++; + // annotate valid jump targets with 0 + for (size_t i = 0; i < insns.size();) + { + int target = getJumpTarget(insns[i], uint32_t(i)); - dumpinstoffs.resize(insns.size() + 1, -1); + if (target >= 0) + { + LUAU_ASSERT(size_t(target) < insns.size()); + labels[target] = 0; + } - for (size_t i = 0; i < insns.size();) - { - const uint32_t* code = &insns[i]; - uint8_t op = LUAU_INSN_OP(*code); + i += getOpLength(LuauOpcode(LUAU_INSN_OP(insns[i]))); + LUAU_ASSERT(i <= insns.size()); + } - dumpinstoffs[i] = int(result.size()); + int nextLabel = 0; - if (op == LOP_PREPVARARGS) - { - // Don't emit function header in bytecode - it's used for call dispatching and doesn't contain "interesting" information - i++; - continue; - } + // compute label ids (sequential integers for all jump targets) + for (size_t i = 0; i < labels.size(); ++i) + if (labels[i] == 0) + labels[i] = nextLabel++; + + dumpinstoffs.resize(insns.size() + 1, -1); - if (dumpFlags & Dump_Remarks) + for (size_t i = 0; i < insns.size();) { - while (nextRemark < debugRemarks.size() && debugRemarks[nextRemark].first == i) + const uint32_t* code = &insns[i]; + uint8_t op = LUAU_INSN_OP(*code); + + dumpinstoffs[i] = int(result.size()); + + if (op == LOP_PREPVARARGS) { - formatAppend(result, "REMARK %s\n", debugRemarkBuffer.c_str() + debugRemarks[nextRemark].second); - nextRemark++; + // Don't emit function header in bytecode - it's used for call dispatching and doesn't contain "interesting" information + i++; + continue; } - } - if (dumpFlags & Dump_Source) - { - int line = lines[i]; + if (dumpFlags & Dump_Remarks) + { + while (nextRemark < debugRemarks.size() && debugRemarks[nextRemark].first == i) + { + formatAppend(result, "REMARK %s\n", debugRemarkBuffer.c_str() + debugRemarks[nextRemark].second); + nextRemark++; + } + } - if (line > 0 && line != lastLine) + if (dumpFlags & Dump_Source) { - LUAU_ASSERT(size_t(line - 1) < dumpSource.size()); - formatAppend(result, "%5d: %s\n", line, dumpSource[line - 1].c_str()); - lastLine = line; + int line = lines[i]; + + if (line > 0 && line != lastLine) + { + LUAU_ASSERT(size_t(line - 1) < dumpSource.size()); + formatAppend(result, "%5d: %s\n", line, dumpSource[line - 1].c_str()); + lastLine = line; + } } - } - if (dumpFlags & Dump_Lines) - formatAppend(result, "%d: ", lines[i]); + if (dumpFlags & Dump_Lines) + formatAppend(result, "%d: ", lines[i]); - if (labels[i] != -1) - formatAppend(result, "L%d: ", labels[i]); + if (labels[i] != -1) + formatAppend(result, "L%d: ", labels[i]); - int target = getJumpTarget(*code, uint32_t(i)); + int target = getJumpTarget(*code, uint32_t(i)); - dumpInstruction(code, result, target >= 0 ? labels[target] : -1); + dumpInstruction(code, result, target >= 0 ? labels[target] : -1); - i += getOpLength(LuauOpcode(op)); - LUAU_ASSERT(i <= insns.size()); - } + i += getOpLength(LuauOpcode(op)); + LUAU_ASSERT(i <= insns.size()); + } - dumpinstoffs[insns.size()] = int(result.size()); + dumpinstoffs[insns.size()] = int(result.size()); + } return result; } @@ -2737,6 +3198,18 @@ std::string BytecodeBuilder::dumpTypeInfo() const return result; } +std::vector BytecodeBuilder::getStringTable() +{ + std::vector strings; + strings.resize(stringTable.size()); + for (auto& p : stringTable) + { + LUAU_ASSERT(p.second > 0 && p.second <= strings.size()); + strings[p.second - 1] = std::string_view(p.first.data, p.first.length); + } + return strings; +} + void BytecodeBuilder::annotateInstruction(std::string& result, uint32_t fid, uint32_t instpos) const { if ((dumpFlags & Dump_Code) == 0) diff --git a/Bytecode/src/BytecodeGraph.cpp b/Bytecode/src/BytecodeGraph.cpp new file mode 100644 index 00000000..a05e07d0 --- /dev/null +++ b/Bytecode/src/BytecodeGraph.cpp @@ -0,0 +1,434 @@ +#include "Luau/BytecodeBuilder.h" +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeWire.h" + +#include "BytecodeGraphParser.h" +#include "BytecodeGraphSerializer.h" + +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAG(LuauCostModel) +LUAU_FASTFLAG(LuauCallFeedback) + +namespace Luau +{ +namespace Bytecode +{ + +static std::string_view readString(std::vector& strings, const char* data, size_t& offset) +{ + uint32_t stringId = readVarInt(data, offset); + LUAU_ASSERT(stringId <= strings.size()); + if (stringId == 0) + return ""; + return strings[stringId - 1]; +} + +std::optional fromFunctionBytecode(std::string bytecode, std::vector& strings) +{ + CompTimeBcFunction fn; + size_t offset = 0; + const char* data = bytecode.data(); + fn.maxstacksize = read(data, offset); + fn.numparams = read(data, offset); + fn.nups = read(data, offset); + fn.is_vararg = read(data, offset); + fn.flags = read(data, offset); + + uint32_t typesSize = readVarInt(data, offset); + if (typesSize > 0) + { + uint32_t typeInfoSize = readVarInt(data, offset); + uint32_t typedUpvalSize = readVarInt(data, offset); + uint32_t typedLocalSize = readVarInt(data, offset); + fn.typeInfo = bytecode.substr(offset, typeInfoSize); + offset += typeInfoSize; + + fn.upvalueTypes.resize(typedUpvalSize); + for (uint32_t i = 0; i < typedUpvalSize; i++) + fn.upvalueTypes[i] = static_cast(read(data, offset)); + + fn.localTypes.resize(typedLocalSize); + for (uint32_t i = 0; i < typedLocalSize; i++) + { + LuauBytecodeType type = static_cast(read(data, offset)); + uint8_t reg = read(data, offset); + uint32_t startpc = readVarInt(data, offset); + uint32_t endpc = startpc + readVarInt(data, offset); + fn.localTypes[i] = {type, reg, startpc, endpc}; + } + } + + int32_t codesize = readVarInt(data, offset); + // ServerLua: We need to copy this to a 4-byte aligned memory address to appease UBSan. + std::vector code(codesize); + memcpy(code.data(), data + offset, size_t(codesize) * sizeof(Instruction)); + + offset += codesize * sizeof(Instruction); + + // read constants + const uint32_t sizek = readVarInt(data, offset); + fn.constants.resize(sizek); + for (uint32_t i = 0; i < sizek; i++) + { + uint8_t constType = read(data, offset); + switch (constType) + { + case LBC_CONSTANT_NIL: + fn.constants[i].kind = BcVmConstKind::Nil; + break; + + case LBC_CONSTANT_BOOLEAN: + { + fn.constants[i].kind = BcVmConstKind::Boolean; + fn.constants[i].valueBoolean = read(data, offset); + break; + } + + case LBC_CONSTANT_NUMBER: + { + fn.constants[i].kind = BcVmConstKind::Number; + fn.constants[i].valueNumber = read(data, offset); + break; + } + + case LBC_CONSTANT_VECTOR: + { + fn.constants[i].kind = BcVmConstKind::Vectorf; + fn.constants[i].valueVectorf[0] = read(data, offset); + fn.constants[i].valueVectorf[1] = read(data, offset); + fn.constants[i].valueVectorf[2] = read(data, offset); + fn.constants[i].valueVectorf[3] = read(data, offset); + break; + } + + case LBC_CONSTANT_VECTORD: + { + fn.constants[i].kind = BcVmConstKind::Vectord; + fn.constants[i].valueVectord[0] = read(data, offset); + fn.constants[i].valueVectord[1] = read(data, offset); + fn.constants[i].valueVectord[2] = read(data, offset); + fn.constants[i].valueVectord[3] = read(data, offset); + break; + } + + case LBC_CONSTANT_STRING: + { + fn.constants[i].kind = BcVmConstKind::String; + fn.constants[i].valueString = readString(strings, data, offset); + break; + } + + case LBC_CONSTANT_IMPORT: + { + fn.constants[i].kind = BcVmConstKind::Import; + fn.constants[i].valueImport = read(data, offset); + break; + } + + case LBC_CONSTANT_TABLE: + case LBC_CONSTANT_TABLE_WITH_CONSTANTS: + { + fn.constants[i].kind = BcVmConstKind::Table; + fn.constants[i].valueTable = uint32_t(fn.tableShapes.size()); + + BytecodeBuilder::TableShape shape; + shape.length = readVarInt(data, offset); + shape.hasConstants = constType == LBC_CONSTANT_TABLE_WITH_CONSTANTS; + + for (uint32_t i = 0; i < shape.length; ++i) + { + uint32_t key = readVarInt(data, offset); + LUAU_ASSERT(key < sizek); + shape.keys[i] = key; + if (shape.hasConstants) + { + int32_t value = read(data, offset); + LUAU_ASSERT(value < static_cast(sizek)); + shape.constants[i] = value; + } + } + fn.tableShapes.push_back(shape); + break; + } + + case LBC_CONSTANT_CLOSURE: + { + fn.constants[i].kind = BcVmConstKind::Closure; + fn.constants[i].valueClosure = readVarInt(data, offset); + break; + } + + case LBC_CONSTANT_INTEGER: + { + fn.constants[i].kind = BcVmConstKind::Integer; + bool isNegative = read(data, offset); + uint64_t magnitude = readVarInt64(data, offset); + fn.constants[i].valueInteger = isNegative ? (int64_t)(~magnitude + 1) : (int64_t)magnitude; + break; + } + + case LBC_CONSTANT_CLASS_SHAPE: + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + + fn.constants[i].kind = BcVmConstKind::ClassShape; + fn.constants[i].valueClassShape = uint32_t(fn.classShapes.size()); + + BytecodeBuilder::ClassShape shape; + shape.className = readVarInt(data, offset); + + uint32_t numProps = readVarInt(data, offset); + uint32_t numMethods = readVarInt(data, offset); + + shape.propertyNames.resize(numProps); + shape.methodNames.resize(numMethods); + + for (uint32_t i = 0; i < numProps; ++i) + shape.propertyNames.emplace_back(readVarInt(data, offset)); + + for (uint32_t i = 0; i < numMethods; ++i) + shape.methodNames.emplace_back(readVarInt(data, offset)); + + fn.classShapes.push_back(shape); + break; + } + + default: + LUAU_ASSERT(!"Unknown constant type!"); + } + } + + uint32_t psize = readVarInt(data, offset); + fn.protos.resize(psize); + for (uint32_t i = 0; i < psize; i++) + fn.protos[i] = readVarInt(data, offset); + + fn.linedefined = readVarInt(data, offset); + fn.debugname = readString(strings, data, offset); + + uint8_t lineinfo = read(data, offset); + std::vector lines; + + if (lineinfo != 0) + { + uint8_t linegaplog2 = read(data, offset); + + int intervals = ((codesize - 1) >> linegaplog2) + 1; + int absoffset = (codesize + 3) & ~3; + + const int sizelineinfo = absoffset + intervals * sizeof(int); + std::vector lineinfo; + lineinfo.resize(sizelineinfo); + int* abslineinfo = reinterpret_cast(lineinfo.data() + absoffset); + + uint8_t lastoffset = 0; + for (int i = 0; i < codesize; i++) + { + lastoffset += read(data, offset); + lineinfo[i] = lastoffset; + } + + int lastline = 0; + for (int i = 0; i < intervals; i++) + { + lastline += read(data, offset); + abslineinfo[i] = lastline; + } + lines.resize(codesize); + for (int i = 0; i < codesize; i++) + lines[i] = abslineinfo[i >> linegaplog2] + lineinfo[i]; + } + + uint8_t debuginfo = read(data, offset); + + if (debuginfo != 0) + { + const int sizelocvars = readVarInt(data, offset); + fn.locals.resize(sizelocvars); + + for (int i = 0; i < sizelocvars; i++) + { + std::string_view varname = readString(strings, data, offset); + uint32_t startpc = readVarInt(data, offset); + uint32_t endpc = readVarInt(data, offset); + uint8_t reg = read(data, offset); + fn.locals[i] = {varname, reg, startpc, endpc}; + } + + const int sizeupvalues = readVarInt(data, offset); + fn.upvalueNames.resize(sizeupvalues); + + for (int i = 0; i < sizeupvalues; i++) + fn.upvalueNames[i] = readString(strings, data, offset); + } + + // ServerLua: yield points, recomputed by tagYieldPoints() on re-serialization. + uint32_t numyields = readVarInt(data, offset); + for (uint32_t j = 0; j < numyields; j++) + readVarInt(data, offset); + + if (FFlag::LuauCallFeedback) + { + uint32_t feedbackvecsize = readVarInt(data, offset); + for (uint32_t j = 0; j < feedbackvecsize; j++) + { + uint8_t slottype = read(data, offset); + LUAU_ASSERT(slottype == LFT_CALLTARGET); + // read slot PC. ignore it for now. + readVarInt(data, offset); + } + } + + if (FFlag::LuauCostModel) + { + if ((fn.flags & LPF_INLINABLE) != 0) + readVarInt64(data, offset); + } + + std::vector insnsPC; + BytecodeGraphParser graphParser(fn); + if (!graphParser.rebuildGraph(code.data(), codesize, lines, insnsPC)) + return {}; + + for (TypedLocal& l : fn.localTypes) + { + l.startpc = l.startpc < insnsPC.size() ? insnsPC[l.startpc] : codesize; + l.endpc = l.endpc < insnsPC.size() ? insnsPC[l.endpc] : codesize; + } + + for (DebugLocal& l : fn.locals) + { + l.startpc = l.startpc < insnsPC.size() ? insnsPC[l.startpc] : codesize; + l.endpc = l.endpc < insnsPC.size() ? insnsPC[l.endpc] : codesize; + } + + return {fn}; +} + +struct CompTimeBytecodeGraphSerializer : public BytecodeGraphSerializer +{ + std::vector& consts; + CompTimeBytecodeGraphSerializer(BytecodeBuilder& bcb, CompTimeBcFunction& fn, std::vector& consts) + : BytecodeGraphSerializer(bcb, fn) + , consts(consts) + { + } + + uint32_t getVmConstInputRaw(BcInst& insn, uint8_t index) override + { + uint32_t cid = BytecodeGraphSerializer::getVmConstInputRaw(insn, index); + LUAU_ASSERT(cid < consts.size()); + return consts[cid]; + } +}; + +std::string toFunctionBytecode(BytecodeBuilder& bcb, CompTimeBcFunction& fn) +{ + uint32_t functionId = bcb.beginFunction(fn.numparams, fn.is_vararg); + if (fn.debugname != "") + bcb.setDebugFunctionName({fn.debugname.data(), fn.debugname.size()}); + bcb.setDebugFunctionLineDefined(fn.linedefined); + bcb.setFunctionTypeInfo(fn.typeInfo); + for (LuauBytecodeType t : fn.upvalueTypes) + bcb.pushUpvalTypeInfo(t); + for (auto& upval : fn.upvalueNames) + bcb.pushDebugUpval({upval.data(), upval.size()}); + + std::vector consts; + consts.reserve(fn.constants.size()); + for (auto& c : fn.constants) + { + switch (c.kind) + { + case BcVmConstKind::Nil: + consts.push_back(bcb.addConstantNil()); + break; + + case BcVmConstKind::Boolean: + consts.push_back(bcb.addConstantBoolean(c.valueBoolean)); + break; + + case BcVmConstKind::Number: + consts.push_back(bcb.addConstantNumber(c.valueNumber)); + break; + + case BcVmConstKind::Vectorf: + consts.push_back(bcb.addConstantVectorf(c.valueVectorf[0], c.valueVectorf[1], c.valueVectorf[2], c.valueVectorf[3])); + break; + + case BcVmConstKind::Vectord: + consts.push_back(bcb.addConstantVectord(c.valueVectord[0], c.valueVectord[1], c.valueVectord[2], c.valueVectord[3])); + break; + + case BcVmConstKind::String: + consts.push_back(bcb.addConstantString({c.valueString.data(), c.valueString.size()})); + break; + + case BcVmConstKind::Import: + consts.push_back(bcb.addImport(c.valueImport)); + break; + + case BcVmConstKind::Table: + { + LUAU_ASSERT(c.valueTable < fn.tableShapes.size()); + consts.push_back(bcb.addConstantTable(fn.tableShapes[c.valueTable])); + break; + } + + case BcVmConstKind::Closure: + consts.push_back(bcb.addConstantClosure(c.valueClosure)); + break; + + case BcVmConstKind::Integer: + consts.push_back(bcb.addConstantInteger(c.valueInteger)); + break; + + case BcVmConstKind::ClassShape: + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + LUAU_ASSERT(c.valueClassShape < fn.classShapes.size()); + consts.push_back(bcb.addClassShape(fn.classShapes[c.valueClassShape])); + break; + } + } + } + + for (auto fid : fn.protos) + bcb.addChildFunction(fid); + + CompTimeBytecodeGraphSerializer serializer(bcb, fn, consts); + std::vector insnsPC = serializer.emitBytecode(); + + for (auto& local : fn.localTypes) + { + uint32_t startpc = local.startpc < insnsPC.size() ? insnsPC[local.startpc] : bcb.getDebugPC(); + uint32_t endpc = local.endpc < insnsPC.size() ? insnsPC[local.endpc] : bcb.getDebugPC(); + bcb.pushLocalTypeInfo(local.type, local.reg, startpc, endpc); + } + for (auto& local : fn.locals) + { + uint32_t startpc = local.startpc < insnsPC.size() ? insnsPC[local.startpc] : bcb.getDebugPC(); + uint32_t endpc = local.endpc < insnsPC.size() ? insnsPC[local.endpc] : bcb.getDebugPC(); + bcb.pushDebugLocal({local.varname.data(), local.varname.size()}, local.reg, startpc, endpc); + } + + bcb.foldJumps(); + + bcb.expandJumps(); + + bcb.endFunction(fn.maxstacksize, fn.nups, fn.flags); + + if (serializer.error) + return ""; + + return bcb.getFunctionData(functionId); +} + +std::string toFunctionBytecode(CompTimeBcFunction& fn) +{ + BytecodeBuilder bcb; + return toFunctionBytecode(bcb, fn); +} + +}; // namespace Bytecode +}; // namespace Luau diff --git a/Bytecode/src/BytecodeGraphParser.h b/Bytecode/src/BytecodeGraphParser.h new file mode 100644 index 00000000..8dac36a0 --- /dev/null +++ b/Bytecode/src/BytecodeGraphParser.h @@ -0,0 +1,1047 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeUtils.h" +#include "Luau/Common.h" + +#include +#include +#include + +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) + +namespace Luau +{ +namespace Bytecode +{ + +template +struct BytecodeGraphParser +{ + struct BlockProducers + { + std::unordered_map own; + std::unordered_map cached; + BcOp multiReturn; + Reg multiReturnStart; + int invalidAfter = 255; + // incomplete phi construction state: + // - a block is sealed once all its predecessors have been emitted + // - reads that cross a not-yet-emitted predecessor (a back-edge) + // create an operand-less phi in incompletePhis until the block is sealed and the operands can be filled + // - incomplete phis are filled in with operands that all occupy the same register, after all preds have been emitted + // + // this enables loop phis to be created without a separate pass (Braun "Simple and Efficient Construction of Static Single Assignment Form") + bool sealed = false; + uint32_t unsealedPreds = 0; + std::unordered_map incompletePhis; + }; + + using Producers = std::vector; + + BcFunction& func; + std::unordered_map blockByPC; + Producers producers; + BcOp currentBlock; + std::unordered_map phiBlock; + + BytecodeGraphParser(BcFunction& func) + : func(func) + { + } + + void addSuccessor(BcOp fromOp, BcOp toOp, BcBlockEdgeKind kind) + { + BcBlock& from = func.blockOp(fromOp); + BcBlock& to = func.blockOp(toOp); + from.successors.push_back({kind, toOp}); + to.predecessors.push_back({kind, fromOp}); + } + + BcOp makeBlock(uint32_t pc) + { + BcOp newBlockOp = func.addBlock(); + blockByPC[pc] = newBlockOp; + BcBlock& newBlock = func.blockOp(newBlockOp); + newBlock.sortkey = pc; + return newBlockOp; + } + + bool isJumpTrampoline(uint32_t pc, const Instruction* code, uint32_t codesize) + { + return LuauOpcode(LUAU_INSN_OP(code[pc])) == LOP_JUMP && pc + 1 < codesize && LuauOpcode(LUAU_INSN_OP(code[pc + 1])) == LOP_JUMPX && + static_cast(getJumpTarget(code[pc + 2], pc + 2)) == pc + 1; + } + + size_t rebuildBlocks(const Instruction code[], uint32_t codesize) + { + BcOp entryBlock = func.entryBlock = makeBlock(0); + BcOp exitBlock = func.exitBlock = makeBlock(kBlockNoStartPc); + uint32_t i = 0; + BcOp currentBlock = entryBlock; + size_t instructionCount = 0; + while (i < codesize) + { + Instruction insn = code[i]; + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + int target = getJumpTarget(insn, i); + if (target >= 0 && LuauOpcode(LUAU_INSN_OP(code[target])) == LOP_JUMPX) + target = getJumpTarget(code[target], target); + + bool needsBlock = target >= 0 && !isFastCall(op) && op != LOP_JUMPX && !isJumpTrampoline(i, code, codesize); + if (needsBlock) + { + if (blockByPC.count(target) == 0) + { + BcOp newBlockOp = makeBlock(target); + if (target < static_cast(i)) // We are jumping back. + { + // The new block was created in the middle of the existing one. + // We need to maintain predecessor/successor relations. + uint32_t blockStartPc = target - 1; + while (blockByPC.count(blockStartPc) == 0 && blockStartPc-- != 0) + ; + LUAU_ASSERT(blockByPC.count(blockStartPc) > 0); + BcOp prevBlockOp = blockByPC[blockStartPc]; + BcBlock& prevBlock = func.blockOp(prevBlockOp); + BcBlock& newBlock = func.blockOp(newBlockOp); + // Steal successors of the previous block. + newBlock.successors = prevBlock.successors; + // Now it should only fallsthrough to the new block. + prevBlock.successors.clear(); + addSuccessor(prevBlockOp, newBlockOp, BcBlockEdgeKind::Fallthrough); + // Update all successors to have the new block as a predecessor instead of the old one. + for (auto& edge : newBlock.successors) + for (auto& backEdge : func.blockOp(edge.target).predecessors) + if (backEdge.target == prevBlockOp) + backEdge.target = newBlockOp; + } + } + addSuccessor(currentBlock, blockByPC[target], isLoopJump(op) ? BcBlockEdgeKind::Loop : BcBlockEdgeKind::Branch); + } + if (op == LOP_RETURN) + addSuccessor(currentBlock, exitBlock, BcBlockEdgeKind::Fallthrough); + i += getOpLength(op); + if ((needsBlock || (op == LOP_RETURN && i < codesize)) && blockByPC.count(i) == 0) + makeBlock(i); + + if (blockByPC.count(i) != 0) + { + if (isFallthrough(op)) + addSuccessor(currentBlock, blockByPC[i], BcBlockEdgeKind::Fallthrough); + currentBlock = blockByPC[i]; + } + instructionCount++; + } + return instructionCount; + } + + BcOp makePhi(BcOp block, Reg reg) + { + BcOp phiOp = func.addPhi(); + func.regs[phiOp] = reg; + func.blockOp(block).phis.push_back(phiOp); + phiBlock[phiOp] = block; + return phiOp; + } + + std::optional readVariable(BcOp block, Reg reg) + { + BlockProducers& bp = producers.at(block.index); + if (static_cast(reg) > bp.invalidAfter) + return {}; + if (auto it = bp.own.find(reg); it != bp.own.end()) + return it->second; + if (auto it = bp.cached.find(reg); it != bp.cached.end()) + return it->second; + if (bp.multiReturn.kind != BcOpKind::None && reg >= bp.multiReturnStart) + { + // cache the projection so repeated reads (and phi operands across edges) share identity, + // which keeps tryRemoveTrivialPhi able to recognize equal operands + BcOp proj = func.addProj(bp.multiReturn, reg - bp.multiReturnStart); + bp.cached[reg] = proj; + return proj; + } + return readVariableRecursive(block, reg); + } + + BcOp readVariableRecursive(BcOp block, Reg reg) + { + BlockProducers& bp = producers.at(block.index); + BcEdges& preds = func.blockOp(block).predecessors; + + if (!bp.sealed) + { + // predecessors not all emitted yet, so we create an incomplete phi to fill on seal + BcOp phiOp = makePhi(block, reg); + bp.incompletePhis[reg] = phiOp; + bp.cached[reg] = phiOp; + return phiOp; + } + + if (preds.empty()) + return BcOp{BcOpKind::VmReg, reg}; // undefined (entry/unreachable) + + if (preds.size() == 1) + { + // No phi needed, as a single-predecessor block can never a loop header so this can only cycle inside a fully-unreachable single-pred loop + // we cache anyway so if we have a re-entry, we just resolve to undef, rather than recursing forever + bp.cached[reg] = BcOp{BcOpKind::VmReg, reg}; + BcOp val = readVariable(preds[0].target, reg).value_or(BcOp{BcOpKind::VmReg, reg}); + producers.at(block.index).cached[reg] = val; + return val; + } + + // multiple predecessors: create the phi and cache it *before* filling so a back-edge read + // that recurses back into this block resolves to the phi instead of looping forever + BcOp phiOp = makePhi(block, reg); + bp.cached[reg] = phiOp; + BcOp val = addPhiOperands(reg, phiOp, block); + producers.at(block.index).cached[reg] = val; + return val; + } + + BcOp addPhiOperands(Reg reg, BcOp phiOp, BcOp block) + { + LUAU_ASSERT(phiOp.kind == BcOpKind::Phi); + + BcRef phi = func.phi(phiOp); + + // include every predecessor edge, back-edges included + for (auto& [_, pred] : func.blockOp(block).predecessors) + { + if (std::optional v = readVariable(pred, reg)) + { + func.addUse(phi, *v); + } + } + return tryRemoveTrivialPhi(phiOp); + } + + // collapse a phi whose operands (ignoring self-references) are a single distinct value + // NOTE: a self-referential loop phi left in place would make the serializer's getRegister recurse forever + BcOp tryRemoveTrivialPhi(BcOp phiOp) + { + std::optional trivialValue = std::nullopt; + for (BcOp op : func.phiOp(phiOp).ops) + { + if (op == phiOp || (trivialValue.has_value() && op == *trivialValue)) + continue; + + if (trivialValue.has_value()) + return phiOp; // two distinct values, we cannot eliminate this + + trivialValue = op; + } + + Reg reg = static_cast(func.regs.at(phiOp)); + if (!trivialValue.has_value()) + trivialValue = BcOp{BcOpKind::VmReg, reg}; // unreachable or undefined, so we collapse to an VmReg read + + BcRef phiRef = func.phi(phiOp); + std::vector users = std::move(phiRef->uses); + + // we need to now update users to point to the new trivial value + for (BcOp user : users) + { + if (user == phiOp) + continue; + + BcOps& userOps = (user.kind == BcOpKind::Phi) ? func.phiOp(user).ops : func.instOp(user).ops; + for (BcOp& op : userOps) + if (op == phiOp) + { + op = *trivialValue; + + // re-record the reverse edge on whichever def now owns this operand + func.recordUse(*trivialValue, user); + } + } + + // remove the collapsed phi from its block + if (auto bit = phiBlock.find(phiOp); bit != phiBlock.end()) + { + func.blockOp(bit->second).phis.remove(phiOp); + BlockProducers& bp = producers.at(bit->second.index); + if (auto cit = bp.cached.find(reg); cit != bp.cached.end() && cit->second == phiOp) + cit->second = *trivialValue; + phiBlock.erase(bit); + } + + for (BcOp user : users) + if (user.kind == BcOpKind::Phi && user != phiOp) + tryRemoveTrivialPhi(user); + + return *trivialValue; + } + + void sealBlock(BcOp block) + { + BlockProducers& bp = producers.at(block.index); + if (bp.sealed) + return; + // mark sealed first so reads triggered while filling use the normal (non-incomplete) path + bp.sealed = true; + std::vector> pending(bp.incompletePhis.begin(), bp.incompletePhis.end()); + bp.incompletePhis.clear(); + for (auto& [reg, phiOp] : pending) + { + BcOp val = addPhiOperands(reg, phiOp, block); + BlockProducers& cur = producers.at(block.index); + if (auto cit = cur.cached.find(reg); cit != cur.cached.end() && cit->second == phiOp) + cit->second = val; + } + } + + void finalizeBlock(BcOp block) + { + for (auto& [_, succ] : func.blockOp(block).successors) + { + BlockProducers& sp = producers.at(succ.index); + if (sp.unsealedPreds > 0) + { + --sp.unsealedPreds; + if (sp.unsealedPreds == 0) + sealBlock(succ); + } + } + } + + void sealAllRemaining() + { + for (uint32_t b = 0; b < func.blocks.size(); b++) + if (!producers.at(b).sealed) + sealBlock(BcOp{BcOpKind::Block, b}); + } + + std::vector findProducersUpToTop(BcOp block, Reg reg) + { + // We assume it called only for search of var return calls. + LUAU_ASSERT(block.index < producers.size()); + BlockProducers& blockProducers = producers.at(block.index); + // So we need to find all producers from reg to blockProducers.multiReturnStart. + LUAU_ASSERT(blockProducers.multiReturn.kind == BcOpKind::Inst); + std::vector res; + res.reserve(blockProducers.multiReturnStart - reg + 1); + for (; reg < blockProducers.multiReturnStart; reg++) + { + auto staticRegOp = readVariable(block, reg); + LUAU_ASSERT(staticRegOp); + res.push_back(*staticRegOp); + } + res.push_back(blockProducers.multiReturn); + // multireturn is consumed, clean it up + blockProducers.multiReturn = BcOp{}; + blockProducers.multiReturnStart = 0xFF; + return res; + } + + bool isUnreachable(BcOp blockOp) + { + if (blockOp == func.entryBlock) + return false; + BcBlock& block = func.blockOp(blockOp); + for (auto [ctrl, pred] : block.predecessors) + { + if (ctrl == BcBlockEdgeKind::Loop) + continue; + if (!isUnreachable(pred)) + return false; + } + return true; + } + + void addProducer(Reg reg, BcOp op) + { + BlockProducers& blockProducers = producers[currentBlock.index]; + blockProducers.own[reg] = op; + func.regs[op] = reg; + blockProducers.invalidAfter = std::max(static_cast(reg), blockProducers.invalidAfter); + } + + void applyCall(BlockProducers& producers, BcOp callOp, Reg targetReg, int nresults) + { + for (auto it = producers.own.begin(); it != producers.own.end();) + { + if (it->first >= targetReg) + { + it = producers.own.erase(it); + } + else + { + ++it; + } + } + for (auto it = producers.cached.begin(); it != producers.cached.end();) + { + if (it->first >= targetReg) + { + it = producers.cached.erase(it); + } + else + { + ++it; + } + } + if (nresults < 0) + { + producers.multiReturn = callOp; + producers.multiReturnStart = targetReg; + producers.invalidAfter = 255; + } + else + { + producers.invalidAfter = static_cast(targetReg) - 1 + nresults; + } + } + + void addImmInput(BcRef inst, bool value) + { + BcOp op{BcOpKind::Imm, 0}; + func.immediates.push_back({BcImmKind::Boolean}); + func.immediates.back().valueBoolean = value; + op.index = func.immediates.size() - 1; + func.addUse(inst, op); + } + + void addImmInput(BcRef inst, int32_t value) + { + BcOp op{BcOpKind::Imm, 0}; + func.immediates.push_back({BcImmKind::Int}); + func.immediates.back().valueInt = value; + op.index = func.immediates.size() - 1; + func.addUse(inst, op); + } + + void addImmInput(BcRef inst, uint32_t value) + { + BcOp op{BcOpKind::Imm, 0}; + func.immediates.push_back({BcImmKind::Import}); + func.immediates.back().valueImport = value; + op.index = func.immediates.size() - 1; + func.addUse(inst, op); + } + + void addVmConstInput(BcRef inst, uint32_t idx) + { + LUAU_ASSERT(idx < func.constants.size()); + func.addUse(inst, BcOp{BcOpKind::VmConst, idx}); + } + + void addUpvalInput(BcRef inst, uint32_t idx) + { + LUAU_ASSERT(idx < func.nups); + func.addUse(inst, BcOp{BcOpKind::VmUpvalue, idx}); + } + + void addProtoInput(BcRef inst, uint32_t idx) + { + func.addUse(inst, BcOp{BcOpKind::VmProto, idx}); + } + + void addVmRegInput(BcRef inst, Reg reg) + { + std::optional source = readVariable(currentBlock, reg); + if (!source && isUnreachable(currentBlock)) + { + func.addUse(inst, BcOp{BcOpKind::VmReg, reg}); + return; + } + LUAU_ASSERT(source); + func.addUse(inst, *source); + } + + void addJumpInput(BcRef inst, int target) + { + LUAU_ASSERT(!isFastCall(inst->op)); + if (target < 0) + { + LUAU_ASSERT(inst->op == LOP_LOADB); + return; + } + auto it = blockByPC.find(target); + LUAU_ASSERT(it != blockByPC.end()); + func.addUse(inst, it->second); + } + + static const uint32_t kMaxCFGBlocks = 1000; + + bool rebuildGraph(const Instruction code[], uint32_t codesize, std::vector& lines, std::vector& pcs) + { + size_t instructionsCount = rebuildBlocks(code, codesize); + if (blockByPC.size() > kMaxCFGBlocks) + return false; + + producers.resize(func.blocks.size()); + pcs.resize(codesize); + + currentBlock = func.entryBlock; + + for (Reg i = 0; i < func.numparams; i++) + addProducer(i, {BcOpKind::VmReg, i}); + + // a block is sealable once all its predecessors are emitted; seed the counts from the CFG + // (already fully built by rebuildBlocks) and seal blocks with no predecessors immediately + for (BcBlock& block : func.blocks) + producers.at(func.getBlockIndex(block)).unsealedPreds = uint32_t(block.predecessors.size()); + for (uint32_t blockIdx = 0; blockIdx < func.blocks.size(); blockIdx++) + if (producers.at(blockIdx).unsealedPreds == 0) + sealBlock(BcOp{BcOpKind::Block, blockIdx}); + + // Create instructions. + currentBlock = func.entryBlock; + func.instructions.reserve(instructionsCount); + + for (uint32_t i = 0; i < codesize;) + { + Instruction insn = code[i]; + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + int opLength = getOpLength(op); + uint32_t aux = (opLength > 1 && i + 1 < codesize) ? code[i + 1] : 0; + BcOp nodeOp = func.addInst(); + func.blockOp(currentBlock).appendInstruction(nodeOp); + BcRef node = func.inst(nodeOp); + node->block = currentBlock; + if (i < lines.size()) + node->line = lines[i]; + node->op = op; + + pcs[i] = nodeOp.index; + + auto parseJump = [&](LuauOpcode op, int jumpTarget) -> void + { + node->op = op; + switch (op) + { + case LOP_JUMPXEQKNIL: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(aux >> 31)); + addJumpInput(node, jumpTarget); + break; + + case LOP_JUMPXEQKB: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(aux >> 31)); + addJumpInput(node, jumpTarget); + addImmInput(node, static_cast(aux & 0x1)); + break; + + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(aux >> 31)); + addJumpInput(node, jumpTarget); + addVmConstInput(node, aux & 0xFFFFFF); + break; + + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + addVmRegInput(node, LUAU_INSN_A(insn)); + addJumpInput(node, jumpTarget); + break; + + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, aux); + addJumpInput(node, jumpTarget); + break; + + case LOP_FORNPREP: + // forg loop protocol: A, A+1, A+2 are used for iteration protocol; A+3, ... are loop variables + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_A(insn) + 1); + addVmRegInput(node, LUAU_INSN_A(insn) + 2); + addJumpInput(node, jumpTarget); + func.regs[nodeOp] = LUAU_INSN_A(insn); + addProducer(LUAU_INSN_A(insn), func.addProj(nodeOp, 0)); + addProducer(LUAU_INSN_A(insn) + 1, func.addProj(nodeOp, 1)); + addProducer(LUAU_INSN_A(insn) + 2, func.addProj(nodeOp, 2)); + break; + + case LOP_FORNLOOP: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_A(insn) + 1); + addVmRegInput(node, LUAU_INSN_A(insn) + 2); + addJumpInput(node, jumpTarget); + break; + + default: + LUAU_UNREACHABLE(); + } + }; + switch (op) + { + case LOP_NOP: + case LOP_BREAK: + case LOP_NATIVECALL: + break; + + case LOP_LOADNIL: + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_LOADB: + addImmInput(node, static_cast(LUAU_INSN_B(insn))); + addJumpInput(node, getJumpTarget(insn, i)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_LOADN: + addImmInput(node, static_cast(LUAU_INSN_D(insn))); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_LOADK: + addVmConstInput(node, LUAU_INSN_D(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_MOVE: + addVmRegInput(node, LUAU_INSN_B(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_GETGLOBAL: + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + addVmConstInput(node, aux); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETGLOBAL: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + addVmConstInput(node, aux); + break; + + case LOP_GETUPVAL: + addUpvalInput(node, LUAU_INSN_B(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETUPVAL: + addVmRegInput(node, LUAU_INSN_A(insn)); + addUpvalInput(node, LUAU_INSN_B(insn)); + break; + + case LOP_CLOSEUPVALS: + func.addUse(node, BcOp{BcOpKind::VmReg, LUAU_INSN_A(insn)}); + break; + + case LOP_GETIMPORT: + { + addVmConstInput(node, LUAU_INSN_D(insn)); + int32_t componentsCount = aux >> 30; + addImmInput(node, componentsCount); + for (int32_t i = 0; i < componentsCount; i++) + addVmConstInput(node, (aux >> (20 - 10 * i)) & 0x3FF); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + } + + case LOP_GETTABLE: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETTABLE: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + break; + + case LOP_GETUDATAKS: + case LOP_GETTABLEKS: + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + addVmConstInput(node, aux); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETUDATAKS: + case LOP_SETTABLEKS: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + addVmConstInput(node, aux); + break; + + case LOP_GETTABLEN: + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn) + 1)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETTABLEN: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn) + 1)); + break; + + case LOP_NEWCLOSURE: + addProtoInput(node, LUAU_INSN_D(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_NAMECALLUDATA: + case LOP_NAMECALL: + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + addVmConstInput(node, aux); + func.regs[nodeOp] = LUAU_INSN_A(insn); + addProducer(LUAU_INSN_A(insn), func.addProj(nodeOp, 0)); + addProducer(LUAU_INSN_A(insn) + 1, func.addProj(nodeOp, 1)); + break; + + case LOP_CALL: + case LOP_CALLFB: + { + int nparams = LUAU_INSN_B(insn) - 1; + int nresults = LUAU_INSN_C(insn) - 1; + addImmInput(node, static_cast(nparams)); + addImmInput(node, static_cast(nresults)); + if (op == LOP_CALLFB) + addImmInput(node, static_cast(aux)); + + // Call target. + addVmRegInput(node, LUAU_INSN_A(insn)); + // Fixed arguments. + for (int i = 1; i <= nparams; i++) + addVmRegInput(node, LUAU_INSN_A(insn) + i); + + if (nparams < 0) + { + // all arguments prepared before call in the same block + for (auto& inp : findProducersUpToTop(currentBlock, LUAU_INSN_A(insn) + 1)) + { + func.addUse(node, inp); + } + } + + BlockProducers& blockProducers = producers[currentBlock.index]; + applyCall(blockProducers, nodeOp, LUAU_INSN_A(insn), nresults); + + func.regs[nodeOp] = LUAU_INSN_A(insn); + for (int i = 0; i < nresults; i++) + addProducer(LUAU_INSN_A(insn) + i, func.addProj(nodeOp, i)); + break; + } + + case LOP_RETURN: + { + int nresults = LUAU_INSN_B(insn) - 1; + addImmInput(node, static_cast(nresults)); + for (int i = 0; i < nresults; i++) + addVmRegInput(node, LUAU_INSN_A(insn) + i); + if (nresults < 0) + for (auto& inp : findProducersUpToTop(currentBlock, LUAU_INSN_A(insn))) + func.addUse(node, inp); + if (nresults == 0) + func.addUse(node, BcOp{BcOpKind::VmReg, LUAU_INSN_A(insn)}); + break; + } + + case LOP_JUMP: + { + if (isJumpTrampoline(i, code, codesize)) + { + // it is long jump trampoline + int longOffset = LUAU_INSN_E(code[i + 1]); + i += getOpLength(LOP_JUMP) + getOpLength(LOP_JUMPX); + op = LuauOpcode(LUAU_INSN_OP(code[i])); + opLength = getOpLength(op); + aux = (opLength > 1 && i + 1 < codesize) ? code[i + 1] : 0; + parseJump(op, i + longOffset); + } + else + addJumpInput(node, getJumpTarget(insn, i)); + break; + } + + case LOP_JUMPBACK: + // repeat .. until loops use it for back edge. + addJumpInput(node, getJumpTarget(insn, i)); + break; + + case LOP_JUMPXEQKNIL: + case LOP_JUMPXEQKB: + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + case LOP_FORNPREP: + case LOP_FORNLOOP: + parseJump(op, getJumpTarget(insn, i)); + break; + + case LOP_ADD: + case LOP_SUB: + case LOP_MUL: + case LOP_DIV: + case LOP_MOD: + case LOP_POW: + case LOP_AND: + case LOP_OR: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_ADDK: + case LOP_SUBK: + case LOP_MULK: + case LOP_DIVK: + case LOP_MODK: + case LOP_POWK: + case LOP_ANDK: + case LOP_ORK: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmConstInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_CONCAT: + { + LUAU_ASSERT(LUAU_INSN_B(insn) <= LUAU_INSN_C(insn)); + for (Reg param = LUAU_INSN_B(insn); param <= LUAU_INSN_C(insn); param++) + addVmRegInput(node, param); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + } + + case LOP_NOT: + case LOP_MINUS: + case LOP_LENGTH: + addVmRegInput(node, LUAU_INSN_B(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_NEWTABLE: + addImmInput(node, static_cast(LUAU_INSN_B(insn))); + addImmInput(node, static_cast(aux)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_DUPTABLE: + addProducer(LUAU_INSN_A(insn), nodeOp); + addVmConstInput(node, LUAU_INSN_D(insn)); + break; + + case LOP_SETLIST: + { + int count = LUAU_INSN_C(insn) - 1; + addImmInput(node, static_cast(aux)); + addImmInput(node, static_cast(count)); + addVmRegInput(node, LUAU_INSN_A(insn)); + for (Reg param = 0; param < count; param++) + addVmRegInput(node, LUAU_INSN_B(insn) + param); + if (count < 0) + for (auto inp : findProducersUpToTop(currentBlock, LUAU_INSN_B(insn))) + func.addUse(node, inp); + break; + } + + case LOP_FORGPREP: + case LOP_FORGPREP_NEXT: + case LOP_FORGPREP_INEXT: + { + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_A(insn) + 1); + addVmRegInput(node, LUAU_INSN_A(insn) + 2); + int loopInsnPc = getJumpTarget(insn, i); + addJumpInput(node, loopInsnPc); + LUAU_ASSERT(loopInsnPc + 1 < static_cast(codesize) && LuauOpcode(LUAU_INSN_OP(code[loopInsnPc])) == LOP_FORGLOOP); + int32_t vars = code[loopInsnPc + 1] & 0xFF; + func.regs[nodeOp] = LUAU_INSN_A(insn); + for (int i = 0; i <= std::max(vars, 2); i++) + addProducer(LUAU_INSN_A(insn) + 2 + i, func.addProj(nodeOp, 2 + i)); + break; + } + + case LOP_FORGLOOP: + { + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_A(insn) + 1); + addVmRegInput(node, LUAU_INSN_A(insn) + 2); + addImmInput(node, static_cast(aux >> 31)); + int32_t vars = aux & 0xFF; + addImmInput(node, vars); + addJumpInput(node, getJumpTarget(insn, i)); + break; + } + + case LOP_FASTCALL: + // Note that FASTCALL will read the actual call arguments, such as argument/result registers and counts, from the CALL instruction + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_FASTCALL1: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + addVmRegInput(node, LUAU_INSN_B(insn)); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_FASTCALL2: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, aux & 0xFF); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_FASTCALL2K: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmConstInput(node, aux); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_FASTCALL3: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, aux & 0xFF); + addVmRegInput(node, (aux >> 8) & 0xFF); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_GETVARARGS: + { + func.addUse(node, BcOp{BcOpKind::VmReg, LUAU_INSN_A(insn)}); + int count = LUAU_INSN_B(insn) - 1; + addImmInput(node, static_cast(count)); + func.regs[nodeOp] = LUAU_INSN_A(insn); + if (count < 0) + { + BlockProducers& blockProducers = producers[currentBlock.index]; + blockProducers.multiReturn = nodeOp; + blockProducers.multiReturnStart = LUAU_INSN_A(insn); + blockProducers.invalidAfter = 255; + } + else + for (int i = 0; i < count; i++) + addProducer(LUAU_INSN_A(insn) + i, func.addProj(nodeOp, i)); + break; + } + + case LOP_DUPCLOSURE: + addVmConstInput(node, LUAU_INSN_D(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_PREPVARARGS: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + break; + + case LOP_LOADKX: + addVmConstInput(node, aux); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_JUMPX: + LUAU_ASSERT(!"Shouldn't parse it directly"); + addJumpInput(node, getJumpTarget(insn, i)); + break; + + case LOP_COVERAGE: + addImmInput(node, static_cast(LUAU_INSN_E(insn))); + break; + + case LOP_CAPTURE: + { + uint8_t captureType = LUAU_INSN_A(insn); + addImmInput(node, static_cast(captureType)); + if (captureType == LCT_VAL || captureType == LCT_REF) + addVmRegInput(node, LUAU_INSN_B(insn)); + else + addUpvalInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + } + + case LOP_SUBRK: + case LOP_DIVRK: + addVmConstInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_IDIV: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_IDIVK: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmConstInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + // ServerLua: LSL instructions + case LOP_LSL_CASTINTFLOAT: + case LOP_LSL_DOUBLE2FLOAT: + addVmRegInput(node, LUAU_INSN_B(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_CMPPROTO: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(aux)); + addJumpInput(node, getJumpTarget(insn, i)); + break; + + case LOP_NEWCLASSMEMBER: + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addVmConstInput(node, aux); + break; + + case LOP_NEWCLASS: + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmConstInput(node, aux); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + + case LOP__COUNT: + LUAU_UNREACHABLE(); + } + + i += opLength; + if (blockByPC.count(i) > 0) + { + // currentBlock is fully emitted: release it so its successors can seal once all their + // predecessors are emitted (a loop header seals here, after its back-edge source) + finalizeBlock(currentBlock); + currentBlock = blockByPC[i]; + } + } + + finalizeBlock(currentBlock); + sealAllRemaining(); // seal any block whose predecessors were never all emitted + + return true; + } +}; + +} // namespace Bytecode +} // namespace Luau diff --git a/Bytecode/src/BytecodeGraphSerializer.h b/Bytecode/src/BytecodeGraphSerializer.h new file mode 100644 index 00000000..1773566d --- /dev/null +++ b/Bytecode/src/BytecodeGraphSerializer.h @@ -0,0 +1,614 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeUtils.h" +#include "Luau/BytecodeOps.h" + +namespace Luau +{ +namespace Bytecode +{ + +template +struct BytecodeGraphSerializer +{ + struct JumpInfo + { + LuauOpcode op; + uint32_t instructionPC; + BcOp targetBlock; + }; + using Jumps = std::vector; + + BytecodeBuilder& bcb; + BcFunction& func; + Jumps jumps; + bool error = false; + + BytecodeGraphSerializer(BytecodeBuilder& bcb, BcFunction& func) + : bcb(bcb) + , func(func) + { + } + + std::vector reschedule() + { + std::vector sortedBlocks; + sortedBlocks.reserve(func.blocks.size()); + for (uint32_t i = 0; i < func.blocks.size(); i++) + if ((func.blocks[i].flags & BcBlockFlag::Dead) == 0) + sortedBlocks.push_back(BcOp{BcOpKind::Block, i}); + + std::sort( + sortedBlocks.begin(), + sortedBlocks.end(), + [&](BcOp opA, BcOp opB) + { + const BcBlock& a = func.blockOp(opA); + const BcBlock& b = func.blockOp(opB); + + if (a.sortkey == b.sortkey) + return a.chainkey < b.chainkey; + + return a.sortkey < b.sortkey; + } + ); + + LUAU_ASSERT(sortedBlocks.back() == func.exitBlock); + sortedBlocks.pop_back(); + + return sortedBlocks; + } + + uint8_t getRegister(BcOp op) + { + switch (op.kind) + { + case BcOpKind::Phi: + { + BcPhi& phi = func.phiOp(op); + LUAU_ASSERT(phi.ops.size() > 0); + // Parser-built phis record their register at creation in `makePhi` + // Loop-carried phis form operand cycles, as a nested loop's inner and outer accumulator phis are mutual operands, so recursing through + // phi.ops to derive the register would not terminate, so we should use the recorded register instead + // Return-value merges, inserted by the inliner, have no recorded register but are acyclic, so they resolve through their first operand + if (auto it = func.regs.find(op); it != func.regs.end()) + return it->second; + LUAU_ASSERT(phi.ops[0] != op); + return getRegister(phi.ops[0]); + } + case BcOpKind::Inst: + { + auto it = func.regs.find(op); + LUAU_ASSERT(it != func.regs.end()); + return it->second; + } + case BcOpKind::Proj: + { + BcProj& proj = func.projOp(op); + Reg base = getRegister(proj.op); + return base + proj.index; + } + case BcOpKind::VmReg: + return op.index; + default: + LUAU_UNREACHABLE(); + } + LUAU_UNREACHABLE(); + return 0; + } + + BcImm& getImm(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::Imm); + return func.immOp(inp); + } + + int32_t getImmInt(BcInst& insn, uint8_t index) + { + BcImm& imm = getImm(insn, index); + LUAU_ASSERT(imm.kind == BcImmKind::Int); + return imm.valueInt; + } + + bool getImmBool(BcInst& insn, uint8_t index) + { + BcImm& imm = getImm(insn, index); + LUAU_ASSERT(imm.kind == BcImmKind::Boolean); + return imm.valueBoolean; + } + + uint32_t getImmImport(BcInst& insn, uint8_t index) + { + BcImm& imm = getImm(insn, index); + LUAU_ASSERT(imm.kind == BcImmKind::Import); + return imm.valueImport; + } + + virtual uint32_t getVmConstInputRaw(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::VmConst); + LUAU_ASSERT(inp.index < func.constants.size()); + return inp.index; + } + + uint8_t getVmConstInputABC(BcInst& insn, uint8_t index) + { + uint32_t cid = getVmConstInputRaw(insn, index); + + if (cid > 0xff) + error = true; + + return uint8_t(cid); + } + + uint16_t getVmConstInputD(BcInst& insn, uint8_t index) + { + uint32_t cid = getVmConstInputRaw(insn, index); + + if (cid > 0xffff) + error = true; + + return uint16_t(cid); + } + + uint32_t getVmConstInputAux(BcInst& insn, uint8_t index) + { + return getVmConstInputRaw(insn, index); + } + + uint8_t getUpvalInput(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::VmUpvalue); + LUAU_ASSERT(inp.index < func.nups); + return uint8_t(inp.index); + } + + uint16_t getProtoInput(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::VmProto); + + if (inp.index > 0xffff) + error = true; + + return uint16_t(inp.index); + } + + uint8_t getRegInput(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + return getRegister(insn.ops[index]); + } + + void recordJump(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::Block); + jumps.push_back({insn.op, static_cast(bcb.getInstructionCount()), inp}); + } + + void patchJump(JumpInfo& jump) + { + BcBlock& target = func.blockOp(jump.targetBlock); + LUAU_ASSERT(target.startpc != kBlockNoStartPc); + if (isJumpD(jump.op)) + { + [[maybe_unused]] bool patched = bcb.patchJumpD(jump.instructionPC, target.startpc); + LUAU_ASSERT(patched); + } + else if (isSkipC(jump.op)) + { + [[maybe_unused]] bool patched = bcb.patchSkipC(jump.instructionPC, target.startpc); + LUAU_ASSERT(patched); + } + } + + void emitInstruction(BcOp insnOp) + { + BcInst& insn = func.instOp(insnOp); + bcb.setDebugLine(insn.line); + switch (insn.op) + { + case LOP_NOP: + case LOP_BREAK: + case LOP_NATIVECALL: + bcb.emitABC(insn.op, 0, 0, 0); + break; + + case LOP_LOADNIL: + bcb.emitABC(LOP_LOADNIL, getRegister(insnOp), 0, 0); + break; + + case LOP_LOADB: + { + if (insn.ops.size() > 1) + recordJump(insn, 1); + bcb.emitABC(LOP_LOADB, getRegister(insnOp), getImmBool(insn, 0), 0); + break; + } + + case LOP_LOADN: + bcb.emitAD(LOP_LOADN, getRegister(insnOp), getImmInt(insn, 0)); + break; + + case LOP_LOADK: + bcb.emitAD(LOP_LOADK, getRegister(insnOp), getVmConstInputD(insn, 0)); + break; + + case LOP_MOVE: + bcb.emitABC(LOP_MOVE, getRegister(insnOp), getRegInput(insn, 0), 0); + break; + + case LOP_GETGLOBAL: + bcb.emitABC(LOP_GETGLOBAL, getRegister(insnOp), 0, getImmInt(insn, 0)); + bcb.emitAux(getVmConstInputAux(insn, 1)); + break; + + case LOP_SETGLOBAL: + bcb.emitABC(LOP_SETGLOBAL, getRegInput(insn, 0), 0, getImmInt(insn, 1)); + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_GETUPVAL: + bcb.emitABC(LOP_GETUPVAL, getRegister(insnOp), getUpvalInput(insn, 0), 0); + break; + + case LOP_SETUPVAL: + bcb.emitABC(LOP_SETUPVAL, getRegInput(insn, 0), getUpvalInput(insn, 1), 0); + break; + + case LOP_CLOSEUPVALS: + LUAU_ASSERT(insn.ops.size() == 1 && insn.ops[0].kind == BcOpKind::VmReg); + bcb.emitABC(LOP_CLOSEUPVALS, insn.ops[0].index, 0, 0); + break; + + case LOP_GETIMPORT: + { + bcb.emitAD(LOP_GETIMPORT, getRegister(insnOp), getVmConstInputD(insn, 0)); + uint32_t componentsCount = getImmInt(insn, 1); + LUAU_ASSERT(componentsCount > 0 && componentsCount <= 3); + LUAU_ASSERT(insn.ops.size() - 2 == componentsCount); + uint32_t aux = componentsCount << 30; + for (uint32_t i = 0; i < componentsCount; i++) + { + uint32_t componentId = getVmConstInputRaw(insn, 2 + i); + if (componentId > 0x3FF) + error = true; + aux |= componentId << (20 - 10 * i); + } + bcb.emitAux(aux); + break; + } + + case LOP_GETTABLE: + bcb.emitABC(LOP_GETTABLE, getRegister(insnOp), getRegInput(insn, 0), getRegInput(insn, 1)); + break; + + case LOP_SETTABLE: + bcb.emitABC(LOP_SETTABLE, getRegInput(insn, 0), getRegInput(insn, 1), getRegInput(insn, 2)); + break; + + case LOP_GETUDATAKS: + case LOP_GETTABLEKS: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), getImmInt(insn, 1)); + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_SETUDATAKS: + case LOP_SETTABLEKS: + bcb.emitABC(insn.op, getRegInput(insn, 0), getRegInput(insn, 1), getImmInt(insn, 2)); + bcb.emitAux(getVmConstInputAux(insn, 3)); + break; + + case LOP_GETTABLEN: + bcb.emitABC(LOP_GETTABLEN, getRegister(insnOp), getRegInput(insn, 0), getImmInt(insn, 1) - 1); + break; + + case LOP_SETTABLEN: + bcb.emitABC(LOP_SETTABLEN, getRegInput(insn, 0), getRegInput(insn, 1), getImmInt(insn, 2) - 1); + break; + + case LOP_NEWCLOSURE: + bcb.emitAD(LOP_NEWCLOSURE, getRegister(insnOp), getProtoInput(insn, 0)); + break; + + case LOP_NAMECALLUDATA: + case LOP_NAMECALL: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), getImmInt(insn, 1)); + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_CALL: + bcb.emitABC(LOP_CALL, getRegInput(insn, 2), getImmInt(insn, 0) + 1, getImmInt(insn, 1) + 1); + break; + + case LOP_CALLFB: + bcb.emitABC(LOP_CALLFB, getRegInput(insn, 3), getImmInt(insn, 0) + 1, getImmInt(insn, 1) + 1); + bcb.emitAux(getImmInt(insn, 2)); + break; + + case LOP_RETURN: + { + LUAU_ASSERT(insn.ops.size() > 1); + bcb.emitABC(LOP_RETURN, getRegInput(insn, 1), getImmInt(insn, 0) + 1, 0); + break; + } + + case LOP_JUMP: + recordJump(insn, 0); + bcb.emitAD(LOP_JUMP, 0, 0); + break; + + case LOP_JUMPBACK: + recordJump(insn, 0); + bcb.emitAD(LOP_JUMPBACK, 0, 0); + break; + + case LOP_JUMPIFNOT: + case LOP_JUMPIF: + recordJump(insn, 1); + bcb.emitAD(insn.op, getRegInput(insn, 0), 0); + break; + + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + recordJump(insn, 2); + bcb.emitAD(insn.op, getRegInput(insn, 0), 0); + bcb.emitAux(getRegInput(insn, 1)); + break; + + case LOP_ADD: + case LOP_SUB: + case LOP_MUL: + case LOP_DIV: + case LOP_MOD: + case LOP_POW: + case LOP_AND: + case LOP_OR: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), getRegInput(insn, 1)); + break; + + case LOP_ADDK: + case LOP_SUBK: + case LOP_MULK: + case LOP_DIVK: + case LOP_MODK: + case LOP_POWK: + case LOP_ANDK: + case LOP_ORK: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), getVmConstInputABC(insn, 1)); + break; + + case LOP_CONCAT: + LUAU_ASSERT(insn.ops.size() > 0); + bcb.emitABC(LOP_CONCAT, getRegister(insnOp), getRegInput(insn, 0), getRegInput(insn, insn.ops.size() - 1)); + break; + + case LOP_NOT: + case LOP_MINUS: + case LOP_LENGTH: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), 0); + break; + + case LOP_NEWTABLE: + bcb.emitABC(LOP_NEWTABLE, getRegister(insnOp), getImmInt(insn, 0), 0); + bcb.emitAux(getImmInt(insn, 1)); + break; + + case LOP_DUPTABLE: + bcb.emitAD(LOP_DUPTABLE, getRegister(insnOp), getVmConstInputD(insn, 0)); + break; + + case LOP_SETLIST: + LUAU_ASSERT(insn.ops.size() > 2); + bcb.emitABC(LOP_SETLIST, getRegInput(insn, 2), getRegInput(insn, 3), getImmInt(insn, 1) + 1); + bcb.emitAux(getImmInt(insn, 0)); + break; + + case LOP_FORNPREP: + recordJump(insn, 3); + bcb.emitAD(LOP_FORNPREP, getRegInput(insn, 0), 0); + break; + + case LOP_FORNLOOP: + recordJump(insn, 3); + bcb.emitAD(LOP_FORNLOOP, getRegInput(insn, 0), 0); + break; + + case LOP_FORGPREP: + case LOP_FORGPREP_NEXT: + case LOP_FORGPREP_INEXT: + recordJump(insn, 3); + bcb.emitAD(insn.op, getRegInput(insn, 0), 0); + break; + + case LOP_FORGLOOP: + recordJump(insn, 5); + bcb.emitAD(LOP_FORGLOOP, getRegInput(insn, 0), 0); + bcb.emitAux(static_cast(getImmBool(insn, 3)) << 31 | getImmInt(insn, 4)); + break; + + case LOP_FASTCALL: + bcb.emitABC(LOP_FASTCALL, getImmInt(insn, 0), 0, getImmInt(insn, 1)); + break; + + case LOP_FASTCALL1: + bcb.emitABC(LOP_FASTCALL1, getImmInt(insn, 0), getRegInput(insn, 1), getImmInt(insn, 2)); + break; + + case LOP_FASTCALL2: + bcb.emitABC(LOP_FASTCALL2, getImmInt(insn, 0), getRegInput(insn, 1), getImmInt(insn, 3)); + bcb.emitAux(getRegInput(insn, 2)); + break; + + case LOP_FASTCALL2K: + bcb.emitABC(LOP_FASTCALL2K, getImmInt(insn, 0), getRegInput(insn, 1), getImmInt(insn, 3)); + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_FASTCALL3: + bcb.emitABC(LOP_FASTCALL3, getImmInt(insn, 0), getRegInput(insn, 1), getImmInt(insn, 4)); + bcb.emitAux(getRegInput(insn, 2) | static_cast(getRegInput(insn, 3)) << 8); + break; + + case LOP_GETVARARGS: + LUAU_ASSERT(insn.ops.size() == 2 && insn.ops[0].kind == BcOpKind::VmReg); + bcb.emitABC(LOP_GETVARARGS, insn.ops[0].index, getImmInt(insn, 1) + 1, 0); + break; + + case LOP_DUPCLOSURE: + bcb.emitAD(LOP_DUPCLOSURE, getRegister(insnOp), getVmConstInputD(insn, 0)); + break; + + case LOP_PREPVARARGS: + bcb.emitAD(LOP_PREPVARARGS, getImmInt(insn, 0), 0); + break; + + case LOP_LOADKX: + bcb.emitAD(LOP_LOADKX, getRegister(insnOp), 0); + bcb.emitAux(getVmConstInputAux(insn, 0)); + break; + + case LOP_JUMPX: + recordJump(insn, 0); + bcb.emitE(LOP_JUMPX, 0); + break; + + case LOP_COVERAGE: + bcb.emitE(LOP_COVERAGE, getImmInt(insn, 0)); + break; + + case LOP_CAPTURE: + { + uint8_t captureType = getImmInt(insn, 0); + if (captureType == LCT_VAL || captureType == LCT_REF) + bcb.emitABC(LOP_CAPTURE, captureType, getRegInput(insn, 1), getImmInt(insn, 2)); + else + bcb.emitABC(LOP_CAPTURE, captureType, getUpvalInput(insn, 1), getImmInt(insn, 2)); + break; + } + + case LOP_SUBRK: + case LOP_DIVRK: + bcb.emitABC(insn.op, getRegister(insnOp), getVmConstInputABC(insn, 0), getRegInput(insn, 1)); + break; + + case LOP_JUMPXEQKNIL: + recordJump(insn, 2); + bcb.emitAD(LOP_JUMPXEQKNIL, getRegInput(insn, 0), 0); + bcb.emitAux(static_cast(getImmBool(insn, 1)) << 31); + break; + + case LOP_JUMPXEQKB: + recordJump(insn, 2); + bcb.emitAD(LOP_JUMPXEQKB, getRegInput(insn, 0), 0); + bcb.emitAux(static_cast(getImmBool(insn, 1)) << 31 | static_cast(getImmBool(insn, 3))); + break; + + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + recordJump(insn, 2); + bcb.emitAD(insn.op, getRegInput(insn, 0), 0); + bcb.emitAux(static_cast(getImmBool(insn, 1)) << 31 | getVmConstInputAux(insn, 3)); + break; + + case LOP_IDIV: + bcb.emitABC(LOP_IDIV, getRegister(insnOp), getRegInput(insn, 0), getRegInput(insn, 1)); + break; + + case LOP_IDIVK: + bcb.emitABC(LOP_IDIVK, getRegister(insnOp), getRegInput(insn, 0), getVmConstInputABC(insn, 1)); + break; + + // ServerLua: LSL instructions + case LOP_LSL_CASTINTFLOAT: + case LOP_LSL_DOUBLE2FLOAT: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), 0); + break; + + case LOP_NEWCLASSMEMBER: + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + bcb.emitABC(LOP_NEWCLASSMEMBER, getRegInput(insn, 0), 0, getRegInput(insn, 1)); + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_CMPPROTO: + recordJump(insn, 2); + bcb.emitAD(LOP_CMPPROTO, getRegInput(insn, 0), 0); + bcb.emitAux(getImmInt(insn, 1)); + break; + + case LOP_NEWCLASS: + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + bcb.emitABC(LOP_NEWCLASS, getRegister(insnOp), getRegInput(insn, 0), 0); + bcb.emitAux(getVmConstInputAux(insn, 1)); + break; + + case LOP__COUNT: + LUAU_UNREACHABLE(); + } + } + + std::optional getFallthrough(BcBlock& block) + { + for (auto [ctrl, target] : block.successors) + if (ctrl == BcBlockEdgeKind::Fallthrough) + return {target}; + return {}; + } + + std::vector emitBytecode() + { + std::vector schedule = reschedule(); + std::vector insnsPC; + insnsPC.resize(func.instructions.size(), ~0u); + + for (size_t i = 0; i < schedule.size(); i++) + { + BcOp blockOp = schedule[i]; + BcBlock& block = func.blockOp(blockOp); + std::optional fallthrough = getFallthrough(block); + if (fallthrough && *fallthrough != func.exitBlock && !(func.blockOp(*fallthrough).flags & BcBlockFlag::Dead) && + (i + 1 >= schedule.size() || *fallthrough != schedule[i + 1])) + { + BcJump jump = BcJump::create(func); + jump.setTarget(*fallthrough); + jump.appendTo(blockOp); + insnsPC.resize(func.instructions.size()); + } + block.startpc = bcb.getDebugPC(); + for (BcOp op : block.ops) + { + LUAU_ASSERT(op.kind == BcOpKind::Inst); + insnsPC[op.index] = bcb.getDebugPC(); + emitInstruction(op); + } + } + + for (auto& jump : jumps) + patchJump(jump); + + // Serialization failed + if (error) + return {}; + + return insnsPC; + } +}; + +} // namespace Bytecode +} // namespace Luau diff --git a/Bytecode/src/Sccp.cpp b/Bytecode/src/Sccp.cpp new file mode 100644 index 00000000..ccee4852 --- /dev/null +++ b/Bytecode/src/Sccp.cpp @@ -0,0 +1,600 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Sccp.h" +#include "Luau/BytecodeGraph.h" + +#include + +namespace Luau +{ +namespace Bytecode +{ + +// standard three way comparison: -1 if a < b, 0 if a == b, 1 if a > b +template +static int threeWay(const T& a, const T& b) +{ + return static_cast(a > b) - static_cast(a < b); +} + +static BcOp findOrAddConst(BcFunction& func, const BcVmConst& value) +{ + for (size_t i = 0; i < func.constants.size(); i++) + { + if (func.constants[i] == value) + return BcOp{BcOpKind::VmConst, static_cast(i)}; + } + return func.addConst(value); +} + +std::optional BcVmConstImpl::evaluate(const BcOp& lhsOp, const BcOp& rhsOp, LuauOpcode op) const +{ + BcVmConst& lhs = func.constOp(lhsOp); + BcVmConst& rhs = func.constOp(rhsOp); + + // arithmetic folding is only defined for two numbers + if (lhs.kind != rhs.kind || lhs.kind != BcVmConstKind::Number) + return std::nullopt; + + const double a = lhs.valueNumber; + const double b = rhs.valueNumber; + double r; + + switch (op) + { + case LuauOpcode::LOP_ADD: + r = a + b; + break; + case LuauOpcode::LOP_SUB: + r = a - b; + break; + case LuauOpcode::LOP_MUL: + r = a * b; + break; + case LuauOpcode::LOP_DIV: + r = a / b; + break; + case LuauOpcode::LOP_MOD: + if (b == 0.0) + return std::nullopt; + r = a - floor(a / b) * b; + break; + case LuauOpcode::LOP_POW: + r = pow(a, b); + break; + case LuauOpcode::LOP_IDIV: + r = floor(a / b); + break; + default: + return std::nullopt; + } + + BcVmConst result; + result.kind = BcVmConstKind::Number; + result.valueNumber = r; + return findOrAddConst(func, result); +} + +bool BcVmConstImpl::falsey(const BcOp& falseyOp) const +{ + if (falseyOp.kind == BcOpKind::VmConst) + { + BcVmConst& vmConst = func.constOp(falseyOp); + + return vmConst.kind == BcVmConstKind::Nil || (vmConst.kind == BcVmConstKind::Boolean && vmConst.valueBoolean == false); + } + else if (falseyOp.kind == BcOpKind::Imm) + { + BcImm& imm = func.immOp(falseyOp); + return imm.kind == BcImmKind::Boolean && imm.valueBoolean == false; + } + + return false; +} + +int BcVmConstImpl::cmp(const BcOp& lhsOp, const BcOp& rhsOp) const +{ + BcVmConst& lhs = func.constOp(lhsOp); + BcVmConst& rhs = func.constOp(rhsOp); + LUAU_ASSERT(lhs.kind == rhs.kind); + + switch (lhs.kind) + { + case BcVmConstKind::Number: + return threeWay(lhs.valueNumber, rhs.valueNumber); + case BcVmConstKind::Integer: + return threeWay(lhs.valueInteger, rhs.valueInteger); + case BcVmConstKind::Boolean: + return (lhs.valueBoolean == rhs.valueBoolean) ? 0 : 1; + case BcVmConstKind::String: + return threeWay(lhs.valueString.compare(rhs.valueString), 0); + default: + return 0; + } +}; + +int BcVmConstImpl::cmp(const BcOp& lhsOp, const BcImm& rhs) const +{ + BcVmConst& lhs = func.constOp(lhsOp); + + if (rhs.kind == BcImmKind::Int) + { + if (lhs.kind == BcVmConstKind::Number) + return threeWay(lhs.valueNumber, static_cast(rhs.valueInt)); + else if (lhs.kind == BcVmConstKind::Integer) + return threeWay(lhs.valueInteger, static_cast(rhs.valueInt)); + } + else if (rhs.kind == BcImmKind::Boolean) + { + if (lhs.kind == BcVmConstKind::Boolean) + return (lhs.valueBoolean == rhs.valueBoolean) ? 0 : 1; + } + + LUAU_ASSERT(!"incompatible types for immCmpBcVmConst"); + return 0; +} + +BcOp BcVmConstImpl::makeNil() const +{ + BcVmConst result{}; + result.kind = BcVmConstKind::Nil; + return findOrAddConst(func, result); +} + +BcImm BcVmConstImpl::makeImm(bool value) const +{ + BcImm result{}; + result.kind = BcImmKind::Boolean; + result.valueBoolean = value; + return result; +} + +BcImm BcVmConstImpl::makeImm(int32_t value) const +{ + BcImm result{}; + result.kind = BcImmKind::Int; + result.valueInt = value; + return result; +} + +BcRef BcVmConstImpl::asImm(BcOp op) const +{ + return func.imm(op); +} + +bool BcVmConstImpl::isOrderable(const BcOp& vmConstOp) const +{ + BcVmConst& vmConst = func.constOp(vmConstOp); + return vmConst.kind == BcVmConstKind::Number || vmConst.kind == BcVmConstKind::Integer || vmConst.kind == BcVmConstKind::String; +} +bool BcVmConstImpl::kindEquals(const BcOp& lhsOp, const BcOp& rhsOp) const +{ + BcVmConst& lhs = func.constOp(lhsOp); + BcVmConst& rhs = func.constOp(rhsOp); + + return lhs.kind == rhs.kind; +} + +std::optional BcVmConstImpl::eq(const BcOp& lhsOp, const BcOp& rhsOp) const +{ + if (lhsOp.kind == BcOpKind::VmConst && rhsOp.kind == BcOpKind::VmConst) + { + + BcVmConst& lhs = func.constOp(lhsOp); + BcVmConst& rhs = func.constOp(rhsOp); + + if (lhs.kind == BcVmConstKind::Number && rhs.kind == BcVmConstKind::Number) + return lhs.valueNumber == rhs.valueNumber; + if (lhs.kind == BcVmConstKind::Integer && rhs.kind == BcVmConstKind::Integer) + return lhs.valueInteger == rhs.valueInteger; + if (lhs.kind == BcVmConstKind::Number && rhs.kind == BcVmConstKind::Integer) + return lhs.valueNumber == static_cast(rhs.valueInteger); + if (lhs.kind == BcVmConstKind::Integer && rhs.kind == BcVmConstKind::Number) + return static_cast(lhs.valueInteger) == rhs.valueNumber; + if (lhs.kind == BcVmConstKind::String && rhs.kind == BcVmConstKind::String) + return lhs.valueString == rhs.valueString; + } + else if (lhsOp.kind == BcOpKind::VmConst && rhsOp.kind == BcOpKind::Imm) + { + + BcVmConst& lhs = func.constOp(lhsOp); + BcImm& rhs = func.immOp(rhsOp); + if (lhs.kind == BcVmConstKind::Boolean && rhs.kind == BcImmKind::Boolean) + return lhs.valueBoolean == rhs.valueBoolean; + } + else if (lhsOp.kind == BcOpKind::Imm && rhsOp.kind == BcOpKind::Imm) + { + BcImm& lhs = func.immOp(lhsOp); + BcImm& rhs = func.immOp(rhsOp); + if (lhs.kind == BcImmKind::Boolean && rhs.kind == BcImmKind::Boolean) + return lhs.valueBoolean == rhs.valueBoolean; + else if (lhs.kind == BcImmKind::Int && rhs.kind == BcImmKind::Int) + return lhs.valueInt == rhs.valueInt; + } + return std::nullopt; +} + +std::optional BcVmConstImpl::eq(const BcOp& lhsOp, bool rhs) const +{ + BcVmConst& lhs = func.constOp(lhsOp); + + if (lhs.kind == BcVmConstKind::Boolean) + return lhs.valueBoolean == rhs; + return std::nullopt; +} + +std::optional BcVmConstImpl::eq(const BcOp& lhsOp, int32_t rhs) const +{ + BcVmConst& lhs = func.constOp(lhsOp); + + if (lhs.kind == BcVmConstKind::Number) + return static_cast(rhs) == lhs.valueNumber; + if (lhs.kind == BcVmConstKind::Integer) + return static_cast(rhs) == lhs.valueInteger; + return std::nullopt; +} + +bool BcVmConstImpl::isArithmeticConstant(const BcOp& vmConstOp) const +{ + BcVmConst& vmConst = func.constOp(vmConstOp); + return vmConst.kind == BcVmConstKind::Number; +} + +double BcVmConstImpl::asNumber(const BcOp& vmConstOp) const +{ + BcVmConst& vmConst = func.constOp(vmConstOp); + LUAU_ASSERT(vmConst.kind == BcVmConstKind::Number); + return vmConst.valueNumber; +} + +// TODO: support imm + vmconst +ConstnessLattice SccpInterpreter::evaluateArith(LuauOpcode opcode, BcRef instRepr) +{ + auto lhs = instRepr->ops[0]; + auto rhs = instRepr->ops[1]; + + ConstnessLattice lhsConstness = this->state->operandLattice(lhs); + ConstnessLattice rhsConstness = this->state->operandLattice(rhs); + + if (lhsConstness.kind == Constness::ImmConstant && rhsConstness.kind == Constness::ImmConstant) + { + const BcImm& lhsImm = lhsConstness.immConst.value(); + const BcImm& rhsImm = rhsConstness.immConst.value(); + + if (lhsImm.kind == BcImmKind::Int && rhsImm.kind == BcImmKind::Int) + { + int lv = lhsImm.valueInt; + int rv = rhsImm.valueInt; + + // Division/modulo by zero cannot be folded + if (rv == 0 && (opcode == LOP_DIV || opcode == LOP_MOD || opcode == LOP_IDIV)) + return ConstnessLattice(Constness::NotAConstant); + + // LOP_DIV and LOP_POW are always floating-point, but BcImm cannot represent floats + if (opcode == LOP_DIV || opcode == LOP_POW) + return ConstnessLattice(Constness::NotAConstant); + + int64_t result; + switch (opcode) + { + case LOP_ADD: + result = int64_t(lv) + rv; + break; + case LOP_SUB: + result = int64_t(lv) - rv; + break; + case LOP_MUL: + result = int64_t(lv) * rv; + break; + case LOP_MOD: + { + // Lua modulo: result has the sign of the divisor + int64_t remainder = int64_t(lv) % rv; + if ((remainder != 0) && ((lv < 0) != (rv < 0))) + remainder += rv; + result = remainder; + break; + } + case LOP_IDIV: + { + // Lua floor division: round toward negative infinity + result = int64_t(lv) / rv; + if ((result < 0) && ((int64_t(lv) % rv) != 0)) + result -= 1; + break; + } + default: + LUAU_ASSERT(!"Unhandled opcode"); + return ConstnessLattice(Constness::NotAConstant); + } + + // LOADN is max 16-bit signed, and we use LOADN in replaceUses + // we could investigate adding a new VmConst for > 16 bit representable numbers + if (result < INT16_MIN || result > INT16_MAX) + return ConstnessLattice(Constness::NotAConstant); + + return ConstnessLattice(Constness::ImmConstant, impl->makeImm(static_cast(result))); + } + } + else if (lhsConstness.kind == Constness::VmConstant && rhsConstness.kind == Constness::VmConstant) + { + std::optional vmConst = impl->evaluate(lhsConstness.vmConst.value(), rhsConstness.vmConst.value(), opcode); + if (vmConst) + return ConstnessLattice(Constness::VmConstant, vmConst.value()); + else + return ConstnessLattice(Constness::NotAConstant); + } + else if (lhsConstness.kind == Constness::Undetermined && rhsConstness.kind == Constness::Undetermined) + { + return ConstnessLattice(Constness::Undetermined); + } + + return ConstnessLattice(Constness::NotAConstant); +} + +ConditionState SccpInterpreter::evaluateComparisonCondition(LuauOpcode op, const BcOp& lhs, const BcOp& rhs) +{ + ConstnessLattice lhsConst = this->state->operandLattice(lhs); + ConstnessLattice rhsConst = this->state->operandLattice(rhs); + + bool isOrderingOp = (op == LOP_JUMPIFLT || op == LOP_JUMPIFLE || op == LOP_JUMPIFNOTLT || op == LOP_JUMPIFNOTLE); + + auto isOrderableLattice = [&](const ConstnessLattice& c) -> bool + { + if (c.kind == Constness::VmConstant) + return impl->isOrderable(c.vmConst.value()); + if (c.kind == Constness::ImmConstant) + return c.immConst.value().kind == BcImmKind::Int; + return false; + }; + + if (isOrderingOp && (!isOrderableLattice(lhsConst) || !isOrderableLattice(rhsConst))) + return ConditionState::Unknown; + + // Mismatched VM constant kinds have no defined ordering/equality + // we may be able to compare imm bools and vm bools, imm numbers and vm numbers, but we are not currently + if (lhsConst.kind == Constness::VmConstant && rhsConst.kind == Constness::VmConstant && + !impl->kindEquals(lhsConst.vmConst.value(), rhsConst.vmConst.value())) + return ConditionState::Unknown; + + auto applyOp = [](int cmp, LuauOpcode op) -> bool + { + switch (op) + { + case LOP_JUMPIFEQ: + case LOP_JUMPIFNOTEQ: + return cmp == 0; + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTLT: + return cmp < 0; + case LOP_JUMPIFLE: + case LOP_JUMPIFNOTLE: + return cmp <= 0; + default: + LUAU_ASSERT(!"Unhandled comparison opcode"); + return false; + } + }; + + if (lhsConst.kind == Constness::VmConstant && rhsConst.kind == Constness::VmConstant) + { + int cmp = impl->cmp(lhsConst.vmConst.value(), rhsConst.vmConst.value()); + bool condTrue = applyOp(cmp, op); + return condTrue ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + else if (lhsConst.kind == Constness::ImmConstant && rhsConst.kind == Constness::ImmConstant) + { + const BcImm& lhsImm = lhsConst.immConst.value(); + const BcImm& rhsImm = rhsConst.immConst.value(); + + if (lhsImm.kind == BcImmKind::Int && rhsImm.kind == BcImmKind::Int) + { + int lv = lhsImm.valueInt; + int rv = rhsImm.valueInt; + + int cmp = static_cast(lv > rv) - static_cast(lv < rv); + bool condTrue = applyOp(cmp, op); + return condTrue ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + else if (lhsImm.kind == BcImmKind::Boolean && rhsImm.kind == BcImmKind::Boolean) + { + bool lv = lhsImm.valueBoolean; + bool rv = rhsImm.valueBoolean; + + int cmp = (lv == rv) ? 0 : 1; + bool condTrue = applyOp(cmp, op); + return condTrue ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + } + else if (lhsConst.kind == Constness::VmConstant && rhsConst.kind == Constness::ImmConstant) + { + int cmp = impl->cmp(lhsConst.vmConst.value(), rhsConst.immConst.value()); + bool condTrue = applyOp(cmp, op); + return condTrue ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + else if (lhsConst.kind == Constness::ImmConstant && rhsConst.kind == Constness::VmConstant) + { + int cmp = -impl->cmp(rhsConst.vmConst.value(), lhsConst.immConst.value()); + bool condTrue = applyOp(cmp, op); + return condTrue ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + + return ConditionState::Unknown; +} + +ConditionState SccpInterpreter::evaluateXeqkCondition(BcRef inst) +{ + ConstnessLattice valConst = this->state->operandLattice(inst->ops[0]); + + switch (inst->op) + { + case LOP_JUMPXEQKNIL: + if (valConst.kind == Constness::VmConstant && impl->falsey(valConst.vmConst.value()) && + impl->kindEquals(valConst.vmConst.value(), impl->makeNil())) + return ConditionState::AlwaysTrue; + else if ( + valConst.kind == Constness::ImmConstant || + (valConst.kind == Constness::VmConstant && !impl->kindEquals(valConst.vmConst.value(), impl->makeNil())) + ) + return ConditionState::AlwaysFalse; + break; + case LOP_JUMPXEQKB: + { + const BcOp& cmpImmOp = inst->ops[3]; + LUAU_ASSERT(cmpImmOp.kind == BcOpKind::Imm); + if (valConst.kind == Constness::ImmConstant && valConst.immConst.value().kind == BcImmKind::Boolean) + { + std::optional eq = impl->eq(valConst.vmConst.value(), cmpImmOp); + if (eq) + return *eq ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + else if (valConst.kind == Constness::VmConstant) + { + std::optional eq = impl->eq(valConst.vmConst.value(), cmpImmOp); + if (eq) + return *eq ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + break; + } + case LOP_JUMPXEQKN: + { + const BcOp& cmpConstOp = inst->ops[3]; + LUAU_ASSERT(cmpConstOp.kind == BcOpKind::VmConst); + if (valConst.kind == Constness::VmConstant) + { + std::optional eq = impl->eq(valConst.vmConst.value(), cmpConstOp); + if (eq) + return *eq ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + else if (valConst.kind == Constness::ImmConstant && valConst.immConst.value().kind == BcImmKind::Int) + { + std::optional eq = impl->eq(cmpConstOp, valConst.immConst.value().valueInt); + if (eq) + return *eq ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + break; + } + case LOP_JUMPXEQKS: + { + const BcOp& cmpConstOp = inst->ops[3]; + LUAU_ASSERT(cmpConstOp.kind == BcOpKind::VmConst); + if (valConst.kind == Constness::VmConstant) + { + std::optional eq = impl->eq(valConst.vmConst.value(), cmpConstOp); + if (eq) + return *eq ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + break; + } + default: + break; + } + + return ConditionState::Unknown; +} + +ConditionState SccpInterpreter::evaluateCondition(const BcOp& op) +{ + ConstnessLattice lhs = this->state->operandLattice(op); + if (lhs.kind == Constness::VmConstant) + return impl->falsey(lhs.vmConst.value()) ? ConditionState::AlwaysFalse : ConditionState::AlwaysTrue; + else if (lhs.kind == Constness::ImmConstant) + { + const BcImm& imm = lhs.immConst.value(); + if (imm.kind == BcImmKind::Boolean) + return imm.valueBoolean == false ? ConditionState::AlwaysFalse : ConditionState::AlwaysTrue; + } + return ConditionState::Unknown; +} + +ConstnessLattice SccpInterpreter::evaluate(LuauOpcode op, BcRef instRepr) +{ + switch (op) + { + case LOP_LOADK: + case LOP_LOADKX: + { + const BcOp& op = instRepr->ops[0]; + LUAU_ASSERT(op.kind == BcOpKind::VmConst); + return ConstnessLattice(Constness::VmConstant, op); + } + case LOP_LOADB: + case LOP_LOADN: + { + const BcOp& op = instRepr->ops[0]; + LUAU_ASSERT(op.kind == BcOpKind::Imm); + return ConstnessLattice(Constness::ImmConstant, *impl->asImm(op)); + } + case LOP_LOADNIL: + { + BcOp nilConst = impl->makeNil(); + return ConstnessLattice(Constness::VmConstant, nilConst); + } + + case LOP_ADD: + case LOP_SUB: + case LOP_MUL: + case LOP_DIV: + case LOP_MOD: + case LOP_POW: + case LOP_IDIV: + { + return evaluateArith(op, instRepr); + } + case LOP_MOVE: + { + return this->state->operandLattice(instRepr->ops[0]); + } + + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + { + ConditionState cond = evaluateCondition(instRepr->ops[0]); + if (cond == ConditionState::Unknown) + return ConstnessLattice(this->state->unknownConditionConstness({instRepr->ops[0]})); + + bool jumpsOnTrue = (instRepr->op == LOP_JUMPIF); + bool takesJump = (cond == ConditionState::AlwaysTrue) == jumpsOnTrue; + return ConstnessLattice(Constness::ImmConstant, impl->makeImm(takesJump)); + } + + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + { + ConditionState cond = evaluateComparisonCondition(instRepr->op, instRepr->ops[0], instRepr->ops[1]); + if (cond == ConditionState::Unknown) + return ConstnessLattice(this->state->unknownConditionConstness({instRepr->ops[0], instRepr->ops[1]})); + + bool negated = (instRepr->op == LOP_JUMPIFNOTEQ || instRepr->op == LOP_JUMPIFNOTLE || instRepr->op == LOP_JUMPIFNOTLT); + bool takesJump = (cond == ConditionState::AlwaysTrue) != negated; + return ConstnessLattice(Constness::ImmConstant, impl->makeImm(takesJump)); + } + + case LOP_JUMPXEQKNIL: + case LOP_JUMPXEQKB: + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + { + ConditionState cond = evaluateXeqkCondition(instRepr); + if (cond == ConditionState::Unknown) + return ConstnessLattice(this->state->unknownConditionConstness({instRepr->ops[0]})); + + const BcOp& negImmOp = instRepr->ops[1]; + bool negated = !impl->falsey(negImmOp); + bool takesJump = (cond == ConditionState::AlwaysTrue) != negated; + + return ConstnessLattice(Constness::ImmConstant, impl->makeImm(takesJump)); + } + + case LOP_JUMP: + case LOP_JUMPBACK: + default: + return ConstnessLattice(Constness::NotAConstant); + } +} + +} // namespace Bytecode +} // namespace Luau diff --git a/CLI/include/Luau/AnalyzeRequirer.h b/CLI/include/Luau/AnalyzeRequirer.h index de672b67..69598283 100644 --- a/CLI/include/Luau/AnalyzeRequirer.h +++ b/CLI/include/Luau/AnalyzeRequirer.h @@ -20,7 +20,7 @@ struct FileNavigationContext : Luau::Require::NavigationContext ConfigStatus getConfigStatus() const override; ConfigBehavior getConfigBehavior() const override; - std::optional getAlias(const std::string& alias) const override; + std::optional getAlias(const std::string& alias) override; std::optional getConfig() const override; // Custom capabilities diff --git a/CLI/src/Analyze.cpp b/CLI/src/Analyze.cpp index 60385421..b67f8c3e 100644 --- a/CLI/src/Analyze.cpp +++ b/CLI/src/Analyze.cpp @@ -140,6 +140,7 @@ static void displayHelp(const char* argv0) printf(" --formatter=plain: report analysis errors in Luacheck-compatible format\n"); printf(" --formatter=gnu: report analysis errors in GNU-compatible format\n"); printf(" --mode=strict: default to strict mode when typechecking\n"); + printf(" --solver={new|old}: selects which typechecker to use (defaults to the new solver)\n"); printf(" --timetrace: record compiler time tracing information into trace.json\n"); } @@ -407,6 +408,7 @@ int main(int argc, char** argv) bool annotate = false; int threadCount = 0; std::string basePath = ""; + Luau::SolverMode solverMode = Luau::SolverMode::New; for (int i = 1; i < argc; ++i) { @@ -429,6 +431,8 @@ int main(int argc, char** argv) threadCount = int(strtol(argv[i] + 2, nullptr, 10)); else if (strncmp(argv[i], "--logbase=", 10) == 0) basePath = std::string{argv[i] + 10}; + else if (strcmp(argv[i], "--solver=old") == 0) + solverMode = Luau::SolverMode::Old; } #if !defined(LUAU_ENABLE_TIME_TRACE) @@ -445,7 +449,8 @@ int main(int argc, char** argv) CliFileResolver fileResolver; CliConfigResolver configResolver(mode); - Luau::Frontend frontend(&fileResolver, &configResolver, frontendOptions); + + Luau::Frontend frontend(solverMode, &fileResolver, &configResolver, frontendOptions); if (FFlag::DebugLuauLogSolverToJsonFile) { diff --git a/CLI/src/AnalyzeRequirer.cpp b/CLI/src/AnalyzeRequirer.cpp index d6584926..24728bde 100644 --- a/CLI/src/AnalyzeRequirer.cpp +++ b/CLI/src/AnalyzeRequirer.cpp @@ -80,7 +80,7 @@ Luau::Require::NavigationContext::ConfigBehavior FileNavigationContext::getConfi return Luau::Require::NavigationContext::ConfigBehavior::GetConfig; } -std::optional FileNavigationContext::getAlias(const std::string& alias) const +std::optional FileNavigationContext::getAlias(const std::string& alias) { return std::nullopt; } diff --git a/CLI/src/Bytecode.cpp b/CLI/src/Bytecode.cpp index dc8e4833..92f3c10d 100644 --- a/CLI/src/Bytecode.cpp +++ b/CLI/src/Bytecode.cpp @@ -38,7 +38,7 @@ static void displayHelp(const char* argv0) printf(" -h, --help: Display this usage message.\n"); printf(" -O: compile with optimization level n (default 1, n should be between 0 and 2).\n"); printf(" -g: compile with debug level n (default 1, n should be between 0 and 2).\n"); - printf(" --fflags=: flags to be enabled.\n"); + printf(" --fflags=: comma-separated list of fast flags to enable/disable (--fflags=true,false,LuauFlag1=true,LuauFlag2=false).\n"); printf(" --summary-file=: file in which bytecode analysis summary will be recorded (default 'bytecode-summary.json').\n"); exit(0); diff --git a/CLI/src/Compile.cpp b/CLI/src/Compile.cpp index 06e14d29..1c2a9f7c 100644 --- a/CLI/src/Compile.cpp +++ b/CLI/src/Compile.cpp @@ -1,4 +1,5 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/CodeGenOptions.h" #include "lua.h" #include "lualib.h" @@ -55,6 +56,11 @@ struct GlobalOptions const char* vectorLib = nullptr; const char* vectorCtor = nullptr; const char* vectorType = nullptr; + + bool onlyParse = false; + bool parseCst = false; + + bool dumpRegSpills = false; } globalOptions; static Luau::CompileOptions copts() @@ -80,8 +86,6 @@ static std::optional getCompileFormat(const char* name) return CompileFormat::Text; else if (strcmp(name, "binary") == 0) return CompileFormat::Binary; - else if (strcmp(name, "text") == 0) - return CompileFormat::Text; else if (strcmp(name, "remarks") == 0) return CompileFormat::Remarks; else if (strcmp(name, "codegen") == 0) @@ -301,7 +305,13 @@ static double recordDeltaTime(double& timer) return delta; } -static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::AssemblyOptions::Target assemblyTarget, CompileStats& stats) +static bool compileFile( + const char* name, + CompileFormat format, + Luau::CodeGen::AssemblyOptions::Target assemblyTarget, + CompileStats& stats, + bool dumpConstants +) { double currts = Luau::TimeTrace::getClock(); std::string sName = name; @@ -323,6 +333,7 @@ static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::A Luau::BytecodeBuilder bcb; Luau::CodeGen::AssemblyOptions options; + options.compilationOptions.flags = Luau::CodeGen::CodeGen_ColdFunctions; options.target = assemblyTarget; options.outputBinary = format == CompileFormat::CodegenNull; @@ -330,8 +341,10 @@ static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::A { options.includeAssembly = format != CompileFormat::CodegenIr; options.includeIr = format != CompileFormat::CodegenAsm; + options.includeIrTypes = format != CompileFormat::CodegenAsm; options.includeOutlinedCode = format == CompileFormat::CodegenVerbose; + options.includeRegSpills = globalOptions.dumpRegSpills; } options.annotator = annotateInstruction; @@ -339,10 +352,11 @@ static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::A if (format == CompileFormat::Text) { - bcb.setDumpFlags( - Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals | - Luau::BytecodeBuilder::Dump_Remarks | Luau::BytecodeBuilder::Dump_Types - ); + uint32_t flags = Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals | + Luau::BytecodeBuilder::Dump_Remarks | Luau::BytecodeBuilder::Dump_Types; + if (dumpConstants) + flags |= Luau::BytecodeBuilder::Dump_Constants; + bcb.setDumpFlags(flags); bcb.setDumpSource(*source); } else if (format == CompileFormat::Remarks) @@ -376,7 +390,9 @@ static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::A { Luau::Allocator allocator; Luau::AstNameTable names(allocator); - Luau::ParseResult result = Luau::Parser::parse(source->c_str(), source->size(), names, allocator); + Luau::ParseOptions parseOptions; + parseOptions.storeCstData = globalOptions.parseCst; + Luau::ParseResult result = Luau::Parser::parse(source->c_str(), source->size(), names, allocator, parseOptions); if (!result.errors.empty()) throw Luau::ParseErrors(result.errors); @@ -384,8 +400,12 @@ static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::A stats.lines += result.lines; stats.parseTime += recordDeltaTime(currts); + if (globalOptions.onlyParse) + return true; + Luau::compileOrThrow(bcb, result, names, copts()); } + stats.bytecode += bcb.getBytecode().size(); stats.bytecodeInstructionCount = bcb.getTotalInstructionCount(); stats.compileTime += recordDeltaTime(currts); @@ -435,20 +455,26 @@ static void displayHelp(const char* argv0) printf("Usage: %s [--mode] [options] [file list]\n", argv0); printf("\n"); printf("Available modes:\n"); - printf(" binary, text, remarks, codegen\n"); + printf(" binary, text, remarks, codegen, codegenir, codegenasm, codegenverbose, codegennull, null\n"); printf("\n"); printf("Available options:\n"); printf(" -h, --help: Display this usage message.\n"); printf(" -O: compile with optimization level n (default 1, n should be between 0 and 2).\n"); printf(" -g: compile with debug level n (default 1, n should be between 0 and 2).\n"); + printf(" -t: compile with type information level n (default 0, n should be between 0 and 1).\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=: granularity of compilation stats (total, file, function).\n"); printf(" --bytecode-summary: Compute bytecode operation distribution.\n"); + printf(" --dump-constants: Dump constant table for each function (text mode only).\n"); + printf(" --dump-regspills: include register spill events in codegen output.\n"); printf(" --stats-file=: file in which compilation stats will be recored (default 'stats.json').\n"); printf(" --vector-lib=: name of the library providing vector type operations.\n"); printf(" --vector-ctor=: name of the function constructing a vector value.\n"); printf(" --vector-type=: name of the vector type.\n"); + printf(" --only-parse: Only parse the input.\n"); + printf(" --parse-cst: Whether parser should parse CST in addition to AST.\n"); + printf(" --fflags=: comma-separated list of fast flags to enable/disable (--fflags=true,false,LuauFlag1=true,LuauFlag2=false).\n"); } static int assertionHandler(const char* expr, const char* file, int line, const char* function) @@ -492,6 +518,7 @@ int main(int argc, char** argv) RecordStats recordStats = RecordStats::None; std::string statsFile("stats.json"); bool bytecodeSummary = false; + bool dumpConstants = false; for (int i = 1; i < argc; i++) { @@ -572,6 +599,14 @@ int main(int argc, char** argv) { bytecodeSummary = true; } + else if (strcmp(argv[i], "--dump-constants") == 0) + { + dumpConstants = true; + } + else if (strcmp(argv[i], "--dump-regspills") == 0) + { + globalOptions.dumpRegSpills = true; + } else if (strncmp(argv[i], "--stats-file=", 13) == 0) { statsFile = argv[i] + 13; @@ -603,6 +638,14 @@ int main(int argc, char** argv) luauSL_init_global_builtins(argv[i] + 14); globalOptions.slLibraries = 1; } + else if (strncmp(argv[i], "--parse-cst", 11) == 0) + { + globalOptions.parseCst = true; + } + else if (strncmp(argv[i], "--only-parse", 12) == 0) + { + globalOptions.onlyParse = true; + } else if (argv[i][0] == '-' && argv[i][1] == '-' && getCompileFormat(argv[i] + 2)) { compileFormat = *getCompileFormat(argv[i] + 2); @@ -650,7 +693,7 @@ int main(int argc, char** argv) { CompileStats fileStat = {}; fileStat.lowerStats.functionStatsFlags = functionStats; - failed += !compileFile(path.c_str(), compileFormat, assemblyTarget, fileStat); + failed += !compileFile(path.c_str(), compileFormat, assemblyTarget, fileStat, dumpConstants); stats += fileStat; if (recordStats == RecordStats::File || recordStats == RecordStats::Function) fileStats.push_back(fileStat); diff --git a/CLI/src/Counters.cpp b/CLI/src/Counters.cpp index 020b4815..8c9e2ec1 100644 --- a/CLI/src/Counters.cpp +++ b/CLI/src/Counters.cpp @@ -142,7 +142,16 @@ void countersDump(const char* path) for (const auto& [line, counters] : sortedCounters) { if (counters.regularExecuted != 0 || counters.fallbackExecuted != 0 || counters.vmExitTaken != 0) - fprintf(f, "%d %lld %lld %lld\n", line, (long long)counters.regularExecuted, (long long)counters.fallbackExecuted, (long long)counters.vmExitTaken); + { + fprintf( + f, + "%d %lld %lld %lld\n", + line, + (long long)counters.regularExecuted, + (long long)counters.fallbackExecuted, + (long long)counters.vmExitTaken + ); + } } } } diff --git a/CLI/src/Flags.cpp b/CLI/src/Flags.cpp index 4bdad341..2ebe94d9 100644 --- a/CLI/src/Flags.cpp +++ b/CLI/src/Flags.cpp @@ -2,6 +2,7 @@ #include "Luau/Common.h" #include "Luau/ExperimentalFlags.h" +#include #include #include @@ -16,6 +17,14 @@ static void setLuauFlag(std::string_view name, bool state) flag->value = state; return; } + if (flag->version != 0) + { + if (name == std::string(flag->name) + std::to_string(flag->version)) + { + flag->value = state; + return; + } + } } fprintf(stderr, "Warning: unrecognized flag '%.*s'.\n", int(name.length()), name.data()); diff --git a/CLI/src/Repl.cpp b/CLI/src/Repl.cpp index d1a76cc9..a23f4057 100644 --- a/CLI/src/Repl.cpp +++ b/CLI/src/Repl.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/Repl.h" +#include "Luau/CodeGenOptions.h" #include "Luau/Common.h" #include "lua.h" #include "lualib.h" @@ -14,6 +15,7 @@ #include "Luau/Coverage.h" #include "Luau/FileUtils.h" #include "Luau/Flags.h" +#include "Luau/JitInliner.h" #include "Luau/Profiler.h" #include "Luau/ReplRequirer.h" #include "Luau/Require.h" @@ -50,16 +52,19 @@ #include LUAU_FASTFLAG(DebugLuauTimeTracing) -LUAU_FASTFLAG(LuauCodegenCounterSupport) +LUAU_FASTFLAG(LuauAutoStack) constexpr int MaxTraversalLimit = 50; static bool codegen = false; +static bool codegenCold = false; + static bool lsl = false; static bool sl = false; static bool builtinsLoaded = false; static lua_SLRuntimeState lsl_state; +static bool jitInliner = false; static int program_argc = 0; char** program_argv = nullptr; @@ -273,6 +278,9 @@ void setupState(lua_State* L) if (codegen) Luau::CodeGen::create(L); + if (jitInliner) + Luau::JitInliner::setup(L); + luaL_openlibs(L); // ServerLua: add cast operations and such @@ -332,7 +340,8 @@ void setupState(lua_State* L) void setupArguments(lua_State* L, int argc, char** argv) { - lua_checkstack(L, argc); + if (!FFlag::LuauAutoStack) + lua_checkstack(L, argc); for (int i = 0; i < argc; ++i) lua_pushstring(L, argv[i]); @@ -340,7 +349,8 @@ void setupArguments(lua_State* L, int argc, char** argv) std::string runCode(lua_State* L, const std::string& source) { - lua_checkstack(L, LUA_MINSTACK); + if (!FFlag::LuauAutoStack) + lua_checkstack(L, LUA_MINSTACK); std::string bytecode = Luau::compile(source, copts()); @@ -517,7 +527,8 @@ static void completeIndexer(lua_State* L, const std::string& editBuffer, const A std::string_view lookup = editBuffer; bool completeOnlyFunctions = false; - lua_checkstack(L, LUA_MINSTACK); + if (!FFlag::LuauAutoStack) + lua_checkstack(L, LUA_MINSTACK); // Push the global variable table to begin the search lua_pushvalue(L, LUA_GLOBALSINDEX); @@ -683,10 +694,28 @@ static void runRepl() runReplImpl(L); } +static std::string getFilePath(const char* name) +{ + if (isFile(name)) + return name; + + std::string base = name; + + std::string luauPath = base + ".luau"; + if (isFile(luauPath)) + return luauPath; + + std::string luaPath = base + ".lua"; + if (isFile(luaPath)) + return luaPath; + + return ""; +} + // `repl` is used it indicate if a repl should be started after executing the file. static bool runFile(const char* name, lua_State* GL, bool repl) { - std::optional source = readFile(name); + std::optional source = readFile(getFilePath(name)); if (!source) { fprintf(stderr, "Error opening %s\n", name); @@ -745,6 +774,10 @@ static bool runFile(const char* name, lua_State* GL, bool repl) if (codegen) { Luau::CodeGen::CompilationOptions nativeOptions; + if (codegenCold) + { + nativeOptions.flags = Luau::CodeGen::CodeGen_ColdFunctions; + } if (countersActive()) nativeOptions.recordCounters = true; @@ -818,7 +851,11 @@ static void displayHelp(const char* argv0) printf(" --lsl: run REPL with LSL semantics\n"); printf(" --sl: run REPL with SL semantics\n"); printf(" --codegen: execute code using native code generation\n"); + printf(" --codegen-cold: execute code using native code generation, including any functions deemed not profitable to natively compile\n"); + printf(" --codegen-perf: execute code using native code generation and profile using perf (only on Linux)\n"); printf(" --program-args,-a: declare start of arguments to be passed to the Luau program\n"); + printf(" --fflags=: comma-separated list of fast flags to enable/disable (--fflags=true,false,LuauFlag1=true,LuauFlag2=false).\n"); + printf(" --jit-inliner: enable JIT bytecode inliner\n"); } static int assertionHandler(const char* expr, const char* file, int line, const char* function) @@ -885,6 +922,11 @@ int replMain(int argc, char** argv) { codegen = true; } + else if (strcmp(argv[i], "--codegen-cold") == 0) + { + codegen = true; + codegenCold = true; + } else if (strcmp(argv[i], "--codegen-perf") == 0) { codegen = true; @@ -897,7 +939,6 @@ int replMain(int argc, char** argv) else if (strcmp(argv[i], "--counters") == 0) { counters = true; - FFlag::LuauCodegenCounterSupport.value = true; } else if (strcmp(argv[i], "--timetrace") == 0) { @@ -916,6 +957,10 @@ int replMain(int argc, char** argv) luauSL_init_global_builtins(argv[i] + 14); builtinsLoaded = true; } + else if (strcmp(argv[i], "--jit-inliner") == 0) + { + jitInliner = true; + } else if (strncmp(argv[i], "--fflags=", 9) == 0) { setLuauFlags(argv[i] + 9); @@ -968,7 +1013,9 @@ int replMain(int argc, char** argv) codegenPerfLog, [](void* context, uintptr_t addr, unsigned size, const char* symbol) { - fprintf(static_cast(context), "%016lx %08x %s\n", long(addr), size, symbol); + FILE* outputFile = static_cast(context); + fprintf(outputFile, "%016lx %08x %s\n", long(addr), size, symbol); + fflush(outputFile); } ); #else diff --git a/CLI/src/ReplRequirer.cpp b/CLI/src/ReplRequirer.cpp index e822a497..05da2809 100644 --- a/CLI/src/ReplRequirer.cpp +++ b/CLI/src/ReplRequirer.cpp @@ -14,6 +14,8 @@ #include #include +LUAU_FASTFLAG(LuauCyclicRequireShortCircuit) + static luarequire_WriteResult write(std::optional contents, char* buffer, size_t bufferSize, size_t* sizeOut) { if (!contents) @@ -165,6 +167,9 @@ static int load(lua_State* L, void* ctx, const char* path, const char* chunkname if (status == 0) { + if (FFlag::LuauCyclicRequireShortCircuit && lua_usesexport(ML, -1) != 0) + luarequire_createplaceholder(L); + if (req->codegenEnabled()) { Luau::CodeGen::CompilationOptions nativeOptions; diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a337774..5b9881b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,8 @@ if (LUAU_BUILD_SHARED) add_library(Luau.CLI.lib SHARED) add_library(Luau.LSLBuiltins SHARED) add_library(Luau.Ast SHARED) + add_library(Luau.Bytecode SHARED) + add_library(Luau.Inliner SHARED) add_library(Luau.Compiler SHARED) add_library(Luau.Config SHARED) add_library(Luau.Analysis SHARED) @@ -66,6 +68,8 @@ else() add_library(Luau.CLI.lib STATIC) add_library(Luau.LSLBuiltins STATIC) add_library(Luau.Ast STATIC) + add_library(Luau.Bytecode STATIC) + add_library(Luau.Inliner STATIC) add_library(Luau.Compiler STATIC) add_library(Luau.Config STATIC) add_library(Luau.Analysis STATIC) @@ -124,9 +128,19 @@ target_compile_features(Luau.Ast PUBLIC cxx_std_17) target_include_directories(Luau.Ast PUBLIC Ast/include) target_link_libraries(Luau.Ast PUBLIC Luau.Common) +target_compile_features(Luau.Bytecode PUBLIC cxx_std_17) +target_include_directories(Luau.Bytecode PUBLIC Bytecode/include) +target_link_libraries(Luau.Bytecode PUBLIC Luau.Common) + +target_compile_features(Luau.Inliner PUBLIC cxx_std_17) +target_include_directories(Luau.Inliner PUBLIC Inliner/include) +target_include_directories(Luau.Inliner PRIVATE Bytecode/src) +target_link_libraries(Luau.Inliner PRIVATE Luau.VM Luau.VM.Internals) +target_link_libraries(Luau.Inliner PUBLIC Luau.Bytecode) + target_compile_features(Luau.Compiler PUBLIC cxx_std_17) target_include_directories(Luau.Compiler PUBLIC Compiler/include "${PACKAGE_INCLUDE_DIR}") -target_link_libraries(Luau.Compiler PUBLIC Luau.Ast) +target_link_libraries(Luau.Compiler PUBLIC Luau.Ast Luau.Bytecode) target_compile_features(Luau.Config PUBLIC cxx_std_17) target_include_directories(Luau.Config PUBLIC Config/include) @@ -246,11 +260,13 @@ if(MSVC AND LUAU_BUILD_CLI) # the default stack size that MSVC linker uses is 1 MB; we need more stack space in Debug because stack frames are larger set_target_properties(Luau.Analyze.CLI PROPERTIES LINK_FLAGS_DEBUG /STACK:2097152) set_target_properties(Luau.Repl.CLI PROPERTIES LINK_FLAGS_DEBUG /STACK:2097152) + set_target_properties(Luau.UnitTest PROPERTIES LINK_FLAGS_DEBUG /STACK:2097152) endif() if(MSVC AND LUAU_BUILD_TESTS) # the default stack size that MSVC linker uses is 1 MB; we need more stack space in Debug because stack frames are larger set_target_properties(Luau.CLI.Test PROPERTIES LINK_FLAGS_DEBUG /STACK:2097152) + set_target_properties(Luau.Conformance PROPERTIES LINK_FLAGS_DEBUG /STACK:2097152) endif() # embed .natvis inside the library debug information @@ -295,7 +311,7 @@ if(LUAU_BUILD_CLI) target_include_directories(Luau.Repl.CLI PRIVATE extern extern/isocline/include) target_link_directories(Luau.Repl.CLI PRIVATE "${PACKAGE_LIB_DIR}") - target_link_libraries(Luau.Repl.CLI PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.VM Luau.Require Luau.CLI.lib isocline "${TAILSLIDE_LIBRARY}") + target_link_libraries(Luau.Repl.CLI PRIVATE Luau.Compiler Luau.Inliner Luau.Config Luau.CodeGen Luau.VM Luau.Require Luau.CLI.lib isocline "${TAILSLIDE_LIBRARY}") target_link_libraries(Luau.Repl.CLI PRIVATE osthreads) target_link_libraries(Luau.Reduce.CLI PRIVATE osthreads) @@ -320,16 +336,16 @@ endif() if(LUAU_BUILD_TESTS) target_compile_options(Luau.UnitTest PRIVATE ${LUAU_OPTIONS}) - target_compile_definitions(Luau.UnitTest PRIVATE DOCTEST_CONFIG_DOUBLE_STRINGIFY) + target_compile_definitions(Luau.UnitTest PRIVATE DOCTEST_CONFIG_DOUBLE_STRINGIFY DOCTEST_CONFIG_USE_STD_HEADERS) target_include_directories(Luau.UnitTest PRIVATE extern "${PACKAGE_INCLUDE_DIR}") target_link_directories(Luau.UnitTest PRIVATE "${PACKAGE_LIB_DIR}") - target_link_libraries(Luau.UnitTest PRIVATE Luau.Analysis Luau.Compiler Luau.CodeGen "${TAILSLIDE_LIBRARY}") + target_link_libraries(Luau.UnitTest PRIVATE Luau.Analysis Luau.Bytecode Luau.Compiler Luau.CodeGen "${TAILSLIDE_LIBRARY}") target_compile_options(Luau.Conformance PRIVATE ${LUAU_OPTIONS}) - target_compile_definitions(Luau.Conformance PRIVATE DOCTEST_CONFIG_DOUBLE_STRINGIFY) - target_include_directories(Luau.Conformance PRIVATE extern "${PACKAGE_INCLUDE_DIR}") + target_compile_definitions(Luau.Conformance PRIVATE DOCTEST_CONFIG_DOUBLE_STRINGIFY DOCTEST_CONFIG_USE_STD_HEADERS) + target_include_directories(Luau.Conformance PRIVATE extern VM/src "${PACKAGE_INCLUDE_DIR}") target_link_directories(Luau.Conformance PRIVATE "${PACKAGE_LIB_DIR}") - target_link_libraries(Luau.Conformance PRIVATE Luau.Analysis Luau.Compiler Luau.CodeGen Luau.VM Luau.LSLBuiltins "${TAILSLIDE_LIBRARY}") + target_link_libraries(Luau.Conformance PRIVATE Luau.Analysis Luau.Bytecode Luau.Inliner Luau.Compiler Luau.CodeGen Luau.VM Luau.LSLBuiltins "${TAILSLIDE_LIBRARY}") if(CMAKE_SYSTEM_NAME MATCHES "Android|iOS") set(LUAU_CONFORMANCE_SOURCE_DIR "Client/Luau/tests/conformance") @@ -341,10 +357,11 @@ if(LUAU_BUILD_TESTS) target_compile_definitions(Luau.Conformance PRIVATE LUAU_CONFORMANCE_SOURCE_DIR="${LUAU_CONFORMANCE_SOURCE_DIR}") target_compile_options(Luau.CLI.Test PRIVATE ${LUAU_OPTIONS}) + target_compile_definitions(Luau.CLI.Test PRIVATE DOCTEST_CONFIG_USE_STD_HEADERS) target_include_directories(Luau.CLI.Test PRIVATE extern CLI) target_link_directories(Luau.CLI.Test PRIVATE "${PACKAGE_LIB_DIR}") - target_link_libraries(Luau.CLI.Test PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.VM Luau.Require Luau.CLI.lib isocline "${TAILSLIDE_LIBRARY}") + target_link_libraries(Luau.CLI.Test PRIVATE Luau.Compiler Luau.Inliner Luau.Config Luau.CodeGen Luau.VM Luau.Require Luau.CLI.lib isocline "${TAILSLIDE_LIBRARY}") target_link_libraries(Luau.CLI.Test PRIVATE osthreads) add_subdirectory(fuzz) diff --git a/CodeGen/include/Luau/AssemblyBuilderA64.h b/CodeGen/include/Luau/AssemblyBuilderA64.h index fe8ff0df..36c85a47 100644 --- a/CodeGen/include/Luau/AssemblyBuilderA64.h +++ b/CodeGen/include/Luau/AssemblyBuilderA64.h @@ -5,6 +5,7 @@ #include "Luau/AddressA64.h" #include "Luau/ConditionA64.h" #include "Luau/Label.h" +#include "Luau/LogBuilder.h" #include #include @@ -25,7 +26,7 @@ enum FeaturesA64 class AssemblyBuilderA64 { public: - explicit AssemblyBuilderA64(bool logText, unsigned int features = 0); + explicit AssemblyBuilderA64(LogBuilder* logger, bool logText_DEPRECATED, unsigned int features); ~AssemblyBuilderA64(); // Moves @@ -43,6 +44,12 @@ class AssemblyBuilderA64 void sub(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2, int shift = 0); void sub(RegisterA64 dst, RegisterA64 src1, uint16_t src2); void neg(RegisterA64 dst, RegisterA64 src); + void mul(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2); + void msub(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2, RegisterA64 src3); + void sdiv(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2); + void udiv(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2); + // predicate: dst is the result of an sdiv/udiv (quotient); src1 is the dividend, src2 is the divisor; dst != src1 + void rem(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2); // Prevent implicit conversions from happening template @@ -58,6 +65,11 @@ class AssemblyBuilderA64 template void cmp(RegisterA64 src1, T src2) = delete; // Prevent implicit conversions from happening + void ccmp(RegisterA64 src1, RegisterA64 src2, ConditionA64 cond, uint8_t nzcv); + void ccmn(RegisterA64 src1, RegisterA64 src2, ConditionA64 cond, uint8_t nzcv); + void ccmn(RegisterA64 src1, uint8_t src2, ConditionA64 cond, uint8_t nzcv); + void cmn(RegisterA64 src1, uint16_t src2); + void csel(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2, ConditionA64 cond); void cset(RegisterA64 dst, ConditionA64 cond); @@ -192,6 +204,8 @@ class AssemblyBuilderA64 void udf(); + void nop(uint32_t bytes = 4); + // Run final checks bool finalize(); @@ -208,6 +222,7 @@ class AssemblyBuilderA64 return label.location * 4; } + // Make private with FFlagLuauCodegenSharedLog removal void logAppend(const char* fmt, ...) LUAU_PRINTF_ATTR(2, 3); // Code size is measured in 'code' array units - uint8_t on x64 and uint32_t on arm64 @@ -220,9 +235,12 @@ class AssemblyBuilderA64 std::vector data; std::vector code; + // Remove with FFlagLuauCodegenSharedLog std::string text; + // Make private with FFlagLuauCodegenSharedLog removal const bool logText = false; + const unsigned int features = 0; // Maximum immediate argument to functions like add/sub/cmp @@ -237,7 +255,7 @@ class AssemblyBuilderA64 private: // Instruction archetypes - void place0(const char* name, uint32_t word); + void place0(const char* name, uint32_t op); void placeSR3(const char* name, RegisterA64 dst, RegisterA64 src1, RegisterA64 src2, uint8_t op, int shift = 0, int N = 0); void placeSR2(const char* name, RegisterA64 dst, RegisterA64 src, uint8_t op, uint8_t op2 = 0); void placeR3(const char* name, RegisterA64 dst, RegisterA64 src1, RegisterA64 src2, uint8_t op, uint8_t op2); @@ -247,12 +265,13 @@ class AssemblyBuilderA64 void placeA(const char* name, RegisterA64 dst, AddressA64 src, uint16_t opsize, int sizelog); void placeB(const char* name, Label& label, uint8_t op); void placeBC(const char* name, Label& label, uint8_t op, uint8_t cond); - void placeBCR(const char* name, Label& label, uint8_t op, RegisterA64 cond); + void placeBCR(const char* name, const char* nameInv, Label& label, uint8_t op, RegisterA64 cond); void placeBR(const char* name, RegisterA64 src, uint32_t op); - void placeBTR(const char* name, Label& label, uint8_t op, RegisterA64 cond, uint8_t bit); - void placeADR(const char* name, RegisterA64 src, uint8_t op); - void placeADR(const char* name, RegisterA64 src, uint8_t op, Label& label); - void placeP(const char* name, RegisterA64 dst1, RegisterA64 dst2, AddressA64 src, uint8_t op, uint8_t opc, int sizelog); + void placeBTR(const char* name, const char* nameInv, Label& label, uint8_t op, RegisterA64 cond, uint8_t bit); + void placeADR(const char* name, RegisterA64 dst, uint8_t op); + void placeADR(const char* name, RegisterA64 dst, uint8_t op, Label& label); + void placeADRP(const char* name, RegisterA64 dst, int32_t pageOffset); + void placeP(const char* name, RegisterA64 src1, RegisterA64 src2, AddressA64 dst, uint8_t op, uint8_t opc, int sizelog); void placeCS(const char* name, RegisterA64 dst, RegisterA64 src1, RegisterA64 src2, ConditionA64 cond, uint8_t op, uint8_t opc, int invert = 0); void placeFCMP(const char* name, RegisterA64 src1, RegisterA64 src2, uint8_t op, uint8_t opc); void placeFMOV(const char* name, RegisterA64 dst, double src, uint32_t op); @@ -277,7 +296,9 @@ class AssemblyBuilderA64 uint32_t location; }; + void patchDataRef(RegisterA64 dst, uint32_t location, size_t pos); void patchLabel(Label& label, Patch::Kind kind); + Label patchLabelFar(Label& label, Patch::Kind kind, uint32_t invertBit); void patchOffset(uint32_t location, int value, Patch::Kind kind); void commit(); @@ -303,6 +324,8 @@ class AssemblyBuilderA64 LUAU_NOINLINE void log(RegisterA64 reg); LUAU_NOINLINE void log(AddressA64 addr); + LogBuilder* logger = nullptr; + uint32_t nextLabel = 1; std::vector pendingLabels; std::vector labelLocations; diff --git a/CodeGen/include/Luau/AssemblyBuilderX64.h b/CodeGen/include/Luau/AssemblyBuilderX64.h index 6ffa4be4..1319f88d 100644 --- a/CodeGen/include/Luau/AssemblyBuilderX64.h +++ b/CodeGen/include/Luau/AssemblyBuilderX64.h @@ -4,6 +4,7 @@ #include "Luau/Common.h" #include "Luau/DenseHash.h" #include "Luau/Label.h" +#include "Luau/LogBuilder.h" #include "Luau/ConditionX64.h" #include "Luau/OperandX64.h" #include "Luau/RegisterX64.h" @@ -48,8 +49,8 @@ enum class ABIX64 class AssemblyBuilderX64 { public: - explicit AssemblyBuilderX64(bool logText, ABIX64 abi, unsigned int features = 0); - explicit AssemblyBuilderX64(bool logText, unsigned int features = 0); + explicit AssemblyBuilderX64(LogBuilder* logger, bool logText_DEPRECATED, ABIX64 abi, unsigned int features); + explicit AssemblyBuilderX64(LogBuilder* logger, bool logText_DEPRECATED, unsigned int features); ~AssemblyBuilderX64(); // Base two operand instructions with 9 opcode selection @@ -110,6 +111,9 @@ class AssemblyBuilderX64 void int3(); void ud2(); + void cqo(); + void cdq(); + void bsr(RegisterX64 dst, OperandX64 src); void bsf(RegisterX64 dst, OperandX64 src); void bswap(RegisterX64 dst); @@ -169,7 +173,7 @@ class AssemblyBuilderX64 void vmovaps(OperandX64 dst, OperandX64 src); void vmovupd(OperandX64 dst, OperandX64 src); void vmovups(OperandX64 dst, OperandX64 src); - void vmovq(OperandX64 lhs, OperandX64 rhs); + void vmovq(OperandX64 dst, OperandX64 src); void vmaxps(OperandX64 dst, OperandX64 src1, OperandX64 src2); void vmaxsd(OperandX64 dst, OperandX64 src1, OperandX64 src2); @@ -220,6 +224,7 @@ class AssemblyBuilderX64 OperandX64 f64x2(double x, double y); OperandX64 bytes(const void* ptr, size_t size, size_t align = 8); + // Make private with FFlagLuauCodegenSharedLog removal void logAppend(const char* fmt, ...) LUAU_PRINTF_ATTR(2, 3); // Code size is measured in 'code' array units - uint8_t on x64 and uint32_t on arm64 @@ -227,13 +232,18 @@ class AssemblyBuilderX64 unsigned getInstructionCount() const; + static const char* getSizeName(SizeX64 size); + static const char* getRegisterName(RegisterX64 reg); + // Resulting data and code that need to be copied over one after the other // The *end* of 'data' has to be aligned to 16 bytes, this will also align 'code' std::vector data; std::vector code; + // Remove with FFlagLuauCodegenSharedLog std::string text; + // Make private with FFlagLuauCodegenSharedLog removal const bool logText = false; const ABIX64 abi; @@ -313,8 +323,7 @@ class AssemblyBuilderX64 LUAU_NOINLINE void log(const char* opcode, RegisterX64 reg, Label label); void log(OperandX64 op); - const char* getSizeName(SizeX64 size) const; - const char* getRegisterName(RegisterX64 reg) const; + LogBuilder* logger = nullptr; uint32_t nextLabel = 1; std::vector