diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 9d26186e..deb8be86 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -13,12 +13,9 @@ on: jobs: callgrind: - name: callgrind ${{ matrix.compiler }} strategy: - fail-fast: false matrix: os: [ubuntu-22.04] - compiler: [g++] benchResultsRepo: - { name: "luau-lang/benchmark-data", branch: "main" } @@ -31,8 +28,19 @@ jobs: run: | sudo apt-get install valgrind - - name: Build Luau - run: CXX=${{ matrix.compiler }} make config=release CALLGRIND=1 luau luau-analyze + - name: Build Luau (gcc) + run: | + CXX=g++ make config=profile luau + cp luau luau-gcc + + - name: Build Luau (clang) + run: | + make config=profile clean + CXX=clang++ make config=profile luau luau-analyze + + - name: Run benchmark (bench-gcc) + run: | + python bench/bench.py --callgrind --vm "./luau-gcc -O2" | tee -a bench-gcc-output.txt - name: Run benchmark (bench) run: | @@ -71,11 +79,19 @@ jobs: - name: Store results (bench) uses: Roblox/rhysd-github-action-benchmark@v-luau with: - name: callgrind ${{ matrix.compiler }} + name: callgrind clang tool: "benchmarkluau" output-file-path: ./bench-output.txt external-data-json-path: ./gh-pages/bench.json + - name: Store results (bench-gcc) + uses: Roblox/rhysd-github-action-benchmark@v-luau + with: + name: callgrind gcc + tool: "benchmarkluau" + output-file-path: ./bench-gcc-output.txt + external-data-json-path: ./gh-pages/bench-gcc.json + - name: Store results (analyze) uses: Roblox/rhysd-github-action-benchmark@v-luau with: diff --git a/Analysis/include/Luau/Anyification.h b/Analysis/include/Luau/Anyification.h new file mode 100644 index 00000000..ee8d6689 --- /dev/null +++ b/Analysis/include/Luau/Anyification.h @@ -0,0 +1,38 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#pragma once + +#include "Luau/NotNull.h" +#include "Luau/Substitution.h" +#include "Luau/TypeVar.h" + +#include + +namespace Luau +{ + +struct TypeArena; +struct Scope; +struct InternalErrorReporter; +using ScopePtr = std::shared_ptr; + +// A substitution which replaces free types by any +struct Anyification : Substitution +{ + Anyification(TypeArena* arena, const ScopePtr& scope, InternalErrorReporter* iceHandler, TypeId anyType, TypePackId anyTypePack); + NotNull scope; + InternalErrorReporter* iceHandler; + + TypeId anyType; + TypePackId anyTypePack; + bool normalizationTooComplex = false; + bool isDirty(TypeId ty) override; + bool isDirty(TypePackId tp) override; + TypeId clean(TypeId ty) override; + TypePackId clean(TypePackId tp) override; + + bool ignoreChildren(TypeId ty) override; + bool ignoreChildren(TypePackId ty) override; +}; + +} \ No newline at end of file diff --git a/Analysis/include/Luau/Autocomplete.h b/Analysis/include/Luau/Autocomplete.h index 5e8d6605..f40f8b49 100644 --- a/Analysis/include/Luau/Autocomplete.h +++ b/Analysis/include/Luau/Autocomplete.h @@ -19,6 +19,17 @@ struct TypeChecker; using ModulePtr = std::shared_ptr; +enum class AutocompleteContext +{ + Unknown, + Expression, + Statement, + Property, + Type, + Keyword, + String, +}; + enum class AutocompleteEntryKind { Property, @@ -66,11 +77,13 @@ struct AutocompleteResult { AutocompleteEntryMap entryMap; std::vector ancestry; + AutocompleteContext context = AutocompleteContext::Unknown; AutocompleteResult() = default; - AutocompleteResult(AutocompleteEntryMap entryMap, std::vector ancestry) + AutocompleteResult(AutocompleteEntryMap entryMap, std::vector ancestry, AutocompleteContext context) : entryMap(std::move(entryMap)) , ancestry(std::move(ancestry)) + , context(context) { } }; diff --git a/Analysis/include/Luau/Clone.h b/Analysis/include/Luau/Clone.h index 548a58f5..217e1cc3 100644 --- a/Analysis/include/Luau/Clone.h +++ b/Analysis/include/Luau/Clone.h @@ -25,6 +25,6 @@ TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState); TypeId clone(TypeId tp, TypeArena& dest, CloneState& cloneState); TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState); -TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log); +TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool alwaysClone = false); } // namespace Luau diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index 5b737146..ce90f2c5 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -40,7 +40,6 @@ struct GeneralizationConstraint { TypeId generalizedType; TypeId sourceType; - Scope* scope; }; // subType ~ inst superType @@ -85,13 +84,14 @@ using ConstraintPtr = std::unique_ptr; struct Constraint { - explicit Constraint(ConstraintV&& c); + Constraint(ConstraintV&& c, NotNull scope); Constraint(const Constraint&) = delete; Constraint& operator=(const Constraint&) = delete; ConstraintV c; std::vector> dependencies; + NotNull scope; }; inline Constraint& asMutable(const Constraint& c) diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGraphBuilder.h index 69f35d46..0e41e1ee 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGraphBuilder.h @@ -28,6 +28,7 @@ struct ConstraintGraphBuilder std::vector> scopes; ModuleName moduleName; + ModulePtr module; SingletonTypes& singletonTypes; const NotNull arena; // The root scope of the module we're generating constraints for. @@ -53,9 +54,9 @@ struct ConstraintGraphBuilder // Occasionally constraint generation needs to produce an ICE. const NotNull ice; - NotNull globalScope; + ScopePtr globalScope; - ConstraintGraphBuilder(const ModuleName& moduleName, TypeArena* arena, NotNull ice, NotNull globalScope); + ConstraintGraphBuilder(const ModuleName& moduleName, ModulePtr module, TypeArena* arena, NotNull ice, const ScopePtr& globalScope); /** * Fabricates a new free type belonging to a given scope. @@ -71,10 +72,10 @@ struct ConstraintGraphBuilder /** * Fabricates a scope that is a child of another scope. - * @param location the lexical extent of the scope in the source code. + * @param node the lexical node that the scope belongs to. * @param parent the parent scope of the new scope. Must not be null. */ - ScopePtr childScope(Location location, const ScopePtr& parent); + ScopePtr childScope(AstNode* node, const ScopePtr& parent); /** * Adds a new constraint with no dependencies to a given scope. @@ -103,10 +104,13 @@ struct ConstraintGraphBuilder void visit(const ScopePtr& scope, AstStatBlock* block); void visit(const ScopePtr& scope, AstStatLocal* local); void visit(const ScopePtr& scope, AstStatFor* for_); + void visit(const ScopePtr& scope, AstStatWhile* while_); + void visit(const ScopePtr& scope, AstStatRepeat* repeat); void visit(const ScopePtr& scope, AstStatLocalFunction* function); void visit(const ScopePtr& scope, AstStatFunction* function); void visit(const ScopePtr& scope, AstStatReturn* ret); void visit(const ScopePtr& scope, AstStatAssign* assign); + void visit(const ScopePtr& scope, AstStatCompoundAssign* assign); void visit(const ScopePtr& scope, AstStatIf* ifStatement); void visit(const ScopePtr& scope, AstStatTypeAlias* alias); void visit(const ScopePtr& scope, AstStatDeclareGlobal* declareGlobal); @@ -131,6 +135,8 @@ struct ConstraintGraphBuilder TypeId check(const ScopePtr& scope, AstExprIndexExpr* indexExpr); TypeId check(const ScopePtr& scope, AstExprUnary* unary); TypeId check(const ScopePtr& scope, AstExprBinary* binary); + TypeId check(const ScopePtr& scope, AstExprIfElse* ifElse); + TypeId check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert); struct FunctionSignature { diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index 9cc0e4cb..661d120e 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -60,6 +60,9 @@ struct ConstraintSolver // Memoized instantiations of type aliases. DenseHashMap instantiatedAliases{{}}; + // Recorded errors that take place within the solver. + ErrorVec errors; + ConstraintSolverLogger logger; explicit ConstraintSolver(TypeArena* arena, NotNull rootScope); @@ -115,7 +118,7 @@ struct ConstraintSolver * @param subType the sub-type to unify. * @param superType the super-type to unify. */ - void unify(TypeId subType, TypeId superType); + void unify(TypeId subType, TypeId superType, NotNull scope); /** * Creates a new Unifier and performs a single unification operation. Commits @@ -123,13 +126,15 @@ struct ConstraintSolver * @param subPack the sub-type pack to unify. * @param superPack the super-type pack to unify. */ - void unify(TypePackId subPack, TypePackId superPack); + void unify(TypePackId subPack, TypePackId superPack, NotNull scope); /** Pushes a new solver constraint to the solver. * @param cv the body of the constraint. **/ - void pushConstraint(ConstraintV cv); + void pushConstraint(ConstraintV cv, NotNull scope); + void reportError(TypeErrorData&& data, const Location& location); + void reportError(TypeError e); private: /** * Marks a constraint as being blocked on a type or type pack. The constraint diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index 9b8ec19e..82df493a 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -154,7 +154,7 @@ struct Frontend LoadDefinitionFileResult loadDefinitionFile(std::string_view source, const std::string& packageName); - NotNull getGlobalScope(); + ScopePtr getGlobalScope(); private: ModulePtr check(const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope); diff --git a/Analysis/include/Luau/Linter.h b/Analysis/include/Luau/Linter.h index 2cd91d54..0e3d9880 100644 --- a/Analysis/include/Luau/Linter.h +++ b/Analysis/include/Luau/Linter.h @@ -53,6 +53,7 @@ struct LintWarning Code_MisleadingAndOr = 25, Code_CommentDirective = 26, Code_IntegerParsing = 27, + Code_ComparisonPrecedence = 28, Code__Count }; diff --git a/Analysis/include/Luau/Module.h b/Analysis/include/Luau/Module.h index 6f4c6098..bec51b81 100644 --- a/Analysis/include/Luau/Module.h +++ b/Analysis/include/Luau/Module.h @@ -77,6 +77,8 @@ struct Module DenseHashMap astOverloadResolvedTypes{nullptr}; DenseHashMap astResolvedTypes{nullptr}; DenseHashMap astResolvedTypePacks{nullptr}; + // Map AST nodes to the scope they create. Cannot be NotNull because we need a sentinel value for the map. + DenseHashMap astScopes{nullptr}; std::unordered_map declaredGlobals; ErrorVec errors; diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index f5fd9886..78b241e4 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -1,20 +1,28 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/Substitution.h" -#include "Luau/TypeVar.h" #include "Luau/Module.h" +#include "Luau/NotNull.h" +#include "Luau/TypeVar.h" + +#include namespace Luau { struct InternalErrorReporter; +struct Module; +struct Scope; -bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice); -bool isSubtype(TypePackId subTy, TypePackId superTy, InternalErrorReporter& ice); +using ModulePtr = std::shared_ptr; -std::pair normalize(TypeId ty, TypeArena& arena, InternalErrorReporter& ice); +bool isSubtype(TypeId subTy, TypeId superTy, NotNull scope, InternalErrorReporter& ice); +bool isSubtype(TypePackId subTy, TypePackId superTy, NotNull scope, InternalErrorReporter& ice); + +std::pair normalize(TypeId ty, NotNull scope, TypeArena& arena, InternalErrorReporter& ice); +std::pair normalize(TypeId ty, NotNull module, InternalErrorReporter& ice); std::pair normalize(TypeId ty, const ModulePtr& module, InternalErrorReporter& ice); -std::pair normalize(TypePackId ty, TypeArena& arena, InternalErrorReporter& ice); +std::pair normalize(TypePackId ty, NotNull scope, TypeArena& arena, InternalErrorReporter& ice); +std::pair normalize(TypePackId ty, NotNull module, InternalErrorReporter& ice); std::pair normalize(TypePackId ty, const ModulePtr& module, InternalErrorReporter& ice); } // namespace Luau diff --git a/Analysis/include/Luau/Scope.h b/Analysis/include/Luau/Scope.h index 55ca54c6..dc123335 100644 --- a/Analysis/include/Luau/Scope.h +++ b/Analysis/include/Luau/Scope.h @@ -36,8 +36,6 @@ struct Scope // All the children of this scope. std::vector> children; std::unordered_map bindings; - std::unordered_map typeBindings; - std::unordered_map typePackBindings; TypePackId returnType; std::optional varargPack; // All constraints belonging to this scope. @@ -52,8 +50,6 @@ struct Scope std::unordered_map> importedTypeBindings; std::optional lookup(Symbol sym); - std::optional lookupTypeBinding(const Name& name); - std::optional lookupTypePackBinding(const Name& name); std::optional lookupType(const Name& name); std::optional lookupImportedType(const Name& moduleAlias, const Name& name); diff --git a/Analysis/include/Luau/TypeInfer.h b/Analysis/include/Luau/TypeInfer.h index a255be4d..e253eddf 100644 --- a/Analysis/include/Luau/TypeInfer.h +++ b/Analysis/include/Luau/TypeInfer.h @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/Anyification.h" #include "Luau/Predicate.h" #include "Luau/Error.h" #include "Luau/Module.h" @@ -16,6 +17,8 @@ #include #include +LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution) + namespace Luau { @@ -34,37 +37,6 @@ const AstStat* getFallthrough(const AstStat* node); struct UnifierOptions; struct Unifier; -// A substitution which replaces free types by any -struct Anyification : Substitution -{ - Anyification(TypeArena* arena, InternalErrorReporter* iceHandler, TypeId anyType, TypePackId anyTypePack) - : Substitution(TxnLog::empty(), arena) - , iceHandler(iceHandler) - , anyType(anyType) - , anyTypePack(anyTypePack) - { - } - - InternalErrorReporter* iceHandler; - - TypeId anyType; - TypePackId anyTypePack; - bool normalizationTooComplex = false; - bool isDirty(TypeId ty) override; - bool isDirty(TypePackId tp) override; - TypeId clean(TypeId ty) override; - TypePackId clean(TypePackId tp) override; - - bool ignoreChildren(TypeId ty) override - { - return ty->persistent; - } - bool ignoreChildren(TypePackId ty) override - { - return ty->persistent; - } -}; - struct GenericTypeDefinitions { std::vector genericTypes; @@ -192,32 +164,32 @@ struct TypeChecker /** Attempt to unify the types. * Treat any failures as type errors in the final typecheck report. */ - bool unify(TypeId subTy, TypeId superTy, const Location& location); - bool unify(TypeId subTy, TypeId superTy, const Location& location, const UnifierOptions& options); - bool unify(TypePackId subTy, TypePackId superTy, const Location& location, CountMismatch::Context ctx = CountMismatch::Context::Arg); + bool unify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location); + bool unify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location, const UnifierOptions& options); + bool unify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location, CountMismatch::Context ctx = CountMismatch::Context::Arg); /** Attempt to unify the types. * If this fails, and the subTy type can be instantiated, do so and try unification again. */ - bool unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, const Location& location); - void unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, Unifier& state); + bool unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location); + void unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, Unifier& state); /** Attempt to unify. * If there are errors, undo everything and return the errors. * If there are no errors, commit and return an empty error vector. */ template - ErrorVec tryUnify_(Id subTy, Id superTy, const Location& location); - ErrorVec tryUnify(TypeId subTy, TypeId superTy, const Location& location); - ErrorVec tryUnify(TypePackId subTy, TypePackId superTy, const Location& location); + ErrorVec tryUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location); + ErrorVec tryUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location); + ErrorVec tryUnify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location); // Test whether the two type vars unify. Never commits the result. template - ErrorVec canUnify_(Id subTy, Id superTy, const Location& location); - ErrorVec canUnify(TypeId subTy, TypeId superTy, const Location& location); - ErrorVec canUnify(TypePackId subTy, TypePackId superTy, const Location& location); + ErrorVec canUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location); + ErrorVec canUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location); + ErrorVec canUnify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location); - void unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const Location& location); + void unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const ScopePtr& scope, const Location& location); std::optional findMetatableEntry(TypeId type, std::string entry, const Location& location, bool addErrors); std::optional findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location, bool addErrors); @@ -286,7 +258,7 @@ private: void reportErrorCodeTooComplex(const Location& location); private: - Unifier mkUnifier(const Location& location); + Unifier mkUnifier(const ScopePtr& scope, const Location& location); // These functions are only safe to call when we are in the process of typechecking a module. @@ -308,7 +280,7 @@ public: std::pair, bool> pickTypesFromSense(TypeId type, bool sense); private: - TypeId unionOfTypes(TypeId a, TypeId b, const Location& location, bool unifyFreeTypes = true); + TypeId unionOfTypes(TypeId a, TypeId b, const ScopePtr& scope, const Location& location, bool unifyFreeTypes = true); // ex // TypeId id = addType(FreeTypeVar()); diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 42c1bc0b..0aff5a7d 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -13,7 +13,10 @@ namespace Luau using ScopePtr = std::shared_ptr; -std::optional findMetatableEntry(ErrorVec& errors, TypeId type, std::string entry, Location location); -std::optional findTablePropertyRespectingMeta(ErrorVec& errors, TypeId ty, Name name, Location location); +std::optional findMetatableEntry(ErrorVec& errors, TypeId type, const std::string& entry, Location location); +std::optional findTablePropertyRespectingMeta(ErrorVec& errors, TypeId ty, const std::string& name, Location location); +std::optional getIndexTypeFromType( + const ScopePtr& scope, ErrorVec& errors, TypeArena* arena, TypeId type, const std::string& prop, const Location& location, bool addErrors, + InternalErrorReporter& handle); } // namespace Luau diff --git a/Analysis/include/Luau/TypeVar.h b/Analysis/include/Luau/TypeVar.h index 6a13b11c..35b37394 100644 --- a/Analysis/include/Luau/TypeVar.h +++ b/Analysis/include/Luau/TypeVar.h @@ -717,6 +717,7 @@ struct TypeIterator stack.push_front({t, 0}); seen.insert(t); + descend(); } TypeIterator& operator++() @@ -748,17 +749,19 @@ struct TypeIterator const TypeId& operator*() { - LUAU_ASSERT(!stack.empty()); - descend(); + LUAU_ASSERT(!stack.empty()); + auto [t, currentIndex] = stack.front(); LUAU_ASSERT(t); + const std::vector& types = getTypes(t); LUAU_ASSERT(currentIndex < types.size()); const TypeId& ty = types[currentIndex]; LUAU_ASSERT(!get(follow(ty))); + return ty; } diff --git a/Analysis/include/Luau/TypedAllocator.h b/Analysis/include/Luau/TypedAllocator.h index f67e3d8e..a5dd17b6 100644 --- a/Analysis/include/Luau/TypedAllocator.h +++ b/Analysis/include/Luau/TypedAllocator.h @@ -10,7 +10,7 @@ namespace Luau { void* pagedAllocate(size_t size); -void pagedDeallocate(void* ptr); +void pagedDeallocate(void* ptr, size_t size); void pagedFreeze(void* ptr, size_t size); void pagedUnfreeze(void* ptr, size_t size); @@ -113,7 +113,7 @@ private: for (size_t i = 0; i < blockSize; ++i) block[i].~T(); - pagedDeallocate(block); + pagedDeallocate(block, kBlockSizeBytes); } stuff.clear(); diff --git a/Analysis/include/Luau/Unifier.h b/Analysis/include/Luau/Unifier.h index f460dc87..312b0584 100644 --- a/Analysis/include/Luau/Unifier.h +++ b/Analysis/include/Luau/Unifier.h @@ -3,9 +3,10 @@ #include "Luau/Error.h" #include "Luau/Location.h" +#include "Luau/Scope.h" #include "Luau/TxnLog.h" -#include "Luau/TypeInfer.h" #include "Luau/TypeArena.h" +#include "Luau/TypeInfer.h" #include "Luau/UnifierSharedState.h" #include @@ -48,6 +49,7 @@ struct Unifier TypeArena* const types; Mode mode; + NotNull scope; // const Scope maybe TxnLog log; ErrorVec errors; Location location; @@ -57,7 +59,7 @@ struct Unifier UnifierSharedState& sharedState; - Unifier(TypeArena* types, Mode mode, const Location& location, Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog = nullptr); + Unifier(TypeArena* types, Mode mode, NotNull scope, const Location& location, Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog = nullptr); // Test whether the two type vars unify. Never commits the result. ErrorVec canUnify(TypeId subTy, TypeId superTy); @@ -109,11 +111,11 @@ private: public: void unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel); - // Report an "infinite type error" if the type "needle" already occurs within "haystack" - void occursCheck(TypeId needle, TypeId haystack); - void occursCheck(DenseHashSet& seen, TypeId needle, TypeId haystack); - void occursCheck(TypePackId needle, TypePackId haystack); - void occursCheck(DenseHashSet& seen, TypePackId needle, TypePackId haystack); + // Returns true if the type "needle" already occurs within "haystack" and reports an "infinite type error" + bool occursCheck(TypeId needle, TypeId haystack); + bool occursCheck(DenseHashSet& seen, TypeId needle, TypeId haystack); + bool occursCheck(TypePackId needle, TypePackId haystack); + bool occursCheck(DenseHashSet& seen, TypePackId needle, TypePackId haystack); Unifier makeChildUnifier(); diff --git a/Analysis/include/Luau/VisitTypeVar.h b/Analysis/include/Luau/VisitTypeVar.h index 7e5d71d6..9d7fa9fe 100644 --- a/Analysis/include/Luau/VisitTypeVar.h +++ b/Analysis/include/Luau/VisitTypeVar.h @@ -69,12 +69,14 @@ struct GenericTypeVarVisitor using Set = S; Set seen; + bool skipBoundTypes = false; int recursionCounter = 0; GenericTypeVarVisitor() = default; - explicit GenericTypeVarVisitor(Set seen) + explicit GenericTypeVarVisitor(Set seen, bool skipBoundTypes = false) : seen(std::move(seen)) + , skipBoundTypes(skipBoundTypes) { } @@ -199,7 +201,9 @@ struct GenericTypeVarVisitor if (auto btv = get(ty)) { - if (visit(ty, *btv)) + if (skipBoundTypes) + traverse(btv->boundTo); + else if (visit(ty, *btv)) traverse(btv->boundTo); } else if (auto ftv = get(ty)) @@ -229,7 +233,11 @@ struct GenericTypeVarVisitor else if (auto ttv = get(ty)) { // Some visitors want to see bound tables, that's why we traverse the original type - if (visit(ty, *ttv)) + if (skipBoundTypes && ttv->boundTo) + { + traverse(*ttv->boundTo); + } + else if (visit(ty, *ttv)) { if (ttv->boundTo) { @@ -394,13 +402,17 @@ struct GenericTypeVarVisitor */ struct TypeVarVisitor : GenericTypeVarVisitor> { + explicit TypeVarVisitor(bool skipBoundTypes = false) + : GenericTypeVarVisitor{{}, skipBoundTypes} + { + } }; /// Visit each type under a given type. Each type will only be checked once even if there are multiple paths to it. struct TypeVarOnceVisitor : GenericTypeVarVisitor> { - TypeVarOnceVisitor() - : GenericTypeVarVisitor{DenseHashSet{nullptr}} + explicit TypeVarOnceVisitor(bool skipBoundTypes = false) + : GenericTypeVarVisitor{DenseHashSet{nullptr}, skipBoundTypes} { } }; diff --git a/Analysis/src/Anyification.cpp b/Analysis/src/Anyification.cpp new file mode 100644 index 00000000..b6e58009 --- /dev/null +++ b/Analysis/src/Anyification.cpp @@ -0,0 +1,96 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/Anyification.h" + +#include "Luau/Common.h" +#include "Luau/Normalize.h" +#include "Luau/TxnLog.h" + +LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution) + +namespace Luau +{ + +Anyification::Anyification(TypeArena* arena, const ScopePtr& scope, InternalErrorReporter* iceHandler, TypeId anyType, TypePackId anyTypePack) + : Substitution(TxnLog::empty(), arena) + , scope(NotNull{scope.get()}) + , iceHandler(iceHandler) + , anyType(anyType) + , anyTypePack(anyTypePack) +{ +} + +bool Anyification::isDirty(TypeId ty) +{ + if (ty->persistent) + return false; + + if (const TableTypeVar* ttv = log->getMutable(ty)) + return (ttv->state == TableState::Free || ttv->state == TableState::Unsealed); + else if (log->getMutable(ty)) + return true; + else if (get(ty)) + return true; + else + return false; +} + +bool Anyification::isDirty(TypePackId tp) +{ + if (tp->persistent) + return false; + + if (log->getMutable(tp)) + return true; + else + return false; +} + +TypeId Anyification::clean(TypeId ty) +{ + LUAU_ASSERT(isDirty(ty)); + if (const TableTypeVar* ttv = log->getMutable(ty)) + { + TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, ttv->level, TableState::Sealed}; + clone.definitionModuleName = ttv->definitionModuleName; + clone.name = ttv->name; + clone.syntheticName = ttv->syntheticName; + clone.tags = ttv->tags; + TypeId res = addType(std::move(clone)); + asMutable(res)->normal = ty->normal; + return res; + } + else if (auto ctv = get(ty)) + { + std::vector copy = ctv->parts; + for (TypeId& ty : copy) + ty = replace(ty); + TypeId res = copy.size() == 1 ? copy[0] : addType(UnionTypeVar{std::move(copy)}); + auto [t, ok] = normalize(res, scope, *arena, *iceHandler); + if (!ok) + normalizationTooComplex = true; + return t; + } + else + return anyType; +} + +TypePackId Anyification::clean(TypePackId tp) +{ + LUAU_ASSERT(isDirty(tp)); + return anyTypePack; +} + +bool Anyification::ignoreChildren(TypeId ty) +{ + if (FFlag::LuauClassTypeVarsInSubstitution && get(ty)) + return true; + + return ty->persistent; +} +bool Anyification::ignoreChildren(TypePackId ty) +{ + return ty->persistent; +} + +} diff --git a/Analysis/src/ApplyTypeFunction.cpp b/Analysis/src/ApplyTypeFunction.cpp index c6ac3e19..b293ed3d 100644 --- a/Analysis/src/ApplyTypeFunction.cpp +++ b/Analysis/src/ApplyTypeFunction.cpp @@ -2,6 +2,8 @@ #include "Luau/ApplyTypeFunction.h" +LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution) + namespace Luau { @@ -31,6 +33,8 @@ bool ApplyTypeFunction::ignoreChildren(TypeId ty) { if (get(ty)) return true; + else if (FFlag::LuauClassTypeVarsInSubstitution && get(ty)) + return true; else return false; } diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index a57a789f..5c484899 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -14,6 +14,8 @@ LUAU_FASTFLAG(LuauSelfCallAutocompleteFix3) +LUAU_FASTFLAGVARIABLE(LuauAutocompleteFixGlobalOrder, false) + static const std::unordered_set kStatementStartingKeywords = { "while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; @@ -135,11 +137,11 @@ static std::optional findExpectedTypeAt(const Module& module, AstNode* n return *it; } -static bool checkTypeMatch(TypeArena* typeArena, TypeId subTy, TypeId superTy) +static bool checkTypeMatch(TypeId subTy, TypeId superTy, NotNull scope, TypeArena* typeArena) { InternalErrorReporter iceReporter; UnifierSharedState unifierState(&iceReporter); - Unifier unifier(typeArena, Mode::Strict, Location(), Variance::Covariant, unifierState); + Unifier unifier(typeArena, Mode::Strict, scope, Location(), Variance::Covariant, unifierState); return unifier.canUnify(subTy, superTy).empty(); } @@ -148,12 +150,14 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ { ty = follow(ty); - auto canUnify = [&typeArena](TypeId subTy, TypeId superTy) { + NotNull moduleScope{module.getModuleScope().get()}; + + auto canUnify = [&typeArena, moduleScope](TypeId subTy, TypeId superTy) { LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix3); InternalErrorReporter iceReporter; UnifierSharedState unifierState(&iceReporter); - Unifier unifier(typeArena, Mode::Strict, Location(), Variance::Covariant, unifierState); + Unifier unifier(typeArena, Mode::Strict, moduleScope, Location(), Variance::Covariant, unifierState); unifier.tryUnify(subTy, superTy); bool ok = unifier.errors.empty(); @@ -167,11 +171,11 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ TypeId expectedType = follow(*typeAtPosition); - auto checkFunctionType = [typeArena, &canUnify, &expectedType](const FunctionTypeVar* ftv) { + auto checkFunctionType = [typeArena, moduleScope, &canUnify, &expectedType](const FunctionTypeVar* ftv) { if (FFlag::LuauSelfCallAutocompleteFix3) { if (std::optional firstRetTy = first(ftv->retTypes)) - return checkTypeMatch(typeArena, *firstRetTy, expectedType); + return checkTypeMatch(*firstRetTy, expectedType, moduleScope, typeArena); return false; } @@ -210,7 +214,7 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ } if (FFlag::LuauSelfCallAutocompleteFix3) - return checkTypeMatch(typeArena, ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None; + return checkTypeMatch(ty, expectedType, NotNull{module.getModuleScope().get()}, typeArena) ? TypeCorrectKind::Correct : TypeCorrectKind::None; else return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None; } @@ -268,7 +272,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId return colonIndex; } }; - auto isWrongIndexer = [typeArena, rootTy, indexType](Luau::TypeId type) { + auto isWrongIndexer = [typeArena, &module, rootTy, indexType](Luau::TypeId type) { LUAU_ASSERT(FFlag::LuauSelfCallAutocompleteFix3); if (indexType == PropIndexType::Key) @@ -276,7 +280,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId bool calledWithSelf = indexType == PropIndexType::Colon; - auto isCompatibleCall = [typeArena, rootTy, calledWithSelf](const FunctionTypeVar* ftv) { + auto isCompatibleCall = [typeArena, &module, rootTy, calledWithSelf](const FunctionTypeVar* ftv) { // Strong match with definition is a success if (calledWithSelf == ftv->hasSelf) return true; @@ -289,7 +293,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId // When called with '.', but declared with 'self', it is considered invalid if first argument is compatible if (std::optional firstArgTy = first(ftv->argTypes)) { - if (checkTypeMatch(typeArena, rootTy, *firstArgTy)) + if (checkTypeMatch(rootTy, *firstArgTy, NotNull{module.getModuleScope().get()}, typeArena)) return calledWithSelf; } @@ -1073,10 +1077,21 @@ T* extractStat(const std::vector& ancestry) return nullptr; } -static bool isBindingLegalAtCurrentPosition(const Binding& binding, Position pos) +static bool isBindingLegalAtCurrentPosition(const Symbol& symbol, const Binding& binding, Position pos) { - // Default Location used for global bindings, which are always legal. - return binding.location == Location() || binding.location.end < pos; + if (FFlag::LuauAutocompleteFixGlobalOrder) + { + if (symbol.local) + return binding.location.end < pos; + + // Builtin globals have an empty location; for defined globals, we want pos to be outside of the definition range to suggest it + return binding.location == Location() || !binding.location.containsClosed(pos); + } + else + { + // Default Location used for global bindings, which are always legal. + return binding.location == Location() || binding.location.end < pos; + } } static AutocompleteEntryMap autocompleteStatement( @@ -1097,7 +1112,7 @@ static AutocompleteEntryMap autocompleteStatement( { for (const auto& [name, binding] : scope->bindings) { - if (!isBindingLegalAtCurrentPosition(binding, position)) + if (!isBindingLegalAtCurrentPosition(name, binding, position)) continue; std::string n = toString(name); @@ -1200,7 +1215,7 @@ static bool autocompleteIfElseExpression( } } -static void autocompleteExpression(const SourceModule& sourceModule, const Module& module, const TypeChecker& typeChecker, TypeArena* typeArena, +static AutocompleteContext autocompleteExpression(const SourceModule& sourceModule, const Module& module, const TypeChecker& typeChecker, TypeArena* typeArena, const std::vector& ancestry, Position position, AutocompleteEntryMap& result) { LUAU_ASSERT(!ancestry.empty()); @@ -1213,9 +1228,9 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul autocompleteProps(module, typeArena, *it, PropIndexType::Point, ancestry, result); } else if (autocompleteIfElseExpression(node, ancestry, position, result)) - return; + return AutocompleteContext::Keyword; else if (node->is()) - return; + return AutocompleteContext::Unknown; else { // This is inefficient. :( @@ -1225,7 +1240,7 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul { for (const auto& [name, binding] : scope->bindings) { - if (!isBindingLegalAtCurrentPosition(binding, position)) + if (!isBindingLegalAtCurrentPosition(name, binding, position)) continue; if (isBeingDefined(ancestry, name)) @@ -1260,14 +1275,16 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul if (auto ty = findExpectedTypeAt(module, node, position)) autocompleteStringSingleton(*ty, true, result); } + + return AutocompleteContext::Expression; } -static AutocompleteEntryMap autocompleteExpression(const SourceModule& sourceModule, const Module& module, const TypeChecker& typeChecker, +static AutocompleteResult autocompleteExpression(const SourceModule& sourceModule, const Module& module, const TypeChecker& typeChecker, TypeArena* typeArena, const std::vector& ancestry, Position position) { AutocompleteEntryMap result; - autocompleteExpression(sourceModule, module, typeChecker, typeArena, ancestry, position, result); - return result; + AutocompleteContext context = autocompleteExpression(sourceModule, module, typeChecker, typeArena, ancestry, position, result); + return {result, ancestry, context}; } static std::optional getMethodContainingClass(const ModulePtr& module, AstExpr* funcExpr) @@ -1406,27 +1423,27 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M if (!FFlag::LuauSelfCallAutocompleteFix3 && isString(ty)) return { - autocompleteProps(*module, typeArena, typeChecker.globalScope->bindings[AstName{"string"}].typeId, indexType, ancestry), ancestry}; + autocompleteProps(*module, typeArena, typeChecker.globalScope->bindings[AstName{"string"}].typeId, indexType, ancestry), ancestry, AutocompleteContext::Property}; else - return {autocompleteProps(*module, typeArena, ty, indexType, ancestry), ancestry}; + return {autocompleteProps(*module, typeArena, ty, indexType, ancestry), ancestry, AutocompleteContext::Property}; } else if (auto typeReference = node->as()) { if (typeReference->prefix) - return {autocompleteModuleTypes(*module, position, typeReference->prefix->value), ancestry}; + return {autocompleteModuleTypes(*module, position, typeReference->prefix->value), ancestry, AutocompleteContext::Type}; else - return {autocompleteTypeNames(*module, position, ancestry), ancestry}; + return {autocompleteTypeNames(*module, position, ancestry), ancestry, AutocompleteContext::Type}; } else if (node->is()) { - return {autocompleteTypeNames(*module, position, ancestry), ancestry}; + return {autocompleteTypeNames(*module, position, ancestry), ancestry, AutocompleteContext::Type}; } else if (AstStatLocal* statLocal = node->as()) { if (statLocal->vars.size == 1 && (!statLocal->equalsSignLocation || position < statLocal->equalsSignLocation->begin)) - return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Unknown}; else if (statLocal->equalsSignLocation && position >= statLocal->equalsSignLocation->end) - return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry}; + return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position); else return {}; } @@ -1436,16 +1453,16 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M if (!statFor->hasDo || position < statFor->doLocation.begin) { if (!statFor->from->is() && !statFor->to->is() && (!statFor->step || !statFor->step->is())) - return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; if (statFor->from->location.containsClosed(position) || statFor->to->location.containsClosed(position) || (statFor->step && statFor->step->location.containsClosed(position))) - return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry}; + return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position); return {}; } - return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry}; + return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; } else if (AstStatForIn* statForIn = parent->as(); statForIn && (node->is() || isIdentifier(node))) @@ -1461,7 +1478,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M return {}; } - return {{{"in", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + return {{{"in", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; } if (!statForIn->hasDo || position <= statForIn->doLocation.begin) @@ -1470,10 +1487,10 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M AstExpr* lastExpr = statForIn->values.data[statForIn->values.size - 1]; if (lastExpr->location.containsClosed(position)) - return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry}; + return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position); if (position > lastExpr->location.end) - return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; return {}; // Not sure what this means } @@ -1483,45 +1500,45 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M // The AST looks a bit differently if the cursor is at a position where only the "do" keyword is allowed. // ex "for f in f do" if (!statForIn->hasDo) - return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; - return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry}; + return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; } else if (AstStatWhile* statWhile = parent->as(); node->is() && statWhile) { if (!statWhile->hasDo && !statWhile->condition->is() && position > statWhile->condition->location.end) - return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; if (!statWhile->hasDo || position < statWhile->doLocation.begin) - return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry}; + return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position); if (statWhile->hasDo && position > statWhile->doLocation.end) - return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry}; + return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; } else if (AstStatWhile* statWhile = extractStat(ancestry); statWhile && !statWhile->hasDo) - return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; else if (AstStatIf* statIf = node->as(); statIf && !statIf->elseLocation.has_value()) { return { - {{"else", AutocompleteEntry{AutocompleteEntryKind::Keyword}}, {"elseif", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + {{"else", AutocompleteEntry{AutocompleteEntryKind::Keyword}}, {"elseif", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; } else if (AstStatIf* statIf = parent->as(); statIf && node->is()) { if (statIf->condition->is()) - return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry}; + return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position); else if (!statIf->thenLocation || statIf->thenLocation->containsClosed(position)) - return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; } else if (AstStatIf* statIf = extractStat(ancestry); statIf && (!statIf->thenLocation || statIf->thenLocation->containsClosed(position))) - return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; + return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; else if (AstStatRepeat* statRepeat = node->as(); statRepeat && statRepeat->condition->is()) - return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry}; + return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position); else if (AstStatRepeat* statRepeat = extractStat(ancestry); statRepeat) - return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry}; + return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; else if (AstExprTable* exprTable = parent->as(); exprTable && (node->is() || node->is())) { for (const auto& [kind, key, value] : exprTable->items) @@ -1547,7 +1564,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M if (!key) autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position, result); - return {result, ancestry}; + return {result, ancestry, AutocompleteContext::Property}; } break; @@ -1555,11 +1572,11 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M } } else if (isIdentifier(node) && (parent->is() || parent->is())) - return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry}; + return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; if (std::optional ret = autocompleteStringParams(sourceModule, module, ancestry, position, callback)) { - return {*ret, ancestry}; + return {*ret, ancestry, AutocompleteContext::String}; } else if (node->is()) { @@ -1585,7 +1602,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M } } - return {result, ancestry}; + return {result, ancestry, AutocompleteContext::String}; } if (node->is()) @@ -1594,9 +1611,9 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M } if (node->asExpr()) - return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry}; + return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position); else if (node->asStat()) - return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry}; + return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; return {}; } diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 51ad61d5..2e04b527 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -7,6 +7,7 @@ #include "Luau/Unifiable.h" LUAU_FASTFLAG(DebugLuauCopyBeforeNormalizing) +LUAU_FASTFLAG(LuauClonePublicInterfaceLess) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) @@ -445,7 +446,7 @@ TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState) return result; } -TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log) +TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool alwaysClone) { ty = log->follow(ty); @@ -504,6 +505,15 @@ TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log) PendingExpansionTypeVar clone{petv->fn, petv->typeArguments, petv->packArguments}; result = dest.addType(std::move(clone)); } + else if (const ClassTypeVar* ctv = get(ty); FFlag::LuauClonePublicInterfaceLess && ctv && alwaysClone) + { + ClassTypeVar clone{ctv->name, ctv->props, ctv->parent, ctv->metatable, ctv->tags, ctv->userData, ctv->definitionModuleName}; + result = dest.addType(std::move(clone)); + } + else if (FFlag::LuauClonePublicInterfaceLess && alwaysClone) + { + result = dest.addType(*ty); + } else return result; diff --git a/Analysis/src/Constraint.cpp b/Analysis/src/Constraint.cpp index 64e3a666..d272c027 100644 --- a/Analysis/src/Constraint.cpp +++ b/Analysis/src/Constraint.cpp @@ -5,8 +5,9 @@ namespace Luau { -Constraint::Constraint(ConstraintV&& c) +Constraint::Constraint(ConstraintV&& c, NotNull scope) : c(std::move(c)) + , scope(scope) { } diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index ea7037bf..c1e54df2 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -1,6 +1,9 @@ // 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/Ast.h" +#include "Luau/Common.h" +#include "Luau/Constraint.h" #include "Luau/RecursionCounter.h" #include "Luau/ToString.h" @@ -14,8 +17,9 @@ namespace Luau const AstStat* getFallthrough(const AstStat* node); // TypeInfer.cpp ConstraintGraphBuilder::ConstraintGraphBuilder( - const ModuleName& moduleName, TypeArena* arena, NotNull ice, NotNull globalScope) + const ModuleName& moduleName, ModulePtr module, TypeArena* arena, NotNull ice, const ScopePtr& globalScope) : moduleName(moduleName) + , module(module) , singletonTypes(getSingletonTypes()) , arena(arena) , rootScope(nullptr) @@ -23,6 +27,7 @@ ConstraintGraphBuilder::ConstraintGraphBuilder( , globalScope(globalScope) { LUAU_ASSERT(arena); + LUAU_ASSERT(module); } TypeId ConstraintGraphBuilder::freshType(const ScopePtr& scope) @@ -36,20 +41,22 @@ TypePackId ConstraintGraphBuilder::freshTypePack(const ScopePtr& scope) return arena->addTypePack(TypePackVar{std::move(f)}); } -ScopePtr ConstraintGraphBuilder::childScope(Location location, const ScopePtr& parent) +ScopePtr ConstraintGraphBuilder::childScope(AstNode* node, const ScopePtr& parent) { auto scope = std::make_shared(parent); - scopes.emplace_back(location, scope); + scopes.emplace_back(node->location, scope); scope->returnType = parent->returnType; - parent->children.push_back(NotNull(scope.get())); + + parent->children.push_back(NotNull{scope.get()}); + module->astScopes[node] = scope.get(); return scope; } void ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, ConstraintV cv) { - scope->constraints.emplace_back(new Constraint{std::move(cv)}); + scope->constraints.emplace_back(new Constraint{std::move(cv), NotNull{scope.get()}}); } void ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, std::unique_ptr c) @@ -61,20 +68,21 @@ void ConstraintGraphBuilder::visit(AstStatBlock* block) { LUAU_ASSERT(scopes.empty()); LUAU_ASSERT(rootScope == nullptr); - ScopePtr scope = std::make_shared(singletonTypes.anyTypePack); + ScopePtr scope = std::make_shared(globalScope); rootScope = scope.get(); scopes.emplace_back(block->location, scope); + module->astScopes[block] = NotNull{scope.get()}; rootScope->returnType = freshTypePack(scope); prepopulateGlobalScope(scope, block); // TODO: We should share the global scope. - rootScope->typeBindings["nil"] = TypeFun{singletonTypes.nilType}; - rootScope->typeBindings["number"] = TypeFun{singletonTypes.numberType}; - rootScope->typeBindings["string"] = TypeFun{singletonTypes.stringType}; - rootScope->typeBindings["boolean"] = TypeFun{singletonTypes.booleanType}; - rootScope->typeBindings["thread"] = TypeFun{singletonTypes.threadType}; + rootScope->privateTypeBindings["nil"] = TypeFun{singletonTypes.nilType}; + rootScope->privateTypeBindings["number"] = TypeFun{singletonTypes.numberType}; + rootScope->privateTypeBindings["string"] = TypeFun{singletonTypes.stringType}; + rootScope->privateTypeBindings["boolean"] = TypeFun{singletonTypes.booleanType}; + rootScope->privateTypeBindings["thread"] = TypeFun{singletonTypes.threadType}; visitBlockWithoutChildScope(scope, block); } @@ -99,7 +107,7 @@ void ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope, { if (auto alias = stat->as()) { - if (scope->typeBindings.count(alias->name.value) != 0) + if (scope->privateTypeBindings.count(alias->name.value) != 0) { auto it = aliasDefinitionLocations.find(alias->name.value); LUAU_ASSERT(it != aliasDefinitionLocations.end()); @@ -112,7 +120,7 @@ void ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope, ScopePtr defnScope = scope; if (hasGenerics) { - defnScope = childScope(alias->location, scope); + defnScope = childScope(alias, scope); } TypeId initialType = freshType(scope); @@ -121,16 +129,16 @@ void ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope, for (const auto& [name, gen] : createGenerics(defnScope, alias->generics)) { initialFun.typeParams.push_back(gen); - defnScope->typeBindings[name] = TypeFun{gen.ty}; + defnScope->privateTypeBindings[name] = TypeFun{gen.ty}; } for (const auto& [name, genPack] : createGenericPacks(defnScope, alias->genericPacks)) { initialFun.typePackParams.push_back(genPack); - defnScope->typePackBindings[name] = genPack.tp; + defnScope->privateTypePackBindings[name] = genPack.tp; } - scope->typeBindings[alias->name.value] = std::move(initialFun); + scope->privateTypeBindings[alias->name.value] = std::move(initialFun); astTypeAliasDefiningScopes[alias] = defnScope; aliasDefinitionLocations[alias->name.value] = alias->location; } @@ -150,6 +158,10 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) visit(scope, s); else if (auto s = stat->as()) visit(scope, s); + else if (auto s = stat->as()) + visit(scope, s); + else if (auto s = stat->as()) + visit(scope, s); else if (auto f = stat->as()) visit(scope, f); else if (auto f = stat->as()) @@ -158,6 +170,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) visit(scope, r); else if (auto a = stat->as()) visit(scope, a); + else if (auto a = stat->as()) + visit(scope, a); else if (auto e = stat->as()) checkPack(scope, e->expr); else if (auto i = stat->as()) @@ -236,12 +250,32 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for_) checkNumber(for_->to); checkNumber(for_->step); - ScopePtr forScope = childScope(for_->location, scope); + ScopePtr forScope = childScope(for_, scope); forScope->bindings[for_->var] = Binding{singletonTypes.numberType, for_->var->location}; visit(forScope, for_->body); } +void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatWhile* while_) +{ + check(scope, while_->condition); + + ScopePtr whileScope = childScope(while_, scope); + + visit(whileScope, while_->body); +} + +void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatRepeat* repeat) +{ + ScopePtr repeatScope = childScope(repeat, scope); + + visit(repeatScope, repeat->body); + + // The condition does indeed have access to bindings from within the body of + // the loop. + check(repeatScope, repeat->condition); +} + void addConstraints(Constraint* constraint, NotNull scope) { scope->constraints.reserve(scope->constraints.size() + scope->constraints.size()); @@ -272,8 +306,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFunction* checkFunctionBody(sig.bodyScope, function->func); - std::unique_ptr c{ - new Constraint{GeneralizationConstraint{functionType, sig.signature, sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}}}; + NotNull constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}; + std::unique_ptr c = std::make_unique(GeneralizationConstraint{functionType, sig.signature}, constraintScope); addConstraints(c.get(), NotNull(sig.bodyScope.get())); addConstraint(scope, std::move(c)); @@ -342,8 +376,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction* funct checkFunctionBody(sig.bodyScope, function->func); - std::unique_ptr c{ - new Constraint{GeneralizationConstraint{functionType, sig.signature, sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}}}; + NotNull constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}; + std::unique_ptr c = std::make_unique(GeneralizationConstraint{functionType, sig.signature}, constraintScope); addConstraints(c.get(), NotNull(sig.bodyScope.get())); addConstraint(scope, std::move(c)); @@ -357,7 +391,7 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatReturn* ret) void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatBlock* block) { - ScopePtr innerScope = childScope(block->location, scope); + ScopePtr innerScope = childScope(block, scope); visitBlockWithoutChildScope(innerScope, block); } @@ -370,16 +404,30 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign) addConstraint(scope, PackSubtypeConstraint{valuePack, varPackId}); } +void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompoundAssign* assign) +{ + // Synthesize A = A op B from A op= B and then build constraints for that instead. + + AstExprBinary exprBinary{assign->location, assign->op, assign->var, assign->value}; + AstExpr* exprBinaryPtr = &exprBinary; + + AstArray vars{&assign->var, 1}; + AstArray values{&exprBinaryPtr, 1}; + AstStatAssign syntheticAssign{assign->location, vars, values}; + + visit(scope, &syntheticAssign); +} + void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement) { check(scope, ifStatement->condition); - ScopePtr thenScope = childScope(ifStatement->thenbody->location, scope); + ScopePtr thenScope = childScope(ifStatement->thenbody, scope); visit(thenScope, ifStatement->thenbody); if (ifStatement->elsebody) { - ScopePtr elseScope = childScope(ifStatement->elsebody->location, scope); + ScopePtr elseScope = childScope(ifStatement->elsebody, scope); visit(elseScope, ifStatement->elsebody); } } @@ -388,11 +436,11 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatTypeAlias* alia { // TODO: Exported type aliases - auto bindingIt = scope->typeBindings.find(alias->name.value); + auto bindingIt = scope->privateTypeBindings.find(alias->name.value); ScopePtr* defnIt = astTypeAliasDefiningScopes.find(alias); // These will be undefined if the alias was a duplicate definition, in which // case we just skip over it. - if (bindingIt == scope->typeBindings.end() || defnIt == nullptr) + if (bindingIt == scope->privateTypeBindings.end() || defnIt == nullptr) { return; } @@ -416,17 +464,152 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareGlobal* LUAU_ASSERT(global->type); TypeId globalTy = resolveType(scope, global->type); + Name globalName(global->name.value); + + module->declaredGlobals[globalName] = globalTy; scope->bindings[global->name] = Binding{globalTy, global->location}; } -void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareClass* global) +static bool isMetamethod(const Name& name) { - LUAU_ASSERT(false); // TODO: implement + 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"; +} + +void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareClass* declaredClass) +{ + std::optional superTy = std::nullopt; + if (declaredClass->superName) + { + Name superName = Name(declaredClass->superName->value); + std::optional lookupType = scope->lookupType(superName); + + if (!lookupType) + { + reportError(declaredClass->location, UnknownSymbol{superName, UnknownSymbol::Type}); + return; + } + + // We don't have generic classes, so this assertion _should_ never be hit. + LUAU_ASSERT(lookupType->typeParams.size() == 0 && lookupType->typePackParams.size() == 0); + superTy = lookupType->type; + + if (!get(follow(*superTy))) + { + reportError(declaredClass->location, + GenericError{format("Cannot use non-class type '%s' as a superclass of class '%s'", superName.c_str(), declaredClass->name.value)}); + + return; + } + } + + Name className(declaredClass->name.value); + + TypeId classTy = arena->addType(ClassTypeVar(className, {}, superTy, std::nullopt, {}, {}, moduleName)); + ClassTypeVar* ctv = getMutable(classTy); + + TypeId metaTy = arena->addType(TableTypeVar{TableState::Sealed, scope->level}); + TableTypeVar* metatable = getMutable(metaTy); + + ctv->metatable = metaTy; + + scope->exportedTypeBindings[className] = TypeFun{{}, classTy}; + + for (const AstDeclaredClassProp& prop : declaredClass->props) + { + Name propName(prop.name.value); + TypeId propTy = resolveType(scope, prop.ty); + + bool assignToMetatable = isMetamethod(propName); + + // Function types always take 'self', but this isn't reflected in the + // parsed annotation. Add it here. + if (prop.isMethod) + { + if (FunctionTypeVar* ftv = getMutable(propTy)) + { + ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}}); + ftv->argTypes = arena->addTypePack(TypePack{{classTy}, ftv->argTypes}); + + ftv->hasSelf = true; + } + } + + if (ctv->props.count(propName) == 0) + { + if (assignToMetatable) + metatable->props[propName] = {propTy}; + else + ctv->props[propName] = {propTy}; + } + else + { + TypeId currentTy = assignToMetatable ? metatable->props[propName].type : ctv->props[propName].type; + + // We special-case this logic to keep the intersection flat; otherwise we + // would create a ton of nested intersection types. + if (const IntersectionTypeVar* itv = get(currentTy)) + { + std::vector options = itv->parts; + options.push_back(propTy); + TypeId newItv = arena->addType(IntersectionTypeVar{std::move(options)}); + + if (assignToMetatable) + metatable->props[propName] = {newItv}; + else + ctv->props[propName] = {newItv}; + } + else if (get(currentTy)) + { + TypeId intersection = arena->addType(IntersectionTypeVar{{currentTy, propTy}}); + + if (assignToMetatable) + metatable->props[propName] = {intersection}; + else + ctv->props[propName] = {intersection}; + } + else + { + reportError(declaredClass->location, GenericError{format("Cannot overload non-function class member '%s'", propName.c_str())}); + } + } + } } void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareFunction* global) { - LUAU_ASSERT(false); // TODO: implement + + std::vector> generics = createGenerics(scope, global->generics); + std::vector> genericPacks = createGenericPacks(scope, global->genericPacks); + + std::vector genericTys; + genericTys.reserve(generics.size()); + for (auto& [name, generic] : generics) + genericTys.push_back(generic.ty); + + std::vector genericTps; + genericTps.reserve(genericPacks.size()); + for (auto& [name, generic] : genericPacks) + genericTps.push_back(generic.tp); + + ScopePtr funScope = scope; + if (!generics.empty() || !genericPacks.empty()) + funScope = childScope(global, scope); + + TypePackId paramPack = resolveTypePack(funScope, global->params); + TypePackId retPack = resolveTypePack(funScope, global->retTypes); + TypeId fnType = arena->addType(FunctionTypeVar{funScope->level, std::move(genericTys), std::move(genericTps), paramPack, retPack}); + FunctionTypeVar* ftv = getMutable(fnType); + + ftv->argNames.reserve(global->paramNames.size); + for (const auto& el : global->paramNames) + ftv->argNames.push_back(FunctionArgument{el.first.value, el.second}); + + Name fnName(global->name.value); + + module->declaredGlobals[fnName] = fnType; + scope->bindings[global->name] = Binding{fnType, global->location}; } TypePackId ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstArray exprs) @@ -590,6 +773,10 @@ TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExpr* expr) result = check(scope, unary); else if (auto binary = expr->as()) result = check(scope, binary); + else if (auto ifElse = expr->as()) + result = check(scope, ifElse); + else if (auto typeAssert = expr->as()) + result = check(scope, typeAssert); else if (auto err = expr->as()) { // Open question: Should we traverse into this? @@ -668,6 +855,12 @@ TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binar addConstraint(scope, SubtypeConstraint{leftType, rightType}); return leftType; } + case AstExprBinary::Add: + { + TypeId resultType = arena->addType(BlockedTypeVar{}); + addConstraint(scope, BinaryConstraint{AstExprBinary::Add, leftType, rightType, resultType}); + return resultType; + } case AstExprBinary::Sub: { TypeId resultType = arena->addType(BlockedTypeVar{}); @@ -682,6 +875,30 @@ TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binar return nullptr; } +TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* ifElse) +{ + check(scope, ifElse->condition); + + TypeId thenType = check(scope, ifElse->trueExpr); + TypeId elseType = check(scope, ifElse->falseExpr); + + if (ifElse->hasElse) + { + TypeId resultType = arena->addType(BlockedTypeVar{}); + addConstraint(scope, SubtypeConstraint{thenType, resultType}); + addConstraint(scope, SubtypeConstraint{elseType, resultType}); + return resultType; + } + + return thenType; +} + +TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert) +{ + check(scope, typeAssert->expr); + return resolveType(scope, typeAssert->annotation); +} + TypeId ConstraintGraphBuilder::checkExprTable(const ScopePtr& scope, AstExprTable* expr) { TypeId ty = arena->addType(TableTypeVar{}); @@ -748,14 +965,14 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS // generics properly. if (hasGenerics) { - signatureScope = childScope(fn->location, parent); + signatureScope = childScope(fn, parent); // We need to assign returnType before creating bodyScope so that the // return type gets propogated to bodyScope. returnType = freshTypePack(signatureScope); signatureScope->returnType = returnType; - bodyScope = childScope(fn->body->location, signatureScope); + bodyScope = childScope(fn->body, signatureScope); std::vector> genericDefinitions = createGenerics(signatureScope, fn->generics); std::vector> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks); @@ -765,18 +982,18 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS for (const auto& [name, g] : genericDefinitions) { genericTypes.push_back(g.ty); - signatureScope->typeBindings[name] = TypeFun{g.ty}; + signatureScope->privateTypeBindings[name] = TypeFun{g.ty}; } for (const auto& [name, g] : genericPackDefinitions) { genericTypePacks.push_back(g.tp); - signatureScope->typePackBindings[name] = g.tp; + signatureScope->privateTypePackBindings[name] = g.tp; } } else { - bodyScope = childScope(fn->body->location, parent); + bodyScope = childScope(fn->body, parent); returnType = freshTypePack(bodyScope); bodyScope->returnType = returnType; @@ -851,7 +1068,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b // TODO: Support imported types w/ require tracing. LUAU_ASSERT(!ref->prefix); - std::optional alias = scope->lookupTypeBinding(ref->name.value); + std::optional alias = scope->lookupType(ref->name.value); if (alias.has_value()) { @@ -941,7 +1158,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b // for the generic bindings to live on. if (hasGenerics) { - signatureScope = childScope(fn->location, scope); + signatureScope = childScope(fn, scope); std::vector> genericDefinitions = createGenerics(signatureScope, fn->generics); std::vector> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks); @@ -949,13 +1166,13 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b for (const auto& [name, g] : genericDefinitions) { genericTypes.push_back(g.ty); - signatureScope->typeBindings[name] = TypeFun{g.ty}; + signatureScope->privateTypeBindings[name] = TypeFun{g.ty}; } for (const auto& [name, g] : genericPackDefinitions) { genericTypePacks.push_back(g.tp); - signatureScope->typePackBindings[name] = g.tp; + signatureScope->privateTypePackBindings[name] = g.tp; } } else @@ -1059,7 +1276,7 @@ TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, AstTyp } else if (auto gen = tp->as()) { - if (std::optional lookup = scope->lookupTypePackBinding(gen->genericName.value)) + if (std::optional lookup = scope->lookupPack(gen->genericName.value)) { result = *lookup; } diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 0898f9aa..6c6d2722 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -373,7 +373,7 @@ bool ConstraintSolver::tryDispatch(const SubtypeConstraint& c, NotNullscope); unblock(c.subType); unblock(c.superType); @@ -383,7 +383,7 @@ bool ConstraintSolver::tryDispatch(const SubtypeConstraint& c, NotNull constraint, bool force) { - unify(c.subPack, c.superPack); + unify(c.subPack, c.superPack, constraint->scope); unblock(c.subPack); unblock(c.superPack); @@ -398,9 +398,9 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNullty.emplace(c.sourceType); else - unify(c.generalizedType, c.sourceType); + unify(c.generalizedType, c.sourceType, constraint->scope); - TypeId generalized = quantify(arena, c.sourceType, c.scope); + TypeId generalized = quantify(arena, c.sourceType, constraint->scope); *asMutable(c.sourceType) = *generalized; unblock(c.generalizedType); @@ -422,7 +422,7 @@ bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNullty.emplace(*instantiated); else - unify(c.subType, *instantiated); + unify(c.subType, *instantiated, constraint->scope); unblock(c.subType); @@ -465,7 +465,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNullscope); asMutable(c.resultType)->ty.emplace(leftType); return true; } @@ -484,6 +484,10 @@ bool ConstraintSolver::tryDispatch(const NameConstraint& c, NotNullpersistent) + return true; + if (TableTypeVar* ttv = getMutable(target)) ttv->name = c.name; else if (MetatableTypeVar* mtv = getMutable(target)) @@ -524,16 +528,18 @@ struct InstantiationQueuer : TypeVarOnceVisitor { ConstraintSolver* solver; const InstantiationSignature& signature; + NotNull scope; - explicit InstantiationQueuer(ConstraintSolver* solver, const InstantiationSignature& signature) + explicit InstantiationQueuer(ConstraintSolver* solver, const InstantiationSignature& signature, NotNull scope) : solver(solver) , signature(signature) + , scope(scope) { } bool visit(TypeId ty, const PendingExpansionTypeVar& petv) override { - solver->pushConstraint(TypeAliasExpansionConstraint{ty}); + solver->pushConstraint(TypeAliasExpansionConstraint{ty}, scope); return false; } }; @@ -637,6 +643,10 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul TypeId instantiated = *maybeInstantiated; TypeId target = follow(instantiated); + + if (target->persistent) + return true; + // Type function application will happily give us the exact same type if // there are e.g. generic saturatedTypeArguments that go unused. bool needsClone = follow(petv->fn.type) == target; @@ -678,7 +688,7 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul // The application is not recursive, so we need to queue up application of // any child type function instantiations within the result in order for it // to be complete. - InstantiationQueuer queuer{this, signature}; + InstantiationQueuer queuer{this, signature, constraint->scope}; queuer.traverse(target); instantiatedAliases[signature] = target; @@ -758,30 +768,40 @@ bool ConstraintSolver::isBlocked(NotNull constraint) return blockedIt != blockedConstraints.end() && blockedIt->second > 0; } -void ConstraintSolver::unify(TypeId subType, TypeId superType) +void ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull scope) { UnifierSharedState sharedState{&iceReporter}; - Unifier u{arena, Mode::Strict, Location{}, Covariant, sharedState}; + Unifier u{arena, Mode::Strict, scope, Location{}, Covariant, sharedState}; u.tryUnify(subType, superType); u.log.commit(); } -void ConstraintSolver::unify(TypePackId subPack, TypePackId superPack) +void ConstraintSolver::unify(TypePackId subPack, TypePackId superPack, NotNull scope) { UnifierSharedState sharedState{&iceReporter}; - Unifier u{arena, Mode::Strict, Location{}, Covariant, sharedState}; + Unifier u{arena, Mode::Strict, scope, Location{}, Covariant, sharedState}; u.tryUnify(subPack, superPack); u.log.commit(); } -void ConstraintSolver::pushConstraint(ConstraintV cv) +void ConstraintSolver::pushConstraint(ConstraintV cv, NotNull scope) { - std::unique_ptr c = std::make_unique(std::move(cv)); + std::unique_ptr c = std::make_unique(std::move(cv), scope); NotNull borrow = NotNull(c.get()); solverConstraints.push_back(std::move(c)); unsolvedConstraints.push_back(borrow); } +void ConstraintSolver::reportError(TypeErrorData&& data, const Location& location) +{ + errors.emplace_back(location, std::move(data)); +} + +void ConstraintSolver::reportError(TypeError e) +{ + errors.emplace_back(std::move(e)); +} + } // namespace Luau diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index fe65853d..8ab4e865 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -818,27 +818,30 @@ const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) cons return const_cast(this)->getSourceModule(moduleName); } -NotNull Frontend::getGlobalScope() +ScopePtr Frontend::getGlobalScope() { if (!globalScope) { globalScope = typeChecker.globalScope; } - return NotNull(globalScope.get()); + return globalScope; } ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope) { ModulePtr result = std::make_shared(); - ConstraintGraphBuilder cgb{sourceModule.name, &result->internalTypes, NotNull(&iceHandler), getGlobalScope()}; + ConstraintGraphBuilder cgb{sourceModule.name, result, &result->internalTypes, NotNull(&iceHandler), getGlobalScope()}; cgb.visit(sourceModule.root); result->errors = std::move(cgb.errors); ConstraintSolver cs{&result->internalTypes, NotNull(cgb.rootScope)}; cs.run(); + for (TypeError& e : cs.errors) + result->errors.emplace_back(std::move(e)); + result->scopes = std::move(cgb.scopes); result->astTypes = std::move(cgb.astTypes); result->astTypePacks = std::move(cgb.astTypePacks); diff --git a/Analysis/src/Instantiation.cpp b/Analysis/src/Instantiation.cpp index 1a6013af..e98ab185 100644 --- a/Analysis/src/Instantiation.cpp +++ b/Analysis/src/Instantiation.cpp @@ -82,6 +82,8 @@ bool ReplaceGenerics::ignoreChildren(TypeId ty) // whenever we quantify, so the vectors overlap if and only if they are equal. return (!generics.empty() || !genericPacks.empty()) && (ftv->generics == generics) && (ftv->genericPacks == genericPacks); } + else if (FFlag::LuauClassTypeVarsInSubstitution && get(ty)) + return true; else { return false; diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index e77f1e00..9f89efe9 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -14,6 +14,7 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) LUAU_FASTFLAGVARIABLE(LuauLintGlobalNeverReadBeforeWritten, false) +LUAU_FASTFLAGVARIABLE(LuauLintComparisonPrecedence, false) namespace Luau { @@ -49,6 +50,7 @@ static const char* kWarningNames[] = { "MisleadingAndOr", "CommentDirective", "IntegerParsing", + "ComparisonPrecedence", }; // clang-format on @@ -2647,6 +2649,65 @@ private: } }; +class LintComparisonPrecedence : AstVisitor +{ +public: + LUAU_NOINLINE static void process(LintContext& context) + { + LintComparisonPrecedence pass; + pass.context = &context; + + context.root->visit(&pass); + } + +private: + LintContext* context; + + bool isComparison(AstExprBinary::Op op) + { + return op == AstExprBinary::CompareNe || op == AstExprBinary::CompareEq || op == AstExprBinary::CompareLt || op == AstExprBinary::CompareLe || + op == AstExprBinary::CompareGt || op == AstExprBinary::CompareGe; + } + + bool isNot(AstExpr* node) + { + AstExprUnary* expr = node->as(); + + return expr && expr->op == AstExprUnary::Not; + } + + bool visit(AstExprBinary* node) override + { + if (!isComparison(node->op)) + return true; + + // not X == Y; we silence this for not X == not Y as it's likely an intentional boolean comparison + if (isNot(node->left) && !isNot(node->right)) + { + std::string op = toString(node->op); + + if (node->op == AstExprBinary::CompareEq || node->op == AstExprBinary::CompareNe) + emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location, + "not X %s Y is equivalent to (not X) %s Y; consider using X %s Y, or wrap one of the expressions in parentheses to silence", + op.c_str(), op.c_str(), node->op == AstExprBinary::CompareEq ? "~=" : "=="); + else + emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location, + "not X %s Y is equivalent to (not X) %s Y; wrap one of the expressions in parentheses to silence", op.c_str(), op.c_str()); + } + else if (AstExprBinary* left = node->left->as(); left && isComparison(left->op)) + { + std::string lop = toString(left->op); + std::string rop = toString(node->op); + + emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location, + "X %s Y %s Z is equivalent to (X %s Y) %s Z; wrap one of the expressions in parentheses to silence", lop.c_str(), rop.c_str(), + lop.c_str(), rop.c_str()); + } + + return true; + } +}; + static void fillBuiltinGlobals(LintContext& context, const AstNameTable& names, const ScopePtr& env) { ScopePtr current = env; @@ -2871,6 +2932,9 @@ std::vector lint(AstStat* root, const AstNameTable& names, const Sc if (context.warningEnabled(LintWarning::Code_IntegerParsing)) LintIntegerParsing::process(context); + if (context.warningEnabled(LintWarning::Code_ComparisonPrecedence) && FFlag::LuauLintComparisonPrecedence) + LintComparisonPrecedence::process(context); + std::sort(context.result.begin(), context.result.end(), WarningComparator()); return context.result; diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index 2b46da87..de796c7a 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -17,6 +17,10 @@ LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAGVARIABLE(LuauForceExportSurfacesToBeNormal, false); +LUAU_FASTFLAGVARIABLE(LuauClonePublicInterfaceLess, false); +LUAU_FASTFLAG(LuauSubstitutionReentrant); +LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution); +LUAU_FASTFLAG(LuauSubstitutionFixMissingFields); namespace Luau { @@ -86,6 +90,118 @@ struct ForceNormal : TypeVarOnceVisitor } }; +struct ClonePublicInterface : Substitution +{ + NotNull module; + + ClonePublicInterface(const TxnLog* log, Module* module) + : Substitution(log, &module->interfaceTypes) + , module(module) + { + LUAU_ASSERT(module); + } + + bool isDirty(TypeId ty) override + { + if (ty->owningArena == &module->internalTypes) + return true; + + if (const FunctionTypeVar* ftv = get(ty)) + return ftv->level.level != 0; + if (const TableTypeVar* ttv = get(ty)) + return ttv->level.level != 0; + return false; + } + + bool isDirty(TypePackId tp) override + { + return tp->owningArena == &module->internalTypes; + } + + TypeId clean(TypeId ty) override + { + TypeId result = clone(ty); + + if (FunctionTypeVar* ftv = getMutable(result)) + ftv->level = TypeLevel{0, 0}; + else if (TableTypeVar* ttv = getMutable(result)) + ttv->level = TypeLevel{0, 0}; + + return result; + } + + TypePackId clean(TypePackId tp) override + { + return clone(tp); + } + + TypeId cloneType(TypeId ty) + { + LUAU_ASSERT(FFlag::LuauSubstitutionReentrant && FFlag::LuauSubstitutionFixMissingFields); + + std::optional result = substitute(ty); + if (result) + { + return *result; + } + else + { + module->errors.push_back(TypeError{module->scopes[0].first, UnificationTooComplex{}}); + return getSingletonTypes().errorRecoveryType(); + } + } + + TypePackId cloneTypePack(TypePackId tp) + { + LUAU_ASSERT(FFlag::LuauSubstitutionReentrant && FFlag::LuauSubstitutionFixMissingFields); + + std::optional result = substitute(tp); + if (result) + { + return *result; + } + else + { + module->errors.push_back(TypeError{module->scopes[0].first, UnificationTooComplex{}}); + return getSingletonTypes().errorRecoveryTypePack(); + } + } + + TypeFun cloneTypeFun(const TypeFun& tf) + { + LUAU_ASSERT(FFlag::LuauSubstitutionReentrant && FFlag::LuauSubstitutionFixMissingFields); + + std::vector typeParams; + std::vector typePackParams; + + for (GenericTypeDefinition typeParam : tf.typeParams) + { + TypeId ty = cloneType(typeParam.ty); + std::optional defaultValue; + + if (typeParam.defaultValue) + defaultValue = cloneType(*typeParam.defaultValue); + + typeParams.push_back(GenericTypeDefinition{ty, defaultValue}); + } + + for (GenericTypePackDefinition typePackParam : tf.typePackParams) + { + TypePackId tp = cloneTypePack(typePackParam.tp); + std::optional defaultValue; + + if (typePackParam.defaultValue) + defaultValue = cloneTypePack(*typePackParam.defaultValue); + + typePackParams.push_back(GenericTypePackDefinition{tp, defaultValue}); + } + + TypeId type = cloneType(tf.type); + + return TypeFun{typeParams, typePackParams, type}; + } +}; + Module::~Module() { unfreeze(interfaceTypes); @@ -106,12 +222,21 @@ void Module::clonePublicInterface(InternalErrorReporter& ice) std::unordered_map* exportedTypeBindings = FFlag::DebugLuauDeferredConstraintResolution ? nullptr : &moduleScope->exportedTypeBindings; - returnType = clone(returnType, interfaceTypes, cloneState); + TxnLog log; + ClonePublicInterface clonePublicInterface{&log, this}; + + if (FFlag::LuauClonePublicInterfaceLess) + returnType = clonePublicInterface.cloneTypePack(returnType); + else + returnType = clone(returnType, interfaceTypes, cloneState); moduleScope->returnType = returnType; if (varargPack) { - varargPack = clone(*varargPack, interfaceTypes, cloneState); + if (FFlag::LuauClonePublicInterfaceLess) + varargPack = clonePublicInterface.cloneTypePack(*varargPack); + else + varargPack = clone(*varargPack, interfaceTypes, cloneState); moduleScope->varargPack = varargPack; } @@ -119,12 +244,12 @@ void Module::clonePublicInterface(InternalErrorReporter& ice) if (FFlag::LuauLowerBoundsCalculation) { - normalize(returnType, interfaceTypes, ice); + normalize(returnType, NotNull{this}, ice); if (FFlag::LuauForceExportSurfacesToBeNormal) forceNormal.traverse(returnType); if (varargPack) { - normalize(*varargPack, interfaceTypes, ice); + normalize(*varargPack, NotNull{this}, ice); if (FFlag::LuauForceExportSurfacesToBeNormal) forceNormal.traverse(*varargPack); } @@ -134,10 +259,13 @@ void Module::clonePublicInterface(InternalErrorReporter& ice) { for (auto& [name, tf] : *exportedTypeBindings) { - tf = clone(tf, interfaceTypes, cloneState); + if (FFlag::LuauClonePublicInterfaceLess) + tf = clonePublicInterface.cloneTypeFun(tf); + else + tf = clone(tf, interfaceTypes, cloneState); if (FFlag::LuauLowerBoundsCalculation) { - normalize(tf.type, interfaceTypes, ice); + normalize(tf.type, NotNull{this}, ice); // We're about to freeze the memory. We know that the flag is conservative by design. Cyclic tables // won't be marked normal. If the types aren't normal by now, they never will be. @@ -148,7 +276,7 @@ void Module::clonePublicInterface(InternalErrorReporter& ice) if (param.defaultValue) { - normalize(*param.defaultValue, interfaceTypes, ice); + normalize(*param.defaultValue, NotNull{this}, ice); forceNormal.traverse(*param.defaultValue); } } @@ -168,10 +296,13 @@ void Module::clonePublicInterface(InternalErrorReporter& ice) for (auto& [name, ty] : declaredGlobals) { - ty = clone(ty, interfaceTypes, cloneState); + if (FFlag::LuauClonePublicInterfaceLess) + ty = clonePublicInterface.cloneType(ty); + else + ty = clone(ty, interfaceTypes, cloneState); if (FFlag::LuauLowerBoundsCalculation) { - normalize(ty, interfaceTypes, ice); + normalize(ty, NotNull{this}, ice); if (FFlag::LuauForceExportSurfacesToBeNormal) forceNormal.traverse(ty); diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 9ae3b404..94adaf5c 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -15,7 +15,6 @@ LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200); LUAU_FASTFLAGVARIABLE(LuauNormalizeCombineTableFix, false); LUAU_FASTFLAGVARIABLE(LuauFixNormalizationOfCyclicUnions, false); LUAU_FASTFLAG(LuauUnknownAndNeverType) -LUAU_FASTFLAG(LuauQuantifyConstrained) namespace Luau { @@ -55,11 +54,11 @@ struct Replacer } // anonymous namespace -bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice) +bool isSubtype(TypeId subTy, TypeId superTy, NotNull scope, InternalErrorReporter& ice) { UnifierSharedState sharedState{&ice}; TypeArena arena; - Unifier u{&arena, Mode::Strict, Location{}, Covariant, sharedState}; + Unifier u{&arena, Mode::Strict, scope, Location{}, Covariant, sharedState}; u.anyIsTop = true; u.tryUnify(subTy, superTy); @@ -67,11 +66,11 @@ bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice) return ok; } -bool isSubtype(TypePackId subPack, TypePackId superPack, InternalErrorReporter& ice) +bool isSubtype(TypePackId subPack, TypePackId superPack, NotNull scope, InternalErrorReporter& ice) { UnifierSharedState sharedState{&ice}; TypeArena arena; - Unifier u{&arena, Mode::Strict, Location{}, Covariant, sharedState}; + Unifier u{&arena, Mode::Strict, scope, Location{}, Covariant, sharedState}; u.anyIsTop = true; u.tryUnify(subPack, superPack); @@ -134,13 +133,15 @@ struct Normalize final : TypeVarVisitor { using TypeVarVisitor::Set; - Normalize(TypeArena& arena, InternalErrorReporter& ice) + Normalize(TypeArena& arena, NotNull scope, InternalErrorReporter& ice) : arena(arena) + , scope(scope) , ice(ice) { } TypeArena& arena; + NotNull scope; InternalErrorReporter& ice; int iterationLimit = 0; @@ -162,7 +163,8 @@ struct Normalize final : TypeVarVisitor // It should never be the case that this TypeVar is normal, but is bound to a non-normal type, except in nontrivial cases. LUAU_ASSERT(!ty->normal || ty->normal == btv.boundTo->normal); - asMutable(ty)->normal = btv.boundTo->normal; + if (!ty->normal) + asMutable(ty)->normal = btv.boundTo->normal; return !ty->normal; } @@ -214,22 +216,7 @@ struct Normalize final : TypeVarVisitor traverse(part); std::vector newParts = normalizeUnion(parts); - - if (FFlag::LuauQuantifyConstrained) - { - ctv->parts = std::move(newParts); - } - else - { - const bool normal = areNormal(newParts, seen, ice); - - if (newParts.size() == 1) - *asMutable(ty) = BoundTypeVar{newParts[0]}; - else - *asMutable(ty) = UnionTypeVar{std::move(newParts)}; - - asMutable(ty)->normal = normal; - } + ctv->parts = std::move(newParts); return false; } @@ -287,12 +274,7 @@ struct Normalize final : TypeVarVisitor } // An unsealed table can never be normal, ditto for free tables iff the type it is bound to is also not normal. - if (FFlag::LuauQuantifyConstrained) - { - if (ttv.state == TableState::Generic || ttv.state == TableState::Sealed || (ttv.state == TableState::Free && follow(ty)->normal)) - asMutable(ty)->normal = normal; - } - else + if (ttv.state == TableState::Generic || ttv.state == TableState::Sealed || (ttv.state == TableState::Free && follow(ty)->normal)) asMutable(ty)->normal = normal; return false; @@ -517,9 +499,9 @@ struct Normalize final : TypeVarVisitor for (TypeId& part : result) { - if (isSubtype(ty, part, ice)) + if (isSubtype(ty, part, scope, ice)) return; // no need to do anything - else if (isSubtype(part, ty, ice)) + else if (isSubtype(part, ty, scope, ice)) { part = ty; // replace the less general type by the more general one return; @@ -571,12 +553,12 @@ struct Normalize final : TypeVarVisitor bool merged = false; for (TypeId& part : result->parts) { - if (isSubtype(part, ty, ice)) + if (isSubtype(part, ty, scope, ice)) { merged = true; break; // no need to do anything } - else if (isSubtype(ty, part, ice)) + else if (isSubtype(ty, part, scope, ice)) { merged = true; part = ty; // replace the less general type by the more general one @@ -709,13 +691,13 @@ struct Normalize final : TypeVarVisitor /** * @returns A tuple of TypeId and a success indicator. (true indicates that the normalization completed successfully) */ -std::pair normalize(TypeId ty, TypeArena& arena, InternalErrorReporter& ice) +std::pair normalize(TypeId ty, NotNull scope, TypeArena& arena, InternalErrorReporter& ice) { CloneState state; if (FFlag::DebugLuauCopyBeforeNormalizing) (void)clone(ty, arena, state); - Normalize n{arena, ice}; + Normalize n{arena, scope, ice}; n.traverse(ty); return {ty, !n.limitExceeded}; @@ -725,29 +707,39 @@ std::pair normalize(TypeId ty, TypeArena& arena, InternalErrorRepo // reclaim memory used by wantonly allocated intermediate types here. // The main wrinkle here is that we don't want clone() to copy a type if the source and dest // arena are the same. +std::pair normalize(TypeId ty, NotNull module, InternalErrorReporter& ice) +{ + return normalize(ty, NotNull{module->getModuleScope().get()}, module->internalTypes, ice); +} + std::pair normalize(TypeId ty, const ModulePtr& module, InternalErrorReporter& ice) { - return normalize(ty, module->internalTypes, ice); + return normalize(ty, NotNull{module.get()}, ice); } /** * @returns A tuple of TypeId and a success indicator. (true indicates that the normalization completed successfully) */ -std::pair normalize(TypePackId tp, TypeArena& arena, InternalErrorReporter& ice) +std::pair normalize(TypePackId tp, NotNull scope, TypeArena& arena, InternalErrorReporter& ice) { CloneState state; if (FFlag::DebugLuauCopyBeforeNormalizing) (void)clone(tp, arena, state); - Normalize n{arena, ice}; + Normalize n{arena, scope, ice}; n.traverse(tp); return {tp, !n.limitExceeded}; } +std::pair normalize(TypePackId tp, NotNull module, InternalErrorReporter& ice) +{ + return normalize(tp, NotNull{module->getModuleScope().get()}, module->internalTypes, ice); +} + std::pair normalize(TypePackId tp, const ModulePtr& module, InternalErrorReporter& ice) { - return normalize(tp, module->internalTypes, ice); + return normalize(tp, NotNull{module.get()}, ice); } } // namespace Luau diff --git a/Analysis/src/Quantify.cpp b/Analysis/src/Quantify.cpp index 03049cca..7e6ff2f9 100644 --- a/Analysis/src/Quantify.cpp +++ b/Analysis/src/Quantify.cpp @@ -5,11 +5,12 @@ #include "Luau/Scope.h" #include "Luau/Substitution.h" #include "Luau/TxnLog.h" +#include "Luau/TypeVar.h" #include "Luau/VisitTypeVar.h" LUAU_FASTFLAG(DebugLuauSharedSelf) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); -LUAU_FASTFLAGVARIABLE(LuauQuantifyConstrained, false) +LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution) namespace Luau { @@ -80,30 +81,25 @@ struct Quantifier final : TypeVarOnceVisitor bool visit(TypeId ty, const ConstrainedTypeVar&) override { - if (FFlag::LuauQuantifyConstrained) - { - ConstrainedTypeVar* ctv = getMutable(ty); + ConstrainedTypeVar* ctv = getMutable(ty); - seenMutableType = true; - - if (FFlag::DebugLuauDeferredConstraintResolution ? !subsumes(scope, ctv->scope) : !level.subsumes(ctv->level)) - return false; - - std::vector opts = std::move(ctv->parts); - - // We might transmute, so it's not safe to rely on the builtin traversal logic - for (TypeId opt : opts) - traverse(opt); - - if (opts.size() == 1) - *asMutable(ty) = BoundTypeVar{opts[0]}; - else - *asMutable(ty) = UnionTypeVar{std::move(opts)}; + seenMutableType = true; + if (FFlag::DebugLuauDeferredConstraintResolution ? !subsumes(scope, ctv->scope) : !level.subsumes(ctv->level)) return false; - } + + std::vector opts = std::move(ctv->parts); + + // We might transmute, so it's not safe to rely on the builtin traversal logic + for (TypeId opt : opts) + traverse(opt); + + if (opts.size() == 1) + *asMutable(ty) = BoundTypeVar{opts[0]}; else - return true; + *asMutable(ty) = UnionTypeVar{std::move(opts)}; + + return false; } bool visit(TypeId ty, const TableTypeVar&) override @@ -117,12 +113,6 @@ struct Quantifier final : TypeVarOnceVisitor if (ttv.state == TableState::Free) seenMutableType = true; - if (!FFlag::LuauQuantifyConstrained) - { - if (ttv.state == TableState::Sealed || ttv.state == TableState::Generic) - return false; - } - if (FFlag::DebugLuauDeferredConstraintResolution ? !subsumes(scope, ttv.scope) : !level.subsumes(ttv.level)) { if (ttv.state == TableState::Unsealed) @@ -297,6 +287,9 @@ struct PureQuantifier : Substitution bool ignoreChildren(TypeId ty) override { + if (FFlag::LuauClassTypeVarsInSubstitution && get(ty)) + return true; + return ty->persistent; } bool ignoreChildren(TypePackId ty) override diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index bee16908..c129b973 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -122,34 +122,4 @@ std::optional Scope::lookup(Symbol sym) } } -std::optional Scope::lookupTypeBinding(const Name& name) -{ - Scope* s = this; - while (s) - { - auto it = s->typeBindings.find(name); - if (it != s->typeBindings.end()) - return it->second; - - s = s->parent.get(); - } - - return std::nullopt; -} - -std::optional Scope::lookupTypePackBinding(const Name& name) -{ - Scope* s = this; - while (s) - { - auto it = s->typePackBindings.find(name); - if (it != s->typePackBindings.end()) - return it->second; - - s = s->parent.get(); - } - - return std::nullopt; -} - } // namespace Luau diff --git a/Analysis/src/Substitution.cpp b/Analysis/src/Substitution.cpp index 148c9ee2..0beeb58c 100644 --- a/Analysis/src/Substitution.cpp +++ b/Analysis/src/Substitution.cpp @@ -8,9 +8,9 @@ #include #include -LUAU_FASTFLAGVARIABLE(LuauAnyificationMustClone, false) LUAU_FASTFLAGVARIABLE(LuauSubstitutionFixMissingFields, false) LUAU_FASTFLAG(LuauLowerBoundsCalculation) +LUAU_FASTFLAG(LuauClonePublicInterfaceLess) LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000) LUAU_FASTFLAGVARIABLE(LuauClassTypeVarsInSubstitution, false) LUAU_FASTFLAG(LuauUnknownAndNeverType) @@ -472,7 +472,7 @@ std::optional Substitution::substitute(TypePackId tp) TypeId Substitution::clone(TypeId ty) { - return shallowClone(ty, *arena, log); + return shallowClone(ty, *arena, log, /* alwaysClone */ FFlag::LuauClonePublicInterfaceLess); } TypePackId Substitution::clone(TypePackId tp) @@ -497,6 +497,10 @@ TypePackId Substitution::clone(TypePackId tp) clone.hidden = vtp->hidden; return addTypePack(std::move(clone)); } + else if (FFlag::LuauClonePublicInterfaceLess) + { + return addTypePack(*tp); + } else return tp; } @@ -557,7 +561,7 @@ void Substitution::replaceChildren(TypeId ty) if (ignoreChildren(ty)) return; - if (FFlag::LuauAnyificationMustClone && ty->owningArena != arena) + if (ty->owningArena != arena) return; if (FunctionTypeVar* ftv = getMutable(ty)) @@ -638,7 +642,7 @@ void Substitution::replaceChildren(TypePackId tp) if (ignoreChildren(tp)) return; - if (FFlag::LuauAnyificationMustClone && tp->owningArena != arena) + if (tp->owningArena != arena) return; if (TypePack* tpp = getMutable(tp)) diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 53b069cf..e5813cd2 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -8,21 +8,59 @@ #include "Luau/Clone.h" #include "Luau/Instantiation.h" #include "Luau/Normalize.h" +#include "Luau/ToString.h" #include "Luau/TxnLog.h" #include "Luau/TypeUtils.h" +#include "Luau/TypeVar.h" #include "Luau/Unifier.h" -#include "Luau/ToString.h" namespace Luau { -struct TypeChecker2 : public AstVisitor +/* Push a scope onto the end of a stack for the lifetime of the StackPusher instance. + * TypeChecker2 uses this to maintain knowledge about which scope encloses every + * given AstNode. + */ +struct StackPusher +{ + std::vector>* stack; + NotNull scope; + + explicit StackPusher(std::vector>& stack, Scope* scope) + : stack(&stack) + , scope(scope) + { + stack.push_back(NotNull{scope}); + } + + ~StackPusher() + { + if (stack) + { + LUAU_ASSERT(stack->back() == scope); + stack->pop_back(); + } + } + + StackPusher(const StackPusher&) = delete; + StackPusher&& operator=(const StackPusher&) = delete; + + StackPusher(StackPusher&& other) + : stack(std::exchange(other.stack, nullptr)) + , scope(other.scope) + { + } +}; + +struct TypeChecker2 { const SourceModule* sourceModule; Module* module; InternalErrorReporter ice; // FIXME accept a pointer from Frontend SingletonTypes& singletonTypes; + std::vector> stack; + TypeChecker2(const SourceModule* sourceModule, Module* module) : sourceModule(sourceModule) , module(module) @@ -30,7 +68,13 @@ struct TypeChecker2 : public AstVisitor { } - using AstVisitor::visit; + std::optional pushStack(AstNode* node) + { + if (Scope** scope = module->astScopes.find(node)) + return StackPusher{stack, *scope}; + else + return std::nullopt; + } TypePackId lookupPack(AstExpr* expr) { @@ -117,11 +161,128 @@ struct TypeChecker2 : public AstVisitor return bestScope; } - bool visit(AstStatLocal* local) override + void visit(AstStat* stat) + { + auto pusher = pushStack(stat); + + if (0) + {} + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else if (auto s = stat->as()) + return visit(s); + else + LUAU_ASSERT(!"TypeChecker2 encountered an unknown node type"); + } + + void visit(AstStatBlock* block) + { + auto StackPusher = pushStack(block); + + for (AstStat* statement : block->body) + visit(statement); + } + + void visit(AstStatIf* ifStatement) + { + visit(ifStatement->condition); + visit(ifStatement->thenbody); + if (ifStatement->elsebody) + visit(ifStatement->elsebody); + } + + void visit(AstStatWhile* whileStatement) + { + visit(whileStatement->condition); + visit(whileStatement->body); + } + + void visit(AstStatRepeat* repeatStatement) + { + visit(repeatStatement->body); + visit(repeatStatement->condition); + } + + void visit(AstStatBreak*) + {} + + void visit(AstStatContinue*) + {} + + void visit(AstStatReturn* ret) + { + Scope* scope = findInnermostScope(ret->location); + TypePackId expectedRetType = scope->returnType; + + TypeArena arena; + TypePackId actualRetType = reconstructPack(ret->list, arena); + + UnifierSharedState sharedState{&ice}; + Unifier u{&arena, Mode::Strict, stack.back(), ret->location, Covariant, sharedState}; + u.anyIsTop = true; + + u.tryUnify(actualRetType, expectedRetType); + const bool ok = u.errors.empty() && u.log.empty(); + + if (!ok) + { + for (const TypeError& e : u.errors) + reportError(e); + } + + for (AstExpr* expr : ret->list) + visit(expr); + } + + void visit(AstStatExpr* expr) + { + visit(expr->expr); + } + + void visit(AstStatLocal* local) { for (size_t i = 0; i < local->values.size; ++i) { AstExpr* value = local->values.data[i]; + + visit(value); + if (i == local->values.size - 1) { if (i < local->values.size) @@ -139,7 +300,7 @@ struct TypeChecker2 : public AstVisitor if (var->annotation) { TypeId varType = lookupAnnotation(var->annotation); - if (!isSubtype(*it, varType, ice)) + if (!isSubtype(*it, varType, stack.back(), ice)) { reportError(TypeMismatch{varType, *it}, value->location); } @@ -157,64 +318,244 @@ struct TypeChecker2 : public AstVisitor if (var->annotation) { TypeId varType = lookupAnnotation(var->annotation); - if (!isSubtype(varType, valueType, ice)) + if (!isSubtype(varType, valueType, stack.back(), ice)) { reportError(TypeMismatch{varType, valueType}, value->location); } } } } - - return true; } - bool visit(AstStatAssign* assign) override + void visit(AstStatFor* forStatement) + { + if (forStatement->var->annotation) + visit(forStatement->var->annotation); + + visit(forStatement->from); + visit(forStatement->to); + if (forStatement->step) + visit(forStatement->step); + visit(forStatement->body); + } + + void visit(AstStatForIn* forInStatement) + { + for (AstLocal* local : forInStatement->vars) + { + if (local->annotation) + visit(local->annotation); + } + + for (AstExpr* expr : forInStatement->values) + visit(expr); + + visit(forInStatement->body); + } + + void visit(AstStatAssign* assign) { size_t count = std::min(assign->vars.size, assign->values.size); for (size_t i = 0; i < count; ++i) { AstExpr* lhs = assign->vars.data[i]; + visit(lhs); TypeId lhsType = lookupType(lhs); AstExpr* rhs = assign->values.data[i]; + visit(rhs); TypeId rhsType = lookupType(rhs); - if (!isSubtype(rhsType, lhsType, ice)) + if (!isSubtype(rhsType, lhsType, stack.back(), ice)) { reportError(TypeMismatch{lhsType, rhsType}, rhs->location); } } - - return true; } - bool visit(AstStatReturn* ret) override + void visit(AstStatCompoundAssign* stat) { - Scope* scope = findInnermostScope(ret->location); - TypePackId expectedRetType = scope->returnType; + visit(stat->var); + visit(stat->value); + } - TypeArena arena; - TypePackId actualRetType = reconstructPack(ret->list, arena); + void visit(AstStatFunction* stat) + { + visit(stat->name); + visit(stat->func); + } - UnifierSharedState sharedState{&ice}; - Unifier u{&arena, Mode::Strict, ret->location, Covariant, sharedState}; - u.anyIsTop = true; + void visit(AstStatLocalFunction* stat) + { + visit(stat->func); + } - u.tryUnify(actualRetType, expectedRetType); - const bool ok = u.errors.empty() && u.log.empty(); + void visit(const AstTypeList* typeList) + { + for (AstType* ty : typeList->types) + visit(ty); - if (!ok) + if (typeList->tailType) + visit(typeList->tailType); + } + + void visit(AstStatTypeAlias* stat) + { + for (const AstGenericType& el : stat->generics) { - for (const TypeError& e : u.errors) - reportError(e); + if (el.defaultValue) + visit(el.defaultValue); } - return true; + for (const AstGenericTypePack& el : stat->genericPacks) + { + if (el.defaultValue) + visit(el.defaultValue); + } + + visit(stat->type); } - bool visit(AstExprCall* call) override + void visit(AstTypeList types) { + for (AstType* type : types.types) + visit(type); + if (types.tailType) + visit(types.tailType); + } + + void visit(AstStatDeclareFunction* stat) + { + visit(stat->params); + visit(stat->retTypes); + } + + void visit(AstStatDeclareGlobal* stat) + { + visit(stat->type); + } + + void visit(AstStatDeclareClass* stat) + { + for (const AstDeclaredClassProp& prop : stat->props) + visit(prop.ty); + } + + void visit(AstStatError* stat) + { + for (AstExpr* expr : stat->expressions) + visit(expr); + + for (AstStat* s : stat->statements) + visit(s); + } + + void visit(AstExpr* expr) + { + auto StackPusher = pushStack(expr); + + if (0) + {} + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else if (auto e = expr->as()) + return visit(e); + else + LUAU_ASSERT(!"TypeChecker2 encountered an unknown expression type"); + } + + void visit(AstExprGroup* expr) + { + visit(expr->expr); + } + + void visit(AstExprConstantNil* expr) + { + // TODO! + } + + void visit(AstExprConstantBool* expr) + { + // TODO! + } + + void visit(AstExprConstantNumber* number) + { + TypeId actualType = lookupType(number); + TypeId numberType = getSingletonTypes().numberType; + + if (!isSubtype(numberType, actualType, stack.back(), ice)) + { + reportError(TypeMismatch{actualType, numberType}, number->location); + } + } + + void visit(AstExprConstantString* string) + { + TypeId actualType = lookupType(string); + TypeId stringType = getSingletonTypes().stringType; + + if (!isSubtype(stringType, actualType, stack.back(), ice)) + { + reportError(TypeMismatch{actualType, stringType}, string->location); + } + } + + void visit(AstExprLocal* expr) + { + // TODO! + } + + void visit(AstExprGlobal* expr) + { + // TODO! + } + + void visit(AstExprVarargs* expr) + { + // TODO! + } + + void visit(AstExprCall* call) + { + visit(call->func); + + for (AstExpr* arg : call->args) + visit(arg); + TypeArena arena; Instantiation instantiation{TxnLog::empty(), &arena, TypeLevel{}}; @@ -224,7 +565,7 @@ struct TypeChecker2 : public AstVisitor LUAU_ASSERT(functionType); TypePack args; - for (const auto& arg : call->args) + for (AstExpr* arg : call->args) { TypeId argTy = module->astTypes[arg]; LUAU_ASSERT(argTy); @@ -234,7 +575,7 @@ struct TypeChecker2 : public AstVisitor TypePackId argsTp = arena.addTypePack(args); FunctionTypeVar ftv{argsTp, expectedRetType}; TypeId expectedType = arena.addType(ftv); - if (!isSubtype(expectedType, instantiatedFunctionType, ice)) + if (!isSubtype(expectedType, instantiatedFunctionType, stack.back(), ice)) { unfreeze(module->interfaceTypes); CloneState cloneState; @@ -242,12 +583,36 @@ struct TypeChecker2 : public AstVisitor freeze(module->interfaceTypes); reportError(TypeMismatch{expectedType, functionType}, call->location); } - - return true; } - bool visit(AstExprFunction* fn) override + void visit(AstExprIndexName* indexName) { + TypeId leftType = lookupType(indexName->expr); + TypeId resultType = lookupType(indexName); + + // leftType must have a property called indexName->index + + std::optional ty = getIndexTypeFromType(module->getModuleScope(), leftType, indexName->index.value, indexName->location, /* addErrors */ true); + if (ty) + { + if (!isSubtype(resultType, *ty, stack.back(), ice)) + { + reportError(TypeMismatch{resultType, *ty}, indexName->location); + } + } + } + + void visit(AstExprIndexExpr* indexExpr) + { + // TODO! + visit(indexExpr->expr); + visit(indexExpr->index); + } + + void visit(AstExprFunction* fn) + { + auto StackPusher = pushStack(fn); + TypeId inferredFnTy = lookupType(fn); const FunctionTypeVar* inferredFtv = get(inferredFnTy); LUAU_ASSERT(inferredFtv); @@ -263,7 +628,7 @@ struct TypeChecker2 : public AstVisitor TypeId inferredArgTy = *argIt; TypeId annotatedArgTy = lookupAnnotation(arg->annotation); - if (!isSubtype(annotatedArgTy, inferredArgTy, ice)) + if (!isSubtype(annotatedArgTy, inferredArgTy, stack.back(), ice)) { reportError(TypeMismatch{annotatedArgTy, inferredArgTy}, arg->location); } @@ -272,56 +637,64 @@ struct TypeChecker2 : public AstVisitor ++argIt; } - return true; + visit(fn->body); } - bool visit(AstExprIndexName* indexName) override + void visit(AstExprTable* expr) { - TypeId leftType = lookupType(indexName->expr); - TypeId resultType = lookupType(indexName); - - // leftType must have a property called indexName->index - - std::optional t = findTablePropertyRespectingMeta(module->errors, leftType, indexName->index.value, indexName->location); - if (t) + // TODO! + for (const AstExprTable::Item& item : expr->items) { - if (!isSubtype(resultType, *t, ice)) - { - reportError(TypeMismatch{resultType, *t}, indexName->location); - } + if (item.key) + visit(item.key); + visit(item.value); } - else - { - reportError(UnknownProperty{leftType, indexName->index.value}, indexName->location); - } - - return true; } - bool visit(AstExprConstantNumber* number) override + void visit(AstExprUnary* expr) { - TypeId actualType = lookupType(number); - TypeId numberType = getSingletonTypes().numberType; - - if (!isSubtype(numberType, actualType, ice)) - { - reportError(TypeMismatch{actualType, numberType}, number->location); - } - - return true; + // TODO! + visit(expr->expr); } - bool visit(AstExprConstantString* string) override + void visit(AstExprBinary* expr) { - TypeId actualType = lookupType(string); - TypeId stringType = getSingletonTypes().stringType; + // TODO! + visit(expr->left); + visit(expr->right); + } - if (!isSubtype(stringType, actualType, ice)) - { - reportError(TypeMismatch{actualType, stringType}, string->location); - } + void visit(AstExprTypeAssertion* expr) + { + visit(expr->expr); + visit(expr->annotation); - return true; + TypeId annotationType = lookupAnnotation(expr->annotation); + TypeId computedType = lookupType(expr->expr); + + // Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case. + if (isSubtype(annotationType, computedType, stack.back(), ice)) + return; + + if (isSubtype(computedType, annotationType, stack.back(), ice)) + return; + + reportError(TypesAreUnrelated{computedType, annotationType}, expr->location); + } + + void visit(AstExprIfElse* expr) + { + // TODO! + visit(expr->condition); + visit(expr->trueExpr); + visit(expr->falseExpr); + } + + void visit(AstExprError* expr) + { + // TODO! + for (AstExpr* e : expr->expressions) + visit(e); } /** Extract a TypeId for the first type of the provided pack. @@ -362,19 +735,38 @@ struct TypeChecker2 : public AstVisitor ice.ice("flattenPack got a weird pack!"); } - bool visit(AstType* ty) override + void visit(AstType* ty) { - return true; + if (auto t = ty->as()) + return visit(t); + else if (auto t = ty->as()) + return visit(t); + else if (auto t = ty->as()) + return visit(t); + else if (auto t = ty->as()) + return visit(t); + else if (auto t = ty->as()) + return visit(t); + else if (auto t = ty->as()) + return visit(t); } - bool visit(AstTypeReference* ty) override + void visit(AstTypeReference* ty) { + for (const AstTypeOrPack& param : ty->parameters) + { + if (param.type) + visit(param.type); + else + visit(param.typePack); + } + Scope* scope = findInnermostScope(ty->location); LUAU_ASSERT(scope); // TODO: Imported types - std::optional alias = scope->lookupTypeBinding(ty->name.value); + std::optional alias = scope->lookupType(ty->name.value); if (alias.has_value()) { @@ -473,7 +865,7 @@ struct TypeChecker2 : public AstVisitor } else { - if (scope->lookupTypePackBinding(ty->name.value)) + if (scope->lookupPack(ty->name.value)) { reportError( SwappedGenericTypeParameter{ @@ -487,24 +879,84 @@ struct TypeChecker2 : public AstVisitor reportError(UnknownSymbol{ty->name.value, UnknownSymbol::Context::Type}, ty->location); } } - - return true; } - bool visit(AstTypePack*) override + void visit(AstTypeTable* table) { - return true; + // TODO! + + for (const AstTableProp& prop : table->props) + visit(prop.type); + + if (table->indexer) + { + visit(table->indexer->indexType); + visit(table->indexer->resultType); + } } - bool visit(AstTypePackGeneric* tp) override + void visit(AstTypeFunction* ty) + { + // TODO! + + visit(ty->argTypes); + visit(ty->returnTypes); + } + + void visit(AstTypeTypeof* ty) + { + visit(ty->expr); + } + + void visit(AstTypeUnion* ty) + { + // TODO! + for (AstType* type : ty->types) + visit(type); + } + + void visit(AstTypeIntersection* ty) + { + // TODO! + for (AstType* type : ty->types) + visit(type); + } + + void visit(AstTypePack* pack) + { + if (auto p = pack->as()) + return visit(p); + else if (auto p = pack->as()) + return visit(p); + else if (auto p = pack->as()) + return visit(p); + } + + void visit(AstTypePackExplicit* tp) + { + // TODO! + for (AstType* type : tp->typeList.types) + visit(type); + + if (tp->typeList.tailType) + visit(tp->typeList.tailType); + } + + void visit(AstTypePackVariadic* tp) + { + // TODO! + visit(tp->variadicType); + } + + void visit(AstTypePackGeneric* tp) { Scope* scope = findInnermostScope(tp->location); LUAU_ASSERT(scope); - std::optional alias = scope->lookupTypePackBinding(tp->genericName.value); + std::optional alias = scope->lookupPack(tp->genericName.value); if (!alias.has_value()) { - if (scope->lookupTypeBinding(tp->genericName.value)) + if (scope->lookupType(tp->genericName.value)) { reportError( SwappedGenericTypeParameter{ @@ -518,8 +970,6 @@ struct TypeChecker2 : public AstVisitor reportError(UnknownSymbol{tp->genericName.value, UnknownSymbol::Context::Type}, tp->location); } } - - return true; } void reportError(TypeErrorData&& data, const Location& location) @@ -531,13 +981,19 @@ struct TypeChecker2 : public AstVisitor { module->errors.emplace_back(std::move(e)); } + + std::optional getIndexTypeFromType( + const ScopePtr& scope, TypeId type, const std::string& prop, const Location& location, bool addErrors) + { + return Luau::getIndexTypeFromType(scope, module->errors, &module->internalTypes, type, prop, location, addErrors, ice); + } }; void check(const SourceModule& sourceModule, Module* module) { TypeChecker2 typeChecker{&sourceModule, module}; - sourceModule.root->visit(&typeChecker); + typeChecker.visit(sourceModule.root); } } // namespace Luau diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 7f79c234..77168053 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -33,16 +33,13 @@ LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500) LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAG(LuauAutocompleteDynamicLimits) LUAU_FASTFLAGVARIABLE(LuauExpectedTableUnionIndexerType, false) -LUAU_FASTFLAGVARIABLE(LuauIndexSilenceErrors, false) +LUAU_FASTFLAGVARIABLE(LuauInplaceDemoteSkipAllBound, false) LUAU_FASTFLAGVARIABLE(LuauLowerBoundsCalculation, false) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix3, false) LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false. LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false); -LUAU_FASTFLAGVARIABLE(LuauReportErrorsOnIndexerKeyMismatch, false) LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false) -LUAU_FASTFLAG(LuauQuantifyConstrained) -LUAU_FASTFLAGVARIABLE(LuauFalsyPredicateReturnsNilInstead, false) LUAU_FASTFLAGVARIABLE(LuauCheckGenericHOFTypes, false) LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false) LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false) @@ -474,7 +471,8 @@ struct InplaceDemoter : TypeVarOnceVisitor TypeArena* arena; InplaceDemoter(TypeLevel level, TypeArena* arena) - : newLevel(level) + : TypeVarOnceVisitor(/* skipBoundTypes= */ FFlag::LuauInplaceDemoteSkipAllBound) + , newLevel(level) , arena(arena) { } @@ -495,6 +493,7 @@ struct InplaceDemoter : TypeVarOnceVisitor bool visit(TypeId ty, const BoundTypeVar& btyRef) override { + LUAU_ASSERT(!FFlag::LuauInplaceDemoteSkipAllBound); return true; } @@ -657,7 +656,7 @@ void TypeChecker::checkBlockWithoutRecursionCheck(const ScopePtr& scope, const A TypeId leftType = follow(checkFunctionName(scope, *fun->name, funScope->level)); - unify(funTy, leftType, fun->location); + unify(funTy, leftType, scope, fun->location); } else if (auto fun = (*protoIter)->as()) { @@ -769,20 +768,20 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatIf& statement) } template -ErrorVec TypeChecker::canUnify_(Id subTy, Id superTy, const Location& location) +ErrorVec TypeChecker::canUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location) { - Unifier state = mkUnifier(location); + Unifier state = mkUnifier(scope, location); return state.canUnify(subTy, superTy); } -ErrorVec TypeChecker::canUnify(TypeId subTy, TypeId superTy, const Location& location) +ErrorVec TypeChecker::canUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location) { - return canUnify_(subTy, superTy, location); + return canUnify_(subTy, superTy, scope, location); } -ErrorVec TypeChecker::canUnify(TypePackId subTy, TypePackId superTy, const Location& location) +ErrorVec TypeChecker::canUnify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location) { - return canUnify_(subTy, superTy, location); + return canUnify_(subTy, superTy, scope, location); } void TypeChecker::check(const ScopePtr& scope, const AstStatWhile& statement) @@ -803,9 +802,9 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatRepeat& statement) checkExpr(repScope, *statement.condition); } -void TypeChecker::unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const Location& location) +void TypeChecker::unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const ScopePtr& scope, const Location& location) { - Unifier state = mkUnifier(location); + Unifier state = mkUnifier(scope, location); state.unifyLowerBound(subTy, superTy, demotedLevel); state.log.commit(); @@ -830,6 +829,14 @@ struct Demoter : Substitution return get(tp); } + bool ignoreChildren(TypeId ty) override + { + if (FFlag::LuauClassTypeVarsInSubstitution && get(ty)) + return true; + + return false; + } + TypeId clean(TypeId ty) override { auto ftv = get(ty); @@ -851,8 +858,6 @@ struct Demoter : Substitution void demote(std::vector>& expectedTypes) { - if (!FFlag::LuauQuantifyConstrained) - return; for (std::optional& ty : expectedTypes) { if (ty) @@ -890,7 +895,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_) if (useConstrainedIntersections()) { - unifyLowerBound(retPack, scope->returnType, demoter.demotedLevel(scope->level), return_.location); + unifyLowerBound(retPack, scope->returnType, demoter.demotedLevel(scope->level), scope, return_.location); return; } @@ -898,7 +903,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_) // start typechecking everything across module boundaries. if (isNonstrictMode() && follow(scope->returnType) == follow(currentModule->getModuleScope()->returnType)) { - ErrorVec errors = tryUnify(retPack, scope->returnType, return_.location); + ErrorVec errors = tryUnify(retPack, scope->returnType, scope, return_.location); if (!errors.empty()) currentModule->getModuleScope()->returnType = addTypePack({anyType}); @@ -906,13 +911,13 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_) return; } - unify(retPack, scope->returnType, return_.location, CountMismatch::Context::Return); + unify(retPack, scope->returnType, scope, return_.location, CountMismatch::Context::Return); } template -ErrorVec TypeChecker::tryUnify_(Id subTy, Id superTy, const Location& location) +ErrorVec TypeChecker::tryUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location) { - Unifier state = mkUnifier(location); + Unifier state = mkUnifier(scope, location); if (FFlag::DebugLuauFreezeDuringUnification) freeze(currentModule->internalTypes); @@ -928,14 +933,14 @@ ErrorVec TypeChecker::tryUnify_(Id subTy, Id superTy, const Location& location) return state.errors; } -ErrorVec TypeChecker::tryUnify(TypeId subTy, TypeId superTy, const Location& location) +ErrorVec TypeChecker::tryUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location) { - return tryUnify_(subTy, superTy, location); + return tryUnify_(subTy, superTy, scope, location); } -ErrorVec TypeChecker::tryUnify(TypePackId subTy, TypePackId superTy, const Location& location) +ErrorVec TypeChecker::tryUnify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location) { - return tryUnify_(subTy, superTy, location); + return tryUnify_(subTy, superTy, scope, location); } void TypeChecker::check(const ScopePtr& scope, const AstStatAssign& assign) @@ -1029,9 +1034,9 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatAssign& assign) { // In nonstrict mode, any assignments where the lhs is free and rhs isn't a function, we give it any typevar. if (isNonstrictMode() && get(follow(left)) && !get(follow(right))) - unify(anyType, left, loc); + unify(anyType, left, scope, loc); else - unify(right, left, loc); + unify(right, left, scope, loc); } } } @@ -1046,7 +1051,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatCompoundAssign& assi TypeId result = checkBinaryOperation(scope, expr, left, right); - unify(result, left, assign.location); + unify(result, left, scope, assign.location); } void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local) @@ -1101,7 +1106,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local) TypePackId valuePack = checkExprList(scope, local.location, local.values, /* substituteFreeForNil= */ true, instantiateGenerics, expectedTypes).type; - Unifier state = mkUnifier(local.location); + Unifier state = mkUnifier(scope, local.location); state.ctx = CountMismatch::Result; state.tryUnify(valuePack, variablePack); reportErrors(state.errors); @@ -1177,7 +1182,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatFor& expr) TypeId loopVarType = numberType; if (expr.var->annotation) - unify(loopVarType, resolveType(scope, *expr.var->annotation), expr.location); + unify(loopVarType, resolveType(scope, *expr.var->annotation), scope, expr.location); loopScope->bindings[expr.var] = {loopVarType, expr.var->location}; @@ -1187,11 +1192,11 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatFor& expr) if (!expr.to) ice("Bad AstStatFor has no to expr"); - unify(checkExpr(loopScope, *expr.from).type, loopVarType, expr.from->location); - unify(checkExpr(loopScope, *expr.to).type, loopVarType, expr.to->location); + unify(checkExpr(loopScope, *expr.from).type, loopVarType, scope, expr.from->location); + unify(checkExpr(loopScope, *expr.to).type, loopVarType, scope, expr.to->location); if (expr.step) - unify(checkExpr(loopScope, *expr.step).type, loopVarType, expr.step->location); + unify(checkExpr(loopScope, *expr.step).type, loopVarType, scope, expr.step->location); check(loopScope, *expr.body); } @@ -1244,12 +1249,12 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin) if (get(callRetPack)) { iterTy = freshType(scope); - unify(callRetPack, addTypePack({{iterTy}, freshTypePack(scope)}), forin.location); + unify(callRetPack, addTypePack({{iterTy}, freshTypePack(scope)}), scope, forin.location); } else if (get(callRetPack) || !first(callRetPack)) { for (TypeId var : varTypes) - unify(errorRecoveryType(scope), var, forin.location); + unify(errorRecoveryType(scope), var, scope, forin.location); return check(loopScope, *forin.body); } @@ -1270,7 +1275,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin) // TODO: this needs to typecheck all returned values by __iter as if they were for loop arguments // the structure of the function makes it difficult to do this especially since we don't have actual expressions, only types for (TypeId var : varTypes) - unify(anyType, var, forin.location); + unify(anyType, var, scope, forin.location); return check(loopScope, *forin.body); } @@ -1282,25 +1287,25 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin) if (iterTable->indexer) { if (varTypes.size() > 0) - unify(iterTable->indexer->indexType, varTypes[0], forin.location); + unify(iterTable->indexer->indexType, varTypes[0], scope, forin.location); if (varTypes.size() > 1) - unify(iterTable->indexer->indexResultType, varTypes[1], forin.location); + unify(iterTable->indexer->indexResultType, varTypes[1], scope, forin.location); for (size_t i = 2; i < varTypes.size(); ++i) - unify(nilType, varTypes[i], forin.location); + unify(nilType, varTypes[i], scope, forin.location); } else if (isNonstrictMode()) { for (TypeId var : varTypes) - unify(anyType, var, forin.location); + unify(anyType, var, scope, forin.location); } else { TypeId varTy = errorRecoveryType(loopScope); for (TypeId var : varTypes) - unify(varTy, var, forin.location); + unify(varTy, var, scope, forin.location); reportError(firstValue->location, GenericError{"Cannot iterate over a table without indexer"}); } @@ -1314,7 +1319,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin) TypeId varTy = get(iterTy) ? anyType : errorRecoveryType(loopScope); for (TypeId var : varTypes) - unify(varTy, var, forin.location); + unify(varTy, var, scope, forin.location); if (!get(iterTy) && !get(iterTy) && !get(iterTy) && !get(iterTy)) reportError(firstValue->location, CannotCallNonFunction{iterTy}); @@ -1339,7 +1344,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin) argPack = addTypePack(TypePack{}); } - Unifier state = mkUnifier(firstValue->location); + Unifier state = mkUnifier(loopScope, firstValue->location); checkArgumentList(loopScope, state, argPack, iterFunc->argTypes, /*argLocations*/ {}); state.log.commit(); @@ -1358,10 +1363,10 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin) AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()}; TypePackId retPack = checkExprPack(scope, exprCall).type; - unify(retPack, varPack, forin.location); + unify(retPack, varPack, scope, forin.location); } else - unify(iterFunc->retTypes, varPack, forin.location); + unify(iterFunc->retTypes, varPack, scope, forin.location); check(loopScope, *forin.body); } @@ -1596,7 +1601,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias TypeId& bindingType = bindingsMap[name].type; - if (unify(ty, bindingType, typealias.location)) + if (unify(ty, bindingType, aliasScope, typealias.location)) bindingType = ty; if (FFlag::LuauLowerBoundsCalculation) @@ -1886,7 +1891,7 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp TypeLevel level = FFlag::LuauLowerBoundsCalculation ? ftp->level : scope->level; TypeId head = freshType(level); TypePackId pack = addTypePack(TypePackVar{TypePack{{head}, freshTypePack(level)}}); - unify(pack, retPack, expr.location); + unify(pack, retPack, scope, expr.location); return {head, std::move(result.predicates)}; } if (get(retPack)) @@ -1927,7 +1932,7 @@ std::optional TypeChecker::findTablePropertyRespectingMeta(TypeId lhsTyp { ErrorVec errors; auto result = Luau::findTablePropertyRespectingMeta(errors, lhsType, name, location); - if (!FFlag::LuauIndexSilenceErrors || addErrors) + if (addErrors) reportErrors(errors); return result; } @@ -1936,7 +1941,7 @@ std::optional TypeChecker::findMetatableEntry(TypeId type, std::string e { ErrorVec errors; auto result = Luau::findMetatableEntry(errors, type, entry, location); - if (!FFlag::LuauIndexSilenceErrors || addErrors) + if (addErrors) reportErrors(errors); return result; } @@ -1948,7 +1953,7 @@ std::optional TypeChecker::getIndexTypeFromType( std::optional result = getIndexTypeFromTypeImpl(scope, type, name, location, addErrors); - if (FFlag::LuauIndexSilenceErrors && !addErrors) + if (!addErrors) LUAU_ASSERT(errorCount == currentModule->errors.size()); return result; @@ -1978,20 +1983,15 @@ std::optional TypeChecker::getIndexTypeFromTypeImpl( else if (auto indexer = tableType->indexer) { // TODO: Property lookup should work with string singletons or unions thereof as the indexer key type. - ErrorVec errors = tryUnify(stringType, indexer->indexType, location); + ErrorVec errors = tryUnify(stringType, indexer->indexType, scope, location); - if (FFlag::LuauReportErrorsOnIndexerKeyMismatch) - { - if (errors.empty()) - return indexer->indexResultType; - - if (addErrors) - reportError(location, UnknownProperty{type, name}); - - return std::nullopt; - } - else + if (errors.empty()) return indexer->indexResultType; + + if (addErrors) + reportError(location, UnknownProperty{type, name}); + + return std::nullopt; } else if (tableType->state == TableState::Free) { @@ -2223,8 +2223,8 @@ TypeId TypeChecker::checkExprTable( if (indexer) { - unify(numberType, indexer->indexType, value->location); - unify(valueType, indexer->indexResultType, value->location); + unify(numberType, indexer->indexType, scope, value->location); + unify(valueType, indexer->indexResultType, scope, value->location); } else indexer = TableIndexer{numberType, anyIfNonstrict(valueType)}; @@ -2243,13 +2243,13 @@ TypeId TypeChecker::checkExprTable( if (it != expectedTable->props.end()) { Property expectedProp = it->second; - ErrorVec errors = tryUnify(exprType, expectedProp.type, k->location); + ErrorVec errors = tryUnify(exprType, expectedProp.type, scope, k->location); if (errors.empty()) exprType = expectedProp.type; } else if (expectedTable->indexer && maybeString(expectedTable->indexer->indexType)) { - ErrorVec errors = tryUnify(exprType, expectedTable->indexer->indexResultType, k->location); + ErrorVec errors = tryUnify(exprType, expectedTable->indexer->indexResultType, scope, k->location); if (errors.empty()) exprType = expectedTable->indexer->indexResultType; } @@ -2264,8 +2264,8 @@ TypeId TypeChecker::checkExprTable( if (indexer) { - unify(keyType, indexer->indexType, k->location); - unify(valueType, indexer->indexResultType, value->location); + unify(keyType, indexer->indexType, scope, k->location); + unify(valueType, indexer->indexResultType, scope, value->location); } else if (isNonstrictMode()) { @@ -2406,7 +2406,7 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp TypePackId retTypePack = freshTypePack(scope); TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack)); - Unifier state = mkUnifier(expr.location); + Unifier state = mkUnifier(scope, expr.location); state.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true); state.log.commit(); @@ -2424,7 +2424,7 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp return {errorRecoveryType(scope)}; } - reportErrors(tryUnify(operandType, numberType, expr.location)); + reportErrors(tryUnify(operandType, numberType, scope, expr.location)); return {numberType}; } case AstExprUnary::Len: @@ -2454,7 +2454,7 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp TypePackId retTypePack = addTypePack({numberType}); TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack)); - Unifier state = mkUnifier(expr.location); + Unifier state = mkUnifier(scope, expr.location); state.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true); state.log.commit(); @@ -2504,11 +2504,11 @@ std::string opToMetaTableEntry(const AstExprBinary::Op& op) } } -TypeId TypeChecker::unionOfTypes(TypeId a, TypeId b, const Location& location, bool unifyFreeTypes) +TypeId TypeChecker::unionOfTypes(TypeId a, TypeId b, const ScopePtr& scope, const Location& location, bool unifyFreeTypes) { if (unifyFreeTypes && (get(a) || get(b))) { - if (unify(b, a, location)) + if (unify(b, a, scope, location)) return a; return errorRecoveryType(anyType); @@ -2583,7 +2583,7 @@ TypeId TypeChecker::checkRelationalOperation( { ScopePtr subScope = childScope(scope, subexp->location); resolve(predicates, subScope, true); - return unionOfTypes(rhsType, stripNil(checkExpr(subScope, *subexp->right).type, true), expr.location); + return unionOfTypes(rhsType, stripNil(checkExpr(subScope, *subexp->right).type, true), subScope, expr.location); } } @@ -2619,7 +2619,7 @@ TypeId TypeChecker::checkRelationalOperation( * report any problems that might have been surfaced as a result of this step because we might already * have a better, more descriptive error teed up. */ - Unifier state = mkUnifier(expr.location); + Unifier state = mkUnifier(scope, expr.location); if (!isEquality) { state.tryUnify(rhsType, lhsType); @@ -2698,7 +2698,7 @@ TypeId TypeChecker::checkRelationalOperation( { if (isEquality) { - Unifier state = mkUnifier(expr.location); + Unifier state = mkUnifier(scope, expr.location); state.tryUnify(addTypePack({booleanType}), ftv->retTypes); if (!state.errors.empty()) @@ -2750,11 +2750,11 @@ TypeId TypeChecker::checkRelationalOperation( case AstExprBinary::And: if (lhsIsAny) return lhsType; - return unionOfTypes(rhsType, booleanType, expr.location, false); + return unionOfTypes(rhsType, booleanType, scope, expr.location, false); case AstExprBinary::Or: if (lhsIsAny) return lhsType; - return unionOfTypes(lhsType, rhsType, expr.location); + return unionOfTypes(lhsType, rhsType, scope, expr.location); default: LUAU_ASSERT(0); ice(format("checkRelationalOperation called with incorrect binary expression '%s'", toString(expr.op).c_str()), expr.location); @@ -2811,7 +2811,7 @@ TypeId TypeChecker::checkBinaryOperation( } if (get(rhsType)) - unify(rhsType, lhsType, expr.location); + unify(rhsType, lhsType, scope, expr.location); if (typeCouldHaveMetatable(lhsType) || typeCouldHaveMetatable(rhsType)) { @@ -2821,7 +2821,7 @@ TypeId TypeChecker::checkBinaryOperation( TypePackId retTypePack = freshTypePack(scope); TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack)); - Unifier state = mkUnifier(expr.location); + Unifier state = mkUnifier(scope, expr.location); state.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true); reportErrors(state.errors); @@ -2871,8 +2871,8 @@ TypeId TypeChecker::checkBinaryOperation( switch (expr.op) { case AstExprBinary::Concat: - reportErrors(tryUnify(lhsType, addType(UnionTypeVar{{stringType, numberType}}), expr.left->location)); - reportErrors(tryUnify(rhsType, addType(UnionTypeVar{{stringType, numberType}}), expr.right->location)); + reportErrors(tryUnify(lhsType, addType(UnionTypeVar{{stringType, numberType}}), scope, expr.left->location)); + reportErrors(tryUnify(rhsType, addType(UnionTypeVar{{stringType, numberType}}), scope, expr.right->location)); return stringType; case AstExprBinary::Add: case AstExprBinary::Sub: @@ -2880,8 +2880,8 @@ TypeId TypeChecker::checkBinaryOperation( case AstExprBinary::Div: case AstExprBinary::Mod: case AstExprBinary::Pow: - reportErrors(tryUnify(lhsType, numberType, expr.left->location)); - reportErrors(tryUnify(rhsType, numberType, expr.right->location)); + reportErrors(tryUnify(lhsType, numberType, scope, expr.left->location)); + reportErrors(tryUnify(rhsType, numberType, scope, expr.right->location)); return numberType; default: // These should have been handled with checkRelationalOperation @@ -2956,10 +2956,10 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp WithPredicate result = checkExpr(scope, *expr.expr, annotationType); // Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case. - if (canUnify(annotationType, result.type, expr.location).empty()) + if (canUnify(annotationType, result.type, scope, expr.location).empty()) return {annotationType, std::move(result.predicates)}; - if (canUnify(result.type, annotationType, expr.location).empty()) + if (canUnify(result.type, annotationType, scope, expr.location).empty()) return {annotationType, std::move(result.predicates)}; reportError(expr.location, TypesAreUnrelated{result.type, annotationType}); @@ -3104,7 +3104,7 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex } else if (auto indexer = lhsTable->indexer) { - Unifier state = mkUnifier(expr.location); + Unifier state = mkUnifier(scope, expr.location); state.tryUnify(stringType, indexer->indexType); TypeId retType = indexer->indexResultType; if (!state.errors.empty()) @@ -3216,7 +3216,7 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex if (exprTable->indexer) { const TableIndexer& indexer = *exprTable->indexer; - unify(indexType, indexer.indexType, expr.index->location); + unify(indexType, indexer.indexType, scope, expr.index->location); return indexer.indexResultType; } else if (exprTable->state == TableState::Unsealed || exprTable->state == TableState::Free) @@ -3832,7 +3832,7 @@ void TypeChecker::checkArgumentList( { // The use of unify here is deliberate. We don't want this unification // to be undoable. - unify(errorRecoveryType(scope), *argIter, state.location); + unify(errorRecoveryType(scope), *argIter, scope, state.location); ++argIter; } reportCountMismatchError(); @@ -3855,7 +3855,7 @@ void TypeChecker::checkArgumentList( TypeId e = errorRecoveryType(scope); while (argIter != endIter) { - unify(e, *argIter, state.location); + unify(e, *argIter, scope, state.location); ++argIter; } @@ -3872,7 +3872,7 @@ void TypeChecker::checkArgumentList( if (argIndex < argLocations.size()) location = argLocations[argIndex]; - unify(*argIter, vtp->ty, location); + unify(*argIter, vtp->ty, scope, location); ++argIter; ++argIndex; } @@ -3909,7 +3909,7 @@ void TypeChecker::checkArgumentList( } else { - unifyWithInstantiationIfNeeded(scope, *argIter, *paramIter, state); + unifyWithInstantiationIfNeeded(*argIter, *paramIter, scope, state); ++argIter; ++paramIter; } @@ -4117,7 +4117,7 @@ std::optional> TypeChecker::checkCallOverload(const Sc if (get(fn)) { - unify(anyTypePack, argPack, expr.location); + unify(anyTypePack, argPack, scope, expr.location); return {{anyTypePack}}; } @@ -4163,7 +4163,7 @@ std::optional> TypeChecker::checkCallOverload(const Sc UnifierOptions options; options.isFunctionCall = true; - unify(r, fn, expr.location, options); + unify(r, fn, scope, expr.location, options); return {{retPack}}; } @@ -4197,7 +4197,7 @@ std::optional> TypeChecker::checkCallOverload(const Sc if (!ftv) { reportError(TypeError{expr.func->location, CannotCallNonFunction{fn}}); - unify(errorRecoveryTypePack(scope), retPack, expr.func->location); + unify(errorRecoveryTypePack(scope), retPack, scope, expr.func->location); return {{errorRecoveryTypePack(retPack)}}; } @@ -4210,7 +4210,7 @@ std::optional> TypeChecker::checkCallOverload(const Sc return *ret; } - Unifier state = mkUnifier(expr.location); + Unifier state = mkUnifier(scope, expr.location); // Unify return types checkArgumentList(scope, state, retPack, ftv->retTypes, /*argLocations*/ {}); @@ -4272,7 +4272,7 @@ bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCal std::vector editedParamList(args->head.begin() + 1, args->head.end()); TypePackId editedArgPack = addTypePack(TypePack{editedParamList}); - Unifier editedState = mkUnifier(expr.location); + Unifier editedState = mkUnifier(scope, expr.location); checkArgumentList(scope, editedState, editedArgPack, ftv->argTypes, editedArgLocations); if (editedState.errors.empty()) @@ -4302,7 +4302,7 @@ bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCal editedArgList.insert(editedArgList.begin(), checkExpr(scope, *indexName->expr).type); TypePackId editedArgPack = addTypePack(TypePack{editedArgList}); - Unifier editedState = mkUnifier(expr.location); + Unifier editedState = mkUnifier(scope, expr.location); checkArgumentList(scope, editedState, editedArgPack, ftv->argTypes, editedArgLocations); @@ -4368,7 +4368,7 @@ void TypeChecker::reportOverloadResolutionError(const ScopePtr& scope, const Ast for (size_t i = 0; i < overloadTypes.size(); ++i) { TypeId overload = overloadTypes[i]; - Unifier state = mkUnifier(expr.location); + Unifier state = mkUnifier(scope, expr.location); // Unify return types if (const FunctionTypeVar* ftv = get(overload)) @@ -4418,7 +4418,7 @@ WithPredicate TypeChecker::checkExprList(const ScopePtr& scope, cons size_t lastIndex = exprs.size - 1; tp->head.reserve(lastIndex); - Unifier state = mkUnifier(location); + Unifier state = mkUnifier(scope, location); std::vector inverseLogs; @@ -4583,15 +4583,15 @@ TypeId TypeChecker::anyIfNonstrict(TypeId ty) const return ty; } -bool TypeChecker::unify(TypeId subTy, TypeId superTy, const Location& location) +bool TypeChecker::unify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location) { UnifierOptions options; - return unify(subTy, superTy, location, options); + return unify(subTy, superTy, scope, location, options); } -bool TypeChecker::unify(TypeId subTy, TypeId superTy, const Location& location, const UnifierOptions& options) +bool TypeChecker::unify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location, const UnifierOptions& options) { - Unifier state = mkUnifier(location); + Unifier state = mkUnifier(scope, location); state.tryUnify(subTy, superTy, options.isFunctionCall); state.log.commit(); @@ -4601,9 +4601,9 @@ bool TypeChecker::unify(TypeId subTy, TypeId superTy, const Location& location, return state.errors.empty(); } -bool TypeChecker::unify(TypePackId subTy, TypePackId superTy, const Location& location, CountMismatch::Context ctx) +bool TypeChecker::unify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location, CountMismatch::Context ctx) { - Unifier state = mkUnifier(location); + Unifier state = mkUnifier(scope, location); state.ctx = ctx; state.tryUnify(subTy, superTy); @@ -4614,10 +4614,10 @@ bool TypeChecker::unify(TypePackId subTy, TypePackId superTy, const Location& lo return state.errors.empty(); } -bool TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, const Location& location) +bool TypeChecker::unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location) { - Unifier state = mkUnifier(location); - unifyWithInstantiationIfNeeded(scope, subTy, superTy, state); + Unifier state = mkUnifier(scope, location); + unifyWithInstantiationIfNeeded(subTy, superTy, scope, state); state.log.commit(); @@ -4626,7 +4626,7 @@ bool TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId s return state.errors.empty(); } -void TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, Unifier& state) +void TypeChecker::unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, Unifier& state) { if (!maybeGeneric(subTy)) // Quick check to see if we definitely can't instantiate @@ -4665,77 +4665,6 @@ void TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId s } } -bool Anyification::isDirty(TypeId ty) -{ - if (ty->persistent) - return false; - - if (const TableTypeVar* ttv = log->getMutable(ty)) - return (ttv->state == TableState::Free || ttv->state == TableState::Unsealed); - else if (log->getMutable(ty)) - return true; - else if (get(ty)) - return true; - else - return false; -} - -bool Anyification::isDirty(TypePackId tp) -{ - if (tp->persistent) - return false; - - if (log->getMutable(tp)) - return true; - else - return false; -} - -TypeId Anyification::clean(TypeId ty) -{ - LUAU_ASSERT(isDirty(ty)); - if (const TableTypeVar* ttv = log->getMutable(ty)) - { - TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, ttv->level, TableState::Sealed}; - clone.definitionModuleName = ttv->definitionModuleName; - clone.name = ttv->name; - clone.syntheticName = ttv->syntheticName; - clone.tags = ttv->tags; - TypeId res = addType(std::move(clone)); - asMutable(res)->normal = ty->normal; - return res; - } - else if (auto ctv = get(ty)) - { - if (FFlag::LuauQuantifyConstrained) - { - std::vector copy = ctv->parts; - for (TypeId& ty : copy) - ty = replace(ty); - TypeId res = copy.size() == 1 ? copy[0] : addType(UnionTypeVar{std::move(copy)}); - auto [t, ok] = normalize(res, *arena, *iceHandler); - if (!ok) - normalizationTooComplex = true; - return t; - } - else - { - auto [t, ok] = normalize(ty, *arena, *iceHandler); - if (!ok) - normalizationTooComplex = true; - return t; - } - } - else - return anyType; -} - -TypePackId Anyification::clean(TypePackId tp) -{ - LUAU_ASSERT(isDirty(tp)); - return anyTypePack; -} - TypeId TypeChecker::quantify(const ScopePtr& scope, TypeId ty, Location location) { ty = follow(ty); @@ -4807,7 +4736,7 @@ TypeId TypeChecker::anyify(const ScopePtr& scope, TypeId ty, Location location) ty = t; } - Anyification anyification{¤tModule->internalTypes, iceHandler, anyType, anyTypePack}; + Anyification anyification{¤tModule->internalTypes, scope, iceHandler, anyType, anyTypePack}; std::optional any = anyification.substitute(ty); if (anyification.normalizationTooComplex) reportError(location, NormalizationTooComplex{}); @@ -4830,7 +4759,7 @@ TypePackId TypeChecker::anyify(const ScopePtr& scope, TypePackId ty, Location lo ty = t; } - Anyification anyification{¤tModule->internalTypes, iceHandler, anyType, anyTypePack}; + Anyification anyification{¤tModule->internalTypes, scope, iceHandler, anyType, anyTypePack}; std::optional any = anyification.substitute(ty); if (any.has_value()) return *any; @@ -4966,9 +4895,9 @@ void TypeChecker::merge(RefinementMap& l, const RefinementMap& r) }); } -Unifier TypeChecker::mkUnifier(const Location& location) +Unifier TypeChecker::mkUnifier(const ScopePtr& scope, const Location& location) { - return Unifier{¤tModule->internalTypes, currentModule->mode, location, Variance::Covariant, unifierState}; + return Unifier{¤tModule->internalTypes, currentModule->mode, NotNull{scope.get()}, location, Variance::Covariant, unifierState}; } TypeId TypeChecker::freshType(const ScopePtr& scope) @@ -5032,10 +4961,7 @@ TypeIdPredicate TypeChecker::mkTruthyPredicate(bool sense) return sense ? std::nullopt : std::optional(ty); // at this point, anything else is kept if sense is true, or replaced by nil - if (FFlag::LuauFalsyPredicateReturnsNilInstead) - return sense ? ty : nilType; - else - return sense ? std::optional(ty) : std::nullopt; + return sense ? ty : nilType; }; } @@ -5878,8 +5804,8 @@ void TypeChecker::resolve(const IsAPredicate& isaP, RefinementMap& refis, const { auto predicate = [&](TypeId option) -> std::optional { // This by itself is not truly enough to determine that A is stronger than B or vice versa. - bool optionIsSubtype = canUnify(option, isaP.ty, isaP.location).empty(); - bool targetIsSubtype = canUnify(isaP.ty, option, isaP.location).empty(); + bool optionIsSubtype = canUnify(option, isaP.ty, scope, isaP.location).empty(); + bool targetIsSubtype = canUnify(isaP.ty, option, scope, isaP.location).empty(); // If A is a superset of B, then if sense is true, we promote A to B, otherwise we keep A. if (!optionIsSubtype && targetIsSubtype) @@ -6022,7 +5948,7 @@ void TypeChecker::resolve(const EqPredicate& eqP, RefinementMap& refis, const Sc if (maybeSingleton(eqP.type)) { // Normally we'd write option <: eqP.type, but singletons are always the subtype, so we flip this. - if (!sense || canUnify(eqP.type, option, eqP.location).empty()) + if (!sense || canUnify(eqP.type, option, scope, eqP.location).empty()) return sense ? eqP.type : option; // local variable works around an odd gcc 9.3 warning: may be used uninitialized @@ -6056,7 +5982,7 @@ std::vector TypeChecker::unTypePack(const ScopePtr& scope, TypePackId tp size_t oldErrorsSize = currentModule->errors.size(); - unify(tp, expectedTypePack, location); + unify(tp, expectedTypePack, scope, location); // HACK: tryUnify would undo the changes to the expectedTypePack if the length mismatches, but // we want to tie up free types to be error types, so we do this instead. diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index 66b38cf3..60bca0a3 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypeUtils.h" +#include "Luau/Normalize.h" #include "Luau/Scope.h" #include "Luau/ToString.h" #include "Luau/TypeInfer.h" @@ -8,7 +9,7 @@ namespace Luau { -std::optional findMetatableEntry(ErrorVec& errors, TypeId type, std::string entry, Location location) +std::optional findMetatableEntry(ErrorVec& errors, TypeId type, const std::string& entry, Location location) { type = follow(type); @@ -35,7 +36,7 @@ std::optional findMetatableEntry(ErrorVec& errors, TypeId type, std::str return std::nullopt; } -std::optional findTablePropertyRespectingMeta(ErrorVec& errors, TypeId ty, Name name, Location location) +std::optional findTablePropertyRespectingMeta(ErrorVec& errors, TypeId ty, const std::string& name, Location location) { if (get(ty)) return ty; @@ -83,4 +84,110 @@ std::optional findTablePropertyRespectingMeta(ErrorVec& errors, TypeId t return std::nullopt; } +std::optional getIndexTypeFromType( + const ScopePtr& scope, ErrorVec& errors, TypeArena* arena, TypeId type, const std::string& prop, const Location& location, bool addErrors, + InternalErrorReporter& handle) +{ + type = follow(type); + + if (get(type) || get(type) || get(type)) + return type; + + if (auto f = get(type)) + *asMutable(type) = TableTypeVar{TableState::Free, f->level}; + + if (isString(type)) + { + std::optional mtIndex = Luau::findMetatableEntry(errors, getSingletonTypes().stringType, "__index", location); + LUAU_ASSERT(mtIndex); + type = *mtIndex; + } + + if (getTableType(type)) + { + return findTablePropertyRespectingMeta(errors, type, prop, location); + } + else if (const ClassTypeVar* cls = get(type)) + { + if (const Property* p = lookupClassProp(cls, prop)) + return p->type; + } + else if (const UnionTypeVar* utv = get(type)) + { + std::vector goodOptions; + std::vector badOptions; + + for (TypeId t : utv) + { + // TODO: we should probably limit recursion here? + // RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit); + + // Not needed when we normalize types. + if (get(follow(t))) + return t; + + if (std::optional ty = getIndexTypeFromType(scope, errors, arena, t, prop, location, /* addErrors= */ false, handle)) + goodOptions.push_back(*ty); + else + badOptions.push_back(t); + } + + if (!badOptions.empty()) + { + if (addErrors) + { + if (goodOptions.empty()) + errors.push_back(TypeError{location, UnknownProperty{type, prop}}); + else + errors.push_back(TypeError{location, MissingUnionProperty{type, badOptions, prop}}); + } + return std::nullopt; + } + + if (goodOptions.empty()) + return getSingletonTypes().neverType; + + if (goodOptions.size() == 1) + return goodOptions[0]; + + // TODO: inefficient. + TypeId result = arena->addType(UnionTypeVar{std::move(goodOptions)}); + auto [ty, ok] = normalize(result, NotNull{scope.get()}, *arena, handle); + if (!ok && addErrors) + errors.push_back(TypeError{location, NormalizationTooComplex{}}); + return ok ? ty : getSingletonTypes().anyType; + } + else if (const IntersectionTypeVar* itv = get(type)) + { + std::vector parts; + + for (TypeId t : itv->parts) + { + // TODO: we should probably limit recursion here? + // RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit); + + if (std::optional ty = getIndexTypeFromType(scope, errors, arena, t, prop, location, /* addErrors= */ false, handle)) + parts.push_back(*ty); + } + + // If no parts of the intersection had the property we looked up for, it never existed at all. + if (parts.empty()) + { + if (addErrors) + errors.push_back(TypeError{location, UnknownProperty{type, prop}}); + return std::nullopt; + } + + if (parts.size() == 1) + return parts[0]; + + return arena->addType(IntersectionTypeVar{std::move(parts)}); // Not at all correct. + } + + if (addErrors) + errors.push_back(TypeError{location, UnknownProperty{type, prop}}); + + return std::nullopt; +} + } // namespace Luau diff --git a/Analysis/src/TypeVar.cpp b/Analysis/src/TypeVar.cpp index ada2b012..9020b1ac 100644 --- a/Analysis/src/TypeVar.cpp +++ b/Analysis/src/TypeVar.cpp @@ -1135,7 +1135,7 @@ std::optional> magicFunctionFormat( { Location location = expr.args.data[std::min(i + dataOffset, expr.args.size - 1)]->location; - typechecker.unify(params[i + paramOffset], expected[i], location); + typechecker.unify(params[i + paramOffset], expected[i], scope, location); } // if we know the argument count or if we have too many arguments for sure, we can issue an error @@ -1234,7 +1234,7 @@ static std::optional> magicFunctionGmatch( if (returnTypes.empty()) return std::nullopt; - typechecker.unify(params[0], typechecker.stringType, expr.args.data[0]->location); + typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location); const TypePackId emptyPack = arena.addTypePack({}); const TypePackId returnList = arena.addTypePack(returnTypes); @@ -1269,13 +1269,13 @@ static std::optional> magicFunctionMatch( if (returnTypes.empty()) return std::nullopt; - typechecker.unify(params[0], typechecker.stringType, expr.args.data[0]->location); + typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location); const TypeId optionalNumber = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.numberType}}); size_t initIndex = expr.self ? 1 : 2; if (params.size() == 3 && expr.args.size > initIndex) - typechecker.unify(params[2], optionalNumber, expr.args.data[initIndex]->location); + typechecker.unify(params[2], optionalNumber, scope, expr.args.data[initIndex]->location); const TypePackId returnList = arena.addTypePack(returnTypes); return WithPredicate{returnList}; @@ -1320,17 +1320,17 @@ static std::optional> magicFunctionFind( return std::nullopt; } - typechecker.unify(params[0], typechecker.stringType, expr.args.data[0]->location); + typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location); const TypeId optionalNumber = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.numberType}}); const TypeId optionalBoolean = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.booleanType}}); size_t initIndex = expr.self ? 1 : 2; if (params.size() >= 3 && expr.args.size > initIndex) - typechecker.unify(params[2], optionalNumber, expr.args.data[initIndex]->location); + typechecker.unify(params[2], optionalNumber, scope, expr.args.data[initIndex]->location); if (params.size() == 4 && expr.args.size > plainIndex) - typechecker.unify(params[3], optionalBoolean, expr.args.data[plainIndex]->location); + typechecker.unify(params[3], optionalBoolean, scope, expr.args.data[plainIndex]->location); returnTypes.insert(returnTypes.begin(), {optionalNumber, optionalNumber}); diff --git a/Analysis/src/TypedAllocator.cpp b/Analysis/src/TypedAllocator.cpp index c7f31822..9ce8c3dc 100644 --- a/Analysis/src/TypedAllocator.cpp +++ b/Analysis/src/TypedAllocator.cpp @@ -27,27 +27,6 @@ LUAU_FASTFLAG(DebugLuauFreezeArena) namespace Luau { -static void* systemAllocateAligned(size_t size, size_t align) -{ -#ifdef _WIN32 - return _aligned_malloc(size, align); -#elif defined(__ANDROID__) // for Android 4.1 - return memalign(align, size); -#else - void* ptr; - return posix_memalign(&ptr, align, size) == 0 ? ptr : 0; -#endif -} - -static void systemDeallocateAligned(void* ptr) -{ -#ifdef _WIN32 - _aligned_free(ptr); -#else - free(ptr); -#endif -} - static size_t pageAlign(size_t size) { return (size + kPageSize - 1) & ~(kPageSize - 1); @@ -55,18 +34,31 @@ static size_t pageAlign(size_t size) void* pagedAllocate(size_t size) { - if (FFlag::DebugLuauFreezeArena) - return systemAllocateAligned(pageAlign(size), kPageSize); - else + // By default we use operator new/delete instead of malloc/free so that they can be overridden externally + if (!FFlag::DebugLuauFreezeArena) return ::operator new(size, std::nothrow); + + // On Windows, VirtualAlloc results in 64K granularity allocations; we allocate in chunks of ~32K so aligned_malloc is a little more efficient + // On Linux, we must use mmap because using regular heap results in mprotect() fragmenting the page table and us bumping into 64K mmap limit. +#ifdef _WIN32 + return _aligned_malloc(size, kPageSize); +#else + return mmap(nullptr, pageAlign(size), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); +#endif } -void pagedDeallocate(void* ptr) +void pagedDeallocate(void* ptr, size_t size) { - if (FFlag::DebugLuauFreezeArena) - systemDeallocateAligned(ptr); - else - ::operator delete(ptr); + // By default we use operator new/delete instead of malloc/free so that they can be overridden externally + if (!FFlag::DebugLuauFreezeArena) + return ::operator delete(ptr); + +#ifdef _WIN32 + _aligned_free(ptr); +#else + int rc = munmap(ptr, size); + LUAU_ASSERT(rc == 0); +#endif } void pagedFreeze(void* ptr, size_t size) diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index e099817f..b5f58c83 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -20,8 +20,8 @@ LUAU_FASTINTVARIABLE(LuauTypeInferLowerBoundsIterationLimit, 2000); LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(LuauErrorRecoveryType); LUAU_FASTFLAG(LuauUnknownAndNeverType) -LUAU_FASTFLAG(LuauQuantifyConstrained) LUAU_FASTFLAGVARIABLE(LuauScalarShapeSubtyping, false) +LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution) namespace Luau { @@ -273,6 +273,9 @@ TypePackId Widen::clean(TypePackId) bool Widen::ignoreChildren(TypeId ty) { + if (FFlag::LuauClassTypeVarsInSubstitution && get(ty)) + return true; + return !log->is(ty); } @@ -314,9 +317,10 @@ static std::optional> getTableMat return std::nullopt; } -Unifier::Unifier(TypeArena* types, Mode mode, const Location& location, Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog) +Unifier::Unifier(TypeArena* types, Mode mode, NotNull scope, const Location& location, Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog) : types(types) , mode(mode) + , scope(scope) , log(parentLog) , location(location) , variance(variance) @@ -370,25 +374,14 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool if (superFree && subFree && superFree->level.subsumes(subFree->level)) { - occursCheck(subTy, superTy); - - // The occurrence check might have caused superTy no longer to be a free type - bool occursFailed = bool(log.getMutable(subTy)); - - if (!occursFailed) - { + if (!occursCheck(subTy, superTy)) log.replace(subTy, BoundTypeVar(superTy)); - } return; } else if (superFree && subFree) { - occursCheck(superTy, subTy); - - bool occursFailed = bool(log.getMutable(superTy)); - - if (!occursFailed) + if (!occursCheck(superTy, subTy)) { if (superFree->level.subsumes(subFree->level)) { @@ -402,24 +395,18 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool } else if (superFree) { - TypeLevel superLevel = superFree->level; - - occursCheck(superTy, subTy); - bool occursFailed = bool(log.getMutable(superTy)); - // Unification can't change the level of a generic. auto subGeneric = log.getMutable(subTy); - if (subGeneric && !subGeneric->level.subsumes(superLevel)) + if (subGeneric && !subGeneric->level.subsumes(superFree->level)) { // TODO: a more informative error message? CLI-39912 reportError(TypeError{location, GenericError{"Generic subtype escaping scope"}}); return; } - // The occurrence check might have caused superTy no longer to be a free type - if (!occursFailed) + if (!occursCheck(superTy, subTy)) { - promoteTypeLevels(log, types, superLevel, subTy); + promoteTypeLevels(log, types, superFree->level, subTy); Widen widen{types}; log.replace(superTy, BoundTypeVar(widen(subTy))); @@ -437,11 +424,6 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool return; } - TypeLevel subLevel = subFree->level; - - occursCheck(subTy, superTy); - bool occursFailed = bool(log.getMutable(subTy)); - // Unification can't change the level of a generic. auto superGeneric = log.getMutable(superTy); if (superGeneric && !superGeneric->level.subsumes(subFree->level)) @@ -451,9 +433,9 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool return; } - if (!occursFailed) + if (!occursCheck(subTy, superTy)) { - promoteTypeLevels(log, types, subLevel, superTy); + promoteTypeLevels(log, types, subFree->level, superTy); log.replace(subTy, BoundTypeVar(superTy)); } @@ -1033,9 +1015,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal if (log.getMutable(superTp)) { - occursCheck(superTp, subTp); - - if (!log.getMutable(superTp)) + if (!occursCheck(superTp, subTp)) { Widen widen{types}; log.replace(superTp, Unifiable::Bound(widen(subTp))); @@ -1043,9 +1023,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal } else if (log.getMutable(subTp)) { - occursCheck(subTp, superTp); - - if (!log.getMutable(subTp)) + if (!occursCheck(subTp, superTp)) { log.replace(subTp, Unifiable::Bound(superTp)); } @@ -2106,20 +2084,18 @@ void Unifier::unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel de { TypePackId tailPack = follow(*t); - if (log.get(tailPack)) - occursCheck(tailPack, subTy); + if (log.get(tailPack) && occursCheck(tailPack, subTy)) + return; FreeTypePack* freeTailPack = log.getMutable(tailPack); if (!freeTailPack) return; - TypeLevel level = FFlag::LuauQuantifyConstrained ? demotedLevel : freeTailPack->level; - TypePack* tp = getMutable(log.replace(tailPack, TypePack{})); for (; subIter != subEndIter; ++subIter) { - tp->head.push_back(types->addType(ConstrainedTypeVar{level, {follow(*subIter)}})); + tp->head.push_back(types->addType(ConstrainedTypeVar{demotedLevel, {follow(*subIter)}})); } tp->tail = subIter.tail(); @@ -2180,32 +2156,35 @@ void Unifier::unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel de } } -void Unifier::occursCheck(TypeId needle, TypeId haystack) +bool Unifier::occursCheck(TypeId needle, TypeId haystack) { sharedState.tempSeenTy.clear(); return occursCheck(sharedState.tempSeenTy, needle, haystack); } -void Unifier::occursCheck(DenseHashSet& seen, TypeId needle, TypeId haystack) +bool Unifier::occursCheck(DenseHashSet& seen, TypeId needle, TypeId haystack) { RecursionLimiter _ra(&sharedState.counters.recursionCount, FFlag::LuauAutocompleteDynamicLimits ? sharedState.counters.recursionLimit : FInt::LuauTypeInferRecursionLimit); + bool occurrence = false; + auto check = [&](TypeId tv) { - occursCheck(seen, needle, tv); + if (occursCheck(seen, needle, tv)) + occurrence = true; }; needle = log.follow(needle); haystack = log.follow(haystack); if (seen.find(haystack)) - return; + return false; seen.insert(haystack); if (log.getMutable(needle)) - return; + return false; if (!log.getMutable(needle)) ice("Expected needle to be free"); @@ -2215,11 +2194,11 @@ void Unifier::occursCheck(DenseHashSet& seen, TypeId needle, TypeId hays reportError(TypeError{location, OccursCheckFailed{}}); log.replace(needle, *getSingletonTypes().errorRecoveryType()); - return; + return true; } if (log.getMutable(haystack)) - return; + return false; else if (auto a = log.getMutable(haystack)) { for (TypeId ty : a->options) @@ -2235,27 +2214,29 @@ void Unifier::occursCheck(DenseHashSet& seen, TypeId needle, TypeId hays for (TypeId ty : a->parts) check(ty); } + + return occurrence; } -void Unifier::occursCheck(TypePackId needle, TypePackId haystack) +bool Unifier::occursCheck(TypePackId needle, TypePackId haystack) { sharedState.tempSeenTp.clear(); return occursCheck(sharedState.tempSeenTp, needle, haystack); } -void Unifier::occursCheck(DenseHashSet& seen, TypePackId needle, TypePackId haystack) +bool Unifier::occursCheck(DenseHashSet& seen, TypePackId needle, TypePackId haystack) { needle = log.follow(needle); haystack = log.follow(haystack); if (seen.find(haystack)) - return; + return false; seen.insert(haystack); if (log.getMutable(needle)) - return; + return false; if (!log.getMutable(needle)) ice("Expected needle pack to be free"); @@ -2270,7 +2251,7 @@ void Unifier::occursCheck(DenseHashSet& seen, TypePackId needle, Typ reportError(TypeError{location, OccursCheckFailed{}}); log.replace(needle, *getSingletonTypes().errorRecoveryTypePack()); - return; + return true; } if (auto a = get(haystack); a && a->tail) @@ -2281,11 +2262,13 @@ void Unifier::occursCheck(DenseHashSet& seen, TypePackId needle, Typ break; } + + return false; } Unifier Unifier::makeChildUnifier() { - Unifier u = Unifier{types, mode, location, variance, sharedState, &log}; + Unifier u = Unifier{types, mode, scope, location, variance, sharedState, &log}; u.anyIsTop = anyIsTop; return u; } diff --git a/Ast/include/Luau/Parser.h b/Ast/include/Luau/Parser.h index 32229ded..6f56f90d 100644 --- a/Ast/include/Luau/Parser.h +++ b/Ast/include/Luau/Parser.h @@ -138,7 +138,7 @@ private: // funcbody ::= `(' [parlist] `)' block end // parlist ::= namelist [`,' `...'] | `...' std::pair parseFunctionBody( - bool hasself, const Lexeme& matchFunction, const AstName& debugname, std::optional localName); + bool hasself, const Lexeme& matchFunction, const AstName& debugname, const Name* localName); // explist ::= {exp `,'} exp void parseExprList(TempVector& result); @@ -217,7 +217,7 @@ private: AstExpr* parseSimpleExpr(); // args ::= `(' [explist] `)' | tableconstructor | String - AstExpr* parseFunctionArgs(AstExpr* func, bool self, const Location& selfLocation); + AstExpr* parseFunctionArgs(AstExpr* func, bool self); // tableconstructor ::= `{' [fieldlist] `}' // fieldlist ::= field {fieldsep field} [fieldsep] @@ -244,6 +244,7 @@ private: std::optional> parseCharArray(); AstExpr* parseString(); + AstExpr* parseNumber(); AstLocal* pushLocal(const Binding& binding); @@ -256,11 +257,24 @@ private: bool expectAndConsume(Lexeme::Type type, const char* context = nullptr); void expectAndConsumeFail(Lexeme::Type type, const char* context); - bool expectMatchAndConsume(char value, const Lexeme& begin, bool searchForMissing = false); - void expectMatchAndConsumeFail(Lexeme::Type type, const Lexeme& begin, const char* extra = nullptr); + struct MatchLexeme + { + MatchLexeme(const Lexeme& l) + : type(l.type) + , position(l.location.begin) + { + } - bool expectMatchEndAndConsume(Lexeme::Type type, const Lexeme& begin); - void expectMatchEndAndConsumeFail(Lexeme::Type type, const Lexeme& begin); + Lexeme::Type type; + Position position; + }; + + bool expectMatchAndConsume(char value, const MatchLexeme& begin, bool searchForMissing = false); + void expectMatchAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin, const char* extra = nullptr); + bool expectMatchAndConsumeRecover(char value, const MatchLexeme& begin, bool searchForMissing); + + bool expectMatchEndAndConsume(Lexeme::Type type, const MatchLexeme& begin); + void expectMatchEndAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin); template AstArray copy(const T* data, std::size_t size); @@ -286,6 +300,9 @@ private: AstTypeError* reportTypeAnnotationError(const Location& location, const AstArray& types, bool isMissing, const char* format, ...) LUAU_PRINTF_ATTR(5, 6); + AstExpr* reportFunctionArgsError(AstExpr* func, bool self); + void reportAmbiguousCallError(); + void nextLexeme(); struct Function @@ -353,7 +370,7 @@ private: AstName nameError; AstName nameNil; - Lexeme endMismatchSuspect; + MatchLexeme endMismatchSuspect; std::vector functionStack; diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 7e10eb0a..621b0374 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -14,8 +14,6 @@ LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) -LUAU_FASTFLAGVARIABLE(LuauParserFunctionKeywordAsTypeHelp, false) - LUAU_FASTFLAGVARIABLE(LuauFixNamedFunctionParse, false) LUAU_DYNAMIC_FASTFLAGVARIABLE(LuaReportParseWrongNamedType, false) @@ -181,7 +179,7 @@ Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Alloc , lexer(buffer, bufferSize, names) , allocator(allocator) , recursionCounter(0) - , endMismatchSuspect(Location(), Lexeme::Eof) + , endMismatchSuspect(Lexeme(Location(), Lexeme::Eof)) , localMap(AstName()) { Function top; @@ -661,7 +659,7 @@ AstStat* Parser::parseFunctionStat() matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; - AstExprFunction* body = parseFunctionBody(hasself, matchFunction, debugname, {}).first; + AstExprFunction* body = parseFunctionBody(hasself, matchFunction, debugname, nullptr).first; matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; @@ -690,7 +688,7 @@ AstStat* Parser::parseLocal() matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; - auto [body, var] = parseFunctionBody(false, matchFunction, name.name, name); + auto [body, var] = parseFunctionBody(false, matchFunction, name.name, &name); matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; @@ -782,7 +780,7 @@ AstDeclaredClassProp Parser::parseDeclaredClassMethod() genericPacks.size = 0; genericPacks.data = nullptr; - Lexeme matchParen = lexer.current(); + MatchLexeme matchParen = lexer.current(); expectAndConsume('(', "function parameter list start"); TempVector args(scratchBinding); @@ -838,7 +836,7 @@ AstStat* Parser::parseDeclaration(const Location& start) auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false); - Lexeme matchParen = lexer.current(); + MatchLexeme matchParen = lexer.current(); expectAndConsume('(', "global function declaration"); @@ -974,13 +972,13 @@ AstStat* Parser::parseCompoundAssignment(AstExpr* initial, AstExprBinary::Op op) // funcbody ::= `(' [parlist] `)' [`:' ReturnType] block end // parlist ::= bindinglist [`,' `...'] | `...' std::pair Parser::parseFunctionBody( - bool hasself, const Lexeme& matchFunction, const AstName& debugname, std::optional localName) + bool hasself, const Lexeme& matchFunction, const AstName& debugname, const Name* localName) { Location start = matchFunction.location; auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false); - Lexeme matchParen = lexer.current(); + MatchLexeme matchParen = lexer.current(); expectAndConsume('(', "function"); TempVector args(scratchBinding); @@ -992,7 +990,7 @@ std::pair Parser::parseFunctionBody( std::tie(vararg, varargAnnotation) = parseBindingList(args, /* allowDot3= */ true); std::optional argLocation = matchParen.type == Lexeme::Type('(') && lexer.current().type == Lexeme::Type(')') - ? std::make_optional(Location(matchParen.location.begin, lexer.current().location.end)) + ? std::make_optional(Location(matchParen.position, lexer.current().location.end)) : std::nullopt; expectMatchAndConsume(')', matchParen, true); @@ -1259,7 +1257,7 @@ AstType* Parser::parseTableTypeAnnotation() Location start = lexer.current().location; - Lexeme matchBrace = lexer.current(); + MatchLexeme matchBrace = lexer.current(); expectAndConsume('{', "table type"); while (lexer.current().type != '}') @@ -1638,7 +1636,7 @@ AstTypeOrPack Parser::parseSimpleTypeAnnotation(bool allowPack) { return parseFunctionTypeAnnotation(allowPack); } - else if (FFlag::LuauParserFunctionKeywordAsTypeHelp && lexer.current().type == Lexeme::ReservedFunction) + else if (lexer.current().type == Lexeme::ReservedFunction) { Location location = lexer.current().location; @@ -1922,14 +1920,14 @@ AstExpr* Parser::parsePrefixExpr() { if (lexer.current().type == '(') { - Location start = lexer.current().location; + Position start = lexer.current().location.begin; - Lexeme matchParen = lexer.current(); + MatchLexeme matchParen = lexer.current(); nextLexeme(); AstExpr* expr = parseExpr(); - Location end = lexer.current().location; + Position end = lexer.current().location.end; if (lexer.current().type != ')') { @@ -1937,7 +1935,7 @@ AstExpr* Parser::parsePrefixExpr() expectMatchAndConsumeFail(static_cast(')'), matchParen, suggestion); - end = lexer.previousLocation(); + end = lexer.previousLocation().end; } else { @@ -1955,7 +1953,7 @@ AstExpr* Parser::parsePrefixExpr() // primaryexp -> prefixexp { `.' NAME | `[' exp `]' | `:' NAME funcargs | funcargs } AstExpr* Parser::parsePrimaryExpr(bool asStatement) { - Location start = lexer.current().location; + Position start = lexer.current().location.begin; AstExpr* expr = parsePrefixExpr(); @@ -1970,16 +1968,16 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement) Name index = parseIndexName(nullptr, opPosition); - expr = allocator.alloc(Location(start, index.location), expr, index.name, index.location, opPosition, '.'); + expr = allocator.alloc(Location(start, index.location.end), expr, index.name, index.location, opPosition, '.'); } else if (lexer.current().type == '[') { - Lexeme matchBracket = lexer.current(); + MatchLexeme matchBracket = lexer.current(); nextLexeme(); AstExpr* index = parseExpr(); - Location end = lexer.current().location; + Position end = lexer.current().location.end; expectMatchAndConsume(']', matchBracket); @@ -1991,23 +1989,20 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement) nextLexeme(); Name index = parseIndexName("method name", opPosition); - AstExpr* func = allocator.alloc(Location(start, index.location), expr, index.name, index.location, opPosition, ':'); + AstExpr* func = allocator.alloc(Location(start, index.location.end), expr, index.name, index.location, opPosition, ':'); - expr = parseFunctionArgs(func, true, index.location); + expr = parseFunctionArgs(func, true); } else if (lexer.current().type == '(') { // This error is handled inside 'parseFunctionArgs' as well, but for better error recovery we need to break out the current loop here if (!asStatement && expr->location.end.line != lexer.current().location.begin.line) { - report(lexer.current().location, - "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of " - "new statement; use ';' to separate statements"); - + reportAmbiguousCallError(); break; } - expr = parseFunctionArgs(expr, false, Location()); + expr = parseFunctionArgs(expr, false); } else if ( lexer.current().type == '{' @@ -2017,7 +2012,7 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement) || lexer.current().type == Lexeme::InterpStringSimple ) { - expr = parseFunctionArgs(expr, false, Location()); + expr = parseFunctionArgs(expr, false); } else { @@ -2172,7 +2167,7 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data, return ConstantNumberParseResult::Ok; } -static ConstantNumberParseResult parseNumber(double& result, const char* data) +static ConstantNumberParseResult parseDouble(double& result, const char* data) { LUAU_ASSERT(FFlag::LuauLintParseIntegerIssues); @@ -2230,61 +2225,11 @@ AstExpr* Parser::parseSimpleExpr() Lexeme matchFunction = lexer.current(); nextLexeme(); - return parseFunctionBody(false, matchFunction, AstName(), {}).first; + return parseFunctionBody(false, matchFunction, AstName(), nullptr).first; } else if (lexer.current().type == Lexeme::Number) { - scratchData.assign(lexer.current().data, lexer.current().length); - - // Remove all internal _ - they don't hold any meaning and this allows parsing code to just pass the string pointer to strtod et al - if (scratchData.find('_') != std::string::npos) - { - scratchData.erase(std::remove(scratchData.begin(), scratchData.end(), '_'), scratchData.end()); - } - - if (FFlag::LuauLintParseIntegerIssues) - { - double value = 0; - ConstantNumberParseResult result = parseNumber(value, scratchData.c_str()); - nextLexeme(); - - if (result == ConstantNumberParseResult::Malformed) - return reportExprError(start, {}, "Malformed number"); - - return allocator.alloc(start, value, result); - } - else if (DFFlag::LuaReportParseIntegerIssues) - { - double value = 0; - if (const char* error = parseNumber_DEPRECATED2(value, scratchData.c_str())) - { - nextLexeme(); - - return reportExprError(start, {}, "%s", error); - } - else - { - nextLexeme(); - - return allocator.alloc(start, value); - } - } - else - { - double value = 0; - if (parseNumber_DEPRECATED(value, scratchData.c_str())) - { - nextLexeme(); - - return allocator.alloc(start, value); - } - else - { - nextLexeme(); - - return reportExprError(start, {}, "Malformed number"); - } - } + return parseNumber(); } else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString || (FFlag::LuauInterpolatedStringBaseSupport && lexer.current().type == Lexeme::InterpStringSimple)) { @@ -2334,18 +2279,15 @@ AstExpr* Parser::parseSimpleExpr() } // args ::= `(' [explist] `)' | tableconstructor | String | InterpString -AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self, const Location& selfLocation) +AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self) { if (lexer.current().type == '(') { Position argStart = lexer.current().location.end; if (func->location.end.line != lexer.current().location.begin.line) - { - report(lexer.current().location, "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of " - "new statement; use ';' to separate statements"); - } + reportAmbiguousCallError(); - Lexeme matchParen = lexer.current(); + MatchLexeme matchParen = lexer.current(); nextLexeme(); TempVector args(scratchExpr); @@ -2385,18 +2327,29 @@ AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self, const Location& sel } else { - if (self && lexer.current().location.begin.line != func->location.end.line) - { - return reportExprError(func->location, copy({func}), "Expected function call arguments after '('"); - } - else - { - return reportExprError(Location(func->location.begin, lexer.current().location.begin), copy({func}), - "Expected '(', '{' or when parsing function call, got %s", lexer.current().toString().c_str()); - } + return reportFunctionArgsError(func, self); } } +LUAU_NOINLINE AstExpr* Parser::reportFunctionArgsError(AstExpr* func, bool self) +{ + if (self && lexer.current().location.begin.line != func->location.end.line) + { + return reportExprError(func->location, copy({func}), "Expected function call arguments after '('"); + } + else + { + return reportExprError(Location(func->location.begin, lexer.current().location.begin), copy({func}), + "Expected '(', '{' or when parsing function call, got %s", lexer.current().toString().c_str()); + } +} + +LUAU_NOINLINE void Parser::reportAmbiguousCallError() +{ + report(lexer.current().location, "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of " + "new statement; use ';' to separate statements"); +} + // tableconstructor ::= `{' [fieldlist] `}' // fieldlist ::= field {fieldsep field} [fieldsep] // field ::= `[' exp `]' `=' exp | Name `=' exp | exp @@ -2407,14 +2360,14 @@ AstExpr* Parser::parseTableConstructor() Location start = lexer.current().location; - Lexeme matchBrace = lexer.current(); + MatchLexeme matchBrace = lexer.current(); expectAndConsume('{', "table literal"); while (lexer.current().type != '}') { if (lexer.current().type == '[') { - Lexeme matchLocationBracket = lexer.current(); + MatchLexeme matchLocationBracket = lexer.current(); nextLexeme(); AstExpr* key = parseExpr(); @@ -2789,6 +2742,63 @@ AstExpr* Parser::parseInterpString() } while (true); } +AstExpr* Parser::parseNumber() +{ + Location start = lexer.current().location; + + scratchData.assign(lexer.current().data, lexer.current().length); + + // Remove all internal _ - they don't hold any meaning and this allows parsing code to just pass the string pointer to strtod et al + if (scratchData.find('_') != std::string::npos) + { + scratchData.erase(std::remove(scratchData.begin(), scratchData.end(), '_'), scratchData.end()); + } + + if (FFlag::LuauLintParseIntegerIssues) + { + double value = 0; + ConstantNumberParseResult result = parseDouble(value, scratchData.c_str()); + nextLexeme(); + + if (result == ConstantNumberParseResult::Malformed) + return reportExprError(start, {}, "Malformed number"); + + return allocator.alloc(start, value, result); + } + else if (DFFlag::LuaReportParseIntegerIssues) + { + double value = 0; + if (const char* error = parseNumber_DEPRECATED2(value, scratchData.c_str())) + { + nextLexeme(); + + return reportExprError(start, {}, "%s", error); + } + else + { + nextLexeme(); + + return allocator.alloc(start, value); + } + } + else + { + double value = 0; + if (parseNumber_DEPRECATED(value, scratchData.c_str())) + { + nextLexeme(); + + return allocator.alloc(start, value); + } + else + { + nextLexeme(); + + return reportExprError(start, {}, "Malformed number"); + } + } +} + AstLocal* Parser::pushLocal(const Binding& binding) { const Name& name = binding.name; @@ -2860,7 +2870,7 @@ LUAU_NOINLINE void Parser::expectAndConsumeFail(Lexeme::Type type, const char* c report(lexer.current().location, "Expected %s, got %s", typeString.c_str(), currLexemeString.c_str()); } -bool Parser::expectMatchAndConsume(char value, const Lexeme& begin, bool searchForMissing) +bool Parser::expectMatchAndConsume(char value, const MatchLexeme& begin, bool searchForMissing) { Lexeme::Type type = static_cast(static_cast(value)); @@ -2868,42 +2878,7 @@ bool Parser::expectMatchAndConsume(char value, const Lexeme& begin, bool searchF { expectMatchAndConsumeFail(type, begin); - if (searchForMissing) - { - // previous location is taken because 'current' lexeme is already the next token - unsigned currentLine = lexer.previousLocation().end.line; - - // search to the end of the line for expected token - // we will also stop if we hit a token that can be handled by parsing function above the current one - Lexeme::Type lexemeType = lexer.current().type; - - while (currentLine == lexer.current().location.begin.line && lexemeType != type && matchRecoveryStopOnToken[lexemeType] == 0) - { - nextLexeme(); - lexemeType = lexer.current().type; - } - - if (lexemeType == type) - { - nextLexeme(); - - return true; - } - } - else - { - // check if this is an extra token and the expected token is next - if (lexer.lookahead().type == type) - { - // skip invalid and consume expected - nextLexeme(); - nextLexeme(); - - return true; - } - } - - return false; + return expectMatchAndConsumeRecover(value, begin, searchForMissing); } else { @@ -2913,21 +2888,64 @@ bool Parser::expectMatchAndConsume(char value, const Lexeme& begin, bool searchF } } -// LUAU_NOINLINE is used to limit the stack cost of this function due to std::string objects, and to increase caller performance since this code is -// cold -LUAU_NOINLINE void Parser::expectMatchAndConsumeFail(Lexeme::Type type, const Lexeme& begin, const char* extra) +LUAU_NOINLINE bool Parser::expectMatchAndConsumeRecover(char value, const MatchLexeme& begin, bool searchForMissing) { - std::string typeString = Lexeme(Location(Position(0, 0), 0), type).toString(); + Lexeme::Type type = static_cast(static_cast(value)); - if (lexer.current().location.begin.line == begin.location.begin.line) - report(lexer.current().location, "Expected %s (to close %s at column %d), got %s%s", typeString.c_str(), begin.toString().c_str(), - begin.location.begin.column + 1, lexer.current().toString().c_str(), extra ? extra : ""); + if (searchForMissing) + { + // previous location is taken because 'current' lexeme is already the next token + unsigned currentLine = lexer.previousLocation().end.line; + + // search to the end of the line for expected token + // we will also stop if we hit a token that can be handled by parsing function above the current one + Lexeme::Type lexemeType = lexer.current().type; + + while (currentLine == lexer.current().location.begin.line && lexemeType != type && matchRecoveryStopOnToken[lexemeType] == 0) + { + nextLexeme(); + lexemeType = lexer.current().type; + } + + if (lexemeType == type) + { + nextLexeme(); + + return true; + } + } else - report(lexer.current().location, "Expected %s (to close %s at line %d), got %s%s", typeString.c_str(), begin.toString().c_str(), - begin.location.begin.line + 1, lexer.current().toString().c_str(), extra ? extra : ""); + { + // check if this is an extra token and the expected token is next + if (lexer.lookahead().type == type) + { + // skip invalid and consume expected + nextLexeme(); + nextLexeme(); + + return true; + } + } + + return false; } -bool Parser::expectMatchEndAndConsume(Lexeme::Type type, const Lexeme& begin) +// LUAU_NOINLINE is used to limit the stack cost of this function due to std::string objects, and to increase caller performance since this code is +// cold +LUAU_NOINLINE void Parser::expectMatchAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin, const char* extra) +{ + std::string typeString = Lexeme(Location(Position(0, 0), 0), type).toString(); + std::string matchString = Lexeme(Location(Position(0, 0), 0), begin.type).toString(); + + if (lexer.current().location.begin.line == begin.position.line) + report(lexer.current().location, "Expected %s (to close %s at column %d), got %s%s", typeString.c_str(), matchString.c_str(), + begin.position.column + 1, lexer.current().toString().c_str(), extra ? extra : ""); + else + report(lexer.current().location, "Expected %s (to close %s at line %d), got %s%s", typeString.c_str(), matchString.c_str(), + begin.position.line + 1, lexer.current().toString().c_str(), extra ? extra : ""); +} + +bool Parser::expectMatchEndAndConsume(Lexeme::Type type, const MatchLexeme& begin) { if (lexer.current().type != type) { @@ -2949,9 +2967,9 @@ bool Parser::expectMatchEndAndConsume(Lexeme::Type type, const Lexeme& begin) { // If the token matches on a different line and a different column, it suggests misleading indentation // This can be used to pinpoint the problem location for a possible future *actual* mismatch - if (lexer.current().location.begin.line != begin.location.begin.line && - lexer.current().location.begin.column != begin.location.begin.column && - endMismatchSuspect.location.begin.line < begin.location.begin.line) // Only replace the previous suspect with more recent suspects + if (lexer.current().location.begin.line != begin.position.line && + lexer.current().location.begin.column != begin.position.column && + endMismatchSuspect.position.line < begin.position.line) // Only replace the previous suspect with more recent suspects { endMismatchSuspect = begin; } @@ -2964,12 +2982,12 @@ bool Parser::expectMatchEndAndConsume(Lexeme::Type type, const Lexeme& begin) // LUAU_NOINLINE is used to limit the stack cost of this function due to std::string objects, and to increase caller performance since this code is // cold -LUAU_NOINLINE void Parser::expectMatchEndAndConsumeFail(Lexeme::Type type, const Lexeme& begin) +LUAU_NOINLINE void Parser::expectMatchEndAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin) { - if (endMismatchSuspect.type != Lexeme::Eof && endMismatchSuspect.location.begin.line > begin.location.begin.line) + if (endMismatchSuspect.type != Lexeme::Eof && endMismatchSuspect.position.line > begin.position.line) { - std::string suggestion = - format("; did you forget to close %s at line %d?", endMismatchSuspect.toString().c_str(), endMismatchSuspect.location.begin.line + 1); + std::string matchString = Lexeme(Location(Position(0, 0), 0), endMismatchSuspect.type).toString(); + std::string suggestion = format("; did you forget to close %s at line %d?", matchString.c_str(), endMismatchSuspect.position.line + 1); expectMatchAndConsumeFail(type, begin, suggestion.c_str()); } diff --git a/CLI/Reduce.cpp b/CLI/Reduce.cpp new file mode 100644 index 00000000..d24c9874 --- /dev/null +++ b/CLI/Reduce.cpp @@ -0,0 +1,511 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/Ast.h" +#include "Luau/Common.h" +#include "Luau/Parser.h" +#include "Luau/Transpiler.h" + +#include "FileUtils.h" + +#include +#include +#include +#include +#include + +#define VERBOSE 0 // 1 - print out commandline invocations. 2 - print out stdout + +#ifdef _WIN32 + +const auto popen = &_popen; +const auto pclose = &_pclose; + +#endif + +using namespace Luau; + +enum class TestResult +{ + BugFound, // We encountered the bug we are trying to isolate + NoBug, // We did not encounter the bug we are trying to isolate +}; + +struct Enqueuer : public AstVisitor +{ + std::queue* queue; + + explicit Enqueuer(std::queue* queue) + : queue(queue) + { + LUAU_ASSERT(queue); + } + + bool visit(AstStatBlock* block) override + { + queue->push(block); + return false; + } +}; + +struct Reducer +{ + Allocator allocator; + AstNameTable nameTable{allocator}; + ParseOptions parseOptions; + + ParseResult parseResult; + AstStatBlock* root; + + std::string tempScriptName; + + std::string appName; + std::vector appArgs; + std::string_view searchText; + + Reducer() + { + parseOptions.captureComments = true; + } + + std::string readLine(FILE* f) + { + std::string line = ""; + char buffer[256]; + while (fgets(buffer, sizeof(buffer), f)) + { + auto len = strlen(buffer); + line += std::string(buffer, len); + if (buffer[len - 1] == '\n') + break; + } + + return line; + } + + void writeTempScript(bool minify = false) + { + std::string source = transpileWithTypes(*root); + + if (minify) + { + size_t pos = 0; + do + { + pos = source.find("\n\n", pos); + if (pos == std::string::npos) + break; + + source.erase(pos, 1); + } while (true); + } + + FILE* f = fopen(tempScriptName.c_str(), "w"); + if (!f) + { + printf("Unable to open temp script to %s\n", tempScriptName.c_str()); + exit(2); + } + + for (const HotComment& comment : parseResult.hotcomments) + fprintf(f, "--!%s\n", comment.content.c_str()); + + auto written = fwrite(source.data(), 1, source.size(), f); + if (written != source.size()) + { + printf("??? %zu %zu\n", written, source.size()); + printf("Unable to write to temp script %s\n", tempScriptName.c_str()); + exit(3); + } + + fclose(f); + } + + int step = 0; + + std::string escape(const std::string& s) + { + std::string result; + result.reserve(s.size() + 20); // guess + result += '"'; + for (char c : s) + { + if (c == '"') + result += '\\'; + result += c; + } + result += '"'; + + return result; + } + + TestResult run() + { + writeTempScript(); + + std::string command = appName + " " + escape(tempScriptName); + for (const auto& arg : appArgs) + command += " " + escape(arg); + +#if VERBOSE >= 1 + printf("running %s\n", command.c_str()); +#endif + + TestResult result = TestResult::NoBug; + + ++step; + printf("Step %4d...\n", step); + + FILE* p = popen(command.c_str(), "r"); + + while (!feof(p)) + { + std::string s = readLine(p); +#if VERBOSE >= 2 + printf("%s", s.c_str()); +#endif + if (std::string::npos != s.find(searchText)) + { + result = TestResult::BugFound; + break; + } + } + + pclose(p); + + return result; + } + + std::vector getNestedStats(AstStat* stat) + { + std::vector result; + + auto append = [&](AstStatBlock* block) { + if (block) + result.insert(result.end(), block->body.data, block->body.data + block->body.size); + }; + + if (auto block = stat->as()) + append(block); + else if (auto ifs = stat->as()) + { + append(ifs->thenbody); + if (ifs->elsebody) + { + if (AstStatBlock* elseBlock = ifs->elsebody->as()) + append(elseBlock); + else if (AstStatIf* elseIf = ifs->elsebody->as()) + { + auto innerStats = getNestedStats(elseIf); + result.insert(end(result), begin(innerStats), end(innerStats)); + } + else + { + printf("AstStatIf's else clause can have more statement types than I thought\n"); + LUAU_ASSERT(0); + } + } + } + else if (auto w = stat->as()) + append(w->body); + else if (auto r = stat->as()) + append(r->body); + else if (auto f = stat->as()) + append(f->body); + else if (auto f = stat->as()) + append(f->body); + else if (auto f = stat->as()) + append(f->func->body); + else if (auto f = stat->as()) + append(f->func->body); + + return result; + } + + // Move new body data into allocator-managed storage so that it's safe to keep around longterm. + AstStat** reallocateStatements(const std::vector& statements) + { + AstStat** newData = static_cast(allocator.allocate(sizeof(AstStat*) * statements.size())); + std::copy(statements.data(), statements.data() + statements.size(), newData); + + return newData; + } + + // Semiopen interval + using Span = std::pair; + + // Generates 'chunks' semiopen spans of equal-ish size to span the indeces running from 0 to 'size' + // Also inverses. + std::vector> generateSpans(size_t size, size_t chunks) + { + if (size <= 1) + return {}; + + LUAU_ASSERT(chunks > 0); + size_t chunkLength = std::max(1, size / chunks); + + std::vector> result; + + auto append = [&result](Span a, Span b) { + if (a.first == a.second && b.first == b.second) + return; + else + result.emplace_back(a, b); + }; + + size_t i = 0; + while (i < size) + { + size_t end = std::min(i + chunkLength, size); + append(Span{0, i}, Span{end, size}); + + i = end; + } + + i = 0; + while (i < size) + { + size_t end = std::min(i + chunkLength, size); + append(Span{i, end}, Span{size, size}); + + i = end; + } + + return result; + } + + // Returns the statements of block within span1 and span2 + // Also has the hokey restriction that span1 must come before span2 + std::vector prunedSpan(AstStatBlock* block, Span span1, Span span2) + { + std::vector result; + + for (size_t i = span1.first; i < span1.second; ++i) + result.push_back(block->body.data[i]); + + for (size_t i = span2.first; i < span2.second; ++i) + result.push_back(block->body.data[i]); + + return result; + } + + // returns true if anything was culled plus the chunk count + std::pair deleteChildStatements(AstStatBlock* block, size_t chunkCount) + { + if (block->body.size == 0) + return {false, chunkCount}; + + do + { + auto permutations = generateSpans(block->body.size, chunkCount); + for (const auto& [span1, span2] : permutations) + { + auto tempStatements = prunedSpan(block, span1, span2); + AstArray backupBody{tempStatements.data(), tempStatements.size()}; + + std::swap(block->body, backupBody); + TestResult result = run(); + if (result == TestResult::BugFound) + { + // The bug still reproduces without the statements we've culled. Commit. + block->body.data = reallocateStatements(tempStatements); + return {true, std::max(2, chunkCount - 1)}; + } + else + { + // The statements we've culled are critical for the reproduction of the bug. + // TODO try promoting its contents into this scope + std::swap(block->body, backupBody); + } + } + + chunkCount *= 2; + } while (chunkCount <= block->body.size); + + return {false, block->body.size}; + } + + bool deleteChildStatements(AstStatBlock* b) + { + bool result = false; + + size_t chunkCount = 2; + while (true) + { + auto [workDone, newChunkCount] = deleteChildStatements(b, chunkCount); + if (workDone) + { + result = true; + chunkCount = newChunkCount; + continue; + } + else + break; + } + + return result; + } + + bool tryPromotingChildStatements(AstStatBlock* b, size_t index) + { + std::vector tempStats(b->body.data, b->body.data + b->body.size); + AstStat* removed = tempStats.at(index); + tempStats.erase(begin(tempStats) + index); + + std::vector nestedStats = getNestedStats(removed); + tempStats.insert(begin(tempStats) + index, begin(nestedStats), end(nestedStats)); + + AstArray tempArray{tempStats.data(), tempStats.size()}; + std::swap(b->body, tempArray); + + TestResult result = run(); + + if (result == TestResult::BugFound) + { + b->body.data = reallocateStatements(tempStats); + return true; + } + else + { + std::swap(b->body, tempArray); + return false; + } + } + + // We live with some weirdness because I'm kind of lazy: If a statement's + // contents are promoted, we try promoting those prometed statements right + // away. I don't think it matters: If we can delete a statement and still + // exhibit the bug, we should do so. The order isn't so important. + bool tryPromotingChildStatements(AstStatBlock* b) + { + size_t i = 0; + while (i < b->body.size) + { + bool promoted = tryPromotingChildStatements(b, i); + if (!promoted) + ++i; + } + + return false; + } + + void walk(AstStatBlock* block) + { + std::queue queue; + Enqueuer enqueuer{&queue}; + + queue.push(block); + + while (!queue.empty()) + { + AstStatBlock* b = queue.front(); + queue.pop(); + + bool result = false; + do + { + result = deleteChildStatements(b); + + /* Try other reductions here before we walk into child statements + * Other reductions to try someday: + * + * Promoting a statement's children to the enclosing block. + * Deleting type annotations + * Deleting parts of type annotations + * Replacing subexpressions with ({} :: any) + * Inlining type aliases + * Inlining constants + * Inlining functions + */ + result |= tryPromotingChildStatements(b); + } while (result); + + for (AstStat* stat : b->body) + stat->visit(&enqueuer); + } + } + + void run(const std::string scriptName, const std::string appName, const std::vector& appArgs, std::string_view source, + std::string_view searchText) + { + tempScriptName = scriptName; + if (tempScriptName.substr(tempScriptName.size() - 4) == ".lua") + { + tempScriptName.erase(tempScriptName.size() - 4); + tempScriptName += "-reduced.lua"; + } + else + { + this->tempScriptName = scriptName + "-reduced"; + } + +#if 0 + // Handy debugging trick: VS Code will update its view of the file in realtime as it is edited. + std::string wheee = "code " + tempScriptName; + system(wheee.c_str()); +#endif + + printf("Temp script: %s\n", tempScriptName.c_str()); + + this->appName = appName; + this->appArgs = appArgs; + this->searchText = searchText; + + parseResult = Parser::parse(source.data(), source.size(), nameTable, allocator, parseOptions); + if (!parseResult.errors.empty()) + { + printf("Parse errors\n"); + exit(1); + } + + root = parseResult.root; + + const TestResult initialResult = run(); + if (initialResult == TestResult::NoBug) + { + printf("Could not find failure string in the unmodified script! Check your commandline arguments\n"); + exit(2); + } + + walk(root); + + writeTempScript(/* minify */ true); + + printf("Done! Check %s\n", tempScriptName.c_str()); + } +}; + +[[noreturn]] void help(const std::vector& args) +{ + printf("Syntax: %s script application \"search text\" [arguments]\n", args[0].data()); + exit(1); +} + +int main(int argc, char** argv) +{ + const std::vector args(argv, argv + argc); + + if (args.size() < 4) + help(args); + + for (int i = 1; i < args.size(); ++i) + { + if (args[i] == "--help") + help(args); + } + + const std::string scriptName = argv[1]; + const std::string appName = argv[2]; + const std::string searchText = argv[3]; + const std::vector appArgs(begin(args) + 4, end(args)); + + std::optional source = readFile(scriptName); + + if (!source) + { + printf("Could not read source %s\n", argv[1]); + exit(1); + } + + Reducer reducer; + reducer.run(scriptName, appName, appArgs, *source, searchText); +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 92006344..3dafe5fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,11 +32,13 @@ if(LUAU_BUILD_CLI) add_executable(Luau.Repl.CLI) add_executable(Luau.Analyze.CLI) add_executable(Luau.Ast.CLI) + add_executable(Luau.Reduce.CLI) # This also adds target `name` on Linux/macOS and `name.exe` on Windows set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau) set_target_properties(Luau.Analyze.CLI PROPERTIES OUTPUT_NAME luau-analyze) set_target_properties(Luau.Ast.CLI PROPERTIES OUTPUT_NAME luau-ast) + set_target_properties(Luau.Reduce.CLI PROPERTIES OUTPUT_NAME luau-reduce) endif() if(LUAU_BUILD_TESTS) @@ -49,6 +51,7 @@ if(LUAU_BUILD_WEB) add_executable(Luau.Web) endif() + include(Sources.cmake) target_include_directories(Luau.Common INTERFACE Common/include) @@ -171,6 +174,10 @@ if(LUAU_BUILD_CLI) target_link_libraries(Luau.Analyze.CLI PRIVATE Luau.Analysis) target_link_libraries(Luau.Ast.CLI PRIVATE Luau.Ast Luau.Analysis) + + target_compile_features(Luau.Reduce.CLI PRIVATE cxx_std_17) + target_include_directories(Luau.Reduce.CLI PUBLIC Reduce/include) + target_link_libraries(Luau.Reduce.CLI PRIVATE Luau.Common Luau.Ast Luau.Analysis) endif() if(LUAU_BUILD_TESTS) diff --git a/CodeGen/include/Luau/AssemblyBuilderX64.h b/CodeGen/include/Luau/AssemblyBuilderX64.h index 028b2d16..cb799d31 100644 --- a/CodeGen/include/Luau/AssemblyBuilderX64.h +++ b/CodeGen/include/Luau/AssemblyBuilderX64.h @@ -38,14 +38,21 @@ public: // Two operand mov instruction has additional specialized encodings void mov(OperandX64 lhs, OperandX64 rhs); void mov64(RegisterX64 lhs, int64_t imm); + void movsx(RegisterX64 lhs, OperandX64 rhs); + void movzx(RegisterX64 lhs, OperandX64 rhs); // Base one operand instruction with 2 opcode selection void div(OperandX64 op); void idiv(OperandX64 op); void mul(OperandX64 op); + void imul(OperandX64 op); void neg(OperandX64 op); void not_(OperandX64 op); + // Additional forms of imul + void imul(OperandX64 lhs, OperandX64 rhs); + void imul(OperandX64 dst, OperandX64 lhs, int32_t rhs); + void test(OperandX64 lhs, OperandX64 rhs); void lea(OperandX64 lhs, OperandX64 rhs); @@ -76,6 +83,12 @@ public: void vxorpd(OperandX64 dst, OperandX64 src1, OperandX64 src2); void vcomisd(OperandX64 src1, OperandX64 src2); + void vucomisd(OperandX64 src1, OperandX64 src2); + + void vcvttsd2si(OperandX64 dst, OperandX64 src); + void vcvtsi2sd(OperandX64 dst, OperandX64 src1, OperandX64 src2); + + void vroundsd(OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t mode); void vsqrtpd(OperandX64 dst, OperandX64 src); void vsqrtps(OperandX64 dst, OperandX64 src); @@ -105,6 +118,7 @@ public: OperandX64 f32(float value); OperandX64 f64(double value); OperandX64 f32x4(float x, float y, float z, float w); + OperandX64 bytes(const void* ptr, size_t size, size_t align = 8); // Resulting data and code that need to be copied over one after the other // The *end* of 'data' has to be aligned to 16 bytes, this will also align 'code' @@ -130,6 +144,8 @@ private: void placeAvx(const char* name, OperandX64 dst, OperandX64 src, uint8_t code, bool setW, uint8_t mode, uint8_t prefix); void placeAvx(const char* name, OperandX64 dst, OperandX64 src, uint8_t code, uint8_t coderev, bool setW, uint8_t mode, uint8_t prefix); void placeAvx(const char* name, OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t code, bool setW, uint8_t mode, uint8_t prefix); + void placeAvx( + const char* name, OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t imm8, uint8_t code, bool setW, uint8_t mode, uint8_t prefix); // Instruction components void placeRegAndModRegMem(OperandX64 lhs, OperandX64 rhs); @@ -157,6 +173,7 @@ private: LUAU_NOINLINE void log(const char* opcode, OperandX64 op); LUAU_NOINLINE void log(const char* opcode, OperandX64 op1, OperandX64 op2); LUAU_NOINLINE void log(const char* opcode, OperandX64 op1, OperandX64 op2, OperandX64 op3); + LUAU_NOINLINE void log(const char* opcode, OperandX64 op1, OperandX64 op2, OperandX64 op3, OperandX64 op4); LUAU_NOINLINE void log(Label label); LUAU_NOINLINE void log(const char* opcode, Label label); void log(OperandX64 op); diff --git a/CodeGen/src/AssemblyBuilderX64.cpp b/CodeGen/src/AssemblyBuilderX64.cpp index f88063cf..0fd10328 100644 --- a/CodeGen/src/AssemblyBuilderX64.cpp +++ b/CodeGen/src/AssemblyBuilderX64.cpp @@ -46,6 +46,44 @@ const unsigned AVX_F2 = 0b11; const unsigned kMaxAlign = 16; +// Utility functions to correctly write data on big endian machines +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#include + +static void writeu32(uint8_t* target, uint32_t value) +{ + value = htole32(value); + memcpy(target, &value, sizeof(value)); +} + +static void writeu64(uint8_t* target, uint64_t value) +{ + value = htole64(value); + memcpy(target, &value, sizeof(value)); +} + +static void writef32(uint8_t* target, float value) +{ + static_assert(sizeof(float) == sizeof(uint32_t), "type size must match to reinterpret data"); + uint32_t data; + memcpy(&data, &value, sizeof(value)); + writeu32(target, data); +} + +static void writef64(uint8_t* target, double value) +{ + static_assert(sizeof(double) == sizeof(uint64_t), "type size must match to reinterpret data"); + uint64_t data; + memcpy(&data, &value, sizeof(value)); + writeu64(target, data); +} +#else +#define writeu32(target, value) memcpy(target, &value, sizeof(value)) +#define writeu64(target, value) memcpy(target, &value, sizeof(value)) +#define writef32(target, value) memcpy(target, &value, sizeof(value)) +#define writef64(target, value) memcpy(target, &value, sizeof(value)) +#endif + AssemblyBuilderX64::AssemblyBuilderX64(bool logText) : logText(logText) { @@ -195,6 +233,34 @@ void AssemblyBuilderX64::mov64(RegisterX64 lhs, int64_t imm) commit(); } +void AssemblyBuilderX64::movsx(RegisterX64 lhs, OperandX64 rhs) +{ + if (logText) + log("movsx", lhs, rhs); + + LUAU_ASSERT(rhs.memSize == SizeX64::byte || rhs.memSize == SizeX64::word); + + placeRex(lhs, rhs); + place(0x0f); + place(rhs.memSize == SizeX64::byte ? 0xbe : 0xbf); + placeRegAndModRegMem(lhs, rhs); + commit(); +} + +void AssemblyBuilderX64::movzx(RegisterX64 lhs, OperandX64 rhs) +{ + if (logText) + log("movzx", lhs, rhs); + + LUAU_ASSERT(rhs.memSize == SizeX64::byte || rhs.memSize == SizeX64::word); + + placeRex(lhs, rhs); + place(0x0f); + place(rhs.memSize == SizeX64::byte ? 0xb6 : 0xb7); + placeRegAndModRegMem(lhs, rhs); + commit(); +} + void AssemblyBuilderX64::div(OperandX64 op) { placeUnaryModRegMem("div", op, 0xf6, 0xf7, 6); @@ -210,6 +276,11 @@ void AssemblyBuilderX64::mul(OperandX64 op) placeUnaryModRegMem("mul", op, 0xf6, 0xf7, 4); } +void AssemblyBuilderX64::imul(OperandX64 op) +{ + placeUnaryModRegMem("imul", op, 0xf6, 0xf7, 5); +} + void AssemblyBuilderX64::neg(OperandX64 op) { placeUnaryModRegMem("neg", op, 0xf6, 0xf7, 3); @@ -220,6 +291,41 @@ void AssemblyBuilderX64::not_(OperandX64 op) placeUnaryModRegMem("not", op, 0xf6, 0xf7, 2); } +void AssemblyBuilderX64::imul(OperandX64 lhs, OperandX64 rhs) +{ + if (logText) + log("imul", lhs, rhs); + + placeRex(lhs.base, rhs); + place(0x0f); + place(0xaf); + placeRegAndModRegMem(lhs, rhs); + commit(); +} + +void AssemblyBuilderX64::imul(OperandX64 dst, OperandX64 lhs, int32_t rhs) +{ + if (logText) + log("imul", dst, lhs, rhs); + + placeRex(dst.base, lhs); + + if (int8_t(rhs) == rhs) + { + place(0x6b); + placeRegAndModRegMem(dst, lhs); + placeImm8(rhs); + } + else + { + place(0x69); + placeRegAndModRegMem(dst, lhs); + placeImm32(rhs); + } + + commit(); +} + void AssemblyBuilderX64::test(OperandX64 lhs, OperandX64 rhs) { // No forms for r/m*, imm8 and reg, r/m* @@ -368,6 +474,26 @@ void AssemblyBuilderX64::vcomisd(OperandX64 src1, OperandX64 src2) placeAvx("vcomisd", src1, src2, 0x2f, false, AVX_0F, AVX_66); } +void AssemblyBuilderX64::vucomisd(OperandX64 src1, OperandX64 src2) +{ + placeAvx("vucomisd", src1, src2, 0x2e, false, AVX_0F, AVX_66); +} + +void AssemblyBuilderX64::vcvttsd2si(OperandX64 dst, OperandX64 src) +{ + placeAvx("vcvttsd2si", dst, src, 0x2c, dst.base.size == SizeX64::dword, AVX_0F, AVX_F2); +} + +void AssemblyBuilderX64::vcvtsi2sd(OperandX64 dst, OperandX64 src1, OperandX64 src2) +{ + placeAvx("vcvtsi2sd", dst, src1, src2, 0x2a, (src2.cat == CategoryX64::reg ? src2.base.size : src2.memSize) == SizeX64::dword, AVX_0F, AVX_F2); +} + +void AssemblyBuilderX64::vroundsd(OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t mode) +{ + placeAvx("vroundsd", dst, src1, src2, mode, 0x0b, false, AVX_0F3A, AVX_66); +} + void AssemblyBuilderX64::vsqrtpd(OperandX64 dst, OperandX64 src) { placeAvx("vsqrtpd", dst, src, 0x51, false, AVX_0F, AVX_66); @@ -436,7 +562,7 @@ void AssemblyBuilderX64::finalize() for (Label fixup : pendingLabels) { uint32_t value = labelLocations[fixup.id - 1] - (fixup.location + 4); - memcpy(&code[fixup.location], &value, sizeof(value)); + writeu32(&code[fixup.location], value); } size_t dataSize = data.size() - dataPos; @@ -479,34 +605,41 @@ void AssemblyBuilderX64::setLabel(Label& label) OperandX64 AssemblyBuilderX64::i64(int64_t value) { size_t pos = allocateData(8, 8); - memcpy(&data[pos], &value, sizeof(value)); + writeu64(&data[pos], value); return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size())); } OperandX64 AssemblyBuilderX64::f32(float value) { size_t pos = allocateData(4, 4); - memcpy(&data[pos], &value, sizeof(value)); + writef32(&data[pos], value); return OperandX64(SizeX64::dword, noreg, 1, rip, int32_t(pos - data.size())); } OperandX64 AssemblyBuilderX64::f64(double value) { size_t pos = allocateData(8, 8); - memcpy(&data[pos], &value, sizeof(value)); + writef64(&data[pos], value); return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size())); } OperandX64 AssemblyBuilderX64::f32x4(float x, float y, float z, float w) { size_t pos = allocateData(16, 16); - memcpy(&data[pos], &x, sizeof(x)); - memcpy(&data[pos + 4], &y, sizeof(y)); - memcpy(&data[pos + 8], &z, sizeof(z)); - memcpy(&data[pos + 12], &w, sizeof(w)); + writef32(&data[pos], x); + writef32(&data[pos + 4], y); + writef32(&data[pos + 8], z); + writef32(&data[pos + 12], w); return OperandX64(SizeX64::xmmword, noreg, 1, rip, int32_t(pos - data.size())); } +OperandX64 AssemblyBuilderX64::bytes(const void* ptr, size_t size, size_t align) +{ + size_t pos = allocateData(size, align); + memcpy(&data[pos], ptr, size); + return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size())); +} + void AssemblyBuilderX64::placeBinary(const char* name, OperandX64 lhs, OperandX64 rhs, uint8_t codeimm8, uint8_t codeimm, uint8_t codeimmImm8, uint8_t code8rev, uint8_t coderev, uint8_t code8, uint8_t code, uint8_t opreg) { @@ -700,6 +833,24 @@ void AssemblyBuilderX64::placeAvx( commit(); } +void AssemblyBuilderX64::placeAvx( + const char* name, OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t imm8, uint8_t code, bool setW, uint8_t mode, uint8_t prefix) +{ + LUAU_ASSERT(dst.cat == CategoryX64::reg); + LUAU_ASSERT(src1.cat == CategoryX64::reg); + LUAU_ASSERT(src2.cat == CategoryX64::reg || src2.cat == CategoryX64::mem); + + if (logText) + log(name, dst, src1, src2, imm8); + + placeVex(dst, src1, src2, setW, mode, prefix); + place(code); + placeRegAndModRegMem(dst, src2); + placeImm8(imm8); + + commit(); +} + void AssemblyBuilderX64::placeRex(RegisterX64 op) { uint8_t code = REX_W(op.size == SizeX64::qword) | REX_B(op); @@ -861,16 +1012,18 @@ void AssemblyBuilderX64::placeImm8(int32_t imm) void AssemblyBuilderX64::placeImm32(int32_t imm) { - LUAU_ASSERT(codePos + sizeof(imm) < codeEnd); - memcpy(codePos, &imm, sizeof(imm)); - codePos += sizeof(imm); + uint8_t* pos = codePos; + LUAU_ASSERT(pos + sizeof(imm) < codeEnd); + writeu32(pos, imm); + codePos = pos + sizeof(imm); } void AssemblyBuilderX64::placeImm64(int64_t imm) { - LUAU_ASSERT(codePos + sizeof(imm) < codeEnd); - memcpy(codePos, &imm, sizeof(imm)); - codePos += sizeof(imm); + uint8_t* pos = codePos; + LUAU_ASSERT(pos + sizeof(imm) < codeEnd); + writeu64(pos, imm); + codePos = pos + sizeof(imm); } void AssemblyBuilderX64::placeLabel(Label& label) @@ -970,6 +1123,19 @@ void AssemblyBuilderX64::log(const char* opcode, OperandX64 op1, OperandX64 op2, text.append("\n"); } +void AssemblyBuilderX64::log(const char* opcode, OperandX64 op1, OperandX64 op2, OperandX64 op3, OperandX64 op4) +{ + logAppend(" %-12s", opcode); + log(op1); + text.append(","); + log(op2); + text.append(","); + log(op3); + text.append(","); + log(op4); + text.append("\n"); +} + void AssemblyBuilderX64::log(Label label) { logAppend(".L%d:\n", label.id); diff --git a/Common/include/Luau/Bytecode.h b/Common/include/Luau/Bytecode.h index 1d6b18e5..d6660f05 100644 --- a/Common/include/Luau/Bytecode.h +++ b/Common/include/Luau/Bytecode.h @@ -515,6 +515,9 @@ enum LuauBuiltinFunction // rawlen LBF_RAWLEN, + + // bit32.extract(_, k, k) + LBF_BIT32_EXTRACTK, }; // Capture type, used in LOP_CAPTURE diff --git a/Compiler/src/BuiltinFolding.cpp b/Compiler/src/BuiltinFolding.cpp index e76da4e2..03b5918c 100644 --- a/Compiler/src/BuiltinFolding.cpp +++ b/Compiler/src/BuiltinFolding.cpp @@ -319,11 +319,12 @@ Constant foldBuiltin(int bfid, const Constant* args, size_t count) break; case LBF_BIT32_EXTRACT: - if (count == 3 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && args[2].type == Constant::Type_Number) + if (count >= 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && + (count == 2 || args[2].type == Constant::Type_Number)) { uint32_t u = bit32(args[0].valueNumber); int f = int(args[1].valueNumber); - int w = int(args[2].valueNumber); + int w = count == 2 ? 1 : int(args[2].valueNumber); if (f >= 0 && w > 0 && f + w <= 32) { @@ -356,13 +357,13 @@ Constant foldBuiltin(int bfid, const Constant* args, size_t count) break; case LBF_BIT32_REPLACE: - if (count == 4 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && args[2].type == Constant::Type_Number && - args[3].type == Constant::Type_Number) + if (count >= 3 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && args[2].type == Constant::Type_Number && + (count == 3 || args[3].type == Constant::Type_Number)) { uint32_t n = bit32(args[0].valueNumber); uint32_t v = bit32(args[1].valueNumber); int f = int(args[2].valueNumber); - int w = int(args[3].valueNumber); + int w = count == 3 ? 1 : int(args[3].valueNumber); if (f >= 0 && w > 0 && f + w <= 32) { diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 03779ffd..17256e15 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -24,13 +24,14 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) -LUAU_FASTFLAGVARIABLE(LuauCompileNoIpairs, false) - -LUAU_FASTFLAGVARIABLE(LuauCompileFreeReassign, false) LUAU_FASTFLAGVARIABLE(LuauCompileXEQ, false) LUAU_FASTFLAG(LuauInterpolatedStringBaseSupport) +LUAU_FASTFLAGVARIABLE(LuauCompileOptimalAssignment, false) + +LUAU_FASTFLAGVARIABLE(LuauCompileExtractK, false) + namespace Luau { @@ -40,6 +41,8 @@ static const uint32_t kMaxRegisterCount = 255; static const uint32_t kMaxUpvalueCount = 200; static const uint32_t kMaxLocalCount = 200; +static const uint8_t kInvalidReg = 255; + CompileError::CompileError(const Location& location, const std::string& message) : location(location) , message(message) @@ -402,18 +405,37 @@ struct Compiler } } - void compileExprFastcallN(AstExprCall* expr, uint8_t target, uint8_t targetCount, bool targetTop, bool multRet, uint8_t regs, int bfid) + void compileExprFastcallN(AstExprCall* expr, uint8_t target, uint8_t targetCount, bool targetTop, bool multRet, uint8_t regs, int bfid, int bfK = -1) { LUAU_ASSERT(!expr->self); - LUAU_ASSERT(expr->args.size <= 2); + LUAU_ASSERT(expr->args.size >= 1); + LUAU_ASSERT(expr->args.size <= 2 || (bfid == LBF_BIT32_EXTRACTK && expr->args.size == 3)); + LUAU_ASSERT(bfid == LBF_BIT32_EXTRACTK ? bfK >= 0 : bfK < 0); LuauOpcode opc = expr->args.size == 1 ? LOP_FASTCALL1 : LOP_FASTCALL2; - uint32_t args[2] = {}; + if (FFlag::LuauCompileExtractK) + { + opc = expr->args.size == 1 ? LOP_FASTCALL1 : (bfK >= 0 || isConstant(expr->args.data[1])) ? LOP_FASTCALL2K : LOP_FASTCALL2; + } + + uint32_t args[3] = {}; for (size_t i = 0; i < expr->args.size; ++i) { - if (i > 0) + if (FFlag::LuauCompileExtractK) + { + if (i > 0 && opc == LOP_FASTCALL2K) + { + int32_t cid = getConstantIndex(expr->args.data[i]); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + args[i] = cid; + continue; // TODO: remove this and change if below to else if + } + } + else if (i > 0) { if (int32_t cid = getConstantIndex(expr->args.data[i]); cid >= 0) { @@ -424,7 +446,9 @@ struct Compiler } if (int reg = getExprLocalReg(expr->args.data[i]); reg >= 0) + { args[i] = uint8_t(reg); + } else { args[i] = uint8_t(regs + 1 + i); @@ -436,21 +460,31 @@ struct Compiler bytecode.emitABC(opc, uint8_t(bfid), uint8_t(args[0]), 0); if (opc != LOP_FASTCALL1) - bytecode.emitAux(args[1]); + bytecode.emitAux(bfK >= 0 ? bfK : args[1]); // Set up a traditional Lua stack for the subsequent LOP_CALL. // Note, as with other instructions that immediately follow FASTCALL, these are normally not executed and are used as a fallback for // these FASTCALL variants. for (size_t i = 0; i < expr->args.size; ++i) { - if (i > 0 && opc == LOP_FASTCALL2K) + if (FFlag::LuauCompileExtractK) { - emitLoadK(uint8_t(regs + 1 + i), args[i]); - break; + if (i > 0 && opc == LOP_FASTCALL2K) + emitLoadK(uint8_t(regs + 1 + i), args[i]); + else if (args[i] != regs + 1 + i) + bytecode.emitABC(LOP_MOVE, uint8_t(regs + 1 + i), uint8_t(args[i]), 0); } + else + { + if (i > 0 && opc == LOP_FASTCALL2K) + { + emitLoadK(uint8_t(regs + 1 + i), args[i]); + break; + } - if (args[i] != regs + 1 + i) - bytecode.emitABC(LOP_MOVE, uint8_t(regs + 1 + i), uint8_t(args[i]), 0); + if (args[i] != regs + 1 + i) + bytecode.emitABC(LOP_MOVE, uint8_t(regs + 1 + i), uint8_t(args[i]), 0); + } } // note, these instructions are normally not executed and are used as a fallback for FASTCALL @@ -599,7 +633,7 @@ struct Compiler } else { - AstExprLocal* le = FFlag::LuauCompileFreeReassign ? getExprLocal(arg) : arg->as(); + AstExprLocal* le = getExprLocal(arg); Variable* lv = le ? variables.find(le->local) : nullptr; // if the argument is a local that isn't mutated, we will simply reuse the existing register @@ -722,6 +756,26 @@ struct Compiler bfid = -1; } + // Optimization: for bit32.extract with constant in-range f/w we compile using FASTCALL2K and a special builtin + if (FFlag::LuauCompileExtractK && bfid == LBF_BIT32_EXTRACT && expr->args.size == 3 && isConstant(expr->args.data[1]) && isConstant(expr->args.data[2])) + { + Constant fc = getConstant(expr->args.data[1]); + Constant wc = getConstant(expr->args.data[2]); + + int fi = fc.type == Constant::Type_Number ? int(fc.valueNumber) : -1; + int wi = wc.type == Constant::Type_Number ? int(wc.valueNumber) : -1; + + if (fi >= 0 && wi > 0 && fi + wi <= 32) + { + int fwp = fi | ((wi - 1) << 5); + int32_t cid = bytecode.addConstantNumber(fwp); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, LBF_BIT32_EXTRACTK, cid); + } + } + // Optimization: for 1/2 argument fast calls use specialized opcodes if (bfid >= 0 && expr->args.size >= 1 && expr->args.size <= 2 && !isExprMultRet(expr->args.data[expr->args.size - 1])) return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid); @@ -1217,7 +1271,7 @@ struct Compiler { // disambiguation: there's 4 cases (we only need truthy or falsy results based on onlyTruth) // onlyTruth = 1: a and b transforms to a ? b : dontcare - // onlyTruth = 1: a or b transforms to a ? a : a + // onlyTruth = 1: a or b transforms to a ? a : b // onlyTruth = 0: a and b transforms to !a ? a : b // onlyTruth = 0: a or b transforms to !a ? b : dontcare if (onlyTruth == (expr->op == AstExprBinary::And)) @@ -2107,9 +2161,35 @@ struct Compiler return reg; } + // initializes target..target+targetCount-1 range using expression + // if expression is a call/vararg, we assume it returns all values, otherwise we fill the rest with nil + // assumes target register range can be clobbered and is at the top of the register space if targetTop = true + void compileExprTempN(AstExpr* node, uint8_t target, uint8_t targetCount, bool targetTop) + { + // we assume that target range is at the top of the register space and can be clobbered + // this is what allows us to compile the last call expression - if it's a call - using targetTop=true + LUAU_ASSERT(!targetTop || unsigned(target + targetCount) == regTop); + + if (AstExprCall* expr = node->as()) + { + compileExprCall(expr, target, targetCount, targetTop); + } + else if (AstExprVarargs* expr = node->as()) + { + compileExprVarargs(expr, target, targetCount); + } + else + { + compileExprTemp(node, target); + + for (size_t i = 1; i < targetCount; ++i) + bytecode.emitABC(LOP_LOADNIL, uint8_t(target + i), 0, 0); + } + } + // initializes target..target+targetCount-1 range using expressions from the list - // if list has fewer expressions, and last expression is a call, we assume the call returns the rest of the values - // if list has fewer expressions, and last expression isn't a call, we fill the rest with nil + // if list has fewer expressions, and last expression is multret, we assume it returns the rest of the values + // if list has fewer expressions, and last expression isn't multret, we fill the rest with nil // assumes target register range can be clobbered and is at the top of the register space if targetTop = true void compileExprListTemp(const AstArray& list, uint8_t target, uint8_t targetCount, bool targetTop) { @@ -2139,23 +2219,7 @@ struct Compiler for (size_t i = 0; i < list.size - 1; ++i) compileExprTemp(list.data[i], uint8_t(target + i)); - AstExpr* last = list.data[list.size - 1]; - - if (AstExprCall* expr = last->as()) - { - compileExprCall(expr, uint8_t(target + list.size - 1), uint8_t(targetCount - (list.size - 1)), targetTop); - } - else if (AstExprVarargs* expr = last->as()) - { - compileExprVarargs(expr, uint8_t(target + list.size - 1), uint8_t(targetCount - (list.size - 1))); - } - else - { - compileExprTemp(last, uint8_t(target + list.size - 1)); - - for (size_t i = list.size; i < targetCount; ++i) - bytecode.emitABC(LOP_LOADNIL, uint8_t(target + i), 0, 0); - } + compileExprTempN(list.data[list.size - 1], uint8_t(target + list.size - 1), uint8_t(targetCount - (list.size - 1)), targetTop); } else { @@ -2639,7 +2703,7 @@ struct Compiler return; // Optimization: for 1-1 local assignments, we can reuse the register *if* neither local is mutated - if (FFlag::LuauCompileFreeReassign && options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1) + if (options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1) { if (AstExprLocal* re = getExprLocal(stat->values.data[0])) { @@ -2853,7 +2917,6 @@ struct Compiler LUAU_ASSERT(vars == regs + 3); LuauOpcode skipOp = LOP_FORGPREP; - LuauOpcode loopOp = LOP_FORGLOOP; // Optimization: when we iterate via pairs/ipairs, we generate special bytecode that optimizes the traversal using internal iteration index // These instructions dynamically check if generator is equal to next/inext and bail out @@ -2865,25 +2928,16 @@ struct Compiler Builtin builtin = getBuiltin(stat->values.data[0]->as()->func, globals, variables); if (builtin.isGlobal("ipairs")) // for .. in ipairs(t) - { skipOp = LOP_FORGPREP_INEXT; - loopOp = FFlag::LuauCompileNoIpairs ? LOP_FORGLOOP : LOP_FORGLOOP_INEXT; - } else if (builtin.isGlobal("pairs")) // for .. in pairs(t) - { skipOp = LOP_FORGPREP_NEXT; - loopOp = LOP_FORGLOOP; - } } else if (stat->values.size == 2) { Builtin builtin = getBuiltin(stat->values.data[0], globals, variables); if (builtin.isGlobal("next")) // for .. in next,t - { skipOp = LOP_FORGPREP_NEXT; - loopOp = LOP_FORGLOOP; - } } } @@ -2909,19 +2963,9 @@ struct Compiler size_t backLabel = bytecode.emitLabel(); - bytecode.emitAD(loopOp, regs, 0); - - if (FFlag::LuauCompileNoIpairs) - { - // TODO: remove loopOp as it's a constant now - LUAU_ASSERT(loopOp == LOP_FORGLOOP); - - // FORGLOOP uses aux to encode variable count and fast path flag for ipairs traversal in the high bit - bytecode.emitAux((skipOp == LOP_FORGPREP_INEXT ? 0x80000000 : 0) | uint32_t(stat->vars.size)); - } - // note: FORGLOOP needs variable count encoded in AUX field, other loop instructions assume a fixed variable count - else if (loopOp == LOP_FORGLOOP) - bytecode.emitAux(uint32_t(stat->vars.size)); + // FORGLOOP uses aux to encode variable count and fast path flag for ipairs traversal in the high bit + bytecode.emitAD(LOP_FORGLOOP, regs, 0); + bytecode.emitAux((skipOp == LOP_FORGPREP_INEXT ? 0x80000000 : 0) | uint32_t(stat->vars.size)); size_t endLabel = bytecode.emitLabel(); @@ -2936,6 +2980,8 @@ struct Compiler void resolveAssignConflicts(AstStat* stat, std::vector& vars) { + LUAU_ASSERT(!FFlag::LuauCompileOptimalAssignment); + // regsUsed[i] is true if we have assigned the register during earlier assignments // regsRemap[i] is set to the register where the original (pre-assignment) copy was made // note: regsRemap is uninitialized intentionally to speed small assignments up; regsRemap[i] is valid iff regsUsed[i] @@ -2988,12 +3034,86 @@ struct Compiler } } + struct Assignment + { + LValue lvalue; + + uint8_t conflictReg = kInvalidReg; + uint8_t valueReg = kInvalidReg; + }; + + void resolveAssignConflicts(AstStat* stat, std::vector& vars, const AstArray& values) + { + struct Visitor : AstVisitor + { + Compiler* self; + + std::bitset<256> conflict; + std::bitset<256> assigned; + + Visitor(Compiler* self) + : self(self) + { + } + + bool visit(AstExprLocal* node) override + { + int reg = self->getLocalReg(node->local); + + if (reg >= 0 && assigned[reg]) + conflict[reg] = true; + + return true; + } + }; + + Visitor visitor(this); + + // mark any registers that are used *after* assignment as conflicting + for (size_t i = 0; i < vars.size(); ++i) + { + const LValue& li = vars[i].lvalue; + + if (i < values.size) + values.data[i]->visit(&visitor); + + if (li.kind == LValue::Kind_Local) + visitor.assigned[li.reg] = true; + } + + // mark any registers used in trailing expressions as conflicting as well + for (size_t i = vars.size(); i < values.size; ++i) + values.data[i]->visit(&visitor); + + // mark any registers used on left hand side that are also assigned anywhere as conflicting + // this is order-independent because we evaluate all right hand side arguments into registers before doing table assignments + for (const Assignment& var : vars) + { + const LValue& li = var.lvalue; + + if ((li.kind == LValue::Kind_IndexName || li.kind == LValue::Kind_IndexNumber || li.kind == LValue::Kind_IndexExpr) && + visitor.assigned[li.reg]) + visitor.conflict[li.reg] = true; + + if (li.kind == LValue::Kind_IndexExpr && visitor.assigned[li.index]) + visitor.conflict[li.index] = true; + } + + // for any conflicting var, we need to allocate a temporary register where the assignment is performed, so that we can move the value later + for (Assignment& var : vars) + { + const LValue& li = var.lvalue; + + if (li.kind == LValue::Kind_Local && visitor.conflict[li.reg]) + var.conflictReg = allocReg(stat, 1); + } + } + void compileStatAssign(AstStatAssign* stat) { RegScope rs(this); - // Optimization: one to one assignments don't require complex conflict resolution machinery and allow us to skip temporary registers for - // locals + // Optimization: one to one assignments don't require complex conflict resolution machinery if (stat->vars.size == 1 && stat->values.size == 1) { LValue var = compileLValue(stat->vars.data[0], rs); @@ -3013,28 +3133,110 @@ struct Compiler return; } - // compute all l-values: note that this doesn't assign anything yet but it allocates registers and computes complex expressions on the left - // hand side for example, in "a[expr] = foo" expr will get evaluated here - std::vector vars(stat->vars.size); - - for (size_t i = 0; i < stat->vars.size; ++i) - vars[i] = compileLValue(stat->vars.data[i], rs); - - // perform conflict resolution: if any lvalue refers to a local reg that will be reassigned before that, we save the local variable in a - // temporary reg - resolveAssignConflicts(stat, vars); - - // compute values into temporaries - uint8_t regs = allocReg(stat, unsigned(stat->vars.size)); - - compileExprListTemp(stat->values, regs, uint8_t(stat->vars.size), /* targetTop= */ true); - - // assign variables that have associated values; note that if we have fewer values than variables, we'll assign nil because - // compileExprListTemp will generate nils - for (size_t i = 0; i < stat->vars.size; ++i) + if (FFlag::LuauCompileOptimalAssignment) { - setDebugLine(stat->vars.data[i]); - compileAssign(vars[i], uint8_t(regs + i)); + // compute all l-values: note that this doesn't assign anything yet but it allocates registers and computes complex expressions on the + // left hand side - for example, in "a[expr] = foo" expr will get evaluated here + std::vector vars(stat->vars.size); + + for (size_t i = 0; i < stat->vars.size; ++i) + vars[i].lvalue = compileLValue(stat->vars.data[i], rs); + + // perform conflict resolution: if any expression refers to a local that is assigned before evaluating it, we assign to a temporary + // register after this, vars[i].conflictReg is set for locals that need to be assigned in the second pass + resolveAssignConflicts(stat, vars, stat->values); + + // compute rhs into (mostly) fresh registers + // note that when the lhs assigment is a local, we evaluate directly into that register + // this is possible because resolveAssignConflicts renamed conflicting locals into temporaries + // after this, vars[i].valueReg is set to a register with the value for *all* vars, but some have already been assigned + for (size_t i = 0; i < stat->vars.size && i < stat->values.size; ++i) + { + AstExpr* value = stat->values.data[i]; + + if (i + 1 == stat->values.size && stat->vars.size > stat->values.size) + { + // allocate a consecutive range of regs for all remaining vars and compute everything into temps + // note, this also handles trailing nils + uint8_t rest = uint8_t(stat->vars.size - stat->values.size + 1); + uint8_t temp = allocReg(stat, rest); + + compileExprTempN(value, temp, rest, /* targetTop= */ true); + + for (size_t j = i; j < stat->vars.size; ++j) + vars[j].valueReg = uint8_t(temp + (j - i)); + } + else + { + Assignment& var = vars[i]; + + // if target is a local, use compileExpr directly to target + if (var.lvalue.kind == LValue::Kind_Local) + { + var.valueReg = (var.conflictReg == kInvalidReg) ? var.lvalue.reg : var.conflictReg; + + compileExpr(stat->values.data[i], var.valueReg); + } + else + { + var.valueReg = compileExprAuto(stat->values.data[i], rs); + } + } + } + + // compute expressions with side effects for lulz + for (size_t i = stat->vars.size; i < stat->values.size; ++i) + { + RegScope rsi(this); + compileExprAuto(stat->values.data[i], rsi); + } + + // almost done... let's assign everything left to right, noting that locals were either written-to directly, or will be written-to in a + // separate pass to avoid conflicts + for (const Assignment& var : vars) + { + LUAU_ASSERT(var.valueReg != kInvalidReg); + + if (var.lvalue.kind != LValue::Kind_Local) + { + setDebugLine(var.lvalue.location); + compileAssign(var.lvalue, var.valueReg); + } + } + + // all regular local writes are done by the prior loops by computing result directly into target, so this just handles conflicts OR + // local copies from temporary registers in multret context, since in that case we have to allocate consecutive temporaries + for (const Assignment& var : vars) + { + if (var.lvalue.kind == LValue::Kind_Local && var.valueReg != var.lvalue.reg) + bytecode.emitABC(LOP_MOVE, var.lvalue.reg, var.valueReg, 0); + } + } + else + { + // compute all l-values: note that this doesn't assign anything yet but it allocates registers and computes complex expressions on the + // left hand side for example, in "a[expr] = foo" expr will get evaluated here + std::vector vars(stat->vars.size); + + for (size_t i = 0; i < stat->vars.size; ++i) + vars[i] = compileLValue(stat->vars.data[i], rs); + + // perform conflict resolution: if any lvalue refers to a local reg that will be reassigned before that, we save the local variable in a + // temporary reg + resolveAssignConflicts(stat, vars); + + // compute values into temporaries + uint8_t regs = allocReg(stat, unsigned(stat->vars.size)); + + compileExprListTemp(stat->values, regs, uint8_t(stat->vars.size), /* targetTop= */ true); + + // assign variables that have associated values; note that if we have fewer values than variables, we'll assign nil because + // compileExprListTemp will generate nils + for (size_t i = 0; i < stat->vars.size; ++i) + { + setDebugLine(stat->vars.data[i]); + compileAssign(vars[i], uint8_t(regs + i)); + } } } diff --git a/Makefile b/Makefile index dd32d237..4333a973 100644 --- a/Makefile +++ b/Makefile @@ -97,8 +97,8 @@ ifeq ($(config),fuzz) LDFLAGS+=-fsanitize=address,fuzzer endif -ifneq ($(CALLGRIND),) - CXXFLAGS+=-DCALLGRIND=$(CALLGRIND) +ifeq ($(config),profile) + CXXFLAGS+=-O2 -DNDEBUG -gdwarf-4 -DCALLGRIND=1 endif # target-specific flags diff --git a/Sources.cmake b/Sources.cmake index 9a6019a9..50770f92 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -66,6 +66,7 @@ target_sources(Luau.CodeGen PRIVATE # Luau.Analysis Sources target_sources(Luau.Analysis PRIVATE + Analysis/include/Luau/Anyification.h Analysis/include/Luau/ApplyTypeFunction.h Analysis/include/Luau/AstJsonEncoder.h Analysis/include/Luau/AstQuery.h @@ -115,6 +116,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Variant.h Analysis/include/Luau/VisitTypeVar.h + Analysis/src/Anyification.cpp Analysis/src/ApplyTypeFunction.cpp Analysis/src/AstJsonEncoder.cpp Analysis/src/AstQuery.cpp @@ -126,6 +128,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/ConstraintGraphBuilder.cpp Analysis/src/ConstraintSolver.cpp Analysis/src/ConstraintSolverLogger.cpp + Analysis/src/EmbeddedBuiltinDefinitions.cpp Analysis/src/Error.cpp Analysis/src/Frontend.cpp Analysis/src/Instantiation.cpp @@ -155,7 +158,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/TypeVar.cpp Analysis/src/Unifiable.cpp Analysis/src/Unifier.cpp - Analysis/src/EmbeddedBuiltinDefinitions.cpp ) # Luau.VM Sources @@ -347,3 +349,11 @@ if(TARGET Luau.Web) target_sources(Luau.Web PRIVATE CLI/Web.cpp) endif() + +if(TARGET Luau.Reduce.CLI) + target_sources(Luau.Reduce.CLI PRIVATE + CLI/Reduce.cpp + CLI/FileUtils.cpp + CLI/FileUtils.h + ) +endif() diff --git a/VM/src/lapi.cpp b/VM/src/lapi.cpp index bb994fb4..af97dc4a 100644 --- a/VM/src/lapi.cpp +++ b/VM/src/lapi.cpp @@ -1209,7 +1209,9 @@ void* lua_newuserdatadtor(lua_State* L, size_t sz, void (*dtor)(void*)) { luaC_checkGC(L); luaC_checkthreadsleep(L); - Udata* u = luaU_newudata(L, sz + sizeof(dtor), UTAG_IDTOR); + // make sure sz + sizeof(dtor) doesn't overflow; luaU_newdata will reject SIZE_MAX correctly + size_t as = sz < SIZE_MAX - sizeof(dtor) ? sz + sizeof(dtor) : SIZE_MAX; + Udata* u = luaU_newudata(L, as, UTAG_IDTOR); memcpy(&u->data + sz, &dtor, sizeof(dtor)); setuvalue(L, L->top, u); api_incr_top(L); diff --git a/VM/src/lbuiltins.cpp b/VM/src/lbuiltins.cpp index e98660a7..422c82b1 100644 --- a/VM/src/lbuiltins.cpp +++ b/VM/src/lbuiltins.cpp @@ -15,6 +15,8 @@ #include #endif +LUAU_FASTFLAGVARIABLE(LuauFasterBit32NoWidth, false) + // luauF functions implement FASTCALL instruction that performs a direct execution of some builtin functions from the VM // The rule of thumb is that FASTCALL functions can not call user code, yield, fail, or reallocate stack. // If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path @@ -600,24 +602,39 @@ static int luauF_btest(lua_State* L, StkId res, TValue* arg0, int nresults, StkI static int luauF_extract(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) { - if (nparams >= 3 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args) && ttisnumber(args + 1)) + if (nparams >= (3 - FFlag::LuauFasterBit32NoWidth) && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) { double a1 = nvalue(arg0); double a2 = nvalue(args); - double a3 = nvalue(args + 1); unsigned n; luai_num2unsigned(n, a1); int f = int(a2); - int w = int(a3); - if (f >= 0 && w > 0 && f + w <= 32) + if (nparams == 2) { - uint32_t m = ~(0xfffffffeu << (w - 1)); - uint32_t r = (n >> f) & m; + if (unsigned(f) < 32) + { + uint32_t m = 1; + uint32_t r = (n >> f) & m; - setnvalue(res, double(r)); - return 1; + setnvalue(res, double(r)); + return 1; + } + } + else if (ttisnumber(args + 1)) + { + double a3 = nvalue(args + 1); + int w = int(a3); + + if (f >= 0 && w > 0 && f + w <= 32) + { + uint32_t m = ~(0xfffffffeu << (w - 1)); + uint32_t r = (n >> f) & m; + + setnvalue(res, double(r)); + return 1; + } } } @@ -676,26 +693,41 @@ static int luauF_lshift(lua_State* L, StkId res, TValue* arg0, int nresults, Stk static int luauF_replace(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) { - if (nparams >= 4 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args) && ttisnumber(args + 1) && ttisnumber(args + 2)) + if (nparams >= (4 - FFlag::LuauFasterBit32NoWidth) && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args) && ttisnumber(args + 1)) { double a1 = nvalue(arg0); double a2 = nvalue(args); double a3 = nvalue(args + 1); - double a4 = nvalue(args + 2); unsigned n, v; luai_num2unsigned(n, a1); luai_num2unsigned(v, a2); int f = int(a3); - int w = int(a4); - if (f >= 0 && w > 0 && f + w <= 32) + if (nparams == 3) { - uint32_t m = ~(0xfffffffeu << (w - 1)); - uint32_t r = (n & ~(m << f)) | ((v & m) << f); + if (unsigned(f) < 32) + { + uint32_t m = 1; + uint32_t r = (n & ~(m << f)) | ((v & m) << f); - setnvalue(res, double(r)); - return 1; + setnvalue(res, double(r)); + return 1; + } + } + else if (ttisnumber(args + 2)) + { + double a4 = nvalue(args + 2); + int w = int(a4); + + if (f >= 0 && w > 0 && f + w <= 32) + { + uint32_t m = ~(0xfffffffeu << (w - 1)); + uint32_t r = (n & ~(m << f)) | ((v & m) << f); + + setnvalue(res, double(r)); + return 1; + } } } @@ -1138,6 +1170,31 @@ static int luauF_rawlen(lua_State* L, StkId res, TValue* arg0, int nresults, Stk return -1; } +static int luauF_extractk(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + // args is known to contain a number constant with packed in-range f/w + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned n; + luai_num2unsigned(n, a1); + int fw = int(a2); + + int f = fw & 31; + int w1 = fw >> 5; + + uint32_t m = ~(0xfffffffeu << w1); + uint32_t r = (n >> f) & m; + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + luau_FastFunction luauF_table[256] = { NULL, luauF_assert, @@ -1211,4 +1268,6 @@ luau_FastFunction luauF_table[256] = { luauF_select, luauF_rawlen, + + luauF_extractk, }; diff --git a/VM/src/lfunc.cpp b/VM/src/lfunc.cpp index dfde6dcb..bd0f8264 100644 --- a/VM/src/lfunc.cpp +++ b/VM/src/lfunc.cpp @@ -175,8 +175,7 @@ void luaF_freeclosure(lua_State* L, Closure* c, lua_Page* page) const LocVar* luaF_getlocal(const Proto* f, int local_number, int pc) { - int i; - for (i = 0; i < f->sizelocvars; i++) + for (int i = 0; i < f->sizelocvars; i++) { if (pc >= f->locvars[i].startpc && pc < f->locvars[i].endpc) { // is variable active? @@ -185,5 +184,15 @@ const LocVar* luaF_getlocal(const Proto* f, int local_number, int pc) return &f->locvars[i]; } } + + return NULL; // not found +} + +const LocVar* luaF_findlocal(const Proto* f, int local_reg, int pc) +{ + for (int i = 0; i < f->sizelocvars; i++) + if (local_reg == f->locvars[i].reg && pc >= f->locvars[i].startpc && pc < f->locvars[i].endpc) + return &f->locvars[i]; + return NULL; // not found } diff --git a/VM/src/lfunc.h b/VM/src/lfunc.h index a260d00a..59ab5726 100644 --- a/VM/src/lfunc.h +++ b/VM/src/lfunc.h @@ -17,3 +17,4 @@ LUAI_FUNC void luaF_freeclosure(lua_State* L, Closure* c, struct lua_Page* page) LUAI_FUNC void luaF_unlinkupval(UpVal* uv); LUAI_FUNC void luaF_freeupval(lua_State* L, UpVal* uv, struct lua_Page* page); LUAI_FUNC const LocVar* luaF_getlocal(const Proto* func, int local_number, int pc); +LUAI_FUNC const LocVar* luaF_findlocal(const Proto* func, int local_reg, int pc); diff --git a/VM/src/lgcdebug.cpp b/VM/src/lgcdebug.cpp index bc997d44..b6204b1a 100644 --- a/VM/src/lgcdebug.cpp +++ b/VM/src/lgcdebug.cpp @@ -345,6 +345,9 @@ static void dumpclosure(FILE* f, Closure* cl) if (cl->isC) { + if (cl->c.debugname) + fprintf(f, ",\"name\":\"%s\"", cl->c.debugname + 0); + if (cl->nupvalues) { fprintf(f, ",\"upvalues\":["); @@ -354,6 +357,9 @@ static void dumpclosure(FILE* f, Closure* cl) } else { + if (cl->l.p->debugname) + fprintf(f, ",\"name\":\"%s\"", getstr(cl->l.p->debugname)); + fprintf(f, ",\"proto\":"); dumpref(f, obj2gco(cl->l.p)); if (cl->nupvalues) @@ -403,7 +409,7 @@ static void dumpthread(FILE* f, lua_State* th) fprintf(f, ",\"source\":\""); dumpstringdata(f, p->source->data, p->source->len); - fprintf(f, "\",\"line\":%d", p->abslineinfo ? p->abslineinfo[0] : 0); + fprintf(f, "\",\"line\":%d", p->linedefined); } if (th->top > th->stack) @@ -411,6 +417,55 @@ static void dumpthread(FILE* f, lua_State* th) fprintf(f, ",\"stack\":["); dumprefs(f, th->stack, th->top - th->stack); fprintf(f, "]"); + + CallInfo* ci = th->base_ci; + bool first = true; + + fprintf(f, ",\"stacknames\":["); + for (StkId v = th->stack; v < th->top; ++v) + { + if (!iscollectable(v)) + continue; + + while (ci < th->ci && v >= (ci + 1)->func) + ci++; + + if (!first) + fputc(',', f); + first = false; + + if (v == ci->func) + { + Closure* cl = ci_func(ci); + + if (cl->isC) + { + fprintf(f, "\"frame:%s\"", cl->c.debugname ? cl->c.debugname : "[C]"); + } + else + { + Proto* p = cl->l.p; + fprintf(f, "\"frame:"); + if (p->source) + dumpstringdata(f, p->source->data, p->source->len); + fprintf(f, ":%d:%s\"", p->linedefined, p->debugname ? getstr(p->debugname) : ""); + } + } + else if (isLua(ci)) + { + Proto* p = ci_func(ci)->l.p; + int pc = pcRel(ci->savedpc, p); + const LocVar* var = luaF_findlocal(p, int(v - ci->base), pc); + + if (var && var->varname) + fprintf(f, "\"%s\"", getstr(var->varname)); + else + fprintf(f, "null"); + } + else + fprintf(f, "null"); + } + fprintf(f, "]"); } fprintf(f, "}"); } diff --git a/bench/bench.py b/bench/bench.py index bb3ea5f7..42a0ac97 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -12,9 +12,6 @@ import json from color import colored, Color from tabulate import TablePrinter, Alignment -# Based on rotest, specialized for benchmark results -import influxbench - try: import matplotlib import matplotlib.pyplot as plt @@ -721,6 +718,7 @@ def run(args, argsubcb): argumentSubstituionCallback = argsubcb if arguments.report_metrics or arguments.print_influx_debugging: + import influxbench influxReporter = influxbench.InfluxReporter(arguments) else: influxReporter = None diff --git a/bench/influxbench.py b/bench/influxbench.py index adcddeba..20122456 100644 --- a/bench/influxbench.py +++ b/bench/influxbench.py @@ -4,12 +4,7 @@ import platform import shlex import socket import sys - -try: - import requests -except: - print("Please install 'requests' using using '{} -m pip install requests' command and try again".format(sys.executable)) - exit(-1) +import requests _hostname = socket.gethostname() diff --git a/docs/_pages/compatibility.md b/docs/_pages/compatibility.md index bdb3d814..1c15e9b8 100644 --- a/docs/_pages/compatibility.md +++ b/docs/_pages/compatibility.md @@ -70,6 +70,7 @@ Sandboxing challenges are [covered in the dedicated section](sandbox). | `bit32` library | ✔️ | | | `string.gsub` is stricter about using `%` on special characters only | ✔️ | | | light C functions | 😞 | this changes semantics of fenv on C functions and has complex implications wrt runtime performance | +| NaN keys are supported for tables with `__newindex` | ✔️ | | Two things that are important to call out here are various new metamethods for tables and yielding in metamethods. In both cases, there are performance implications to supporting this - our implementation is *very* highly tuned for performance, so any changes that affect the core fundamentals of how Lua works have a price. To support yielding in metamethods we'd need to make the core of the VM more involved, since almost every single "interesting" opcode would need to learn how to be resumable - which also complicates future JIT/AOT story. Metamethods in general are important for extensibility, but very challenging to deal with in implementation, so we err on the side of not supporting any new metamethods unless a strong need arises. diff --git a/docs/_pages/lint.md b/docs/_pages/lint.md index 340d35a2..681a275a 100644 --- a/docs/_pages/lint.md +++ b/docs/_pages/lint.md @@ -326,4 +326,26 @@ Luau uses comments that start from `!` to control certain aspects of analysis, f --!nostrict -- Unknown comment directive 'nostrict'; did you mean 'nonstrict'?" ``` + +## IntegerParsing (27) + +Luau parses hexadecimal and binary literals as 64-bit integers before converting them to Luau numbers. As a result, numbers that exceed 2^64 are silently truncated to 2^64, which can result in unexpected program behavior. This warning flags literals that are truncated: + +``` +-- Hexadecimal number literal exceeded available precision and has been truncated to 2^64 +local x = 0x1111111111111111111111111111111111111 +``` + +## ComparisonPrecedence (28) + +Because of operator precedence rules, not X == Y parses as (not X) == Y; however, often the intent was to invert the result of the comparison. This warning flags erroneous conditions like that, as well as flagging cases where two comparisons happen in a row without any parentheses: + +``` +-- not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or wrap one of the expressions in parentheses to silence +if not x == 5 then +end + +-- X <= Y <= Z is equivalent to (X <= Y) <= Z; wrap one of the expressions in parentheses to silence +if 1 <= x <= 3 then +end ``` diff --git a/rfcs/generalized-iteration.md b/rfcs/generalized-iteration.md index 99671090..c28156ff 100644 --- a/rfcs/generalized-iteration.md +++ b/rfcs/generalized-iteration.md @@ -51,8 +51,10 @@ end To support self-iterating objects, we modify the iteration protocol as follows: instead of simply expanding the result of expression `iter` into three variables (`gen`, `state` and `index`), we check if the first result has an `__iter` metamethod (which can be the case if it's a table, userdata or another composite object (e.g. a record in the future). If it does, the metamethod is called with `gen` as the first argument, and the returned three values replace `gen`/`state`/`index`. This happens *before* the loop: ```lua -if getmetatable(gen) and getmetatable(gen).__iter then - gen, state, index = getmetatable(gen).__iter(gen) +local genmt = rawgetmetatable(gen) -- pseudo code for getmetatable that bypasses __metatable +local iterf = genmt and rawget(genmt, "__iter") +if iterf then + gen, state, index = iterf(gen) end ``` diff --git a/tests/AssemblyBuilderX64.test.cpp b/tests/AssemblyBuilderX64.test.cpp index 28ce6a82..3f75ebac 100644 --- a/tests/AssemblyBuilderX64.test.cpp +++ b/tests/AssemblyBuilderX64.test.cpp @@ -22,7 +22,7 @@ std::string bytecodeAsArray(const std::vector& bytecode) class AssemblyBuilderX64Fixture { public: - void check(std::function f, std::vector result) + void check(std::function f, std::vector code, std::vector data = {}) { AssemblyBuilderX64 build(/* logText= */ false); @@ -30,9 +30,15 @@ public: build.finalize(); - if (build.code != result) + if (build.code != code) { - printf("Expected: %s\nReceived: %s\n", bytecodeAsArray(result).c_str(), bytecodeAsArray(build.code).c_str()); + printf("Expected code: %s\nReceived code: %s\n", bytecodeAsArray(code).c_str(), bytecodeAsArray(build.code).c_str()); + CHECK(false); + } + + if (build.data != data) + { + printf("Expected data: %s\nReceived data: %s\n", bytecodeAsArray(data).c_str(), bytecodeAsArray(build.data).c_str()); CHECK(false); } } @@ -169,6 +175,7 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "BaseUnaryInstructionForms") SINGLE_COMPARE(div(rcx), 0x48, 0xf7, 0xf1); SINGLE_COMPARE(idiv(qword[rax]), 0x48, 0xf7, 0x38); SINGLE_COMPARE(mul(qword[rax + rbx]), 0x48, 0xf7, 0x24, 0x18); + SINGLE_COMPARE(imul(r9), 0x49, 0xf7, 0xe9); SINGLE_COMPARE(neg(r9), 0x49, 0xf7, 0xd9); SINGLE_COMPARE(not_(r12), 0x49, 0xf7, 0xd4); } @@ -191,6 +198,18 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfMov") SINGLE_COMPARE(mov(byte[rsi], al), 0x88, 0x06); } +TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfMovExtended") +{ + SINGLE_COMPARE(movsx(eax, byte[rcx]), 0x0f, 0xbe, 0x01); + SINGLE_COMPARE(movsx(r12, byte[r10]), 0x4d, 0x0f, 0xbe, 0x22); + SINGLE_COMPARE(movsx(ebx, word[r11]), 0x41, 0x0f, 0xbf, 0x1b); + SINGLE_COMPARE(movsx(rdx, word[rcx]), 0x48, 0x0f, 0xbf, 0x11); + SINGLE_COMPARE(movzx(eax, byte[rcx]), 0x0f, 0xb6, 0x01); + SINGLE_COMPARE(movzx(r12, byte[r10]), 0x4d, 0x0f, 0xb6, 0x22); + SINGLE_COMPARE(movzx(ebx, word[r11]), 0x41, 0x0f, 0xb7, 0x1b); + SINGLE_COMPARE(movzx(rdx, word[rcx]), 0x48, 0x0f, 0xb7, 0x11); +} + TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfTest") { SINGLE_COMPARE(test(al, 8), 0xf6, 0xc0, 0x08); @@ -230,6 +249,19 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfAbsoluteJumps") SINGLE_COMPARE(call(qword[r14 + rdx * 4]), 0x49, 0xff, 0x14, 0x96); } +TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfImul") +{ + SINGLE_COMPARE(imul(ecx, esi), 0x0f, 0xaf, 0xce); + SINGLE_COMPARE(imul(r12, rax), 0x4c, 0x0f, 0xaf, 0xe0); + SINGLE_COMPARE(imul(r12, qword[rdx + rdi]), 0x4c, 0x0f, 0xaf, 0x24, 0x3a); + SINGLE_COMPARE(imul(ecx, edx, 8), 0x6b, 0xca, 0x08); + SINGLE_COMPARE(imul(ecx, r9d, 0xabcd), 0x41, 0x69, 0xc9, 0xcd, 0xab, 0x00, 0x00); + SINGLE_COMPARE(imul(r8d, eax, -9), 0x44, 0x6b, 0xc0, 0xf7); + SINGLE_COMPARE(imul(rcx, rdx, 17), 0x48, 0x6b, 0xca, 0x11); + SINGLE_COMPARE(imul(rcx, r12, 0xabcd), 0x49, 0x69, 0xcc, 0xcd, 0xab, 0x00, 0x00); + SINGLE_COMPARE(imul(r12, rax, -13), 0x4c, 0x6b, 0xe0, 0xf3); +} + TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "ControlFlow") { // Jump back @@ -335,6 +367,7 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXUnaryMergeInstructionForms") // Coverage for other instructions that follow the same pattern SINGLE_COMPARE(vcomisd(xmm8, xmm10), 0xc4, 0x41, 0xf9, 0x2f, 0xc2); + SINGLE_COMPARE(vucomisd(xmm1, xmm4), 0xc4, 0xe1, 0xf9, 0x2e, 0xcc); } TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXMoveInstructionForms") @@ -359,6 +392,25 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXMoveInstructionForms") SINGLE_COMPARE(vmovups(ymm8, ymmword[r9]), 0xc4, 0x41, 0xfc, 0x10, 0x01); } +TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXConversionInstructionForms") +{ + SINGLE_COMPARE(vcvttsd2si(ecx, xmm0), 0xc4, 0xe1, 0x7b, 0x2c, 0xc8); + SINGLE_COMPARE(vcvttsd2si(r9d, xmmword[rcx + rdx]), 0xc4, 0x61, 0x7b, 0x2c, 0x0c, 0x11); + SINGLE_COMPARE(vcvttsd2si(rdx, xmm0), 0xc4, 0xe1, 0xfb, 0x2c, 0xd0); + SINGLE_COMPARE(vcvttsd2si(r13, xmmword[rcx + rdx]), 0xc4, 0x61, 0xfb, 0x2c, 0x2c, 0x11); + SINGLE_COMPARE(vcvtsi2sd(xmm5, xmm10, ecx), 0xc4, 0xe1, 0x2b, 0x2a, 0xe9); + SINGLE_COMPARE(vcvtsi2sd(xmm6, xmm11, dword[rcx + rdx]), 0xc4, 0xe1, 0x23, 0x2a, 0x34, 0x11); + SINGLE_COMPARE(vcvtsi2sd(xmm5, xmm10, r13), 0xc4, 0xc1, 0xab, 0x2a, 0xed); + SINGLE_COMPARE(vcvtsi2sd(xmm6, xmm11, qword[rcx + rdx]), 0xc4, 0xe1, 0xa3, 0x2a, 0x34, 0x11); +} + +TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXTernaryInstructionForms") +{ + SINGLE_COMPARE(vroundsd(xmm7, xmm12, xmm3, 9), 0xc4, 0xe3, 0x99, 0x0b, 0xfb, 0x09); + SINGLE_COMPARE(vroundsd(xmm8, xmm13, xmmword[r13 + rdx], 9), 0xc4, 0x43, 0x91, 0x0b, 0x44, 0x15, 0x00, 0x09); + SINGLE_COMPARE(vroundsd(xmm9, xmm14, xmmword[rcx + r10], 1), 0xc4, 0x23, 0x89, 0x0b, 0x0c, 0x11, 0x01); +} + TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "MiscInstructions") { SINGLE_COMPARE(int3(), 0xcc); @@ -386,6 +438,11 @@ TEST_CASE("LogTest") build.neg(qword[rbp + r12 * 2]); build.mov64(r10, 0x1234567812345678ll); build.vmovapd(xmmword[rax], xmm11); + build.movzx(eax, byte[rcx]); + build.movsx(rsi, word[r12]); + build.imul(rcx, rdx); + build.imul(rcx, rdx, 8); + build.vroundsd(xmm1, xmm2, xmm3, 5); build.pop(r12); build.ret(); build.int3(); @@ -409,6 +466,11 @@ TEST_CASE("LogTest") neg qword ptr [rbp+r12*2] mov r10,1234567812345678h vmovapd xmmword ptr [rax],xmm11 + movzx eax,byte ptr [rcx] + movsx rsi,word ptr [r12] + imul rcx,rdx + imul rcx,rdx,8 + vroundsd xmm1,xmm2,xmm3,5 pop r12 ret int3 @@ -426,6 +488,8 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "Constants") build.vmovss(xmm2, build.f32(1.0f)); build.vmovsd(xmm3, build.f64(1.0)); build.vmovaps(xmm4, build.f32x4(1.0f, 2.0f, 4.0f, 8.0f)); + char arr[16] = "hello world!123"; + build.vmovupd(xmm5, build.bytes(arr, 16, 8)); build.ret(); }, { @@ -434,7 +498,20 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "Constants") 0xc4, 0xe1, 0xfa, 0x10, 0x15, 0xe1, 0xff, 0xff, 0xff, 0xc4, 0xe1, 0xfb, 0x10, 0x1d, 0xcc, 0xff, 0xff, 0xff, 0xc4, 0xe1, 0xf8, 0x28, 0x25, 0xab, 0xff, 0xff, 0xff, + 0xc4, 0xe1, 0xf9, 0x10, 0x2d, 0x92, 0xff, 0xff, 0xff, 0xc3 + }, + { + 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!', '1', '2', '3', 0x0, + 0x00, 0x00, 0x80, 0x3f, + 0x00, 0x00, 0x00, 0x40, + 0x00, 0x00, 0x80, 0x40, + 0x00, 0x00, 0x00, 0x41, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding to align f32x4 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, + 0x00, 0x00, 0x00, 0x00, // padding to align f64 + 0x00, 0x00, 0x80, 0x3f, + 0x21, 0x43, 0x65, 0x87, 0x78, 0x56, 0x34, 0x12, }); // clang-format on } @@ -444,7 +521,7 @@ TEST_CASE("ConstantStorage") AssemblyBuilderX64 build(/* logText= */ false); for (int i = 0; i <= 3000; i++) - build.vaddss(xmm0, xmm0, build.f32(float(i))); + build.vaddss(xmm0, xmm0, build.f32(1.0f)); build.finalize(); @@ -452,9 +529,10 @@ TEST_CASE("ConstantStorage") for (int i = 0; i <= 3000; i++) { - float v; - memcpy(&v, &build.data[build.data.size() - (i + 1) * sizeof(float)], sizeof(v)); - LUAU_ASSERT(v == float(i)); + LUAU_ASSERT(build.data[i * 4 + 0] == 0x00); + LUAU_ASSERT(build.data[i * 4 + 1] == 0x00); + LUAU_ASSERT(build.data[i * 4 + 2] == 0x80); + LUAU_ASSERT(build.data[i * 4 + 3] == 0x3f); } } diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 75c5a606..25447dd8 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -129,6 +129,7 @@ TEST_CASE_FIXTURE(ACFixture, "empty_program") CHECK(!ac.entryMap.empty()); CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "local_initializer") @@ -138,6 +139,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_initializer") auto ac = autocomplete('1'); CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); + CHECK_EQ(ac.context, AutocompleteContext::Expression); } TEST_CASE_FIXTURE(ACFixture, "leave_numbers_alone") @@ -146,6 +148,7 @@ TEST_CASE_FIXTURE(ACFixture, "leave_numbers_alone") auto ac = autocomplete('1'); CHECK(ac.entryMap.empty()); + CHECK_EQ(ac.context, AutocompleteContext::Unknown); } TEST_CASE_FIXTURE(ACFixture, "user_defined_globals") @@ -157,6 +160,7 @@ TEST_CASE_FIXTURE(ACFixture, "user_defined_globals") CHECK(ac.entryMap.count("myLocal")); CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "dont_suggest_local_before_its_definition") @@ -191,6 +195,7 @@ TEST_CASE_FIXTURE(ACFixture, "recursive_function") auto ac = autocomplete('1'); CHECK(ac.entryMap.count("foo")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "nested_recursive_function") @@ -293,6 +298,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "get_member_completions") CHECK(ac.entryMap.count("find")); CHECK(ac.entryMap.count("pack")); CHECK(!ac.entryMap.count("math")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "nested_member_completions") @@ -306,6 +312,7 @@ TEST_CASE_FIXTURE(ACFixture, "nested_member_completions") CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("def")); CHECK(ac.entryMap.count("egh")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "unsealed_table") @@ -319,6 +326,7 @@ TEST_CASE_FIXTURE(ACFixture, "unsealed_table") auto ac = autocomplete('1'); CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("prop")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "unsealed_table_2") @@ -333,6 +341,7 @@ TEST_CASE_FIXTURE(ACFixture, "unsealed_table_2") auto ac = autocomplete('1'); CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("prop")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "cyclic_table") @@ -346,6 +355,7 @@ TEST_CASE_FIXTURE(ACFixture, "cyclic_table") auto ac = autocomplete('1'); CHECK(ac.entryMap.count("abc")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "table_union") @@ -361,6 +371,7 @@ TEST_CASE_FIXTURE(ACFixture, "table_union") auto ac = autocomplete('1'); CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("b2")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "table_intersection") @@ -378,6 +389,7 @@ TEST_CASE_FIXTURE(ACFixture, "table_intersection") CHECK(ac.entryMap.count("a1")); CHECK(ac.entryMap.count("b2")); CHECK(ac.entryMap.count("c3")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACBuiltinsFixture, "get_string_completions") @@ -389,6 +401,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "get_string_completions") auto ac = autocomplete('1'); CHECK_EQ(17, ac.entryMap.size()); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_new_statement") @@ -400,6 +413,7 @@ TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_new_statement") CHECK_NE(0, ac.entryMap.size()); CHECK(ac.entryMap.count("table")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_the_very_start_of_the_script") @@ -412,6 +426,7 @@ TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_the_very_start_of_the_script") auto ac = autocomplete('1'); CHECK(ac.entryMap.count("table")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "method_call_inside_function_body") @@ -429,6 +444,7 @@ TEST_CASE_FIXTURE(ACFixture, "method_call_inside_function_body") CHECK_NE(0, ac.entryMap.size()); CHECK(!ac.entryMap.count("math")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACBuiltinsFixture, "method_call_inside_if_conditional") @@ -442,6 +458,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "method_call_inside_if_conditional") CHECK_NE(0, ac.entryMap.size()); CHECK(ac.entryMap.count("concat")); CHECK(!ac.entryMap.count("math")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "statement_between_two_statements") @@ -459,6 +476,8 @@ TEST_CASE_FIXTURE(ACFixture, "statement_between_two_statements") CHECK_NE(0, ac.entryMap.size()); CHECK(ac.entryMap.count("getmyscripts")); + + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "bias_toward_inner_scope") @@ -476,6 +495,7 @@ TEST_CASE_FIXTURE(ACFixture, "bias_toward_inner_scope") auto ac = autocomplete('1'); CHECK(ac.entryMap.count("A")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); TypeId t = follow(*ac.entryMap["A"].type); const TableTypeVar* tt = get(t); @@ -489,10 +509,12 @@ TEST_CASE_FIXTURE(ACFixture, "recommend_statement_starting_keywords") check("@1"); auto ac = autocomplete('1'); CHECK(ac.entryMap.count("local")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); check("local i = @1"); auto ac2 = autocomplete('1'); CHECK(!ac2.entryMap.count("local")); + CHECK_EQ(ac2.context, AutocompleteContext::Expression); } TEST_CASE_FIXTURE(ACFixture, "do_not_overwrite_context_sensitive_kws") @@ -508,6 +530,7 @@ TEST_CASE_FIXTURE(ACFixture, "do_not_overwrite_context_sensitive_kws") AutocompleteEntry entry = ac.entryMap["continue"]; CHECK(entry.kind == AutocompleteEntryKind::Binding); + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_comment") @@ -525,6 +548,7 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_comment") auto ac = autocomplete('1'); CHECK_EQ(0, ac.entryMap.size()); + CHECK_EQ(ac.context, AutocompleteContext::Unknown); } TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_the_end_of_a_comment") @@ -536,6 +560,7 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_the_end_of_a_comme auto ac = autocomplete('1'); CHECK_EQ(0, ac.entryMap.size()); + CHECK_EQ(ac.context, AutocompleteContext::Unknown); } TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_comment") @@ -547,6 +572,7 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_co auto ac = autocomplete('1'); CHECK_EQ(0, ac.entryMap.size()); + CHECK_EQ(ac.context, AutocompleteContext::Unknown); } TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_comment_at_the_very_end_of_the_file") @@ -555,6 +581,7 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_co auto ac = autocomplete('1'); CHECK_EQ(0, ac.entryMap.size()); + CHECK_EQ(ac.context, AutocompleteContext::Unknown); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") @@ -566,6 +593,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") auto ac1 = autocomplete('1'); CHECK_EQ(ac1.entryMap.count("do"), 0); CHECK_EQ(ac1.entryMap.count("end"), 0); + CHECK_EQ(ac1.context, AutocompleteContext::Unknown); check(R"( for x =@1 1 @@ -574,6 +602,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") auto ac2 = autocomplete('1'); CHECK_EQ(ac2.entryMap.count("do"), 0); CHECK_EQ(ac2.entryMap.count("end"), 0); + CHECK_EQ(ac2.context, AutocompleteContext::Unknown); check(R"( for x = 1,@1 2 @@ -582,6 +611,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") auto ac3 = autocomplete('1'); CHECK_EQ(1, ac3.entryMap.size()); CHECK_EQ(ac3.entryMap.count("do"), 1); + CHECK_EQ(ac3.context, AutocompleteContext::Keyword); check(R"( for x = 1, @12, @@ -590,6 +620,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") auto ac4 = autocomplete('1'); CHECK_EQ(ac4.entryMap.count("do"), 0); CHECK_EQ(ac4.entryMap.count("end"), 0); + CHECK_EQ(ac4.context, AutocompleteContext::Expression); check(R"( for x = 1, 2, @15 @@ -598,6 +629,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") auto ac5 = autocomplete('1'); CHECK_EQ(ac5.entryMap.count("do"), 1); CHECK_EQ(ac5.entryMap.count("end"), 0); + CHECK_EQ(ac5.context, AutocompleteContext::Keyword); check(R"( for x = 1, 2, 5 f@1 @@ -606,6 +638,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") auto ac6 = autocomplete('1'); CHECK_EQ(ac6.entryMap.size(), 1); CHECK_EQ(ac6.entryMap.count("do"), 1); + CHECK_EQ(ac6.context, AutocompleteContext::Keyword); check(R"( for x = 1, 2, 5 do @1 @@ -613,6 +646,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") auto ac7 = autocomplete('1'); CHECK_EQ(ac7.entryMap.count("end"), 1); + CHECK_EQ(ac7.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") @@ -623,6 +657,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") auto ac1 = autocomplete('1'); CHECK_EQ(0, ac1.entryMap.size()); + CHECK_EQ(ac1.context, AutocompleteContext::Unknown); check(R"( for x@1 @2 @@ -630,10 +665,12 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") auto ac2 = autocomplete('1'); CHECK_EQ(0, ac2.entryMap.size()); + CHECK_EQ(ac2.context, AutocompleteContext::Unknown); auto ac2a = autocomplete('2'); CHECK_EQ(1, ac2a.entryMap.size()); CHECK_EQ(1, ac2a.entryMap.count("in")); + CHECK_EQ(ac2a.context, AutocompleteContext::Keyword); check(R"( for x in y@1 @@ -642,6 +679,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") auto ac3 = autocomplete('1'); CHECK_EQ(ac3.entryMap.count("table"), 1); CHECK_EQ(ac3.entryMap.count("do"), 0); + CHECK_EQ(ac3.context, AutocompleteContext::Expression); check(R"( for x in y @1 @@ -650,6 +688,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") auto ac4 = autocomplete('1'); CHECK_EQ(ac4.entryMap.size(), 1); CHECK_EQ(ac4.entryMap.count("do"), 1); + CHECK_EQ(ac4.context, AutocompleteContext::Keyword); check(R"( for x in f f@1 @@ -658,6 +697,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") auto ac5 = autocomplete('1'); CHECK_EQ(ac5.entryMap.size(), 1); CHECK_EQ(ac5.entryMap.count("do"), 1); + CHECK_EQ(ac5.context, AutocompleteContext::Keyword); check(R"( for x in y do @1 @@ -668,6 +708,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") CHECK_EQ(ac6.entryMap.count("table"), 1); CHECK_EQ(ac6.entryMap.count("end"), 1); CHECK_EQ(ac6.entryMap.count("function"), 1); + CHECK_EQ(ac6.context, AutocompleteContext::Statement); check(R"( for x in y do e@1 @@ -678,6 +719,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") CHECK_EQ(ac7.entryMap.count("table"), 1); CHECK_EQ(ac7.entryMap.count("end"), 1); CHECK_EQ(ac7.entryMap.count("function"), 1); + CHECK_EQ(ac7.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords") @@ -689,6 +731,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords") auto ac1 = autocomplete('1'); CHECK_EQ(ac1.entryMap.count("do"), 0); CHECK_EQ(ac1.entryMap.count("end"), 0); + CHECK_EQ(ac1.context, AutocompleteContext::Expression); check(R"( while true @1 @@ -697,6 +740,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords") auto ac2 = autocomplete('1'); CHECK_EQ(1, ac2.entryMap.size()); CHECK_EQ(ac2.entryMap.count("do"), 1); + CHECK_EQ(ac2.context, AutocompleteContext::Keyword); check(R"( while true do @1 @@ -704,6 +748,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords") auto ac3 = autocomplete('1'); CHECK_EQ(ac3.entryMap.count("end"), 1); + CHECK_EQ(ac3.context, AutocompleteContext::Statement); check(R"( while true d@1 @@ -712,6 +757,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords") auto ac4 = autocomplete('1'); CHECK_EQ(1, ac4.entryMap.size()); CHECK_EQ(ac4.entryMap.count("do"), 1); + CHECK_EQ(ac4.context, AutocompleteContext::Keyword); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords") @@ -728,6 +774,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords") CHECK_EQ(ac1.entryMap.count("else"), 0); CHECK_EQ(ac1.entryMap.count("elseif"), 0); CHECK_EQ(ac1.entryMap.count("end"), 0); + CHECK_EQ(ac1.context, AutocompleteContext::Expression); check(R"( if x @1 @@ -739,6 +786,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords") CHECK_EQ(ac2.entryMap.count("else"), 0); CHECK_EQ(ac2.entryMap.count("elseif"), 0); CHECK_EQ(ac2.entryMap.count("end"), 0); + CHECK_EQ(ac2.context, AutocompleteContext::Keyword); check(R"( if x t@1 @@ -747,6 +795,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords") auto ac3 = autocomplete('1'); CHECK_EQ(1, ac3.entryMap.size()); CHECK_EQ(ac3.entryMap.count("then"), 1); + CHECK_EQ(ac3.context, AutocompleteContext::Keyword); check(R"( if x then @@ -760,6 +809,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords") CHECK_EQ(ac4.entryMap.count("function"), 1); CHECK_EQ(ac4.entryMap.count("elseif"), 1); CHECK_EQ(ac4.entryMap.count("end"), 0); + CHECK_EQ(ac4.context, AutocompleteContext::Statement); check(R"( if x then @@ -772,6 +822,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords") CHECK_EQ(ac4a.entryMap.count("table"), 1); CHECK_EQ(ac4a.entryMap.count("else"), 1); CHECK_EQ(ac4a.entryMap.count("elseif"), 1); + CHECK_EQ(ac4a.context, AutocompleteContext::Statement); check(R"( if x then @@ -786,6 +837,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords") CHECK_EQ(ac5.entryMap.count("else"), 0); CHECK_EQ(ac5.entryMap.count("elseif"), 0); CHECK_EQ(ac5.entryMap.count("end"), 0); + CHECK_EQ(ac5.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_in_repeat") @@ -797,6 +849,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_in_repeat") auto ac = autocomplete('1'); CHECK_EQ(ac.entryMap.count("table"), 1); CHECK_EQ(ac.entryMap.count("until"), 1); + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_expression") @@ -808,6 +861,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_expression") auto ac = autocomplete('1'); CHECK_EQ(ac.entryMap.count("table"), 1); + CHECK_EQ(ac.context, AutocompleteContext::Expression); } TEST_CASE_FIXTURE(ACFixture, "local_names") @@ -819,6 +873,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_names") auto ac1 = autocomplete('1'); CHECK_EQ(ac1.entryMap.size(), 1); CHECK_EQ(ac1.entryMap.count("function"), 1); + CHECK_EQ(ac1.context, AutocompleteContext::Unknown); check(R"( local ab, cd@1 @@ -826,6 +881,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_names") auto ac2 = autocomplete('1'); CHECK(ac2.entryMap.empty()); + CHECK_EQ(ac2.context, AutocompleteContext::Unknown); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_fn_exprs") @@ -836,6 +892,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_fn_exprs") auto ac = autocomplete('1'); CHECK_EQ(ac.entryMap.count("end"), 1); + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_lambda") @@ -846,6 +903,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_lambda") auto ac = autocomplete('1'); CHECK_EQ(ac.entryMap.count("end"), 1); + CHECK_EQ(ac.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "stop_at_first_stat_when_recommending_keywords") @@ -858,6 +916,7 @@ TEST_CASE_FIXTURE(ACFixture, "stop_at_first_stat_when_recommending_keywords") auto ac1 = autocomplete('1'); CHECK_EQ(ac1.entryMap.count("in"), 1); CHECK_EQ(ac1.entryMap.count("until"), 0); + CHECK_EQ(ac1.context, AutocompleteContext::Keyword); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_repeat_middle_keyword") @@ -980,6 +1039,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_function_params") auto ac2 = autocomplete('1'); CHECK_EQ(ac2.entryMap.count("abc"), 1); CHECK_EQ(ac2.entryMap.count("def"), 1); + CHECK_EQ(ac2.context, AutocompleteContext::Statement); check(R"( local function abc(def, ghi@1) @@ -988,6 +1048,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_function_params") auto ac3 = autocomplete('1'); CHECK(ac3.entryMap.empty()); + CHECK_EQ(ac3.context, AutocompleteContext::Unknown); } TEST_CASE_FIXTURE(ACFixture, "global_function_params") @@ -1022,6 +1083,7 @@ TEST_CASE_FIXTURE(ACFixture, "global_function_params") auto ac2 = autocomplete('1'); CHECK_EQ(ac2.entryMap.count("abc"), 1); CHECK_EQ(ac2.entryMap.count("def"), 1); + CHECK_EQ(ac2.context, AutocompleteContext::Statement); check(R"( function abc(def, ghi@1) @@ -1030,6 +1092,7 @@ TEST_CASE_FIXTURE(ACFixture, "global_function_params") auto ac3 = autocomplete('1'); CHECK(ac3.entryMap.empty()); + CHECK_EQ(ac3.context, AutocompleteContext::Unknown); } TEST_CASE_FIXTURE(ACFixture, "arguments_to_global_lambda") @@ -1074,6 +1137,7 @@ TEST_CASE_FIXTURE(ACFixture, "function_expr_params") auto ac2 = autocomplete('1'); CHECK_EQ(ac2.entryMap.count("def"), 1); + CHECK_EQ(ac2.context, AutocompleteContext::Statement); } TEST_CASE_FIXTURE(ACFixture, "local_initializer") @@ -1135,6 +1199,7 @@ local b: string = "don't trip" CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACFixture, "private_types") @@ -1203,6 +1268,7 @@ local a: aa auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 11}, nullCallback); CHECK(ac.entryMap.count("aaa")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACFixture, "module_type_members") @@ -1227,6 +1293,7 @@ local a: aaa. CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("A")); CHECK(ac.entryMap.count("B")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACFixture, "argument_types") @@ -1240,6 +1307,7 @@ local b: string = "don't trip" CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACFixture, "return_types") @@ -1253,6 +1321,7 @@ local b: string = "don't trip" CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACFixture, "as_types") @@ -1266,6 +1335,7 @@ local b: number = (a :: n@1 CHECK(ac.entryMap.count("nil")); CHECK(ac.entryMap.count("number")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACFixture, "function_type_types") @@ -1314,6 +1384,7 @@ local b: string = "don't trip" auto ac = autocomplete('1'); CHECK(ac.entryMap.count("Tee")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACFixture, "type_correct_suggestion_in_argument") @@ -1402,6 +1473,7 @@ local b: Foo = { a = a.@1 CHECK(ac.entryMap.count("one")); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::None); + CHECK_EQ(ac.context, AutocompleteContext::Property); check(R"( type Foo = { a: number, b: string } @@ -1414,6 +1486,7 @@ local b: Foo = { b = a.@1 CHECK(ac.entryMap.count("two")); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::None); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "type_correct_function_return_types") @@ -2395,6 +2468,7 @@ local t: Test = { f@1 } auto ac = autocomplete('1'); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); + CHECK_EQ(ac.context, AutocompleteContext::Property); // Intersection check(R"( @@ -2405,6 +2479,7 @@ local t: Test = { f@1 } ac = autocomplete('1'); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); + CHECK_EQ(ac.context, AutocompleteContext::Property); // Union check(R"( @@ -2416,6 +2491,7 @@ local t: Test = { s@1 } CHECK(ac.entryMap.count("second")); CHECK(!ac.entryMap.count("first")); CHECK(!ac.entryMap.count("third")); + CHECK_EQ(ac.context, AutocompleteContext::Property); // No parenthesis suggestion check(R"( @@ -2426,6 +2502,7 @@ local t: Test = { f@1 } ac = autocomplete('1'); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap["first"].parens == ParenthesesRecommendation::None); + CHECK_EQ(ac.context, AutocompleteContext::Property); // When key is changed check(R"( @@ -2436,6 +2513,7 @@ local t: Test = { f@1 = 2 } ac = autocomplete('1'); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); + CHECK_EQ(ac.context, AutocompleteContext::Property); // Alternative key syntax check(R"( @@ -2446,6 +2524,7 @@ local t: Test = { ["f@1"] } ac = autocomplete('1'); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); + CHECK_EQ(ac.context, AutocompleteContext::Property); // Not an alternative key syntax check(R"( @@ -2456,6 +2535,7 @@ local t: Test = { "f@1" } ac = autocomplete('1'); CHECK(!ac.entryMap.count("first")); CHECK(!ac.entryMap.count("second")); + CHECK_EQ(ac.context, AutocompleteContext::String); // Skip keys that are already defined check(R"( @@ -2466,6 +2546,7 @@ local t: Test = { first = 2, s@1 } ac = autocomplete('1'); CHECK(!ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); + CHECK_EQ(ac.context, AutocompleteContext::Property); // Don't skip active key check(R"( @@ -2476,6 +2557,7 @@ local t: Test = { first@1 } ac = autocomplete('1'); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); + CHECK_EQ(ac.context, AutocompleteContext::Property); // Inference after first key check(R"( @@ -2488,6 +2570,7 @@ local t = { ac = autocomplete('1'); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); + CHECK_EQ(ac.context, AutocompleteContext::Property); check(R"( local t = { @@ -2499,6 +2582,7 @@ local t = { ac = autocomplete('1'); CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("second")); + CHECK_EQ(ac.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_documentation_symbols") @@ -2542,6 +2626,7 @@ a = if temp then even elseif true then temp else e@9 CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); + CHECK_EQ(ac.context, AutocompleteContext::Expression); ac = autocomplete('2'); CHECK(ac.entryMap.count("temp") == 0); @@ -2549,18 +2634,21 @@ a = if temp then even elseif true then temp else e@9 CHECK(ac.entryMap.count("then")); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); + CHECK_EQ(ac.context, AutocompleteContext::Keyword); ac = autocomplete('3'); CHECK(ac.entryMap.count("even")); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); + CHECK_EQ(ac.context, AutocompleteContext::Expression); ac = autocomplete('4'); CHECK(ac.entryMap.count("even") == 0); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else")); CHECK(ac.entryMap.count("elseif")); + CHECK_EQ(ac.context, AutocompleteContext::Keyword); ac = autocomplete('5'); CHECK(ac.entryMap.count("temp")); @@ -2568,6 +2656,7 @@ a = if temp then even elseif true then temp else e@9 CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); + CHECK_EQ(ac.context, AutocompleteContext::Expression); ac = autocomplete('6'); CHECK(ac.entryMap.count("temp") == 0); @@ -2575,6 +2664,7 @@ a = if temp then even elseif true then temp else e@9 CHECK(ac.entryMap.count("then")); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); + CHECK_EQ(ac.context, AutocompleteContext::Keyword); ac = autocomplete('7'); CHECK(ac.entryMap.count("temp")); @@ -2582,17 +2672,20 @@ a = if temp then even elseif true then temp else e@9 CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); + CHECK_EQ(ac.context, AutocompleteContext::Expression); ac = autocomplete('8'); CHECK(ac.entryMap.count("even") == 0); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else")); CHECK(ac.entryMap.count("elseif")); + CHECK_EQ(ac.context, AutocompleteContext::Keyword); ac = autocomplete('9'); CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("elseif") == 0); + CHECK_EQ(ac.context, AutocompleteContext::Expression); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_else_regression") @@ -2626,6 +2719,7 @@ local a: A<(number, s@1> CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap.count("string")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_first_function_arg_expected_type") @@ -2686,6 +2780,7 @@ type A = () -> T CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap.count("string")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_default_type_pack_parameters") @@ -2698,6 +2793,7 @@ type A = () -> T CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap.count("string")); + CHECK_EQ(ac.context, AutocompleteContext::Type); } TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_oop_implicit_self") @@ -2752,16 +2848,19 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singletons") CHECK(ac.entryMap.count("cat")); CHECK(ac.entryMap.count("dog")); + CHECK_EQ(ac.context, AutocompleteContext::String); ac = autocomplete('2'); CHECK(ac.entryMap.count("\"cat\"")); CHECK(ac.entryMap.count("\"dog\"")); + CHECK_EQ(ac.context, AutocompleteContext::Expression); ac = autocomplete('3'); CHECK(ac.entryMap.count("cat")); CHECK(ac.entryMap.count("dog")); + CHECK_EQ(ac.context, AutocompleteContext::String); check(R"( type tagged = {tag:"cat", fieldx:number} | {tag:"dog", fieldy:number} @@ -2772,6 +2871,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singletons") CHECK(ac.entryMap.count("cat")); CHECK(ac.entryMap.count("dog")); + CHECK_EQ(ac.context, AutocompleteContext::String); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_equality") @@ -2808,6 +2908,7 @@ f(@1) CHECK(ac.entryMap["true"].typeCorrect == TypeCorrectKind::Correct); REQUIRE(ac.entryMap.count("false")); CHECK(ac.entryMap["false"].typeCorrect == TypeCorrectKind::None); + CHECK_EQ(ac.context, AutocompleteContext::Expression); } TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_escape") @@ -3088,4 +3189,27 @@ a.@1 CHECK(ac.entryMap.count("y")); } +TEST_CASE_FIXTURE(ACFixture, "globals_are_order_independent") +{ + ScopedFastFlag sff("LuauAutocompleteFixGlobalOrder", true); + + check(R"( + local myLocal = 4 + function abc0() + local myInnerLocal = 1 +@1 + end + + function abc1() + local myInnerLocal = 1 + end + )"); + + auto ac = autocomplete('1'); + CHECK(ac.entryMap.count("myLocal")); + CHECK(ac.entryMap.count("myInnerLocal")); + CHECK(ac.entryMap.count("abc0")); + CHECK(ac.entryMap.count("abc1")); +} + TEST_SUITE_END(); diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index 90aa7139..1aa69112 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -261,8 +261,6 @@ L1: RETURN R0 0 TEST_CASE("ForBytecode") { - ScopedFastFlag sff("LuauCompileNoIpairs", true); - // basic for loop: variable directly refers to internal iteration index (R2) CHECK_EQ("\n" + compileFunction0("for i=1,5 do print(i) end"), R"( LOADN R2 1 @@ -349,8 +347,6 @@ RETURN R0 0 TEST_CASE("ForBytecodeBuiltin") { - ScopedFastFlag sff("LuauCompileNoIpairs", true); - // we generally recognize builtins like pairs/ipairs and emit special opcodes CHECK_EQ("\n" + compileFunction0("for k,v in ipairs({}) do end"), R"( GETIMPORT R0 1 @@ -2117,6 +2113,69 @@ TEST_CASE("RecursionParse") { CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your block to make the code compile"); } + + try + { + Luau::compileOrThrow(bcb, rep("a(", 1500) + "42" + rep(")", 1500)); + CHECK(!"Expected exception"); + } + catch (std::exception& e) + { + CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your expression to make the code compile"); + } + + try + { + Luau::compileOrThrow(bcb, "return " + rep("{", 1500) + "42" + rep("}", 1500)); + CHECK(!"Expected exception"); + } + catch (std::exception& e) + { + CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your expression to make the code compile"); + } + + try + { + Luau::compileOrThrow(bcb, rep("while true do ", 1500) + "print()" + rep(" end", 1500)); + CHECK(!"Expected exception"); + } + catch (std::exception& e) + { + CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your expression to make the code compile"); + } + + try + { + Luau::compileOrThrow(bcb, rep("for i=1,1 do ", 1500) + "print()" + rep(" end", 1500)); + CHECK(!"Expected exception"); + } + catch (std::exception& e) + { + CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your expression to make the code compile"); + } + +#if 0 + // This currently requires too much stack space on MSVC/x64 and crashes with stack overflow at recursion depth 935 + try + { + Luau::compileOrThrow(bcb, rep("function a() ", 1500) + "print()" + rep(" end", 1500)); + CHECK(!"Expected exception"); + } + catch (std::exception& e) + { + CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your block to make the code compile"); + } + + try + { + Luau::compileOrThrow(bcb, "return " + rep("function() return ", 1500) + "42" + rep(" end", 1500)); + CHECK(!"Expected exception"); + } + catch (std::exception& e) + { + CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your block to make the code compile"); + } +#endif } TEST_CASE("ArrayIndexLiteral") @@ -2163,8 +2222,6 @@ L1: RETURN R3 -1 TEST_CASE("UpvaluesLoopsBytecode") { - ScopedFastFlag sff("LuauCompileNoIpairs", true); - CHECK_EQ("\n" + compileFunction(R"( function test() for i=1,10 do @@ -2780,46 +2837,44 @@ RETURN R0 0 TEST_CASE("AssignmentConflict") { + ScopedFastFlag sff("LuauCompileOptimalAssignment", true); + // assignments are left to right CHECK_EQ("\n" + compileFunction0("local a, b a, b = 1, 2"), R"( LOADNIL R0 LOADNIL R1 -LOADN R2 1 -LOADN R3 2 -MOVE R0 R2 -MOVE R1 R3 +LOADN R0 1 +LOADN R1 2 RETURN R0 0 )"); - // if assignment of a local invalidates a direct register reference in later assignments, the value is evacuated to a temp register + // if assignment of a local invalidates a direct register reference in later assignments, the value is assigned to a temp register first CHECK_EQ("\n" + compileFunction0("local a a, a[1] = 1, 2"), R"( LOADNIL R0 -MOVE R1 R0 -LOADN R2 1 -LOADN R3 2 -MOVE R0 R2 -SETTABLEN R3 R1 1 +LOADN R1 1 +LOADN R2 2 +SETTABLEN R2 R0 1 +MOVE R0 R1 RETURN R0 0 )"); // note that this doesn't happen if the local assignment happens last naturally CHECK_EQ("\n" + compileFunction0("local a a[1], a = 1, 2"), R"( LOADNIL R0 -LOADN R1 1 -LOADN R2 2 -SETTABLEN R1 R0 1 -MOVE R0 R2 +LOADN R2 1 +LOADN R1 2 +SETTABLEN R2 R0 1 +MOVE R0 R1 RETURN R0 0 )"); // this will happen if assigned register is used in any table expression, including as an object... CHECK_EQ("\n" + compileFunction0("local a a, a.foo = 1, 2"), R"( LOADNIL R0 -MOVE R1 R0 -LOADN R2 1 -LOADN R3 2 -MOVE R0 R2 -SETTABLEKS R3 R1 K0 +LOADN R1 1 +LOADN R2 2 +SETTABLEKS R2 R0 K0 +MOVE R0 R1 RETURN R0 0 )"); @@ -2827,22 +2882,20 @@ RETURN R0 0 CHECK_EQ("\n" + compileFunction0("local a a, foo[a] = 1, 2"), R"( LOADNIL R0 GETIMPORT R1 1 -MOVE R2 R0 -LOADN R3 1 -LOADN R4 2 -MOVE R0 R3 -SETTABLE R4 R1 R2 +LOADN R2 1 +LOADN R3 2 +SETTABLE R3 R1 R0 +MOVE R0 R2 RETURN R0 0 )"); // ... or both ... CHECK_EQ("\n" + compileFunction0("local a a, a[a] = 1, 2"), R"( LOADNIL R0 -MOVE R1 R0 -LOADN R2 1 -LOADN R3 2 -MOVE R0 R2 -SETTABLE R3 R1 R1 +LOADN R1 1 +LOADN R2 2 +SETTABLE R2 R0 R0 +MOVE R0 R1 RETURN R0 0 )"); @@ -2850,14 +2903,12 @@ RETURN R0 0 CHECK_EQ("\n" + compileFunction0("local a, b a, b, a[b] = 1, 2, 3"), R"( LOADNIL R0 LOADNIL R1 -MOVE R2 R0 -MOVE R3 R1 -LOADN R4 1 -LOADN R5 2 -LOADN R6 3 -MOVE R0 R4 -MOVE R1 R5 -SETTABLE R6 R2 R3 +LOADN R2 1 +LOADN R3 2 +LOADN R4 3 +SETTABLE R4 R0 R1 +MOVE R0 R2 +MOVE R1 R3 RETURN R0 0 )"); @@ -2867,10 +2918,9 @@ RETURN R0 0 LOADNIL R0 GETIMPORT R1 1 ADDK R2 R0 K2 -LOADN R3 1 -LOADN R4 2 -MOVE R0 R3 -SETTABLE R4 R1 R2 +LOADN R0 1 +LOADN R3 2 +SETTABLE R3 R1 R2 RETURN R0 0 )"); } @@ -3849,8 +3899,6 @@ RETURN R0 1 TEST_CASE("SharedClosure") { - ScopedFastFlag sff("LuauCompileFreeReassign", true); - // closures can be shared even if functions refer to upvalues, as long as upvalues are top-level CHECK_EQ("\n" + compileFunction(R"( local val = ... @@ -6063,6 +6111,8 @@ return math.clamp(-1, 0, 1), math.sign(77), math.round(7.6), + bit32.extract(-1, 31), + bit32.replace(100, 1, 0), (type("fin")) )", 0, 2), @@ -6114,8 +6164,10 @@ LOADK R43 K2 LOADN R44 0 LOADN R45 1 LOADN R46 8 -LOADK R47 K3 -RETURN R0 48 +LOADN R47 1 +LOADN R48 101 +LOADK R49 K3 +RETURN R0 50 )"); } @@ -6126,7 +6178,8 @@ return math.abs(), math.max(1, true), string.byte("abc", 42), - bit32.rshift(10, 42) + bit32.rshift(10, 42), + bit32.extract(1, 2, "3") )", 0, 2), R"( @@ -6147,8 +6200,14 @@ L2: LOADN R4 10 FASTCALL2K 39 R4 K7 L3 LOADK R5 K7 GETIMPORT R3 13 -CALL R3 2 -1 -L3: RETURN R0 -1 +CALL R3 2 1 +L3: LOADN R5 1 +LOADN R6 2 +LOADK R7 K14 +FASTCALL 34 L4 +GETIMPORT R4 16 +CALL R4 3 -1 +L4: RETURN R0 -1 )"); } @@ -6205,8 +6264,6 @@ RETURN R0 1 TEST_CASE("LocalReassign") { - ScopedFastFlag sff("LuauCompileFreeReassign", true); - // locals can be re-assigned and the register gets reused CHECK_EQ("\n" + compileFunction0(R"( local function test(a, b) @@ -6294,4 +6351,250 @@ RETURN R2 1 )"); } +TEST_CASE("MultipleAssignments") +{ + ScopedFastFlag sff("LuauCompileOptimalAssignment", true); + + // order of assignments is left to right + CHECK_EQ("\n" + compileFunction0(R"( + local a, b + a, b = f(1), f(2) + )"), + R"( +LOADNIL R0 +LOADNIL R1 +GETIMPORT R2 1 +LOADN R3 1 +CALL R2 1 1 +MOVE R0 R2 +GETIMPORT R2 1 +LOADN R3 2 +CALL R2 1 1 +MOVE R1 R2 +RETURN R0 0 +)"); + + // this includes table assignments + CHECK_EQ("\n" + compileFunction0(R"( + local t + t[1], t[2] = 3, 4 + )"), + R"( +LOADNIL R0 +LOADNIL R1 +LOADN R2 3 +LOADN R3 4 +SETTABLEN R2 R0 1 +SETTABLEN R3 R1 2 +RETURN R0 0 +)"); + + // semantically, we evaluate the right hand side first; this allows us to e.g swap elements in a table easily + CHECK_EQ("\n" + compileFunction0(R"( + local t = ... + t[1], t[2] = t[2], t[1] + )"), + R"( +GETVARARGS R0 1 +GETTABLEN R1 R0 2 +GETTABLEN R2 R0 1 +SETTABLEN R1 R0 1 +SETTABLEN R2 R0 2 +RETURN R0 0 +)"); + + // however, we need to optimize local assignments; to do this well, we need to handle assignment conflicts + // let's first go through a few cases where there are no conflicts: + + // when multiple assignments have no conflicts (all local vars are read after being assigned), codegen is the same as a series of single + // assignments + CHECK_EQ("\n" + compileFunction0(R"( + local xm1, x, xp1, xi = ... + + xm1,x,xp1,xi = x,xp1,xp1+1,xi-1 + )"), + R"( +GETVARARGS R0 4 +MOVE R0 R1 +MOVE R1 R2 +ADDK R2 R2 K0 +SUBK R3 R3 K0 +RETURN R0 0 +)"); + + // similar example to above from a more complex case + CHECK_EQ("\n" + compileFunction0(R"( + local a, b, c, d, e, f, g, h, t1, t2 = ... + + h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2 + )"), + R"( +GETVARARGS R0 10 +MOVE R7 R6 +MOVE R6 R5 +MOVE R5 R4 +ADD R4 R3 R8 +MOVE R3 R2 +MOVE R2 R1 +MOVE R1 R0 +ADD R0 R8 R9 +RETURN R0 0 +)"); + + // when locals have a conflict, we assign temporaries instead of locals, and at the end copy the values back + // the basic example of this is a swap/rotate + CHECK_EQ("\n" + compileFunction0(R"( + local a, b = ... + a, b = b, a + )"), + R"( +GETVARARGS R0 2 +MOVE R2 R1 +MOVE R1 R0 +MOVE R0 R2 +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction0(R"( + local a, b, c = ... + a, b, c = c, a, b + )"), + R"( +GETVARARGS R0 3 +MOVE R3 R2 +MOVE R4 R0 +MOVE R2 R1 +MOVE R0 R3 +MOVE R1 R4 +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction0(R"( + local a, b, c = ... + a, b, c = b, c, a + )"), + R"( +GETVARARGS R0 3 +MOVE R3 R1 +MOVE R1 R2 +MOVE R2 R0 +MOVE R0 R3 +RETURN R0 0 +)"); + + // multiple assignments with multcall handling - foo() evalutes to temporary registers and they are copied out to target + CHECK_EQ("\n" + compileFunction0(R"( + local a, b, c, d = ... + a, b, c, d = 1, foo() + )"), + R"( +GETVARARGS R0 4 +LOADN R0 1 +GETIMPORT R4 1 +CALL R4 0 3 +MOVE R1 R4 +MOVE R2 R5 +MOVE R3 R6 +RETURN R0 0 +)"); + + // note that during this we still need to handle local reassignment, eg when table assignments are performed + CHECK_EQ("\n" + compileFunction0(R"( + local a, b, c, d = ... + a, b[a], c[d], d = 1, foo() + )"), + R"( +GETVARARGS R0 4 +LOADN R4 1 +GETIMPORT R6 1 +CALL R6 0 3 +SETTABLE R6 R1 R0 +SETTABLE R7 R2 R3 +MOVE R0 R4 +MOVE R3 R8 +RETURN R0 0 +)"); + + // multiple assignments with multcall handling - foo evaluates to a single argument so all remaining locals are assigned to nil + // note that here we don't assign the locals directly, as this case is very rare so we use the similar code path as above + CHECK_EQ("\n" + compileFunction0(R"( + local a, b, c, d = ... + a, b, c, d = 1, foo + )"), + R"( +GETVARARGS R0 4 +LOADN R0 1 +GETIMPORT R4 1 +LOADNIL R5 +LOADNIL R6 +MOVE R1 R4 +MOVE R2 R5 +MOVE R3 R6 +RETURN R0 0 +)"); + + // note that we also try to use locals as a source of assignment directly when assigning fields; this works using old local value when possible + CHECK_EQ("\n" + compileFunction0(R"( + local a, b = ... + a[1], a[2] = b, b + 1 + )"), + R"( +GETVARARGS R0 2 +ADDK R2 R1 K0 +SETTABLEN R1 R0 1 +SETTABLEN R2 R0 2 +RETURN R0 0 +)"); + + // ... of course if the local is reassigned, we defer the assignment until later + CHECK_EQ("\n" + compileFunction0(R"( + local a, b = ... + b, a[1] = 42, b + )"), + R"( +GETVARARGS R0 2 +LOADN R2 42 +SETTABLEN R1 R0 1 +MOVE R1 R2 +RETURN R0 0 +)"); + + // when there are more expressions when values, we evalute them for side effects, but they also participate in conflict handling + CHECK_EQ("\n" + compileFunction0(R"( + local a, b = ... + a, b = 1, 2, a + b + )"), + R"( +GETVARARGS R0 2 +LOADN R2 1 +LOADN R3 2 +ADD R4 R0 R1 +MOVE R0 R2 +MOVE R1 R3 +RETURN R0 0 +)"); +} + +TEST_CASE("BuiltinExtractK") +{ + ScopedFastFlag sff("LuauCompileExtractK", true); + + // below, K0 refers to a packed f+w constant for bit32.extractk builtin + // K1 and K2 refer to 1 and 3 and are only used during fallback path + CHECK_EQ("\n" + compileFunction0(R"( +local v = ... + +return bit32.extract(v, 1, 3) +)"), R"( +GETVARARGS R0 1 +FASTCALL2K 59 R0 K0 L0 +MOVE R2 R0 +LOADK R3 K1 +LOADK R4 K2 +GETIMPORT R1 5 +CALL R1 3 -1 +L0: RETURN R1 -1 +)"); +} + TEST_SUITE_END(); diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 0739ca79..89cb0751 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -724,6 +724,23 @@ TEST_CASE("Reference") CHECK(dtorhits == 2); } +TEST_CASE("NewUserdataOverflow") +{ + StateRef globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + lua_pushcfunction(L, [](lua_State* L1) { + // The following userdata request might cause an overflow. + lua_newuserdatadtor(L1, SIZE_MAX, [](void* d){}); + // The overflow might segfault in the following call. + lua_getmetatable(L1, -1); + return 0; + }, nullptr); + + CHECK(lua_pcall(L, 0, 0, 0) == LUA_ERRRUN); + CHECK(strcmp(lua_tostring(L, -1), "memory allocation error: block too big") == 0); +} + TEST_CASE("ApiTables") { StateRef globalState(luaL_newstate(), lua_close); diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index f51a9d1b..40be39a0 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -372,17 +372,25 @@ void Fixture::registerTestTypes() void Fixture::dumpErrors(const CheckResult& cr) { - dumpErrors(std::cout, cr.errors); + std::string error = getErrors(cr); + if (!error.empty()) + MESSAGE(error); } void Fixture::dumpErrors(const ModulePtr& module) { - dumpErrors(std::cout, module->errors); + std::stringstream ss; + dumpErrors(ss, module->errors); + if (!ss.str().empty()) + MESSAGE(ss.str()); } void Fixture::dumpErrors(const Module& module) { - dumpErrors(std::cout, module.errors); + std::stringstream ss; + dumpErrors(ss, module.errors); + if (!ss.str().empty()) + MESSAGE(ss.str()); } std::string Fixture::getErrors(const CheckResult& cr) @@ -413,6 +421,7 @@ LoadDefinitionFileResult Fixture::loadDefinition(const std::string& source) LoadDefinitionFileResult result = frontend.loadDefinitionFile(source, "@test"); freeze(typeChecker.globalTypes); + dumpErrors(result.module); REQUIRE_MESSAGE(result.success, "loadDefinition: unable to load definition file"); return result; } @@ -434,7 +443,8 @@ BuiltinsFixture::BuiltinsFixture(bool freeze, bool prepareAutocomplete) ConstraintGraphBuilderFixture::ConstraintGraphBuilderFixture() : Fixture() - , cgb(mainModuleName, &arena, NotNull(&ice), frontend.getGlobalScope()) + , mainModule(new Module) + , cgb(mainModuleName, mainModule, &arena, NotNull(&ice), frontend.getGlobalScope()) , forceTheFlag{"DebugLuauDeferredConstraintResolution", true} { BlockedTypeVar::nextIndex = 0; diff --git a/tests/Fixture.h b/tests/Fixture.h index a716fe9b..8dc3dd24 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -162,6 +162,7 @@ struct BuiltinsFixture : Fixture struct ConstraintGraphBuilderFixture : Fixture { TypeArena arena; + ModulePtr mainModule; ConstraintGraphBuilder cgb; ScopedFastFlag forceTheFlag; diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index 5a32cc84..5a4ab33e 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -1689,7 +1689,7 @@ TEST_CASE_FIXTURE(Fixture, "TestStringInterpolation") REQUIRE_EQ(result.warnings.size(), 1); } -TEST_CASE_FIXTURE(Fixture, "LintIntegerParsing") +TEST_CASE_FIXTURE(Fixture, "IntegerParsing") { ScopedFastFlag luauLintParseIntegerIssues{"LuauLintParseIntegerIssues", true}; @@ -1704,7 +1704,7 @@ local _ = 0x10000000000000000 } // TODO: remove with FFlagLuauErrorDoubleHexPrefix -TEST_CASE_FIXTURE(Fixture, "LintIntegerParsingDoublePrefix") +TEST_CASE_FIXTURE(Fixture, "IntegerParsingDoublePrefix") { ScopedFastFlag luauLintParseIntegerIssues{"LuauLintParseIntegerIssues", true}; ScopedFastFlag luauErrorDoubleHexPrefix{"LuauErrorDoubleHexPrefix", false}; // Lint will be available until we start rejecting code @@ -1721,4 +1721,36 @@ local _ = 0x0xffffffffffffffffffffffffffffffffff "Hexadecimal number literal has a double prefix, which will fail to parse in the future; remove the extra 0x to fix"); } +TEST_CASE_FIXTURE(Fixture, "ComparisonPrecedence") +{ + ScopedFastFlag sff("LuauLintComparisonPrecedence", true); + + LintResult result = lint(R"( +local a, b = ... + +local _ = not a == b +local _ = not a ~= b +local _ = not a <= b +local _ = a <= b == 0 + +local _ = not a == not b -- weird but ok + +-- silence tests for all of the above +local _ = not (a == b) +local _ = (not a) == b +local _ = not (a ~= b) +local _ = (not a) ~= b +local _ = not (a <= b) +local _ = (not a) <= b +local _ = (a <= b) == 0 +local _ = a <= (b == 0) +)"); + + REQUIRE_EQ(result.warnings.size(), 4); + CHECK_EQ(result.warnings[0].text, "not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or wrap one of the expressions in parentheses to silence"); + CHECK_EQ(result.warnings[1].text, "not X ~= Y is equivalent to (not X) ~= Y; consider using X == Y, or wrap one of the expressions in parentheses to silence"); + CHECK_EQ(result.warnings[2].text, "not X <= Y is equivalent to (not X) <= Y; wrap one of the expressions in parentheses to silence"); + CHECK_EQ(result.warnings[3].text, "X <= Y == Z is equivalent to (X <= Y) == Z; wrap one of the expressions in parentheses to silence"); +} + TEST_SUITE_END(); diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index dd94e9d7..5ec375c1 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -317,4 +317,78 @@ type B = A CHECK(toString(it->second.type) == "any"); } +TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_reexports") +{ + ScopedFastFlag flags[] = { + {"LuauClonePublicInterfaceLess", true}, + {"LuauSubstitutionReentrant", true}, + {"LuauClassTypeVarsInSubstitution", true}, + {"LuauSubstitutionFixMissingFields", true}, + }; + + fileResolver.source["Module/A"] = R"( +export type A = {p : number} +return {} + )"; + + fileResolver.source["Module/B"] = R"( +local a = require(script.Parent.A) +export type B = {q : a.A} +return {} + )"; + + CheckResult result = frontend.check("Module/B"); + LUAU_REQUIRE_NO_ERRORS(result); + + ModulePtr modA = frontend.moduleResolver.getModule("Module/A"); + ModulePtr modB = frontend.moduleResolver.getModule("Module/B"); + REQUIRE(modA); + REQUIRE(modB); + auto modAiter = modA->getModuleScope()->exportedTypeBindings.find("A"); + auto modBiter = modB->getModuleScope()->exportedTypeBindings.find("B"); + REQUIRE(modAiter != modA->getModuleScope()->exportedTypeBindings.end()); + REQUIRE(modBiter != modB->getModuleScope()->exportedTypeBindings.end()); + TypeId typeA = modAiter->second.type; + TypeId typeB = modBiter->second.type; + TableTypeVar* tableB = getMutable(typeB); + REQUIRE(tableB); + CHECK(typeA == tableB->props["q"].type); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_types_of_reexported_values") +{ + ScopedFastFlag flags[] = { + {"LuauClonePublicInterfaceLess", true}, + {"LuauSubstitutionReentrant", true}, + {"LuauClassTypeVarsInSubstitution", true}, + {"LuauSubstitutionFixMissingFields", true}, + }; + + fileResolver.source["Module/A"] = R"( +local exports = {a={p=5}} +return exports + )"; + + fileResolver.source["Module/B"] = R"( +local a = require(script.Parent.A) +local exports = {b=a.a} +return exports + )"; + + CheckResult result = frontend.check("Module/B"); + LUAU_REQUIRE_NO_ERRORS(result); + + ModulePtr modA = frontend.moduleResolver.getModule("Module/A"); + ModulePtr modB = frontend.moduleResolver.getModule("Module/B"); + REQUIRE(modA); + REQUIRE(modB); + std::optional typeA = first(modA->getModuleScope()->returnType); + std::optional typeB = first(modB->getModuleScope()->returnType); + REQUIRE(typeA); + REQUIRE(typeB); + TableTypeVar* tableA = getMutable(*typeA); + TableTypeVar* tableB = getMutable(*typeB); + CHECK(tableA->props["a"].type == tableB->props["b"].type); +} + TEST_SUITE_END(); diff --git a/tests/NonstrictMode.test.cpp b/tests/NonstrictMode.test.cpp index 02e02e6b..89aab5ee 100644 --- a/tests/NonstrictMode.test.cpp +++ b/tests/NonstrictMode.test.cpp @@ -169,6 +169,7 @@ TEST_CASE_FIXTURE(Fixture, "table_props_are_any") REQUIRE(ttv != nullptr); + REQUIRE(ttv->props.count("foo")); TypeId fooProp = ttv->props["foo"].type; REQUIRE(fooProp != nullptr); diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index c64c41c5..b017d8dd 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -12,6 +12,11 @@ using namespace Luau; struct NormalizeFixture : Fixture { ScopedFastFlag sff1{"LuauLowerBoundsCalculation", true}; + + bool isSubtype(TypeId a, TypeId b) + { + return ::Luau::isSubtype(a, b, NotNull{getMainModule()->getModuleScope().get()}, ice); + } }; void createSomeClasses(TypeChecker& typeChecker) @@ -49,12 +54,6 @@ void createSomeClasses(TypeChecker& typeChecker) freeze(arena); } -static bool isSubtype(TypeId a, TypeId b) -{ - InternalErrorReporter ice; - return isSubtype(a, b, ice); -} - TEST_SUITE_BEGIN("isSubtype"); TEST_CASE_FIXTURE(NormalizeFixture, "primitives") @@ -511,6 +510,8 @@ TEST_CASE_FIXTURE(NormalizeFixture, "classes") { createSomeClasses(typeChecker); + check(""); // Ensure that we have a main Module. + TypeId p = typeChecker.globalScope->lookupType("Parent")->type; TypeId c = typeChecker.globalScope->lookupType("Child")->type; TypeId u = typeChecker.globalScope->lookupType("Unrelated")->type; @@ -595,6 +596,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "union_with_overlapping_field_that_has_a_sub )"); ModulePtr tempModule{new Module}; + tempModule->scopes.emplace_back(Location(), std::make_shared(getSingletonTypes().anyTypePack)); // HACK: Normalization is an in-place operation. We need to cheat a little here and unfreeze // the arena that the type lives in. @@ -880,7 +882,6 @@ TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersect { ScopedFastFlag flags[] = { {"LuauLowerBoundsCalculation", true}, - {"LuauQuantifyConstrained", true}, }; // We use a function and inferred parameter types to prevent intermediate normalizations from being performed. @@ -921,7 +922,6 @@ TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersect { ScopedFastFlag flags[] = { {"LuauLowerBoundsCalculation", true}, - {"LuauQuantifyConstrained", true}, }; // We use a function and inferred parameter types to prevent intermediate normalizations from being performed. @@ -961,7 +961,6 @@ TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersect { ScopedFastFlag flags[] = { {"LuauLowerBoundsCalculation", true}, - {"LuauQuantifyConstrained", true}, }; // We use a function and inferred parameter types to prevent intermediate normalizations from being performed. @@ -1149,7 +1148,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "normalization_does_not_convert_ever") { ScopedFastFlag sff[]{ {"LuauLowerBoundsCalculation", true}, - {"LuauQuantifyConstrained", true}, }; CheckResult result = check(R"( diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index 95fccb06..d01a4279 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -2658,7 +2658,6 @@ end TEST_CASE_FIXTURE(Fixture, "error_message_for_using_function_as_type_annotation") { - ScopedFastFlag sff{"LuauParserFunctionKeywordAsTypeHelp", true}; ParseResult result = tryParse(R"( type Foo = function )"); diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index 4545b8db..fd6fb83f 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -11,6 +11,33 @@ using namespace Luau; TEST_SUITE_BEGIN("DefinitionTests"); +TEST_CASE_FIXTURE(Fixture, "definition_file_simple") +{ + loadDefinition(R"( + declare foo: number + declare function bar(x: number): string + declare foo2: typeof(foo) + )"); + + TypeId globalFooTy = getGlobalBinding(frontend.typeChecker, "foo"); + CHECK_EQ(toString(globalFooTy), "number"); + + TypeId globalBarTy = getGlobalBinding(frontend.typeChecker, "bar"); + CHECK_EQ(toString(globalBarTy), "(number) -> string"); + + TypeId globalFoo2Ty = getGlobalBinding(frontend.typeChecker, "foo2"); + CHECK_EQ(toString(globalFoo2Ty), "number"); + + CheckResult result = check(R"( + local x: number = foo - 1 + local y: string = bar(x) + local z: number | string = x + z = y + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_CASE_FIXTURE(Fixture, "definition_file_loading") { loadDefinition(R"( diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 074c86c3..5cc759d6 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -1637,7 +1637,6 @@ TEST_CASE_FIXTURE(Fixture, "quantify_constrained_types") { ScopedFastFlag sff[]{ {"LuauLowerBoundsCalculation", true}, - {"LuauQuantifyConstrained", true}, }; CheckResult result = check(R"( @@ -1662,7 +1661,6 @@ TEST_CASE_FIXTURE(Fixture, "call_o_with_another_argument_after_foo_was_quantifie { ScopedFastFlag sff[]{ {"LuauLowerBoundsCalculation", true}, - {"LuauQuantifyConstrained", true}, }; CheckResult result = check(R"( diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index a8325727..9ac259cf 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -845,6 +845,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_table_method") TableTypeVar* tTable = getMutable(tType); REQUIRE(tTable != nullptr); + REQUIRE(tTable->props.count("bar")); TypeId barType = tTable->props["bar"].type; REQUIRE(barType != nullptr); diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index a1d41339..b5f2296c 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -329,6 +329,35 @@ function tbl:foo(b: number, c: number) -- introduce BoundTypeVar to imported type arrayops.foo(self._regions) end +-- this alias decreases function type level and causes a demotion of its type +type Table = typeof(tbl) +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_5") +{ + ScopedFastFlag luauInplaceDemoteSkipAllBound{"LuauInplaceDemoteSkipAllBound", true}; + + fileResolver.source["game/A"] = R"( +export type Type = {x: number, y: number} +local arrayops = {} +function arrayops.foo(x: Type) end +return arrayops + )"; + + CheckResult result = check(R"( +local arrayops = require(game.A) + +local tbl = {} +tbl.a = 2 +function tbl:foo(b: number, c: number) + -- introduce boundTo TableTypeVar to imported type + self.x.a = 2 + arrayops.foo(self.x) +end +-- this alias decreases function type level and causes a demotion of its type type Table = typeof(tbl) )"); @@ -398,8 +427,6 @@ caused by: TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types") { - ScopedFastFlag luauAnyificationMustClone{"LuauAnyificationMustClone", true}; - fileResolver.source["game/A"] = R"( return function(...) end )"; diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index 01923f38..9a917a6f 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -485,7 +485,6 @@ TEST_CASE_FIXTURE(Fixture, "constrained_is_level_dependent") { ScopedFastFlag sff[]{ {"LuauLowerBoundsCalculation", true}, - {"LuauQuantifyConstrained", true}, }; CheckResult result = check(R"( diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 8a1cadcf..e61e6e45 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -41,6 +41,7 @@ struct RefinementClassFixture : Fixture RefinementClassFixture() { TypeArena& arena = typeChecker.globalTypes; + NotNull scope{typeChecker.globalScope.get()}; unfreeze(arena); TypeId vec3 = arena.addType(ClassTypeVar{"Vector3", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"}); @@ -49,7 +50,7 @@ struct RefinementClassFixture : Fixture {"Y", Property{typeChecker.numberType}}, {"Z", Property{typeChecker.numberType}}, }; - normalize(vec3, arena, *typeChecker.iceHandler); + normalize(vec3, scope, arena, *typeChecker.iceHandler); TypeId inst = arena.addType(ClassTypeVar{"Instance", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"}); @@ -57,21 +58,21 @@ struct RefinementClassFixture : Fixture TypePackId isARets = arena.addTypePack({typeChecker.booleanType}); TypeId isA = arena.addType(FunctionTypeVar{isAParams, isARets}); getMutable(isA)->magicFunction = magicFunctionInstanceIsA; - normalize(isA, arena, *typeChecker.iceHandler); + normalize(isA, scope, arena, *typeChecker.iceHandler); getMutable(inst)->props = { {"Name", Property{typeChecker.stringType}}, {"IsA", Property{isA}}, }; - normalize(inst, arena, *typeChecker.iceHandler); + normalize(inst, scope, arena, *typeChecker.iceHandler); TypeId folder = typeChecker.globalTypes.addType(ClassTypeVar{"Folder", {}, inst, std::nullopt, {}, nullptr, "Test"}); - normalize(folder, arena, *typeChecker.iceHandler); + normalize(folder, scope, arena, *typeChecker.iceHandler); TypeId part = typeChecker.globalTypes.addType(ClassTypeVar{"Part", {}, inst, std::nullopt, {}, nullptr, "Test"}); getMutable(part)->props = { {"Position", Property{vec3}}, }; - normalize(part, arena, *typeChecker.iceHandler); + normalize(part, scope, arena, *typeChecker.iceHandler); typeChecker.globalScope->exportedTypeBindings["Vector3"] = TypeFun{{}, vec3}; typeChecker.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, inst}; @@ -934,8 +935,6 @@ TEST_CASE_FIXTURE(Fixture, "apply_refinements_on_astexprindexexpr_whose_subscrip TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x") { - ScopedFastFlag sff{"LuauFalsyPredicateReturnsNilInstead", true}; - CheckResult result = check(R"( type T = {tag: "missing", x: nil} | {tag: "exists", x: string} @@ -1230,8 +1229,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknowns") TEST_CASE_FIXTURE(BuiltinsFixture, "falsiness_of_TruthyPredicate_narrows_into_nil") { - ScopedFastFlag sff{"LuauFalsyPredicateReturnsNilInstead", true}; - CheckResult result = check(R"( local function f(t: {number}) local x = t[1] diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index d9bfc89d..95b85c48 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -1847,6 +1847,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "quantifying_a_bound_var_works") TypeId ty = requireType("clazz"); TableTypeVar* ttv = getMutable(ty); REQUIRE(ttv); + REQUIRE(ttv->props.count("new")); Property& prop = ttv->props["new"]; REQUIRE(prop.type); const FunctionTypeVar* ftv = get(follow(prop.type)); @@ -2516,6 +2517,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "dont_quantify_table_that_belongs_to_outer_sc TableTypeVar* counterType = getMutable(requireType("Counter")); REQUIRE(counterType); + REQUIRE(counterType->props.count("new")); const FunctionTypeVar* newType = get(follow(counterType->props["new"].type)); REQUIRE(newType); @@ -3001,8 +3003,6 @@ TEST_CASE_FIXTURE(Fixture, "expected_indexer_from_table_union") TEST_CASE_FIXTURE(Fixture, "prop_access_on_key_whose_types_mismatches") { - ScopedFastFlag sff{"LuauReportErrorsOnIndexerKeyMismatch", true}; - CheckResult result = check(R"( local t: {number} = {} local x = t.x @@ -3014,8 +3014,6 @@ TEST_CASE_FIXTURE(Fixture, "prop_access_on_key_whose_types_mismatches") TEST_CASE_FIXTURE(Fixture, "prop_access_on_unions_of_indexers_where_key_whose_types_mismatches") { - ScopedFastFlag sff{"LuauReportErrorsOnIndexerKeyMismatch", true}; - CheckResult result = check(R"( local t: { [number]: number } | { [boolean]: number } = {} local u = t.x @@ -3081,8 +3079,6 @@ TEST_CASE_FIXTURE(Fixture, "quantify_even_that_table_was_never_exported_at_all") TEST_CASE_FIXTURE(BuiltinsFixture, "leaking_bad_metatable_errors") { - ScopedFastFlag luauIndexSilenceErrors{"LuauIndexSilenceErrors", true}; - CheckResult result = check(R"( local a = setmetatable({}, 1) local b = a.x diff --git a/tests/TypeInfer.tryUnify.test.cpp b/tests/TypeInfer.tryUnify.test.cpp index e0a0e5b5..02fdfd73 100644 --- a/tests/TypeInfer.tryUnify.test.cpp +++ b/tests/TypeInfer.tryUnify.test.cpp @@ -17,7 +17,8 @@ struct TryUnifyFixture : Fixture ScopePtr globalScope{new Scope{arena.addTypePack({TypeId{}})}}; InternalErrorReporter iceHandler; UnifierSharedState unifierState{&iceHandler}; - Unifier state{&arena, Mode::Strict, Location{}, Variance::Covariant, unifierState}; + + Unifier state{&arena, Mode::Strict, NotNull{globalScope.get()}, Location{}, Variance::Covariant, unifierState}; }; TEST_SUITE_BEGIN("TryUnifyTests"); diff --git a/tests/TypeVar.test.cpp b/tests/TypeVar.test.cpp index f4670048..32be8215 100644 --- a/tests/TypeVar.test.cpp +++ b/tests/TypeVar.test.cpp @@ -182,6 +182,22 @@ TEST_CASE_FIXTURE(Fixture, "UnionTypeVarIterator_with_empty_union") CHECK(actual.empty()); } +TEST_CASE_FIXTURE(Fixture, "UnionTypeVarIterator_with_only_cyclic_union") +{ + TypeVar tv{UnionTypeVar{}}; + auto utv = getMutable(&tv); + utv->options.push_back(&tv); + utv->options.push_back(&tv); + + std::vector actual(begin(utv), end(utv)); + CHECK(actual.empty()); +} + + +/* FIXME: This test is pretty weird. It would be much nicer if we could + * perform this operation without a TypeChecker so that we don't have to jam + * all this state into it to make stuff work. + */ TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure") { TypeVar ftv11{FreeTypeVar{TypeLevel{}}}; @@ -257,6 +273,7 @@ TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure") TypeId root = &ttvTweenResult; typeChecker.currentModule = std::make_shared(); + typeChecker.currentModule->scopes.emplace_back(Location{}, std::make_shared(getSingletonTypes().anyTypePack)); TypeId result = typeChecker.anyify(typeChecker.globalScope, root, Location{}); diff --git a/tests/conformance/basic.lua b/tests/conformance/basic.lua index b2dcaf94..d9274751 100644 --- a/tests/conformance/basic.lua +++ b/tests/conformance/basic.lua @@ -49,6 +49,12 @@ assert((function() _G.foo = 1 return _G['foo'] end)() == 1) assert((function() _G['bar'] = 1 return _G.bar end)() == 1) assert((function() local a = 1 (function () a = 2 end)() return a end)() == 2) +-- assignments with local conflicts +assert((function() local a, b = 1, {} a, b[a] = 43, -1 return a + b[1] end)() == 42) +assert((function() local a = {} local b = a a[1], a = 43, -1 return a + b[1] end)() == 42) +assert((function() local a, b = 1, {} a, b[a] = (function() return 43, -1 end)() return a + b[1] end)() == 42) +assert((function() local a = {} local b = a a[1], a = (function() return 43, -1 end)() return a + b[1] end)() == 42) + -- upvalues assert((function() local a = 1 function foo() return a end return foo() end)() == 1) diff --git a/tests/conformance/bitwise.lua b/tests/conformance/bitwise.lua index 13be3f94..f0c5698d 100644 --- a/tests/conformance/bitwise.lua +++ b/tests/conformance/bitwise.lua @@ -100,6 +100,9 @@ assert(bit32.extract(0xa0001111, 28, 4) == 0xa) assert(bit32.extract(0xa0001111, 31, 1) == 1) assert(bit32.extract(0x50000111, 31, 1) == 0) assert(bit32.extract(0xf2345679, 0, 32) == 0xf2345679) +assert(bit32.extract(0xa0001111, 16) == 0) +assert(bit32.extract(0xa0001111, 31) == 1) +assert(bit32.extract(42, 1, 3) == 5) assert(not pcall(bit32.extract, 0, -1)) assert(not pcall(bit32.extract, 0, 32)) @@ -152,5 +155,6 @@ assert(bit32.btest(1, "3") == true) assert(bit32.btest("1", 3) == true) assert(bit32.countlz("42") == 26) assert(bit32.countrz("42") == 1) +assert(bit32.extract("42", 1, 3) == 5) return('OK') diff --git a/tests/conformance/errors.lua b/tests/conformance/errors.lua index b13e7a82..b69d437b 100644 --- a/tests/conformance/errors.lua +++ b/tests/conformance/errors.lua @@ -295,7 +295,7 @@ end -- testing syntax limits -local syntaxdepth = if limitedstack then 200 else 1000 +local syntaxdepth = if limitedstack then 200 else 500 local function testrep (init, rep) local s = "local a; "..init .. string.rep(rep, syntaxdepth) local a,b = loadstring(s) diff --git a/tests/conformance/strings.lua b/tests/conformance/strings.lua index c87cf15c..3d8fdd1f 100644 --- a/tests/conformance/strings.lua +++ b/tests/conformance/strings.lua @@ -145,6 +145,14 @@ end) == false) assert(string.format("%*", "a\0b\0c") == "a\0b\0c") assert(string.format("%*", string.rep("doge", 3000)) == string.rep("doge", 3000)) +assert(string.format("%*", 42) == "42") +assert(string.format("%*", true) == "true") + +assert(string.format("%*", setmetatable({}, { __tostring = function() return "ok" end })) == "ok") + +local ud = newproxy(true) +getmetatable(ud).__tostring = function() return "good" end +assert(string.format("%*", ud) == "good") assert(pcall(function() string.format("%#*", "bad form") diff --git a/tools/faillist.txt b/tools/faillist.txt index 6e93345b..630bf9f7 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -1,13 +1,8 @@ -AnnotationTests.as_expr_does_not_propagate_type_info -AnnotationTests.as_expr_is_bidirectional -AnnotationTests.as_expr_warns_on_unrelated_cast AnnotationTests.builtin_types_are_not_exported AnnotationTests.cannot_use_nonexported_type AnnotationTests.cloned_interface_maintains_pointers_between_definitions -AnnotationTests.define_generic_type_alias AnnotationTests.duplicate_type_param_name AnnotationTests.for_loop_counter_annotation_is_checked -AnnotationTests.function_return_annotations_are_checked AnnotationTests.generic_aliases_are_cloned_properly AnnotationTests.interface_types_belong_to_interface_arena AnnotationTests.luau_ice_triggers_an_ice @@ -15,41 +10,28 @@ AnnotationTests.luau_ice_triggers_an_ice_exception_with_flag AnnotationTests.luau_ice_triggers_an_ice_exception_with_flag_handler AnnotationTests.luau_ice_triggers_an_ice_handler AnnotationTests.luau_print_is_magic_if_the_flag_is_set -AnnotationTests.luau_print_is_not_special_without_the_flag AnnotationTests.occurs_check_on_cyclic_intersection_typevar AnnotationTests.occurs_check_on_cyclic_union_typevar -AnnotationTests.self_referential_type_alias AnnotationTests.too_many_type_params AnnotationTests.two_type_params -AnnotationTests.type_annotations_inside_function_bodies -AnnotationTests.type_assertion_expr -AnnotationTests.unknown_type_reference_generates_error AnnotationTests.use_type_required_from_another_file AstQuery.last_argument_function_call_type -AstQuery::getDocumentationSymbolAtPosition.binding -AstQuery::getDocumentationSymbolAtPosition.event_callback_arg AstQuery::getDocumentationSymbolAtPosition.overloaded_fn -AstQuery::getDocumentationSymbolAtPosition.prop AutocompleteTest.argument_types AutocompleteTest.arguments_to_global_lambda -AutocompleteTest.as_types AutocompleteTest.autocomplete_boolean_singleton AutocompleteTest.autocomplete_end_with_fn_exprs AutocompleteTest.autocomplete_end_with_lambda AutocompleteTest.autocomplete_first_function_arg_expected_type AutocompleteTest.autocomplete_for_in_middle_keywords AutocompleteTest.autocomplete_for_middle_keywords -AutocompleteTest.autocomplete_if_else_regression AutocompleteTest.autocomplete_if_middle_keywords -AutocompleteTest.autocomplete_ifelse_expressions AutocompleteTest.autocomplete_on_string_singletons AutocompleteTest.autocomplete_oop_implicit_self AutocompleteTest.autocomplete_repeat_middle_keyword AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.autocomplete_string_singleton_escape AutocompleteTest.autocomplete_string_singletons -AutocompleteTest.autocomplete_until_expression -AutocompleteTest.autocomplete_until_in_repeat AutocompleteTest.autocomplete_while_middle_keywords AutocompleteTest.autocompleteProp_index_function_metamethod_is_variadic AutocompleteTest.bias_toward_inner_scope @@ -72,7 +54,6 @@ AutocompleteTest.get_suggestions_for_the_very_start_of_the_script AutocompleteTest.global_function_params AutocompleteTest.global_functions_are_not_scoped_lexically AutocompleteTest.if_then_else_elseif_completions -AutocompleteTest.if_then_else_full_keywords AutocompleteTest.keyword_methods AutocompleteTest.keyword_types AutocompleteTest.library_non_self_calls_are_fine @@ -127,7 +108,6 @@ BuiltinTests.assert_removes_falsy_types2 BuiltinTests.assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type BuiltinTests.assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy BuiltinTests.bad_select_should_not_crash -BuiltinTests.builtin_tables_sealed BuiltinTests.coroutine_resume_anything_goes BuiltinTests.coroutine_wrap_anything_goes BuiltinTests.debug_info_is_crazy @@ -136,28 +116,20 @@ BuiltinTests.dont_add_definitions_to_persistent_types BuiltinTests.find_capture_types BuiltinTests.find_capture_types2 BuiltinTests.find_capture_types3 -BuiltinTests.gcinfo BuiltinTests.getfenv BuiltinTests.global_singleton_types_are_sealed BuiltinTests.gmatch_capture_types BuiltinTests.gmatch_capture_types2 BuiltinTests.gmatch_capture_types_balanced_escaped_parens BuiltinTests.gmatch_capture_types_default_capture -BuiltinTests.gmatch_capture_types_invalid_pattern_fallback_to_builtin -BuiltinTests.gmatch_capture_types_invalid_pattern_fallback_to_builtin2 -BuiltinTests.gmatch_capture_types_leading_end_bracket_is_part_of_set BuiltinTests.gmatch_capture_types_parens_in_sets_are_ignored BuiltinTests.gmatch_capture_types_set_containing_lbracket BuiltinTests.gmatch_definition BuiltinTests.ipairs_iterator_should_infer_types_and_type_check -BuiltinTests.lua_51_exported_globals_all_exist BuiltinTests.match_capture_types BuiltinTests.match_capture_types2 BuiltinTests.math_max_checks_for_numbers -BuiltinTests.math_max_variatic -BuiltinTests.math_things_are_defined BuiltinTests.next_iterator_should_infer_types_and_type_check -BuiltinTests.no_persistent_typelevel_change BuiltinTests.os_time_takes_optional_date_table BuiltinTests.pairs_iterator_should_infer_types_and_type_check BuiltinTests.see_thru_select @@ -170,7 +142,6 @@ BuiltinTests.select_with_variadic_typepack_tail BuiltinTests.select_with_variadic_typepack_tail_and_string_head BuiltinTests.set_metatable_needs_arguments BuiltinTests.setmetatable_should_not_mutate_persisted_types -BuiltinTests.setmetatable_unpacks_arg_types_correctly BuiltinTests.sort BuiltinTests.sort_with_bad_predicate BuiltinTests.sort_with_predicate @@ -179,6 +150,8 @@ BuiltinTests.string_format_arg_types_inference BuiltinTests.string_format_as_method BuiltinTests.string_format_correctly_ordered_types BuiltinTests.string_format_report_all_type_errors_at_correct_positions +BuiltinTests.string_format_tostring_specifier +BuiltinTests.string_format_tostring_specifier_type_constraint BuiltinTests.string_format_use_correct_argument BuiltinTests.string_format_use_correct_argument2 BuiltinTests.string_lib_self_noself @@ -190,53 +163,36 @@ BuiltinTests.table_insert_correctly_infers_type_of_array_3_args_overload BuiltinTests.table_pack BuiltinTests.table_pack_reduce BuiltinTests.table_pack_variadic -BuiltinTests.thread_is_a_type BuiltinTests.tonumber_returns_optional_number_type BuiltinTests.tonumber_returns_optional_number_type2 -BuiltinTests.xpcall -DefinitionTests.class_definition_function_prop DefinitionTests.declaring_generic_functions -DefinitionTests.definition_file_class_function_args DefinitionTests.definition_file_classes DefinitionTests.definition_file_loading +DefinitionTests.definitions_documentation_symbols +DefinitionTests.documentation_symbols_dont_attach_to_persistent_types DefinitionTests.single_class_type_identity_in_global_types -FrontendTest.accumulate_cached_errors -FrontendTest.accumulate_cached_errors_in_consistent_order -FrontendTest.any_annotation_breaks_cycle FrontendTest.ast_node_at_position -FrontendTest.automatically_check_cyclically_dependent_scripts FrontendTest.automatically_check_dependent_scripts FrontendTest.check_without_builtin_next -FrontendTest.clearStats -FrontendTest.cycle_detection_between_check_and_nocheck -FrontendTest.cycle_detection_disabled_in_nocheck -FrontendTest.cycle_error_paths -FrontendTest.cycle_errors_can_be_fixed -FrontendTest.cycle_incremental_type_surface -FrontendTest.cycle_incremental_type_surface_longer -FrontendTest.dont_recheck_script_that_hasnt_been_marked_dirty FrontendTest.dont_reparse_clean_file_when_linting FrontendTest.environments -FrontendTest.ignore_require_to_nonexistent_file FrontendTest.imported_table_modification_2 FrontendTest.it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded FrontendTest.no_use_after_free_with_type_fun_instantiation FrontendTest.nocheck_cycle_used_by_checked FrontendTest.nocheck_modules_are_typed FrontendTest.produce_errors_for_unchanged_file_with_a_syntax_error -FrontendTest.re_report_type_error_in_required_file FrontendTest.recheck_if_dependent_script_is_dirty FrontendTest.reexport_cyclic_type FrontendTest.reexport_type_alias FrontendTest.report_require_to_nonexistent_file FrontendTest.report_syntax_error_in_required_file -FrontendTest.reports_errors_from_multiple_sources -FrontendTest.stats_are_not_reset_between_checks FrontendTest.trace_requires_in_nonstrict_mode GenericsTests.apply_type_function_nested_generics1 GenericsTests.apply_type_function_nested_generics2 GenericsTests.better_mismatch_error_messages GenericsTests.bound_tables_do_not_clone_original_fields +GenericsTests.calling_self_generic_methods GenericsTests.check_generic_typepack_function GenericsTests.check_mutual_generic_functions GenericsTests.correctly_instantiate_polymorphic_member_functions @@ -248,8 +204,6 @@ GenericsTests.duplicate_generic_types GenericsTests.error_detailed_function_mismatch_generic_pack GenericsTests.error_detailed_function_mismatch_generic_types GenericsTests.factories_of_generics -GenericsTests.function_arguments_can_be_polytypes -GenericsTests.function_results_can_be_polytypes GenericsTests.generic_argument_count_too_few GenericsTests.generic_argument_count_too_many GenericsTests.generic_factories @@ -265,8 +219,7 @@ GenericsTests.generic_type_pack_unification3 GenericsTests.infer_generic_function_function_argument GenericsTests.infer_generic_function_function_argument_overloaded GenericsTests.infer_generic_lib_function_function_argument -GenericsTests.infer_generic_property -GenericsTests.inferred_local_vars_can_be_polytypes +GenericsTests.infer_generic_methods GenericsTests.instantiate_cyclic_generic_function GenericsTests.instantiate_generic_function_in_assignments GenericsTests.instantiate_generic_function_in_assignments2 @@ -276,41 +229,32 @@ GenericsTests.local_vars_can_be_instantiated_polytypes GenericsTests.mutable_state_polymorphism GenericsTests.no_stack_overflow_from_quantifying GenericsTests.properties_can_be_instantiated_polytypes -GenericsTests.properties_can_be_polytypes GenericsTests.rank_N_types_via_typeof GenericsTests.reject_clashing_generic_and_pack_names GenericsTests.self_recursive_instantiated_param -GenericsTests.variadic_generics IntersectionTypes.argument_is_intersection IntersectionTypes.error_detailed_intersection_all IntersectionTypes.error_detailed_intersection_part IntersectionTypes.fx_intersection_as_argument IntersectionTypes.fx_union_as_argument_fails IntersectionTypes.index_on_an_intersection_type_with_mixed_types -IntersectionTypes.index_on_an_intersection_type_with_one_part_missing_the_property -IntersectionTypes.index_on_an_intersection_type_with_one_property_of_type_any IntersectionTypes.index_on_an_intersection_type_with_property_guaranteed_to_exist IntersectionTypes.index_on_an_intersection_type_works_at_arbitrary_depth IntersectionTypes.no_stack_overflow_from_flattenintersection IntersectionTypes.overload_is_not_a_function IntersectionTypes.select_correct_union_fn IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions -IntersectionTypes.table_intersection_setmetatable IntersectionTypes.table_intersection_write IntersectionTypes.table_intersection_write_sealed IntersectionTypes.table_intersection_write_sealed_indirect IntersectionTypes.table_write_sealed_indirect -isSubtype.functions_and_any -isSubtype.intersection_of_functions_of_different_arities isSubtype.intersection_of_tables -isSubtype.table_with_any_prop isSubtype.table_with_table_prop -isSubtype.tables -Linter.DeprecatedApi Linter.TableOperations -ModuleTests.builtin_types_point_into_globalTypes_arena ModuleTests.clone_self_property ModuleTests.deepClone_cyclic_table +ModuleTests.do_not_clone_reexports +ModuleTests.do_not_clone_types_of_reexported_values NonstrictModeTests.delay_function_does_not_require_its_argument_to_return_anything NonstrictModeTests.for_in_iterator_variables_are_any NonstrictModeTests.function_parameters_are_any @@ -333,8 +277,6 @@ Normalize.cyclic_intersection Normalize.cyclic_table_normalizes_sensibly Normalize.cyclic_union Normalize.fuzz_failure_bound_type_is_normal_but_not_its_bounded_to -Normalize.fuzz_failure_instersection_combine_must_follow -Normalize.higher_order_function Normalize.intersection_combine_on_bound_self Normalize.intersection_inside_a_table_inside_another_intersection Normalize.intersection_inside_a_table_inside_another_intersection_2 @@ -345,13 +287,10 @@ Normalize.intersection_of_disjoint_tables Normalize.intersection_of_functions Normalize.intersection_of_overlapping_tables Normalize.intersection_of_tables_with_indexers -Normalize.nested_table_normalization_with_non_table__no_ice Normalize.normalization_does_not_convert_ever Normalize.normalize_module_return_type Normalize.normalize_unions_containing_never Normalize.normalize_unions_containing_unknown -Normalize.return_type_is_not_a_constrained_intersection -Normalize.skip_force_normal_on_external_types Normalize.union_of_distinct_free_types Normalize.variadic_tail_is_marked_normal Normalize.visiting_a_type_twice_is_not_considered_normal @@ -365,7 +304,6 @@ ProvisionalTests.constrained_is_level_dependent ProvisionalTests.discriminate_from_x_not_equal_to_nil ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean -ProvisionalTests.free_is_not_bound_to_any ProvisionalTests.function_returns_many_things_but_first_of_it_is_forgotten ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns ProvisionalTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound @@ -380,7 +318,6 @@ ProvisionalTests.typeguard_inference_incomplete ProvisionalTests.weird_fail_to_unify_type_pack ProvisionalTests.weirditer_should_not_loop_forever ProvisionalTests.while_body_are_also_refined -ProvisionalTests.xpcall_returns_what_f_returns RefinementTest.and_constraint RefinementTest.and_or_peephole_refinement RefinementTest.apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string @@ -420,7 +357,6 @@ RefinementTest.not_and_constraint RefinementTest.not_t_or_some_prop_of_t RefinementTest.or_predicate_with_truthy_predicates RefinementTest.parenthesized_expressions_are_followed_through -RefinementTest.refine_a_property_not_to_be_nil_through_an_intersection_table RefinementTest.refine_the_correct_types_opposite_of_when_a_is_not_number_or_string RefinementTest.refine_unknowns RefinementTest.string_not_equal_to_string_or_nil @@ -456,7 +392,6 @@ TableTests.augment_nested_table TableTests.augment_table TableTests.builtin_table_names TableTests.call_method -TableTests.call_method_with_explicit_self_argument TableTests.cannot_augment_sealed_table TableTests.cannot_call_tables TableTests.cannot_change_type_of_unsealed_table_prop @@ -469,16 +404,13 @@ TableTests.common_table_element_union_in_call_tail TableTests.confusing_indexing TableTests.defining_a_method_for_a_builtin_sealed_table_must_fail TableTests.defining_a_method_for_a_local_sealed_table_must_fail -TableTests.defining_a_method_for_a_local_unsealed_table_is_ok TableTests.defining_a_self_method_for_a_builtin_sealed_table_must_fail TableTests.defining_a_self_method_for_a_local_sealed_table_must_fail -TableTests.defining_a_self_method_for_a_local_unsealed_table_is_ok TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index TableTests.dont_invalidate_the_properties_iterator_of_free_table_when_rolled_back TableTests.dont_leak_free_table_props TableTests.dont_quantify_table_that_belongs_to_outer_scope -TableTests.dont_seal_an_unsealed_table_by_passing_it_to_a_function_that_takes_a_sealed_table TableTests.dont_suggest_exact_match_keys TableTests.error_detailed_indexer_key TableTests.error_detailed_indexer_value @@ -508,24 +440,19 @@ TableTests.infer_array_2 TableTests.infer_indexer_from_value_property_in_literal TableTests.inferred_return_type_of_free_table TableTests.inferring_crazy_table_should_also_be_quick -TableTests.instantiate_table_cloning -TableTests.instantiate_table_cloning_2 TableTests.instantiate_table_cloning_3 TableTests.instantiate_tables_at_scope_level -TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound TableTests.leaking_bad_metatable_errors TableTests.length_operator_intersection TableTests.length_operator_non_table_union TableTests.length_operator_union TableTests.length_operator_union_errors -TableTests.less_exponential_blowup_please TableTests.meta_add TableTests.meta_add_both_ways TableTests.meta_add_inferred TableTests.metatable_mismatch_should_fail TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred TableTests.mixed_tables_with_implicit_numbered_keys -TableTests.MixedPropertiesAndIndexers TableTests.nil_assign_doesnt_hit_indexer TableTests.okay_to_add_property_to_unsealed_tables_by_function_call TableTests.only_ascribe_synthetic_names_at_module_scope @@ -535,8 +462,8 @@ TableTests.open_table_unification_2 TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2 TableTests.pass_incompatible_union_to_a_generic_table_without_crashing -TableTests.passing_compatible_unions_to_a_generic_table_without_crashing TableTests.persistent_sealed_table_is_immutable +TableTests.prop_access_on_key_whose_types_mismatches TableTests.property_lookup_through_tabletypevar_metatable TableTests.quantify_even_that_table_was_never_exported_at_all TableTests.quantify_metatables_of_metatables_of_table @@ -570,23 +497,16 @@ TableTests.tc_member_function_2 TableTests.top_table_type TableTests.type_mismatch_on_massive_table_is_cut_short TableTests.unification_of_unions_in_a_self_referential_type -TableTests.unifying_tables_shouldnt_uaf1 TableTests.unifying_tables_shouldnt_uaf2 -TableTests.used_colon_correctly TableTests.used_colon_instead_of_dot TableTests.used_dot_instead_of_colon -TableTests.used_dot_instead_of_colon_but_correctly TableTests.width_subtyping ToDot.bound_table -ToDot.class ToDot.function ToDot.metatable -ToDot.primitive ToDot.table ToString.exhaustive_toString_of_cyclic_table -ToString.function_type_with_argument_names_and_self ToString.function_type_with_argument_names_generic -ToString.named_metatable_toStringNamedFunction ToString.no_parentheses_around_cyclic_function_type_in_union ToString.toStringDetailed2 ToString.toStringErrorPack @@ -605,13 +525,12 @@ TryUnifyTests.typepack_unification_should_trim_free_tails TryUnifyTests.variadics_should_use_reversed_properly TypeAliases.cli_38393_recursive_intersection_oom TypeAliases.corecursive_types_generic -TypeAliases.do_not_quantify_unresolved_aliases TypeAliases.forward_declared_alias_is_not_clobbered_by_prior_unification_with_any TypeAliases.general_require_multi_assign TypeAliases.generic_param_remap TypeAliases.mismatched_generic_pack_type_param TypeAliases.mismatched_generic_type_param -TypeAliases.mutually_recursive_generic_aliases +TypeAliases.mutually_recursive_types_errors TypeAliases.mutually_recursive_types_restriction_not_ok_1 TypeAliases.mutually_recursive_types_restriction_not_ok_2 TypeAliases.mutually_recursive_types_swapsies_not_ok @@ -619,29 +538,19 @@ TypeAliases.recursive_types_restriction_not_ok TypeAliases.stringify_optional_parameterized_alias TypeAliases.stringify_type_alias_of_recursive_template_table_type TypeAliases.stringify_type_alias_of_recursive_template_table_type2 -TypeAliases.type_alias_import_mutation +TypeAliases.type_alias_fwd_declaration_is_precise TypeAliases.type_alias_local_mutation TypeAliases.type_alias_local_rename TypeAliases.type_alias_of_an_imported_recursive_generic_type TypeAliases.type_alias_of_an_imported_recursive_type -TypeInfer.check_expr_recursion_limit TypeInfer.checking_should_not_ice -TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error TypeInfer.cyclic_follow TypeInfer.do_not_bind_a_free_table_to_a_union_containing_that_table TypeInfer.dont_report_type_errors_within_an_AstStatError -TypeInfer.follow_on_new_types_in_substitution -TypeInfer.free_typevars_introduced_within_control_flow_constructs_do_not_get_an_elevated_TypeLevel TypeInfer.globals TypeInfer.globals2 -TypeInfer.index_expr_should_be_checked -TypeInfer.infer_assignment_value_types TypeInfer.infer_assignment_value_types_mutable_lval -TypeInfer.infer_through_group_expr -TypeInfer.infer_type_assertion_value_type -TypeInfer.no_heap_use_after_free_error TypeInfer.no_stack_overflow_from_isoptional -TypeInfer.recursive_metatable_crash TypeInfer.tc_after_error_recovery_no_replacement_name_in_error TypeInfer.tc_if_else_expressions1 TypeInfer.tc_if_else_expressions2 @@ -650,82 +559,50 @@ TypeInfer.tc_if_else_expressions_expected_type_2 TypeInfer.tc_if_else_expressions_expected_type_3 TypeInfer.tc_if_else_expressions_type_union TypeInfer.type_infer_recursion_limit_no_ice -TypeInfer.types stored in astResolvedTypes TypeInfer.warn_on_lowercase_parent_property -TypeInfer.weird_case -TypeInferAnyError.any_type_propagates TypeInferAnyError.assign_prop_to_table_by_calling_any_yields_any -TypeInferAnyError.call_to_any_yields_any -TypeInferAnyError.calling_error_type_yields_error TypeInferAnyError.can_get_length_of_any -TypeInferAnyError.can_subscript_any -TypeInferAnyError.CheckMethodsOfAny TypeInferAnyError.for_in_loop_iterator_is_any TypeInferAnyError.for_in_loop_iterator_is_any2 TypeInferAnyError.for_in_loop_iterator_is_error TypeInferAnyError.for_in_loop_iterator_is_error2 TypeInferAnyError.for_in_loop_iterator_returns_any TypeInferAnyError.for_in_loop_iterator_returns_any2 -TypeInferAnyError.indexing_error_type_does_not_produce_an_error TypeInferAnyError.length_of_error_type_does_not_produce_an_error -TypeInferAnyError.metatable_of_any_can_be_a_table -TypeInferAnyError.prop_access_on_any_with_other_options -TypeInferAnyError.quantify_any_does_not_bind_to_itself TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any -TypeInferAnyError.type_error_addition -TypeInferClasses.assign_to_prop_of_class TypeInferClasses.call_base_method TypeInferClasses.call_instance_method -TypeInferClasses.call_method_of_a_child_class -TypeInferClasses.call_method_of_a_class -TypeInferClasses.can_assign_to_prop_of_base_class -TypeInferClasses.can_assign_to_prop_of_base_class_using_string -TypeInferClasses.can_read_prop_of_base_class -TypeInferClasses.can_read_prop_of_base_class_using_string -TypeInferClasses.cannot_call_method_of_child_on_base_instance -TypeInferClasses.cannot_call_unknown_method_of_a_class -TypeInferClasses.cannot_unify_class_instance_with_primitive TypeInferClasses.class_type_mismatch_with_name_conflict -TypeInferClasses.class_unification_type_mismatch_is_correct_order TypeInferClasses.classes_can_have_overloaded_operators TypeInferClasses.classes_without_overloaded_operators_cannot_be_added TypeInferClasses.detailed_class_unification_error TypeInferClasses.function_arguments_are_covariant -TypeInferClasses.higher_order_function_arguments_are_contravariant TypeInferClasses.higher_order_function_return_type_is_not_contravariant TypeInferClasses.higher_order_function_return_values_are_covariant TypeInferClasses.optional_class_field_access_error TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties -TypeInferClasses.table_indexers_are_invariant -TypeInferClasses.table_properties_are_invariant TypeInferClasses.warn_when_prop_almost_matches -TypeInferClasses.we_can_infer_that_a_parameter_must_be_a_particular_class TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class TypeInferFunctions.another_indirect_function_case_where_it_is_ok_to_provide_too_many_arguments TypeInferFunctions.another_recursive_local_function TypeInferFunctions.calling_function_with_anytypepack_doesnt_leak_free_types TypeInferFunctions.calling_function_with_incorrect_argument_type_yields_errors_spanning_argument -TypeInferFunctions.cannot_hoist_interior_defns_into_signature -TypeInferFunctions.check_function_before_lambda_that_uses_it TypeInferFunctions.complicated_return_types_require_an_explicit_annotation TypeInferFunctions.cyclic_function_type_in_args TypeInferFunctions.dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists TypeInferFunctions.dont_infer_parameter_types_for_functions_from_their_call_site -TypeInferFunctions.dont_mutate_the_underlying_head_of_typepack_when_calling_with_self TypeInferFunctions.duplicate_functions_with_different_signatures_not_allowed_in_nonstrict TypeInferFunctions.error_detailed_function_mismatch_arg TypeInferFunctions.error_detailed_function_mismatch_arg_count TypeInferFunctions.error_detailed_function_mismatch_ret TypeInferFunctions.error_detailed_function_mismatch_ret_count TypeInferFunctions.error_detailed_function_mismatch_ret_mult -TypeInferFunctions.first_argument_can_be_optional TypeInferFunctions.free_is_not_bound_to_unknown TypeInferFunctions.func_expr_doesnt_leak_free TypeInferFunctions.function_cast_error_uses_correct_language TypeInferFunctions.function_decl_non_self_sealed_overwrite TypeInferFunctions.function_decl_non_self_sealed_overwrite_2 TypeInferFunctions.function_decl_non_self_unsealed_overwrite -TypeInferFunctions.function_decl_quantify_right_type TypeInferFunctions.function_does_not_return_enough_values TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer TypeInferFunctions.higher_order_function_2 @@ -734,16 +611,12 @@ TypeInferFunctions.ignored_return_values TypeInferFunctions.inconsistent_higher_order_function TypeInferFunctions.inconsistent_return_types TypeInferFunctions.infer_anonymous_function_arguments -TypeInferFunctions.infer_anonymous_function_arguments_outside_call TypeInferFunctions.infer_return_type_from_selected_overload TypeInferFunctions.infer_that_function_does_not_return_a_table -TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time -TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time2 TypeInferFunctions.it_is_ok_not_to_supply_enough_retvals TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count -TypeInferFunctions.mutual_recursion TypeInferFunctions.no_lossy_function_type TypeInferFunctions.occurs_check_failure_in_function_return_type TypeInferFunctions.quantify_constrained_types @@ -759,10 +632,8 @@ TypeInferFunctions.too_few_arguments_variadic_generic TypeInferFunctions.too_few_arguments_variadic_generic2 TypeInferFunctions.too_many_arguments TypeInferFunctions.too_many_return_values -TypeInferFunctions.toposort_doesnt_break_mutual_recursion TypeInferFunctions.vararg_function_is_quantified TypeInferFunctions.vararg_functions_should_allow_calls_of_any_types_and_size -TypeInferLoops.correctly_scope_locals_while TypeInferLoops.for_in_loop TypeInferLoops.for_in_loop_error_on_factory_not_returning_the_right_amount_of_values TypeInferLoops.for_in_loop_error_on_iterator_requiring_args_but_none_given @@ -784,19 +655,11 @@ TypeInferLoops.loop_iter_no_indexer_strict TypeInferLoops.loop_iter_trailing_nil TypeInferLoops.loop_typecheck_crash_on_empty_optional TypeInferLoops.properly_infer_iteratee_is_a_free_table -TypeInferLoops.repeat_loop -TypeInferLoops.repeat_loop_condition_binds_to_its_block -TypeInferLoops.symbols_in_repeat_block_should_not_be_visible_beyond_until_condition TypeInferLoops.unreachable_code_after_infinite_loop TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free -TypeInferLoops.while_loop -TypeInferModules.bound_free_table_export_is_ok -TypeInferModules.constrained_anyification_clone_immutable_types -TypeInferModules.custom_require_global TypeInferModules.do_not_modify_imported_types TypeInferModules.do_not_modify_imported_types_2 TypeInferModules.do_not_modify_imported_types_3 -TypeInferModules.do_not_modify_imported_types_4 TypeInferModules.general_require_call_expression TypeInferModules.general_require_type_mismatch TypeInferModules.module_type_conflict @@ -808,16 +671,14 @@ TypeInferModules.require_module_that_does_not_export TypeInferModules.require_types TypeInferModules.type_error_of_unknown_qualified_type TypeInferModules.warn_if_you_try_to_require_a_non_modulescript +TypeInferOOP.CheckMethodsOfSealed TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_another_overload_works TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2 TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon TypeInferOOP.inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory -TypeInferOOP.method_depends_on_table TypeInferOOP.methods_are_topologically_sorted TypeInferOOP.nonstrict_self_mismatch_tail -TypeInferOOP.object_constructor_can_refer_to_method_of_self -TypeInferOOP.table_oop TypeInferOperators.and_adds_boolean TypeInferOperators.and_adds_boolean_no_superfluous_union TypeInferOperators.and_binexps_dont_unify @@ -829,8 +690,6 @@ TypeInferOperators.cannot_indirectly_compare_types_that_do_not_offer_overloaded_ TypeInferOperators.cli_38355_recursive_union TypeInferOperators.compare_numbers TypeInferOperators.compare_strings -TypeInferOperators.compound_assign_basic -TypeInferOperators.compound_assign_metatable TypeInferOperators.compound_assign_mismatch_metatable TypeInferOperators.compound_assign_mismatch_op TypeInferOperators.compound_assign_mismatch_result @@ -872,37 +731,25 @@ TypeInferPrimitives.string_function_other TypeInferPrimitives.string_index TypeInferPrimitives.string_length TypeInferPrimitives.string_method -TypeInferUnknownNever.array_like_table_of_never_is_inhabitable TypeInferUnknownNever.assign_to_global_which_is_never TypeInferUnknownNever.assign_to_local_which_is_never TypeInferUnknownNever.assign_to_prop_which_is_never TypeInferUnknownNever.assign_to_subscript_which_is_never TypeInferUnknownNever.call_never TypeInferUnknownNever.dont_unify_operands_if_one_of_the_operand_is_never_in_any_ordering_operators -TypeInferUnknownNever.index_on_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never TypeInferUnknownNever.length_of_never TypeInferUnknownNever.math_operators_and_never -TypeInferUnknownNever.never_is_reflexive -TypeInferUnknownNever.never_subtype_and_string_supertype -TypeInferUnknownNever.pick_never_from_variadic_type_pack -TypeInferUnknownNever.string_subtype_and_never_supertype -TypeInferUnknownNever.string_subtype_and_unknown_supertype -TypeInferUnknownNever.table_with_prop_of_type_never_is_also_reflexive -TypeInferUnknownNever.table_with_prop_of_type_never_is_uninhabitable TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable2 TypeInferUnknownNever.unary_minus_of_never TypeInferUnknownNever.unknown_is_reflexive -TypeInferUnknownNever.unknown_subtype_and_string_supertype -TypePackTests.cyclic_type_packs TypePackTests.higher_order_function TypePackTests.multiple_varargs_inference_are_not_confused TypePackTests.no_return_size_should_be_zero TypePackTests.pack_tail_unification_check TypePackTests.parenthesized_varargs_returns_any -TypePackTests.self_and_varargs_should_work TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_default_export TypePackTests.type_alias_default_mixed_self @@ -913,8 +760,6 @@ TypePackTests.type_alias_default_type_pack_self_tp TypePackTests.type_alias_default_type_self TypePackTests.type_alias_defaults_confusing_types TypePackTests.type_alias_defaults_recursive_type -TypePackTests.type_alias_type_pack_explicit -TypePackTests.type_alias_type_pack_explicit_multi TypePackTests.type_alias_type_pack_multi TypePackTests.type_alias_type_pack_variadic TypePackTests.type_alias_type_packs @@ -949,6 +794,7 @@ TypeSingletons.string_singletons_escape_chars TypeSingletons.string_singletons_mismatch TypeSingletons.table_insert_with_a_singleton_argument TypeSingletons.table_properties_type_error_escapes +TypeSingletons.tagged_unions_immutable_tag TypeSingletons.tagged_unions_using_singletons TypeSingletons.taking_the_length_of_string_singleton TypeSingletons.taking_the_length_of_union_of_string_singleton @@ -978,5 +824,4 @@ UnionTypes.optional_union_members UnionTypes.optional_union_methods UnionTypes.return_types_can_be_disjoint UnionTypes.table_union_write_indirect -UnionTypes.unify_unsealed_table_union_check UnionTypes.union_equality_comparisons diff --git a/tools/heapgraph.py b/tools/heapgraph.py index d4d29af1..b8dc207f 100644 --- a/tools/heapgraph.py +++ b/tools/heapgraph.py @@ -39,6 +39,16 @@ class Node(svg.Node): def details(self, root): return "{} ({:,} bytes, {:.1%}); self: {:,} bytes in {:,} objects".format(self.name, self.width, self.width / root.width, self.size, self.count) +def getkey(heap, obj, key): + pairs = obj.get("pairs", []) + for i in range(0, len(pairs), 2): + if pairs[i] and heap[pairs[i]]["type"] == "string" and heap[pairs[i]]["data"] == key: + if pairs[i + 1] and heap[pairs[i + 1]]["type"] == "string": + return heap[pairs[i + 1]]["data"] + else: + return None + return None + # load files if arguments.snapshotnew == None: dumpold = None @@ -50,6 +60,8 @@ else: with open(arguments.snapshotnew) as f: dump = json.load(f) +heap = dump["objects"] + # reachability analysis: how much of the heap is reachable from roots? visited = set() queue = [] @@ -66,7 +78,7 @@ while offset < len(queue): continue visited.add(addr) - obj = dump["objects"][addr] + obj = heap[addr] if not dumpold or not addr in dumpold["objects"]: node.count += 1 @@ -75,17 +87,27 @@ while offset < len(queue): if obj["type"] == "table": pairs = obj.get("pairs", []) + weakkey = False + weakval = False + + if "metatable" in obj: + modemt = getkey(heap, heap[obj["metatable"]], "__mode") + if modemt: + weakkey = "k" in modemt + weakval = "v" in modemt for i in range(0, len(pairs), 2): key = pairs[i+0] val = pairs[i+1] - if key and val and dump["objects"][key]["type"] == "string": + if key and heap[key]["type"] == "string": + # string keys are always strong queue.append((key, node)) - queue.append((val, node.child(dump["objects"][key]["data"]))) + if val and not weakval: + queue.append((val, node.child(heap[key]["data"]))) else: - if key: + if key and not weakkey: queue.append((key, node)) - if val: + if val and not weakval: queue.append((val, node)) for a in obj.get("array", []): @@ -97,7 +119,7 @@ while offset < len(queue): source = "" if "proto" in obj: - proto = dump["objects"][obj["proto"]] + proto = heap[obj["proto"]] if "source" in proto: source = proto["source"] @@ -110,8 +132,16 @@ while offset < len(queue): queue.append((obj["metatable"], node.child("__meta"))) elif obj["type"] == "thread": queue.append((obj["env"], node.child("__env"))) - for a in obj.get("stack", []): - queue.append((a, node.child("__stack"))) + stack = obj.get("stack") + stacknames = obj.get("stacknames", []) + stacknode = node.child("__stack") + framenode = None + for i in range(len(stack)): + name = stacknames[i] if stacknames else None + if name and name.startswith("frame:"): + framenode = stacknode.child(name[6:]) + name = None + queue.append((stack[i], framenode.child(name) if framenode and name else framenode or stacknode)) elif obj["type"] == "proto": for a in obj.get("constants", []): queue.append((a, node)) diff --git a/tools/heapstat.py b/tools/heapstat.py index 838521b6..7337aa44 100644 --- a/tools/heapstat.py +++ b/tools/heapstat.py @@ -15,12 +15,20 @@ def updatesize(d, k, s): def sortedsize(p): return sorted(p, key = lambda s: s[1][1], reverse = True) +def getkey(heap, obj, key): + pairs = obj.get("pairs", []) + for i in range(0, len(pairs), 2): + if pairs[i] and heap[pairs[i]]["type"] == "string" and heap[pairs[i]]["data"] == key: + if pairs[i + 1] and heap[pairs[i + 1]]["type"] == "string": + return heap[pairs[i + 1]]["data"] + else: + return None + return None + with open(sys.argv[1]) as f: dump = json.load(f) heap = dump["objects"] -type_addr = next((addr for addr,obj in heap.items() if obj["type"] == "string" and obj["data"] == "__type"), None) - size_type = {} size_udata = {} size_category = {} @@ -33,11 +41,7 @@ for addr, obj in heap.items(): if obj["type"] == "userdata" and "metatable" in obj: metatable = heap[obj["metatable"]] - pairs = metatable.get("pairs", []) - typemt = "unknown" - for i in range(0, len(pairs), 2): - if type_addr and pairs[i] == type_addr and pairs[i + 1] and heap[pairs[i + 1]]["type"] == "string": - typemt = heap[pairs[i + 1]]["data"] + typemt = getkey(heap, metatable, "__type") or "unknown" updatesize(size_udata, typemt, obj["size"]) print("objects by type:") @@ -55,5 +59,6 @@ if len(size_category) != 0: print("objects by category:") for type, (count, size) in sortedsize(size_category.items()): - name = dump["stats"]["categories"][type]["name"] + cat = dump["stats"]["categories"][type] + name = cat["name"] if "name" in cat else str(type) print(name.ljust(30), str(size).rjust(8), "bytes", str(count).rjust(5), "objects") diff --git a/tools/perfgraph.py b/tools/perfgraph.py index ae74b7d6..25bb0dd9 100644 --- a/tools/perfgraph.py +++ b/tools/perfgraph.py @@ -56,18 +56,28 @@ def nodeFromCallstackListFile(source_file): return root +def getDuration(obj): + total = obj['TotalDuration'] -def nodeFromJSONbject(node, key, obj): + if 'Children' in obj: + for key, obj in obj['Children'].items(): + total -= obj['TotalDuration'] + + return total + + +def nodeFromJSONObject(node, key, obj): source, function, line = key.split(",") node.function = function node.source = source node.line = int(line) if len(line) > 0 else 0 - node.ticks = obj['Duration'] + node.ticks = getDuration(obj) - for key, obj in obj['Children'].items(): - nodeFromJSONbject(node.child(key), key, obj) + if 'Children' in obj: + for key, obj in obj['Children'].items(): + nodeFromJSONObject(node.child(key), key, obj) return node @@ -77,8 +87,9 @@ def nodeFromJSONFile(source_file): root = Node() - for key, obj in dump['Children'].items(): - nodeFromJSONbject(root.child(key), key, obj) + if 'Children' in dump: + for key, obj in dump['Children'].items(): + nodeFromJSONObject(root.child(key), key, obj) return root