diff --git a/.github/codecov.yml b/.github/codecov.yml index b5f7ec42..7e0dee17 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -2,3 +2,6 @@ comment: false coverage: status: patch: false + project: + default: + informational: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b278c437..86284d74 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: unix: strategy: matrix: - os: [{name: ubuntu, version: ubuntu-20.04}, {name: macos, version: macos-latest}] + os: [{name: ubuntu, version: ubuntu-latest}, {name: macos, version: macos-latest}] name: ${{matrix.os.name}} runs-on: ${{matrix.os.version}} steps: @@ -83,7 +83,7 @@ jobs: Debug/luau-compile tests/conformance/assert.lua coverage: - runs-on: ubuntu-20.04 + runs-on: ubuntu-20.04 # needed for clang++-10 to avoid gcov compatibility issues steps: - uses: actions/checkout@v2 - name: install @@ -99,7 +99,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} web: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/checkout@v2 diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 52b2eb0c..078a18f9 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -12,7 +12,7 @@ permissions: jobs: create-release: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: @@ -29,7 +29,7 @@ jobs: build: needs: ["create-release"] strategy: - matrix: + matrix: # using ubuntu-20.04 to build a Linux binary targeting older glibc to improve compatibility os: [{name: ubuntu, version: ubuntu-20.04}, {name: macos, version: macos-latest}, {name: windows, version: windows-latest}] name: ${{matrix.os.name}} runs-on: ${{matrix.os.version}} @@ -56,7 +56,7 @@ jobs: web: needs: ["create-release"] - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f796a7c1..7d8a5c44 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ on: jobs: build: strategy: - matrix: + matrix: # using ubuntu-20.04 to build a Linux binary targeting older glibc to improve compatibility os: [{name: ubuntu, version: ubuntu-20.04}, {name: macos, version: macos-latest}, {name: windows, version: windows-latest}] name: ${{matrix.os.name}} runs-on: ${{matrix.os.version}} @@ -35,7 +35,7 @@ jobs: path: Release\luau*.exe web: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/checkout@v2 diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index ad10ca99..bba3fced 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -49,8 +49,8 @@ struct InstantiationConstraint TypeId superType; }; -// iteratee is iterable -// iterators is the iteration types. +// variables ~ iterate iterator +// Unpack the iterator, figure out what types it iterates over, and bind those types to variables. struct IterableConstraint { TypePackId iterator; @@ -190,6 +190,11 @@ struct UnpackConstraint { TypePackId resultPack; TypePackId sourcePack; + + // UnpackConstraint is sometimes used to resolve the types of assignments. + // When this is the case, any LocalTypes in resultPack can have their + // domains extended by the corresponding type from sourcePack. + bool resultIsLValue = false; }; // resultType ~ refine type mode discriminant diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGenerator.h similarity index 90% rename from Analysis/include/Luau/ConstraintGraphBuilder.h rename to Analysis/include/Luau/ConstraintGenerator.h index bba5ebd9..a3e1092f 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -57,7 +57,7 @@ struct InferencePack } }; -struct ConstraintGraphBuilder +struct ConstraintGenerator { // A list of all the scopes in the module. This vector holds ownership of the // scope pointers; the scopes themselves borrow pointers to other scopes to @@ -68,7 +68,7 @@ struct ConstraintGraphBuilder NotNull builtinTypes; const NotNull arena; // The root scope of the module we're generating constraints for. - // This is null when the CGB is initially constructed. + // This is null when the CG is initially constructed. Scope* rootScope; struct InferredBinding @@ -78,11 +78,13 @@ struct ConstraintGraphBuilder TypeIds types; }; - // During constraint generation, we only populate the Scope::bindings - // property for annotated symbols. Unannotated symbols must be handled in a - // postprocessing step because we have not yet allocated the types that will - // be assigned to those unannotated symbols, so we queue them up here. - std::map inferredBindings; + // Some locals have multiple type states. We wish for Scope::bindings to + // map each local name onto the union of every type that the local can have + // over its lifetime, so we use this map to accumulate the set of types it + // might have. + // + // See the functions recordInferredBinding and fillInInferredBindings. + DenseHashMap inferredBindings{{}}; // Constraints that go straight to the solver. std::vector constraints; @@ -116,13 +118,13 @@ struct ConstraintGraphBuilder DcrLogger* logger; - ConstraintGraphBuilder(ModulePtr module, NotNull normalizer, NotNull moduleResolver, + ConstraintGenerator(ModulePtr module, NotNull normalizer, NotNull moduleResolver, NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, std::vector requireCycles); /** - * The entry point to the ConstraintGraphBuilder. This will construct a set + * The entry point to the ConstraintGenerator. This will construct a set * of scopes, constraints, and free types that can be solved later. * @param block the root block to generate constraints for. */ @@ -148,6 +150,8 @@ private: */ ScopePtr childScope(AstNode* node, const ScopePtr& parent); + std::optional lookup(Scope* scope, DefId def); + /** * Adds a new constraint with no dependencies to a given scope. * @param scope the scope to add the constraint to. @@ -221,6 +225,7 @@ private: Inference check(const ScopePtr& scope, AstExprConstantBool* bool_, std::optional expectedType, bool forceSingleton); Inference check(const ScopePtr& scope, AstExprLocal* local); Inference check(const ScopePtr& scope, AstExprGlobal* global); + Inference checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, std::string index); Inference check(const ScopePtr& scope, AstExprIndexName* indexName); Inference check(const ScopePtr& scope, AstExprIndexExpr* indexExpr); Inference check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize); @@ -232,14 +237,16 @@ private: Inference check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType); std::tuple checkBinary(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType); - std::optional checkLValue(const ScopePtr& scope, AstExpr* expr); - std::optional checkLValue(const ScopePtr& scope, AstExprLocal* local); - std::optional checkLValue(const ScopePtr& scope, AstExprGlobal* global); - std::optional checkLValue(const ScopePtr& scope, AstExprIndexName* indexName); - std::optional checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr); - TypeId updateProperty(const ScopePtr& scope, AstExpr* expr); - - void updateLValueType(AstExpr* lvalue, TypeId ty); + /** + * Generate constraints to assign assignedTy to the expression expr + * @returns the type of the expression. This may or may not be assignedTy itself. + */ + std::optional checkLValue(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprIndexName* indexName, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy); + TypeId updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy); struct FunctionSignature { @@ -324,12 +331,16 @@ private: /** Scan the program for global definitions. * - * ConstraintGraphBuilder needs to differentiate between globals and accesses to undefined symbols. Doing this "for + * ConstraintGenerator needs to differentiate between globals and accesses to undefined symbols. Doing this "for * real" in a general way is going to be pretty hard, so we are choosing not to tackle that yet. For now, we do an * initial scan of the AST and note what globals are defined. */ void prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program); + // Record the fact that a particular local has a particular type in at least + // one of its states. + void recordInferredBinding(AstLocal* local, TypeId ty); + void fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block); /** Given a function type annotation, return a vector describing the expected types of the calls to the function diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index a0afeed7..4a4d639b 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -3,14 +3,18 @@ #pragma once #include "Luau/Constraint.h" +#include "Luau/DenseHash.h" #include "Luau/Error.h" +#include "Luau/Location.h" #include "Luau/Module.h" #include "Luau/Normalize.h" #include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeCheckLimits.h" +#include "Luau/TypeFwd.h" #include "Luau/Variant.h" +#include #include namespace Luau @@ -74,6 +78,10 @@ struct ConstraintSolver 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 + // these to provide more helpful error messages when a free type is solved + // as never unexpectedly. + DenseHashMap>> upperBoundContributors{nullptr}; // A mapping from free types to the number of unresolved constraints that mention them. DenseHashMap unresolvedConstraints{{}}; @@ -140,7 +148,7 @@ struct ConstraintSolver std::pair, std::optional> lookupTableProp( TypeId subjectType, const std::string& propName, bool suppressSimplification = false); std::pair, std::optional> lookupTableProp( - TypeId subjectType, const std::string& propName, bool suppressSimplification, std::unordered_set& seen); + TypeId subjectType, const std::string& propName, bool suppressSimplification, DenseHashSet& seen); void block(NotNull target, NotNull constraint); /** diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index 34a0484a..083e5046 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -3,6 +3,7 @@ // Do not include LValue. It should never be used here. #include "Luau/Ast.h" +#include "Luau/ControlFlow.h" #include "Luau/DenseHash.h" #include "Luau/Def.h" #include "Luau/Symbol.h" @@ -34,7 +35,7 @@ struct DataFlowGraph DataFlowGraph& operator=(DataFlowGraph&&) = default; DefId getDef(const AstExpr* expr) const; - // Look up for the rvalue breadcrumb for a compound assignment. + // Look up for the rvalue def for a compound assignment. std::optional getRValueDefForCompoundAssign(const AstExpr* expr) const; DefId getDef(const AstLocal* local) const; @@ -64,7 +65,7 @@ private: // Compound assignments are in a weird situation where the local being assigned to is also being used at its // previous type implicitly in an rvalue position. This map provides the previous binding. - DenseHashMap compoundAssignBreadcrumbs{nullptr}; + DenseHashMap compoundAssignDefs{nullptr}; DenseHashMap astRefinementKeys{nullptr}; @@ -74,11 +75,21 @@ private: struct DfgScope { DfgScope* parent; - DenseHashMap bindings{Symbol{}}; - DenseHashMap> props{nullptr}; + bool isLoopScope; + + using Bindings = DenseHashMap; + using Props = DenseHashMap>; + + Bindings bindings{Symbol{}}; + Props props{nullptr}; std::optional lookup(Symbol symbol) const; std::optional lookup(DefId def, const std::string& key) const; + + void inherit(const DfgScope* childScope); + + bool canUpdateDefinition(Symbol symbol) const; + bool canUpdateDefinition(DefId def, const std::string& key) const; }; struct DataFlowResult @@ -106,31 +117,38 @@ private: std::vector> scopes; - DfgScope* childScope(DfgScope* scope); + DfgScope* childScope(DfgScope* scope, bool isLoopScope = false); - void visit(DfgScope* scope, AstStatBlock* b); - void visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b); + void join(DfgScope* p, DfgScope* a, DfgScope* b); + void joinBindings(DfgScope::Bindings& p, const DfgScope::Bindings& a, const DfgScope::Bindings& b); + void joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b); - void visit(DfgScope* scope, AstStat* s); - void visit(DfgScope* scope, AstStatIf* i); - void visit(DfgScope* scope, AstStatWhile* w); - void visit(DfgScope* scope, AstStatRepeat* r); - void visit(DfgScope* scope, AstStatBreak* b); - void visit(DfgScope* scope, AstStatContinue* c); - void visit(DfgScope* scope, AstStatReturn* r); - void visit(DfgScope* scope, AstStatExpr* e); - void visit(DfgScope* scope, AstStatLocal* l); - void visit(DfgScope* scope, AstStatFor* f); - void visit(DfgScope* scope, AstStatForIn* f); - void visit(DfgScope* scope, AstStatAssign* a); - void visit(DfgScope* scope, AstStatCompoundAssign* c); - void visit(DfgScope* scope, AstStatFunction* f); - void visit(DfgScope* scope, AstStatLocalFunction* l); - void visit(DfgScope* scope, AstStatTypeAlias* t); - void visit(DfgScope* scope, AstStatDeclareGlobal* d); - void visit(DfgScope* scope, AstStatDeclareFunction* d); - void visit(DfgScope* scope, AstStatDeclareClass* d); - void visit(DfgScope* scope, AstStatError* error); + DefId lookup(DfgScope* scope, Symbol symbol); + DefId lookup(DfgScope* scope, DefId def, const std::string& key); + + ControlFlow visit(DfgScope* scope, AstStatBlock* b); + ControlFlow visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b); + + ControlFlow visit(DfgScope* scope, AstStat* s); + ControlFlow visit(DfgScope* scope, AstStatIf* i); + ControlFlow visit(DfgScope* scope, AstStatWhile* w); + ControlFlow visit(DfgScope* scope, AstStatRepeat* r); + ControlFlow visit(DfgScope* scope, AstStatBreak* b); + ControlFlow visit(DfgScope* scope, AstStatContinue* c); + ControlFlow visit(DfgScope* scope, AstStatReturn* r); + ControlFlow visit(DfgScope* scope, AstStatExpr* e); + ControlFlow visit(DfgScope* scope, AstStatLocal* l); + ControlFlow visit(DfgScope* scope, AstStatFor* f); + ControlFlow visit(DfgScope* scope, AstStatForIn* f); + ControlFlow visit(DfgScope* scope, AstStatAssign* a); + ControlFlow visit(DfgScope* scope, AstStatCompoundAssign* c); + ControlFlow visit(DfgScope* scope, AstStatFunction* f); + ControlFlow visit(DfgScope* scope, AstStatLocalFunction* l); + ControlFlow visit(DfgScope* scope, AstStatTypeAlias* t); + ControlFlow visit(DfgScope* scope, AstStatDeclareGlobal* d); + ControlFlow visit(DfgScope* scope, AstStatDeclareFunction* d); + ControlFlow visit(DfgScope* scope, AstStatDeclareClass* d); + ControlFlow visit(DfgScope* scope, AstStatError* error); DataFlowResult visitExpr(DfgScope* scope, AstExpr* e); DataFlowResult visitExpr(DfgScope* scope, AstExprGroup* group); diff --git a/Analysis/include/Luau/Def.h b/Analysis/include/Luau/Def.h index 0a286ae9..e3fec9b6 100644 --- a/Analysis/include/Luau/Def.h +++ b/Analysis/include/Luau/Def.h @@ -79,8 +79,8 @@ struct DefArena TypedAllocator allocator; DefId freshCell(bool subscripted = false); - // TODO: implement once we have cases where we need to merge in definitions - // DefId phi(const std::vector& defs); + DefId phi(DefId a, DefId b); + DefId phi(const std::vector& defs); }; } // namespace Luau diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index 4b6c64c3..06ea9601 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -322,6 +322,7 @@ struct TypePackMismatch { TypePackId wantedTp; TypePackId givenTp; + std::string reason; bool operator==(const TypePackMismatch& rhs) const; }; @@ -371,13 +372,21 @@ struct CheckedFunctionCallError bool operator==(const CheckedFunctionCallError& rhs) const; }; +struct NonStrictFunctionDefinitionError +{ + std::string functionName; + std::string argument; + TypeId argumentType; + bool operator==(const NonStrictFunctionDefinitionError& rhs) const; +}; + using TypeErrorData = Variant; + UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError, NonStrictFunctionDefinitionError>; struct TypeErrorSummary { diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index 25af5200..2b83c443 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -71,7 +71,7 @@ struct SourceNode ModuleName name; std::string humanReadableName; - std::unordered_set requireSet; + DenseHashSet requireSet{{}}; std::vector> requireLocations; bool dirtySourceModule = true; bool dirtyModule = true; @@ -206,7 +206,7 @@ private: std::vector& buildQueue, const ModuleName& root, bool forAutocomplete, std::function canSkip = {}); void addBuildQueueItems(std::vector& items, std::vector& buildQueue, bool cycleDetected, - std::unordered_set& seen, const FrontendOptions& frontendOptions); + DenseHashSet& seen, const FrontendOptions& frontendOptions); void checkBuildQueueItem(BuildQueueItem& item); void checkBuildQueueItems(std::vector& items); void recordItemResult(const BuildQueueItem& item); diff --git a/Analysis/include/Luau/Module.h b/Analysis/include/Luau/Module.h index d647750f..197c7f9c 100644 --- a/Analysis/include/Luau/Module.h +++ b/Analysis/include/Luau/Module.h @@ -102,6 +102,8 @@ struct Module DenseHashMap astResolvedTypes{nullptr}; DenseHashMap astResolvedTypePacks{nullptr}; + DenseHashMap>> upperBoundContributors{nullptr}; + // Map AST nodes to the scope they create. Cannot be NotNull because // we need a sentinel value for the map. DenseHashMap astScopes{nullptr}; diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index ebb80e0f..4508d4a4 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -2,6 +2,7 @@ #pragma once #include "Luau/NotNull.h" +#include "Luau/Set.h" #include "Luau/TypeFwd.h" #include "Luau/UnifierSharedState.h" @@ -9,7 +10,6 @@ #include #include #include -#include #include namespace Luau @@ -29,7 +29,7 @@ bool isConsistentSubtype(TypePackId subTy, TypePackId superTy, NotNull sc class TypeIds { private: - std::unordered_set types; + DenseHashMap types{nullptr}; std::vector order; std::size_t hash = 0; @@ -254,6 +254,10 @@ struct NormalizedType // This type is either never or thread. TypeId threads; + // The buffer part of the type. + // This type is either never or buffer. + TypeId buffers; + // The (meta)table part of the type. // Each element of this set is a (meta)table type, or the top `table` type. // An empty set denotes never. @@ -277,6 +281,7 @@ struct NormalizedType NormalizedType& operator=(NormalizedType&&) = default; // IsType functions + bool isUnknown() const; /// Returns true if the type is exactly a number. Behaves like Type::isNumber() bool isExactlyNumber() const; @@ -298,6 +303,7 @@ struct NormalizedType bool hasNumbers() const; bool hasStrings() const; bool hasThreads() const; + bool hasBuffers() const; bool hasTables() const; bool hasFunctions() const; bool hasTyvars() const; @@ -358,7 +364,7 @@ public: void unionTablesWithTable(TypeIds& heres, TypeId there); void unionTables(TypeIds& heres, const TypeIds& theres); bool unionNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1); - bool unionNormalWithTy(NormalizedType& here, TypeId there, std::unordered_set& seenSetTypes, int ignoreSmallerTyvars = -1); + bool unionNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes, int ignoreSmallerTyvars = -1); // ------- Negations std::optional negateNormal(const NormalizedType& here); @@ -380,15 +386,15 @@ public: std::optional intersectionOfFunctions(TypeId here, TypeId there); void intersectFunctionsWithFunction(NormalizedFunctionType& heress, TypeId there); void intersectFunctions(NormalizedFunctionType& heress, const NormalizedFunctionType& theress); - bool intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, std::unordered_set& seenSetTypes); + bool intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, Set& seenSetTypes); bool intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1); - bool intersectNormalWithTy(NormalizedType& here, TypeId there, std::unordered_set& seenSetTypes); + bool intersectNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes); bool normalizeIntersections(const std::vector& intersections, NormalizedType& outType); // Check for inhabitance bool isInhabited(TypeId ty); - bool isInhabited(TypeId ty, std::unordered_set seen); - bool isInhabited(const NormalizedType* norm, std::unordered_set seen = {}); + bool isInhabited(TypeId ty, Set seen); + bool isInhabited(const NormalizedType* norm, Set seen = {nullptr}); // Check for intersections being inhabited bool isIntersectionInhabited(TypeId left, TypeId right); diff --git a/Analysis/include/Luau/Scope.h b/Analysis/include/Luau/Scope.h index 8cdffcb4..3f2b7355 100644 --- a/Analysis/include/Luau/Scope.h +++ b/Analysis/include/Luau/Scope.h @@ -56,7 +56,7 @@ struct Scope void addBuiltinTypeBinding(const Name& name, const TypeFun& tyFun); std::optional lookup(Symbol sym) const; - std::optional lookupLValue(DefId def) const; + std::optional lookupUnrefinedType(DefId def) const; std::optional lookup(DefId def) const; std::optional> lookupEx(DefId def); std::optional> lookupEx(Symbol sym); @@ -80,6 +80,7 @@ struct Scope // types here. DenseHashMap rvalueRefinements{nullptr}; + void inheritAssignments(const ScopePtr& childScope); void inheritRefinements(const ScopePtr& childScope); // For mutually recursive type aliases, it's important that diff --git a/Analysis/include/Luau/Set.h b/Analysis/include/Luau/Set.h new file mode 100644 index 00000000..3f34c325 --- /dev/null +++ b/Analysis/include/Luau/Set.h @@ -0,0 +1,171 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/DenseHash.h" + +namespace Luau +{ + +template +using SetHashDefault = std::conditional_t, DenseHashPointer, std::hash>; + +// This is an implementation of `unordered_set` using `DenseHashMap` to support erasure. +// This lets us work around `DenseHashSet` limitations and get a more traditional set interface. +template> +class Set +{ +private: + using Impl = DenseHashMap; + Impl mapping; + size_t entryCount = 0; + +public: + class const_iterator; + using iterator = const_iterator; + + Set(const T& empty_key) + : mapping{empty_key} + { + } + + bool insert(const T& element) + { + bool& entry = mapping[element]; + bool fresh = !entry; + + if (fresh) + { + entry = true; + entryCount++; + } + + return fresh; + } + + template + void insert(Iterator begin, Iterator end) + { + for (Iterator it = begin; it != end; ++it) + insert(*it); + } + + void erase(const T& element) + { + bool& entry = mapping[element]; + + if (entry) + { + entry = false; + entryCount--; + } + } + + void clear() + { + mapping.clear(); + entryCount = 0; + } + + size_t size() const + { + return entryCount; + } + + bool empty() const + { + return entryCount == 0; + } + + size_t count(const T& element) const + { + const bool* entry = mapping.find(element); + return (entry && *entry) ? 1 : 0; + } + + bool contains(const T& element) const + { + return count(element) != 0; + } + + const_iterator begin() const + { + return const_iterator(mapping.begin(), mapping.end()); + } + + const_iterator end() const + { + return const_iterator(mapping.end(), mapping.end()); + } + + bool operator==(const Set& there) const + { + // if the sets are unequal sizes, then they cannot possibly be equal. + if (size() != there.size()) + return false; + + // otherwise, we'll need to check that every element we have here is in `there`. + for (auto [elem, present] : mapping) + { + // if it's not, we'll return `false` + if (present && there.contains(elem)) + return false; + } + + // otherwise, we've proven the two equal! + return true; + } + + class const_iterator + { + public: + const_iterator(typename Impl::const_iterator impl, typename Impl::const_iterator end) + : impl(impl) + , end(end) + {} + + const T& operator*() const + { + return impl->first; + } + + const T* operator->() const + { + return &impl->first; + } + + + bool operator==(const const_iterator& other) const + { + return impl == other.impl; + } + + bool operator!=(const const_iterator& other) const + { + return impl != other.impl; + } + + + const_iterator& operator++() + { + do + { + impl++; + } while (impl != end && impl->second == false); + // keep iterating past pairs where the value is `false` + + return *this; + } + + const_iterator operator++(int) + { + const_iterator res = *this; + ++*this; + return res; + } + private: + typename Impl::const_iterator impl; + typename Impl::const_iterator end; + }; +}; + +} // namespace Luau diff --git a/Analysis/include/Luau/Simplify.h b/Analysis/include/Luau/Simplify.h index 064648d7..10f27d4e 100644 --- a/Analysis/include/Luau/Simplify.h +++ b/Analysis/include/Luau/Simplify.h @@ -2,11 +2,10 @@ #pragma once +#include "Luau/DenseHash.h" #include "Luau/NotNull.h" #include "Luau/TypeFwd.h" -#include - namespace Luau { @@ -16,7 +15,7 @@ struct SimplifyResult { TypeId result; - std::set blockedTypes; + DenseHashSet blockedTypes; }; SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, TypeId ty, TypeId discriminant); diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index 321563e7..d2619fe2 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -1,10 +1,11 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/Set.h" #include "Luau/TypeFwd.h" #include "Luau/TypePairHash.h" -#include "Luau/UnifierSharedState.h" #include "Luau/TypePath.h" +#include "Luau/DenseHash.h" #include #include @@ -22,15 +23,40 @@ struct NormalizedType; struct NormalizedClassType; struct NormalizedStringType; struct NormalizedFunctionType; +struct TypeArena; +struct Scope; +struct TableIndexer; + +enum class SubtypingVariance +{ + // Used for an empty key. Should never appear in actual code. + Invalid, + Covariant, + // This is used to identify cases where we have a covariant + a + // contravariant reason and we need to merge them. + Contravariant, + Invariant, +}; struct SubtypingReasoning { + // The path, relative to the _root subtype_, where subtyping failed. Path subPath; + // The path, relative to the _root supertype_, where subtyping failed. Path superPath; + SubtypingVariance variance = SubtypingVariance::Covariant; bool operator==(const SubtypingReasoning& other) const; }; +struct SubtypingReasoningHash +{ + size_t operator()(const SubtypingReasoning& r) const; +}; + +using SubtypingReasonings = DenseHashSet; +static const SubtypingReasoning kEmptyReasoning = SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invalid}; + struct SubtypingResult { bool isSubtype = false; @@ -40,7 +66,7 @@ struct SubtypingResult /// The reason for isSubtype to be false. May not be present even if /// isSubtype is false, depending on the input types. - std::optional reasoning; + SubtypingReasonings reasoning{kEmptyReasoning}; SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other); @@ -92,9 +118,9 @@ struct Subtyping Variance variance = Variance::Covariant; - using SeenSet = std::unordered_set, TypeIdPairHash>; + using SeenSet = Set, TypePairHash>; - SeenSet seenTypes; + SeenSet seenTypes{{}}; Subtyping(NotNull builtinTypes, NotNull typeArena, NotNull normalizer, NotNull iceReporter, NotNull scope); diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index 9b8564fb..70494685 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -21,7 +21,6 @@ #include #include #include -#include #include LUAU_FASTINT(LuauTableTypeMaximumStringifierLength) @@ -87,6 +86,24 @@ struct FreeType TypeId upperBound = nullptr; }; +/** A type that tracks the domain of a local variable. + * + * We consider each local's domain to be the union of all types assigned to it. + * We accomplish this with LocalType. Each time we dispatch an assignment to a + * local, we accumulate this union and decrement blockCount. + * + * When blockCount reaches 0, we can consider the LocalType to be "fully baked" + * and replace it with the union we've built. + */ +struct LocalType +{ + TypeId domain; + int blockCount = 0; + + // Used for debugging + std::string name; +}; + struct GenericType { // By default, generics are global, with a synthetic name @@ -141,6 +158,7 @@ struct PrimitiveType Thread, Function, Table, + Buffer, }; Type type; @@ -373,7 +391,15 @@ struct Property bool deprecated = false; std::string deprecatedSuggestion; + + // If this property was inferred from an expression, this field will be + // populated with the source location of the corresponding table property. std::optional location = std::nullopt; + + // If this property was built from an explicit type annotation, this field + // will be populated with the source location of that table property. + std::optional typeLocation = std::nullopt; + Tags tags; std::optional documentationSymbol; @@ -381,7 +407,7 @@ struct Property // TODO: Kill all constructors in favor of `Property::rw(TypeId read, TypeId write)` and friends. Property(); Property(TypeId readTy, bool deprecated = false, const std::string& deprecatedSuggestion = "", std::optional location = std::nullopt, - const Tags& tags = {}, const std::optional& documentationSymbol = std::nullopt); + const Tags& tags = {}, const std::optional& documentationSymbol = std::nullopt, std::optional typeLocation = std::nullopt); // DEPRECATED: Should only be called in non-RWP! We assert that the `readTy` is not nullopt. // TODO: Kill once we don't have non-RWP. @@ -615,7 +641,7 @@ struct NegationType using ErrorType = Unifiable::Error; using TypeVariant = - Unifiable::Variant; struct Type final @@ -739,6 +765,7 @@ bool isBoolean(TypeId ty); bool isNumber(TypeId ty); bool isString(TypeId ty); bool isThread(TypeId ty); +bool isBuffer(TypeId ty); bool isOptional(TypeId ty); bool isTableIntersection(TypeId ty); bool isOverloadedFunction(TypeId ty); @@ -797,6 +824,7 @@ public: const TypeId stringType; const TypeId booleanType; const TypeId threadType; + const TypeId bufferType; const TypeId functionType; const TypeId classType; const TypeId tableType; @@ -965,7 +993,7 @@ private: using SavedIterInfo = std::pair; std::deque stack; - std::unordered_set seen; // Only needed to protect the iterator from hanging the thread. + DenseHashSet seen{nullptr}; // Only needed to protect the iterator from hanging the thread. void advance() { @@ -992,7 +1020,7 @@ private: { // If we're about to descend into a cyclic type, we should skip over this. // Ideally this should never happen, but alas it does from time to time. :( - if (seen.find(inner) != seen.end()) + if (seen.contains(inner)) advance(); else { diff --git a/Analysis/include/Luau/TypeChecker2.h b/Analysis/include/Luau/TypeChecker2.h index aeeab0f8..b30cfe01 100644 --- a/Analysis/include/Luau/TypeChecker2.h +++ b/Analysis/include/Luau/TypeChecker2.h @@ -2,8 +2,6 @@ #pragma once -#include "Luau/Ast.h" -#include "Luau/Module.h" #include "Luau/NotNull.h" namespace Luau @@ -13,6 +11,8 @@ struct BuiltinTypes; struct DcrLogger; struct TypeCheckLimits; struct UnifierSharedState; +struct SourceModule; +struct Module; void check(NotNull builtinTypes, NotNull sharedState, NotNull limits, DcrLogger* logger, const SourceModule& sourceModule, Module* module); diff --git a/Analysis/include/Luau/TypeInfer.h b/Analysis/include/Luau/TypeInfer.h index 7de01406..26a67c7a 100644 --- a/Analysis/include/Luau/TypeInfer.h +++ b/Analysis/include/Luau/TypeInfer.h @@ -377,6 +377,7 @@ public: const TypeId stringType; const TypeId booleanType; const TypeId threadType; + const TypeId bufferType; const TypeId anyType; const TypeId unknownType; const TypeId neverType; diff --git a/Analysis/include/Luau/TypeOrPack.h b/Analysis/include/Luau/TypeOrPack.h index 2bdca1df..87001910 100644 --- a/Analysis/include/Luau/TypeOrPack.h +++ b/Analysis/include/Luau/TypeOrPack.h @@ -12,32 +12,28 @@ namespace Luau const void* ptr(TypeOrPack ty); -template -const T* get(TypeOrPack ty) +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) { - if constexpr (std::is_same_v) - return ty.get_if(); - else if constexpr (std::is_same_v) - return ty.get_if(); - else if constexpr (TypeVariant::is_part_of_v) - { - if (auto innerTy = ty.get_if()) - return get(*innerTy); - else - return nullptr; - } - else if constexpr (TypePackVariant::is_part_of_v) - { - if (auto innerTp = ty.get_if()) - return get(*innerTp); - else - return nullptr; - } + return tyOrTp.get_if(); +} + +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) +{ + if (const TypeId* ty = get(tyOrTp)) + return get(*ty); else - { - static_assert(always_false_v, "invalid T to get from TypeOrPack"); - LUAU_UNREACHABLE(); - } + return nullptr; +} + +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) +{ + if (const TypePackId* tp = get(tyOrTp)) + return get(*tp); + else + return nullptr; } TypeOrPack follow(TypeOrPack ty); diff --git a/Analysis/include/Luau/TypePath.h b/Analysis/include/Luau/TypePath.h index 96fcdcb1..bdca95a4 100644 --- a/Analysis/include/Luau/TypePath.h +++ b/Analysis/include/Luau/TypePath.h @@ -4,7 +4,6 @@ #include "Luau/TypeFwd.h" #include "Luau/Variant.h" #include "Luau/NotNull.h" -#include "Luau/TypeOrPack.h" #include #include @@ -153,6 +152,16 @@ struct Path } }; +struct PathHash +{ + size_t operator()(const Property& prop) const; + size_t operator()(const Index& idx) const; + size_t operator()(const TypeField& field) const; + size_t operator()(const PackField& field) const; + size_t operator()(const Component& component) const; + size_t operator()(const Path& path) const; +}; + /// The canonical "empty" Path, meaning a Path with no components. static const Path kEmpty{}; @@ -184,7 +193,7 @@ using Path = TypePath::Path; /// Converts a Path to a string for debugging purposes. This output may not be /// terribly clear to end users of the Luau type system. -std::string toString(const TypePath::Path& path); +std::string toString(const TypePath::Path& path, bool prefixDot = false); std::optional traverse(TypeId root, const Path& path, NotNull builtinTypes); std::optional traverse(TypePackId root, const Path& path, NotNull builtinTypes); diff --git a/Analysis/include/Luau/Unifier2.h b/Analysis/include/Luau/Unifier2.h index 3d5b5a1a..4930df6f 100644 --- a/Analysis/include/Luau/Unifier2.h +++ b/Analysis/include/Luau/Unifier2.h @@ -6,7 +6,6 @@ #include "Luau/NotNull.h" #include "Luau/TypePairHash.h" #include "Luau/TypeCheckLimits.h" -#include "Luau/TypeChecker2.h" #include "Luau/TypeFwd.h" #include @@ -37,6 +36,8 @@ struct Unifier2 DenseHashSet, TypePairHash> seenTypePairings{{nullptr, nullptr}}; DenseHashSet, TypePairHash> seenTypePackPairings{{nullptr, nullptr}}; + DenseHashMap> expandedFreeTypes{nullptr}; + int recursionCount = 0; int recursionLimit = 0; @@ -60,7 +61,7 @@ struct Unifier2 bool unify(TypeId subTy, const UnionType* superUnion); bool unify(const IntersectionType* subIntersection, TypeId superTy); bool unify(TypeId subTy, const IntersectionType* superIntersection); - bool unify(const TableType* subTable, const TableType* superTable); + bool unify(TableType* subTable, const TableType* superTable); bool unify(const MetatableType* subMetatable, const MetatableType* superMetatable); // TODO think about this one carefully. We don't do unions or intersections of type packs diff --git a/Analysis/include/Luau/VisitType.h b/Analysis/include/Luau/VisitType.h index ea0acd2b..6e1fea6a 100644 --- a/Analysis/include/Luau/VisitType.h +++ b/Analysis/include/Luau/VisitType.h @@ -97,6 +97,10 @@ struct GenericTypeVisitor { return visit(ty); } + virtual bool visit(TypeId ty, const LocalType& ftv) + { + return visit(ty); + } virtual bool visit(TypeId ty, const GenericType& gtv) { return visit(ty); @@ -241,6 +245,11 @@ struct GenericTypeVisitor else visit(ty, *ftv); } + else if (auto lt = get(ty)) + { + if (visit(ty, *lt)) + traverse(lt->domain); + } else if (auto gtv = get(ty)) visit(ty, *gtv); else if (auto etv = get(ty)) diff --git a/Analysis/src/AstJsonEncoder.cpp b/Analysis/src/AstJsonEncoder.cpp index 920517c2..2d1940d4 100644 --- a/Analysis/src/AstJsonEncoder.cpp +++ b/Analysis/src/AstJsonEncoder.cpp @@ -8,7 +8,6 @@ #include -LUAU_FASTFLAG(LuauFloorDivision); LUAU_FASTFLAG(LuauClipExtraHasEndProps); namespace Luau @@ -519,7 +518,6 @@ struct AstJsonEncoder : public AstVisitor case AstExprBinary::Div: return writeString("Div"); case AstExprBinary::FloorDiv: - LUAU_ASSERT(FFlag::LuauFloorDivision); return writeString("FloorDiv"); case AstExprBinary::Mod: return writeString("Mod"); diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index d73598c7..52cb54e3 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -5,6 +5,7 @@ #include "Luau/BuiltinDefinitions.h" #include "Luau/Frontend.h" #include "Luau/ToString.h" +#include "Luau/Subtyping.h" #include "Luau/TypeInfer.h" #include "Luau/TypePack.h" @@ -12,6 +13,7 @@ #include #include +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauReadWriteProperties); LUAU_FASTFLAG(LuauClipExtraHasEndProps); LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false); @@ -143,13 +145,24 @@ static bool checkTypeMatch(TypeId subTy, TypeId superTy, NotNull scope, T InternalErrorReporter iceReporter; UnifierSharedState unifierState(&iceReporter); Normalizer normalizer{typeArena, builtinTypes, NotNull{&unifierState}}; - Unifier unifier(NotNull{&normalizer}, scope, Location(), Variance::Covariant); - // Cost of normalization can be too high for autocomplete response time requirements - unifier.normalize = false; - unifier.checkInhabited = false; + if (FFlag::DebugLuauDeferredConstraintResolution) + { + Subtyping subtyping{builtinTypes, NotNull{typeArena}, NotNull{&normalizer}, NotNull{&iceReporter}, scope}; + + return subtyping.isSubtype(subTy, superTy).isSubtype; + } + else + { + Unifier unifier(NotNull{&normalizer}, scope, Location(), Variance::Covariant); + + // Cost of normalization can be too high for autocomplete response time requirements + unifier.normalize = false; + unifier.checkInhabited = false; + + return unifier.canUnify(subTy, superTy).empty(); + } - return unifier.canUnify(subTy, superTy).empty(); } static TypeCorrectKind checkTypeCorrectKind( diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index b7631460..5ce12873 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -7,7 +7,7 @@ #include "Luau/Common.h" #include "Luau/ToString.h" #include "Luau/ConstraintSolver.h" -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/NotNull.h" #include "Luau/TypeInfer.h" #include "Luau/TypeFamily.h" diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 01b0bdfd..5fe9e787 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -12,9 +12,8 @@ LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) -LUAU_FASTFLAGVARIABLE(LuauCloneCyclicUnions, false) -LUAU_FASTFLAGVARIABLE(LuauStacklessTypeClone2, false) +LUAU_FASTFLAGVARIABLE(LuauStacklessTypeClone3, false) LUAU_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000) namespace Luau @@ -118,6 +117,8 @@ private: ty = follow(ty, FollowOption::DisableLazyTypeThunks); if (auto it = types->find(ty); it != types->end()) return it->second; + else if (ty->persistent) + return ty; return std::nullopt; } @@ -126,6 +127,8 @@ private: tp = follow(tp); if (auto it = packs->find(tp); it != packs->end()) return it->second; + else if (tp->persistent) + return tp; return std::nullopt; } @@ -258,6 +261,11 @@ private: t->upperBound = shallowClone(t->upperBound); } + void cloneChildren(LocalType* t) + { + t->domain = shallowClone(t->domain); + } + void cloneChildren(GenericType* t) { // TOOD: clone upper bounds. @@ -501,6 +509,7 @@ struct TypeCloner void defaultClone(const T& t); void operator()(const FreeType& t); + void operator()(const LocalType& t); void operator()(const GenericType& t); void operator()(const BoundType& t); void operator()(const ErrorType& t); @@ -628,6 +637,11 @@ void TypeCloner::operator()(const FreeType& t) defaultClone(t); } +void TypeCloner::operator()(const LocalType& t) +{ + defaultClone(t); +} + void TypeCloner::operator()(const GenericType& t) { defaultClone(t); @@ -778,33 +792,19 @@ void TypeCloner::operator()(const AnyType& t) void TypeCloner::operator()(const UnionType& t) { - if (FFlag::LuauCloneCyclicUnions) - { - // We're just using this FreeType as a placeholder until we've finished - // cloning the parts of this union so it is okay that its bounds are - // nullptr. We'll never indirect them. - TypeId result = dest.addType(FreeType{nullptr, /*lowerBound*/ nullptr, /*upperBound*/ nullptr}); - seenTypes[typeId] = result; + // We're just using this FreeType as a placeholder until we've finished + // cloning the parts of this union so it is okay that its bounds are + // nullptr. We'll never indirect them. + TypeId result = dest.addType(FreeType{nullptr, /*lowerBound*/ nullptr, /*upperBound*/ nullptr}); + seenTypes[typeId] = result; - std::vector options; - options.reserve(t.options.size()); + std::vector options; + options.reserve(t.options.size()); - for (TypeId ty : t.options) - options.push_back(clone(ty, dest, cloneState)); + for (TypeId ty : t.options) + options.push_back(clone(ty, dest, cloneState)); - asMutable(result)->ty.emplace(std::move(options)); - } - else - { - std::vector options; - options.reserve(t.options.size()); - - for (TypeId ty : t.options) - options.push_back(clone(ty, dest, cloneState)); - - TypeId result = dest.addType(UnionType{std::move(options)}); - seenTypes[typeId] = result; - } + asMutable(result)->ty.emplace(std::move(options)); } void TypeCloner::operator()(const IntersectionType& t) @@ -879,7 +879,7 @@ TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState) if (tp->persistent) return tp; - if (FFlag::LuauStacklessTypeClone2) + if (FFlag::LuauStacklessTypeClone3) { TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}}; return cloner.clone(tp); @@ -905,7 +905,7 @@ TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState) if (typeId->persistent) return typeId; - if (FFlag::LuauStacklessTypeClone2) + if (FFlag::LuauStacklessTypeClone3) { TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}}; return cloner.clone(typeId); @@ -934,7 +934,7 @@ TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState) TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState) { - if (FFlag::LuauStacklessTypeClone2) + if (FFlag::LuauStacklessTypeClone3) { TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}}; diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGenerator.cpp similarity index 87% rename from Analysis/src/ConstraintGraphBuilder.cpp rename to Analysis/src/ConstraintGenerator.cpp index 4f4ff306..cdfe9a7f 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGenerator.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/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/Ast.h" #include "Luau/Def.h" @@ -7,6 +7,7 @@ #include "Luau/Constraint.h" #include "Luau/ControlFlow.h" #include "Luau/DcrLogger.h" +#include "Luau/DenseHash.h" #include "Luau/ModuleResolver.h" #include "Luau/RecursionCounter.h" #include "Luau/Refinement.h" @@ -23,9 +24,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit); LUAU_FASTFLAG(DebugLuauLogSolverToJson); LUAU_FASTFLAG(DebugLuauMagicTypes); -LUAU_FASTFLAG(LuauParseDeclareClassIndexer); LUAU_FASTFLAG(LuauLoopControlFlowAnalysis); -LUAU_FASTFLAG(LuauFloorDivision); namespace Luau { @@ -126,21 +125,21 @@ struct Checkpoint size_t offset; }; -Checkpoint checkpoint(const ConstraintGraphBuilder* cgb) +Checkpoint checkpoint(const ConstraintGenerator* cg) { - return Checkpoint{cgb->constraints.size()}; + return Checkpoint{cg->constraints.size()}; } template -void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const ConstraintGraphBuilder* cgb, F f) +void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const ConstraintGenerator* cg, F f) { for (size_t i = start.offset; i < end.offset; ++i) - f(cgb->constraints[i]); + f(cg->constraints[i]); } } // namespace -ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, NotNull normalizer, NotNull moduleResolver, +ConstraintGenerator::ConstraintGenerator(ModulePtr module, NotNull normalizer, NotNull moduleResolver, NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, std::vector requireCycles) @@ -160,7 +159,7 @@ ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, NotNullcaptureGenerationModule(module); } -TypeId ConstraintGraphBuilder::freshType(const ScopePtr& scope) +TypeId ConstraintGenerator::freshType(const ScopePtr& scope) { return Luau::freshType(arena, builtinTypes, scope.get()); } -TypePackId ConstraintGraphBuilder::freshTypePack(const ScopePtr& scope) +TypePackId ConstraintGenerator::freshTypePack(const ScopePtr& scope) { FreeTypePack f{scope.get()}; return arena->addTypePack(TypePackVar{std::move(f)}); } -ScopePtr ConstraintGraphBuilder::childScope(AstNode* node, const ScopePtr& parent) +ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent) { auto scope = std::make_shared(parent); scopes.emplace_back(node->location, scope); @@ -206,17 +205,50 @@ ScopePtr ConstraintGraphBuilder::childScope(AstNode* node, const ScopePtr& paren return scope; } -NotNull ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, const Location& location, ConstraintV cv) +std::optional ConstraintGenerator::lookup(Scope* scope, DefId def) +{ + if (get(def)) + return scope->lookup(def); + if (auto phi = get(def)) + { + if (auto found = scope->lookup(def)) + return *found; + + TypeId res = builtinTypes->neverType; + + for (DefId operand : phi->operands) + { + // `scope->lookup(operand)` may return nothing because it could be a phi node of globals, but one of + // the operand of that global has never been assigned a type, and so it should be an error. + // e.g. + // ``` + // if foo() then + // g = 5 + // end + // -- `g` here is a phi node of the assignment to `g`, or the original revision of `g` before the branch. + // ``` + TypeId ty = scope->lookup(operand).value_or(builtinTypes->errorRecoveryType()); + res = simplifyUnion(builtinTypes, arena, res, ty).result; + } + + scope->lvalueTypes[def] = res; + return res; + } + else + ice->ice("ConstraintGenerator::lookup is inexhaustive?"); +} + +NotNull ConstraintGenerator::addConstraint(const ScopePtr& scope, const Location& location, ConstraintV cv) { return NotNull{constraints.emplace_back(new Constraint{NotNull{scope.get()}, location, std::move(cv)}).get()}; } -NotNull ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, std::unique_ptr c) +NotNull ConstraintGenerator::addConstraint(const ScopePtr& scope, std::unique_ptr c) { return NotNull{constraints.emplace_back(std::move(c)).get()}; } -void ConstraintGraphBuilder::unionRefinements(const RefinementContext& lhs, const RefinementContext& rhs, RefinementContext& dest, std::vector* constraints) +void ConstraintGenerator::unionRefinements(const RefinementContext& lhs, const RefinementContext& rhs, RefinementContext& dest, std::vector* constraints) { const auto intersect = [&](const std::vector& types) { if (1 == types.size()) @@ -252,7 +284,7 @@ void ConstraintGraphBuilder::unionRefinements(const RefinementContext& lhs, cons } } -void ConstraintGraphBuilder::computeRefinement(const ScopePtr& scope, RefinementId refinement, RefinementContext* refis, bool sense, bool eq, std::vector* constraints) +void ConstraintGenerator::computeRefinement(const ScopePtr& scope, RefinementId refinement, RefinementContext* refis, bool sense, bool eq, std::vector* constraints) { if (!refinement) return; @@ -382,7 +414,7 @@ bool mustDeferIntersection(TypeId ty) } } // namespace -void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location location, RefinementId refinement) +void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location location, RefinementId refinement) { if (!refinement) return; @@ -393,7 +425,7 @@ void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location lo for (auto& [def, partition] : refinements) { - if (std::optional defTy = scope->lookup(def)) + if (std::optional defTy = lookup(scope.get(), def)) { TypeId ty = *defTy; if (partition.shouldAppendNilType) @@ -439,7 +471,7 @@ void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location lo addConstraint(scope, location, c); } -ControlFlow ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block) +ControlFlow ConstraintGenerator::visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block) { RecursionCounter counter{&recursionCount}; @@ -502,7 +534,7 @@ ControlFlow ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& return firstControlFlow.value_or(ControlFlow::None); } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStat* stat) { RecursionLimiter limiter{&recursionCount, FInt::LuauCheckRecursionLimit}; @@ -560,10 +592,14 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) } } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* statLocal) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* statLocal) { - std::vector> varTypes; - varTypes.reserve(statLocal->vars.size); + std::vector annotatedTypes; + annotatedTypes.reserve(statLocal->vars.size); + bool hasAnnotation = false; + + std::vector> expectedTypes; + expectedTypes.reserve(statLocal->vars.size); std::vector assignees; assignees.reserve(statLocal->vars.size); @@ -576,7 +612,8 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* s { const Location location = local->location; - TypeId assignee = arena->addType(BlockedType{}); + TypeId assignee = arena->addType(LocalType{builtinTypes->neverType, /* blockCount */ 1, local->name.value}); + assignees.push_back(assignee); if (!firstValueType) @@ -584,16 +621,21 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* s if (local->annotation) { + hasAnnotation = true; TypeId annotationTy = resolveType(scope, local->annotation, /* inTypeArguments */ false); - varTypes.push_back(annotationTy); - - addConstraint(scope, local->location, SubtypeConstraint{assignee, annotationTy}); + annotatedTypes.push_back(annotationTy); + expectedTypes.push_back(annotationTy); scope->bindings[local] = Binding{annotationTy, location}; } else { - varTypes.push_back(std::nullopt); + // annotatedTypes must contain one type per local. If a particular + // local has no annotation at, assume the most conservative thing. + annotatedTypes.push_back(builtinTypes->unknownType); + + expectedTypes.push_back(std::nullopt); + scope->bindings[local] = Binding{builtinTypes->unknownType, location}; inferredBindings[local] = {scope.get(), location, {assignee}}; } @@ -602,8 +644,12 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* s scope->lvalueTypes[def] = assignee; } - TypePackId resultPack = checkPack(scope, statLocal->values, varTypes).tp; - addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); + TypePackId resultPack = checkPack(scope, statLocal->values, expectedTypes).tp; + addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true}); + + // Types must flow between whatever annotations were provided and the rhs expression. + if (hasAnnotation) + addConstraint(scope, statLocal->location, PackSubtypeConstraint{resultPack, arena->addTypePack(std::move(annotatedTypes))}); if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope) { @@ -663,7 +709,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* s return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for_) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFor* for_) { TypeId annotationTy = builtinTypes->numberType; if (for_->var->annotation) @@ -693,7 +739,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* forIn) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forIn) { ScopePtr loopScope = childScope(forIn, scope); @@ -704,17 +750,18 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f for (AstLocal* var : forIn->vars) { - TypeId ty = nullptr; - if (var->annotation) - ty = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); - else - ty = freshType(loopScope); - - loopScope->bindings[var] = Binding{ty, var->location}; - TypeId assignee = arena->addType(BlockedType{}); variableTypes.push_back(assignee); + if (var->annotation) + { + TypeId annotationTy = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); + loopScope->bindings[var] = Binding{annotationTy, var->location}; + addConstraint(scope, var->location, SubtypeConstraint{assignee, annotationTy}); + } + else + loopScope->bindings[var] = Binding{assignee, var->location}; + DefId def = dfg->getDef(var); loopScope->lvalueTypes[def] = assignee; } @@ -728,7 +775,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatWhile* while_) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatWhile* while_) { RefinementId refinement = check(scope, while_->condition).refinement; @@ -740,7 +787,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatWhile* w return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatRepeat* repeat) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatRepeat* repeat) { ScopePtr repeatScope = childScope(repeat, scope); @@ -751,7 +798,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatRepeat* return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFunction* function) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFunction* function) { // Local // Global @@ -801,7 +848,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFun return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction* function) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* function) { // Name could be AstStatLocal, AstStatGlobal, AstStatIndexName. // With or without self @@ -811,10 +858,10 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction Checkpoint start = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location); - std::unordered_set excludeList; + DenseHashSet excludeList{nullptr}; DefId def = dfg->getDef(function->name); - std::optional existingFunctionTy = scope->lookupLValue(def); + std::optional existingFunctionTy = scope->lookup(def); if (AstExprLocal* localName = function->name->as()) { @@ -846,7 +893,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction else if (AstExprIndexName* indexName = function->name->as()) { Checkpoint check1 = checkpoint(this); - std::optional lvalueType = checkLValue(scope, indexName); + std::optional lvalueType = checkLValue(scope, indexName, generalizedType); LUAU_ASSERT(lvalueType); Checkpoint check2 = checkpoint(this); @@ -856,12 +903,9 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction // TODO figure out how to populate the location field of the table Property. - if (lvalueType) + if (lvalueType && *lvalueType != generalizedType) { - if (get(*lvalueType)) - asMutable(*lvalueType)->ty.emplace(generalizedType); - else - addConstraint(scope, indexName->location, SubtypeConstraint{*lvalueType, generalizedType}); + addConstraint(scope, indexName->location, SubtypeConstraint{*lvalueType, generalizedType}); } } else if (AstExprError* err = function->name->as()) @@ -883,7 +927,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction Constraint* previous = nullptr; forEachConstraint(start, end, this, [&c, &excludeList, &previous](const ConstraintPtr& constraint) { - if (!excludeList.count(constraint.get())) + if (!excludeList.contains(constraint.get())) c->dependencies.push_back(NotNull{constraint.get()}); if (auto psc = get(*constraint); psc && psc->returns) @@ -900,7 +944,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatReturn* ret) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatReturn* ret) { // At this point, the only way scope->returnType should have anything // interesting in it is if the function has an explicit return annotation. @@ -916,12 +960,16 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatReturn* return ControlFlow::Returns; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatBlock* block) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatBlock* block) { ScopePtr innerScope = childScope(block, scope); ControlFlow flow = visitBlockWithoutChildScope(innerScope, block); + + // 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); return flow; } @@ -944,49 +992,34 @@ static void bindFreeType(TypeId a, TypeId b) asMutable(b)->ty.emplace(a); } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign) { - std::vector> expectedTypes; - expectedTypes.reserve(assign->vars.size); - std::vector assignees; assignees.reserve(assign->vars.size); for (AstExpr* lvalue : assign->vars) { TypeId assignee = arena->addType(BlockedType{}); + + checkLValue(scope, lvalue, assignee); assignees.push_back(assignee); - std::optional upperBound = follow(checkLValue(scope, lvalue)); - if (upperBound) - { - if (get(*upperBound)) - expectedTypes.push_back(std::nullopt); - else - expectedTypes.push_back(*upperBound); - - addConstraint(scope, lvalue->location, SubtypeConstraint{assignee, *upperBound}); - } - DefId def = dfg->getDef(lvalue); scope->lvalueTypes[def] = assignee; - updateLValueType(lvalue, assignee); } - TypePackId resultPack = checkPack(scope, assign->values, expectedTypes).tp; - addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); + TypePackId resultPack = checkPack(scope, assign->values).tp; + addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true}); return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompoundAssign* assign) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatCompoundAssign* assign) { - std::optional varTy = checkLValue(scope, assign->var); - AstExprBinary binop = AstExprBinary{assign->location, assign->op, assign->var, assign->value}; TypeId resultTy = check(scope, &binop).ty; - if (varTy) - addConstraint(scope, assign->location, SubtypeConstraint{resultTy, *varTy}); + + checkLValue(scope, assign->var, resultTy); DefId def = dfg->getDef(assign->var); scope->lvalueTypes[def] = resultTy; @@ -994,7 +1027,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompound return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatIf* ifStatement) { RefinementId refinement = check(scope, ifStatement->condition, std::nullopt).refinement; @@ -1014,6 +1047,11 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifSt 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 (FFlag::LuauLoopControlFlowAnalysis && thencf == elsecf) return thencf; else if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) @@ -1041,7 +1079,7 @@ static bool occursCheck(TypeId needle, TypeId haystack) return false; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatTypeAlias* alias) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias* alias) { ScopePtr* defnScope = astTypeAliasDefiningScopes.find(alias); @@ -1090,7 +1128,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatTypeAlia return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareGlobal* global) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareGlobal* global) { LUAU_ASSERT(global->type); @@ -1112,10 +1150,10 @@ static bool isMetamethod(const Name& name) return name == "__index" || name == "__newindex" || name == "__call" || name == "__concat" || name == "__unm" || name == "__add" || name == "__sub" || name == "__mul" || name == "__div" || name == "__mod" || name == "__pow" || name == "__tostring" || name == "__metatable" || name == "__eq" || name == "__lt" || name == "__le" || name == "__mode" || name == "__iter" || name == "__len" || - (FFlag::LuauFloorDivision && name == "__idiv"); + name == "__idiv"; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareClass* declaredClass) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareClass* declaredClass) { std::optional superTy = std::make_optional(builtinTypes->classType); if (declaredClass->superName) @@ -1154,7 +1192,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareC scope->exportedTypeBindings[className] = TypeFun{{}, classTy}; - if (FFlag::LuauParseDeclareClassIndexer && declaredClass->indexer) + if (declaredClass->indexer) { RecursionCounter counter{&recursionCount}; @@ -1234,7 +1272,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareC return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareFunction* global) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareFunction* global) { std::vector> generics = createGenerics(scope, global->generics); std::vector> genericPacks = createGenericPacks(scope, global->genericPacks); @@ -1279,7 +1317,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareF return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatError* error) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatError* error) { for (AstStat* stat : error->statements) visit(scope, stat); @@ -1289,7 +1327,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatError* e return ControlFlow::None; } -InferencePack ConstraintGraphBuilder::checkPack( +InferencePack ConstraintGenerator::checkPack( const ScopePtr& scope, AstArray exprs, const std::vector>& expectedTypes) { std::vector head; @@ -1320,7 +1358,7 @@ InferencePack ConstraintGraphBuilder::checkPack( return InferencePack{arena->addTypePack(TypePack{std::move(head), tail})}; } -InferencePack ConstraintGraphBuilder::checkPack( +InferencePack ConstraintGenerator::checkPack( const ScopePtr& scope, AstExpr* expr, const std::vector>& expectedTypes, bool generalize) { RecursionCounter counter{&recursionCount}; @@ -1356,7 +1394,7 @@ InferencePack ConstraintGraphBuilder::checkPack( return result; } -InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCall* call) +InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* call) { std::vector exprArgs; @@ -1402,9 +1440,6 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa module->astOriginalCallTypes[call->func] = fnType; module->astOriginalCallTypes[call] = fnType; - TypeId instantiatedFnType = arena->addType(BlockedType{}); - addConstraint(scope, call->location, InstantiationConstraint{instantiatedFnType, fnType}); - Checkpoint argBeginCheckpoint = checkpoint(this); std::vector args; @@ -1491,8 +1526,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa scope->lvalueTypes[def] = resultTy; // TODO: typestates: track this as an assignment scope->rvalueRefinements[def] = resultTy; // TODO: typestates: track this as an assignment - if (auto it = inferredBindings.find(targetLocal->local); it != inferredBindings.end()) - it->second.types.insert(resultTy); + recordInferredBinding(targetLocal->local, resultTy); } return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}}; @@ -1530,7 +1564,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa } } -Inference ConstraintGraphBuilder::check( +Inference ConstraintGenerator::check( const ScopePtr& scope, AstExpr* expr, std::optional expectedType, bool forceSingleton, bool generalize) { RecursionCounter counter{&recursionCount}; @@ -1600,7 +1634,7 @@ Inference ConstraintGraphBuilder::check( return result; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantString* string, std::optional expectedType, bool forceSingleton) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantString* string, std::optional expectedType, bool forceSingleton) { if (forceSingleton) return Inference{arena->addType(SingletonType{StringSingleton{std::string{string->value.data, string->value.size}}})}; @@ -1624,7 +1658,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantSt return Inference{builtinTypes->stringType}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantBool* boolExpr, std::optional expectedType, bool forceSingleton) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantBool* boolExpr, std::optional expectedType, bool forceSingleton) { const TypeId singletonType = boolExpr->value ? builtinTypes->trueType : builtinTypes->falseType; if (forceSingleton) @@ -1649,7 +1683,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantBo return Inference{builtinTypes->booleanType}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* local) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) { const RefinementKey* key = dfg->getRefinementKey(local); std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(local); @@ -1659,26 +1693,26 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* loc // if we have a refinement key, we can look up its type. if (key) - maybeTy = scope->lookup(key->def); + maybeTy = lookup(scope.get(), key->def); // if the current def doesn't have a type, we might be doing a compound assignment // and therefore might need to look at the rvalue def instead. if (!maybeTy && rvalueDef) - maybeTy = scope->lookup(*rvalueDef); + maybeTy = lookup(scope.get(), *rvalueDef); if (maybeTy) { TypeId ty = follow(*maybeTy); - if (auto it = inferredBindings.find(local->local); it != inferredBindings.end()) - it->second.types.insert(ty); + + recordInferredBinding(local->local, ty); return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)}; } else - ice->ice("CGB: AstExprLocal came before its declaration?"); + ice->ice("CG: AstExprLocal came before its declaration?"); } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* global) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* global) { const RefinementKey* key = dfg->getRefinementKey(global); std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(global); @@ -1690,11 +1724,11 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* gl /* prepopulateGlobalScope() has already added all global functions to the environment by this point, so any * global that is not already in-scope is definitely an unknown symbol. */ - if (auto ty = scope->lookup(def)) + if (auto ty = lookup(scope.get(), def)) return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; else if (auto ty = scope->lookup(global->name)) { - rootScope->rvalueRefinements[key->def] = *ty; + rootScope->lvalueTypes[def] = *ty; return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; } else @@ -1704,21 +1738,20 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* gl } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName* indexName) +Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, std::string index) { - TypeId obj = check(scope, indexName->expr).ty; + TypeId obj = check(scope, indexee).ty; TypeId result = arena->addType(BlockedType{}); - const RefinementKey* key = dfg->getRefinementKey(indexName); if (key) { - if (auto ty = scope->lookup(key->def)) + if (auto ty = lookup(scope.get(), key->def)) return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; scope->rvalueRefinements[key->def] = result; } - addConstraint(scope, indexName->expr->location, HasPropConstraint{result, obj, indexName->index.value}); + addConstraint(scope, indexee->location, HasPropConstraint{result, obj, std::move(index)}); if (key) return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)}; @@ -1726,16 +1759,29 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName* return Inference{result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* indexName) { + const RefinementKey* key = dfg->getRefinementKey(indexName); + return checkIndexName(scope, key, indexName->expr, indexName->index.value); +} + +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr) +{ + if (auto constantString = indexExpr->index->as()) + { + const RefinementKey* key = dfg->getRefinementKey(indexExpr); + return checkIndexName(scope, key, indexExpr->expr, constantString->value.data); + } + TypeId obj = check(scope, indexExpr->expr).ty; TypeId indexType = check(scope, indexExpr->index).ty; + TypeId result = freshType(scope); const RefinementKey* key = dfg->getRefinementKey(indexExpr); if (key) { - if (auto ty = scope->lookup(key->def)) + if (auto ty = lookup(scope.get(), key->def)) return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; scope->rvalueRefinements[key->def] = result; @@ -1752,7 +1798,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* return Inference{result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize) { Checkpoint startCheckpoint = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, func, expectedType); @@ -1785,7 +1831,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* unary) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprUnary* unary) { auto [operandType, refinement] = check(scope, unary->expr); @@ -1826,7 +1872,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* una } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) { auto [leftType, rightType, refinement] = checkBinary(scope, binary, expectedType); @@ -1990,7 +2036,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional expectedType) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional expectedType) { ScopePtr condScope = childScope(ifElse->condition, scope); RefinementId refinement = check(condScope, ifElse->condition).refinement; @@ -2006,13 +2052,13 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* if return Inference{expectedType ? *expectedType : simplifyUnion(builtinTypes, arena, thenType, elseType).result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert) { check(scope, typeAssert->expr, std::nullopt); return Inference{resolveType(scope, typeAssert->annotation, /* inTypeArguments */ false)}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprInterpString* interpString) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprInterpString* interpString) { for (AstExpr* expr : interpString->expressions) check(scope, expr); @@ -2020,7 +2066,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprInterpStri return Inference{builtinTypes->stringType}; } -std::tuple ConstraintGraphBuilder::checkBinary( +std::tuple ConstraintGenerator::checkBinary( const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) { if (binary->op == AstExprBinary::And) @@ -2077,6 +2123,8 @@ std::tuple ConstraintGraphBuilder::checkBinary( discriminantTy = builtinTypes->booleanType; else if (typeguard->type == "thread") discriminantTy = builtinTypes->threadType; + else if (typeguard->type == "buffer") + discriminantTy = builtinTypes->bufferType; else if (typeguard->type == "table") discriminantTy = augmentForErrorSupression(builtinTypes->tableType); else if (typeguard->type == "function") @@ -2133,16 +2181,16 @@ std::tuple ConstraintGraphBuilder::checkBinary( } } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy) { if (auto local = expr->as()) - return checkLValue(scope, local); + return checkLValue(scope, local, assignedTy); else if (auto global = expr->as()) - return checkLValue(scope, global); + return checkLValue(scope, global, assignedTy); else if (auto indexName = expr->as()) - return checkLValue(scope, indexName); + return checkLValue(scope, indexName, assignedTy); else if (auto indexExpr = expr->as()) - return checkLValue(scope, indexExpr); + return checkLValue(scope, indexExpr, assignedTy); else if (auto error = expr->as()) { check(scope, error); @@ -2152,59 +2200,52 @@ std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, ice->ice("checkLValue is inexhaustive"); } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprLocal* local) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy) { - /* - * The caller of this method uses the returned type to emit the proper - * SubtypeConstraint. - * - * At this point during constraint generation, the binding table is only - * populated by symbols that have type annotations. - * - * If this local has an interesting type annotation, it is important that we - * return that. - */ std::optional annotatedTy = scope->lookup(local->local); + LUAU_ASSERT(annotatedTy); if (annotatedTy) - return annotatedTy; + addConstraint(scope, local->location, SubtypeConstraint{assignedTy, *annotatedTy}); - /* - * As a safety measure, we'll assert that no type has yet been ascribed to - * the corresponding def. We'll populate this when we generate - * constraints for assignment and compound assignment statements. - */ - LUAU_ASSERT(!scope->lookupLValue(dfg->getDef(local))); - return std::nullopt; + const DefId defId = dfg->getDef(local); + std::optional ty = scope->lookupUnrefinedType(defId); + + if (ty) + { + if (auto lt = getMutable(*ty)) + ++lt->blockCount; + } + else + { + ty = arena->addType(LocalType{builtinTypes->neverType, /* blockCount */ 1, local->local->name.value}); + + scope->lvalueTypes[defId] = *ty; + } + + addConstraint(scope, local->location, UnpackConstraint{ + arena->addTypePack({*ty}), + arena->addTypePack({assignedTy}), + /*resultIsLValue*/ true + }); + + recordInferredBinding(local->local, *ty); + + return ty; } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprGlobal* global) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy) { return scope->lookup(Symbol{global->name}); } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName, TypeId assignedTy) { - return updateProperty(scope, indexName); + return updateProperty(scope, indexName, assignedTy); } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy) { - return updateProperty(scope, indexExpr); -} - -static bool isIndexNameEquivalent(AstExpr* expr) -{ - if (expr->is()) - return true; - - AstExprIndexExpr* e = expr->as(); - if (e == nullptr) - return false; - - if (!e->index->is()) - return false; - - return true; + return updateProperty(scope, indexExpr, assignedTy); } /** @@ -2212,8 +2253,19 @@ static bool isIndexNameEquivalent(AstExpr* expr) * * If expr has the form name.a.b.c */ -TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* expr) +TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy) { + // There are a bunch of cases where we realize that this is not the kind of + // assignment that potentially changes the shape of a table. When we + // encounter them, we call this to fall back and do the "usual thing." + auto fallback = [&]() { + TypeId resTy = check(scope, expr).ty; + addConstraint(scope, expr->location, SubtypeConstraint{assignedTy, resTy}); + return resTy; + }; + + LUAU_ASSERT(expr->is() || expr->is()); + if (auto indexExpr = expr->as(); indexExpr && !indexExpr->index->is()) { // An indexer is only interesting in an lvalue-ey way if it is at the @@ -2231,15 +2283,12 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex TypeId resultType = arena->addType(BlockedType{}); TypeId subjectType = check(scope, indexExpr->expr).ty; TypeId indexType = check(scope, indexExpr->index).ty; - TypeId propType = arena->addType(BlockedType{}); - addConstraint(scope, expr->location, SetIndexerConstraint{resultType, subjectType, indexType, propType}); + addConstraint(scope, expr->location, SetIndexerConstraint{resultType, subjectType, indexType, assignedTy}); - module->astTypes[expr] = propType; + module->astTypes[expr] = assignedTy; - return propType; + return assignedTy; } - else if (!isIndexNameEquivalent(expr)) - return check(scope, expr).ty; Symbol sym; const Def* def = nullptr; @@ -2269,21 +2318,24 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex } else if (auto indexExpr = e->as()) { - // We need to populate the type for the index value - check(scope, indexExpr->index); if (auto strIndex = indexExpr->index->as()) { + // We need to populate astTypes for the index value. + check(scope, indexExpr->index); + segments.push_back(std::string(strIndex->value.data, strIndex->value.size)); exprs.push_back(e); e = indexExpr->expr; } else { - return check(scope, expr).ty; + return fallback(); } } else - return check(scope, expr).ty; + { + return fallback(); + } } LUAU_ASSERT(!segments.empty()); @@ -2294,16 +2346,14 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex LUAU_ASSERT(def); std::optional> lookupResult = scope->lookupEx(NotNull{def}); if (!lookupResult) - return check(scope, expr).ty; + return fallback(); const auto [subjectType, subjectScope] = *lookupResult; - TypeId propTy = freshType(scope); - std::vector segmentStrings(begin(segments), end(segments)); TypeId updatedType = arena->addType(BlockedType{}); - addConstraint(scope, expr->location, SetPropConstraint{updatedType, subjectType, std::move(segmentStrings), propTy}); + addConstraint(scope, expr->location, SetPropConstraint{updatedType, subjectType, std::move(segmentStrings), assignedTy}); TypeId prevSegmentTy = updatedType; for (size_t i = 0; i < segments.size(); ++i) @@ -2330,19 +2380,10 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex } } - return propTy; + return assignedTy; } -void ConstraintGraphBuilder::updateLValueType(AstExpr* lvalue, TypeId ty) -{ - if (auto local = lvalue->as()) - { - if (auto it = inferredBindings.find(local->local); it != inferredBindings.end()) - it->second.types.insert(ty); - } -} - -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) { const bool expectedTypeIsFree = expectedType && get(follow(*expectedType)); @@ -2462,7 +2503,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* exp return Inference{ty}; } -ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionSignature( +ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignature( const ScopePtr& parent, AstExprFunction* fn, std::optional expectedType, std::optional originalName) { ScopePtr signatureScope = nullptr; @@ -2565,13 +2606,7 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS argTypes.push_back(argTy); argNames.emplace_back(FunctionArgument{local->name.value, local->location}); - if (local->annotation) - signatureScope->bindings[local] = Binding{argTy, local->location}; - else - { - signatureScope->bindings[local] = Binding{builtinTypes->neverType, local->location}; - inferredBindings[local] = {signatureScope.get(), {}}; - } + signatureScope->bindings[local] = Binding{argTy, local->location}; DefId def = dfg->getDef(local); signatureScope->lvalueTypes[def] = argTy; @@ -2654,7 +2689,7 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS }; } -void ConstraintGraphBuilder::checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn) +void ConstraintGenerator::checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn) { visitBlockWithoutChildScope(scope, fn->body); @@ -2662,12 +2697,12 @@ void ConstraintGraphBuilder::checkFunctionBody(const ScopePtr& scope, AstExprFun if (nullptr != getFallthrough(fn->body)) { - TypePackId empty = arena->addTypePack({}); // TODO we could have CGB retain one of these forever + TypePackId empty = arena->addTypePack({}); // TODO we could have CG retain one of these forever addConstraint(scope, fn->location, PackSubtypeConstraint{scope->returnType, empty}); } } -TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) +TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) { TypeId result = nullptr; @@ -2895,7 +2930,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b return result; } -TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, AstTypePack* tp, bool inTypeArgument, bool replaceErrorWithFresh) +TypePackId ConstraintGenerator::resolveTypePack(const ScopePtr& scope, AstTypePack* tp, bool inTypeArgument, bool replaceErrorWithFresh) { TypePackId result; if (auto expl = tp->as()) @@ -2929,7 +2964,7 @@ TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, AstTyp return result; } -TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, const AstTypeList& list, bool inTypeArguments, bool replaceErrorWithFresh) +TypePackId ConstraintGenerator::resolveTypePack(const ScopePtr& scope, const AstTypeList& list, bool inTypeArguments, bool replaceErrorWithFresh) { std::vector head; @@ -2947,7 +2982,7 @@ TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, const return arena->addTypePack(TypePack{head, tail}); } -std::vector> ConstraintGraphBuilder::createGenerics( +std::vector> ConstraintGenerator::createGenerics( const ScopePtr& scope, AstArray generics, bool useCache, bool addTypes) { std::vector> result; @@ -2977,7 +3012,7 @@ std::vector> ConstraintGraphBuilder::crea return result; } -std::vector> ConstraintGraphBuilder::createGenericPacks( +std::vector> ConstraintGenerator::createGenericPacks( const ScopePtr& scope, AstArray generics, bool useCache, bool addTypes) { std::vector> result; @@ -3008,7 +3043,7 @@ std::vector> ConstraintGraphBuilder:: return result; } -Inference ConstraintGraphBuilder::flattenPack(const ScopePtr& scope, Location location, InferencePack pack) +Inference ConstraintGenerator::flattenPack(const ScopePtr& scope, Location location, InferencePack pack) { const auto& [tp, refinements] = pack; RefinementId refinement = nullptr; @@ -3025,7 +3060,7 @@ Inference ConstraintGraphBuilder::flattenPack(const ScopePtr& scope, Location lo return Inference{typeResult, refinement}; } -void ConstraintGraphBuilder::reportError(Location location, TypeErrorData err) +void ConstraintGenerator::reportError(Location location, TypeErrorData err) { errors.push_back(TypeError{location, module->name, std::move(err)}); @@ -3033,7 +3068,7 @@ void ConstraintGraphBuilder::reportError(Location location, TypeErrorData err) logger->captureGenerationError(errors.back()); } -void ConstraintGraphBuilder::reportCodeTooComplex(Location location) +void ConstraintGenerator::reportCodeTooComplex(Location location) { errors.push_back(TypeError{location, module->name, CodeTooComplex{}}); @@ -3054,22 +3089,30 @@ struct GlobalPrepopulator : AstVisitor { } + bool visit(AstExprGlobal* global) override + { + if (auto ty = globalScope->lookup(global->name)) + { + DefId def = dfg->getDef(global); + globalScope->lvalueTypes[def] = *ty; + } + + return true; + } + bool visit(AstStatFunction* function) override { if (AstExprGlobal* g = function->name->as()) { TypeId bt = arena->addType(BlockedType{}); globalScope->bindings[g->name] = Binding{bt}; - - DefId def = dfg->getDef(function->name); - globalScope->lvalueTypes[def] = bt; } return true; } }; -void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) +void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) { GlobalPrepopulator gp{NotNull{globalScope.get()}, arena, dfg}; @@ -3079,7 +3122,13 @@ void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, program->visit(&gp); } -void ConstraintGraphBuilder::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) +void ConstraintGenerator::recordInferredBinding(AstLocal* local, TypeId ty) +{ + if (InferredBinding* ib = inferredBindings.find(local)) + ib->types.insert(ty); +} + +void ConstraintGenerator::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) { for (const auto& [symbol, p] : inferredBindings) { @@ -3094,7 +3143,7 @@ void ConstraintGraphBuilder::fillInInferredBindings(const ScopePtr& globalScope, } } -std::vector> ConstraintGraphBuilder::getExpectedCallTypesForFunctionOverloads(const TypeId fnType) +std::vector> ConstraintGenerator::getExpectedCallTypesForFunctionOverloads(const TypeId fnType) { std::vector funTys; if (auto it = get(follow(fnType))) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 3b478494..fa0f767b 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -2,13 +2,11 @@ #include "Luau/Anyification.h" #include "Luau/ApplyTypeFunction.h" -#include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/ConstraintSolver.h" #include "Luau/DcrLogger.h" #include "Luau/Instantiation.h" #include "Luau/Location.h" -#include "Luau/Metamethods.h" #include "Luau/ModuleResolver.h" #include "Luau/Quantify.h" #include "Luau/Simplify.h" @@ -17,12 +15,12 @@ #include "Luau/Type.h" #include "Luau/TypeFamily.h" #include "Luau/TypeUtils.h" -#include "Luau/Unifier.h" #include "Luau/Unifier2.h" #include "Luau/VisitType.h" +#include +#include LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); -LUAU_FASTFLAG(LuauFloorDivision); namespace Luau { @@ -995,6 +993,27 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull std::optional { auto it = begin(t); auto endIt = end(t); @@ -1020,10 +1039,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull callMm = findMetatableEntry(builtinTypes, errors, fn, "__call", constraint->location)) { - auto [head, tail] = flatten(c.argsPack); - head.insert(head.begin(), fn); + argsHead.insert(argsHead.begin(), fn); - argsPack = arena->addTypePack(TypePack{std::move(head), tail}); + if (argsTail && isBlocked(*argsTail)) + return block(*argsTail, constraint); + + argsPack = arena->addTypePack(TypePack{std::move(argsHead), argsTail}); fn = follow(*callMm); asMutable(c.result)->ty.emplace(constraint->scope); } @@ -1103,6 +1124,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNulllocation, addition)); + } + if (occursCheckPassed && c.callSite) (*c.astOverloadResolvedTypes)[c.callSite] = inferredTy; @@ -1132,23 +1159,14 @@ bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNull constraint) { - TypeId subjectType = follow(c.subjectType); + const TypeId subjectType = follow(c.subjectType); + const TypeId resultType = follow(c.resultType); - LUAU_ASSERT(get(c.resultType)); + LUAU_ASSERT(get(resultType)); if (isBlocked(subjectType) || get(subjectType)) return block(subjectType, constraint); - if (get(subjectType)) - { - TableType& ttv = asMutable(subjectType)->ty.emplace(TableState::Free, TypeLevel{}, constraint->scope); - ttv.props[c.prop] = Property{c.resultType}; - TypeId res = freshType(arena, builtinTypes, constraint->scope); - asMutable(c.resultType)->ty.emplace(res); - unblock(c.resultType, constraint->location); - return true; - } - auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.suppressSimplification); if (!blocked.empty()) { @@ -1158,8 +1176,8 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNullanyType), c.subjectType, constraint->location); - unblock(c.resultType, constraint->location); + bindBlockedType(resultType, result.value_or(builtinTypes->anyType), c.subjectType, constraint->location); + unblock(resultType, constraint->location); return true; } @@ -1245,9 +1263,6 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull(subjectType)) - return block(subjectType, constraint); - std::optional existingPropType = subjectType; for (const std::string& segment : c.path) { @@ -1283,25 +1298,13 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull(subjectType)) { - TypeId ty = freshType(arena, builtinTypes, constraint->scope); - - // Mint a chain of free tables per c.path - for (auto it = rbegin(c.path); it != rend(c.path); ++it) - { - TableType t{TableState::Free, TypeLevel{}, constraint->scope}; - t.props[*it] = {ty}; - - ty = arena->addType(std::move(t)); - } - - LUAU_ASSERT(ty); - - bind(subjectType, ty); - if (follow(c.resultType) != follow(ty)) - bind(c.resultType, ty); - unblock(subjectType, constraint->location); - unblock(c.resultType, constraint->location); - return true; + /* + * This should never occur because lookupTableProp() will add bounds to + * any free types it encounters. There will always be an + * existingPropType if the subject is free. + */ + LUAU_ASSERT(false); + return false; } else if (auto ttv = getMutable(subjectType)) { @@ -1310,7 +1313,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNullpersistent); ttv->props[c.path[0]] = Property{c.propType}; - bind(c.resultType, c.subjectType); + bind(c.resultType, subjectType); unblock(c.resultType, constraint->location); return true; } @@ -1319,26 +1322,12 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNullpersistent); updateTheTableType(builtinTypes, NotNull{arena}, subjectType, c.path, c.propType); - bind(c.resultType, c.subjectType); - unblock(subjectType, constraint->location); - unblock(c.resultType, constraint->location); - return true; - } - else - { - bind(c.resultType, subjectType); - unblock(c.resultType, constraint->location); - return true; } } - else - { - // Other kinds of types don't change shape when properties are assigned - // to them. (if they allow properties at all!) - bind(c.resultType, subjectType); - unblock(c.resultType, constraint->location); - return true; - } + + bind(c.resultType, subjectType); + unblock(c.resultType, constraint->location); + return true; } bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull constraint, bool force) @@ -1434,32 +1423,57 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull= srcPack.head.size()) break; - TypeId srcTy = follow(srcPack.head[i]); - if (isBlocked(*destIter)) + TypeId srcTy = follow(srcPack.head[i]); + TypeId resultTy = follow(*resultIter); + + if (resultTy) { - if (follow(srcTy) == *destIter) + if (auto lt = getMutable(resultTy); c.resultIsLValue && lt) { - // Cyclic type dependency. (????) - TypeId f = freshType(arena, builtinTypes, constraint->scope); - asMutable(*destIter)->ty.emplace(f); + lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, srcTy).result; + LUAU_ASSERT(lt->blockCount > 0); + --lt->blockCount; + + LUAU_ASSERT(0 <= lt->blockCount); + + if (0 == lt->blockCount) + asMutable(resultTy)->ty.emplace(lt->domain); + } + else if (get(resultTy)) + { + if (follow(srcTy) == resultTy) + { + // It is sometimes the case that we find that a blocked type + // is only blocked on itself. This doesn't actually + // constitute any meaningful constraint, so we replace it + // with a free type. + TypeId f = freshType(arena, builtinTypes, constraint->scope); + asMutable(resultTy)->ty.emplace(f); + } + else + asMutable(resultTy)->ty.emplace(srcTy); } else - asMutable(*destIter)->ty.emplace(srcTy); - unblock(*destIter, constraint->location); + { + LUAU_ASSERT(c.resultIsLValue); + unify(constraint->scope, constraint->location, resultTy, srcTy); + } + + unblock(resultTy, constraint->location); } else - unify(constraint->scope, constraint->location, *destIter, srcTy); + unify(constraint->scope, constraint->location, resultTy, srcTy); - ++destIter; + ++resultIter; ++i; } @@ -1467,15 +1481,25 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull(resultTy); c.resultIsLValue && lt) { - asMutable(*destIter)->ty.emplace(builtinTypes->nilType); - unblock(*destIter, constraint->location); + lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, builtinTypes->nilType).result; + LUAU_ASSERT(0 <= lt->blockCount); + --lt->blockCount; + + if (0 == lt->blockCount) + asMutable(resultTy)->ty.emplace(lt->domain); + } + else if (get(*resultIter) || get(*resultIter)) + { + asMutable(*resultIter)->ty.emplace(builtinTypes->nilType); + unblock(*resultIter, constraint->location); } - ++destIter; + ++resultIter; } return true; @@ -1490,7 +1514,7 @@ namespace */ struct FindRefineConstraintBlockers : TypeOnceVisitor { - std::unordered_set found; + DenseHashSet found{nullptr}; bool visit(TypeId ty, const BlockedType&) override { found.insert(ty); @@ -1855,6 +1879,7 @@ bool ConstraintSolver::tryDispatchIterableFunction( TypeId retIndex; if (isNil(firstIndexTy) || isOptional(firstIndexTy)) { + // FIXME freshType is suspect here firstIndex = arena->addType(UnionType{{freshType(arena, builtinTypes, constraint->scope), builtinTypes->nilType}}); retIndex = firstIndex; } @@ -1896,7 +1921,7 @@ bool ConstraintSolver::tryDispatchIterableFunction( modifiedNextRetHead.push_back(*it); TypePackId modifiedNextRetPack = arena->addTypePack(std::move(modifiedNextRetHead), it.tail()); - auto psc = pushConstraint(constraint->scope, constraint->location, PackSubtypeConstraint{c.variables, modifiedNextRetPack}); + auto psc = pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, modifiedNextRetPack}); inheritBlocks(constraint, psc); return true; @@ -1905,15 +1930,16 @@ bool ConstraintSolver::tryDispatchIterableFunction( std::pair, std::optional> ConstraintSolver::lookupTableProp( TypeId subjectType, const std::string& propName, bool suppressSimplification) { - std::unordered_set seen; + DenseHashSet seen{nullptr}; return lookupTableProp(subjectType, propName, suppressSimplification, seen); } std::pair, std::optional> ConstraintSolver::lookupTableProp( - TypeId subjectType, const std::string& propName, bool suppressSimplification, std::unordered_set& seen) + TypeId subjectType, const std::string& propName, bool suppressSimplification, DenseHashSet& seen) { - if (!seen.insert(subjectType).second) + if (seen.contains(subjectType)) return {}; + seen.insert(subjectType); subjectType = follow(subjectType); @@ -1994,14 +2020,23 @@ std::pair, std::optional> ConstraintSolver::lookupTa } else if (auto ft = get(subjectType)) { - Scope* scope = ft->scope; + const TypeId upperBound = follow(ft->upperBound); - TableType* tt = &asMutable(subjectType)->ty.emplace(); - tt->state = TableState::Free; - tt->scope = scope; + if (get(upperBound)) + return lookupTableProp(upperBound, propName, suppressSimplification, seen); + + // TODO: The upper bound could be an intersection that contains suitable tables or classes. + + NotNull scope{ft->scope}; + + const TypeId newUpperBound = arena->addType(TableType{TableState::Free, TypeLevel{}, scope}); + TableType* tt = getMutable(newUpperBound); + LUAU_ASSERT(tt); TypeId propType = freshType(arena, builtinTypes, scope); tt->props[propName] = Property{propType}; + unify(scope, Location{}, subjectType, newUpperBound); + return {{}, propType}; } else if (auto utv = get(subjectType)) @@ -2073,7 +2108,15 @@ bool ConstraintSolver::tryUnify(NotNull constraint, TID subTy, bool success = u2.unify(subTy, superTy); - if (!success) + if (success) + { + for (const auto& [expanded, additions] : u2.expandedFreeTypes) + { + for (TypeId addition : additions) + upperBoundContributors[expanded].push_back(std::make_pair(constraint->location, addition)); + } + } + else { // Unification only fails when doing so would fail the occurs check. // ie create a self-bound type or a cyclic type pack @@ -2285,7 +2328,12 @@ void ConstraintSolver::unblock(const std::vector& packs, Location lo bool ConstraintSolver::isBlocked(TypeId ty) { - return nullptr != get(follow(ty)) || nullptr != get(follow(ty)); + ty = follow(ty); + + if (auto lt = get(ty)) + return lt->blockCount > 0; + + return nullptr != get(ty) || nullptr != get(ty); } bool ConstraintSolver::isBlocked(TypePackId tp) @@ -2320,6 +2368,12 @@ ErrorVec ConstraintSolver::unify(NotNull scope, Location location, TypePa u.unify(subPack, superPack); + for (const auto& [expanded, additions] : u.expandedFreeTypes) + { + for (TypeId addition : additions) + upperBoundContributors[expanded].push_back(std::make_pair(location, addition)); + } + unblock(subPack, Location{}); unblock(superPack, Location{}); diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index e3933574..bdefd7f0 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -11,10 +11,13 @@ LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) +LUAU_FASTFLAG(LuauLoopControlFlowAnalysis) namespace Luau { +bool doesCallError(const AstExprCall* call); // TypeInfer.cpp + const RefinementKey* RefinementKeyArena::leaf(DefId def) { return allocator.allocate(RefinementKey{nullptr, def, std::nullopt}); @@ -34,7 +37,7 @@ DefId DataFlowGraph::getDef(const AstExpr* expr) const std::optional DataFlowGraph::getRValueDefForCompoundAssign(const AstExpr* expr) const { - auto def = compoundAssignBreadcrumbs.find(expr); + auto def = compoundAssignDefs.find(expr); return def ? std::optional(*def) : std::nullopt; } @@ -82,9 +85,9 @@ std::optional DfgScope::lookup(DefId def, const std::string& key) const { for (const DfgScope* current = this; current; current = current->parent) { - if (auto map = props.find(def)) + if (auto props = current->props.find(def)) { - if (auto it = map->find(key); it != map->end()) + if (auto it = props->find(key); it != props->end()) return NotNull{it->second}; } } @@ -92,6 +95,47 @@ std::optional DfgScope::lookup(DefId def, const std::string& key) const return std::nullopt; } +void DfgScope::inherit(const DfgScope* childScope) +{ + for (const auto& [k, a] : childScope->bindings) + { + if (lookup(k)) + bindings[k] = a; + } + + for (const auto& [k1, a1] : childScope->props) + { + for (const auto& [k2, a2] : a1) + props[k1][k2] = a2; + } +} + +bool DfgScope::canUpdateDefinition(Symbol symbol) const +{ + for (const DfgScope* current = this; current; current = current->parent) + { + if (current->bindings.find(symbol)) + return true; + else if (current->isLoopScope) + return false; + } + + return true; +} + +bool DfgScope::canUpdateDefinition(DefId def, const std::string& key) const +{ + for (const DfgScope* current = this; current; current = current->parent) + { + if (auto props = current->props.find(def)) + return true; + else if (current->isLoopScope) + return false; + } + + return true; +} + DataFlowGraph DataFlowGraphBuilder::build(AstStatBlock* block, NotNull handle) { LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution); @@ -110,24 +154,138 @@ DataFlowGraph DataFlowGraphBuilder::build(AstStatBlock* block, NotNullbindings, a->bindings, b->bindings); + joinProps(p->props, a->props, b->props); +} + +void DataFlowGraphBuilder::joinBindings(DfgScope::Bindings& p, const DfgScope::Bindings& a, const DfgScope::Bindings& b) +{ + for (const auto& [sym, def1] : a) + { + if (auto def2 = b.find(sym)) + p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + else if (auto def2 = p.find(sym)) + p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + } + + for (const auto& [sym, def1] : b) + { + if (auto def2 = p.find(sym)) + p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + } +} + +void DataFlowGraphBuilder::joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b) +{ + auto phinodify = [this](auto& p, const auto& a, const auto& b) mutable { + for (const auto& [k, defA] : a) + { + if (auto it = b.find(k); it != b.end()) + p[k] = defArena->phi(NotNull{it->second}, NotNull{defA}); + else if (auto it = p.find(k); it != p.end()) + p[k] = defArena->phi(NotNull{it->second}, NotNull{defA}); + else + p[k] = defA; + } + + for (const auto& [k, defB] : b) + { + if (auto it = a.find(k); it != a.end()) + continue; + else if (auto it = p.find(k); it != p.end()) + p[k] = defArena->phi(NotNull{it->second}, NotNull{defB}); + else + p[k] = defB; + } + }; + + for (const auto& [def, a1] : a) + { + p.try_insert(def, {}); + if (auto a2 = b.find(def)) + phinodify(p[def], a1, *a2); + else if (auto a2 = p.find(def)) + phinodify(p[def], a1, *a2); + } + + for (const auto& [def, a1] : b) + { + p.try_insert(def, {}); + if (a.find(def)) + continue; + else if (auto a2 = p.find(def)) + phinodify(p[def], a1, *a2); + } +} + +DefId DataFlowGraphBuilder::lookup(DfgScope* scope, Symbol symbol) +{ + if (auto found = scope->lookup(symbol)) + return *found; + else + { + DefId result = defArena->freshCell(); + if (symbol.local) + scope->bindings[symbol] = result; + else + moduleScope->bindings[symbol] = result; + return result; + } +} + +DefId DataFlowGraphBuilder::lookup(DfgScope* scope, DefId def, const std::string& key) +{ + if (auto found = scope->lookup(def, key)) + return *found; + else if (auto phi = get(def)) + { + std::vector defs; + for (DefId operand : phi->operands) + defs.push_back(lookup(scope, operand, key)); + + DefId result = defArena->phi(defs); + scope->props[def][key] = result; + return result; + } + else if (get(def)) + { + DefId result = defArena->freshCell(); + scope->props[def][key] = result; + return result; + } + else + handle->ice("Inexhaustive lookup cases in DataFlowGraphBuilder::lookup"); +} + +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBlock* b) { DfgScope* child = childScope(scope); - return visitBlockWithoutChildScope(child, b); + ControlFlow cf = visitBlockWithoutChildScope(child, b); + scope->inherit(child); + return cf; } -void DataFlowGraphBuilder::visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b) +ControlFlow DataFlowGraphBuilder::visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b) { - for (AstStat* s : b->body) - visit(scope, s); + std::optional firstControlFlow; + for (AstStat* stat : b->body) + { + ControlFlow cf = visit(scope, stat); + if (cf != ControlFlow::None && !firstControlFlow) + firstControlFlow = cf; + } + + return firstControlFlow.value_or(ControlFlow::None); } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStat* s) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStat* s) { if (auto b = s->as()) return visit(scope, b); @@ -173,56 +331,85 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStat* s) handle->ice("Unknown AstStat in DataFlowGraphBuilder::visit"); } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatIf* i) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatIf* i) { - // TODO: type states and control flow analysis visitExpr(scope, i->condition); - visit(scope, i->thenbody); + + DfgScope* thenScope = childScope(scope); + DfgScope* elseScope = childScope(scope); + + ControlFlow thencf = visit(thenScope, i->thenbody); + ControlFlow elsecf = ControlFlow::None; if (i->elsebody) - visit(scope, i->elsebody); + elsecf = visit(elseScope, i->elsebody); + + if (thencf != ControlFlow::None && elsecf == ControlFlow::None) + join(scope, scope, elseScope); + else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) + join(scope, thenScope, scope); + else if ((thencf | elsecf) == ControlFlow::None) + join(scope, thenScope, elseScope); + + if (FFlag::LuauLoopControlFlowAnalysis && 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 DataFlowGraphBuilder::visit(DfgScope* scope, AstStatWhile* w) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatWhile* w) { // TODO(controlflow): entry point has a back edge from exit point - DfgScope* whileScope = childScope(scope); + DfgScope* whileScope = childScope(scope, /*isLoopScope=*/true); visitExpr(whileScope, w->condition); visit(whileScope, w->body); + + scope->inherit(whileScope); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatRepeat* r) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatRepeat* r) { // TODO(controlflow): entry point has a back edge from exit point - DfgScope* repeatScope = childScope(scope); // TODO: loop scope. + DfgScope* repeatScope = childScope(scope, /*isLoopScope=*/true); visitBlockWithoutChildScope(repeatScope, r->body); visitExpr(repeatScope, r->condition); + + scope->inherit(repeatScope); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBreak* b) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBreak* b) { - // TODO: Control flow analysis - return; // ok + return ControlFlow::Breaks; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatContinue* c) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatContinue* c) { - // TODO: Control flow analysis - return; // ok + return ControlFlow::Continues; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatReturn* r) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatReturn* r) { - // TODO: Control flow analysis for (AstExpr* e : r->list) visitExpr(scope, e); + + return ControlFlow::Returns; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatExpr* e) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatExpr* e) { visitExpr(scope, e->expr); + if (auto call = e->expr->as(); call && doesCallError(call)) + return ControlFlow::Throws; + else + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l) { // We're gonna need a `visitExprList` and `visitVariadicExpr` (function calls and `...`) std::vector defs; @@ -243,11 +430,13 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l) graph.localDefs[local] = def; scope->bindings[local] = def; } + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f) { - DfgScope* forScope = childScope(scope); // TODO: loop scope. + DfgScope* forScope = childScope(scope, /*isLoopScope=*/true); visitExpr(scope, f->from); visitExpr(scope, f->to); @@ -263,11 +452,15 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f) // TODO(controlflow): entry point has a back edge from exit point visit(forScope, f->body); + + scope->inherit(forScope); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) { - DfgScope* forScope = childScope(scope); // TODO: loop scope. + DfgScope* forScope = childScope(scope, /*isLoopScope=*/true); for (AstLocal* local : f->vars) { @@ -285,9 +478,13 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) visitExpr(forScope, e); visit(forScope, f->body); + + scope->inherit(forScope); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a) { std::vector defs; defs.reserve(a->values.size); @@ -299,9 +496,11 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a) AstExpr* v = a->vars.data[i]; visitLValue(scope, v, i < defs.size() ? defs[i] : defArena->freshCell()); } + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) { // TODO: This needs revisiting because this is incorrect. The `c->var` part is both being read and written to, // but the `c->var` only has one pointer address, so we need to come up with a way to store both. @@ -312,9 +511,11 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) // We can't just visit `c->var` as a rvalue and then separately traverse `c->var` as an lvalue, since that's O(n^2). DefId def = visitExpr(scope, c->value).def; visitLValue(scope, c->var, def, /* isCompoundAssignment */ true); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) { // In the old solver, we assumed that the name of the function is always a function in the body // but this isn't true, e.g. the following example will print `5`, not a function address. @@ -329,34 +530,42 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) DefId prototype = defArena->freshCell(); visitLValue(scope, f->name, prototype); visitExpr(scope, f->func); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l) { DefId def = defArena->freshCell(); graph.localDefs[l->name] = def; scope->bindings[l->name] = def; visitExpr(scope, l->func); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t) { DfgScope* unreachable = childScope(scope); visitGenerics(unreachable, t->generics); visitGenericPacks(unreachable, t->genericPacks); visitType(unreachable, t->type); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareGlobal* d) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareGlobal* d) { DefId def = defArena->freshCell(); graph.declaredDefs[d] = def; scope->bindings[d->name] = def; visitType(scope, d->type); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareFunction* d) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareFunction* d) { DefId def = defArena->freshCell(); graph.declaredDefs[d] = def; @@ -367,9 +576,11 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareFunction* d) visitGenericPacks(unreachable, d->genericPacks); visitTypeList(unreachable, d->params); visitTypeList(unreachable, d->retTypes); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareClass* d) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareClass* d) { // This declaration does not "introduce" any bindings in value namespace, // so there's no symbolic value to begin with. We'll traverse the properties @@ -377,19 +588,30 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareClass* d) DfgScope* unreachable = childScope(scope); for (AstDeclaredClassProp prop : d->props) visitType(unreachable, prop.ty); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatError* error) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatError* error) { DfgScope* unreachable = childScope(scope); for (AstStat* s : error->statements) visit(unreachable, s); for (AstExpr* e : error->expressions) visitExpr(unreachable, e); + + return ControlFlow::None; } DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExpr* e) { + // Some subexpressions could be visited two times. If we've already seen it, just extract it. + if (auto def = graph.astDefs.find(e)) + { + auto key = graph.astRefinementKeys.find(e); + return {NotNull{*def}, key ? *key : nullptr}; + } + auto go = [&]() -> DataFlowResult { if (auto g = e->as()) return visitExpr(scope, g); @@ -447,6 +669,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGroup* gr DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) { + // DfgScope::lookup is intentional here: we want to be able to ice. if (auto def = scope->lookup(l->local)) { const RefinementKey* key = keyArena->leaf(*def); @@ -458,11 +681,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g) { - if (auto def = scope->lookup(g->name)) - return {*def, keyArena->leaf(*def)}; - - DefId def = defArena->freshCell(); - moduleScope->bindings[g->name] = def; + DefId def = lookup(scope, g->name); return {def, keyArena->leaf(def)}; } @@ -481,11 +700,9 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexName auto [parentDef, parentKey] = visitExpr(scope, i->expr); std::string index = i->index.value; - auto& propDef = moduleScope->props[parentDef][index]; - if (!propDef) - propDef = defArena->freshCell(); - return {NotNull{propDef}, keyArena->node(parentKey, NotNull{propDef}, index)}; + DefId def = lookup(scope, parentDef, index); + return {def, keyArena->node(parentKey, def, index)}; } DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i) @@ -496,11 +713,9 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr if (auto string = i->index->as()) { std::string index{string->value.data, string->value.size}; - auto& propDef = moduleScope->props[parentDef][index]; - if (!propDef) - propDef = defArena->freshCell(); - return {NotNull{propDef}, keyArena->node(parentKey, NotNull{propDef}, index)}; + DefId def = lookup(scope, parentDef, index); + return {def, keyArena->node(parentKey, def, index)}; } return {defArena->freshCell(/* subscripted= */true), nullptr}; @@ -628,41 +843,56 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomi void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment) { - // We need to keep the previous breadcrumb around for a compound assignment. + // We need to keep the previous def around for a compound assignment. if (isCompoundAssignment) { if (auto def = scope->lookup(l->local)) - graph.compoundAssignBreadcrumbs[l] = *def; + graph.compoundAssignDefs[l] = *def; } // In order to avoid alias tracking, we need to clip the reference to the parent def. - DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); - graph.astDefs[l] = updated; - scope->bindings[l->local] = updated; + if (scope->canUpdateDefinition(l->local)) + { + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[l] = updated; + scope->bindings[l->local] = updated; + } + else + visitExpr(scope, static_cast(l)); } void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment) { - // We need to keep the previous breadcrumb around for a compound assignment. + // We need to keep the previous def around for a compound assignment. if (isCompoundAssignment) { - if (auto def = scope->lookup(g->name)) - graph.compoundAssignBreadcrumbs[g] = *def; + DefId def = lookup(scope, g->name); + graph.compoundAssignDefs[g] = def; } // In order to avoid alias tracking, we need to clip the reference to the parent def. - DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); - graph.astDefs[g] = updated; - scope->bindings[g->name] = updated; + if (scope->canUpdateDefinition(g->name)) + { + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[g] = updated; + scope->bindings[g->name] = updated; + } + else + visitExpr(scope, static_cast(g)); } void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef) { DefId parentDef = visitExpr(scope, i->expr).def; - DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); - graph.astDefs[i] = updated; - scope->props[parentDef][i->index.value] = updated; + if (scope->canUpdateDefinition(parentDef, i->index.value)) + { + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[i] = updated; + scope->props[parentDef][i->index.value] = updated; + } + else + visitExpr(scope, static_cast(i)); } void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef) @@ -672,9 +902,14 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, Def if (auto string = i->index->as()) { - DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); - graph.astDefs[i] = updated; - scope->props[parentDef][string->value.data] = updated; + if (scope->canUpdateDefinition(parentDef, string->value.data)) + { + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[i] = updated; + scope->props[parentDef][string->value.data] = updated; + } + else + visitExpr(scope, static_cast(i)); } graph.astDefs[i] = defArena->freshCell(); diff --git a/Analysis/src/Def.cpp b/Analysis/src/Def.cpp index d34b5cdc..2b3bbeac 100644 --- a/Analysis/src/Def.cpp +++ b/Analysis/src/Def.cpp @@ -1,7 +1,11 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Def.h" + #include "Luau/Common.h" +#include +#include + namespace Luau { @@ -9,9 +13,10 @@ bool containsSubscriptedDefinition(DefId def) { if (auto cell = get(def)) return cell->subscripted; - - LUAU_ASSERT(!"Phi nodes not implemented yet"); - return false; + else if (auto phi = get(def)) + return std::any_of(phi->operands.begin(), phi->operands.end(), containsSubscriptedDefinition); + else + return false; } DefId DefArena::freshCell(bool subscripted) @@ -19,4 +24,35 @@ DefId DefArena::freshCell(bool subscripted) return NotNull{allocator.allocate(Def{Cell{subscripted}})}; } +static void collectOperands(DefId def, std::vector& operands) +{ + if (std::find(operands.begin(), operands.end(), def) != operands.end()) + return; + else if (get(def)) + operands.push_back(def); + else if (auto phi = get(def)) + { + for (const Def* operand : phi->operands) + collectOperands(NotNull{operand}, operands); + } +} + +DefId DefArena::phi(DefId a, DefId b) +{ + return phi({a, b}); +} + +DefId DefArena::phi(const std::vector& defs) +{ + std::vector operands; + for (DefId operand : defs) + collectOperands(operand, operands); + + // There's no need to allocate a Phi node for a singleton set. + if (operands.size() == 1) + return operands[0]; + else + return NotNull{allocator.allocate(Def{Phi{std::move(operands)}})}; +} + } // namespace Luau diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index 632f8e5c..874f8627 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -2,11 +2,12 @@ #include "Luau/BuiltinDefinitions.h" LUAU_FASTFLAGVARIABLE(LuauBufferDefinitions, false) +LUAU_FASTFLAGVARIABLE(LuauBufferTypeck, false) namespace Luau { -static const std::string kBuiltinDefinitionBufferSrc = R"BUILTIN_SRC( +static const std::string kBuiltinDefinitionBufferSrc_DEPRECATED = R"BUILTIN_SRC( -- TODO: this will be replaced with a built-in primitive type declare class buffer end @@ -40,6 +41,36 @@ declare buffer: { )BUILTIN_SRC"; +static const std::string kBuiltinDefinitionBufferSrc = R"BUILTIN_SRC( + +declare buffer: { + create: (size: number) -> buffer, + fromstring: (str: string) -> buffer, + tostring: (b: buffer) -> string, + len: (b: buffer) -> number, + copy: (target: buffer, targetOffset: number, source: buffer, sourceOffset: number?, count: number?) -> (), + fill: (b: buffer, offset: number, value: number, count: number?) -> (), + readi8: (b: buffer, offset: number) -> number, + readu8: (b: buffer, offset: number) -> number, + readi16: (b: buffer, offset: number) -> number, + readu16: (b: buffer, offset: number) -> number, + readi32: (b: buffer, offset: number) -> number, + readu32: (b: buffer, offset: number) -> number, + readf32: (b: buffer, offset: number) -> number, + readf64: (b: buffer, offset: number) -> number, + writei8: (b: buffer, offset: number, value: number) -> (), + writeu8: (b: buffer, offset: number, value: number) -> (), + writei16: (b: buffer, offset: number, value: number) -> (), + writeu16: (b: buffer, offset: number, value: number) -> (), + writei32: (b: buffer, offset: number, value: number) -> (), + writeu32: (b: buffer, offset: number, value: number) -> (), + writef32: (b: buffer, offset: number, value: number) -> (), + writef64: (b: buffer, offset: number, value: number) -> (), + readstring: (b: buffer, offset: number, count: number) -> string, + writestring: (b: buffer, offset: number, value: string, count: number?) -> (), +} + +)BUILTIN_SRC"; static const std::string kBuiltinDefinitionLuaSrc = R"BUILTIN_SRC( declare bit32: { @@ -236,8 +267,10 @@ std::string getBuiltinDefinitionSource() { std::string result = kBuiltinDefinitionLuaSrc; - if (FFlag::LuauBufferDefinitions) + if (FFlag::LuauBufferTypeck) result = kBuiltinDefinitionBufferSrc + result; + else if (FFlag::LuauBufferDefinitions) + result = kBuiltinDefinitionBufferSrc_DEPRECATED + result; return result; } diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index db6a240a..5ec2d52b 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -490,7 +490,12 @@ struct ErrorConverter std::string operator()(const TypePackMismatch& e) const { - return "Type pack '" + toString(e.givenTp) + "' could not be converted into '" + toString(e.wantedTp) + "'"; + std::string ss = "Type pack '" + toString(e.givenTp) + "' could not be converted into '" + toString(e.wantedTp) + "'"; + + if (!e.reason.empty()) + ss += "; " + e.reason; + + return ss; } std::string operator()(const DynamicPropertyLookupOnClassesUnsafe& e) const @@ -528,6 +533,12 @@ struct ErrorConverter return "Function '" + e.checkedFunctionName + "' expects '" + toString(e.expected) + "' at argument #" + std::to_string(e.argumentIndex) + ", but got '" + Luau::toString(e.passed) + "'"; } + + std::string operator()(const NonStrictFunctionDefinitionError& e) const + { + return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' in function '" + e.functionName + + "' is used in a way that will run time error"; + } }; struct InvalidNameChecker @@ -856,6 +867,11 @@ bool CheckedFunctionCallError::operator==(const CheckedFunctionCallError& rhs) c argumentIndex == rhs.argumentIndex; } +bool NonStrictFunctionDefinitionError::operator==(const NonStrictFunctionDefinitionError& rhs) const +{ + return functionName == rhs.functionName && argument == rhs.argument && argumentType == rhs.argumentType; +} + std::string toString(const TypeError& error) { return toString(error, TypeErrorToStringOptions{}); @@ -1027,6 +1043,10 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState) e.expected = clone(e.expected); e.passed = clone(e.passed); } + else if constexpr (std::is_same_v) + { + e.argumentType = clone(e.argumentType); + } else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 6cbc19fa..125f2457 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -5,7 +5,7 @@ #include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/Config.h" -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/ConstraintSolver.h" #include "Luau/DataFlowGraph.h" #include "Luau/DcrLogger.h" @@ -38,6 +38,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false) LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false) LUAU_FASTFLAGVARIABLE(LuauTypecheckLimitControls, false) LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false) +LUAU_FASTFLAGVARIABLE(LuauDefinitionFileSetModuleName, false) namespace Luau { @@ -165,6 +166,11 @@ LoadDefinitionFileResult Frontend::loadDefinitionFile(GlobalTypes& globals, Scop LUAU_TIMETRACE_SCOPE("loadDefinitionFile", "Frontend"); Luau::SourceModule sourceModule; + if (FFlag::LuauDefinitionFileSetModuleName) + { + sourceModule.name = packageName; + sourceModule.humanReadableName = packageName; + } Luau::ParseResult parseResult = parseSourceForModule(source, sourceModule, captureComments); if (parseResult.errors.size() > 0) return LoadDefinitionFileResult{false, parseResult, sourceModule, nullptr}; @@ -251,7 +257,7 @@ namespace static ErrorVec accumulateErrors( const std::unordered_map>& sourceNodes, ModuleResolver& moduleResolver, const ModuleName& name) { - std::unordered_set seen; + DenseHashSet seen{{}}; std::vector queue{name}; ErrorVec result; @@ -261,7 +267,7 @@ static ErrorVec accumulateErrors( ModuleName next = std::move(queue.back()); queue.pop_back(); - if (seen.count(next)) + if (seen.contains(next)) continue; seen.insert(next); @@ -442,7 +448,7 @@ CheckResult Frontend::check(const ModuleName& name, std::optional buildQueue; bool cycleDetected = parseGraph(buildQueue, name, frontendOptions.forAutocomplete); - std::unordered_set seen; + DenseHashSet seen{{}}; std::vector buildQueueItems; addBuildQueueItems(buildQueueItems, buildQueue, cycleDetected, seen, frontendOptions); LUAU_ASSERT(!buildQueueItems.empty()); @@ -495,12 +501,12 @@ std::vector Frontend::checkQueuedModules(std::optional currModuleQueue; std::swap(currModuleQueue, moduleQueue); - std::unordered_set seen; + DenseHashSet seen{{}}; std::vector buildQueueItems; for (const ModuleName& name : currModuleQueue) { - if (seen.count(name)) + if (seen.contains(name)) continue; if (!isDirty(name, frontendOptions.forAutocomplete)) @@ -511,7 +517,7 @@ std::vector Frontend::checkQueuedModules(std::optional queue; bool cycleDetected = parseGraph(queue, name, frontendOptions.forAutocomplete, [&seen](const ModuleName& name) { - return seen.count(name); + return seen.contains(name); }); addBuildQueueItems(buildQueueItems, queue, cycleDetected, seen, frontendOptions); @@ -836,11 +842,11 @@ bool Frontend::parseGraph( } void Frontend::addBuildQueueItems(std::vector& items, std::vector& buildQueue, bool cycleDetected, - std::unordered_set& seen, const FrontendOptions& frontendOptions) + DenseHashSet& seen, const FrontendOptions& frontendOptions) { for (const ModuleName& moduleName : buildQueue) { - if (seen.count(moduleName)) + if (seen.contains(moduleName)) continue; seen.insert(moduleName); @@ -1048,6 +1054,7 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) module->astResolvedTypes.clear(); module->astResolvedTypePacks.clear(); module->astScopes.clear(); + module->upperBoundContributors.clear(); if (!FFlag::DebugLuauDeferredConstraintResolution) module->scopes.clear(); @@ -1255,13 +1262,13 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vectorinternalTypes, builtinTypes, NotNull{&unifierState}}; - ConstraintGraphBuilder cgb{result, NotNull{&normalizer}, moduleResolver, builtinTypes, iceHandler, parentScope, std::move(prepareModuleScope), + ConstraintGenerator cg{result, NotNull{&normalizer}, moduleResolver, builtinTypes, iceHandler, parentScope, std::move(prepareModuleScope), logger.get(), NotNull{&dfg}, requireCycles}; - cgb.visitModuleRoot(sourceModule.root); - result->errors = std::move(cgb.errors); + cg.visitModuleRoot(sourceModule.root); + result->errors = std::move(cg.errors); - ConstraintSolver cs{NotNull{&normalizer}, NotNull(cgb.rootScope), borrowConstraints(cgb.constraints), result->humanReadableName, moduleResolver, + ConstraintSolver cs{NotNull{&normalizer}, NotNull(cg.rootScope), borrowConstraints(cg.constraints), result->humanReadableName, moduleResolver, requireCycles, logger.get(), limits}; if (options.randomizeConstraintResolutionSeed) @@ -1283,8 +1290,9 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vectorerrors.emplace_back(std::move(e)); - result->scopes = std::move(cgb.scopes); + result->scopes = std::move(cg.scopes); result->type = sourceModule.type; + result->upperBoundContributors = std::move(cs.upperBoundContributors); result->clonePublicInterface(builtinTypes, *iceHandler); diff --git a/Analysis/src/GlobalTypes.cpp b/Analysis/src/GlobalTypes.cpp index 654cfa5d..1c96ad70 100644 --- a/Analysis/src/GlobalTypes.cpp +++ b/Analysis/src/GlobalTypes.cpp @@ -3,6 +3,7 @@ #include "Luau/GlobalTypes.h" LUAU_FASTFLAG(LuauInitializeStringMetatableInGlobalTypes) +LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau { @@ -18,6 +19,8 @@ GlobalTypes::GlobalTypes(NotNull builtinTypes) globalScope->addBuiltinTypeBinding("string", TypeFun{{}, builtinTypes->stringType}); globalScope->addBuiltinTypeBinding("boolean", TypeFun{{}, builtinTypes->booleanType}); globalScope->addBuiltinTypeBinding("thread", TypeFun{{}, builtinTypes->threadType}); + if (FFlag::LuauBufferTypeck) + globalScope->addBuiltinTypeBinding("buffer", TypeFun{{}, builtinTypes->bufferType}); globalScope->addBuiltinTypeBinding("unknown", TypeFun{{}, builtinTypes->unknownType}); globalScope->addBuiltinTypeBinding("never", TypeFun{{}, builtinTypes->neverType}); diff --git a/Analysis/src/Instantiation.cpp b/Analysis/src/Instantiation.cpp index e74ece06..235786a8 100644 --- a/Analysis/src/Instantiation.cpp +++ b/Analysis/src/Instantiation.cpp @@ -7,6 +7,8 @@ #include "Luau/TypeArena.h" #include "Luau/TypeCheckLimits.h" +#include + LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) namespace Luau diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 7a58a244..85a03b48 100644 --- a/Analysis/src/IostreamHelpers.cpp +++ b/Analysis/src/IostreamHelpers.cpp @@ -204,6 +204,9 @@ static void errorToString(std::ostream& stream, const T& err) else if constexpr (std::is_same_v) stream << "CheckedFunctionCallError { expected = '" << toString(err.expected) << "', passed = '" << toString(err.passed) << "', checkedFunctionName = " << err.checkedFunctionName << ", argumentIndex = " << std::to_string(err.argumentIndex) << " }"; + else if constexpr (std::is_same_v) + stream << "NonStrictFunctionDefinitionError { functionName = '" + err.functionName + "', argument = '" + err.argument + + "', argumentType = '" + toString(err.argumentType) + "' }"; else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index e957eee7..ea35a6a1 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -14,8 +14,7 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) -LUAU_FASTFLAGVARIABLE(LuauLintDeprecatedFenv, false) -LUAU_FASTFLAGVARIABLE(LuauLintTableIndexer, false) +LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau { @@ -1108,7 +1107,7 @@ private: TypeKind getTypeKind(const std::string& name) { if (name == "nil" || name == "boolean" || name == "userdata" || name == "number" || name == "string" || name == "table" || - name == "function" || name == "thread") + name == "function" || name == "thread" || (FFlag::LuauBufferTypeck && name == "buffer")) return Kind_Primitive; if (name == "vector") @@ -2093,7 +2092,7 @@ private: // getfenv/setfenv are deprecated, however they are still used in some test frameworks and don't have a great general replacement // for now we warn about the deprecation only when they are used with a numeric first argument; this produces fewer warnings and makes use // of getfenv/setfenv a little more localized - if (FFlag::LuauLintDeprecatedFenv && !node->self && node->args.size >= 1) + if (!node->self && node->args.size >= 1) { if (AstExprGlobal* fenv = node->func->as(); fenv && (fenv->name == "getfenv" || fenv->name == "setfenv")) { @@ -2185,7 +2184,7 @@ private: bool visit(AstExprUnary* node) override { - if (FFlag::LuauLintTableIndexer && node->op == AstExprUnary::Len) + if (node->op == AstExprUnary::Len) checkIndexer(node, node->expr, "#"); return true; @@ -2195,7 +2194,7 @@ private: { if (AstExprGlobal* func = node->func->as()) { - if (FFlag::LuauLintTableIndexer && func->name == "ipairs" && node->args.size == 1) + if (func->name == "ipairs" && node->args.size == 1) checkIndexer(node, node->args.data[0], "ipairs"); } else if (AstExprIndexName* func = node->func->as()) @@ -2209,8 +2208,6 @@ private: void checkIndexer(AstExpr* node, AstExpr* expr, const char* op) { - LUAU_ASSERT(FFlag::LuauLintTableIndexer); - std::optional ty = context->getType(expr); if (!ty) return; @@ -2220,7 +2217,8 @@ private: return; if (!tty->indexer && !tty->props.empty() && tty->state != TableState::Generic) - emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table without an array part is likely a bug", op); + emitWarning( + *context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table without an array part is likely a bug", op); else if (tty->indexer && isString(tty->indexer->indexType)) // note: to avoid complexity of subtype tests we just check if the key is a string emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table with string keys is likely a bug", op); } @@ -2653,13 +2651,17 @@ private: case ConstantNumberParseResult::Ok: case ConstantNumberParseResult::Malformed: break; + case ConstantNumberParseResult::Imprecise: + emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, + "Number literal exceeded available precision and was truncated to closest representable number"); + break; case ConstantNumberParseResult::BinOverflow: emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, - "Binary number literal exceeded available precision and has been truncated to 2^64"); + "Binary number literal exceeded available precision and was truncated to 2^64"); break; case ConstantNumberParseResult::HexOverflow: emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, - "Hexadecimal number literal exceeded available precision and has been truncated to 2^64"); + "Hexadecimal number literal exceeded available precision and was truncated to 2^64"); break; } diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index 580f59f3..d50719a9 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -3,7 +3,7 @@ #include "Luau/Clone.h" #include "Luau/Common.h" -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/Normalize.h" #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 595794a0..2372a9a7 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -3,7 +3,9 @@ #include "Luau/Ast.h" #include "Luau/Common.h" +#include "Luau/Simplify.h" #include "Luau/Type.h" +#include "Luau/Simplify.h" #include "Luau/Subtyping.h" #include "Luau/Normalize.h" #include "Luau/Error.h" @@ -12,6 +14,7 @@ #include "Luau/Def.h" #include +#include namespace Luau { @@ -64,24 +67,60 @@ struct NonStrictContext NonStrictContext(NonStrictContext&&) = default; NonStrictContext& operator=(NonStrictContext&&) = default; - void unionContexts(const NonStrictContext& other) + static NonStrictContext disjunction( + NotNull builtinTypes, NotNull arena, const NonStrictContext& left, const NonStrictContext& right) { - // TODO: unimplemented + // disjunction implements union over the domain of keys + // if the default value for a defId not in the map is `never` + // then never | T is T + NonStrictContext disj{}; + + for (auto [def, leftTy] : left.context) + { + if (std::optional rightTy = right.find(def)) + disj.context[def] = simplifyUnion(builtinTypes, arena, leftTy, *rightTy).result; + else + disj.context[def] = leftTy; + } + + for (auto [def, rightTy] : right.context) + { + if (!left.find(def).has_value()) + disj.context[def] = rightTy; + } + + return disj; } - void intersectContexts(const NonStrictContext& other) + static NonStrictContext conjunction( + NotNull builtins, NotNull arena, const NonStrictContext& left, const NonStrictContext& right) { - // TODO: unimplemented + NonStrictContext conj{}; + + for (auto [def, leftTy] : left.context) + { + if (std::optional rightTy = right.find(def)) + conj.context[def] = simplifyIntersection(builtins, arena, leftTy, *rightTy).result; + } + + return conj; } - void removeFromContext(const std::vector& defs) + // Returns true if the removal was successful + bool remove(const DefId& def) { - // TODO: unimplemented + return context.erase(def.get()) == 1; } std::optional find(const DefId& def) const { const Def* d = def.get(); + return find(d); + } + +private: + std::optional find(const Def* d) const + { auto it = context.find(d); if (it != context.end()) return {it->second}; @@ -101,6 +140,7 @@ struct NonStrictTypeChecker NotNull dfg; DenseHashSet noTypeFamilyErrors{nullptr}; std::vector> stack; + DenseHashMap cachedNegations{nullptr}; const NotNull limits; @@ -180,153 +220,282 @@ struct NonStrictTypeChecker return builtinTypes->anyType; } - - void visit(AstStat* stat) - { - NonStrictContext fresh{}; - visit(stat, fresh); - } - - void visit(AstStat* stat, NonStrictContext& context) + NonStrictContext visit(AstStat* stat) { auto pusher = pushStack(stat); if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else - LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown node type"); + { + LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown statement type"); + ice->ice("NonStrictTypeChecker encountered an unknown statement type"); + } } - void visit(AstStatBlock* block, NonStrictContext& context) + NonStrictContext visit(AstStatBlock* block) { auto StackPusher = pushStack(block); - for (AstStat* statement : block->body) - visit(statement, context); + NonStrictContext ctx; + + + for (auto it = block->body.rbegin(); it != block->body.rend(); it++) + { + AstStat* stat = *it; + if (AstStatLocal* local = stat->as()) + { + // Iterating in reverse order + // local x ; B generates the context of B without x + visit(local); + for (auto local : local->vars) + ctx.remove(dfg->getDef(local)); + } + else + ctx = NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, visit(stat), ctx); + } + return ctx; } - void visit(AstStatIf* ifStatement, NonStrictContext& context) {} - void visit(AstStatWhile* whileStatement, NonStrictContext& context) {} - void visit(AstStatRepeat* repeatStatement, NonStrictContext& context) {} - void visit(AstStatBreak* breakStatement, NonStrictContext& context) {} - void visit(AstStatContinue* continueStatement, NonStrictContext& context) {} - void visit(AstStatReturn* returnStatement, NonStrictContext& context) {} - void visit(AstStatExpr* expr, NonStrictContext& context) + NonStrictContext visit(AstStatIf* ifStatement) { - visit(expr->expr, context); + NonStrictContext condB = visit(ifStatement->condition); + NonStrictContext branchContext; + // If there is no else branch, don't bother generating warnings for the then branch - we can't prove there is an error + if (ifStatement->elsebody) + { + NonStrictContext thenBody = visit(ifStatement->thenbody); + NonStrictContext elseBody = visit(ifStatement->elsebody); + branchContext = NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenBody, elseBody); + } + return NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, condB, branchContext); } - void visit(AstStatLocal* local, NonStrictContext& context) {} - void visit(AstStatFor* forStatement, NonStrictContext& context) {} - void visit(AstStatForIn* forInStatement, NonStrictContext& context) {} - void visit(AstStatAssign* assign, NonStrictContext& context) {} - void visit(AstStatCompoundAssign* compoundAssign, NonStrictContext& context) {} - void visit(AstStatFunction* statFn, NonStrictContext& context) {} - void visit(AstStatLocalFunction* localFn, NonStrictContext& context) {} - void visit(AstStatTypeAlias* typeAlias, NonStrictContext& context) {} - void visit(AstStatDeclareFunction* declFn, NonStrictContext& context) {} - void visit(AstStatDeclareGlobal* declGlobal, NonStrictContext& context) {} - void visit(AstStatDeclareClass* declClass, NonStrictContext& context) {} - void visit(AstStatError* error, NonStrictContext& context) {} - void visit(AstExpr* expr, NonStrictContext& context) + NonStrictContext visit(AstStatWhile* whileStatement) + { + return {}; + } + + NonStrictContext visit(AstStatRepeat* repeatStatement) + { + return {}; + } + + NonStrictContext visit(AstStatBreak* breakStatement) + { + return {}; + } + + NonStrictContext visit(AstStatContinue* continueStatement) + { + return {}; + } + + NonStrictContext visit(AstStatReturn* returnStatement) + { + return {}; + } + + NonStrictContext visit(AstStatExpr* expr) + { + return visit(expr->expr); + } + + NonStrictContext visit(AstStatLocal* local) + { + for (AstExpr* rhs : local->values) + visit(rhs); + return {}; + } + + NonStrictContext visit(AstStatFor* forStatement) + { + return {}; + } + + NonStrictContext visit(AstStatForIn* forInStatement) + { + return {}; + } + + NonStrictContext visit(AstStatAssign* assign) + { + return {}; + } + + NonStrictContext visit(AstStatCompoundAssign* compoundAssign) + { + return {}; + } + + NonStrictContext visit(AstStatFunction* statFn) + { + return visit(statFn->func); + } + + NonStrictContext visit(AstStatLocalFunction* localFn) + { + return visit(localFn->func); + } + + NonStrictContext visit(AstStatTypeAlias* typeAlias) + { + return {}; + } + + NonStrictContext visit(AstStatDeclareFunction* declFn) + { + return {}; + } + + NonStrictContext visit(AstStatDeclareGlobal* declGlobal) + { + return {}; + } + + NonStrictContext visit(AstStatDeclareClass* declClass) + { + return {}; + } + + NonStrictContext visit(AstStatError* error) + { + return {}; + } + + NonStrictContext visit(AstExpr* expr) { auto pusher = pushStack(expr); if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else + { LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown expression type"); + ice->ice("NonStrictTypeChecker encountered an unknown expression type"); + } } - void visit(AstExprGroup* group, NonStrictContext& context) {} - void visit(AstExprConstantNil* expr, NonStrictContext& context) {} - void visit(AstExprConstantBool* expr, NonStrictContext& context) {} - void visit(AstExprConstantNumber* expr, NonStrictContext& context) {} - void visit(AstExprConstantString* expr, NonStrictContext& context) {} - void visit(AstExprLocal* local, NonStrictContext& context) {} - void visit(AstExprGlobal* global, NonStrictContext& context) {} - void visit(AstExprVarargs* global, NonStrictContext& context) {} - - void visit(AstExprCall* call, NonStrictContext& context) + NonStrictContext visit(AstExprGroup* group) { + return {}; + } + + NonStrictContext visit(AstExprConstantNil* expr) + { + return {}; + } + + NonStrictContext visit(AstExprConstantBool* expr) + { + return {}; + } + + NonStrictContext visit(AstExprConstantNumber* expr) + { + return {}; + } + + NonStrictContext visit(AstExprConstantString* expr) + { + return {}; + } + + NonStrictContext visit(AstExprLocal* local) + { + return {}; + } + + NonStrictContext visit(AstExprGlobal* global) + { + return {}; + } + + NonStrictContext visit(AstExprVarargs* global) + { + return {}; + } + + + NonStrictContext visit(AstExprCall* call) + { + NonStrictContext fresh{}; TypeId* originalCallTy = module->astOriginalCallTypes.find(call); if (!originalCallTy) - return; + return fresh; TypeId fnTy = *originalCallTy; - // TODO: how should we link this to the passed in context here - NonStrictContext fresh{}; if (auto fn = get(follow(fnTy))) { if (fn->isCheckedFunction) @@ -353,9 +522,7 @@ struct NonStrictTypeChecker AstExpr* arg = call->args.data[i]; TypeId expectedArgType = argTypes[i]; DefId def = dfg->getDef(arg); - // TODO: Cache negations created here!!! - // See Jira Ticket: https://roblox.atlassian.net/browse/CLI-87539 - TypeId runTimeErrorTy = arena.addType(NegationType{expectedArgType}); + TypeId runTimeErrorTy = getOrCreateNegation(expectedArgType); fresh.context[def.get()] = runTimeErrorTy; } @@ -369,21 +536,72 @@ struct NonStrictTypeChecker } } } + + return fresh; } - void visit(AstExprIndexName* indexName, NonStrictContext& context) {} - void visit(AstExprIndexExpr* indexExpr, NonStrictContext& context) {} - void visit(AstExprFunction* exprFn, NonStrictContext& context) + NonStrictContext visit(AstExprIndexName* indexName) { - auto pusher = pushStack(exprFn); + return {}; + } + + NonStrictContext visit(AstExprIndexExpr* indexExpr) + { + return {}; + } + + NonStrictContext visit(AstExprFunction* exprFn) + { + // TODO: should a function being used as an expression generate a context without the arguments? + auto pusher = pushStack(exprFn); + NonStrictContext remainder = visit(exprFn->body); + for (AstLocal* local : exprFn->args) + { + if (std::optional ty = willRunTimeErrorFunctionDefinition(local, remainder)) + reportError(NonStrictFunctionDefinitionError{exprFn->debugname.value, local->name.value, *ty}, local->location); + remainder.remove(dfg->getDef(local)); + } + return remainder; + } + + NonStrictContext visit(AstExprTable* table) + { + return {}; + } + + NonStrictContext visit(AstExprUnary* unary) + { + return {}; + } + + NonStrictContext visit(AstExprBinary* binary) + { + return {}; + } + + NonStrictContext visit(AstExprTypeAssertion* typeAssertion) + { + return {}; + } + + NonStrictContext visit(AstExprIfElse* ifElse) + { + NonStrictContext condB = visit(ifElse->condition); + NonStrictContext thenB = visit(ifElse->trueExpr); + NonStrictContext elseB = visit(ifElse->falseExpr); + return NonStrictContext::disjunction( + builtinTypes, NotNull{&arena}, condB, NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenB, elseB)); + } + + NonStrictContext visit(AstExprInterpString* interpString) + { + return {}; + } + + NonStrictContext visit(AstExprError* error) + { + return {}; } - void visit(AstExprTable* table, NonStrictContext& context) {} - void visit(AstExprUnary* unary, NonStrictContext& context) {} - void visit(AstExprBinary* binary, NonStrictContext& context) {} - void visit(AstExprTypeAssertion* typeAssertion, NonStrictContext& context) {} - void visit(AstExprIfElse* ifElse, NonStrictContext& context) {} - void visit(AstExprInterpString* interpString, NonStrictContext& context) {} - void visit(AstExprError* error, NonStrictContext& context) {} void reportError(TypeErrorData data, const Location& location) { @@ -402,16 +620,37 @@ struct NonStrictTypeChecker SubtypingResult r = subtyping.isSubtype(actualType, *contextTy); if (r.normalizationTooComplex) reportError(NormalizationTooComplex{}, fragment->location); - - if (!r.isSubtype && !r.isErrorSuppressing) - reportError(TypeMismatch{actualType, *contextTy}, fragment->location); - if (r.isSubtype) return {actualType}; } return {}; } + + std::optional willRunTimeErrorFunctionDefinition(AstLocal* fragment, const NonStrictContext& context) + { + DefId def = dfg->getDef(fragment); + if (std::optional contextTy = context.find(def)) + { + SubtypingResult r1 = subtyping.isSubtype(builtinTypes->unknownType, *contextTy); + SubtypingResult r2 = subtyping.isSubtype(*contextTy, builtinTypes->unknownType); + if (r1.normalizationTooComplex || r2.normalizationTooComplex) + reportError(NormalizationTooComplex{}, fragment->location); + bool isUnknown = r1.isSubtype && r2.isSubtype; + if (isUnknown) + return {builtinTypes->unknownType}; + } + return {}; + } + +private: + TypeId getOrCreateNegation(TypeId baseType) + { + TypeId& cachedResult = cachedNegations[baseType]; + if (!cachedResult) + cachedResult = arena.addType(NegationType{baseType}); + return cachedResult; + }; }; void checkNonStrict(NotNull builtinTypes, NotNull ice, NotNull unifierState, diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 52bbc5d9..30ab7895 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -8,7 +8,10 @@ #include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/RecursionCounter.h" +#include "Luau/Set.h" +#include "Luau/Subtyping.h" #include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/Unifier.h" LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant, false) @@ -16,9 +19,10 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant, false) // This could theoretically be 2000 on amd64, but x86 requires this. LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200); LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000); -LUAU_FASTFLAGVARIABLE(LuauNormalizeCyclicUnions, false); LUAU_FASTFLAG(LuauTransitiveSubtyping) LUAU_FASTFLAG(DebugLuauReadWriteProperties) +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) +LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau { @@ -32,9 +36,14 @@ TypeIds::TypeIds(std::initializer_list tys) void TypeIds::insert(TypeId ty) { ty = follow(ty); - auto [_, fresh] = types.insert(ty); - if (fresh) + + // get a reference to the slot for `ty` in `types` + bool& entry = types[ty]; + + // if `ty` is fresh, we can set it to `true`, add it to the order and hash and be done. + if (!entry) { + entry = true; order.push_back(ty); hash ^= std::hash{}(ty); } @@ -75,25 +84,26 @@ TypeIds::const_iterator TypeIds::end() const TypeIds::iterator TypeIds::erase(TypeIds::const_iterator it) { TypeId ty = *it; - types.erase(ty); + types[ty] = false; hash ^= std::hash{}(ty); return order.erase(it); } size_t TypeIds::size() const { - return types.size(); + return order.size(); } bool TypeIds::empty() const { - return types.empty(); + return order.empty(); } size_t TypeIds::count(TypeId ty) const { ty = follow(ty); - return types.count(ty); + const bool* val = types.find(ty); + return (val && *val) ? 1 : 0; } void TypeIds::retain(const TypeIds& there) @@ -122,7 +132,29 @@ bool TypeIds::isNever() const bool TypeIds::operator==(const TypeIds& there) const { - return hash == there.hash && types == there.types; + // we can early return if the hashes don't match. + if (hash != there.hash) + return false; + + // we have to check equality of the sets themselves if not. + + // if the sets are unequal sizes, then they cannot possibly be equal. + // it is important to use `order` here and not `types` since the mappings + // may have different sizes since removal is not possible, and so erase + // simply writes `false` into the map. + if (order.size() != there.order.size()) + return false; + + // otherwise, we'll need to check that every element we have here is in `there`. + for (auto ty : order) + { + // if it's not, we'll return `false` + if (there.count(ty) == 0) + return false; + } + + // otherwise, we've proven the two equal! + return true; } NormalizedStringType::NormalizedStringType() {} @@ -237,19 +269,56 @@ NormalizedType::NormalizedType(NotNull builtinTypes) , numbers(builtinTypes->neverType) , strings{NormalizedStringType::never} , threads(builtinTypes->neverType) + , buffers(builtinTypes->neverType) { } +bool NormalizedType::isUnknown() const +{ + if (get(tops)) + return true; + + // Otherwise, we can still be unknown! + bool hasAllPrimitives = isPrim(booleans, PrimitiveType::Boolean) && isPrim(nils, PrimitiveType::NilType) && isNumber(numbers) && + strings.isString() && isPrim(threads, PrimitiveType::Thread) && isThread(threads); + + // Check is class + bool isTopClass = false; + for (auto [t, disj] : classes.classes) + { + if (auto ct = get(t)) + { + if (ct->name == "class" && disj.empty()) + { + isTopClass = true; + break; + } + } + } + // Check is table + bool isTopTable = false; + for (auto t : tables) + { + if (isPrim(t, PrimitiveType::Table)) + { + isTopTable = true; + break; + } + } + // any = unknown or error ==> we need to make sure we have all the unknown components, but not errors + return get(errors) && hasAllPrimitives && isTopClass && isTopTable && functions.isTop; +} + bool NormalizedType::isExactlyNumber() const { return hasNumbers() && !hasTops() && !hasBooleans() && !hasClasses() && !hasErrors() && !hasNils() && !hasStrings() && !hasThreads() && - !hasTables() && !hasFunctions() && !hasTyvars(); + (!FFlag::LuauBufferTypeck || !hasBuffers()) && !hasTables() && !hasFunctions() && !hasTyvars(); } bool NormalizedType::isSubtypeOfString() const { return hasStrings() && !hasTops() && !hasBooleans() && !hasClasses() && !hasErrors() && !hasNils() && !hasNumbers() && !hasThreads() && - !hasTables() && !hasFunctions() && !hasTyvars(); + (!FFlag::LuauBufferTypeck || !hasBuffers()) && !hasTables() && !hasFunctions() && !hasTyvars(); } bool NormalizedType::shouldSuppressErrors() const @@ -306,6 +375,12 @@ bool NormalizedType::hasThreads() const return !get(threads); } +bool NormalizedType::hasBuffers() const +{ + LUAU_ASSERT(FFlag::LuauBufferTypeck); + return !get(buffers); +} + bool NormalizedType::hasTables() const { return !tables.isNever(); @@ -326,18 +401,18 @@ 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.classes.isNever() || !get(norm.errors) || !get(norm.nils) || !get(norm.numbers) || !norm.strings.isNever() || !get(norm.threads) || - !norm.functions.isNever() || !norm.tables.empty() || !norm.tyvars.empty(); + (FFlag::LuauBufferTypeck && !get(norm.buffers)) || !norm.functions.isNever() || !norm.tables.empty() || !norm.tyvars.empty(); } -bool Normalizer::isInhabited(const NormalizedType* norm, std::unordered_set seen) +bool Normalizer::isInhabited(const NormalizedType* norm, Set seen) { // If normalization failed, the type is complex, and so is more likely than not to be inhabited. if (!norm) return true; if (!get(norm->tops) || !get(norm->booleans) || !get(norm->errors) || !get(norm->nils) || - !get(norm->numbers) || !get(norm->threads) || !norm->classes.isNever() || !norm->strings.isNever() || - !norm->functions.isNever()) + !get(norm->numbers) || !get(norm->threads) || (FFlag::LuauBufferTypeck && !get(norm->buffers)) || + !norm->classes.isNever() || !norm->strings.isNever() || !norm->functions.isNever()) return true; for (const auto& [_, intersect] : norm->tyvars) @@ -363,7 +438,7 @@ bool Normalizer::isInhabited(TypeId ty) return *result; } - bool result = isInhabited(ty, {}); + bool result = isInhabited(ty, {nullptr}); if (cacheInhabitance) cachedIsInhabited[ty] = result; @@ -371,7 +446,7 @@ bool Normalizer::isInhabited(TypeId ty) return result; } -bool Normalizer::isInhabited(TypeId ty, std::unordered_set seen) +bool Normalizer::isInhabited(TypeId ty, Set seen) { // TODO: use log.follow(ty), CLI-64291 ty = follow(ty); @@ -425,7 +500,7 @@ bool Normalizer::isIntersectionInhabited(TypeId left, TypeId right) return *result; } - std::unordered_set seen = {}; + Set seen{nullptr}; seen.insert(left); seen.insert(right); @@ -561,6 +636,18 @@ static bool isNormalizedThread(TypeId ty) return false; } +static bool isNormalizedBuffer(TypeId ty) +{ + LUAU_ASSERT(FFlag::LuauBufferTypeck); + + if (get(ty)) + return true; + else if (const PrimitiveType* ptv = get(ty)) + return ptv->type == PrimitiveType::Buffer; + else + return false; +} + static bool areNormalizedFunctions(const NormalizedFunctionType& tys) { for (TypeId ty : tys.parts) @@ -647,8 +734,7 @@ static bool areNormalizedClasses(const NormalizedClassType& tys) static bool isPlainTyvar(TypeId ty) { - return (get(ty) || get(ty) || get(ty) || - get(ty) || get(ty)); + return (get(ty) || get(ty) || get(ty) || get(ty) || get(ty)); } static bool isNormalizedTyvar(const NormalizedTyvars& tyvars) @@ -682,6 +768,8 @@ static void assertInvariant(const NormalizedType& norm) LUAU_ASSERT(isNormalizedNumber(norm.numbers)); LUAU_ASSERT(isNormalizedString(norm.strings)); LUAU_ASSERT(isNormalizedThread(norm.threads)); + if (FFlag::LuauBufferTypeck) + LUAU_ASSERT(isNormalizedBuffer(norm.buffers)); LUAU_ASSERT(areNormalizedFunctions(norm.functions)); LUAU_ASSERT(areNormalizedTables(norm.tables)); LUAU_ASSERT(isNormalizedTyvar(norm.tyvars)); @@ -708,9 +796,14 @@ const NormalizedType* Normalizer::normalize(TypeId ty) return found->second.get(); NormalizedType norm{builtinTypes}; - std::unordered_set seenSetTypes; + Set seenSetTypes{nullptr}; if (!unionNormalWithTy(norm, ty, seenSetTypes)) return nullptr; + if (norm.isUnknown()) + { + clearNormal(norm); + norm.tops = builtinTypes->unknownType; + } std::unique_ptr uniq = std::make_unique(std::move(norm)); const NormalizedType* result = uniq.get(); cachedNormals[ty] = std::move(uniq); @@ -724,7 +817,7 @@ bool Normalizer::normalizeIntersections(const std::vector& intersections NormalizedType norm{builtinTypes}; norm.tops = builtinTypes->anyType; // Now we need to intersect the two types - std::unordered_set seenSetTypes; + Set seenSetTypes{nullptr}; for (auto ty : intersections) { if (!intersectNormalWithTy(norm, ty, seenSetTypes)) @@ -747,6 +840,8 @@ void Normalizer::clearNormal(NormalizedType& norm) norm.numbers = builtinTypes->neverType; norm.strings.resetToNever(); norm.threads = builtinTypes->neverType; + if (FFlag::LuauBufferTypeck) + norm.buffers = builtinTypes->neverType; norm.tables.clear(); norm.functions.resetToNever(); norm.tyvars.clear(); @@ -1432,6 +1527,8 @@ bool Normalizer::unionNormals(NormalizedType& here, const NormalizedType& there, here.numbers = (get(there.numbers) ? here.numbers : there.numbers); unionStrings(here.strings, there.strings); here.threads = (get(there.threads) ? here.threads : there.threads); + if (FFlag::LuauBufferTypeck) + here.buffers = (get(there.buffers) ? here.buffers : there.buffers); unionFunctions(here.functions, there.functions); unionTables(here.tables, there.tables); return true; @@ -1460,7 +1557,7 @@ bool Normalizer::withinResourceLimits() } // See above for an explaination of `ignoreSmallerTyvars`. -bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unordered_set& seenSetTypes, int ignoreSmallerTyvars) +bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes, int ignoreSmallerTyvars) { RecursionCounter _rc(&sharedState->counters.recursionCount); if (!withinResourceLimits()) @@ -1488,12 +1585,9 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unor } else if (const UnionType* utv = get(there)) { - if (FFlag::LuauNormalizeCyclicUnions) - { - if (seenSetTypes.count(there)) - return true; - seenSetTypes.insert(there); - } + if (seenSetTypes.count(there)) + return true; + seenSetTypes.insert(there); for (UnionTypeIterator it = begin(utv); it != end(utv); ++it) { @@ -1520,8 +1614,8 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unor } else if (FFlag::LuauTransitiveSubtyping && get(here.tops)) return true; - else if (get(there) || get(there) || get(there) || - get(there) || get(there)) + else if (get(there) || get(there) || get(there) || get(there) || + get(there)) { if (tyvarIndex(there) <= ignoreSmallerTyvars) return true; @@ -1529,6 +1623,12 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unor inter.tops = builtinTypes->unknownType; here.tyvars.insert_or_assign(there, std::make_unique(std::move(inter))); } + else if (auto lt = get(there)) + { + // FIXME? This is somewhat questionable. + // Maybe we should assert because this should never happen? + unionNormalWithTy(here, lt->domain, seenSetTypes, ignoreSmallerTyvars); + } else if (get(there)) unionFunctionsWithFunction(here.functions, there); else if (get(there) || get(there)) @@ -1549,6 +1649,8 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unor here.strings.resetToString(); else if (ptv->type == PrimitiveType::Thread) here.threads = there; + else if (FFlag::LuauBufferTypeck && ptv->type == PrimitiveType::Buffer) + here.buffers = there; else if (ptv->type == PrimitiveType::Function) { here.functions.resetToTop(); @@ -1668,6 +1770,8 @@ std::optional Normalizer::negateNormal(const NormalizedType& her result.strings.isCofinite = !result.strings.isCofinite; result.threads = get(here.threads) ? builtinTypes->threadType : builtinTypes->neverType; + if (FFlag::LuauBufferTypeck) + result.buffers = get(here.buffers) ? builtinTypes->bufferType : builtinTypes->neverType; /* * Things get weird and so, so complicated if we allow negations of @@ -1757,6 +1861,10 @@ void Normalizer::subtractPrimitive(NormalizedType& here, TypeId ty) case PrimitiveType::Thread: here.threads = builtinTypes->neverType; break; + case PrimitiveType::Buffer: + if (FFlag::LuauBufferTypeck) + here.buffers = builtinTypes->neverType; + break; case PrimitiveType::Function: here.functions.resetToNever(); break; @@ -2550,7 +2658,7 @@ void Normalizer::intersectFunctions(NormalizedFunctionType& heres, const Normali } } -bool Normalizer::intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, std::unordered_set& seenSetTypes) +bool Normalizer::intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, Set& seenSetTypes) { for (auto it = here.begin(); it != here.end();) { @@ -2587,6 +2695,8 @@ bool Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& th here.numbers = (get(there.numbers) ? there.numbers : here.numbers); intersectStrings(here.strings, there.strings); here.threads = (get(there.threads) ? there.threads : here.threads); + if (FFlag::LuauBufferTypeck) + here.buffers = (get(there.buffers) ? there.buffers : here.buffers); intersectFunctions(here.functions, there.functions); intersectTables(here.tables, there.tables); @@ -2628,7 +2738,7 @@ bool Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& th return true; } -bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std::unordered_set& seenSetTypes) +bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes) { RecursionCounter _rc(&sharedState->counters.recursionCount); if (!withinResourceLimits()) @@ -2661,8 +2771,8 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std:: return false; return true; } - else if (get(there) || get(there) || get(there) || - get(there) || get(there)) + else if (get(there) || get(there) || get(there) || get(there) || + get(there) || get(there)) { NormalizedType thereNorm{builtinTypes}; NormalizedType topNorm{builtinTypes}; @@ -2670,6 +2780,10 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std:: thereNorm.tyvars.insert_or_assign(there, std::make_unique(std::move(topNorm))); return intersectNormals(here, thereNorm); } + else if (auto lt = get(there)) + { + return intersectNormalWithTy(here, lt->domain, seenSetTypes); + } NormalizedTyvars tyvars = std::move(here.tyvars); @@ -2708,6 +2822,7 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std:: NormalizedStringType strings = std::move(here.strings); NormalizedFunctionType functions = std::move(here.functions); TypeId threads = here.threads; + TypeId buffers = here.buffers; TypeIds tables = std::move(here.tables); clearNormal(here); @@ -2722,6 +2837,8 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std:: here.strings = std::move(strings); else if (ptv->type == PrimitiveType::Thread) here.threads = threads; + else if (FFlag::LuauBufferTypeck && ptv->type == PrimitiveType::Buffer) + here.buffers = buffers; else if (ptv->type == PrimitiveType::Function) here.functions = std::move(functions); else if (ptv->type == PrimitiveType::Table) @@ -2892,6 +3009,8 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) } if (!get(norm.threads)) result.push_back(builtinTypes->threadType); + if (FFlag::LuauBufferTypeck && !get(norm.buffers)) + result.push_back(builtinTypes->bufferType); result.insert(result.end(), norm.tables.begin(), norm.tables.end()); for (auto& [tyvar, intersect] : norm.tyvars) @@ -2915,32 +3034,58 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) bool isSubtype(TypeId subTy, TypeId superTy, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { - if (!FFlag::LuauTransitiveSubtyping) + if (!FFlag::LuauTransitiveSubtyping && !FFlag::DebugLuauDeferredConstraintResolution) return isConsistentSubtype(subTy, superTy, scope, builtinTypes, ice); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; - Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; - u.tryUnify(subTy, superTy); - return !u.failure; + // Subtyping under DCR is not implemented using unification! + if (FFlag::DebugLuauDeferredConstraintResolution) + { + Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&ice}, scope}; + + return subtyping.isSubtype(subTy, superTy).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) { - if (!FFlag::LuauTransitiveSubtyping) + if (!FFlag::LuauTransitiveSubtyping && !FFlag::DebugLuauDeferredConstraintResolution) return isConsistentSubtype(subPack, superPack, scope, builtinTypes, ice); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; - Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; - u.tryUnify(subPack, superPack); - return !u.failure; + // Subtyping under DCR is not implemented using unification! + if (FFlag::DebugLuauDeferredConstraintResolution) + { + Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&ice}, scope}; + + return subtyping.isSubtype(subPack, superPack).isSubtype; + } + else + { + Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; + + u.tryUnify(subPack, superPack); + return !u.failure; + } } bool isConsistentSubtype(TypeId subTy, TypeId superTy, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { + LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; @@ -2954,6 +3099,8 @@ bool isConsistentSubtype(TypeId subTy, TypeId superTy, NotNull scope, Not bool isConsistentSubtype( TypePackId subPack, TypePackId superPack, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { + LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index 2ca40bdd..a3182c0a 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -72,7 +72,7 @@ std::optional> Scope::lookupEx(Symbol sym) } } -std::optional Scope::lookupLValue(DefId def) const +std::optional Scope::lookupUnrefinedType(DefId def) const { for (const Scope* current = this; current; current = current->parent.get()) { @@ -83,7 +83,6 @@ std::optional Scope::lookupLValue(DefId def) const return std::nullopt; } -// TODO: We might kill Scope::lookup(Symbol) once data flow is fully fleshed out with type states and control flow analysis. std::optional Scope::lookup(DefId def) const { for (const Scope* current = this; current; current = current->parent.get()) @@ -181,6 +180,16 @@ std::optional Scope::linearSearchForBinding(const std::string& name, bo return std::nullopt; } +// Updates the `this` scope with the assignments from the `childScope` including ones that doesn't exist in `this`. +void Scope::inheritAssignments(const ScopePtr& childScope) +{ + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + for (const auto& [k, a] : childScope->lvalueTypes) + lvalueTypes[k] = a; +} + // Updates the `this` scope with the refinements from the `childScope` excluding ones that doesn't exist in `this`. void Scope::inheritRefinements(const ScopePtr& childScope) { diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index 6519c6ff..d04aeb82 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -2,6 +2,7 @@ #include "Luau/Simplify.h" +#include "Luau/DenseHash.h" #include "Luau/Normalize.h" // TypeIds #include "Luau/RecursionCounter.h" #include "Luau/ToString.h" @@ -21,7 +22,7 @@ struct TypeSimplifier NotNull builtinTypes; NotNull arena; - std::set blockedTypes; + DenseHashSet blockedTypes{nullptr}; int recursionDepth = 0; diff --git a/Analysis/src/Substitution.cpp b/Analysis/src/Substitution.cpp index 176f1506..2f4e9611 100644 --- a/Analysis/src/Substitution.cpp +++ b/Analysis/src/Substitution.cpp @@ -19,8 +19,12 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a auto go = [ty, &dest, alwaysClone](auto&& a) { using T = std::decay_t; + // The pointer identities of free and local types is very important. + // We decline to copy them. if constexpr (std::is_same_v) return ty; + else if constexpr (std::is_same_v) + return ty; else if constexpr (std::is_same_v) { // This should never happen, but visit() cannot see it. diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index e386bf7b..49db9cd3 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -16,6 +16,8 @@ #include +LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity, false); + namespace Luau { @@ -47,16 +49,77 @@ struct VarianceFlipper bool SubtypingReasoning::operator==(const SubtypingReasoning& other) const { - return subPath == other.subPath && superPath == other.superPath; + return subPath == other.subPath && superPath == other.superPath && variance == other.variance; +} + +size_t SubtypingReasoningHash::operator()(const SubtypingReasoning& r) const +{ + return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1) ^ (static_cast(r.variance) << 1); +} + +template +static void assertReasoningValid(TID subTy, TID superTy, const SubtypingResult& result, NotNull builtinTypes) +{ + if (!FFlag::DebugLuauSubtypingCheckPathValidity) + return; + + for (const SubtypingReasoning& reasoning : result.reasoning) + { + LUAU_ASSERT(traverse(subTy, reasoning.subPath, builtinTypes)); + LUAU_ASSERT(traverse(superTy, reasoning.superPath, builtinTypes)); + } +} + +template<> +void assertReasoningValid(TableIndexer subIdx, TableIndexer superIdx, const SubtypingResult& result, NotNull builtinTypes) +{ + // Empty method to satisfy the compiler. +} + +static SubtypingReasonings mergeReasonings(const SubtypingReasonings& a, const SubtypingReasonings& b) +{ + SubtypingReasonings result{kEmptyReasoning}; + + for (const SubtypingReasoning& r : a) + { + if (r.variance == SubtypingVariance::Invariant) + result.insert(r); + else if (r.variance == SubtypingVariance::Covariant || r.variance == SubtypingVariance::Contravariant) + { + SubtypingReasoning inverseReasoning = SubtypingReasoning{ + r.subPath, r.superPath, r.variance == SubtypingVariance::Covariant ? SubtypingVariance::Contravariant : SubtypingVariance::Covariant}; + if (b.contains(inverseReasoning)) + result.insert(SubtypingReasoning{r.subPath, r.superPath, SubtypingVariance::Invariant}); + else + result.insert(r); + } + } + + for (const SubtypingReasoning& r : b) + { + if (r.variance == SubtypingVariance::Invariant) + result.insert(r); + else if (r.variance == SubtypingVariance::Covariant || r.variance == SubtypingVariance::Contravariant) + { + SubtypingReasoning inverseReasoning = SubtypingReasoning{ + r.subPath, r.superPath, r.variance == SubtypingVariance::Covariant ? SubtypingVariance::Contravariant : SubtypingVariance::Covariant}; + if (a.contains(inverseReasoning)) + result.insert(SubtypingReasoning{r.subPath, r.superPath, SubtypingVariance::Invariant}); + else + result.insert(r); + } + } + + return result; } SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) { - // If this result is a subtype, we take the other result's reasoning. If - // this result is not a subtype, we keep the current reasoning, even if the - // other isn't a subtype. - if (isSubtype) - reasoning = other.reasoning; + // 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. + if (!other.isSubtype) + reasoning = mergeReasonings(reasoning, other.reasoning); isSubtype &= other.isSubtype; // `|=` is intentional here, we want to preserve error related flags. @@ -69,10 +132,17 @@ SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other) { - // If the other result is not a subtype, we take the other result's - // reasoning. - if (!other.isSubtype) - reasoning = other.reasoning; + // 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_ + // our reasoning list. If both results are not subtypes, we join the + // reasoning lists. + if (!isSubtype) + { + if (other.isSubtype) + reasoning.clear(); + else + reasoning = mergeReasonings(reasoning, other.reasoning); + } isSubtype |= other.isSubtype; isErrorSuppressing |= other.isErrorSuppressing; @@ -89,20 +159,26 @@ SubtypingResult& SubtypingResult::withBothComponent(TypePath::Component componen SubtypingResult& SubtypingResult::withSubComponent(TypePath::Component component) { - if (!reasoning) - reasoning = SubtypingReasoning{Path(), Path()}; - - reasoning->subPath = reasoning->subPath.push_front(component); + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{Path(component), TypePath::kEmpty}); + else + { + for (auto& r : reasoning) + r.subPath = r.subPath.push_front(component); + } return *this; } SubtypingResult& SubtypingResult::withSuperComponent(TypePath::Component component) { - if (!reasoning) - reasoning = SubtypingReasoning{Path(), Path()}; - - reasoning->superPath = reasoning->superPath.push_front(component); + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{TypePath::kEmpty, Path(component)}); + else + { + for (auto& r : reasoning) + r.superPath = r.superPath.push_front(component); + } return *this; } @@ -114,20 +190,26 @@ SubtypingResult& SubtypingResult::withBothPath(TypePath::Path path) SubtypingResult& SubtypingResult::withSubPath(TypePath::Path path) { - if (!reasoning) - reasoning = SubtypingReasoning{Path(), Path()}; - - reasoning->subPath = path.append(reasoning->subPath); + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{path, TypePath::kEmpty}); + else + { + for (auto& r : reasoning) + r.subPath = path.append(r.subPath); + } return *this; } SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path) { - if (!reasoning) - reasoning = SubtypingReasoning{Path(), Path()}; - - reasoning->superPath = path.append(reasoning->superPath); + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{TypePath::kEmpty, path}); + else + { + for (auto& r : reasoning) + r.superPath = path.append(r.superPath); + } return *this; } @@ -202,7 +284,10 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy) result.isSubtype = false; } - result.andAlso(isCovariantWith(env, lowerBound, upperBound)); + SubtypingResult boundsResult = isCovariantWith(env, lowerBound, upperBound); + boundsResult.reasoning.clear(); + + result.andAlso(boundsResult); } /* TODO: We presently don't store subtype test results in the persistent @@ -281,7 +366,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub return {true}; std::pair typePair{subTy, superTy}; - if (!seenTypes.insert(typePair).second) + if (!seenTypes.insert(typePair)) { /* TODO: Caching results for recursive types is really tricky to think * about. @@ -321,14 +406,34 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (auto subUnion = get(subTy)) result = isCovariantWith(env, subUnion, superTy); else if (auto superUnion = get(superTy)) + { result = isCovariantWith(env, subTy, superUnion); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + { + semantic.reasoning.clear(); + result = semantic; + } + } + } else if (auto superIntersection = get(superTy)) result = isCovariantWith(env, subTy, superIntersection); else if (auto subIntersection = get(subTy)) { result = isCovariantWith(env, subIntersection, superTy); if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) - result = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + { + // Clear the semantic reasoning, as any reasonings within + // potentially contain invalid paths. + semantic.reasoning.clear(); + result = semantic; + } + } } else if (get(superTy)) result = {true}; @@ -356,9 +461,31 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p.first->ty, p.second->ty).withBothComponent(TypePath::TypeField::Negated); else if (auto subNegation = get(subTy)) + { result = isCovariantWith(env, subNegation, superTy); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + { + semantic.reasoning.clear(); + result = semantic; + } + } + } else if (auto superNegation = get(superTy)) + { result = isCovariantWith(env, subTy, superNegation); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + { + semantic.reasoning.clear(); + result = semantic; + } + } + } else if (auto subGeneric = get(subTy); subGeneric && variance == Variance::Covariant) { bool ok = bindGeneric(env, subTy, superTy); @@ -394,6 +521,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p); + assertReasoningValid(subTy, superTy, result, builtinTypes); + return cache(env, result, subTy, superTy); } @@ -481,7 +610,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId for (size_t i = headSize; i < subHead.size(); ++i) results.push_back(isCovariantWith(env, subHead[i], vt->ty) .withSubComponent(TypePath::Index{i}) - .withSuperComponent(TypePath::TypeField::Variadic)); + .withSuperPath(TypePath::PathBuilder().tail().variadic().build())); } else if (auto gt = get(*superTail)) { @@ -609,19 +738,38 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId iceReporter->ice("Subtyping test encountered the unexpected type pack: " + toString(*superTail)); } - return SubtypingResult::all(results); + SubtypingResult result = SubtypingResult::all(results); + assertReasoningValid(subTp, superTp, result, builtinTypes); + + return result; } template SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { SubtypingResult result = isCovariantWith(env, superTy, subTy); - // If we don't swap the paths here, we will end up producing an invalid path - // whenever we involve contravariance. We'll end up appending path - // components that should belong to the supertype to the subtype, and vice - // versa. - if (result.reasoning) - std::swap(result.reasoning->subPath, result.reasoning->superPath); + if (result.reasoning.empty()) + result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Contravariant}); + else + { + // If we don't swap the paths here, we will end up producing an invalid path + // whenever we involve contravariance. We'll end up appending path + // components that should belong to the supertype to the subtype, and vice + // versa. + for (auto& reasoning : result.reasoning) + { + std::swap(reasoning.subPath, reasoning.superPath); + + // Also swap covariant/contravariant, since those are also the other way + // around. + if (reasoning.variance == SubtypingVariance::Covariant) + reasoning.variance = SubtypingVariance::Contravariant; + else if (reasoning.variance == SubtypingVariance::Contravariant) + reasoning.variance = SubtypingVariance::Covariant; + } + } + + assertReasoningValid(subTy, superTy, result, builtinTypes); return result; } @@ -629,7 +777,17 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy& template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { - return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)); + SubtypingResult result = isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)); + if (result.reasoning.empty()) + result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invariant}); + else + { + for (auto& reasoning : result.reasoning) + reasoning.variance = SubtypingVariance::Invariant; + } + + assertReasoningValid(subTy, superTy, result, builtinTypes); + return result; } template @@ -641,13 +799,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TryP template SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(env, pair.second, pair.first); + return isContravariantWith(env, pair.first, pair.second); } template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)); + return isInvariantWith(env, pair.first, pair.second); } /* @@ -733,17 +891,17 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega if (is(negatedTy)) { // ¬never ~ unknown - result = isCovariantWith(env, builtinTypes->unknownType, superTy); + result = isCovariantWith(env, builtinTypes->unknownType, superTy).withSubComponent(TypePath::TypeField::Negated); } else if (is(negatedTy)) { // ¬unknown ~ never - result = isCovariantWith(env, builtinTypes->neverType, superTy); + result = isCovariantWith(env, builtinTypes->neverType, superTy).withSubComponent(TypePath::TypeField::Negated); } else if (is(negatedTy)) { // ¬any ~ any - result = isCovariantWith(env, negatedTy, superTy); + result = isCovariantWith(env, negatedTy, superTy).withSubComponent(TypePath::TypeField::Negated); } else if (auto u = get(negatedTy)) { @@ -753,8 +911,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega for (TypeId ty : u) { - NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy)); + if (auto negatedPart = get(follow(ty))) + subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy).withSubComponent(TypePath::TypeField::Negated)); + else + { + NegationType negatedTmp{ty}; + subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy)); + } } result = SubtypingResult::all(subtypings); @@ -768,7 +931,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega for (TypeId ty : i) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy)); + subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy).withSubComponent(TypePath::TypeField::Negated)); else { NegationType negatedTmp{ty}; @@ -786,10 +949,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega // subtype of other stuff. else { - result = {false}; + result = SubtypingResult{false}.withSubComponent(TypePath::TypeField::Negated); } - return result.withSubComponent(TypePath::TypeField::Negated); + return result; } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const NegationType* superNegation) @@ -830,7 +993,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type } } - result = SubtypingResult::all(subtypings); + return SubtypingResult::all(subtypings); } else if (auto i = get(negatedTy)) { @@ -849,7 +1012,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type } } - result = SubtypingResult::any(subtypings); + return SubtypingResult::any(subtypings); } else if (auto p = get2(subTy, negatedTy)) { @@ -931,8 +1094,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl { std::vector results; if (auto it = subTable->props.find(name); it != subTable->props.end()) - results.push_back(isInvariantWith(env, it->second.type(), prop.type()) - .withBothComponent(TypePath::Property(name))); + results.push_back(isInvariantWith(env, it->second.type(), prop.type()).withBothComponent(TypePath::Property(name))); if (subTable->indexer) { @@ -967,7 +1129,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Meta SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable) { - if (auto subTable = get(subMt->table)) + if (auto subTable = get(follow(subMt->table))) { // Metatables cannot erase properties from the table they're attached to, so // the subtyping rule for this is just if the table component is a subtype @@ -1067,7 +1229,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl { return isInvariantWith(env, subIndexer.indexType, superIndexer.indexType) .withBothComponent(TypePath::TypeField::IndexLookup) - .andAlso(isInvariantWith(env, superIndexer.indexResultType, subIndexer.indexResultType).withBothComponent(TypePath::TypeField::IndexResult)); + .andAlso(isInvariantWith(env, subIndexer.indexResultType, superIndexer.indexResultType).withBothComponent(TypePath::TypeField::IndexResult)); } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm) @@ -1194,12 +1356,11 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type { std::vector results; - size_t i = 0; for (TypeId subTy : subTypes) { results.emplace_back(); for (TypeId superTy : superTypes) - results.back().orElse(isCovariantWith(env, subTy, superTy).withBothComponent(TypePath::Index{i++})); + results.back().orElse(isCovariantWith(env, subTy, superTy)); } return SubtypingResult::all(results); diff --git a/Analysis/src/ToDot.cpp b/Analysis/src/ToDot.cpp index 09851024..c4241711 100644 --- a/Analysis/src/ToDot.cpp +++ b/Analysis/src/ToDot.cpp @@ -261,6 +261,14 @@ void StateDot::visitChildren(TypeId ty, int index) visitChild(t.upperBound, index, "[upperBound]"); } } + else if constexpr (std::is_same_v) + { + formatAppend(result, "LocalType"); + finishNodeLabel(ty); + finishNode(); + + visitChild(t.domain, 1, "[domain]"); + } else if constexpr (std::is_same_v) { formatAppend(result, "AnyType %d", index); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 58c03db4..918da330 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -100,6 +100,16 @@ struct FindCyclicTypes final : TypeVisitor return false; } + bool visit(TypeId ty, const LocalType& lt) override + { + if (!visited.insert(ty).second) + return false; + + traverse(lt.domain); + + return false; + } + bool visit(TypeId ty, const TableType& ttv) override { if (!visited.insert(ty).second) @@ -500,6 +510,15 @@ struct TypeStringifier } } + void operator()(TypeId ty, const LocalType& lt) + { + state.emit("l-"); + state.emit(lt.name); + state.emit("=["); + stringify(lt.domain); + state.emit("]"); + } + void operator()(TypeId, const BoundType& btv) { stringify(btv.boundTo); @@ -562,6 +581,9 @@ struct TypeStringifier case PrimitiveType::Thread: state.emit("thread"); return; + case PrimitiveType::Buffer: + state.emit("buffer"); + return; case PrimitiveType::Function: state.emit("function"); return; @@ -1699,7 +1721,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) std::string iteratorStr = tos(c.iterator); std::string variableStr = tos(c.variables); - return variableStr + " ~ Iterate<" + iteratorStr + ">"; + return variableStr + " ~ iterate " + iteratorStr; } else if constexpr (std::is_same_v) { diff --git a/Analysis/src/Transpiler.cpp b/Analysis/src/Transpiler.cpp index 8fd40772..85b8849f 100644 --- a/Analysis/src/Transpiler.cpp +++ b/Analysis/src/Transpiler.cpp @@ -10,7 +10,6 @@ #include #include -LUAU_FASTFLAG(LuauFloorDivision) namespace { @@ -474,8 +473,6 @@ struct Printer case AstExprBinary::Pow: case AstExprBinary::CompareLt: case AstExprBinary::CompareGt: - LUAU_ASSERT(FFlag::LuauFloorDivision || a->op != AstExprBinary::FloorDiv); - writer.maybeSpace(a->right->location.begin, 2); writer.symbol(toString(a->op)); break; @@ -761,8 +758,6 @@ struct Printer writer.symbol("/="); break; case AstExprBinary::FloorDiv: - LUAU_ASSERT(FFlag::LuauFloorDivision); - writer.maybeSpace(a->value->location.begin, 2); writer.symbol("//="); break; diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index 1859131b..e76837a8 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -27,6 +27,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAGVARIABLE(LuauInitializeStringMetatableInGlobalTypes, false) +LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau { @@ -214,6 +215,13 @@ bool isThread(TypeId ty) return isPrim(ty, PrimitiveType::Thread); } +bool isBuffer(TypeId ty) +{ + LUAU_ASSERT(FFlag::LuauBufferTypeck); + + return isPrim(ty, PrimitiveType::Buffer); +} + bool isOptional(TypeId ty) { if (isNil(ty)) @@ -604,10 +612,11 @@ FunctionType::FunctionType(TypeLevel level, Scope* scope, std::vector ge Property::Property() {} Property::Property(TypeId readTy, bool deprecated, const std::string& deprecatedSuggestion, std::optional location, const Tags& tags, - const std::optional& documentationSymbol) + const std::optional& documentationSymbol, std::optional typeLocation) : deprecated(deprecated) , deprecatedSuggestion(deprecatedSuggestion) , location(location) + , typeLocation(typeLocation) , tags(tags) , documentationSymbol(documentationSymbol) , readTy(readTy) @@ -925,6 +934,7 @@ BuiltinTypes::BuiltinTypes() , 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})) , classType(arena->addType(Type{ClassType{"class", {}, std::nullopt, std::nullopt, {}, {}, {}}, /*persistent*/ true})) , tableType(arena->addType(Type{PrimitiveType{PrimitiveType::Table}, /*persistent*/ true})) diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index 3a1217bf..0e246204 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -13,8 +13,6 @@ #include -LUAU_FASTFLAG(LuauParseDeclareClassIndexer); - static char* allocateString(Luau::Allocator& allocator, std::string_view contents) { char* result = (char*)allocator.allocate(contents.size() + 1); @@ -106,6 +104,8 @@ public: return allocator->alloc(Location(), std::nullopt, AstName("string"), std::nullopt, Location()); case PrimitiveType::Thread: return allocator->alloc(Location(), std::nullopt, AstName("thread"), std::nullopt, Location()); + case PrimitiveType::Buffer: + return allocator->alloc(Location(), std::nullopt, AstName("buffer"), std::nullopt, Location()); default: return nullptr; } @@ -230,7 +230,7 @@ public: } AstTableIndexer* indexer = nullptr; - if (FFlag::LuauParseDeclareClassIndexer && ctv.indexer) + if (ctv.indexer) { RecursionCounter counter(&count); @@ -329,10 +329,14 @@ public: { return Luau::visit(*this, bound.boundTo->ty); } - AstType* operator()(const FreeType& ftv) + AstType* operator()(const FreeType& ft) { return allocator->alloc(Location(), std::nullopt, AstName("free"), std::nullopt, Location()); } + AstType* operator()(const LocalType& lt) + { + return Luau::visit(*this, lt.domain->ty); + } AstType* operator()(const UnionType& uv) { AstArray unionTypes; diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index bf8e362d..32b91637 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -3,9 +3,9 @@ #include "Luau/Ast.h" #include "Luau/AstQuery.h" -#include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/DcrLogger.h" +#include "Luau/DenseHash.h" #include "Luau/Error.h" #include "Luau/InsertionOrderedMap.h" #include "Luau/Instantiation.h" @@ -20,12 +20,12 @@ #include "Luau/TypePack.h" #include "Luau/TypePath.h" #include "Luau/TypeUtils.h" +#include "Luau/TypeOrPack.h" #include "Luau/VisitType.h" #include LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(LuauFloorDivision); namespace Luau { @@ -1660,14 +1660,48 @@ struct TypeChecker2 if (argIt == end(inferredFtv->argTypes)) break; + TypeId inferredArgTy = *argIt; + if (arg->annotation) { - TypeId inferredArgTy = *argIt; TypeId annotatedArgTy = lookupAnnotation(arg->annotation); testIsSubtype(inferredArgTy, annotatedArgTy, arg->location); } + // Some Luau constructs can result in an argument type being + // reduced to never by inference. In this case, we want to + // report an error at the function, instead of reporting an + // error at every callsite. + if (is(follow(inferredArgTy))) + { + // If the annotation simplified to never, we don't want to + // even look at contributors. + bool explicitlyNever = false; + if (arg->annotation) + { + TypeId annotatedArgTy = lookupAnnotation(arg->annotation); + explicitlyNever = is(annotatedArgTy); + } + + // Not following here is deliberate: the contribution map is + // keyed by type pointer, but that type pointer has, at some + // point, been transmuted to a bound type pointing to never. + if (const auto contributors = module->upperBoundContributors.find(inferredArgTy); contributors && !explicitlyNever) + { + // It's unfortunate that we can't link error messages + // together. For now, this will work. + reportError( + GenericError{format( + "Parameter '%s' has been reduced to never. This function is not callable with any possible value.", arg->name.value)}, + arg->location); + for (const auto& [site, component] : *contributors) + reportError(ExtraInformation{format("Parameter '%s' is required to be a subtype of '%s' here.", arg->name.value, + toString(component).c_str())}, + site); + } + } + ++argIt; } } @@ -1819,8 +1853,6 @@ struct TypeChecker2 bool typesHaveIntersection = normalizer.isIntersectionInhabited(leftType, rightType); if (auto it = kBinaryOpMetamethods.find(expr->op); it != kBinaryOpMetamethods.end()) { - LUAU_ASSERT(FFlag::LuauFloorDivision || expr->op != AstExprBinary::Op::FloorDiv); - std::optional leftMt = getMetatable(leftType, builtinTypes); std::optional rightMt = getMetatable(rightType, builtinTypes); bool matches = leftMt == rightMt; @@ -2009,8 +2041,6 @@ struct TypeChecker2 case AstExprBinary::Op::FloorDiv: case AstExprBinary::Op::Pow: case AstExprBinary::Op::Mod: - LUAU_ASSERT(FFlag::LuauFloorDivision || expr->op != AstExprBinary::Op::FloorDiv); - testIsSubtype(leftType, builtinTypes->numberType, expr->left->location); testIsSubtype(rightType, builtinTypes->numberType, expr->right->location); @@ -2413,6 +2443,72 @@ struct TypeChecker2 } } + template + std::optional explainReasonings(TID subTy, TID superTy, Location location, const SubtypingResult& r) + { + if (r.reasoning.empty()) + return std::nullopt; + + std::vector reasons; + for (const SubtypingReasoning& reasoning : r.reasoning) + { + if (reasoning.subPath.empty() && reasoning.superPath.empty()) + continue; + + std::optional subLeaf = traverse(subTy, reasoning.subPath, builtinTypes); + std::optional superLeaf = traverse(superTy, reasoning.superPath, builtinTypes); + + if (!subLeaf || !superLeaf) + ice->ice("Subtyping test returned a reasoning with an invalid path", location); + + if (!get2(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) + ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); + + std::string relation = "a subtype of"; + if (reasoning.variance == SubtypingVariance::Invariant) + relation = "exactly"; + else if (reasoning.variance == SubtypingVariance::Contravariant) + relation = "a supertype of"; + + std::string reason; + if (reasoning.subPath == reasoning.superPath) + reason = "at " + toString(reasoning.subPath) + ", " + toString(*subLeaf) + " is not " + relation + " " + toString(*superLeaf); + else + reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + ") is not " + + relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(*superLeaf) + ")"; + + reasons.push_back(reason); + } + + // DenseHashSet ordering is entirely undefined, so we want to + // sort the reasons here to achieve a stable error + // stringification. + std::sort(reasons.begin(), reasons.end()); + std::string allReasons; + bool first = true; + for (const std::string& reason : reasons) + { + if (first) + first = false; + else + allReasons += "\n\t"; + + allReasons += reason; + } + + return allReasons; + } + + void explainError(TypeId subTy, TypeId superTy, Location location, const SubtypingResult& result) + { + reportError(TypeMismatch{superTy, subTy, explainReasonings(subTy, superTy, location, result).value_or("")}, location); + } + + void explainError(TypePackId subTy, TypePackId superTy, Location location, const SubtypingResult& result) + { + reportError(TypePackMismatch{superTy, subTy, explainReasonings(subTy, superTy, location, result).value_or("")}, location); + } + bool testIsSubtype(TypeId subTy, TypeId superTy, Location location) { SubtypingResult r = subtyping->isSubtype(subTy, superTy); @@ -2421,27 +2517,7 @@ struct TypeChecker2 reportError(NormalizationTooComplex{}, location); if (!r.isSubtype && !r.isErrorSuppressing) - { - if (r.reasoning) - { - std::optional subLeaf = traverse(subTy, r.reasoning->subPath, builtinTypes); - std::optional superLeaf = traverse(superTy, r.reasoning->superPath, builtinTypes); - - if (!subLeaf || !superLeaf) - ice->ice("Subtyping test returned a reasoning with an invalid path", location); - - if (!get2(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) - ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); - - std::string reason = "type " + toString(subTy) + toString(r.reasoning->subPath) + " (" + toString(*subLeaf) + - ") is not a subtype of " + toString(superTy) + toString(r.reasoning->superPath) + " (" + toString(*superLeaf) + - ")"; - - reportError(TypeMismatch{superTy, subTy, reason}, location); - } - else - reportError(TypeMismatch{superTy, subTy}, location); - } + explainError(subTy, superTy, location, r); return r.isSubtype; } @@ -2454,7 +2530,7 @@ struct TypeChecker2 reportError(NormalizationTooComplex{}, location); if (!r.isSubtype && !r.isErrorSuppressing) - reportError(TypePackMismatch{superTy, subTy}, location); + explainError(subTy, superTy, location, r); return r.isSubtype; } @@ -2502,7 +2578,7 @@ struct TypeChecker2 if (!normalizer.isInhabited(ty)) return; - std::unordered_set seen; + DenseHashSet seen{nullptr}; bool found = hasIndexTypeFromType(ty, prop, location, seen, astIndexExprType); foundOneProp |= found; if (!found) @@ -2563,14 +2639,14 @@ struct TypeChecker2 } } - bool hasIndexTypeFromType(TypeId ty, const std::string& prop, const Location& location, std::unordered_set& seen, TypeId astIndexExprType) + bool hasIndexTypeFromType(TypeId ty, const std::string& prop, const Location& location, DenseHashSet& seen, TypeId astIndexExprType) { // If we have already encountered this type, we must assume that some // other codepath will do the right thing and signal false if the // property is not present. - const bool isUnseen = seen.insert(ty).second; - if (!isUnseen) + if (seen.contains(ty)) return true; + seen.insert(ty); if (get(ty) || get(ty) || get(ty)) return true; diff --git a/Analysis/src/TypeFamily.cpp b/Analysis/src/TypeFamily.cpp index e3afb944..a3a67ace 100644 --- a/Analysis/src/TypeFamily.cpp +++ b/Analysis/src/TypeFamily.cpp @@ -751,8 +751,11 @@ TypeFamilyReductionResult andFamilyFn(const std::vector& typePar // And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is truthy. SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->falsyType); SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); - std::vector blockedTypes(filteredLhs.blockedTypes.begin(), filteredLhs.blockedTypes.end()); - blockedTypes.insert(blockedTypes.end(), overallResult.blockedTypes.begin(), overallResult.blockedTypes.end()); + std::vector blockedTypes{}; + for (auto ty : filteredLhs.blockedTypes) + blockedTypes.push_back(ty); + for (auto ty : overallResult.blockedTypes) + blockedTypes.push_back(ty); return {overallResult.result, false, std::move(blockedTypes), {}}; } @@ -776,8 +779,11 @@ TypeFamilyReductionResult orFamilyFn(const std::vector& typePara // Or evalutes to the LHS type if the LHS is truthy, and the RHS type if LHS is falsy. SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->truthyType); SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); - std::vector blockedTypes(filteredLhs.blockedTypes.begin(), filteredLhs.blockedTypes.end()); - blockedTypes.insert(blockedTypes.end(), overallResult.blockedTypes.begin(), overallResult.blockedTypes.end()); + std::vector blockedTypes{}; + for (auto ty : filteredLhs.blockedTypes) + blockedTypes.push_back(ty); + for (auto ty : overallResult.blockedTypes) + blockedTypes.push_back(ty); return {overallResult.result, false, std::move(blockedTypes), {}}; } diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index a29b1e06..0ffe40df 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -35,14 +35,11 @@ LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false) -LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure) LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false) LUAU_FASTFLAGVARIABLE(LuauLoopControlFlowAnalysis, false) -LUAU_FASTFLAGVARIABLE(LuauVariadicOverloadFix, false) LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false) -LUAU_FASTFLAG(LuauParseDeclareClassIndexer) -LUAU_FASTFLAG(LuauFloorDivision); +LUAU_FASTFLAG(LuauBufferTypeck) +LUAU_FASTFLAGVARIABLE(LuauRemoveBadRelationalOperatorWarning, false) namespace Luau { @@ -204,7 +201,7 @@ static bool isMetamethod(const Name& name) return name == "__index" || name == "__newindex" || name == "__call" || name == "__concat" || name == "__unm" || name == "__add" || name == "__sub" || name == "__mul" || name == "__div" || name == "__mod" || name == "__pow" || name == "__tostring" || name == "__metatable" || name == "__eq" || name == "__lt" || name == "__le" || name == "__mode" || name == "__iter" || name == "__len" || - (FFlag::LuauFloorDivision && name == "__idiv"); + name == "__idiv"; } size_t HashBoolNamePair::operator()(const std::pair& pair) const @@ -224,6 +221,7 @@ TypeChecker::TypeChecker(const ScopePtr& globalScope, ModuleResolver* resolver, , stringType(builtinTypes->stringType) , booleanType(builtinTypes->booleanType) , threadType(builtinTypes->threadType) + , bufferType(builtinTypes->bufferType) , anyType(builtinTypes->anyType) , unknownType(builtinTypes->unknownType) , neverType(builtinTypes->neverType) @@ -1628,13 +1626,6 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& ty TypeId& bindingType = bindingsMap[name].type; - if (!FFlag::LuauOccursIsntAlwaysFailure) - { - if (unify(ty, bindingType, aliasScope, typealias.location)) - bindingType = ty; - return ControlFlow::None; - } - unify(ty, bindingType, aliasScope, typealias.location); // It is possible for this unification to succeed but for @@ -1764,7 +1755,7 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatDeclareClass& if (!ctv->metatable) ice("No metatable for declared class"); - if (const auto& indexer = declaredClass.indexer; FFlag::LuauParseDeclareClassIndexer && indexer) + if (const auto& indexer = declaredClass.indexer) ctv->indexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType)); TableType* metatable = getMutable(*ctv->metatable); @@ -2562,7 +2553,6 @@ std::string opToMetaTableEntry(const AstExprBinary::Op& op) case AstExprBinary::Div: return "__div"; case AstExprBinary::FloorDiv: - LUAU_ASSERT(FFlag::LuauFloorDivision); return "__idiv"; case AstExprBinary::Mod: return "__mod"; @@ -2765,10 +2755,26 @@ TypeId TypeChecker::checkRelationalOperation( { reportErrors(state.errors); - if (!isEquality && state.errors.empty() && (get(leftType) || isBoolean(leftType))) + if (FFlag::LuauRemoveBadRelationalOperatorWarning) { - reportError(expr.location, GenericError{format("Type '%s' cannot be compared with relational operator %s", toString(leftType).c_str(), - toString(expr.op).c_str())}); + // The original version of this check also produced this error when we had a union type. + // However, the old solver does not readily have the ability to discern if the union is comparable. + // This is the case when the lhs is e.g. a union of singletons and the rhs is the combined type. + // The new solver has much more powerful logic for resolving relational operators, but for now, + // we need to be conservative in the old solver to deliver a reasonable developer experience. + if (!isEquality && state.errors.empty() && isBoolean(leftType)) + { + reportError(expr.location, GenericError{format("Type '%s' cannot be compared with relational operator %s", + toString(leftType).c_str(), toString(expr.op).c_str())}); + } + } + else + { + if (!isEquality && state.errors.empty() && (get(leftType) || isBoolean(leftType))) + { + reportError(expr.location, GenericError{format("Type '%s' cannot be compared with relational operator %s", + toString(leftType).c_str(), toString(expr.op).c_str())}); + } } return booleanType; @@ -3060,8 +3066,6 @@ TypeId TypeChecker::checkBinaryOperation( case AstExprBinary::FloorDiv: case AstExprBinary::Mod: case AstExprBinary::Pow: - LUAU_ASSERT(FFlag::LuauFloorDivision || expr.op != AstExprBinary::FloorDiv); - reportErrors(tryUnify(lhsType, numberType, scope, expr.left->location)); reportErrors(tryUnify(rhsType, numberType, scope, expr.right->location)); return numberType; @@ -3412,15 +3416,12 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex } } - if (FFlag::LuauAllowIndexClassParameters) + if (const ClassType* exprClass = get(exprType)) { - if (const ClassType* exprClass = get(exprType)) - { - if (isNonstrictMode()) - return unknownType; - reportError(TypeError{expr.location, DynamicPropertyLookupOnClassesUnsafe{exprType}}); - return errorRecoveryType(scope); - } + if (isNonstrictMode()) + return unknownType; + reportError(TypeError{expr.location, DynamicPropertyLookupOnClassesUnsafe{exprType}}); + return errorRecoveryType(scope); } } @@ -4026,13 +4027,9 @@ void TypeChecker::checkArgumentList(const ScopePtr& scope, const AstExpr& funNam if (argIndex < argLocations.size()) location = argLocations[argIndex]; - if (FFlag::LuauVariadicOverloadFix) - { - state.location = location; - state.tryUnify(*argIter, vtp->ty); - } - else - unify(*argIter, vtp->ty, scope, location); + state.location = location; + state.tryUnify(*argIter, vtp->ty); + ++argIter; ++argIndex; } @@ -5403,7 +5400,7 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno std::optional tableIndexer; for (const auto& prop : table->props) - props[prop.name.value] = {resolveType(scope, *prop.type)}; + props[prop.name.value] = {resolveType(scope, *prop.type), /* deprecated: */ false, {}, std::nullopt, {}, std::nullopt, prop.location}; if (const auto& indexer = table->indexer) tableIndexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType)); @@ -6025,6 +6022,8 @@ void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, RefinementMap& r return refine(isBoolean, booleanType); else if (typeguardP.kind == "thread") return refine(isThread, threadType); + else if (FFlag::LuauBufferTypeck && typeguardP.kind == "buffer") + return refine(isBuffer, bufferType); else if (typeguardP.kind == "table") { return refine([](TypeId ty) -> bool { diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index ff515bed..fb4d68cb 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -6,12 +6,12 @@ #include "Luau/Type.h" #include "Luau/TypeFwd.h" #include "Luau/TypePack.h" -#include "Luau/TypeUtils.h" +#include "Luau/TypeOrPack.h" +#include #include #include #include -#include LUAU_FASTFLAG(DebugLuauReadWriteProperties); @@ -104,6 +104,41 @@ bool Path::operator==(const Path& other) const return components == other.components; } +size_t PathHash::operator()(const Property& prop) const +{ + return std::hash()(prop.name) ^ static_cast(prop.isRead); +} + +size_t PathHash::operator()(const Index& idx) const +{ + return idx.index; +} + +size_t PathHash::operator()(const TypeField& field) const +{ + return static_cast(field); +} + +size_t PathHash::operator()(const PackField& field) const +{ + return static_cast(field); +} + +size_t PathHash::operator()(const Component& component) const +{ + return visit(*this, component); +} + +size_t PathHash::operator()(const Path& path) const +{ + size_t hash = 0; + + for (const Component& component : path.components) + hash ^= (*this)(component); + + return hash; +} + Path PathBuilder::build() { return Path(std::move(components)); @@ -216,8 +251,6 @@ struct TraversalState TypeOrPack current; NotNull builtinTypes; - - DenseHashSet seen{nullptr}; int steps = 0; void updateCurrent(TypeId ty) @@ -232,18 +265,6 @@ struct TraversalState current = follow(tp); } - bool haveCycle() - { - const void* currentPtr = ptr(current); - - if (seen.contains(currentPtr)) - return true; - else - seen.insert(currentPtr); - - return false; - } - bool tooLong() { return ++steps > DFInt::LuauTypePathMaximumTraverseSteps; @@ -251,7 +272,7 @@ struct TraversalState bool checkInvariants() { - return haveCycle() || tooLong(); + return tooLong(); } bool traverse(const TypePath::Property& property) @@ -277,18 +298,36 @@ struct TraversalState { prop = lookupClassProp(c, property.name); } - else if (auto m = getMetatable(*currentType, builtinTypes)) + // For a metatable type, the table takes priority; check that before + // falling through to the metatable entry below. + else if (auto m = get(*currentType)) { - // Weird: rather than use findMetatableEntry, which requires a lot - // of stuff that we don't have and don't want to pull in, we use the - // path traversal logic to grab __index and then re-enter the lookup - // logic there. - updateCurrent(*m); + TypeOrPack pinned = current; + updateCurrent(m->table); - if (!traverse(TypePath::Property{"__index"})) - return false; + if (traverse(property)) + return true; - return traverse(property); + // Restore the old current type if we didn't traverse the metatable + // successfully; we'll use the next branch to address this. + current = pinned; + } + + if (!prop) + { + if (auto m = getMetatable(*currentType, builtinTypes)) + { + // Weird: rather than use findMetatableEntry, which requires a lot + // of stuff that we don't have and don't want to pull in, we use the + // path traversal logic to grab __index and then re-enter the lookup + // logic there. + updateCurrent(*m); + + if (!traverse(TypePath::Property{"__index"})) + return false; + + return traverse(property); + } } if (prop) @@ -465,7 +504,7 @@ struct TraversalState } // namespace -std::string toString(const TypePath::Path& path) +std::string toString(const TypePath::Path& path, bool prefixDot) { std::stringstream result; bool first = true; @@ -491,7 +530,7 @@ std::string toString(const TypePath::Path& path) } else if constexpr (std::is_same_v) { - if (!first) + if (!first || prefixDot) result << '.'; switch (c) @@ -523,7 +562,7 @@ std::string toString(const TypePath::Path& path) } else if constexpr (std::is_same_v) { - if (!first) + if (!first || prefixDot) result << '.'; switch (c) @@ -580,7 +619,14 @@ std::optional traverse(TypeId root, const Path& path, NotNull traverse(TypePackId root, const Path& path, NotNull builtinTypes); +std::optional traverse(TypePackId root, const Path& path, NotNull builtinTypes) +{ + TraversalState state(follow(root), builtinTypes); + if (traverse(state, path)) + return state.current; + else + return std::nullopt; +} std::optional traverseForType(TypeId root, const Path& path, NotNull builtinTypes) { diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index c371e81e..93f8a851 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -18,12 +18,11 @@ LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false) -LUAU_FASTFLAGVARIABLE(LuauMaintainScopesInUnifier, false) LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping, false) -LUAU_FASTFLAGVARIABLE(LuauOccursIsntAlwaysFailure, false) LUAU_FASTFLAG(LuauAlwaysCommitInferencesOfFunctionCalls) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAGVARIABLE(LuauFixIndexerSubtypingOrdering, false) +LUAU_FASTFLAGVARIABLE(LuauUnifierShouldNotCopyError, false) namespace Luau { @@ -1514,7 +1513,7 @@ struct WeirdIter auto freePack = log.getMutable(packId); level = freePack->level; - if (FFlag::LuauMaintainScopesInUnifier && freePack->scope != nullptr) + if (freePack->scope != nullptr) scope = freePack->scope; log.replace(packId, BoundTypePack(newTail)); packId = newTail; @@ -1679,11 +1678,8 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal auto superIter = WeirdIter(superTp, log); auto subIter = WeirdIter(subTp, log); - if (FFlag::LuauMaintainScopesInUnifier) - { - superIter.scope = scope.get(); - subIter.scope = scope.get(); - } + superIter.scope = scope.get(); + subIter.scope = scope.get(); auto mkFreshType = [this](Scope* scope, TypeLevel level) { if (FFlag::DebugLuauDeferredConstraintResolution) @@ -2877,7 +2873,7 @@ bool Unifier::occursCheck(TypeId needle, TypeId haystack, bool reversed) bool occurs = occursCheck(sharedState.tempSeenTy, needle, haystack); - if (occurs && FFlag::LuauOccursIsntAlwaysFailure) + if (occurs) { Unifier innerState = makeChildUnifier(); if (const UnionType* ut = get(haystack)) @@ -2935,15 +2931,7 @@ bool Unifier::occursCheck(DenseHashSet& seen, TypeId needle, TypeId hays ice("Expected needle to be free"); if (needle == haystack) - { - if (!FFlag::LuauOccursIsntAlwaysFailure) - { - reportError(location, OccursCheckFailed{}); - log.replace(needle, *builtinTypes->errorRecoveryType()); - } - return true; - } if (log.getMutable(haystack) || (hideousFixMeGenericsAreActuallyFree && log.is(haystack))) return false; @@ -2967,10 +2955,13 @@ bool Unifier::occursCheck(TypePackId needle, TypePackId haystack, bool reversed) bool occurs = occursCheck(sharedState.tempSeenTp, needle, haystack); - if (occurs && FFlag::LuauOccursIsntAlwaysFailure) + if (occurs) { reportError(location, OccursCheckFailed{}); - log.replace(needle, *builtinTypes->errorRecoveryTypePack()); + if (FFlag::LuauUnifierShouldNotCopyError) + log.replace(needle, BoundTypePack{builtinTypes->errorRecoveryTypePack()}); + else + log.replace(needle, *builtinTypes->errorRecoveryTypePack()); } return occurs; @@ -2997,15 +2988,7 @@ bool Unifier::occursCheck(DenseHashSet& seen, TypePackId needle, Typ while (!log.getMutable(haystack)) { if (needle == haystack) - { - if (!FFlag::LuauOccursIsntAlwaysFailure) - { - reportError(location, OccursCheckFailed{}); - log.replace(needle, *builtinTypes->errorRecoveryTypePack()); - } - return true; - } if (auto a = get(haystack); a && a->tail) { diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 11f96ea1..6b213aea 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -5,9 +5,6 @@ #include "Luau/Instantiation.h" #include "Luau/Scope.h" #include "Luau/Simplify.h" -#include "Luau/Substitution.h" -#include "Luau/ToString.h" -#include "Luau/TxnLog.h" #include "Luau/Type.h" #include "Luau/TypeArena.h" #include "Luau/TypeCheckLimits.h" @@ -16,7 +13,6 @@ #include #include -#include LUAU_FASTINT(LuauTypeInferRecursionLimit) @@ -49,7 +45,10 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy) FreeType* superFree = getMutable(superTy); if (subFree) + { subFree->upperBound = mkIntersection(subFree->upperBound, superTy); + expandedFreeTypes[subTy].push_back(superTy); + } if (superFree) superFree->lowerBound = mkUnion(superFree->lowerBound, subTy); @@ -114,7 +113,7 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy) return argResult && retResult; } - auto subTable = get(subTy); + auto subTable = getMutable(subTy); auto superTable = get(superTy); if (subTable && superTable) { @@ -211,7 +210,7 @@ bool Unifier2::unify(TypeId subTy, const IntersectionType* superIntersection) return result; } -bool Unifier2::unify(const TableType* subTable, const TableType* superTable) +bool Unifier2::unify(TableType* subTable, const TableType* superTable) { bool result = true; @@ -257,6 +256,21 @@ bool Unifier2::unify(const TableType* subTable, const TableType* superTable) result &= unify(subTable->indexer->indexResultType, superTable->indexer->indexResultType); } + if (!subTable->indexer && subTable->state == TableState::Unsealed && superTable->indexer) + { + /* + * Unsealed tables are always created from literal table expressions. We + * can't be completely certain whether such a table has an indexer just + * by the content of the expression itself, so we need to be a bit more + * flexible here. + * + * If we are trying to reconcile an unsealed table with a table that has + * an indexer, we therefore conclude that the unsealed table has the + * same indexer. + */ + subTable->indexer = *superTable->indexer; + } + return result; } diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index a3908a56..2abda788 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -3,6 +3,7 @@ #include "Luau/Location.h" +#include #include #include #include @@ -91,10 +92,21 @@ struct AstArray { return data; } + const T* end() const { return data + size; } + + std::reverse_iterator rbegin() const + { + return std::make_reverse_iterator(end()); + } + + std::reverse_iterator rend() const + { + return std::make_reverse_iterator(begin()); + } }; struct AstTypeList @@ -249,6 +261,7 @@ public: enum class ConstantNumberParseResult { Ok, + Imprecise, Malformed, BinOverflow, HexOverflow, diff --git a/Ast/include/Luau/Location.h b/Ast/include/Luau/Location.h index 041a2c63..3fc8921a 100644 --- a/Ast/include/Luau/Location.h +++ b/Ast/include/Luau/Location.h @@ -1,7 +1,6 @@ // 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 { @@ -9,7 +8,11 @@ struct Position { unsigned int line, column; - Position(unsigned int line, unsigned int column); + Position(unsigned int line, unsigned int column) + : line(line) + , column(column) + { + } bool operator==(const Position& rhs) const; bool operator!=(const Position& rhs) const; @@ -25,10 +28,29 @@ struct Location { Position begin, end; - Location(); - Location(const Position& begin, const Position& end); - Location(const Position& begin, unsigned int length); - Location(const Location& begin, const Location& end); + Location() + : begin(0, 0) + , end(0, 0) + { + } + + Location(const Position& begin, const Position& end) + : begin(begin) + , end(end) + { + } + + Location(const Position& begin, unsigned int length) + : begin(begin) + , end(begin.line, begin.column + length) + { + } + + Location(const Location& begin, const Location& end) + : begin(begin.begin) + , end(end.end) + { + } bool operator==(const Location& rhs) const; bool operator!=(const Location& rhs) const; diff --git a/Ast/src/Ast.cpp b/Ast/src/Ast.cpp index a7d7eef7..9a6ca4d7 100644 --- a/Ast/src/Ast.cpp +++ b/Ast/src/Ast.cpp @@ -3,7 +3,6 @@ #include "Luau/Common.h" -LUAU_FASTFLAG(LuauFloorDivision); namespace Luau { @@ -282,7 +281,6 @@ std::string toString(AstExprBinary::Op op) case AstExprBinary::Div: return "/"; case AstExprBinary::FloorDiv: - LUAU_ASSERT(FFlag::LuauFloorDivision); return "//"; case AstExprBinary::Mod: return "%"; diff --git a/Ast/src/Lexer.cpp b/Ast/src/Lexer.cpp index a493acfe..96653a56 100644 --- a/Ast/src/Lexer.cpp +++ b/Ast/src/Lexer.cpp @@ -7,7 +7,6 @@ #include -LUAU_FASTFLAGVARIABLE(LuauFloorDivision, false) LUAU_FASTFLAGVARIABLE(LuauLexerLookaheadRemembersBraceType, false) LUAU_FASTFLAGVARIABLE(LuauCheckedFunctionSyntax, false) @@ -142,7 +141,7 @@ std::string Lexeme::toString() const return "'::'"; case FloorDiv: - return FFlag::LuauFloorDivision ? "'//'" : ""; + return "'//'"; case AddAssign: return "'+='"; @@ -157,7 +156,7 @@ std::string Lexeme::toString() const return "'/='"; case FloorDivAssign: - return FFlag::LuauFloorDivision ? "'//='" : ""; + return "'//='"; case ModAssign: return "'%='"; @@ -909,44 +908,29 @@ Lexeme Lexer::readNext() case '/': { - if (FFlag::LuauFloorDivision) + consume(); + + char ch = peekch(); + + if (ch == '=') { consume(); - - char ch = peekch(); - - if (ch == '=') - { - consume(); - return Lexeme(Location(start, 2), Lexeme::DivAssign); - } - else if (ch == '/') - { - consume(); - - if (peekch() == '=') - { - consume(); - return Lexeme(Location(start, 3), Lexeme::FloorDivAssign); - } - else - return Lexeme(Location(start, 2), Lexeme::FloorDiv); - } - else - return Lexeme(Location(start, 1), '/'); + return Lexeme(Location(start, 2), Lexeme::DivAssign); } - else + else if (ch == '/') { consume(); if (peekch() == '=') { consume(); - return Lexeme(Location(start, 2), Lexeme::DivAssign); + return Lexeme(Location(start, 3), Lexeme::FloorDivAssign); } else - return Lexeme(Location(start, 1), '/'); + return Lexeme(Location(start, 2), Lexeme::FloorDiv); } + else + return Lexeme(Location(start, 1), '/'); } case '*': diff --git a/Ast/src/Location.cpp b/Ast/src/Location.cpp index 40f8e23e..c2c66d9f 100644 --- a/Ast/src/Location.cpp +++ b/Ast/src/Location.cpp @@ -1,16 +1,9 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Location.h" -#include namespace Luau { -Position::Position(unsigned int line, unsigned int column) - : line(line) - , column(column) -{ -} - bool Position::operator==(const Position& rhs) const { return this->column == rhs.column && this->line == rhs.line; @@ -61,30 +54,6 @@ void Position::shift(const Position& start, const Position& oldEnd, const Positi } } -Location::Location() - : begin(0, 0) - , end(0, 0) -{ -} - -Location::Location(const Position& begin, const Position& end) - : begin(begin) - , end(end) -{ -} - -Location::Location(const Position& begin, unsigned int length) - : begin(begin) - , end(begin.line, begin.column + length) -{ -} - -Location::Location(const Location& begin, const Location& end) - : begin(begin.begin) - , end(end.end) -{ -} - bool Location::operator==(const Location& rhs) const { return this->begin == rhs.begin && this->end == rhs.end; diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index a9747143..e4b840b2 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -16,14 +16,9 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) // Warning: If you are introducing new syntax, ensure that it is behind a separate // flag so that we don't break production games by reverting syntax changes. // See docs/SyntaxChanges.md for an explanation. -LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false) LUAU_FASTFLAGVARIABLE(LuauClipExtraHasEndProps, false) -LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauCheckedFunctionSyntax) -LUAU_FASTFLAGVARIABLE(LuauBetterTypeUnionLimits, false) -LUAU_FASTFLAGVARIABLE(LuauBetterTypeRecLimits, false) - namespace Luau { @@ -924,7 +919,7 @@ AstStat* Parser::parseDeclaration(const Location& start) { props.push_back(parseDeclaredClassMethod()); } - else if (lexer.current().type == '[' && (!FFlag::LuauParseDeclareClassIndexer || lexer.lookahead().type == Lexeme::RawString || + else if (lexer.current().type == '[' && (lexer.lookahead().type == Lexeme::RawString || lexer.lookahead().type == Lexeme::QuotedString)) { const Lexeme begin = lexer.current(); @@ -944,7 +939,7 @@ AstStat* Parser::parseDeclaration(const Location& start) else report(begin.location, "String literal contains malformed escape sequence or \\0"); } - else if (lexer.current().type == '[' && FFlag::LuauParseDeclareClassIndexer) + else if (lexer.current().type == '[') { if (indexer) { @@ -1544,8 +1539,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) unsigned int oldRecursionCount = recursionCounter; parts.push_back(parseSimpleType(/* allowPack= */ false).type); - if (FFlag::LuauBetterTypeUnionLimits) - recursionCounter = oldRecursionCount; + recursionCounter = oldRecursionCount; isUnion = true; } @@ -1554,7 +1548,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) Location loc = lexer.current().location; nextLexeme(); - if (!FFlag::LuauBetterTypeUnionLimits || !hasOptional) + if (!hasOptional) parts.push_back(allocator.alloc(loc, std::nullopt, nameNil, std::nullopt, loc)); isUnion = true; @@ -1566,8 +1560,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) unsigned int oldRecursionCount = recursionCounter; parts.push_back(parseSimpleType(/* allowPack= */ false).type); - if (FFlag::LuauBetterTypeUnionLimits) - recursionCounter = oldRecursionCount; + recursionCounter = oldRecursionCount; isIntersection = true; } @@ -1579,7 +1572,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) else break; - if (FFlag::LuauBetterTypeUnionLimits && parts.size() > unsigned(FInt::LuauTypeLengthLimit) + hasOptional) + if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + hasOptional) ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile"); } @@ -1607,10 +1600,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) AstTypeOrPack Parser::parseTypeOrPack() { unsigned int oldRecursionCount = recursionCounter; - // recursion counter is incremented in parseSimpleType - if (!FFlag::LuauBetterTypeRecLimits) - incrementRecursionCounter("type annotation"); Location begin = lexer.current().location; @@ -1630,10 +1620,7 @@ AstTypeOrPack Parser::parseTypeOrPack() AstType* Parser::parseType(bool inDeclarationContext) { unsigned int oldRecursionCount = recursionCounter; - // recursion counter is incremented in parseSimpleType - if (!FFlag::LuauBetterTypeRecLimits) - incrementRecursionCounter("type annotation"); Location begin = lexer.current().location; @@ -1839,11 +1826,7 @@ std::optional Parser::parseBinaryOp(const Lexeme& l) else if (l.type == '/') return AstExprBinary::Div; else if (l.type == Lexeme::FloorDiv) - { - LUAU_ASSERT(FFlag::LuauFloorDivision); - return AstExprBinary::FloorDiv; - } else if (l.type == '%') return AstExprBinary::Mod; else if (l.type == '^') @@ -1881,11 +1864,7 @@ std::optional Parser::parseCompoundOp(const Lexeme& l) else if (l.type == Lexeme::DivAssign) return AstExprBinary::Div; else if (l.type == Lexeme::FloorDivAssign) - { - LUAU_ASSERT(FFlag::LuauFloorDivision); - return AstExprBinary::FloorDiv; - } else if (l.type == Lexeme::ModAssign) return AstExprBinary::Mod; else if (l.type == Lexeme::PowAssign) @@ -2187,6 +2166,9 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data, return base == 2 ? ConstantNumberParseResult::BinOverflow : ConstantNumberParseResult::HexOverflow; } + if (value >= (1ull << 53) && static_cast(result) != value) + return ConstantNumberParseResult::Imprecise; + return ConstantNumberParseResult::Ok; } @@ -2203,8 +2185,24 @@ static ConstantNumberParseResult parseDouble(double& result, const char* data) char* end = nullptr; double value = strtod(data, &end); + // trailing non-numeric characters + if (*end != 0) + return ConstantNumberParseResult::Malformed; + result = value; - return *end == 0 ? ConstantNumberParseResult::Ok : ConstantNumberParseResult::Malformed; + + // for linting, we detect integer constants that are parsed imprecisely + // since the check is expensive we only perform it when the number is larger than the precise integer range + if (value >= double(1ull << 53) && strspn(data, "0123456789") == strlen(data)) + { + char repr[512]; + snprintf(repr, sizeof(repr), "%.0f", value); + + if (strcmp(repr, data) != 0) + return ConstantNumberParseResult::Imprecise; + } + + return ConstantNumberParseResult::Ok; } // simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | FUNCTION body | primaryexp diff --git a/CLI/Bytecode.cpp b/CLI/Bytecode.cpp new file mode 100644 index 00000000..5002ce1d --- /dev/null +++ b/CLI/Bytecode.cpp @@ -0,0 +1,295 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "lua.h" +#include "lualib.h" + +#include "Luau/CodeGen.h" +#include "Luau/Compiler.h" +#include "Luau/BytecodeBuilder.h" +#include "Luau/Parser.h" +#include "Luau/BytecodeSummary.h" +#include "FileUtils.h" +#include "Flags.h" + +#include + +using Luau::CodeGen::FunctionBytecodeSummary; + +struct GlobalOptions +{ + int optimizationLevel = 1; + int debugLevel = 1; +} globalOptions; + +static Luau::CompileOptions copts() +{ + Luau::CompileOptions result = {}; + result.optimizationLevel = globalOptions.optimizationLevel; + result.debugLevel = globalOptions.debugLevel; + + return result; +} + +static void displayHelp(const char* argv0) +{ + printf("Usage: %s [options] [file list]\n", argv0); + 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(" --fflags=: flags to be enabled.\n"); + printf(" --summary-file=: file in which bytecode analysis summary will be recorded (default 'bytecode-summary.json').\n"); + + exit(0); +} + +static bool parseArgs(int argc, char** argv, std::string& summaryFile) +{ + for (int i = 1; i < argc; i++) + { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) + { + displayHelp(argv[0]); + } + else if (strncmp(argv[i], "-O", 2) == 0) + { + int level = atoi(argv[i] + 2); + if (level < 0 || level > 2) + { + fprintf(stderr, "Error: Optimization level must be between 0 and 2 inclusive.\n"); + return false; + } + globalOptions.optimizationLevel = level; + } + else if (strncmp(argv[i], "-g", 2) == 0) + { + int level = atoi(argv[i] + 2); + if (level < 0 || level > 2) + { + fprintf(stderr, "Error: Debug level must be between 0 and 2 inclusive.\n"); + return false; + } + globalOptions.debugLevel = level; + } + else if (strncmp(argv[i], "--summary-file=", 15) == 0) + { + summaryFile = argv[i] + 15; + + if (summaryFile.size() == 0) + { + fprintf(stderr, "Error: filename missing for '--summary-file'.\n\n"); + return false; + } + } + else if (strncmp(argv[i], "--fflags=", 9) == 0) + { + setLuauFlags(argv[i] + 9); + } + else if (argv[i][0] == '-') + { + fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]); + displayHelp(argv[0]); + } + } + + return true; +} + +static void report(const char* name, const Luau::Location& location, const char* type, const char* message) +{ + fprintf(stderr, "%s(%d,%d): %s: %s\n", name, location.begin.line + 1, location.begin.column + 1, type, message); +} + +static void reportError(const char* name, const Luau::ParseError& error) +{ + report(name, error.getLocation(), "SyntaxError", error.what()); +} + +static void reportError(const char* name, const Luau::CompileError& error) +{ + report(name, error.getLocation(), "CompileError", error.what()); +} + +static bool analyzeFile(const char* name, const unsigned nestingLimit, std::vector& summaries) +{ + std::optional source = readFile(name); + + if (!source) + { + fprintf(stderr, "Error opening %s\n", name); + return false; + } + + try + { + Luau::BytecodeBuilder bcb; + + compileOrThrow(bcb, source.value(), copts()); + + const std::string& bytecode = bcb.getBytecode(); + + std::unique_ptr globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + if (luau_load(L, name, bytecode.data(), bytecode.size(), 0) == 0) + { + summaries = Luau::CodeGen::summarizeBytecode(L, -1, nestingLimit); + return true; + } + else + { + fprintf(stderr, "Error loading bytecode %s\n", name); + return false; + } + } + catch (Luau::ParseErrors& e) + { + for (auto& error : e.getErrors()) + reportError(name, error); + return false; + } + catch (Luau::CompileError& e) + { + reportError(name, e); + return false; + } + + return true; +} + +static std::string escapeFilename(const std::string& filename) +{ + std::string escaped; + escaped.reserve(filename.size()); + + for (const char ch : filename) + { + switch (ch) + { + case '\\': + escaped.push_back('/'); + break; + case '"': + escaped.push_back('\\'); + escaped.push_back(ch); + break; + default: + escaped.push_back(ch); + } + } + + return escaped; +} + +static void serializeFunctionSummary(const FunctionBytecodeSummary& summary, FILE* fp) +{ + const unsigned nestingLimit = summary.getNestingLimit(); + const unsigned opLimit = summary.getOpLimit(); + + fprintf(fp, " {\n"); + fprintf(fp, " \"source\": \"%s\",\n", summary.getSource().c_str()); + fprintf(fp, " \"name\": \"%s\",\n", summary.getName().c_str()); + fprintf(fp, " \"line\": %d,\n", summary.getLine()); + fprintf(fp, " \"nestingLimit\": %u,\n", nestingLimit); + fprintf(fp, " \"counts\": ["); + + for (unsigned nesting = 0; nesting <= nestingLimit; ++nesting) + { + fprintf(fp, "\n ["); + + for (unsigned i = 0; i < opLimit; ++i) + { + fprintf(fp, "%d", summary.getCount(nesting, uint8_t(i))); + if (i < opLimit - 1) + fprintf(fp, ", "); + } + + fprintf(fp, "]"); + if (nesting < nestingLimit) + fprintf(fp, ","); + } + + fprintf(fp, "\n ]"); + fprintf(fp, "\n }"); +} + +static void serializeScriptSummary(const std::string& file, const std::vector& scriptSummary, FILE* fp) +{ + std::string escaped(escapeFilename(file)); + const size_t functionCount = scriptSummary.size(); + + fprintf(fp, " \"%s\": [\n", escaped.c_str()); + + for (size_t i = 0; i < functionCount; ++i) + { + serializeFunctionSummary(scriptSummary[i], fp); + fprintf(fp, i == (functionCount - 1) ? "\n" : ",\n"); + } + + fprintf(fp, " ]"); +} + +static bool serializeSummaries( + const std::vector& files, const std::vector>& scriptSummaries, const std::string& summaryFile) +{ + + FILE* fp = fopen(summaryFile.c_str(), "w"); + const size_t fileCount = files.size(); + + if (!fp) + { + fprintf(stderr, "Unable to open '%s'.\n", summaryFile.c_str()); + return false; + } + + fprintf(fp, "{\n"); + + for (size_t i = 0; i < fileCount; ++i) + { + serializeScriptSummary(files[i], scriptSummaries[i], fp); + fprintf(fp, i < (fileCount - 1) ? ",\n" : "\n"); + } + + fprintf(fp, "}"); + fclose(fp); + + return true; +} + +static int assertionHandler(const char* expr, const char* file, int line, const char* function) +{ + printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr); + return 1; +} + +int main(int argc, char** argv) +{ + Luau::assertHandler() = assertionHandler; + + setLuauFlagsDefault(); + + std::string summaryFile("bytecode-summary.json"); + unsigned nestingLimit = 0; + + if (!parseArgs(argc, argv, summaryFile)) + return 1; + + const std::vector files = getSourceFiles(argc, argv); + size_t fileCount = files.size(); + + std::vector> scriptSummaries; + scriptSummaries.reserve(fileCount); + + for (size_t i = 0; i < fileCount; ++i) + { + if (!analyzeFile(files[i].c_str(), nestingLimit, scriptSummaries[i])) + return 1; + } + + if (!serializeSummaries(files, scriptSummaries, summaryFile)) + return 1; + + fprintf(stdout, "Bytecode summary written to '%s'\n", summaryFile.c_str()); + + return 0; +} diff --git a/CLI/Compile.cpp b/CLI/Compile.cpp index 4f6b54b6..95043271 100644 --- a/CLI/Compile.cpp +++ b/CLI/Compile.cpp @@ -37,13 +37,19 @@ enum class RecordStats { None, Total, - Split + File, + Function }; struct GlobalOptions { int optimizationLevel = 1; int debugLevel = 1; + + std::string vectorLib; + std::string vectorCtor; + std::string vectorType; + } globalOptions; static Luau::CompileOptions copts() @@ -52,6 +58,11 @@ static Luau::CompileOptions copts() result.optimizationLevel = globalOptions.optimizationLevel; result.debugLevel = globalOptions.debugLevel; + // globalOptions outlive the CompileOptions, so it's safe to use string data pointers here + result.vectorLib = globalOptions.vectorLib.c_str(); + result.vectorCtor = globalOptions.vectorCtor.c_str(); + result.vectorType = globalOptions.vectorType.c_str(); + return result; } @@ -120,6 +131,7 @@ struct CompileStats { size_t lines; size_t bytecode; + size_t bytecodeInstructionCount; size_t codegen; double readTime; @@ -136,6 +148,7 @@ struct CompileStats fprintf(fp, "{\ \"lines\": %zu, \ \"bytecode\": %zu, \ +\"bytecodeInstructionCount\": %zu, \ \"codegen\": %zu, \ \"readTime\": %f, \ \"miscTime\": %f, \ @@ -153,16 +166,37 @@ struct CompileStats \"maxBlockInstructions\": %u, \ \"regAllocErrors\": %d, \ \"loweringErrors\": %d\ -}}", - lines, bytecode, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions, lowerStats.skippedFunctions, - lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt, lowerStats.blocksPostOpt, - lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors); +}, \ +\"blockLinearizationStats\": {\ +\"constPropInstructionCount\": %u, \ +\"timeSeconds\": %f\ +}", + lines, bytecode, bytecodeInstructionCount, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions, + lowerStats.skippedFunctions, lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt, + lowerStats.blocksPostOpt, lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors, + lowerStats.blockLinearizationStats.constPropInstructionCount, lowerStats.blockLinearizationStats.timeSeconds); + if (lowerStats.collectFunctionStats) + { + fprintf(fp, ", \"functions\": ["); + auto functionCount = lowerStats.functions.size(); + for (size_t i = 0; i < functionCount; ++i) + { + const Luau::CodeGen::FunctionStats& fstat = lowerStats.functions[i]; + fprintf(fp, "{\"name\": \"%s\", \"line\": %d, \"bcodeCount\": %u, \"irCount\": %u, \"asmCount\": %u}", fstat.name.c_str(), fstat.line, + fstat.bcodeCount, fstat.irCount, fstat.asmCount); + if (i < functionCount - 1) + fprintf(fp, ", "); + } + fprintf(fp, "]"); + } + fprintf(fp, "}"); } CompileStats& operator+=(const CompileStats& that) { this->lines += that.lines; this->bytecode += that.bytecode; + this->bytecodeInstructionCount += that.bytecodeInstructionCount; this->codegen += that.codegen; this->readTime += that.readTime; this->miscTime += that.miscTime; @@ -257,6 +291,7 @@ static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::A Luau::compileOrThrow(bcb, result, names, copts()); stats.bytecode += bcb.getBytecode().size(); + stats.bytecodeInstructionCount = bcb.getTotalInstructionCount(); stats.compileTime += recordDeltaTime(currts); switch (format) @@ -312,7 +347,11 @@ static void displayHelp(const char* argv0) printf(" -g: compile with debug level n (default 1, n should be between 0 and 2).\n"); printf(" --target=: compile code for specific architecture (a64, x64, a64_nf, x64_ms).\n"); printf(" --timetrace: record compiler time tracing information into trace.json\n"); - printf(" --record-stats= + + +
+
+

Fuzzer Report

+ +
+ + + + {% for crash in crashes %} +
+

{{ crash.id }}

+

Download reproducer artifact

+ {% if crash.trace() %} +
Trace
+
+
{{ crash.trace() }}
+
+ {% endif %} +
Module set
+
+
{{ crash.modules() }}
+
+
+
+ {% endfor %} +
+ + diff --git a/tools/fuzzfilter.py b/tools/fuzzfilter.py deleted file mode 100644 index 92891a0c..00000000 --- a/tools/fuzzfilter.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/python3 -# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details - -# Given a fuzzer binary and a list of crashing programs, this tool collects unique crash reasons and prints reproducers. - -import re -import sys -import subprocess - -def get_crash_reason(binary, file): - res = subprocess.run([binary, file], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) - if res.returncode == 0: - print(f"Warning: {binary} {file} returned 0") - return None - err = res.stderr.decode("utf-8") - - if (pos := err.find("ERROR: libFuzzer:")) != -1: - return err[pos:] - - print(f"Warning: {binary} {file} returned unrecognized error {err}") - return None - -def get_crash_fingerprint(reason): - # Due to ASLR addresses are different every time, so we filter them out - reason = re.sub(r"0x[0-9a-f]+", "0xXXXX", reason) - return reason - -binary = sys.argv[1] -files = sys.argv[2:] - -seen = set() - -for index, file in enumerate(files): - reason = get_crash_reason(binary, file) - if reason is None: - continue - fingerprint = get_crash_fingerprint(reason) - if fingerprint in seen: - # print a spinning ASCII wheel to indicate that we're making progress - print("-\|/"[index % 4] + "\r", end="") - continue - seen.add(fingerprint) - print(f"Reproducer: {binary} {file}") - print(f"Crash reason: {reason}") - print() - -print(f"Total unique crash reasons: {len(seen)}") \ No newline at end of file diff --git a/stats/compiler-stats.py b/tools/heuristicstat.py similarity index 100% rename from stats/compiler-stats.py rename to tools/heuristicstat.py diff --git a/tools/lldb_formatters.py b/tools/lldb_formatters.py index 661b20fc..30654af3 100644 --- a/tools/lldb_formatters.py +++ b/tools/lldb_formatters.py @@ -1,7 +1,11 @@ # This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +import lldb + # HACK: LLDB's python API doesn't afford anything helpful for getting at variadic template parameters. # We're forced to resort to parsing names as strings. + + def templateParams(s): depth = 0 start = s.find("<") + 1 @@ -172,24 +176,22 @@ class DenseHashTableSyntheticChildrenProvider: class DenseHashMapSyntheticChildrenProvider: fixed_names = ["count", "capacity"] + max_expand_children = 100 + max_expand_capacity = 1000 def __init__(self, valobj, internal_dict): self.valobj = valobj - self.values = [] self.count = 0 + self.capacity = 0 def num_children(self): - return self.count + len(self.fixed_names) + return min(self.max_expand_children, self.count) + len(self.fixed_names) def get_child_index(self, name): try: if name in self.fixed_names: return self.fixed_names.index(name) - for index, (key, _) in enumerate(self.values): - if key == name: - return index + len(self.fixed_names) - return -1 except Exception as e: print("get_child_index exception", e, name) @@ -206,47 +208,51 @@ class DenseHashMapSyntheticChildrenProvider: else: index -= len(self.fixed_names) - pair = self.items[index] - key = pair["key"] - value = pair["value"] + empty_key_valobj = self.valobj.GetValueForExpressionPath( + f".impl.empty_key") + key_type = empty_key_valobj.GetType().GetCanonicalType().GetName() + skipped = 0 - return self.valobj.CreateValueFromData( - f"[{key}]", - value.data, - value.GetType(), - ) + for slot in range(0, min(self.max_expand_capacity, self.capacity)): + slot_pair = self.valobj.GetValueForExpressionPath( + f".impl.data[{slot}]") + slot_key_valobj = slot_pair.GetChildMemberWithName("first") + + eq_test_valobj = self.valobj.EvaluateExpression( + f"*(reinterpret_cast({empty_key_valobj.AddressOf().GetValueAsUnsigned()})) == *(reinterpret_cast({slot_key_valobj.AddressOf().GetValueAsUnsigned()}))") + if eq_test_valobj.GetValue() == "true": + continue + + # Skip over previous occupied slots. + if index > skipped: + skipped += 1 + continue + + slot_key = slot_key_valobj.GetSummary() + if slot_key is None: + slot_key = slot_key_valobj.GetValue() + + if slot_key is None: + slot_key = slot_key_valobj.GetValueAsSigned() + + if slot_key is None: + slot_key = slot_key_valobj.GetValueAsUnsigned() + + if slot_key is None: + slot_key = str(index) + + slot_value_valobj = slot_pair.GetChildMemberWithName("second") + return self.valobj.CreateValueFromData(f"[{slot_key}]", slot_value_valobj.GetData(), slot_value_valobj.GetType()) except Exception as e: print("get_child_at_index error", e, index) def update(self): try: - capacity = self.valobj.GetChildMemberWithName("impl").GetChildMemberWithName( - "capacity" - ).GetValueAsUnsigned() - - self.items = [] - for index in range(0, capacity): - child_pair = self.valobj.GetValueForExpressionPath( - f".impl.data[{index}]") - child_key_valobj = child_pair.GetChildMemberWithName("first") - - if child_key_valobj.TypeIsPointerType() and child_key_valobj.GetValueAsUnsigned() == 0: - continue - - child_key = child_key_valobj.GetValue() - - if child_key is None: - child_key = child_key_valobj.GetSummary() - - if child_key is None: - child_key = f"<{index} ({child_key_valobj.GetTypeName()})>" - - child_value = child_pair.GetChildMemberWithName("second") - self.items.append({"key": child_key, "value": child_value}) - - self.count = len(self.items) - + self.capacity = self.count = self.valobj.GetValueForExpressionPath( + ".impl.capacity").GetValueAsUnsigned() + self.count = self.valobj.GetValueForExpressionPath( + ".impl.count").GetValueAsUnsigned() except Exception as e: print("update error", e) @@ -256,23 +262,22 @@ class DenseHashMapSyntheticChildrenProvider: class DenseHashSetSyntheticChildrenProvider: fixed_names = ["count", "capacity"] + max_expand_children = 100 + max_expand_capacity = 1000 def __init__(self, valobj, internal_dict): self.valobj = valobj - self.values = [] self.count = 0 + self.capacity = 0 def num_children(self): - return self.count + len(self.fixed_names) + return min(self.max_expand_children, self.count) + len(self.fixed_names) def get_child_index(self, name): try: if name in self.fixed_names: return self.fixed_names.index(name) - if name.startswith("[") and name.endswith("]"): - return int(name[1:-1]) + len(self.fixed_names) - return -1 except Exception as e: print("get_child_index exception", e, name) @@ -289,35 +294,36 @@ class DenseHashSetSyntheticChildrenProvider: else: index -= len(self.fixed_names) - value = self.items[index] + empty_key_valobj = self.valobj.GetValueForExpressionPath( + f".impl.empty_key") + key_type = empty_key_valobj.GetType().GetCanonicalType().GetName() + skipped = 0 - return self.valobj.CreateValueFromData( - f"[{index}]", - value.data, - value.GetType(), - ) + for slot in range(0, min(self.max_expand_capacity, self.capacity)): + slot_valobj = self.valobj.GetValueForExpressionPath( + f".impl.data[{slot}]") + + eq_test_valobj = self.valobj.EvaluateExpression( + f"*(reinterpret_cast({empty_key_valobj.AddressOf().GetValueAsUnsigned()})) == *(reinterpret_cast({slot_valobj.AddressOf().GetValueAsUnsigned()}))") + if eq_test_valobj.GetValue() == "true": + continue + + # Skip over previous occupied slots. + if index > skipped: + skipped += 1 + continue + + return self.valobj.CreateValueFromData(f"[{index}]", slot_valobj.GetData(), slot_valobj.GetType()) except Exception as e: print("get_child_at_index error", e, index) def update(self): try: - capacity = self.valobj.GetChildMemberWithName("impl").GetChildMemberWithName( - "capacity" - ).GetValueAsUnsigned() - - self.items = [] - for index in range(0, capacity): - child_value = self.valobj.GetValueForExpressionPath( - f".impl.data[{index}]") - - if child_value.TypeIsPointerType() and child_value.GetValueAsUnsigned() == 0: - continue - - self.items.append(child_value) - - self.count = len(self.items) - + self.capacity = self.count = self.valobj.GetValueForExpressionPath( + ".impl.capacity").GetValueAsUnsigned() + self.count = self.valobj.GetValueForExpressionPath( + ".impl.count").GetValueAsUnsigned() except Exception as e: print("update error", e) diff --git a/tools/test_dcr.py b/tools/test_dcr.py index 30f8a310..3598b02c 100644 --- a/tools/test_dcr.py +++ b/tools/test_dcr.py @@ -113,6 +113,12 @@ def main(): action="store_true", help="Run the tests with read-write properties enabled.", ) + parser.add_argument( + "--ts", + dest="suite", + action="store", + help="Only run a specific suite." + ) parser.add_argument("--randomize", action="store_true", help="Pick a random seed") @@ -139,6 +145,9 @@ def main(): elif args.randomize: commandLine.append("--randomize") + if args.suite: + commandLine.append(f'--ts={args.suite}') + print_stderr(">", " ".join(commandLine)) p = sp.Popen( @@ -146,6 +155,8 @@ def main(): stdout=sp.PIPE, ) + assert p.stdout + handler = Handler(failList) if args.dump: