diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGenerator.h similarity index 94% rename from Analysis/include/Luau/ConstraintGraphBuilder.h rename to Analysis/include/Luau/ConstraintGenerator.h index bba5ebd9..aab31c40 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -57,7 +57,7 @@ struct InferencePack } }; -struct ConstraintGraphBuilder +struct ConstraintGenerator { // A list of all the scopes in the module. This vector holds ownership of the // scope pointers; the scopes themselves borrow pointers to other scopes to @@ -68,7 +68,7 @@ struct ConstraintGraphBuilder NotNull builtinTypes; const NotNull arena; // The root scope of the module we're generating constraints for. - // This is null when the CGB is initially constructed. + // This is null when the CG is initially constructed. Scope* rootScope; struct InferredBinding @@ -116,13 +116,13 @@ struct ConstraintGraphBuilder DcrLogger* logger; - ConstraintGraphBuilder(ModulePtr module, NotNull normalizer, NotNull moduleResolver, + ConstraintGenerator(ModulePtr module, NotNull normalizer, NotNull moduleResolver, NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, std::vector requireCycles); /** - * The entry point to the ConstraintGraphBuilder. This will construct a set + * The entry point to the ConstraintGenerator. This will construct a set * of scopes, constraints, and free types that can be solved later. * @param block the root block to generate constraints for. */ @@ -148,6 +148,8 @@ private: */ ScopePtr childScope(AstNode* node, const ScopePtr& parent); + std::optional lookup(Scope* scope, DefId def); + /** * Adds a new constraint with no dependencies to a given scope. * @param scope the scope to add the constraint to. @@ -232,12 +234,16 @@ private: Inference check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType); std::tuple checkBinary(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType); - std::optional checkLValue(const ScopePtr& scope, AstExpr* expr); - std::optional checkLValue(const ScopePtr& scope, AstExprLocal* local); - std::optional checkLValue(const ScopePtr& scope, AstExprGlobal* global); - std::optional checkLValue(const ScopePtr& scope, AstExprIndexName* indexName); - std::optional checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr); - TypeId updateProperty(const ScopePtr& scope, AstExpr* expr); + /** + * Generate constraints to assign assignedTy to the expression expr + * @returns the type of the expression. This may or may not be assignedTy itself. + */ + std::optional checkLValue(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprIndexName* indexName, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy); + TypeId updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy); void updateLValueType(AstExpr* lvalue, TypeId ty); @@ -324,7 +330,7 @@ private: /** Scan the program for global definitions. * - * ConstraintGraphBuilder needs to differentiate between globals and accesses to undefined symbols. Doing this "for + * ConstraintGenerator needs to differentiate between globals and accesses to undefined symbols. Doing this "for * real" in a general way is going to be pretty hard, so we are choosing not to tackle that yet. For now, we do an * initial scan of the AST and note what globals are defined. */ diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index a0afeed7..4a4d639b 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -3,14 +3,18 @@ #pragma once #include "Luau/Constraint.h" +#include "Luau/DenseHash.h" #include "Luau/Error.h" +#include "Luau/Location.h" #include "Luau/Module.h" #include "Luau/Normalize.h" #include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeCheckLimits.h" +#include "Luau/TypeFwd.h" #include "Luau/Variant.h" +#include #include namespace Luau @@ -74,6 +78,10 @@ struct ConstraintSolver std::unordered_map>, HashBlockedConstraintId> blocked; // Memoized instantiations of type aliases. DenseHashMap instantiatedAliases{{}}; + // Breadcrumbs for where a free type's upper bound was expanded. We use + // these to provide more helpful error messages when a free type is solved + // as never unexpectedly. + DenseHashMap>> upperBoundContributors{nullptr}; // A mapping from free types to the number of unresolved constraints that mention them. DenseHashMap unresolvedConstraints{{}}; @@ -140,7 +148,7 @@ struct ConstraintSolver std::pair, std::optional> lookupTableProp( TypeId subjectType, const std::string& propName, bool suppressSimplification = false); std::pair, std::optional> lookupTableProp( - TypeId subjectType, const std::string& propName, bool suppressSimplification, std::unordered_set& seen); + TypeId subjectType, const std::string& propName, bool suppressSimplification, DenseHashSet& seen); void block(NotNull target, NotNull constraint); /** diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index 34a0484a..ab957b89 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -3,6 +3,7 @@ // Do not include LValue. It should never be used here. #include "Luau/Ast.h" +#include "Luau/ControlFlow.h" #include "Luau/DenseHash.h" #include "Luau/Def.h" #include "Luau/Symbol.h" @@ -34,7 +35,7 @@ struct DataFlowGraph DataFlowGraph& operator=(DataFlowGraph&&) = default; DefId getDef(const AstExpr* expr) const; - // Look up for the rvalue breadcrumb for a compound assignment. + // Look up for the rvalue def for a compound assignment. std::optional getRValueDefForCompoundAssign(const AstExpr* expr) const; DefId getDef(const AstLocal* local) const; @@ -64,7 +65,7 @@ private: // Compound assignments are in a weird situation where the local being assigned to is also being used at its // previous type implicitly in an rvalue position. This map provides the previous binding. - DenseHashMap compoundAssignBreadcrumbs{nullptr}; + DenseHashMap compoundAssignDefs{nullptr}; DenseHashMap astRefinementKeys{nullptr}; @@ -74,11 +75,18 @@ private: struct DfgScope { DfgScope* parent; + bool isLoopScope; + DenseHashMap bindings{Symbol{}}; DenseHashMap> props{nullptr}; std::optional lookup(Symbol symbol) const; std::optional lookup(DefId def, const std::string& key) const; + + void inherit(const DfgScope* childScope); + + bool canUpdateDefinition(Symbol symbol) const; + bool canUpdateDefinition(DefId def, const std::string& key) const; }; struct DataFlowResult @@ -106,31 +114,32 @@ private: std::vector> scopes; - DfgScope* childScope(DfgScope* scope); + DfgScope* childScope(DfgScope* scope, bool isLoopScope = false); + void join(DfgScope* parent, DfgScope* a, DfgScope* b); - void visit(DfgScope* scope, AstStatBlock* b); - void visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b); + ControlFlow visit(DfgScope* scope, AstStatBlock* b); + ControlFlow visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b); - void visit(DfgScope* scope, AstStat* s); - void visit(DfgScope* scope, AstStatIf* i); - void visit(DfgScope* scope, AstStatWhile* w); - void visit(DfgScope* scope, AstStatRepeat* r); - void visit(DfgScope* scope, AstStatBreak* b); - void visit(DfgScope* scope, AstStatContinue* c); - void visit(DfgScope* scope, AstStatReturn* r); - void visit(DfgScope* scope, AstStatExpr* e); - void visit(DfgScope* scope, AstStatLocal* l); - void visit(DfgScope* scope, AstStatFor* f); - void visit(DfgScope* scope, AstStatForIn* f); - void visit(DfgScope* scope, AstStatAssign* a); - void visit(DfgScope* scope, AstStatCompoundAssign* c); - void visit(DfgScope* scope, AstStatFunction* f); - void visit(DfgScope* scope, AstStatLocalFunction* l); - void visit(DfgScope* scope, AstStatTypeAlias* t); - void visit(DfgScope* scope, AstStatDeclareGlobal* d); - void visit(DfgScope* scope, AstStatDeclareFunction* d); - void visit(DfgScope* scope, AstStatDeclareClass* d); - void visit(DfgScope* scope, AstStatError* error); + ControlFlow visit(DfgScope* scope, AstStat* s); + ControlFlow visit(DfgScope* scope, AstStatIf* i); + ControlFlow visit(DfgScope* scope, AstStatWhile* w); + ControlFlow visit(DfgScope* scope, AstStatRepeat* r); + ControlFlow visit(DfgScope* scope, AstStatBreak* b); + ControlFlow visit(DfgScope* scope, AstStatContinue* c); + ControlFlow visit(DfgScope* scope, AstStatReturn* r); + ControlFlow visit(DfgScope* scope, AstStatExpr* e); + ControlFlow visit(DfgScope* scope, AstStatLocal* l); + ControlFlow visit(DfgScope* scope, AstStatFor* f); + ControlFlow visit(DfgScope* scope, AstStatForIn* f); + ControlFlow visit(DfgScope* scope, AstStatAssign* a); + ControlFlow visit(DfgScope* scope, AstStatCompoundAssign* c); + ControlFlow visit(DfgScope* scope, AstStatFunction* f); + ControlFlow visit(DfgScope* scope, AstStatLocalFunction* l); + ControlFlow visit(DfgScope* scope, AstStatTypeAlias* t); + ControlFlow visit(DfgScope* scope, AstStatDeclareGlobal* d); + ControlFlow visit(DfgScope* scope, AstStatDeclareFunction* d); + ControlFlow visit(DfgScope* scope, AstStatDeclareClass* d); + ControlFlow visit(DfgScope* scope, AstStatError* error); DataFlowResult visitExpr(DfgScope* scope, AstExpr* e); DataFlowResult visitExpr(DfgScope* scope, AstExprGroup* group); diff --git a/Analysis/include/Luau/Def.h b/Analysis/include/Luau/Def.h index 0a286ae9..0a85fdee 100644 --- a/Analysis/include/Luau/Def.h +++ b/Analysis/include/Luau/Def.h @@ -79,8 +79,7 @@ struct DefArena TypedAllocator allocator; DefId freshCell(bool subscripted = false); - // TODO: implement once we have cases where we need to merge in definitions - // DefId phi(const std::vector& defs); + DefId phi(DefId a, DefId b); }; } // namespace Luau diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index 4b6c64c3..ddbf6dcb 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -322,6 +322,7 @@ struct TypePackMismatch { TypePackId wantedTp; TypePackId givenTp; + std::string reason; bool operator==(const TypePackMismatch& rhs) const; }; diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index 25af5200..2b83c443 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -71,7 +71,7 @@ struct SourceNode ModuleName name; std::string humanReadableName; - std::unordered_set requireSet; + DenseHashSet requireSet{{}}; std::vector> requireLocations; bool dirtySourceModule = true; bool dirtyModule = true; @@ -206,7 +206,7 @@ private: std::vector& buildQueue, const ModuleName& root, bool forAutocomplete, std::function canSkip = {}); void addBuildQueueItems(std::vector& items, std::vector& buildQueue, bool cycleDetected, - std::unordered_set& seen, const FrontendOptions& frontendOptions); + DenseHashSet& seen, const FrontendOptions& frontendOptions); void checkBuildQueueItem(BuildQueueItem& item); void checkBuildQueueItems(std::vector& items); void recordItemResult(const BuildQueueItem& item); diff --git a/Analysis/include/Luau/Module.h b/Analysis/include/Luau/Module.h index d647750f..197c7f9c 100644 --- a/Analysis/include/Luau/Module.h +++ b/Analysis/include/Luau/Module.h @@ -102,6 +102,8 @@ struct Module DenseHashMap astResolvedTypes{nullptr}; DenseHashMap astResolvedTypePacks{nullptr}; + DenseHashMap>> upperBoundContributors{nullptr}; + // Map AST nodes to the scope they create. Cannot be NotNull because // we need a sentinel value for the map. DenseHashMap astScopes{nullptr}; diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index ebb80e0f..4508d4a4 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -2,6 +2,7 @@ #pragma once #include "Luau/NotNull.h" +#include "Luau/Set.h" #include "Luau/TypeFwd.h" #include "Luau/UnifierSharedState.h" @@ -9,7 +10,6 @@ #include #include #include -#include #include namespace Luau @@ -29,7 +29,7 @@ bool isConsistentSubtype(TypePackId subTy, TypePackId superTy, NotNull sc class TypeIds { private: - std::unordered_set types; + DenseHashMap types{nullptr}; std::vector order; std::size_t hash = 0; @@ -254,6 +254,10 @@ struct NormalizedType // This type is either never or thread. TypeId threads; + // The buffer part of the type. + // This type is either never or buffer. + TypeId buffers; + // The (meta)table part of the type. // Each element of this set is a (meta)table type, or the top `table` type. // An empty set denotes never. @@ -277,6 +281,7 @@ struct NormalizedType NormalizedType& operator=(NormalizedType&&) = default; // IsType functions + bool isUnknown() const; /// Returns true if the type is exactly a number. Behaves like Type::isNumber() bool isExactlyNumber() const; @@ -298,6 +303,7 @@ struct NormalizedType bool hasNumbers() const; bool hasStrings() const; bool hasThreads() const; + bool hasBuffers() const; bool hasTables() const; bool hasFunctions() const; bool hasTyvars() const; @@ -358,7 +364,7 @@ public: void unionTablesWithTable(TypeIds& heres, TypeId there); void unionTables(TypeIds& heres, const TypeIds& theres); bool unionNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1); - bool unionNormalWithTy(NormalizedType& here, TypeId there, std::unordered_set& seenSetTypes, int ignoreSmallerTyvars = -1); + bool unionNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes, int ignoreSmallerTyvars = -1); // ------- Negations std::optional negateNormal(const NormalizedType& here); @@ -380,15 +386,15 @@ public: std::optional intersectionOfFunctions(TypeId here, TypeId there); void intersectFunctionsWithFunction(NormalizedFunctionType& heress, TypeId there); void intersectFunctions(NormalizedFunctionType& heress, const NormalizedFunctionType& theress); - bool intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, std::unordered_set& seenSetTypes); + bool intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, Set& seenSetTypes); bool intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1); - bool intersectNormalWithTy(NormalizedType& here, TypeId there, std::unordered_set& seenSetTypes); + bool intersectNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes); bool normalizeIntersections(const std::vector& intersections, NormalizedType& outType); // Check for inhabitance bool isInhabited(TypeId ty); - bool isInhabited(TypeId ty, std::unordered_set seen); - bool isInhabited(const NormalizedType* norm, std::unordered_set seen = {}); + bool isInhabited(TypeId ty, Set seen); + bool isInhabited(const NormalizedType* norm, Set seen = {nullptr}); // Check for intersections being inhabited bool isIntersectionInhabited(TypeId left, TypeId right); diff --git a/Analysis/include/Luau/Scope.h b/Analysis/include/Luau/Scope.h index 8cdffcb4..2360c986 100644 --- a/Analysis/include/Luau/Scope.h +++ b/Analysis/include/Luau/Scope.h @@ -56,7 +56,6 @@ struct Scope void addBuiltinTypeBinding(const Name& name, const TypeFun& tyFun); std::optional lookup(Symbol sym) const; - std::optional lookupLValue(DefId def) const; std::optional lookup(DefId def) const; std::optional> lookupEx(DefId def); std::optional> lookupEx(Symbol sym); @@ -80,6 +79,7 @@ struct Scope // types here. DenseHashMap rvalueRefinements{nullptr}; + void inheritAssignments(const ScopePtr& childScope); void inheritRefinements(const ScopePtr& childScope); // For mutually recursive type aliases, it's important that diff --git a/Analysis/include/Luau/Set.h b/Analysis/include/Luau/Set.h new file mode 100644 index 00000000..5baff136 --- /dev/null +++ b/Analysis/include/Luau/Set.h @@ -0,0 +1,105 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/DenseHash.h" + +namespace Luau +{ + +template +using SetHashDefault = std::conditional_t, DenseHashPointer, std::hash>; + +// This is an implementation of `unordered_set` using `DenseHashMap` to support erasure. +// This lets us work around `DenseHashSet` limitations and get a more traditional set interface. +template> +class Set +{ +private: + DenseHashMap mapping; + size_t entryCount = 0; + +public: + Set(const T& empty_key) + : mapping{empty_key} + { + } + + bool insert(const T& element) + { + bool& entry = mapping[element]; + bool fresh = !entry; + + if (fresh) + { + entry = true; + entryCount++; + } + + return fresh; + } + + template + void insert(Iterator begin, Iterator end) + { + for (Iterator it = begin; it != end; ++it) + insert(*it); + } + + void erase(const T& element) + { + bool& entry = mapping[element]; + + if (entry) + { + entry = false; + entryCount--; + } + } + + void clear() + { + mapping.clear(); + entryCount = 0; + } + + size_t size() const + { + return entryCount; + } + + bool empty() const + { + return entryCount == 0; + } + + size_t count(const T& element) const + { + const bool* entry = mapping.find(element); + return (entry && *entry) ? 1 : 0; + } + + bool contains(const T& element) const + { + return count(element) != 0; + } + + bool operator==(const Set& there) const + { + // if the sets are unequal sizes, then they cannot possibly be equal. + if (size() != there.size()) + return false; + + // otherwise, we'll need to check that every element we have here is in `there`. + for (auto [elem, present] : mapping) + { + // if it's not, we'll return `false` + if (present && there.contains(elem)) + return false; + } + + // otherwise, we've proven the two equal! + return true; + } +}; + +} // namespace Luau diff --git a/Analysis/include/Luau/Simplify.h b/Analysis/include/Luau/Simplify.h index 064648d7..10f27d4e 100644 --- a/Analysis/include/Luau/Simplify.h +++ b/Analysis/include/Luau/Simplify.h @@ -2,11 +2,10 @@ #pragma once +#include "Luau/DenseHash.h" #include "Luau/NotNull.h" #include "Luau/TypeFwd.h" -#include - namespace Luau { @@ -16,7 +15,7 @@ struct SimplifyResult { TypeId result; - std::set blockedTypes; + DenseHashSet blockedTypes; }; SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, TypeId ty, TypeId discriminant); diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index 321563e7..cb2d48dd 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -1,10 +1,11 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/Set.h" #include "Luau/TypeFwd.h" #include "Luau/TypePairHash.h" -#include "Luau/UnifierSharedState.h" #include "Luau/TypePath.h" +#include "Luau/DenseHash.h" #include #include @@ -22,6 +23,9 @@ struct NormalizedType; struct NormalizedClassType; struct NormalizedStringType; struct NormalizedFunctionType; +struct TypeArena; +struct Scope; +struct TableIndexer; struct SubtypingReasoning { @@ -31,6 +35,11 @@ struct SubtypingReasoning bool operator==(const SubtypingReasoning& other) const; }; +struct SubtypingReasoningHash +{ + size_t operator()(const SubtypingReasoning& r) const; +}; + struct SubtypingResult { bool isSubtype = false; @@ -40,7 +49,7 @@ struct SubtypingResult /// The reason for isSubtype to be false. May not be present even if /// isSubtype is false, depending on the input types. - std::optional reasoning; + DenseHashSet reasoning{SubtypingReasoning{}}; SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other); @@ -92,9 +101,9 @@ struct Subtyping Variance variance = Variance::Covariant; - using SeenSet = std::unordered_set, TypeIdPairHash>; + using SeenSet = Set, TypePairHash>; - SeenSet seenTypes; + SeenSet seenTypes{{}}; Subtyping(NotNull builtinTypes, NotNull typeArena, NotNull normalizer, NotNull iceReporter, NotNull scope); diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index 9b8564fb..51d2ded1 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -21,7 +21,6 @@ #include #include #include -#include #include LUAU_FASTINT(LuauTableTypeMaximumStringifierLength) @@ -141,6 +140,7 @@ struct PrimitiveType Thread, Function, Table, + Buffer, }; Type type; @@ -373,7 +373,15 @@ struct Property bool deprecated = false; std::string deprecatedSuggestion; + + // If this property was inferred from an expression, this field will be + // populated with the source location of the corresponding table property. std::optional location = std::nullopt; + + // If this property was built from an explicit type annotation, this field + // will be populated with the source location of that table property. + std::optional typeLocation = std::nullopt; + Tags tags; std::optional documentationSymbol; @@ -381,7 +389,7 @@ struct Property // TODO: Kill all constructors in favor of `Property::rw(TypeId read, TypeId write)` and friends. Property(); Property(TypeId readTy, bool deprecated = false, const std::string& deprecatedSuggestion = "", std::optional location = std::nullopt, - const Tags& tags = {}, const std::optional& documentationSymbol = std::nullopt); + const Tags& tags = {}, const std::optional& documentationSymbol = std::nullopt, std::optional typeLocation = std::nullopt); // DEPRECATED: Should only be called in non-RWP! We assert that the `readTy` is not nullopt. // TODO: Kill once we don't have non-RWP. @@ -739,6 +747,7 @@ bool isBoolean(TypeId ty); bool isNumber(TypeId ty); bool isString(TypeId ty); bool isThread(TypeId ty); +bool isBuffer(TypeId ty); bool isOptional(TypeId ty); bool isTableIntersection(TypeId ty); bool isOverloadedFunction(TypeId ty); @@ -797,6 +806,7 @@ public: const TypeId stringType; const TypeId booleanType; const TypeId threadType; + const TypeId bufferType; const TypeId functionType; const TypeId classType; const TypeId tableType; @@ -965,7 +975,7 @@ private: using SavedIterInfo = std::pair; std::deque stack; - std::unordered_set seen; // Only needed to protect the iterator from hanging the thread. + DenseHashSet seen{nullptr}; // Only needed to protect the iterator from hanging the thread. void advance() { @@ -992,7 +1002,7 @@ private: { // If we're about to descend into a cyclic type, we should skip over this. // Ideally this should never happen, but alas it does from time to time. :( - if (seen.find(inner) != seen.end()) + if (seen.contains(inner)) advance(); else { diff --git a/Analysis/include/Luau/TypeChecker2.h b/Analysis/include/Luau/TypeChecker2.h index aeeab0f8..b30cfe01 100644 --- a/Analysis/include/Luau/TypeChecker2.h +++ b/Analysis/include/Luau/TypeChecker2.h @@ -2,8 +2,6 @@ #pragma once -#include "Luau/Ast.h" -#include "Luau/Module.h" #include "Luau/NotNull.h" namespace Luau @@ -13,6 +11,8 @@ struct BuiltinTypes; struct DcrLogger; struct TypeCheckLimits; struct UnifierSharedState; +struct SourceModule; +struct Module; void check(NotNull builtinTypes, NotNull sharedState, NotNull limits, DcrLogger* logger, const SourceModule& sourceModule, Module* module); diff --git a/Analysis/include/Luau/TypeInfer.h b/Analysis/include/Luau/TypeInfer.h index 7de01406..26a67c7a 100644 --- a/Analysis/include/Luau/TypeInfer.h +++ b/Analysis/include/Luau/TypeInfer.h @@ -377,6 +377,7 @@ public: const TypeId stringType; const TypeId booleanType; const TypeId threadType; + const TypeId bufferType; const TypeId anyType; const TypeId unknownType; const TypeId neverType; diff --git a/Analysis/include/Luau/TypeOrPack.h b/Analysis/include/Luau/TypeOrPack.h index 2bdca1df..87001910 100644 --- a/Analysis/include/Luau/TypeOrPack.h +++ b/Analysis/include/Luau/TypeOrPack.h @@ -12,32 +12,28 @@ namespace Luau const void* ptr(TypeOrPack ty); -template -const T* get(TypeOrPack ty) +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) { - if constexpr (std::is_same_v) - return ty.get_if(); - else if constexpr (std::is_same_v) - return ty.get_if(); - else if constexpr (TypeVariant::is_part_of_v) - { - if (auto innerTy = ty.get_if()) - return get(*innerTy); - else - return nullptr; - } - else if constexpr (TypePackVariant::is_part_of_v) - { - if (auto innerTp = ty.get_if()) - return get(*innerTp); - else - return nullptr; - } + return tyOrTp.get_if(); +} + +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) +{ + if (const TypeId* ty = get(tyOrTp)) + return get(*ty); else - { - static_assert(always_false_v, "invalid T to get from TypeOrPack"); - LUAU_UNREACHABLE(); - } + return nullptr; +} + +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) +{ + if (const TypePackId* tp = get(tyOrTp)) + return get(*tp); + else + return nullptr; } TypeOrPack follow(TypeOrPack ty); diff --git a/Analysis/include/Luau/TypePath.h b/Analysis/include/Luau/TypePath.h index 96fcdcb1..bdca95a4 100644 --- a/Analysis/include/Luau/TypePath.h +++ b/Analysis/include/Luau/TypePath.h @@ -4,7 +4,6 @@ #include "Luau/TypeFwd.h" #include "Luau/Variant.h" #include "Luau/NotNull.h" -#include "Luau/TypeOrPack.h" #include #include @@ -153,6 +152,16 @@ struct Path } }; +struct PathHash +{ + size_t operator()(const Property& prop) const; + size_t operator()(const Index& idx) const; + size_t operator()(const TypeField& field) const; + size_t operator()(const PackField& field) const; + size_t operator()(const Component& component) const; + size_t operator()(const Path& path) const; +}; + /// The canonical "empty" Path, meaning a Path with no components. static const Path kEmpty{}; @@ -184,7 +193,7 @@ using Path = TypePath::Path; /// Converts a Path to a string for debugging purposes. This output may not be /// terribly clear to end users of the Luau type system. -std::string toString(const TypePath::Path& path); +std::string toString(const TypePath::Path& path, bool prefixDot = false); std::optional traverse(TypeId root, const Path& path, NotNull builtinTypes); std::optional traverse(TypePackId root, const Path& path, NotNull builtinTypes); diff --git a/Analysis/include/Luau/Unifier2.h b/Analysis/include/Luau/Unifier2.h index 3d5b5a1a..49a275d5 100644 --- a/Analysis/include/Luau/Unifier2.h +++ b/Analysis/include/Luau/Unifier2.h @@ -6,7 +6,6 @@ #include "Luau/NotNull.h" #include "Luau/TypePairHash.h" #include "Luau/TypeCheckLimits.h" -#include "Luau/TypeChecker2.h" #include "Luau/TypeFwd.h" #include @@ -37,6 +36,8 @@ struct Unifier2 DenseHashSet, TypePairHash> seenTypePairings{{nullptr, nullptr}}; DenseHashSet, TypePairHash> seenTypePackPairings{{nullptr, nullptr}}; + DenseHashMap> expandedFreeTypes{nullptr}; + int recursionCount = 0; int recursionLimit = 0; diff --git a/Analysis/src/AstJsonEncoder.cpp b/Analysis/src/AstJsonEncoder.cpp index 920517c2..2d1940d4 100644 --- a/Analysis/src/AstJsonEncoder.cpp +++ b/Analysis/src/AstJsonEncoder.cpp @@ -8,7 +8,6 @@ #include -LUAU_FASTFLAG(LuauFloorDivision); LUAU_FASTFLAG(LuauClipExtraHasEndProps); namespace Luau @@ -519,7 +518,6 @@ struct AstJsonEncoder : public AstVisitor case AstExprBinary::Div: return writeString("Div"); case AstExprBinary::FloorDiv: - LUAU_ASSERT(FFlag::LuauFloorDivision); return writeString("FloorDiv"); case AstExprBinary::Mod: return writeString("Mod"); diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index d73598c7..52cb54e3 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -5,6 +5,7 @@ #include "Luau/BuiltinDefinitions.h" #include "Luau/Frontend.h" #include "Luau/ToString.h" +#include "Luau/Subtyping.h" #include "Luau/TypeInfer.h" #include "Luau/TypePack.h" @@ -12,6 +13,7 @@ #include #include +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauReadWriteProperties); LUAU_FASTFLAG(LuauClipExtraHasEndProps); LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false); @@ -143,13 +145,24 @@ static bool checkTypeMatch(TypeId subTy, TypeId superTy, NotNull scope, T InternalErrorReporter iceReporter; UnifierSharedState unifierState(&iceReporter); Normalizer normalizer{typeArena, builtinTypes, NotNull{&unifierState}}; - Unifier unifier(NotNull{&normalizer}, scope, Location(), Variance::Covariant); - // Cost of normalization can be too high for autocomplete response time requirements - unifier.normalize = false; - unifier.checkInhabited = false; + if (FFlag::DebugLuauDeferredConstraintResolution) + { + Subtyping subtyping{builtinTypes, NotNull{typeArena}, NotNull{&normalizer}, NotNull{&iceReporter}, scope}; + + return subtyping.isSubtype(subTy, superTy).isSubtype; + } + else + { + Unifier unifier(NotNull{&normalizer}, scope, Location(), Variance::Covariant); + + // Cost of normalization can be too high for autocomplete response time requirements + unifier.normalize = false; + unifier.checkInhabited = false; + + return unifier.canUnify(subTy, superTy).empty(); + } - return unifier.canUnify(subTy, superTy).empty(); } static TypeCorrectKind checkTypeCorrectKind( diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index b7631460..5ce12873 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -7,7 +7,7 @@ #include "Luau/Common.h" #include "Luau/ToString.h" #include "Luau/ConstraintSolver.h" -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/NotNull.h" #include "Luau/TypeInfer.h" #include "Luau/TypeFamily.h" diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 01b0bdfd..1b97bb89 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -12,9 +12,8 @@ LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) -LUAU_FASTFLAGVARIABLE(LuauCloneCyclicUnions, false) -LUAU_FASTFLAGVARIABLE(LuauStacklessTypeClone2, false) +LUAU_FASTFLAGVARIABLE(LuauStacklessTypeClone3, false) LUAU_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000) namespace Luau @@ -118,6 +117,8 @@ private: ty = follow(ty, FollowOption::DisableLazyTypeThunks); if (auto it = types->find(ty); it != types->end()) return it->second; + else if (ty->persistent) + return ty; return std::nullopt; } @@ -126,6 +127,8 @@ private: tp = follow(tp); if (auto it = packs->find(tp); it != packs->end()) return it->second; + else if (tp->persistent) + return tp; return std::nullopt; } @@ -778,33 +781,19 @@ void TypeCloner::operator()(const AnyType& t) void TypeCloner::operator()(const UnionType& t) { - if (FFlag::LuauCloneCyclicUnions) - { - // We're just using this FreeType as a placeholder until we've finished - // cloning the parts of this union so it is okay that its bounds are - // nullptr. We'll never indirect them. - TypeId result = dest.addType(FreeType{nullptr, /*lowerBound*/ nullptr, /*upperBound*/ nullptr}); - seenTypes[typeId] = result; + // We're just using this FreeType as a placeholder until we've finished + // cloning the parts of this union so it is okay that its bounds are + // nullptr. We'll never indirect them. + TypeId result = dest.addType(FreeType{nullptr, /*lowerBound*/ nullptr, /*upperBound*/ nullptr}); + seenTypes[typeId] = result; - std::vector options; - options.reserve(t.options.size()); + std::vector options; + options.reserve(t.options.size()); - for (TypeId ty : t.options) - options.push_back(clone(ty, dest, cloneState)); + for (TypeId ty : t.options) + options.push_back(clone(ty, dest, cloneState)); - asMutable(result)->ty.emplace(std::move(options)); - } - else - { - std::vector options; - options.reserve(t.options.size()); - - for (TypeId ty : t.options) - options.push_back(clone(ty, dest, cloneState)); - - TypeId result = dest.addType(UnionType{std::move(options)}); - seenTypes[typeId] = result; - } + asMutable(result)->ty.emplace(std::move(options)); } void TypeCloner::operator()(const IntersectionType& t) @@ -879,7 +868,7 @@ TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState) if (tp->persistent) return tp; - if (FFlag::LuauStacklessTypeClone2) + if (FFlag::LuauStacklessTypeClone3) { TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}}; return cloner.clone(tp); @@ -905,7 +894,7 @@ TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState) if (typeId->persistent) return typeId; - if (FFlag::LuauStacklessTypeClone2) + if (FFlag::LuauStacklessTypeClone3) { TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}}; return cloner.clone(typeId); @@ -934,7 +923,7 @@ TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState) TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState) { - if (FFlag::LuauStacklessTypeClone2) + if (FFlag::LuauStacklessTypeClone3) { TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}}; diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGenerator.cpp similarity index 90% rename from Analysis/src/ConstraintGraphBuilder.cpp rename to Analysis/src/ConstraintGenerator.cpp index 4f4ff306..15e64c92 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -1,5 +1,5 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/Ast.h" #include "Luau/Def.h" @@ -7,6 +7,7 @@ #include "Luau/Constraint.h" #include "Luau/ControlFlow.h" #include "Luau/DcrLogger.h" +#include "Luau/DenseHash.h" #include "Luau/ModuleResolver.h" #include "Luau/RecursionCounter.h" #include "Luau/Refinement.h" @@ -23,9 +24,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit); LUAU_FASTFLAG(DebugLuauLogSolverToJson); LUAU_FASTFLAG(DebugLuauMagicTypes); -LUAU_FASTFLAG(LuauParseDeclareClassIndexer); LUAU_FASTFLAG(LuauLoopControlFlowAnalysis); -LUAU_FASTFLAG(LuauFloorDivision); namespace Luau { @@ -126,21 +125,21 @@ struct Checkpoint size_t offset; }; -Checkpoint checkpoint(const ConstraintGraphBuilder* cgb) +Checkpoint checkpoint(const ConstraintGenerator* cg) { - return Checkpoint{cgb->constraints.size()}; + return Checkpoint{cg->constraints.size()}; } template -void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const ConstraintGraphBuilder* cgb, F f) +void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const ConstraintGenerator* cg, F f) { for (size_t i = start.offset; i < end.offset; ++i) - f(cgb->constraints[i]); + f(cg->constraints[i]); } } // namespace -ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, NotNull normalizer, NotNull moduleResolver, +ConstraintGenerator::ConstraintGenerator(ModulePtr module, NotNull normalizer, NotNull moduleResolver, NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, std::vector requireCycles) @@ -160,7 +159,7 @@ ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, NotNullcaptureGenerationModule(module); } -TypeId ConstraintGraphBuilder::freshType(const ScopePtr& scope) +TypeId ConstraintGenerator::freshType(const ScopePtr& scope) { return Luau::freshType(arena, builtinTypes, scope.get()); } -TypePackId ConstraintGraphBuilder::freshTypePack(const ScopePtr& scope) +TypePackId ConstraintGenerator::freshTypePack(const ScopePtr& scope) { FreeTypePack f{scope.get()}; return arena->addTypePack(TypePackVar{std::move(f)}); } -ScopePtr ConstraintGraphBuilder::childScope(AstNode* node, const ScopePtr& parent) +ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent) { auto scope = std::make_shared(parent); scopes.emplace_back(node->location, scope); @@ -206,17 +205,77 @@ ScopePtr ConstraintGraphBuilder::childScope(AstNode* node, const ScopePtr& paren return scope; } -NotNull ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, const Location& location, ConstraintV cv) +static std::vector flatten(const Phi* phi) +{ + std::vector result; + + std::deque queue{phi->operands.begin(), phi->operands.end()}; + DenseHashSet seen{nullptr}; + + while (!queue.empty()) + { + DefId next = queue.front(); + queue.pop_front(); + + // Phi nodes should never be cyclic. + LUAU_ASSERT(!seen.find(next)); + if (seen.find(next)) + continue; + seen.insert(next); + + if (get(next)) + result.push_back(next); + else if (auto phi = get(next)) + queue.insert(queue.end(), phi->operands.begin(), phi->operands.end()); + } + + return result; +} + +std::optional ConstraintGenerator::lookup(Scope* scope, DefId def) +{ + if (get(def)) + return scope->lookup(def); + if (auto phi = get(def)) + { + if (auto found = scope->lookup(def)) + return *found; + + TypeId res = builtinTypes->neverType; + + for (DefId operand : flatten(phi)) + { + // `scope->lookup(operand)` may return nothing because it could be a phi node of globals, but one of + // the operand of that global has never been assigned a type, and so it should be an error. + // e.g. + // ``` + // if foo() then + // g = 5 + // end + // -- `g` here is a phi node of the assignment to `g`, or the original revision of `g` before the branch. + // ``` + TypeId ty = scope->lookup(operand).value_or(builtinTypes->errorRecoveryType()); + res = simplifyUnion(builtinTypes, arena, res, ty).result; + } + + scope->lvalueTypes[def] = res; + return res; + } + else + ice->ice("ConstraintGenerator::lookup is inexhaustive?"); +} + +NotNull ConstraintGenerator::addConstraint(const ScopePtr& scope, const Location& location, ConstraintV cv) { return NotNull{constraints.emplace_back(new Constraint{NotNull{scope.get()}, location, std::move(cv)}).get()}; } -NotNull ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, std::unique_ptr c) +NotNull ConstraintGenerator::addConstraint(const ScopePtr& scope, std::unique_ptr c) { return NotNull{constraints.emplace_back(std::move(c)).get()}; } -void ConstraintGraphBuilder::unionRefinements(const RefinementContext& lhs, const RefinementContext& rhs, RefinementContext& dest, std::vector* constraints) +void ConstraintGenerator::unionRefinements(const RefinementContext& lhs, const RefinementContext& rhs, RefinementContext& dest, std::vector* constraints) { const auto intersect = [&](const std::vector& types) { if (1 == types.size()) @@ -252,7 +311,7 @@ void ConstraintGraphBuilder::unionRefinements(const RefinementContext& lhs, cons } } -void ConstraintGraphBuilder::computeRefinement(const ScopePtr& scope, RefinementId refinement, RefinementContext* refis, bool sense, bool eq, std::vector* constraints) +void ConstraintGenerator::computeRefinement(const ScopePtr& scope, RefinementId refinement, RefinementContext* refis, bool sense, bool eq, std::vector* constraints) { if (!refinement) return; @@ -382,7 +441,7 @@ bool mustDeferIntersection(TypeId ty) } } // namespace -void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location location, RefinementId refinement) +void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location location, RefinementId refinement) { if (!refinement) return; @@ -393,7 +452,7 @@ void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location lo for (auto& [def, partition] : refinements) { - if (std::optional defTy = scope->lookup(def)) + if (std::optional defTy = lookup(scope.get(), def)) { TypeId ty = *defTy; if (partition.shouldAppendNilType) @@ -439,7 +498,7 @@ void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location lo addConstraint(scope, location, c); } -ControlFlow ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block) +ControlFlow ConstraintGenerator::visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block) { RecursionCounter counter{&recursionCount}; @@ -502,7 +561,7 @@ ControlFlow ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& return firstControlFlow.value_or(ControlFlow::None); } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStat* stat) { RecursionLimiter limiter{&recursionCount, FInt::LuauCheckRecursionLimit}; @@ -560,7 +619,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) } } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* statLocal) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* statLocal) { std::vector> varTypes; varTypes.reserve(statLocal->vars.size); @@ -663,7 +722,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* s return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for_) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFor* for_) { TypeId annotationTy = builtinTypes->numberType; if (for_->var->annotation) @@ -693,7 +752,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* forIn) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forIn) { ScopePtr loopScope = childScope(forIn, scope); @@ -728,7 +787,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatWhile* while_) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatWhile* while_) { RefinementId refinement = check(scope, while_->condition).refinement; @@ -740,7 +799,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatWhile* w return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatRepeat* repeat) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatRepeat* repeat) { ScopePtr repeatScope = childScope(repeat, scope); @@ -751,7 +810,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatRepeat* return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFunction* function) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFunction* function) { // Local // Global @@ -801,7 +860,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFun return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction* function) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* function) { // Name could be AstStatLocal, AstStatGlobal, AstStatIndexName. // With or without self @@ -811,10 +870,10 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction Checkpoint start = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location); - std::unordered_set excludeList; + DenseHashSet excludeList{nullptr}; DefId def = dfg->getDef(function->name); - std::optional existingFunctionTy = scope->lookupLValue(def); + std::optional existingFunctionTy = scope->lookup(def); if (AstExprLocal* localName = function->name->as()) { @@ -846,7 +905,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction else if (AstExprIndexName* indexName = function->name->as()) { Checkpoint check1 = checkpoint(this); - std::optional lvalueType = checkLValue(scope, indexName); + std::optional lvalueType = checkLValue(scope, indexName, generalizedType); LUAU_ASSERT(lvalueType); Checkpoint check2 = checkpoint(this); @@ -856,12 +915,9 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction // TODO figure out how to populate the location field of the table Property. - if (lvalueType) + if (lvalueType && *lvalueType != generalizedType) { - if (get(*lvalueType)) - asMutable(*lvalueType)->ty.emplace(generalizedType); - else - addConstraint(scope, indexName->location, SubtypeConstraint{*lvalueType, generalizedType}); + addConstraint(scope, indexName->location, SubtypeConstraint{*lvalueType, generalizedType}); } } else if (AstExprError* err = function->name->as()) @@ -883,7 +939,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction Constraint* previous = nullptr; forEachConstraint(start, end, this, [&c, &excludeList, &previous](const ConstraintPtr& constraint) { - if (!excludeList.count(constraint.get())) + if (!excludeList.contains(constraint.get())) c->dependencies.push_back(NotNull{constraint.get()}); if (auto psc = get(*constraint); psc && psc->returns) @@ -900,7 +956,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatReturn* ret) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatReturn* ret) { // At this point, the only way scope->returnType should have anything // interesting in it is if the function has an explicit return annotation. @@ -916,12 +972,16 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatReturn* return ControlFlow::Returns; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatBlock* block) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatBlock* block) { ScopePtr innerScope = childScope(block, scope); ControlFlow flow = visitBlockWithoutChildScope(innerScope, block); + + // An AstStatBlock has linear control flow, i.e. one entry and one exit, so we can inherit + // all the changes to the environment occurred by the statements in that block. scope->inheritRefinements(innerScope); + scope->inheritAssignments(innerScope); return flow; } @@ -944,7 +1004,7 @@ static void bindFreeType(TypeId a, TypeId b) asMutable(b)->ty.emplace(a); } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign) { std::vector> expectedTypes; expectedTypes.reserve(assign->vars.size); @@ -957,16 +1017,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* TypeId assignee = arena->addType(BlockedType{}); assignees.push_back(assignee); - std::optional upperBound = follow(checkLValue(scope, lvalue)); - if (upperBound) - { - if (get(*upperBound)) - expectedTypes.push_back(std::nullopt); - else - expectedTypes.push_back(*upperBound); - - addConstraint(scope, lvalue->location, SubtypeConstraint{assignee, *upperBound}); - } + checkLValue(scope, lvalue, assignee); DefId def = dfg->getDef(lvalue); scope->lvalueTypes[def] = assignee; @@ -979,14 +1030,12 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompoundAssign* assign) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatCompoundAssign* assign) { - std::optional varTy = checkLValue(scope, assign->var); - AstExprBinary binop = AstExprBinary{assign->location, assign->op, assign->var, assign->value}; TypeId resultTy = check(scope, &binop).ty; - if (varTy) - addConstraint(scope, assign->location, SubtypeConstraint{resultTy, *varTy}); + + checkLValue(scope, assign->var, resultTy); DefId def = dfg->getDef(assign->var); scope->lvalueTypes[def] = resultTy; @@ -994,7 +1043,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompound return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatIf* ifStatement) { RefinementId refinement = check(scope, ifStatement->condition, std::nullopt).refinement; @@ -1014,6 +1063,11 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifSt else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) scope->inheritRefinements(thenScope); + if (thencf == ControlFlow::None) + scope->inheritAssignments(thenScope); + if (elsecf == ControlFlow::None) + scope->inheritAssignments(elseScope); + if (FFlag::LuauLoopControlFlowAnalysis && thencf == elsecf) return thencf; else if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) @@ -1041,7 +1095,7 @@ static bool occursCheck(TypeId needle, TypeId haystack) return false; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatTypeAlias* alias) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias* alias) { ScopePtr* defnScope = astTypeAliasDefiningScopes.find(alias); @@ -1090,7 +1144,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatTypeAlia return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareGlobal* global) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareGlobal* global) { LUAU_ASSERT(global->type); @@ -1112,10 +1166,10 @@ static bool isMetamethod(const Name& name) return name == "__index" || name == "__newindex" || name == "__call" || name == "__concat" || name == "__unm" || name == "__add" || name == "__sub" || name == "__mul" || name == "__div" || name == "__mod" || name == "__pow" || name == "__tostring" || name == "__metatable" || name == "__eq" || name == "__lt" || name == "__le" || name == "__mode" || name == "__iter" || name == "__len" || - (FFlag::LuauFloorDivision && name == "__idiv"); + name == "__idiv"; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareClass* declaredClass) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareClass* declaredClass) { std::optional superTy = std::make_optional(builtinTypes->classType); if (declaredClass->superName) @@ -1154,7 +1208,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareC scope->exportedTypeBindings[className] = TypeFun{{}, classTy}; - if (FFlag::LuauParseDeclareClassIndexer && declaredClass->indexer) + if (declaredClass->indexer) { RecursionCounter counter{&recursionCount}; @@ -1234,7 +1288,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareC return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareFunction* global) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareFunction* global) { std::vector> generics = createGenerics(scope, global->generics); std::vector> genericPacks = createGenericPacks(scope, global->genericPacks); @@ -1279,7 +1333,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareF return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatError* error) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatError* error) { for (AstStat* stat : error->statements) visit(scope, stat); @@ -1289,7 +1343,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatError* e return ControlFlow::None; } -InferencePack ConstraintGraphBuilder::checkPack( +InferencePack ConstraintGenerator::checkPack( const ScopePtr& scope, AstArray exprs, const std::vector>& expectedTypes) { std::vector head; @@ -1320,7 +1374,7 @@ InferencePack ConstraintGraphBuilder::checkPack( return InferencePack{arena->addTypePack(TypePack{std::move(head), tail})}; } -InferencePack ConstraintGraphBuilder::checkPack( +InferencePack ConstraintGenerator::checkPack( const ScopePtr& scope, AstExpr* expr, const std::vector>& expectedTypes, bool generalize) { RecursionCounter counter{&recursionCount}; @@ -1356,7 +1410,7 @@ InferencePack ConstraintGraphBuilder::checkPack( return result; } -InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCall* call) +InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* call) { std::vector exprArgs; @@ -1530,7 +1584,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa } } -Inference ConstraintGraphBuilder::check( +Inference ConstraintGenerator::check( const ScopePtr& scope, AstExpr* expr, std::optional expectedType, bool forceSingleton, bool generalize) { RecursionCounter counter{&recursionCount}; @@ -1600,7 +1654,7 @@ Inference ConstraintGraphBuilder::check( return result; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantString* string, std::optional expectedType, bool forceSingleton) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantString* string, std::optional expectedType, bool forceSingleton) { if (forceSingleton) return Inference{arena->addType(SingletonType{StringSingleton{std::string{string->value.data, string->value.size}}})}; @@ -1624,7 +1678,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantSt return Inference{builtinTypes->stringType}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantBool* boolExpr, std::optional expectedType, bool forceSingleton) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantBool* boolExpr, std::optional expectedType, bool forceSingleton) { const TypeId singletonType = boolExpr->value ? builtinTypes->trueType : builtinTypes->falseType; if (forceSingleton) @@ -1649,7 +1703,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantBo return Inference{builtinTypes->booleanType}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* local) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) { const RefinementKey* key = dfg->getRefinementKey(local); std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(local); @@ -1659,12 +1713,12 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* loc // if we have a refinement key, we can look up its type. if (key) - maybeTy = scope->lookup(key->def); + maybeTy = lookup(scope.get(), key->def); // if the current def doesn't have a type, we might be doing a compound assignment // and therefore might need to look at the rvalue def instead. if (!maybeTy && rvalueDef) - maybeTy = scope->lookup(*rvalueDef); + maybeTy = lookup(scope.get(), *rvalueDef); if (maybeTy) { @@ -1675,10 +1729,10 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* loc return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)}; } else - ice->ice("CGB: AstExprLocal came before its declaration?"); + ice->ice("CG: AstExprLocal came before its declaration?"); } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* global) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* global) { const RefinementKey* key = dfg->getRefinementKey(global); std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(global); @@ -1690,11 +1744,11 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* gl /* prepopulateGlobalScope() has already added all global functions to the environment by this point, so any * global that is not already in-scope is definitely an unknown symbol. */ - if (auto ty = scope->lookup(def)) + if (auto ty = lookup(scope.get(), def)) return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; else if (auto ty = scope->lookup(global->name)) { - rootScope->rvalueRefinements[key->def] = *ty; + rootScope->lvalueTypes[def] = *ty; return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; } else @@ -1704,7 +1758,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* gl } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName* indexName) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* indexName) { TypeId obj = check(scope, indexName->expr).ty; TypeId result = arena->addType(BlockedType{}); @@ -1712,7 +1766,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName* const RefinementKey* key = dfg->getRefinementKey(indexName); if (key) { - if (auto ty = scope->lookup(key->def)) + if (auto ty = lookup(scope.get(), key->def)) return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; scope->rvalueRefinements[key->def] = result; @@ -1726,7 +1780,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName* return Inference{result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr) { TypeId obj = check(scope, indexExpr->expr).ty; TypeId indexType = check(scope, indexExpr->index).ty; @@ -1735,7 +1789,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* const RefinementKey* key = dfg->getRefinementKey(indexExpr); if (key) { - if (auto ty = scope->lookup(key->def)) + if (auto ty = lookup(scope.get(), key->def)) return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; scope->rvalueRefinements[key->def] = result; @@ -1752,7 +1806,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* return Inference{result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize) { Checkpoint startCheckpoint = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, func, expectedType); @@ -1785,7 +1839,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* unary) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprUnary* unary) { auto [operandType, refinement] = check(scope, unary->expr); @@ -1826,7 +1880,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* una } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) { auto [leftType, rightType, refinement] = checkBinary(scope, binary, expectedType); @@ -1990,7 +2044,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional expectedType) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional expectedType) { ScopePtr condScope = childScope(ifElse->condition, scope); RefinementId refinement = check(condScope, ifElse->condition).refinement; @@ -2006,13 +2060,13 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* if return Inference{expectedType ? *expectedType : simplifyUnion(builtinTypes, arena, thenType, elseType).result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert) { check(scope, typeAssert->expr, std::nullopt); return Inference{resolveType(scope, typeAssert->annotation, /* inTypeArguments */ false)}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprInterpString* interpString) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprInterpString* interpString) { for (AstExpr* expr : interpString->expressions) check(scope, expr); @@ -2020,7 +2074,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprInterpStri return Inference{builtinTypes->stringType}; } -std::tuple ConstraintGraphBuilder::checkBinary( +std::tuple ConstraintGenerator::checkBinary( const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) { if (binary->op == AstExprBinary::And) @@ -2077,6 +2131,8 @@ std::tuple ConstraintGraphBuilder::checkBinary( discriminantTy = builtinTypes->booleanType; else if (typeguard->type == "thread") discriminantTy = builtinTypes->threadType; + else if (typeguard->type == "buffer") + discriminantTy = builtinTypes->bufferType; else if (typeguard->type == "table") discriminantTy = augmentForErrorSupression(builtinTypes->tableType); else if (typeguard->type == "function") @@ -2133,16 +2189,16 @@ std::tuple ConstraintGraphBuilder::checkBinary( } } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy) { if (auto local = expr->as()) - return checkLValue(scope, local); + return checkLValue(scope, local, assignedTy); else if (auto global = expr->as()) - return checkLValue(scope, global); + return checkLValue(scope, global, assignedTy); else if (auto indexName = expr->as()) - return checkLValue(scope, indexName); + return checkLValue(scope, indexName, assignedTy); else if (auto indexExpr = expr->as()) - return checkLValue(scope, indexExpr); + return checkLValue(scope, indexExpr, assignedTy); else if (auto error = expr->as()) { check(scope, error); @@ -2152,7 +2208,7 @@ std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, ice->ice("checkLValue is inexhaustive"); } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprLocal* local) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy) { /* * The caller of this method uses the returned type to emit the proper @@ -2162,49 +2218,30 @@ std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, * populated by symbols that have type annotations. * * If this local has an interesting type annotation, it is important that we - * return that. + * return that and constrain the assigned type. */ std::optional annotatedTy = scope->lookup(local->local); if (annotatedTy) - return annotatedTy; + addConstraint(scope, local->location, SubtypeConstraint{assignedTy, *annotatedTy}); + else if (auto it = inferredBindings.find(local->local); it == inferredBindings.end()) + ice->ice("Cannot find AstLocal* in either Scope::bindings or inferredBindings?"); - /* - * As a safety measure, we'll assert that no type has yet been ascribed to - * the corresponding def. We'll populate this when we generate - * constraints for assignment and compound assignment statements. - */ - LUAU_ASSERT(!scope->lookupLValue(dfg->getDef(local))); - return std::nullopt; + return annotatedTy; } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprGlobal* global) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy) { return scope->lookup(Symbol{global->name}); } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName, TypeId assignedTy) { - return updateProperty(scope, indexName); + return updateProperty(scope, indexName, assignedTy); } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy) { - return updateProperty(scope, indexExpr); -} - -static bool isIndexNameEquivalent(AstExpr* expr) -{ - if (expr->is()) - return true; - - AstExprIndexExpr* e = expr->as(); - if (e == nullptr) - return false; - - if (!e->index->is()) - return false; - - return true; + return updateProperty(scope, indexExpr, assignedTy); } /** @@ -2212,8 +2249,19 @@ static bool isIndexNameEquivalent(AstExpr* expr) * * If expr has the form name.a.b.c */ -TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* expr) +TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy) { + // There are a bunch of cases where we realize that this is not the kind of + // assignment that potentially changes the shape of a table. When we + // encounter them, we call this to fall back and do the "usual thing." + auto fallback = [&]() { + TypeId resTy = check(scope, expr).ty; + addConstraint(scope, expr->location, SubtypeConstraint{assignedTy, resTy}); + return resTy; + }; + + LUAU_ASSERT(expr->is() || expr->is()); + if (auto indexExpr = expr->as(); indexExpr && !indexExpr->index->is()) { // An indexer is only interesting in an lvalue-ey way if it is at the @@ -2231,15 +2279,12 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex TypeId resultType = arena->addType(BlockedType{}); TypeId subjectType = check(scope, indexExpr->expr).ty; TypeId indexType = check(scope, indexExpr->index).ty; - TypeId propType = arena->addType(BlockedType{}); - addConstraint(scope, expr->location, SetIndexerConstraint{resultType, subjectType, indexType, propType}); + addConstraint(scope, expr->location, SetIndexerConstraint{resultType, subjectType, indexType, assignedTy}); - module->astTypes[expr] = propType; + module->astTypes[expr] = assignedTy; - return propType; + return assignedTy; } - else if (!isIndexNameEquivalent(expr)) - return check(scope, expr).ty; Symbol sym; const Def* def = nullptr; @@ -2269,21 +2314,24 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex } else if (auto indexExpr = e->as()) { - // We need to populate the type for the index value - check(scope, indexExpr->index); if (auto strIndex = indexExpr->index->as()) { + // We need to populate astTypes for the index value. + check(scope, indexExpr->index); + segments.push_back(std::string(strIndex->value.data, strIndex->value.size)); exprs.push_back(e); e = indexExpr->expr; } else { - return check(scope, expr).ty; + return fallback(); } } else - return check(scope, expr).ty; + { + return fallback(); + } } LUAU_ASSERT(!segments.empty()); @@ -2294,16 +2342,14 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex LUAU_ASSERT(def); std::optional> lookupResult = scope->lookupEx(NotNull{def}); if (!lookupResult) - return check(scope, expr).ty; + return fallback(); const auto [subjectType, subjectScope] = *lookupResult; - TypeId propTy = freshType(scope); - std::vector segmentStrings(begin(segments), end(segments)); TypeId updatedType = arena->addType(BlockedType{}); - addConstraint(scope, expr->location, SetPropConstraint{updatedType, subjectType, std::move(segmentStrings), propTy}); + addConstraint(scope, expr->location, SetPropConstraint{updatedType, subjectType, std::move(segmentStrings), assignedTy}); TypeId prevSegmentTy = updatedType; for (size_t i = 0; i < segments.size(); ++i) @@ -2330,10 +2376,10 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex } } - return propTy; + return assignedTy; } -void ConstraintGraphBuilder::updateLValueType(AstExpr* lvalue, TypeId ty) +void ConstraintGenerator::updateLValueType(AstExpr* lvalue, TypeId ty) { if (auto local = lvalue->as()) { @@ -2342,7 +2388,7 @@ void ConstraintGraphBuilder::updateLValueType(AstExpr* lvalue, TypeId ty) } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) { const bool expectedTypeIsFree = expectedType && get(follow(*expectedType)); @@ -2462,7 +2508,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* exp return Inference{ty}; } -ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionSignature( +ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignature( const ScopePtr& parent, AstExprFunction* fn, std::optional expectedType, std::optional originalName) { ScopePtr signatureScope = nullptr; @@ -2654,7 +2700,7 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS }; } -void ConstraintGraphBuilder::checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn) +void ConstraintGenerator::checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn) { visitBlockWithoutChildScope(scope, fn->body); @@ -2662,12 +2708,12 @@ void ConstraintGraphBuilder::checkFunctionBody(const ScopePtr& scope, AstExprFun if (nullptr != getFallthrough(fn->body)) { - TypePackId empty = arena->addTypePack({}); // TODO we could have CGB retain one of these forever + TypePackId empty = arena->addTypePack({}); // TODO we could have CG retain one of these forever addConstraint(scope, fn->location, PackSubtypeConstraint{scope->returnType, empty}); } } -TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) +TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) { TypeId result = nullptr; @@ -2895,7 +2941,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b return result; } -TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, AstTypePack* tp, bool inTypeArgument, bool replaceErrorWithFresh) +TypePackId ConstraintGenerator::resolveTypePack(const ScopePtr& scope, AstTypePack* tp, bool inTypeArgument, bool replaceErrorWithFresh) { TypePackId result; if (auto expl = tp->as()) @@ -2929,7 +2975,7 @@ TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, AstTyp return result; } -TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, const AstTypeList& list, bool inTypeArguments, bool replaceErrorWithFresh) +TypePackId ConstraintGenerator::resolveTypePack(const ScopePtr& scope, const AstTypeList& list, bool inTypeArguments, bool replaceErrorWithFresh) { std::vector head; @@ -2947,7 +2993,7 @@ TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, const return arena->addTypePack(TypePack{head, tail}); } -std::vector> ConstraintGraphBuilder::createGenerics( +std::vector> ConstraintGenerator::createGenerics( const ScopePtr& scope, AstArray generics, bool useCache, bool addTypes) { std::vector> result; @@ -2977,7 +3023,7 @@ std::vector> ConstraintGraphBuilder::crea return result; } -std::vector> ConstraintGraphBuilder::createGenericPacks( +std::vector> ConstraintGenerator::createGenericPacks( const ScopePtr& scope, AstArray generics, bool useCache, bool addTypes) { std::vector> result; @@ -3008,7 +3054,7 @@ std::vector> ConstraintGraphBuilder:: return result; } -Inference ConstraintGraphBuilder::flattenPack(const ScopePtr& scope, Location location, InferencePack pack) +Inference ConstraintGenerator::flattenPack(const ScopePtr& scope, Location location, InferencePack pack) { const auto& [tp, refinements] = pack; RefinementId refinement = nullptr; @@ -3025,7 +3071,7 @@ Inference ConstraintGraphBuilder::flattenPack(const ScopePtr& scope, Location lo return Inference{typeResult, refinement}; } -void ConstraintGraphBuilder::reportError(Location location, TypeErrorData err) +void ConstraintGenerator::reportError(Location location, TypeErrorData err) { errors.push_back(TypeError{location, module->name, std::move(err)}); @@ -3033,7 +3079,7 @@ void ConstraintGraphBuilder::reportError(Location location, TypeErrorData err) logger->captureGenerationError(errors.back()); } -void ConstraintGraphBuilder::reportCodeTooComplex(Location location) +void ConstraintGenerator::reportCodeTooComplex(Location location) { errors.push_back(TypeError{location, module->name, CodeTooComplex{}}); @@ -3069,7 +3115,7 @@ struct GlobalPrepopulator : AstVisitor } }; -void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) +void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) { GlobalPrepopulator gp{NotNull{globalScope.get()}, arena, dfg}; @@ -3079,7 +3125,7 @@ void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, program->visit(&gp); } -void ConstraintGraphBuilder::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) +void ConstraintGenerator::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) { for (const auto& [symbol, p] : inferredBindings) { @@ -3094,7 +3140,7 @@ void ConstraintGraphBuilder::fillInInferredBindings(const ScopePtr& globalScope, } } -std::vector> ConstraintGraphBuilder::getExpectedCallTypesForFunctionOverloads(const TypeId fnType) +std::vector> ConstraintGenerator::getExpectedCallTypesForFunctionOverloads(const TypeId fnType) { std::vector funTys; if (auto it = get(follow(fnType))) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 3b478494..c056a150 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -2,13 +2,11 @@ #include "Luau/Anyification.h" #include "Luau/ApplyTypeFunction.h" -#include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/ConstraintSolver.h" #include "Luau/DcrLogger.h" #include "Luau/Instantiation.h" #include "Luau/Location.h" -#include "Luau/Metamethods.h" #include "Luau/ModuleResolver.h" #include "Luau/Quantify.h" #include "Luau/Simplify.h" @@ -17,12 +15,11 @@ #include "Luau/Type.h" #include "Luau/TypeFamily.h" #include "Luau/TypeUtils.h" -#include "Luau/Unifier.h" #include "Luau/Unifier2.h" #include "Luau/VisitType.h" +#include LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); -LUAU_FASTFLAG(LuauFloorDivision); namespace Luau { @@ -1103,6 +1100,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNulllocation, addition)); + } + if (occursCheckPassed && c.callSite) (*c.astOverloadResolvedTypes)[c.callSite] = inferredTy; @@ -1490,7 +1493,7 @@ namespace */ struct FindRefineConstraintBlockers : TypeOnceVisitor { - std::unordered_set found; + DenseHashSet found{nullptr}; bool visit(TypeId ty, const BlockedType&) override { found.insert(ty); @@ -1905,15 +1908,16 @@ bool ConstraintSolver::tryDispatchIterableFunction( std::pair, std::optional> ConstraintSolver::lookupTableProp( TypeId subjectType, const std::string& propName, bool suppressSimplification) { - std::unordered_set seen; + DenseHashSet seen{nullptr}; return lookupTableProp(subjectType, propName, suppressSimplification, seen); } std::pair, std::optional> ConstraintSolver::lookupTableProp( - TypeId subjectType, const std::string& propName, bool suppressSimplification, std::unordered_set& seen) + TypeId subjectType, const std::string& propName, bool suppressSimplification, DenseHashSet& seen) { - if (!seen.insert(subjectType).second) + if (seen.contains(subjectType)) return {}; + seen.insert(subjectType); subjectType = follow(subjectType); @@ -2073,7 +2077,15 @@ bool ConstraintSolver::tryUnify(NotNull constraint, TID subTy, bool success = u2.unify(subTy, superTy); - if (!success) + if (success) + { + for (const auto& [expanded, additions] : u2.expandedFreeTypes) + { + for (TypeId addition : additions) + upperBoundContributors[expanded].push_back(std::make_pair(constraint->location, addition)); + } + } + else { // Unification only fails when doing so would fail the occurs check. // ie create a self-bound type or a cyclic type pack @@ -2320,6 +2332,12 @@ ErrorVec ConstraintSolver::unify(NotNull scope, Location location, TypePa u.unify(subPack, superPack); + for (const auto& [expanded, additions] : u.expandedFreeTypes) + { + for (TypeId addition : additions) + upperBoundContributors[expanded].push_back(std::make_pair(location, addition)); + } + unblock(subPack, Location{}); unblock(superPack, Location{}); diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index e3933574..60d95986 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -11,10 +11,13 @@ LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) +LUAU_FASTFLAG(LuauLoopControlFlowAnalysis) namespace Luau { +bool doesCallError(const AstExprCall* call); // TypeInfer.cpp + const RefinementKey* RefinementKeyArena::leaf(DefId def) { return allocator.allocate(RefinementKey{nullptr, def, std::nullopt}); @@ -34,7 +37,7 @@ DefId DataFlowGraph::getDef(const AstExpr* expr) const std::optional DataFlowGraph::getRValueDefForCompoundAssign(const AstExpr* expr) const { - auto def = compoundAssignBreadcrumbs.find(expr); + auto def = compoundAssignDefs.find(expr); return def ? std::optional(*def) : std::nullopt; } @@ -82,9 +85,9 @@ std::optional DfgScope::lookup(DefId def, const std::string& key) const { for (const DfgScope* current = this; current; current = current->parent) { - if (auto map = props.find(def)) + if (auto props = current->props.find(def)) { - if (auto it = map->find(key); it != map->end()) + if (auto it = props->find(key); it != props->end()) return NotNull{it->second}; } } @@ -92,6 +95,47 @@ std::optional DfgScope::lookup(DefId def, const std::string& key) const return std::nullopt; } +void DfgScope::inherit(const DfgScope* childScope) +{ + for (const auto& [k, a] : childScope->bindings) + { + if (lookup(k)) + bindings[k] = a; + } + + for (const auto& [k1, a1] : childScope->props) + { + for (const auto& [k2, a2] : a1) + props[k1][k2] = a2; + } +} + +bool DfgScope::canUpdateDefinition(Symbol symbol) const +{ + for (const DfgScope* current = this; current; current = current->parent) + { + if (current->bindings.find(symbol)) + return true; + else if (current->isLoopScope) + return false; + } + + return true; +} + +bool DfgScope::canUpdateDefinition(DefId def, const std::string& key) const +{ + for (const DfgScope* current = this; current; current = current->parent) + { + if (auto props = current->props.find(def)) + return true; + else if (current->isLoopScope) + return false; + } + + return true; +} + DataFlowGraph DataFlowGraphBuilder::build(AstStatBlock* block, NotNull handle) { LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution); @@ -110,24 +154,54 @@ DataFlowGraph DataFlowGraphBuilder::build(AstStatBlock* block, NotNullbindings) + { + if (auto def2 = b->bindings.find(sym)) + p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + else if (auto def2 = p->bindings.find(sym)) + p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + } + + for (const auto& [sym, def1] : b->bindings) + { + if (a->bindings.find(sym)) + continue; + else if (auto def2 = p->bindings.find(sym)) + p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + } +} + +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBlock* b) { DfgScope* child = childScope(scope); - return visitBlockWithoutChildScope(child, b); + ControlFlow cf = visitBlockWithoutChildScope(child, b); + scope->inherit(child); + return cf; } -void DataFlowGraphBuilder::visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b) +ControlFlow DataFlowGraphBuilder::visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b) { - for (AstStat* s : b->body) - visit(scope, s); + std::optional firstControlFlow; + for (AstStat* stat : b->body) + { + ControlFlow cf = visit(scope, stat); + if (cf != ControlFlow::None && !firstControlFlow) + firstControlFlow = cf; + } + + return firstControlFlow.value_or(ControlFlow::None); } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStat* s) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStat* s) { if (auto b = s->as()) return visit(scope, b); @@ -173,56 +247,85 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStat* s) handle->ice("Unknown AstStat in DataFlowGraphBuilder::visit"); } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatIf* i) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatIf* i) { - // TODO: type states and control flow analysis visitExpr(scope, i->condition); - visit(scope, i->thenbody); + + DfgScope* thenScope = childScope(scope); + DfgScope* elseScope = childScope(scope); + + ControlFlow thencf = visit(thenScope, i->thenbody); + ControlFlow elsecf = ControlFlow::None; if (i->elsebody) - visit(scope, i->elsebody); + elsecf = visit(elseScope, i->elsebody); + + if (thencf != ControlFlow::None && elsecf == ControlFlow::None) + join(scope, scope, elseScope); + else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) + join(scope, thenScope, scope); + else if ((thencf | elsecf) == ControlFlow::None) + join(scope, thenScope, elseScope); + + if (FFlag::LuauLoopControlFlowAnalysis && thencf == elsecf) + return thencf; + else if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + return ControlFlow::Returns; + else + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatWhile* w) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatWhile* w) { // TODO(controlflow): entry point has a back edge from exit point - DfgScope* whileScope = childScope(scope); + DfgScope* whileScope = childScope(scope, /*isLoopScope=*/true); visitExpr(whileScope, w->condition); visit(whileScope, w->body); + + scope->inherit(whileScope); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatRepeat* r) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatRepeat* r) { // TODO(controlflow): entry point has a back edge from exit point - DfgScope* repeatScope = childScope(scope); // TODO: loop scope. + DfgScope* repeatScope = childScope(scope, /*isLoopScope=*/true); visitBlockWithoutChildScope(repeatScope, r->body); visitExpr(repeatScope, r->condition); + + scope->inherit(repeatScope); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBreak* b) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBreak* b) { - // TODO: Control flow analysis - return; // ok + return ControlFlow::Breaks; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatContinue* c) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatContinue* c) { - // TODO: Control flow analysis - return; // ok + return ControlFlow::Continues; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatReturn* r) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatReturn* r) { - // TODO: Control flow analysis for (AstExpr* e : r->list) visitExpr(scope, e); + + return ControlFlow::Returns; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatExpr* e) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatExpr* e) { visitExpr(scope, e->expr); + if (auto call = e->expr->as(); call && doesCallError(call)) + return ControlFlow::Throws; + else + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l) { // We're gonna need a `visitExprList` and `visitVariadicExpr` (function calls and `...`) std::vector defs; @@ -243,11 +346,13 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l) graph.localDefs[local] = def; scope->bindings[local] = def; } + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f) { - DfgScope* forScope = childScope(scope); // TODO: loop scope. + DfgScope* forScope = childScope(scope, /*isLoopScope=*/true); visitExpr(scope, f->from); visitExpr(scope, f->to); @@ -263,11 +368,15 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f) // TODO(controlflow): entry point has a back edge from exit point visit(forScope, f->body); + + scope->inherit(forScope); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) { - DfgScope* forScope = childScope(scope); // TODO: loop scope. + DfgScope* forScope = childScope(scope, /*isLoopScope=*/true); for (AstLocal* local : f->vars) { @@ -285,9 +394,13 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) visitExpr(forScope, e); visit(forScope, f->body); + + scope->inherit(forScope); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a) { std::vector defs; defs.reserve(a->values.size); @@ -299,9 +412,11 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a) AstExpr* v = a->vars.data[i]; visitLValue(scope, v, i < defs.size() ? defs[i] : defArena->freshCell()); } + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) { // TODO: This needs revisiting because this is incorrect. The `c->var` part is both being read and written to, // but the `c->var` only has one pointer address, so we need to come up with a way to store both. @@ -312,9 +427,11 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) // We can't just visit `c->var` as a rvalue and then separately traverse `c->var` as an lvalue, since that's O(n^2). DefId def = visitExpr(scope, c->value).def; visitLValue(scope, c->var, def, /* isCompoundAssignment */ true); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) { // In the old solver, we assumed that the name of the function is always a function in the body // but this isn't true, e.g. the following example will print `5`, not a function address. @@ -329,34 +446,42 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) DefId prototype = defArena->freshCell(); visitLValue(scope, f->name, prototype); visitExpr(scope, f->func); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l) { DefId def = defArena->freshCell(); graph.localDefs[l->name] = def; scope->bindings[l->name] = def; visitExpr(scope, l->func); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t) { DfgScope* unreachable = childScope(scope); visitGenerics(unreachable, t->generics); visitGenericPacks(unreachable, t->genericPacks); visitType(unreachable, t->type); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareGlobal* d) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareGlobal* d) { DefId def = defArena->freshCell(); graph.declaredDefs[d] = def; scope->bindings[d->name] = def; visitType(scope, d->type); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareFunction* d) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareFunction* d) { DefId def = defArena->freshCell(); graph.declaredDefs[d] = def; @@ -367,9 +492,11 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareFunction* d) visitGenericPacks(unreachable, d->genericPacks); visitTypeList(unreachable, d->params); visitTypeList(unreachable, d->retTypes); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareClass* d) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareClass* d) { // This declaration does not "introduce" any bindings in value namespace, // so there's no symbolic value to begin with. We'll traverse the properties @@ -377,19 +504,30 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareClass* d) DfgScope* unreachable = childScope(scope); for (AstDeclaredClassProp prop : d->props) visitType(unreachable, prop.ty); + + return ControlFlow::None; } -void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatError* error) +ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatError* error) { DfgScope* unreachable = childScope(scope); for (AstStat* s : error->statements) visit(unreachable, s); for (AstExpr* e : error->expressions) visitExpr(unreachable, e); + + return ControlFlow::None; } DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExpr* e) { + // Some subexpressions could be visited two times. If we've already seen it, just extract it. + if (auto def = graph.astDefs.find(e)) + { + auto key = graph.astRefinementKeys.find(e); + return {NotNull{*def}, key ? *key : nullptr}; + } + auto go = [&]() -> DataFlowResult { if (auto g = e->as()) return visitExpr(scope, g); @@ -481,11 +619,14 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexName auto [parentDef, parentKey] = visitExpr(scope, i->expr); std::string index = i->index.value; - auto& propDef = moduleScope->props[parentDef][index]; - if (!propDef) - propDef = defArena->freshCell(); - - return {NotNull{propDef}, keyArena->node(parentKey, NotNull{propDef}, index)}; + if (auto propDef = scope->lookup(parentDef, index)) + return {*propDef, keyArena->node(parentKey, *propDef, index)}; + else + { + DefId def = defArena->freshCell(); + scope->props[parentDef][index] = def; + return {def, keyArena->node(parentKey, def, index)}; + } } DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i) @@ -496,11 +637,14 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr if (auto string = i->index->as()) { std::string index{string->value.data, string->value.size}; - auto& propDef = moduleScope->props[parentDef][index]; - if (!propDef) - propDef = defArena->freshCell(); - - return {NotNull{propDef}, keyArena->node(parentKey, NotNull{propDef}, index)}; + if (auto propDef = scope->lookup(parentDef, index)) + return {*propDef, keyArena->node(parentKey, *propDef, index)}; + else + { + DefId def = defArena->freshCell(); + scope->props[parentDef][index] = def; + return {def, keyArena->node(parentKey, def, index)}; + } } return {defArena->freshCell(/* subscripted= */true), nullptr}; @@ -628,41 +772,56 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomi void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment) { - // We need to keep the previous breadcrumb around for a compound assignment. + // We need to keep the previous def around for a compound assignment. if (isCompoundAssignment) { if (auto def = scope->lookup(l->local)) - graph.compoundAssignBreadcrumbs[l] = *def; + graph.compoundAssignDefs[l] = *def; } // In order to avoid alias tracking, we need to clip the reference to the parent def. - DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); - graph.astDefs[l] = updated; - scope->bindings[l->local] = updated; + if (scope->canUpdateDefinition(l->local)) + { + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[l] = updated; + scope->bindings[l->local] = updated; + } + else + visitExpr(scope, static_cast(l)); } void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment) { - // We need to keep the previous breadcrumb around for a compound assignment. + // We need to keep the previous def around for a compound assignment. if (isCompoundAssignment) { if (auto def = scope->lookup(g->name)) - graph.compoundAssignBreadcrumbs[g] = *def; + graph.compoundAssignDefs[g] = *def; } // In order to avoid alias tracking, we need to clip the reference to the parent def. - DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); - graph.astDefs[g] = updated; - scope->bindings[g->name] = updated; + if (scope->canUpdateDefinition(g->name)) + { + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[g] = updated; + scope->bindings[g->name] = updated; + } + else + visitExpr(scope, static_cast(g)); } void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef) { DefId parentDef = visitExpr(scope, i->expr).def; - DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); - graph.astDefs[i] = updated; - scope->props[parentDef][i->index.value] = updated; + if (scope->canUpdateDefinition(parentDef, i->index.value)) + { + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[i] = updated; + scope->props[parentDef][i->index.value] = updated; + } + else + visitExpr(scope, static_cast(i)); } void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef) @@ -672,9 +831,14 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, Def if (auto string = i->index->as()) { - DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); - graph.astDefs[i] = updated; - scope->props[parentDef][string->value.data] = updated; + if (scope->canUpdateDefinition(parentDef, string->value.data)) + { + DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); + graph.astDefs[i] = updated; + scope->props[parentDef][string->value.data] = updated; + } + else + visitExpr(scope, static_cast(i)); } graph.astDefs[i] = defArena->freshCell(); diff --git a/Analysis/src/Def.cpp b/Analysis/src/Def.cpp index d34b5cdc..fdbc089f 100644 --- a/Analysis/src/Def.cpp +++ b/Analysis/src/Def.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/Def.h" #include "Luau/Common.h" +#include "Luau/DenseHash.h" + +#include namespace Luau { @@ -9,8 +12,27 @@ bool containsSubscriptedDefinition(DefId def) { if (auto cell = get(def)) return cell->subscripted; + else if (auto phi = get(def)) + { + std::deque queue(begin(phi->operands), end(phi->operands)); + DenseHashSet seen{nullptr}; - LUAU_ASSERT(!"Phi nodes not implemented yet"); + while (!queue.empty()) + { + DefId next = queue.front(); + queue.pop_front(); + + LUAU_ASSERT(!seen.find(next)); + if (seen.find(next)) + continue; + seen.insert(next); + + if (auto cell_ = get(next); cell_ && cell_->subscripted) + return true; + else if (auto phi_ = get(next)) + queue.insert(queue.end(), phi_->operands.begin(), phi_->operands.end()); + } + } return false; } @@ -19,4 +41,12 @@ DefId DefArena::freshCell(bool subscripted) return NotNull{allocator.allocate(Def{Cell{subscripted}})}; } +DefId DefArena::phi(DefId a, DefId b) +{ + if (a == b) + return a; + else + return NotNull{allocator.allocate(Def{Phi{{a, b}}})}; +} + } // namespace Luau diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index 632f8e5c..874f8627 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -2,11 +2,12 @@ #include "Luau/BuiltinDefinitions.h" LUAU_FASTFLAGVARIABLE(LuauBufferDefinitions, false) +LUAU_FASTFLAGVARIABLE(LuauBufferTypeck, false) namespace Luau { -static const std::string kBuiltinDefinitionBufferSrc = R"BUILTIN_SRC( +static const std::string kBuiltinDefinitionBufferSrc_DEPRECATED = R"BUILTIN_SRC( -- TODO: this will be replaced with a built-in primitive type declare class buffer end @@ -40,6 +41,36 @@ declare buffer: { )BUILTIN_SRC"; +static const std::string kBuiltinDefinitionBufferSrc = R"BUILTIN_SRC( + +declare buffer: { + create: (size: number) -> buffer, + fromstring: (str: string) -> buffer, + tostring: (b: buffer) -> string, + len: (b: buffer) -> number, + copy: (target: buffer, targetOffset: number, source: buffer, sourceOffset: number?, count: number?) -> (), + fill: (b: buffer, offset: number, value: number, count: number?) -> (), + readi8: (b: buffer, offset: number) -> number, + readu8: (b: buffer, offset: number) -> number, + readi16: (b: buffer, offset: number) -> number, + readu16: (b: buffer, offset: number) -> number, + readi32: (b: buffer, offset: number) -> number, + readu32: (b: buffer, offset: number) -> number, + readf32: (b: buffer, offset: number) -> number, + readf64: (b: buffer, offset: number) -> number, + writei8: (b: buffer, offset: number, value: number) -> (), + writeu8: (b: buffer, offset: number, value: number) -> (), + writei16: (b: buffer, offset: number, value: number) -> (), + writeu16: (b: buffer, offset: number, value: number) -> (), + writei32: (b: buffer, offset: number, value: number) -> (), + writeu32: (b: buffer, offset: number, value: number) -> (), + writef32: (b: buffer, offset: number, value: number) -> (), + writef64: (b: buffer, offset: number, value: number) -> (), + readstring: (b: buffer, offset: number, count: number) -> string, + writestring: (b: buffer, offset: number, value: string, count: number?) -> (), +} + +)BUILTIN_SRC"; static const std::string kBuiltinDefinitionLuaSrc = R"BUILTIN_SRC( declare bit32: { @@ -236,8 +267,10 @@ std::string getBuiltinDefinitionSource() { std::string result = kBuiltinDefinitionLuaSrc; - if (FFlag::LuauBufferDefinitions) + if (FFlag::LuauBufferTypeck) result = kBuiltinDefinitionBufferSrc + result; + else if (FFlag::LuauBufferDefinitions) + result = kBuiltinDefinitionBufferSrc_DEPRECATED + result; return result; } diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index db6a240a..3be63f02 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -490,7 +490,12 @@ struct ErrorConverter std::string operator()(const TypePackMismatch& e) const { - return "Type pack '" + toString(e.givenTp) + "' could not be converted into '" + toString(e.wantedTp) + "'"; + std::string ss = "Type pack '" + toString(e.givenTp) + "' could not be converted into '" + toString(e.wantedTp) + "'"; + + if (!e.reason.empty()) + ss += "; " + e.reason; + + return ss; } std::string operator()(const DynamicPropertyLookupOnClassesUnsafe& e) const diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 6cbc19fa..710e3699 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -5,7 +5,7 @@ #include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/Config.h" -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/ConstraintSolver.h" #include "Luau/DataFlowGraph.h" #include "Luau/DcrLogger.h" @@ -251,7 +251,7 @@ namespace static ErrorVec accumulateErrors( const std::unordered_map>& sourceNodes, ModuleResolver& moduleResolver, const ModuleName& name) { - std::unordered_set seen; + DenseHashSet seen{{}}; std::vector queue{name}; ErrorVec result; @@ -261,7 +261,7 @@ static ErrorVec accumulateErrors( ModuleName next = std::move(queue.back()); queue.pop_back(); - if (seen.count(next)) + if (seen.contains(next)) continue; seen.insert(next); @@ -442,7 +442,7 @@ CheckResult Frontend::check(const ModuleName& name, std::optional buildQueue; bool cycleDetected = parseGraph(buildQueue, name, frontendOptions.forAutocomplete); - std::unordered_set seen; + DenseHashSet seen{{}}; std::vector buildQueueItems; addBuildQueueItems(buildQueueItems, buildQueue, cycleDetected, seen, frontendOptions); LUAU_ASSERT(!buildQueueItems.empty()); @@ -495,12 +495,12 @@ std::vector Frontend::checkQueuedModules(std::optional currModuleQueue; std::swap(currModuleQueue, moduleQueue); - std::unordered_set seen; + DenseHashSet seen{{}}; std::vector buildQueueItems; for (const ModuleName& name : currModuleQueue) { - if (seen.count(name)) + if (seen.contains(name)) continue; if (!isDirty(name, frontendOptions.forAutocomplete)) @@ -511,7 +511,7 @@ std::vector Frontend::checkQueuedModules(std::optional queue; bool cycleDetected = parseGraph(queue, name, frontendOptions.forAutocomplete, [&seen](const ModuleName& name) { - return seen.count(name); + return seen.contains(name); }); addBuildQueueItems(buildQueueItems, queue, cycleDetected, seen, frontendOptions); @@ -836,11 +836,11 @@ bool Frontend::parseGraph( } void Frontend::addBuildQueueItems(std::vector& items, std::vector& buildQueue, bool cycleDetected, - std::unordered_set& seen, const FrontendOptions& frontendOptions) + DenseHashSet& seen, const FrontendOptions& frontendOptions) { for (const ModuleName& moduleName : buildQueue) { - if (seen.count(moduleName)) + if (seen.contains(moduleName)) continue; seen.insert(moduleName); @@ -1048,6 +1048,7 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) module->astResolvedTypes.clear(); module->astResolvedTypePacks.clear(); module->astScopes.clear(); + module->upperBoundContributors.clear(); if (!FFlag::DebugLuauDeferredConstraintResolution) module->scopes.clear(); @@ -1255,13 +1256,13 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vectorinternalTypes, builtinTypes, NotNull{&unifierState}}; - ConstraintGraphBuilder cgb{result, NotNull{&normalizer}, moduleResolver, builtinTypes, iceHandler, parentScope, std::move(prepareModuleScope), + ConstraintGenerator cg{result, NotNull{&normalizer}, moduleResolver, builtinTypes, iceHandler, parentScope, std::move(prepareModuleScope), logger.get(), NotNull{&dfg}, requireCycles}; - cgb.visitModuleRoot(sourceModule.root); - result->errors = std::move(cgb.errors); + cg.visitModuleRoot(sourceModule.root); + result->errors = std::move(cg.errors); - ConstraintSolver cs{NotNull{&normalizer}, NotNull(cgb.rootScope), borrowConstraints(cgb.constraints), result->humanReadableName, moduleResolver, + ConstraintSolver cs{NotNull{&normalizer}, NotNull(cg.rootScope), borrowConstraints(cg.constraints), result->humanReadableName, moduleResolver, requireCycles, logger.get(), limits}; if (options.randomizeConstraintResolutionSeed) @@ -1283,8 +1284,9 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vectorerrors.emplace_back(std::move(e)); - result->scopes = std::move(cgb.scopes); + result->scopes = std::move(cg.scopes); result->type = sourceModule.type; + result->upperBoundContributors = std::move(cs.upperBoundContributors); result->clonePublicInterface(builtinTypes, *iceHandler); diff --git a/Analysis/src/GlobalTypes.cpp b/Analysis/src/GlobalTypes.cpp index 654cfa5d..1c96ad70 100644 --- a/Analysis/src/GlobalTypes.cpp +++ b/Analysis/src/GlobalTypes.cpp @@ -3,6 +3,7 @@ #include "Luau/GlobalTypes.h" LUAU_FASTFLAG(LuauInitializeStringMetatableInGlobalTypes) +LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau { @@ -18,6 +19,8 @@ GlobalTypes::GlobalTypes(NotNull builtinTypes) globalScope->addBuiltinTypeBinding("string", TypeFun{{}, builtinTypes->stringType}); globalScope->addBuiltinTypeBinding("boolean", TypeFun{{}, builtinTypes->booleanType}); globalScope->addBuiltinTypeBinding("thread", TypeFun{{}, builtinTypes->threadType}); + if (FFlag::LuauBufferTypeck) + globalScope->addBuiltinTypeBinding("buffer", TypeFun{{}, builtinTypes->bufferType}); globalScope->addBuiltinTypeBinding("unknown", TypeFun{{}, builtinTypes->unknownType}); globalScope->addBuiltinTypeBinding("never", TypeFun{{}, builtinTypes->neverType}); diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index e957eee7..ea35a6a1 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -14,8 +14,7 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) -LUAU_FASTFLAGVARIABLE(LuauLintDeprecatedFenv, false) -LUAU_FASTFLAGVARIABLE(LuauLintTableIndexer, false) +LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau { @@ -1108,7 +1107,7 @@ private: TypeKind getTypeKind(const std::string& name) { if (name == "nil" || name == "boolean" || name == "userdata" || name == "number" || name == "string" || name == "table" || - name == "function" || name == "thread") + name == "function" || name == "thread" || (FFlag::LuauBufferTypeck && name == "buffer")) return Kind_Primitive; if (name == "vector") @@ -2093,7 +2092,7 @@ private: // getfenv/setfenv are deprecated, however they are still used in some test frameworks and don't have a great general replacement // for now we warn about the deprecation only when they are used with a numeric first argument; this produces fewer warnings and makes use // of getfenv/setfenv a little more localized - if (FFlag::LuauLintDeprecatedFenv && !node->self && node->args.size >= 1) + if (!node->self && node->args.size >= 1) { if (AstExprGlobal* fenv = node->func->as(); fenv && (fenv->name == "getfenv" || fenv->name == "setfenv")) { @@ -2185,7 +2184,7 @@ private: bool visit(AstExprUnary* node) override { - if (FFlag::LuauLintTableIndexer && node->op == AstExprUnary::Len) + if (node->op == AstExprUnary::Len) checkIndexer(node, node->expr, "#"); return true; @@ -2195,7 +2194,7 @@ private: { if (AstExprGlobal* func = node->func->as()) { - if (FFlag::LuauLintTableIndexer && func->name == "ipairs" && node->args.size == 1) + if (func->name == "ipairs" && node->args.size == 1) checkIndexer(node, node->args.data[0], "ipairs"); } else if (AstExprIndexName* func = node->func->as()) @@ -2209,8 +2208,6 @@ private: void checkIndexer(AstExpr* node, AstExpr* expr, const char* op) { - LUAU_ASSERT(FFlag::LuauLintTableIndexer); - std::optional ty = context->getType(expr); if (!ty) return; @@ -2220,7 +2217,8 @@ private: return; if (!tty->indexer && !tty->props.empty() && tty->state != TableState::Generic) - emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table without an array part is likely a bug", op); + emitWarning( + *context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table without an array part is likely a bug", op); else if (tty->indexer && isString(tty->indexer->indexType)) // note: to avoid complexity of subtype tests we just check if the key is a string emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table with string keys is likely a bug", op); } @@ -2653,13 +2651,17 @@ private: case ConstantNumberParseResult::Ok: case ConstantNumberParseResult::Malformed: break; + case ConstantNumberParseResult::Imprecise: + emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, + "Number literal exceeded available precision and was truncated to closest representable number"); + break; case ConstantNumberParseResult::BinOverflow: emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, - "Binary number literal exceeded available precision and has been truncated to 2^64"); + "Binary number literal exceeded available precision and was truncated to 2^64"); break; case ConstantNumberParseResult::HexOverflow: emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, - "Hexadecimal number literal exceeded available precision and has been truncated to 2^64"); + "Hexadecimal number literal exceeded available precision and was truncated to 2^64"); break; } diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index 580f59f3..d50719a9 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -3,7 +3,7 @@ #include "Luau/Clone.h" #include "Luau/Common.h" -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/Normalize.h" #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 595794a0..5ff782ea 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -3,7 +3,9 @@ #include "Luau/Ast.h" #include "Luau/Common.h" +#include "Luau/Simplify.h" #include "Luau/Type.h" +#include "Luau/Simplify.h" #include "Luau/Subtyping.h" #include "Luau/Normalize.h" #include "Luau/Error.h" @@ -64,14 +66,43 @@ struct NonStrictContext NonStrictContext(NonStrictContext&&) = default; NonStrictContext& operator=(NonStrictContext&&) = default; - void unionContexts(const NonStrictContext& other) + static NonStrictContext disjunction( + NotNull builtinTypes, NotNull arena, const NonStrictContext& left, const NonStrictContext& right) { - // TODO: unimplemented + // disjunction implements union over the domain of keys + // if the default value for a defId not in the map is `never` + // then never | T is T + NonStrictContext disj{}; + + for (auto [def, leftTy] : left.context) + { + if (std::optional rightTy = right.find(def)) + disj.context[def] = simplifyUnion(builtinTypes, arena, leftTy, *rightTy).result; + else + disj.context[def] = leftTy; + } + + for (auto [def, rightTy] : right.context) + { + if (!right.find(def).has_value()) + disj.context[def] = rightTy; + } + + return disj; } - void intersectContexts(const NonStrictContext& other) + static NonStrictContext conjunction( + NotNull builtins, NotNull arena, const NonStrictContext& left, const NonStrictContext& right) { - // TODO: unimplemented + NonStrictContext conj{}; + + for (auto [def, leftTy] : left.context) + { + if (std::optional rightTy = right.find(def)) + conj.context[def] = simplifyIntersection(builtins, arena, leftTy, *rightTy).result; + } + + return conj; } void removeFromContext(const std::vector& defs) @@ -82,6 +113,12 @@ struct NonStrictContext std::optional find(const DefId& def) const { const Def* d = def.get(); + return find(d); + } + +private: + std::optional find(const Def* d) const + { auto it = context.find(d); if (it != context.end()) return {it->second}; @@ -180,153 +217,260 @@ struct NonStrictTypeChecker return builtinTypes->anyType; } - - void visit(AstStat* stat) - { - NonStrictContext fresh{}; - visit(stat, fresh); - } - - void visit(AstStat* stat, NonStrictContext& context) + NonStrictContext visit(AstStat* stat) { auto pusher = pushStack(stat); if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else if (auto s = stat->as()) - return visit(s, context); + return visit(s); else - LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown node type"); + { + LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown statement type"); + ice->ice("NonStrictTypeChecker encountered an unknown statement type"); + } } - void visit(AstStatBlock* block, NonStrictContext& context) + NonStrictContext visit(AstStatBlock* block) { auto StackPusher = pushStack(block); for (AstStat* statement : block->body) - visit(statement, context); + visit(statement); + return {}; } - void visit(AstStatIf* ifStatement, NonStrictContext& context) {} - void visit(AstStatWhile* whileStatement, NonStrictContext& context) {} - void visit(AstStatRepeat* repeatStatement, NonStrictContext& context) {} - void visit(AstStatBreak* breakStatement, NonStrictContext& context) {} - void visit(AstStatContinue* continueStatement, NonStrictContext& context) {} - void visit(AstStatReturn* returnStatement, NonStrictContext& context) {} - void visit(AstStatExpr* expr, NonStrictContext& context) + NonStrictContext visit(AstStatIf* ifStatement) { - visit(expr->expr, context); + NonStrictContext condB = visit(ifStatement->condition); + NonStrictContext thenB = visit(ifStatement->thenbody); + NonStrictContext elseB = visit(ifStatement->elsebody); + return NonStrictContext::disjunction( + builtinTypes, NotNull{&arena}, condB, NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenB, elseB)); } - void visit(AstStatLocal* local, NonStrictContext& context) {} - void visit(AstStatFor* forStatement, NonStrictContext& context) {} - void visit(AstStatForIn* forInStatement, NonStrictContext& context) {} - void visit(AstStatAssign* assign, NonStrictContext& context) {} - void visit(AstStatCompoundAssign* compoundAssign, NonStrictContext& context) {} - void visit(AstStatFunction* statFn, NonStrictContext& context) {} - void visit(AstStatLocalFunction* localFn, NonStrictContext& context) {} - void visit(AstStatTypeAlias* typeAlias, NonStrictContext& context) {} - void visit(AstStatDeclareFunction* declFn, NonStrictContext& context) {} - void visit(AstStatDeclareGlobal* declGlobal, NonStrictContext& context) {} - void visit(AstStatDeclareClass* declClass, NonStrictContext& context) {} - void visit(AstStatError* error, NonStrictContext& context) {} - void visit(AstExpr* expr, NonStrictContext& context) + NonStrictContext visit(AstStatWhile* whileStatement) + { + return {}; + } + + NonStrictContext visit(AstStatRepeat* repeatStatement) + { + return {}; + } + + NonStrictContext visit(AstStatBreak* breakStatement) + { + return {}; + } + + NonStrictContext visit(AstStatContinue* continueStatement) + { + return {}; + } + + NonStrictContext visit(AstStatReturn* returnStatement) + { + return {}; + } + + NonStrictContext visit(AstStatExpr* expr) + { + return visit(expr->expr); + } + + NonStrictContext visit(AstStatLocal* local) + { + return {}; + } + + NonStrictContext visit(AstStatFor* forStatement) + { + return {}; + } + + NonStrictContext visit(AstStatForIn* forInStatement) + { + return {}; + } + + NonStrictContext visit(AstStatAssign* assign) + { + return {}; + } + + NonStrictContext visit(AstStatCompoundAssign* compoundAssign) + { + return {}; + } + + NonStrictContext visit(AstStatFunction* statFn) + { + return {}; + } + + NonStrictContext visit(AstStatLocalFunction* localFn) + { + return {}; + } + + NonStrictContext visit(AstStatTypeAlias* typeAlias) + { + return {}; + } + + NonStrictContext visit(AstStatDeclareFunction* declFn) + { + return {}; + } + + NonStrictContext visit(AstStatDeclareGlobal* declGlobal) + { + return {}; + } + + NonStrictContext visit(AstStatDeclareClass* declClass) + { + return {}; + } + + NonStrictContext visit(AstStatError* error) + { + return {}; + } + + NonStrictContext visit(AstExpr* expr) { auto pusher = pushStack(expr); if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else if (auto e = expr->as()) - return visit(e, context); + return visit(e); else + { LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown expression type"); + ice->ice("NonStrictTypeChecker encountered an unknown expression type"); + } } - void visit(AstExprGroup* group, NonStrictContext& context) {} - void visit(AstExprConstantNil* expr, NonStrictContext& context) {} - void visit(AstExprConstantBool* expr, NonStrictContext& context) {} - void visit(AstExprConstantNumber* expr, NonStrictContext& context) {} - void visit(AstExprConstantString* expr, NonStrictContext& context) {} - void visit(AstExprLocal* local, NonStrictContext& context) {} - void visit(AstExprGlobal* global, NonStrictContext& context) {} - void visit(AstExprVarargs* global, NonStrictContext& context) {} - - void visit(AstExprCall* call, NonStrictContext& context) + NonStrictContext visit(AstExprGroup* group) { + return {}; + } + + NonStrictContext visit(AstExprConstantNil* expr) + { + return {}; + } + + NonStrictContext visit(AstExprConstantBool* expr) + { + return {}; + } + + NonStrictContext visit(AstExprConstantNumber* expr) + { + return {}; + } + + NonStrictContext visit(AstExprConstantString* expr) + { + return {}; + } + + NonStrictContext visit(AstExprLocal* local) + { + return {}; + } + + NonStrictContext visit(AstExprGlobal* global) + { + return {}; + } + + NonStrictContext visit(AstExprVarargs* global) + { + return {}; + } + + + NonStrictContext visit(AstExprCall* call) + { + NonStrictContext fresh{}; TypeId* originalCallTy = module->astOriginalCallTypes.find(call); if (!originalCallTy) - return; + return fresh; TypeId fnTy = *originalCallTy; - // TODO: how should we link this to the passed in context here - NonStrictContext fresh{}; if (auto fn = get(follow(fnTy))) { if (fn->isCheckedFunction) @@ -369,21 +513,64 @@ struct NonStrictTypeChecker } } } + + return fresh; } - void visit(AstExprIndexName* indexName, NonStrictContext& context) {} - void visit(AstExprIndexExpr* indexExpr, NonStrictContext& context) {} - void visit(AstExprFunction* exprFn, NonStrictContext& context) + NonStrictContext visit(AstExprIndexName* indexName) + { + return {}; + } + + NonStrictContext visit(AstExprIndexExpr* indexExpr) + { + return {}; + } + + NonStrictContext visit(AstExprFunction* exprFn) { auto pusher = pushStack(exprFn); + return {}; + } + + NonStrictContext visit(AstExprTable* table) + { + return {}; + } + + NonStrictContext visit(AstExprUnary* unary) + { + return {}; + } + + NonStrictContext visit(AstExprBinary* binary) + { + return {}; + } + + NonStrictContext visit(AstExprTypeAssertion* typeAssertion) + { + return {}; + } + + NonStrictContext visit(AstExprIfElse* ifElse) + { + NonStrictContext condB = visit(ifElse->condition); + NonStrictContext thenB = visit(ifElse->trueExpr); + NonStrictContext elseB = visit(ifElse->falseExpr); + return NonStrictContext::disjunction( + builtinTypes, NotNull{&arena}, condB, NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenB, elseB)); + } + + NonStrictContext visit(AstExprInterpString* interpString) + { + return {}; + } + + NonStrictContext visit(AstExprError* error) + { + return {}; } - void visit(AstExprTable* table, NonStrictContext& context) {} - void visit(AstExprUnary* unary, NonStrictContext& context) {} - void visit(AstExprBinary* binary, NonStrictContext& context) {} - void visit(AstExprTypeAssertion* typeAssertion, NonStrictContext& context) {} - void visit(AstExprIfElse* ifElse, NonStrictContext& context) {} - void visit(AstExprInterpString* interpString, NonStrictContext& context) {} - void visit(AstExprError* error, NonStrictContext& context) {} void reportError(TypeErrorData data, const Location& location) { diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 52bbc5d9..9f14b355 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -8,7 +8,10 @@ #include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/RecursionCounter.h" +#include "Luau/Set.h" +#include "Luau/Subtyping.h" #include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/Unifier.h" LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant, false) @@ -16,9 +19,10 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant, false) // This could theoretically be 2000 on amd64, but x86 requires this. LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200); LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000); -LUAU_FASTFLAGVARIABLE(LuauNormalizeCyclicUnions, false); LUAU_FASTFLAG(LuauTransitiveSubtyping) LUAU_FASTFLAG(DebugLuauReadWriteProperties) +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) +LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau { @@ -32,9 +36,14 @@ TypeIds::TypeIds(std::initializer_list tys) void TypeIds::insert(TypeId ty) { ty = follow(ty); - auto [_, fresh] = types.insert(ty); - if (fresh) + + // get a reference to the slot for `ty` in `types` + bool& entry = types[ty]; + + // if `ty` is fresh, we can set it to `true`, add it to the order and hash and be done. + if (!entry) { + entry = true; order.push_back(ty); hash ^= std::hash{}(ty); } @@ -75,25 +84,26 @@ TypeIds::const_iterator TypeIds::end() const TypeIds::iterator TypeIds::erase(TypeIds::const_iterator it) { TypeId ty = *it; - types.erase(ty); + types[ty] = false; hash ^= std::hash{}(ty); return order.erase(it); } size_t TypeIds::size() const { - return types.size(); + return order.size(); } bool TypeIds::empty() const { - return types.empty(); + return order.empty(); } size_t TypeIds::count(TypeId ty) const { ty = follow(ty); - return types.count(ty); + const bool* val = types.find(ty); + return (val && *val) ? 1 : 0; } void TypeIds::retain(const TypeIds& there) @@ -122,7 +132,29 @@ bool TypeIds::isNever() const bool TypeIds::operator==(const TypeIds& there) const { - return hash == there.hash && types == there.types; + // we can early return if the hashes don't match. + if (hash != there.hash) + return false; + + // we have to check equality of the sets themselves if not. + + // if the sets are unequal sizes, then they cannot possibly be equal. + // it is important to use `order` here and not `types` since the mappings + // may have different sizes since removal is not possible, and so erase + // simply writes `false` into the map. + if (order.size() != there.order.size()) + return false; + + // otherwise, we'll need to check that every element we have here is in `there`. + for (auto ty : order) + { + // if it's not, we'll return `false` + if (there.count(ty) == 0) + return false; + } + + // otherwise, we've proven the two equal! + return true; } NormalizedStringType::NormalizedStringType() {} @@ -237,19 +269,56 @@ NormalizedType::NormalizedType(NotNull builtinTypes) , numbers(builtinTypes->neverType) , strings{NormalizedStringType::never} , threads(builtinTypes->neverType) + , buffers(builtinTypes->neverType) { } +bool NormalizedType::isUnknown() const +{ + if (get(tops)) + return true; + + // Otherwise, we can still be unknown! + bool hasAllPrimitives = isPrim(booleans, PrimitiveType::Boolean) && isPrim(nils, PrimitiveType::NilType) && isNumber(numbers) && + strings.isString() && isPrim(threads, PrimitiveType::Thread) && isThread(threads); + + // Check is class + bool isTopClass = false; + for (auto [t, disj] : classes.classes) + { + if (auto ct = get(t)) + { + if (ct->name == "class" && disj.empty()) + { + isTopClass = true; + break; + } + } + } + // Check is table + bool isTopTable = false; + for (auto t : tables) + { + if (isPrim(t, PrimitiveType::Table)) + { + isTopTable = true; + break; + } + } + // any = unknown or error ==> we need to make sure we have all the unknown components, but not errors + return get(errors) && hasAllPrimitives && isTopClass && isTopTable && functions.isTop; +} + bool NormalizedType::isExactlyNumber() const { return hasNumbers() && !hasTops() && !hasBooleans() && !hasClasses() && !hasErrors() && !hasNils() && !hasStrings() && !hasThreads() && - !hasTables() && !hasFunctions() && !hasTyvars(); + (!FFlag::LuauBufferTypeck || !hasBuffers()) && !hasTables() && !hasFunctions() && !hasTyvars(); } bool NormalizedType::isSubtypeOfString() const { return hasStrings() && !hasTops() && !hasBooleans() && !hasClasses() && !hasErrors() && !hasNils() && !hasNumbers() && !hasThreads() && - !hasTables() && !hasFunctions() && !hasTyvars(); + (!FFlag::LuauBufferTypeck || !hasBuffers()) && !hasTables() && !hasFunctions() && !hasTyvars(); } bool NormalizedType::shouldSuppressErrors() const @@ -306,6 +375,12 @@ bool NormalizedType::hasThreads() const return !get(threads); } +bool NormalizedType::hasBuffers() const +{ + LUAU_ASSERT(FFlag::LuauBufferTypeck); + return !get(buffers); +} + bool NormalizedType::hasTables() const { return !tables.isNever(); @@ -326,18 +401,18 @@ static bool isShallowInhabited(const NormalizedType& norm) // This test is just a shallow check, for example it returns `true` for `{ p : never }` return !get(norm.tops) || !get(norm.booleans) || !norm.classes.isNever() || !get(norm.errors) || !get(norm.nils) || !get(norm.numbers) || !norm.strings.isNever() || !get(norm.threads) || - !norm.functions.isNever() || !norm.tables.empty() || !norm.tyvars.empty(); + (FFlag::LuauBufferTypeck && !get(norm.buffers)) || !norm.functions.isNever() || !norm.tables.empty() || !norm.tyvars.empty(); } -bool Normalizer::isInhabited(const NormalizedType* norm, std::unordered_set seen) +bool Normalizer::isInhabited(const NormalizedType* norm, Set seen) { // If normalization failed, the type is complex, and so is more likely than not to be inhabited. if (!norm) return true; if (!get(norm->tops) || !get(norm->booleans) || !get(norm->errors) || !get(norm->nils) || - !get(norm->numbers) || !get(norm->threads) || !norm->classes.isNever() || !norm->strings.isNever() || - !norm->functions.isNever()) + !get(norm->numbers) || !get(norm->threads) || (FFlag::LuauBufferTypeck && !get(norm->buffers)) || + !norm->classes.isNever() || !norm->strings.isNever() || !norm->functions.isNever()) return true; for (const auto& [_, intersect] : norm->tyvars) @@ -363,7 +438,7 @@ bool Normalizer::isInhabited(TypeId ty) return *result; } - bool result = isInhabited(ty, {}); + bool result = isInhabited(ty, {nullptr}); if (cacheInhabitance) cachedIsInhabited[ty] = result; @@ -371,7 +446,7 @@ bool Normalizer::isInhabited(TypeId ty) return result; } -bool Normalizer::isInhabited(TypeId ty, std::unordered_set seen) +bool Normalizer::isInhabited(TypeId ty, Set seen) { // TODO: use log.follow(ty), CLI-64291 ty = follow(ty); @@ -425,7 +500,7 @@ bool Normalizer::isIntersectionInhabited(TypeId left, TypeId right) return *result; } - std::unordered_set seen = {}; + Set seen{nullptr}; seen.insert(left); seen.insert(right); @@ -561,6 +636,18 @@ static bool isNormalizedThread(TypeId ty) return false; } +static bool isNormalizedBuffer(TypeId ty) +{ + LUAU_ASSERT(FFlag::LuauBufferTypeck); + + if (get(ty)) + return true; + else if (const PrimitiveType* ptv = get(ty)) + return ptv->type == PrimitiveType::Buffer; + else + return false; +} + static bool areNormalizedFunctions(const NormalizedFunctionType& tys) { for (TypeId ty : tys.parts) @@ -647,8 +734,7 @@ static bool areNormalizedClasses(const NormalizedClassType& tys) static bool isPlainTyvar(TypeId ty) { - return (get(ty) || get(ty) || get(ty) || - get(ty) || get(ty)); + return (get(ty) || get(ty) || get(ty) || get(ty) || get(ty)); } static bool isNormalizedTyvar(const NormalizedTyvars& tyvars) @@ -682,6 +768,8 @@ static void assertInvariant(const NormalizedType& norm) LUAU_ASSERT(isNormalizedNumber(norm.numbers)); LUAU_ASSERT(isNormalizedString(norm.strings)); LUAU_ASSERT(isNormalizedThread(norm.threads)); + if (FFlag::LuauBufferTypeck) + LUAU_ASSERT(isNormalizedBuffer(norm.buffers)); LUAU_ASSERT(areNormalizedFunctions(norm.functions)); LUAU_ASSERT(areNormalizedTables(norm.tables)); LUAU_ASSERT(isNormalizedTyvar(norm.tyvars)); @@ -708,9 +796,14 @@ const NormalizedType* Normalizer::normalize(TypeId ty) return found->second.get(); NormalizedType norm{builtinTypes}; - std::unordered_set seenSetTypes; + Set seenSetTypes{nullptr}; if (!unionNormalWithTy(norm, ty, seenSetTypes)) return nullptr; + if (norm.isUnknown()) + { + clearNormal(norm); + norm.tops = builtinTypes->unknownType; + } std::unique_ptr uniq = std::make_unique(std::move(norm)); const NormalizedType* result = uniq.get(); cachedNormals[ty] = std::move(uniq); @@ -724,7 +817,7 @@ bool Normalizer::normalizeIntersections(const std::vector& intersections NormalizedType norm{builtinTypes}; norm.tops = builtinTypes->anyType; // Now we need to intersect the two types - std::unordered_set seenSetTypes; + Set seenSetTypes{nullptr}; for (auto ty : intersections) { if (!intersectNormalWithTy(norm, ty, seenSetTypes)) @@ -747,6 +840,8 @@ void Normalizer::clearNormal(NormalizedType& norm) norm.numbers = builtinTypes->neverType; norm.strings.resetToNever(); norm.threads = builtinTypes->neverType; + if (FFlag::LuauBufferTypeck) + norm.buffers = builtinTypes->neverType; norm.tables.clear(); norm.functions.resetToNever(); norm.tyvars.clear(); @@ -1432,6 +1527,8 @@ bool Normalizer::unionNormals(NormalizedType& here, const NormalizedType& there, here.numbers = (get(there.numbers) ? here.numbers : there.numbers); unionStrings(here.strings, there.strings); here.threads = (get(there.threads) ? here.threads : there.threads); + if (FFlag::LuauBufferTypeck) + here.buffers = (get(there.buffers) ? here.buffers : there.buffers); unionFunctions(here.functions, there.functions); unionTables(here.tables, there.tables); return true; @@ -1460,7 +1557,7 @@ bool Normalizer::withinResourceLimits() } // See above for an explaination of `ignoreSmallerTyvars`. -bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unordered_set& seenSetTypes, int ignoreSmallerTyvars) +bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes, int ignoreSmallerTyvars) { RecursionCounter _rc(&sharedState->counters.recursionCount); if (!withinResourceLimits()) @@ -1488,12 +1585,9 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unor } else if (const UnionType* utv = get(there)) { - if (FFlag::LuauNormalizeCyclicUnions) - { - if (seenSetTypes.count(there)) - return true; - seenSetTypes.insert(there); - } + if (seenSetTypes.count(there)) + return true; + seenSetTypes.insert(there); for (UnionTypeIterator it = begin(utv); it != end(utv); ++it) { @@ -1520,8 +1614,8 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unor } else if (FFlag::LuauTransitiveSubtyping && get(here.tops)) return true; - else if (get(there) || get(there) || get(there) || - get(there) || get(there)) + else if (get(there) || get(there) || get(there) || get(there) || + get(there)) { if (tyvarIndex(there) <= ignoreSmallerTyvars) return true; @@ -1549,6 +1643,8 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unor here.strings.resetToString(); else if (ptv->type == PrimitiveType::Thread) here.threads = there; + else if (FFlag::LuauBufferTypeck && ptv->type == PrimitiveType::Buffer) + here.buffers = there; else if (ptv->type == PrimitiveType::Function) { here.functions.resetToTop(); @@ -1668,6 +1764,8 @@ std::optional Normalizer::negateNormal(const NormalizedType& her result.strings.isCofinite = !result.strings.isCofinite; result.threads = get(here.threads) ? builtinTypes->threadType : builtinTypes->neverType; + if (FFlag::LuauBufferTypeck) + result.buffers = get(here.buffers) ? builtinTypes->bufferType : builtinTypes->neverType; /* * Things get weird and so, so complicated if we allow negations of @@ -1757,6 +1855,10 @@ void Normalizer::subtractPrimitive(NormalizedType& here, TypeId ty) case PrimitiveType::Thread: here.threads = builtinTypes->neverType; break; + case PrimitiveType::Buffer: + if (FFlag::LuauBufferTypeck) + here.buffers = builtinTypes->neverType; + break; case PrimitiveType::Function: here.functions.resetToNever(); break; @@ -2550,7 +2652,7 @@ void Normalizer::intersectFunctions(NormalizedFunctionType& heres, const Normali } } -bool Normalizer::intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, std::unordered_set& seenSetTypes) +bool Normalizer::intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, Set& seenSetTypes) { for (auto it = here.begin(); it != here.end();) { @@ -2587,6 +2689,8 @@ bool Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& th here.numbers = (get(there.numbers) ? there.numbers : here.numbers); intersectStrings(here.strings, there.strings); here.threads = (get(there.threads) ? there.threads : here.threads); + if (FFlag::LuauBufferTypeck) + here.buffers = (get(there.buffers) ? there.buffers : here.buffers); intersectFunctions(here.functions, there.functions); intersectTables(here.tables, there.tables); @@ -2628,7 +2732,7 @@ bool Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& th return true; } -bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std::unordered_set& seenSetTypes) +bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes) { RecursionCounter _rc(&sharedState->counters.recursionCount); if (!withinResourceLimits()) @@ -2661,8 +2765,8 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std:: return false; return true; } - else if (get(there) || get(there) || get(there) || - get(there) || get(there)) + else if (get(there) || get(there) || get(there) || get(there) || + get(there)) { NormalizedType thereNorm{builtinTypes}; NormalizedType topNorm{builtinTypes}; @@ -2708,6 +2812,7 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std:: NormalizedStringType strings = std::move(here.strings); NormalizedFunctionType functions = std::move(here.functions); TypeId threads = here.threads; + TypeId buffers = here.buffers; TypeIds tables = std::move(here.tables); clearNormal(here); @@ -2722,6 +2827,8 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std:: here.strings = std::move(strings); else if (ptv->type == PrimitiveType::Thread) here.threads = threads; + else if (FFlag::LuauBufferTypeck && ptv->type == PrimitiveType::Buffer) + here.buffers = buffers; else if (ptv->type == PrimitiveType::Function) here.functions = std::move(functions); else if (ptv->type == PrimitiveType::Table) @@ -2892,6 +2999,8 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) } if (!get(norm.threads)) result.push_back(builtinTypes->threadType); + if (FFlag::LuauBufferTypeck && !get(norm.buffers)) + result.push_back(builtinTypes->bufferType); result.insert(result.end(), norm.tables.begin(), norm.tables.end()); for (auto& [tyvar, intersect] : norm.tyvars) @@ -2915,32 +3024,58 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) bool isSubtype(TypeId subTy, TypeId superTy, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { - if (!FFlag::LuauTransitiveSubtyping) + if (!FFlag::LuauTransitiveSubtyping && !FFlag::DebugLuauDeferredConstraintResolution) return isConsistentSubtype(subTy, superTy, scope, builtinTypes, ice); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; - Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; - u.tryUnify(subTy, superTy); - return !u.failure; + // Subtyping under DCR is not implemented using unification! + if (FFlag::DebugLuauDeferredConstraintResolution) + { + Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&ice}, scope}; + + return subtyping.isSubtype(subTy, superTy).isSubtype; + } + else + { + Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; + + u.tryUnify(subTy, superTy); + return !u.failure; + } } bool isSubtype(TypePackId subPack, TypePackId superPack, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { - if (!FFlag::LuauTransitiveSubtyping) + if (!FFlag::LuauTransitiveSubtyping && !FFlag::DebugLuauDeferredConstraintResolution) return isConsistentSubtype(subPack, superPack, scope, builtinTypes, ice); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; - Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; - u.tryUnify(subPack, superPack); - return !u.failure; + // Subtyping under DCR is not implemented using unification! + if (FFlag::DebugLuauDeferredConstraintResolution) + { + Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&ice}, scope}; + + return subtyping.isSubtype(subPack, superPack).isSubtype; + } + else + { + Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; + + u.tryUnify(subPack, superPack); + return !u.failure; + } } bool isConsistentSubtype(TypeId subTy, TypeId superTy, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { + LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; @@ -2954,6 +3089,8 @@ bool isConsistentSubtype(TypeId subTy, TypeId superTy, NotNull scope, Not bool isConsistentSubtype( TypePackId subPack, TypePackId superPack, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { + LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index 2ca40bdd..6beffc2c 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -72,18 +72,6 @@ std::optional> Scope::lookupEx(Symbol sym) } } -std::optional Scope::lookupLValue(DefId def) const -{ - for (const Scope* current = this; current; current = current->parent.get()) - { - if (auto ty = current->lvalueTypes.find(def)) - return *ty; - } - - return std::nullopt; -} - -// TODO: We might kill Scope::lookup(Symbol) once data flow is fully fleshed out with type states and control flow analysis. std::optional Scope::lookup(DefId def) const { for (const Scope* current = this; current; current = current->parent.get()) @@ -181,6 +169,16 @@ std::optional Scope::linearSearchForBinding(const std::string& name, bo return std::nullopt; } +// Updates the `this` scope with the assignments from the `childScope` including ones that doesn't exist in `this`. +void Scope::inheritAssignments(const ScopePtr& childScope) +{ + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + for (const auto& [k, a] : childScope->lvalueTypes) + lvalueTypes[k] = a; +} + // Updates the `this` scope with the refinements from the `childScope` excluding ones that doesn't exist in `this`. void Scope::inheritRefinements(const ScopePtr& childScope) { diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index 6519c6ff..d04aeb82 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -2,6 +2,7 @@ #include "Luau/Simplify.h" +#include "Luau/DenseHash.h" #include "Luau/Normalize.h" // TypeIds #include "Luau/RecursionCounter.h" #include "Luau/ToString.h" @@ -21,7 +22,7 @@ struct TypeSimplifier NotNull builtinTypes; NotNull arena; - std::set blockedTypes; + DenseHashSet blockedTypes{nullptr}; int recursionDepth = 0; diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index e386bf7b..f45f6d3e 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -50,13 +50,21 @@ bool SubtypingReasoning::operator==(const SubtypingReasoning& other) const return subPath == other.subPath && superPath == other.superPath; } +size_t SubtypingReasoningHash::operator()(const SubtypingReasoning& r) const +{ + return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1); +} + SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) { - // If this result is a subtype, we take the other result's reasoning. If - // this result is not a subtype, we keep the current reasoning, even if the - // other isn't a subtype. - if (isSubtype) - reasoning = other.reasoning; + // If the other result is not a subtype, we want to join all of its + // reasonings to this one. If this result already has reasonings of its own, + // those need to be attributed here. + if (!other.isSubtype) + { + for (const SubtypingReasoning& r : other.reasoning) + reasoning.insert(r); + } isSubtype &= other.isSubtype; // `|=` is intentional here, we want to preserve error related flags. @@ -69,10 +77,20 @@ SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other) { - // If the other result is not a subtype, we take the other result's - // reasoning. - if (!other.isSubtype) - reasoning = other.reasoning; + // If this result is a subtype, we do not join the reasoning lists. If this + // result is not a subtype, but the other is a subtype, we want to _clear_ + // our reasoning list. If both results are not subtypes, we join the + // reasoning lists. + if (!isSubtype) + { + if (other.isSubtype) + reasoning.clear(); + else + { + for (const SubtypingReasoning& r : other.reasoning) + reasoning.insert(r); + } + } isSubtype |= other.isSubtype; isErrorSuppressing |= other.isErrorSuppressing; @@ -89,20 +107,26 @@ SubtypingResult& SubtypingResult::withBothComponent(TypePath::Component componen SubtypingResult& SubtypingResult::withSubComponent(TypePath::Component component) { - if (!reasoning) - reasoning = SubtypingReasoning{Path(), Path()}; - - reasoning->subPath = reasoning->subPath.push_front(component); + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{Path(component), TypePath::kEmpty}); + else + { + for (auto& r : reasoning) + r.subPath = r.subPath.push_front(component); + } return *this; } SubtypingResult& SubtypingResult::withSuperComponent(TypePath::Component component) { - if (!reasoning) - reasoning = SubtypingReasoning{Path(), Path()}; - - reasoning->superPath = reasoning->superPath.push_front(component); + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{TypePath::kEmpty, Path(component)}); + else + { + for (auto& r : reasoning) + r.superPath = r.superPath.push_front(component); + } return *this; } @@ -114,20 +138,26 @@ SubtypingResult& SubtypingResult::withBothPath(TypePath::Path path) SubtypingResult& SubtypingResult::withSubPath(TypePath::Path path) { - if (!reasoning) - reasoning = SubtypingReasoning{Path(), Path()}; - - reasoning->subPath = path.append(reasoning->subPath); + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{path, TypePath::kEmpty}); + else + { + for (auto& r : reasoning) + r.subPath = path.append(r.subPath); + } return *this; } SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path) { - if (!reasoning) - reasoning = SubtypingReasoning{Path(), Path()}; - - reasoning->superPath = path.append(reasoning->superPath); + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{TypePath::kEmpty, path}); + else + { + for (auto& r : reasoning) + r.superPath = path.append(r.superPath); + } return *this; } @@ -281,7 +311,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub return {true}; std::pair typePair{subTy, superTy}; - if (!seenTypes.insert(typePair).second) + if (!seenTypes.insert(typePair)) { /* TODO: Caching results for recursive types is really tricky to think * about. @@ -321,14 +351,26 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (auto subUnion = get(subTy)) result = isCovariantWith(env, subUnion, superTy); else if (auto superUnion = get(superTy)) + { result = isCovariantWith(env, subTy, superUnion); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + result = semantic; + } + } else if (auto superIntersection = get(superTy)) result = isCovariantWith(env, subTy, superIntersection); else if (auto subIntersection = get(subTy)) { result = isCovariantWith(env, subIntersection, superTy); if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) - result = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + result = semantic; + } } else if (get(superTy)) result = {true}; @@ -620,8 +662,8 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy& // whenever we involve contravariance. We'll end up appending path // components that should belong to the supertype to the subtype, and vice // versa. - if (result.reasoning) - std::swap(result.reasoning->subPath, result.reasoning->superPath); + for (auto& reasoning : result.reasoning) + std::swap(reasoning.subPath, reasoning.superPath); return result; } diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 58c03db4..cc01d626 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -562,6 +562,9 @@ struct TypeStringifier case PrimitiveType::Thread: state.emit("thread"); return; + case PrimitiveType::Buffer: + state.emit("buffer"); + return; case PrimitiveType::Function: state.emit("function"); return; diff --git a/Analysis/src/Transpiler.cpp b/Analysis/src/Transpiler.cpp index 8fd40772..85b8849f 100644 --- a/Analysis/src/Transpiler.cpp +++ b/Analysis/src/Transpiler.cpp @@ -10,7 +10,6 @@ #include #include -LUAU_FASTFLAG(LuauFloorDivision) namespace { @@ -474,8 +473,6 @@ struct Printer case AstExprBinary::Pow: case AstExprBinary::CompareLt: case AstExprBinary::CompareGt: - LUAU_ASSERT(FFlag::LuauFloorDivision || a->op != AstExprBinary::FloorDiv); - writer.maybeSpace(a->right->location.begin, 2); writer.symbol(toString(a->op)); break; @@ -761,8 +758,6 @@ struct Printer writer.symbol("/="); break; case AstExprBinary::FloorDiv: - LUAU_ASSERT(FFlag::LuauFloorDivision); - writer.maybeSpace(a->value->location.begin, 2); writer.symbol("//="); break; diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index 1859131b..e76837a8 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -27,6 +27,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAGVARIABLE(LuauInitializeStringMetatableInGlobalTypes, false) +LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau { @@ -214,6 +215,13 @@ bool isThread(TypeId ty) return isPrim(ty, PrimitiveType::Thread); } +bool isBuffer(TypeId ty) +{ + LUAU_ASSERT(FFlag::LuauBufferTypeck); + + return isPrim(ty, PrimitiveType::Buffer); +} + bool isOptional(TypeId ty) { if (isNil(ty)) @@ -604,10 +612,11 @@ FunctionType::FunctionType(TypeLevel level, Scope* scope, std::vector ge Property::Property() {} Property::Property(TypeId readTy, bool deprecated, const std::string& deprecatedSuggestion, std::optional location, const Tags& tags, - const std::optional& documentationSymbol) + const std::optional& documentationSymbol, std::optional typeLocation) : deprecated(deprecated) , deprecatedSuggestion(deprecatedSuggestion) , location(location) + , typeLocation(typeLocation) , tags(tags) , documentationSymbol(documentationSymbol) , readTy(readTy) @@ -925,6 +934,7 @@ BuiltinTypes::BuiltinTypes() , stringType(arena->addType(Type{PrimitiveType{PrimitiveType::String}, /*persistent*/ true})) , booleanType(arena->addType(Type{PrimitiveType{PrimitiveType::Boolean}, /*persistent*/ true})) , threadType(arena->addType(Type{PrimitiveType{PrimitiveType::Thread}, /*persistent*/ true})) + , bufferType(arena->addType(Type{PrimitiveType{PrimitiveType::Buffer}, /*persistent*/ true})) , functionType(arena->addType(Type{PrimitiveType{PrimitiveType::Function}, /*persistent*/ true})) , classType(arena->addType(Type{ClassType{"class", {}, std::nullopt, std::nullopt, {}, {}, {}}, /*persistent*/ true})) , tableType(arena->addType(Type{PrimitiveType{PrimitiveType::Table}, /*persistent*/ true})) diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index 3a1217bf..fb47471c 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -13,8 +13,6 @@ #include -LUAU_FASTFLAG(LuauParseDeclareClassIndexer); - static char* allocateString(Luau::Allocator& allocator, std::string_view contents) { char* result = (char*)allocator.allocate(contents.size() + 1); @@ -106,6 +104,8 @@ public: return allocator->alloc(Location(), std::nullopt, AstName("string"), std::nullopt, Location()); case PrimitiveType::Thread: return allocator->alloc(Location(), std::nullopt, AstName("thread"), std::nullopt, Location()); + case PrimitiveType::Buffer: + return allocator->alloc(Location(), std::nullopt, AstName("buffer"), std::nullopt, Location()); default: return nullptr; } @@ -230,7 +230,7 @@ public: } AstTableIndexer* indexer = nullptr; - if (FFlag::LuauParseDeclareClassIndexer && ctv.indexer) + if (ctv.indexer) { RecursionCounter counter(&count); diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index bf8e362d..8df78140 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -3,9 +3,9 @@ #include "Luau/Ast.h" #include "Luau/AstQuery.h" -#include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/DcrLogger.h" +#include "Luau/DenseHash.h" #include "Luau/Error.h" #include "Luau/InsertionOrderedMap.h" #include "Luau/Instantiation.h" @@ -20,12 +20,12 @@ #include "Luau/TypePack.h" #include "Luau/TypePath.h" #include "Luau/TypeUtils.h" +#include "Luau/TypeOrPack.h" #include "Luau/VisitType.h" #include LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(LuauFloorDivision); namespace Luau { @@ -1660,14 +1660,48 @@ struct TypeChecker2 if (argIt == end(inferredFtv->argTypes)) break; + TypeId inferredArgTy = *argIt; + if (arg->annotation) { - TypeId inferredArgTy = *argIt; TypeId annotatedArgTy = lookupAnnotation(arg->annotation); testIsSubtype(inferredArgTy, annotatedArgTy, arg->location); } + // Some Luau constructs can result in an argument type being + // reduced to never by inference. In this case, we want to + // report an error at the function, instead of reporting an + // error at every callsite. + if (is(follow(inferredArgTy))) + { + // If the annotation simplified to never, we don't want to + // even look at contributors. + bool explicitlyNever = false; + if (arg->annotation) + { + TypeId annotatedArgTy = lookupAnnotation(arg->annotation); + explicitlyNever = is(annotatedArgTy); + } + + // Not following here is deliberate: the contribution map is + // keyed by type pointer, but that type pointer has, at some + // point, been transmuted to a bound type pointing to never. + if (const auto contributors = module->upperBoundContributors.find(inferredArgTy); contributors && !explicitlyNever) + { + // It's unfortunate that we can't link error messages + // together. For now, this will work. + reportError( + GenericError{format( + "Parameter '%s' has been reduced to never. This function is not callable with any possible value.", arg->name.value)}, + arg->location); + for (const auto& [site, component] : *contributors) + reportError(ExtraInformation{format("Parameter '%s' is required to be a subtype of '%s' here.", arg->name.value, + toString(component).c_str())}, + site); + } + } + ++argIt; } } @@ -1819,8 +1853,6 @@ struct TypeChecker2 bool typesHaveIntersection = normalizer.isIntersectionInhabited(leftType, rightType); if (auto it = kBinaryOpMetamethods.find(expr->op); it != kBinaryOpMetamethods.end()) { - LUAU_ASSERT(FFlag::LuauFloorDivision || expr->op != AstExprBinary::Op::FloorDiv); - std::optional leftMt = getMetatable(leftType, builtinTypes); std::optional rightMt = getMetatable(rightType, builtinTypes); bool matches = leftMt == rightMt; @@ -2009,8 +2041,6 @@ struct TypeChecker2 case AstExprBinary::Op::FloorDiv: case AstExprBinary::Op::Pow: case AstExprBinary::Op::Mod: - LUAU_ASSERT(FFlag::LuauFloorDivision || expr->op != AstExprBinary::Op::FloorDiv); - testIsSubtype(leftType, builtinTypes->numberType, expr->left->location); testIsSubtype(rightType, builtinTypes->numberType, expr->right->location); @@ -2413,6 +2443,67 @@ struct TypeChecker2 } } + template + std::optional explainReasonings(TID subTy, TID superTy, Location location, const SubtypingResult& r) + { + if (r.reasoning.empty()) + return std::nullopt; + + std::vector reasons; + for (const SubtypingReasoning& reasoning : r.reasoning) + { + if (reasoning.subPath.empty() && reasoning.superPath.empty()) + continue; + + std::optional subLeaf = traverse(subTy, reasoning.subPath, builtinTypes); + std::optional superLeaf = traverse(superTy, reasoning.superPath, builtinTypes); + + if (!subLeaf || !superLeaf) + ice->ice("Subtyping test returned a reasoning with an invalid path", location); + + if (!get2(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) + ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); + + std::string reason; + if (reasoning.subPath == reasoning.superPath) + reason = "at " + toString(reasoning.subPath) + ", " + toString(*subLeaf) + " is not a subtype of " + toString(*superLeaf); + else + reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + + ") is not a subtype of " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + + toString(*superLeaf) + ")"; + + reasons.push_back(reason); + } + + // DenseHashSet ordering is entirely undefined, so we want to + // sort the reasons here to achieve a stable error + // stringification. + std::sort(reasons.begin(), reasons.end()); + std::string allReasons; + bool first = true; + for (const std::string& reason : reasons) + { + if (first) + first = false; + else + allReasons += "\n\t"; + + allReasons += reason; + } + + return allReasons; + } + + void explainError(TypeId subTy, TypeId superTy, Location location, const SubtypingResult& result) + { + reportError(TypeMismatch{superTy, subTy, explainReasonings(subTy, superTy, location, result).value_or("")}, location); + } + + void explainError(TypePackId subTy, TypePackId superTy, Location location, const SubtypingResult& result) + { + reportError(TypePackMismatch{superTy, subTy, explainReasonings(subTy, superTy, location, result).value_or("")}, location); + } + bool testIsSubtype(TypeId subTy, TypeId superTy, Location location) { SubtypingResult r = subtyping->isSubtype(subTy, superTy); @@ -2421,27 +2512,7 @@ struct TypeChecker2 reportError(NormalizationTooComplex{}, location); if (!r.isSubtype && !r.isErrorSuppressing) - { - if (r.reasoning) - { - std::optional subLeaf = traverse(subTy, r.reasoning->subPath, builtinTypes); - std::optional superLeaf = traverse(superTy, r.reasoning->superPath, builtinTypes); - - if (!subLeaf || !superLeaf) - ice->ice("Subtyping test returned a reasoning with an invalid path", location); - - if (!get2(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) - ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); - - std::string reason = "type " + toString(subTy) + toString(r.reasoning->subPath) + " (" + toString(*subLeaf) + - ") is not a subtype of " + toString(superTy) + toString(r.reasoning->superPath) + " (" + toString(*superLeaf) + - ")"; - - reportError(TypeMismatch{superTy, subTy, reason}, location); - } - else - reportError(TypeMismatch{superTy, subTy}, location); - } + explainError(subTy, superTy, location, r); return r.isSubtype; } @@ -2454,7 +2525,7 @@ struct TypeChecker2 reportError(NormalizationTooComplex{}, location); if (!r.isSubtype && !r.isErrorSuppressing) - reportError(TypePackMismatch{superTy, subTy}, location); + explainError(subTy, superTy, location, r); return r.isSubtype; } @@ -2502,7 +2573,7 @@ struct TypeChecker2 if (!normalizer.isInhabited(ty)) return; - std::unordered_set seen; + DenseHashSet seen{nullptr}; bool found = hasIndexTypeFromType(ty, prop, location, seen, astIndexExprType); foundOneProp |= found; if (!found) @@ -2563,14 +2634,14 @@ struct TypeChecker2 } } - bool hasIndexTypeFromType(TypeId ty, const std::string& prop, const Location& location, std::unordered_set& seen, TypeId astIndexExprType) + bool hasIndexTypeFromType(TypeId ty, const std::string& prop, const Location& location, DenseHashSet& seen, TypeId astIndexExprType) { // If we have already encountered this type, we must assume that some // other codepath will do the right thing and signal false if the // property is not present. - const bool isUnseen = seen.insert(ty).second; - if (!isUnseen) + if (seen.contains(ty)) return true; + seen.insert(ty); if (get(ty) || get(ty) || get(ty)) return true; diff --git a/Analysis/src/TypeFamily.cpp b/Analysis/src/TypeFamily.cpp index e3afb944..a3a67ace 100644 --- a/Analysis/src/TypeFamily.cpp +++ b/Analysis/src/TypeFamily.cpp @@ -751,8 +751,11 @@ TypeFamilyReductionResult andFamilyFn(const std::vector& typePar // And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is truthy. SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->falsyType); SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); - std::vector blockedTypes(filteredLhs.blockedTypes.begin(), filteredLhs.blockedTypes.end()); - blockedTypes.insert(blockedTypes.end(), overallResult.blockedTypes.begin(), overallResult.blockedTypes.end()); + std::vector blockedTypes{}; + for (auto ty : filteredLhs.blockedTypes) + blockedTypes.push_back(ty); + for (auto ty : overallResult.blockedTypes) + blockedTypes.push_back(ty); return {overallResult.result, false, std::move(blockedTypes), {}}; } @@ -776,8 +779,11 @@ TypeFamilyReductionResult orFamilyFn(const std::vector& typePara // Or evalutes to the LHS type if the LHS is truthy, and the RHS type if LHS is falsy. SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->truthyType); SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); - std::vector blockedTypes(filteredLhs.blockedTypes.begin(), filteredLhs.blockedTypes.end()); - blockedTypes.insert(blockedTypes.end(), overallResult.blockedTypes.begin(), overallResult.blockedTypes.end()); + std::vector blockedTypes{}; + for (auto ty : filteredLhs.blockedTypes) + blockedTypes.push_back(ty); + for (auto ty : overallResult.blockedTypes) + blockedTypes.push_back(ty); return {overallResult.result, false, std::move(blockedTypes), {}}; } diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index a29b1e06..0ffe40df 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -35,14 +35,11 @@ LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false) -LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure) LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false) LUAU_FASTFLAGVARIABLE(LuauLoopControlFlowAnalysis, false) -LUAU_FASTFLAGVARIABLE(LuauVariadicOverloadFix, false) LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false) -LUAU_FASTFLAG(LuauParseDeclareClassIndexer) -LUAU_FASTFLAG(LuauFloorDivision); +LUAU_FASTFLAG(LuauBufferTypeck) +LUAU_FASTFLAGVARIABLE(LuauRemoveBadRelationalOperatorWarning, false) namespace Luau { @@ -204,7 +201,7 @@ static bool isMetamethod(const Name& name) return name == "__index" || name == "__newindex" || name == "__call" || name == "__concat" || name == "__unm" || name == "__add" || name == "__sub" || name == "__mul" || name == "__div" || name == "__mod" || name == "__pow" || name == "__tostring" || name == "__metatable" || name == "__eq" || name == "__lt" || name == "__le" || name == "__mode" || name == "__iter" || name == "__len" || - (FFlag::LuauFloorDivision && name == "__idiv"); + name == "__idiv"; } size_t HashBoolNamePair::operator()(const std::pair& pair) const @@ -224,6 +221,7 @@ TypeChecker::TypeChecker(const ScopePtr& globalScope, ModuleResolver* resolver, , stringType(builtinTypes->stringType) , booleanType(builtinTypes->booleanType) , threadType(builtinTypes->threadType) + , bufferType(builtinTypes->bufferType) , anyType(builtinTypes->anyType) , unknownType(builtinTypes->unknownType) , neverType(builtinTypes->neverType) @@ -1628,13 +1626,6 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& ty TypeId& bindingType = bindingsMap[name].type; - if (!FFlag::LuauOccursIsntAlwaysFailure) - { - if (unify(ty, bindingType, aliasScope, typealias.location)) - bindingType = ty; - return ControlFlow::None; - } - unify(ty, bindingType, aliasScope, typealias.location); // It is possible for this unification to succeed but for @@ -1764,7 +1755,7 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatDeclareClass& if (!ctv->metatable) ice("No metatable for declared class"); - if (const auto& indexer = declaredClass.indexer; FFlag::LuauParseDeclareClassIndexer && indexer) + if (const auto& indexer = declaredClass.indexer) ctv->indexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType)); TableType* metatable = getMutable(*ctv->metatable); @@ -2562,7 +2553,6 @@ std::string opToMetaTableEntry(const AstExprBinary::Op& op) case AstExprBinary::Div: return "__div"; case AstExprBinary::FloorDiv: - LUAU_ASSERT(FFlag::LuauFloorDivision); return "__idiv"; case AstExprBinary::Mod: return "__mod"; @@ -2765,10 +2755,26 @@ TypeId TypeChecker::checkRelationalOperation( { reportErrors(state.errors); - if (!isEquality && state.errors.empty() && (get(leftType) || isBoolean(leftType))) + if (FFlag::LuauRemoveBadRelationalOperatorWarning) { - reportError(expr.location, GenericError{format("Type '%s' cannot be compared with relational operator %s", toString(leftType).c_str(), - toString(expr.op).c_str())}); + // The original version of this check also produced this error when we had a union type. + // However, the old solver does not readily have the ability to discern if the union is comparable. + // This is the case when the lhs is e.g. a union of singletons and the rhs is the combined type. + // The new solver has much more powerful logic for resolving relational operators, but for now, + // we need to be conservative in the old solver to deliver a reasonable developer experience. + if (!isEquality && state.errors.empty() && isBoolean(leftType)) + { + reportError(expr.location, GenericError{format("Type '%s' cannot be compared with relational operator %s", + toString(leftType).c_str(), toString(expr.op).c_str())}); + } + } + else + { + if (!isEquality && state.errors.empty() && (get(leftType) || isBoolean(leftType))) + { + reportError(expr.location, GenericError{format("Type '%s' cannot be compared with relational operator %s", + toString(leftType).c_str(), toString(expr.op).c_str())}); + } } return booleanType; @@ -3060,8 +3066,6 @@ TypeId TypeChecker::checkBinaryOperation( case AstExprBinary::FloorDiv: case AstExprBinary::Mod: case AstExprBinary::Pow: - LUAU_ASSERT(FFlag::LuauFloorDivision || expr.op != AstExprBinary::FloorDiv); - reportErrors(tryUnify(lhsType, numberType, scope, expr.left->location)); reportErrors(tryUnify(rhsType, numberType, scope, expr.right->location)); return numberType; @@ -3412,15 +3416,12 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex } } - if (FFlag::LuauAllowIndexClassParameters) + if (const ClassType* exprClass = get(exprType)) { - if (const ClassType* exprClass = get(exprType)) - { - if (isNonstrictMode()) - return unknownType; - reportError(TypeError{expr.location, DynamicPropertyLookupOnClassesUnsafe{exprType}}); - return errorRecoveryType(scope); - } + if (isNonstrictMode()) + return unknownType; + reportError(TypeError{expr.location, DynamicPropertyLookupOnClassesUnsafe{exprType}}); + return errorRecoveryType(scope); } } @@ -4026,13 +4027,9 @@ void TypeChecker::checkArgumentList(const ScopePtr& scope, const AstExpr& funNam if (argIndex < argLocations.size()) location = argLocations[argIndex]; - if (FFlag::LuauVariadicOverloadFix) - { - state.location = location; - state.tryUnify(*argIter, vtp->ty); - } - else - unify(*argIter, vtp->ty, scope, location); + state.location = location; + state.tryUnify(*argIter, vtp->ty); + ++argIter; ++argIndex; } @@ -5403,7 +5400,7 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno std::optional tableIndexer; for (const auto& prop : table->props) - props[prop.name.value] = {resolveType(scope, *prop.type)}; + props[prop.name.value] = {resolveType(scope, *prop.type), /* deprecated: */ false, {}, std::nullopt, {}, std::nullopt, prop.location}; if (const auto& indexer = table->indexer) tableIndexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType)); @@ -6025,6 +6022,8 @@ void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, RefinementMap& r return refine(isBoolean, booleanType); else if (typeguardP.kind == "thread") return refine(isThread, threadType); + else if (FFlag::LuauBufferTypeck && typeguardP.kind == "buffer") + return refine(isBuffer, bufferType); else if (typeguardP.kind == "table") { return refine([](TypeId ty) -> bool { diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index ff515bed..9b470a2a 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -6,8 +6,9 @@ #include "Luau/Type.h" #include "Luau/TypeFwd.h" #include "Luau/TypePack.h" -#include "Luau/TypeUtils.h" +#include "Luau/TypeOrPack.h" +#include #include #include #include @@ -104,6 +105,41 @@ bool Path::operator==(const Path& other) const return components == other.components; } +size_t PathHash::operator()(const Property& prop) const +{ + return std::hash()(prop.name) ^ static_cast(prop.isRead); +} + +size_t PathHash::operator()(const Index& idx) const +{ + return idx.index; +} + +size_t PathHash::operator()(const TypeField& field) const +{ + return static_cast(field); +} + +size_t PathHash::operator()(const PackField& field) const +{ + return static_cast(field); +} + +size_t PathHash::operator()(const Component& component) const +{ + return visit(*this, component); +} + +size_t PathHash::operator()(const Path& path) const +{ + size_t hash = 0; + + for (const Component& component : path.components) + hash ^= (*this)(component); + + return hash; +} + Path PathBuilder::build() { return Path(std::move(components)); @@ -465,7 +501,7 @@ struct TraversalState } // namespace -std::string toString(const TypePath::Path& path) +std::string toString(const TypePath::Path& path, bool prefixDot) { std::stringstream result; bool first = true; @@ -491,7 +527,7 @@ std::string toString(const TypePath::Path& path) } else if constexpr (std::is_same_v) { - if (!first) + if (!first || prefixDot) result << '.'; switch (c) @@ -523,7 +559,7 @@ std::string toString(const TypePath::Path& path) } else if constexpr (std::is_same_v) { - if (!first) + if (!first || prefixDot) result << '.'; switch (c) @@ -580,7 +616,14 @@ std::optional traverse(TypeId root, const Path& path, NotNull traverse(TypePackId root, const Path& path, NotNull builtinTypes); +std::optional traverse(TypePackId root, const Path& path, NotNull builtinTypes) +{ + TraversalState state(follow(root), builtinTypes); + if (traverse(state, path)) + return state.current; + else + return std::nullopt; +} std::optional traverseForType(TypeId root, const Path& path, NotNull builtinTypes) { diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index c371e81e..93f8a851 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -18,12 +18,11 @@ LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false) -LUAU_FASTFLAGVARIABLE(LuauMaintainScopesInUnifier, false) LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping, false) -LUAU_FASTFLAGVARIABLE(LuauOccursIsntAlwaysFailure, false) LUAU_FASTFLAG(LuauAlwaysCommitInferencesOfFunctionCalls) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAGVARIABLE(LuauFixIndexerSubtypingOrdering, false) +LUAU_FASTFLAGVARIABLE(LuauUnifierShouldNotCopyError, false) namespace Luau { @@ -1514,7 +1513,7 @@ struct WeirdIter auto freePack = log.getMutable(packId); level = freePack->level; - if (FFlag::LuauMaintainScopesInUnifier && freePack->scope != nullptr) + if (freePack->scope != nullptr) scope = freePack->scope; log.replace(packId, BoundTypePack(newTail)); packId = newTail; @@ -1679,11 +1678,8 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal auto superIter = WeirdIter(superTp, log); auto subIter = WeirdIter(subTp, log); - if (FFlag::LuauMaintainScopesInUnifier) - { - superIter.scope = scope.get(); - subIter.scope = scope.get(); - } + superIter.scope = scope.get(); + subIter.scope = scope.get(); auto mkFreshType = [this](Scope* scope, TypeLevel level) { if (FFlag::DebugLuauDeferredConstraintResolution) @@ -2877,7 +2873,7 @@ bool Unifier::occursCheck(TypeId needle, TypeId haystack, bool reversed) bool occurs = occursCheck(sharedState.tempSeenTy, needle, haystack); - if (occurs && FFlag::LuauOccursIsntAlwaysFailure) + if (occurs) { Unifier innerState = makeChildUnifier(); if (const UnionType* ut = get(haystack)) @@ -2935,15 +2931,7 @@ bool Unifier::occursCheck(DenseHashSet& seen, TypeId needle, TypeId hays ice("Expected needle to be free"); if (needle == haystack) - { - if (!FFlag::LuauOccursIsntAlwaysFailure) - { - reportError(location, OccursCheckFailed{}); - log.replace(needle, *builtinTypes->errorRecoveryType()); - } - return true; - } if (log.getMutable(haystack) || (hideousFixMeGenericsAreActuallyFree && log.is(haystack))) return false; @@ -2967,10 +2955,13 @@ bool Unifier::occursCheck(TypePackId needle, TypePackId haystack, bool reversed) bool occurs = occursCheck(sharedState.tempSeenTp, needle, haystack); - if (occurs && FFlag::LuauOccursIsntAlwaysFailure) + if (occurs) { reportError(location, OccursCheckFailed{}); - log.replace(needle, *builtinTypes->errorRecoveryTypePack()); + if (FFlag::LuauUnifierShouldNotCopyError) + log.replace(needle, BoundTypePack{builtinTypes->errorRecoveryTypePack()}); + else + log.replace(needle, *builtinTypes->errorRecoveryTypePack()); } return occurs; @@ -2997,15 +2988,7 @@ bool Unifier::occursCheck(DenseHashSet& seen, TypePackId needle, Typ while (!log.getMutable(haystack)) { if (needle == haystack) - { - if (!FFlag::LuauOccursIsntAlwaysFailure) - { - reportError(location, OccursCheckFailed{}); - log.replace(needle, *builtinTypes->errorRecoveryTypePack()); - } - return true; - } if (auto a = get(haystack); a && a->tail) { diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 11f96ea1..41a5afb0 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -5,9 +5,6 @@ #include "Luau/Instantiation.h" #include "Luau/Scope.h" #include "Luau/Simplify.h" -#include "Luau/Substitution.h" -#include "Luau/ToString.h" -#include "Luau/TxnLog.h" #include "Luau/Type.h" #include "Luau/TypeArena.h" #include "Luau/TypeCheckLimits.h" @@ -16,7 +13,6 @@ #include #include -#include LUAU_FASTINT(LuauTypeInferRecursionLimit) @@ -49,7 +45,10 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy) FreeType* superFree = getMutable(superTy); if (subFree) + { subFree->upperBound = mkIntersection(subFree->upperBound, superTy); + expandedFreeTypes[subTy].push_back(superTy); + } if (superFree) superFree->lowerBound = mkUnion(superFree->lowerBound, subTy); diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index a3908a56..ad5592f5 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -249,6 +249,7 @@ public: enum class ConstantNumberParseResult { Ok, + Imprecise, Malformed, BinOverflow, HexOverflow, diff --git a/Ast/include/Luau/Location.h b/Ast/include/Luau/Location.h index 041a2c63..3fc8921a 100644 --- a/Ast/include/Luau/Location.h +++ b/Ast/include/Luau/Location.h @@ -1,7 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include namespace Luau { @@ -9,7 +8,11 @@ struct Position { unsigned int line, column; - Position(unsigned int line, unsigned int column); + Position(unsigned int line, unsigned int column) + : line(line) + , column(column) + { + } bool operator==(const Position& rhs) const; bool operator!=(const Position& rhs) const; @@ -25,10 +28,29 @@ struct Location { Position begin, end; - Location(); - Location(const Position& begin, const Position& end); - Location(const Position& begin, unsigned int length); - Location(const Location& begin, const Location& end); + Location() + : begin(0, 0) + , end(0, 0) + { + } + + Location(const Position& begin, const Position& end) + : begin(begin) + , end(end) + { + } + + Location(const Position& begin, unsigned int length) + : begin(begin) + , end(begin.line, begin.column + length) + { + } + + Location(const Location& begin, const Location& end) + : begin(begin.begin) + , end(end.end) + { + } bool operator==(const Location& rhs) const; bool operator!=(const Location& rhs) const; diff --git a/Ast/src/Ast.cpp b/Ast/src/Ast.cpp index a7d7eef7..9a6ca4d7 100644 --- a/Ast/src/Ast.cpp +++ b/Ast/src/Ast.cpp @@ -3,7 +3,6 @@ #include "Luau/Common.h" -LUAU_FASTFLAG(LuauFloorDivision); namespace Luau { @@ -282,7 +281,6 @@ std::string toString(AstExprBinary::Op op) case AstExprBinary::Div: return "/"; case AstExprBinary::FloorDiv: - LUAU_ASSERT(FFlag::LuauFloorDivision); return "//"; case AstExprBinary::Mod: return "%"; diff --git a/Ast/src/Lexer.cpp b/Ast/src/Lexer.cpp index a493acfe..96653a56 100644 --- a/Ast/src/Lexer.cpp +++ b/Ast/src/Lexer.cpp @@ -7,7 +7,6 @@ #include -LUAU_FASTFLAGVARIABLE(LuauFloorDivision, false) LUAU_FASTFLAGVARIABLE(LuauLexerLookaheadRemembersBraceType, false) LUAU_FASTFLAGVARIABLE(LuauCheckedFunctionSyntax, false) @@ -142,7 +141,7 @@ std::string Lexeme::toString() const return "'::'"; case FloorDiv: - return FFlag::LuauFloorDivision ? "'//'" : ""; + return "'//'"; case AddAssign: return "'+='"; @@ -157,7 +156,7 @@ std::string Lexeme::toString() const return "'/='"; case FloorDivAssign: - return FFlag::LuauFloorDivision ? "'//='" : ""; + return "'//='"; case ModAssign: return "'%='"; @@ -909,44 +908,29 @@ Lexeme Lexer::readNext() case '/': { - if (FFlag::LuauFloorDivision) + consume(); + + char ch = peekch(); + + if (ch == '=') { consume(); - - char ch = peekch(); - - if (ch == '=') - { - consume(); - return Lexeme(Location(start, 2), Lexeme::DivAssign); - } - else if (ch == '/') - { - consume(); - - if (peekch() == '=') - { - consume(); - return Lexeme(Location(start, 3), Lexeme::FloorDivAssign); - } - else - return Lexeme(Location(start, 2), Lexeme::FloorDiv); - } - else - return Lexeme(Location(start, 1), '/'); + return Lexeme(Location(start, 2), Lexeme::DivAssign); } - else + else if (ch == '/') { consume(); if (peekch() == '=') { consume(); - return Lexeme(Location(start, 2), Lexeme::DivAssign); + return Lexeme(Location(start, 3), Lexeme::FloorDivAssign); } else - return Lexeme(Location(start, 1), '/'); + return Lexeme(Location(start, 2), Lexeme::FloorDiv); } + else + return Lexeme(Location(start, 1), '/'); } case '*': diff --git a/Ast/src/Location.cpp b/Ast/src/Location.cpp index 40f8e23e..c2c66d9f 100644 --- a/Ast/src/Location.cpp +++ b/Ast/src/Location.cpp @@ -1,16 +1,9 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Location.h" -#include namespace Luau { -Position::Position(unsigned int line, unsigned int column) - : line(line) - , column(column) -{ -} - bool Position::operator==(const Position& rhs) const { return this->column == rhs.column && this->line == rhs.line; @@ -61,30 +54,6 @@ void Position::shift(const Position& start, const Position& oldEnd, const Positi } } -Location::Location() - : begin(0, 0) - , end(0, 0) -{ -} - -Location::Location(const Position& begin, const Position& end) - : begin(begin) - , end(end) -{ -} - -Location::Location(const Position& begin, unsigned int length) - : begin(begin) - , end(begin.line, begin.column + length) -{ -} - -Location::Location(const Location& begin, const Location& end) - : begin(begin.begin) - , end(end.end) -{ -} - bool Location::operator==(const Location& rhs) const { return this->begin == rhs.begin && this->end == rhs.end; diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index a9747143..510e5f0e 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -16,13 +16,10 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) // Warning: If you are introducing new syntax, ensure that it is behind a separate // flag so that we don't break production games by reverting syntax changes. // See docs/SyntaxChanges.md for an explanation. -LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false) LUAU_FASTFLAGVARIABLE(LuauClipExtraHasEndProps, false) -LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauCheckedFunctionSyntax) -LUAU_FASTFLAGVARIABLE(LuauBetterTypeUnionLimits, false) -LUAU_FASTFLAGVARIABLE(LuauBetterTypeRecLimits, false) +LUAU_FASTFLAGVARIABLE(LuauParseImpreciseNumber, false) namespace Luau { @@ -924,7 +921,7 @@ AstStat* Parser::parseDeclaration(const Location& start) { props.push_back(parseDeclaredClassMethod()); } - else if (lexer.current().type == '[' && (!FFlag::LuauParseDeclareClassIndexer || lexer.lookahead().type == Lexeme::RawString || + else if (lexer.current().type == '[' && (lexer.lookahead().type == Lexeme::RawString || lexer.lookahead().type == Lexeme::QuotedString)) { const Lexeme begin = lexer.current(); @@ -944,7 +941,7 @@ AstStat* Parser::parseDeclaration(const Location& start) else report(begin.location, "String literal contains malformed escape sequence or \\0"); } - else if (lexer.current().type == '[' && FFlag::LuauParseDeclareClassIndexer) + else if (lexer.current().type == '[') { if (indexer) { @@ -1544,8 +1541,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) unsigned int oldRecursionCount = recursionCounter; parts.push_back(parseSimpleType(/* allowPack= */ false).type); - if (FFlag::LuauBetterTypeUnionLimits) - recursionCounter = oldRecursionCount; + recursionCounter = oldRecursionCount; isUnion = true; } @@ -1554,7 +1550,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) Location loc = lexer.current().location; nextLexeme(); - if (!FFlag::LuauBetterTypeUnionLimits || !hasOptional) + if (!hasOptional) parts.push_back(allocator.alloc(loc, std::nullopt, nameNil, std::nullopt, loc)); isUnion = true; @@ -1566,8 +1562,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) unsigned int oldRecursionCount = recursionCounter; parts.push_back(parseSimpleType(/* allowPack= */ false).type); - if (FFlag::LuauBetterTypeUnionLimits) - recursionCounter = oldRecursionCount; + recursionCounter = oldRecursionCount; isIntersection = true; } @@ -1579,7 +1574,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) else break; - if (FFlag::LuauBetterTypeUnionLimits && parts.size() > unsigned(FInt::LuauTypeLengthLimit) + hasOptional) + if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + hasOptional) ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile"); } @@ -1607,10 +1602,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) AstTypeOrPack Parser::parseTypeOrPack() { unsigned int oldRecursionCount = recursionCounter; - // recursion counter is incremented in parseSimpleType - if (!FFlag::LuauBetterTypeRecLimits) - incrementRecursionCounter("type annotation"); Location begin = lexer.current().location; @@ -1630,10 +1622,7 @@ AstTypeOrPack Parser::parseTypeOrPack() AstType* Parser::parseType(bool inDeclarationContext) { unsigned int oldRecursionCount = recursionCounter; - // recursion counter is incremented in parseSimpleType - if (!FFlag::LuauBetterTypeRecLimits) - incrementRecursionCounter("type annotation"); Location begin = lexer.current().location; @@ -1839,11 +1828,7 @@ std::optional Parser::parseBinaryOp(const Lexeme& l) else if (l.type == '/') return AstExprBinary::Div; else if (l.type == Lexeme::FloorDiv) - { - LUAU_ASSERT(FFlag::LuauFloorDivision); - return AstExprBinary::FloorDiv; - } else if (l.type == '%') return AstExprBinary::Mod; else if (l.type == '^') @@ -1881,11 +1866,7 @@ std::optional Parser::parseCompoundOp(const Lexeme& l) else if (l.type == Lexeme::DivAssign) return AstExprBinary::Div; else if (l.type == Lexeme::FloorDivAssign) - { - LUAU_ASSERT(FFlag::LuauFloorDivision); - return AstExprBinary::FloorDiv; - } else if (l.type == Lexeme::ModAssign) return AstExprBinary::Mod; else if (l.type == Lexeme::PowAssign) @@ -2187,6 +2168,12 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data, return base == 2 ? ConstantNumberParseResult::BinOverflow : ConstantNumberParseResult::HexOverflow; } + if (FFlag::LuauParseImpreciseNumber) + { + if (value >= (1ull << 53) && static_cast(result) != value) + return ConstantNumberParseResult::Imprecise; + } + return ConstantNumberParseResult::Ok; } @@ -2203,8 +2190,32 @@ static ConstantNumberParseResult parseDouble(double& result, const char* data) char* end = nullptr; double value = strtod(data, &end); - result = value; - return *end == 0 ? ConstantNumberParseResult::Ok : ConstantNumberParseResult::Malformed; + if (FFlag::LuauParseImpreciseNumber) + { + // trailing non-numeric characters + if (*end != 0) + return ConstantNumberParseResult::Malformed; + + result = value; + + // for linting, we detect integer constants that are parsed imprecisely + // since the check is expensive we only perform it when the number is larger than the precise integer range + if (value >= double(1ull << 53) && strspn(data, "0123456789") == strlen(data)) + { + char repr[512]; + snprintf(repr, sizeof(repr), "%.0f", value); + + if (strcmp(repr, data) != 0) + return ConstantNumberParseResult::Imprecise; + } + + return ConstantNumberParseResult::Ok; + } + else + { + result = value; + return *end == 0 ? ConstantNumberParseResult::Ok : ConstantNumberParseResult::Malformed; + } } // simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | FUNCTION body | primaryexp diff --git a/CLI/Compile.cpp b/CLI/Compile.cpp index 4f6b54b6..bc1d5c60 100644 --- a/CLI/Compile.cpp +++ b/CLI/Compile.cpp @@ -120,6 +120,7 @@ struct CompileStats { size_t lines; size_t bytecode; + size_t bytecodeInstructionCount; size_t codegen; double readTime; @@ -136,6 +137,7 @@ struct CompileStats fprintf(fp, "{\ \"lines\": %zu, \ \"bytecode\": %zu, \ +\"bytecodeInstructionCount\": %zu, \ \"codegen\": %zu, \ \"readTime\": %f, \ \"miscTime\": %f, \ @@ -153,16 +155,22 @@ struct CompileStats \"maxBlockInstructions\": %u, \ \"regAllocErrors\": %d, \ \"loweringErrors\": %d\ +}, \ +\"blockLinearizationStats\": {\ +\"constPropInstructionCount\": %u, \ +\"timeSeconds\": %f\ }}", - lines, bytecode, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions, lowerStats.skippedFunctions, - lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt, lowerStats.blocksPostOpt, - lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors); + lines, bytecode, bytecodeInstructionCount, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions, + lowerStats.skippedFunctions, lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt, + lowerStats.blocksPostOpt, lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors, + lowerStats.blockLinearizationStats.constPropInstructionCount, lowerStats.blockLinearizationStats.timeSeconds); } CompileStats& operator+=(const CompileStats& that) { this->lines += that.lines; this->bytecode += that.bytecode; + this->bytecodeInstructionCount += that.bytecodeInstructionCount; this->codegen += that.codegen; this->readTime += that.readTime; this->miscTime += that.miscTime; @@ -257,6 +265,7 @@ static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::A Luau::compileOrThrow(bcb, result, names, copts()); stats.bytecode += bcb.getBytecode().size(); + stats.bytecodeInstructionCount = bcb.getTotalInstructionCount(); stats.compileTime += recordDeltaTime(currts); switch (format) @@ -321,6 +330,30 @@ static int assertionHandler(const char* expr, const char* file, int line, const return 1; } +std::string escapeFilename(const std::string& filename) +{ + std::string escaped; + escaped.reserve(filename.size()); + + for (const char ch : filename) + { + switch (ch) + { + case '\\': + escaped.push_back('/'); + break; + case '"': + escaped.push_back('\\'); + escaped.push_back(ch); + break; + default: + escaped.push_back(ch); + } + } + + return escaped; +} + int main(int argc, char** argv) { Luau::assertHandler() = assertionHandler; @@ -330,6 +363,7 @@ int main(int argc, char** argv) CompileFormat compileFormat = CompileFormat::Text; Luau::CodeGen::AssemblyOptions::Target assemblyTarget = Luau::CodeGen::AssemblyOptions::Host; RecordStats recordStats = RecordStats::None; + std::string statsFile("stats.json"); for (int i = 1; i < argc; i++) { @@ -394,6 +428,16 @@ int main(int argc, char** argv) return 1; } } + else if (strncmp(argv[i], "--stats-file=", 13) == 0) + { + statsFile = argv[i] + 13; + + if (statsFile.size() == 0) + { + fprintf(stderr, "Error: filename missing for '--stats-file'.\n\n"); + return 1; + } + } else if (strncmp(argv[i], "--fflags=", 9) == 0) { setLuauFlags(argv[i] + 9); @@ -463,7 +507,7 @@ int main(int argc, char** argv) if (recordStats != RecordStats::None) { - FILE* fp = fopen("stats.json", "w"); + FILE* fp = fopen(statsFile.c_str(), "w"); if (!fp) { @@ -480,7 +524,8 @@ int main(int argc, char** argv) fprintf(fp, "{\n"); for (size_t i = 0; i < fileCount; ++i) { - fprintf(fp, "\"%s\": ", files[i].c_str()); + std::string escaped(escapeFilename(files[i])); + fprintf(fp, "\"%s\": ", escaped.c_str()); fileStats[i].serializeToJson(fp); fprintf(fp, i == (fileCount - 1) ? "\n" : ",\n"); } diff --git a/CodeGen/include/Luau/AssemblyBuilderX64.h b/CodeGen/include/Luau/AssemblyBuilderX64.h index 8a31e680..65d3ce0a 100644 --- a/CodeGen/include/Luau/AssemblyBuilderX64.h +++ b/CodeGen/include/Luau/AssemblyBuilderX64.h @@ -133,6 +133,7 @@ public: void vcvttsd2si(OperandX64 dst, OperandX64 src); void vcvtsi2sd(OperandX64 dst, OperandX64 src1, OperandX64 src2); void vcvtsd2ss(OperandX64 dst, OperandX64 src1, OperandX64 src2); + void vcvtss2sd(OperandX64 dst, OperandX64 src1, OperandX64 src2); void vroundsd(OperandX64 dst, OperandX64 src1, OperandX64 src2, RoundingModeX64 roundingMode); // inexact @@ -158,7 +159,6 @@ public: void vblendvpd(RegisterX64 dst, RegisterX64 src1, OperandX64 mask, RegisterX64 src3); - // Run final checks bool finalize(); @@ -228,6 +228,7 @@ private: void placeVex(OperandX64 dst, OperandX64 src1, OperandX64 src2, bool setW, uint8_t mode, uint8_t prefix); void placeImm8Or32(int32_t imm); void placeImm8(int32_t imm); + void placeImm16(int16_t imm); void placeImm32(int32_t imm); void placeImm64(int64_t imm); void placeLabel(Label& label); diff --git a/CodeGen/include/Luau/CodeGen.h b/CodeGen/include/Luau/CodeGen.h index 409bc22a..dfa3eeb0 100644 --- a/CodeGen/include/Luau/CodeGen.h +++ b/CodeGen/include/Luau/CodeGen.h @@ -80,6 +80,27 @@ struct AssemblyOptions void* annotatorContext = nullptr; }; +struct BlockLinearizationStats +{ + unsigned int constPropInstructionCount = 0; + double timeSeconds = 0.0; + + BlockLinearizationStats& operator+=(const BlockLinearizationStats& that) + { + this->constPropInstructionCount += that.constPropInstructionCount; + this->timeSeconds += that.timeSeconds; + + return *this; + } + + BlockLinearizationStats operator+(const BlockLinearizationStats& other) const + { + BlockLinearizationStats result(*this); + result += other; + return result; + } +}; + struct LoweringStats { unsigned totalFunctions = 0; @@ -94,6 +115,8 @@ struct LoweringStats int regAllocErrors = 0; int loweringErrors = 0; + BlockLinearizationStats blockLinearizationStats; + LoweringStats operator+(const LoweringStats& other) const { LoweringStats result(*this); @@ -113,6 +136,7 @@ struct LoweringStats this->maxBlockInstructions = std::max(this->maxBlockInstructions, that.maxBlockInstructions); this->regAllocErrors += that.regAllocErrors; this->loweringErrors += that.loweringErrors; + this->blockLinearizationStats += that.blockLinearizationStats; return *this; } }; diff --git a/CodeGen/include/Luau/IrData.h b/CodeGen/include/Luau/IrData.h index 19e082b5..33cae51c 100644 --- a/CodeGen/include/Luau/IrData.h +++ b/CodeGen/include/Luau/IrData.h @@ -251,7 +251,7 @@ enum class IrCmd : uint8_t // A: pointer (Table) DUP_TABLE, - // Insert an integer key into a table + // Insert an integer key into a table and return the pointer to inserted value (TValue) // A: pointer (Table) // B: int (key) TABLE_SETNUM, @@ -281,7 +281,7 @@ enum class IrCmd : uint8_t NUM_TO_UINT, // Adjust stack top (L->top) to point at 'B' TValues *after* the specified register - // This is used to return muliple values + // This is used to return multiple values // A: Rn // B: int (offset) ADJUST_STACK_TO_REG, @@ -420,6 +420,14 @@ enum class IrCmd : uint8_t // When undef is specified instead of a block, execution is aborted on check failure CHECK_NODE_VALUE, + // Guard against access at specified offset/size overflowing the buffer length + // A: pointer (buffer) + // B: int (offset) + // C: int (size) + // D: block/vmexit/undef + // When undef is specified instead of a block, execution is aborted on check failure + CHECK_BUFFER_LEN, + // Special operations // Check interrupt handler @@ -600,6 +608,10 @@ enum class IrCmd : uint8_t BITCOUNTLZ_UINT, BITCOUNTRZ_UINT, + // Swap byte order in A + // A: int + BYTESWAP_UINT, + // Calls native libm function with 1 or 2 arguments // A: builtin function ID // B: double @@ -617,6 +629,71 @@ enum class IrCmd : uint8_t // Find or create an upval at the given level // A: Rn (level) FINDUPVAL, + + // Read i8 (sign-extended to int) from buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + BUFFER_READI8, + + // Read u8 (zero-extended to int) from buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + BUFFER_READU8, + + // Write i8/u8 value (int argument is truncated) to buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + // C: int (value) + BUFFER_WRITEI8, + + // Read i16 (sign-extended to int) from buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + BUFFER_READI16, + + // Read u16 (zero-extended to int) from buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + BUFFER_READU16, + + // Write i16/u16 value (int argument is truncated) to buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + // C: int (value) + BUFFER_WRITEI16, + + // Read i32 value from buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + BUFFER_READI32, + + // Write i32/u32 value to buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + // C: int (value) + BUFFER_WRITEI32, + + // Read float value (converted to double) from buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + BUFFER_READF32, + + // Write float value (converted from double) to buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + // C: double (value) + BUFFER_WRITEF32, + + // Read double value from buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + BUFFER_READF64, + + // Write double value to buffer storage at specified offset + // A: pointer (buffer) + // B: int (offset) + // C: double (value) + BUFFER_WRITEF64, }; enum class IrConstKind : uint8_t diff --git a/CodeGen/include/Luau/IrUtils.h b/CodeGen/include/Luau/IrUtils.h index 3fe1800b..06c87448 100644 --- a/CodeGen/include/Luau/IrUtils.h +++ b/CodeGen/include/Luau/IrUtils.h @@ -128,6 +128,7 @@ inline bool isNonTerminatingJump(IrCmd cmd) case IrCmd::CHECK_SLOT_MATCH: case IrCmd::CHECK_NODE_NO_NEXT: case IrCmd::CHECK_NODE_VALUE: + case IrCmd::CHECK_BUFFER_LEN: return true; default: break; @@ -197,6 +198,13 @@ inline bool hasResult(IrCmd cmd) case IrCmd::GET_TYPEOF: case IrCmd::NEWCLOSURE: case IrCmd::FINDUPVAL: + case IrCmd::BUFFER_READI8: + case IrCmd::BUFFER_READU8: + case IrCmd::BUFFER_READI16: + case IrCmd::BUFFER_READU16: + case IrCmd::BUFFER_READI32: + case IrCmd::BUFFER_READF32: + case IrCmd::BUFFER_READF64: return true; default: break; diff --git a/CodeGen/src/AssemblyBuilderX64.cpp b/CodeGen/src/AssemblyBuilderX64.cpp index 6fdeac27..22978dd4 100644 --- a/CodeGen/src/AssemblyBuilderX64.cpp +++ b/CodeGen/src/AssemblyBuilderX64.cpp @@ -175,6 +175,12 @@ void AssemblyBuilderX64::mov(OperandX64 lhs, OperandX64 rhs) place(OP_PLUS_REG(0xb0, lhs.base.index)); placeImm8(rhs.imm); } + else if (size == SizeX64::word) + { + place(0x66); + place(OP_PLUS_REG(0xb8, lhs.base.index)); + placeImm16(rhs.imm); + } else if (size == SizeX64::dword) { place(OP_PLUS_REG(0xb8, lhs.base.index)); @@ -200,6 +206,13 @@ void AssemblyBuilderX64::mov(OperandX64 lhs, OperandX64 rhs) placeModRegMem(lhs, 0, /*extraCodeBytes=*/1); placeImm8(rhs.imm); } + else if (size == SizeX64::word) + { + place(0x66); + place(0xc7); + placeModRegMem(lhs, 0, /*extraCodeBytes=*/2); + placeImm16(rhs.imm); + } else { LUAU_ASSERT(size == SizeX64::dword || size == SizeX64::qword); @@ -780,6 +793,16 @@ void AssemblyBuilderX64::vcvtsd2ss(OperandX64 dst, OperandX64 src1, OperandX64 s placeAvx("vcvtsd2ss", dst, src1, src2, 0x5a, (src2.cat == CategoryX64::reg ? src2.base.size : src2.memSize) == SizeX64::qword, AVX_0F, AVX_F2); } +void AssemblyBuilderX64::vcvtss2sd(OperandX64 dst, OperandX64 src1, OperandX64 src2) +{ + if (src2.cat == CategoryX64::reg) + LUAU_ASSERT(src2.base.size == SizeX64::xmmword); + else + LUAU_ASSERT(src2.memSize == SizeX64::dword); + + placeAvx("vcvtsd2ss", dst, src1, src2, 0x5a, false, AVX_0F, AVX_F3); +} + void AssemblyBuilderX64::vroundsd(OperandX64 dst, OperandX64 src1, OperandX64 src2, RoundingModeX64 roundingMode) { placeAvx("vroundsd", dst, src1, src2, uint8_t(roundingMode) | kRoundingPrecisionInexact, 0x0b, false, AVX_0F3A, AVX_66); @@ -1086,7 +1109,10 @@ void AssemblyBuilderX64::placeBinaryRegAndRegMem(OperandX64 lhs, OperandX64 rhs, LUAU_ASSERT(lhs.base.size == (rhs.cat == CategoryX64::reg ? rhs.base.size : rhs.memSize)); SizeX64 size = lhs.base.size; - LUAU_ASSERT(size == SizeX64::byte || size == SizeX64::dword || size == SizeX64::qword); + LUAU_ASSERT(size == SizeX64::byte || size == SizeX64::word || size == SizeX64::dword || size == SizeX64::qword); + + if (size == SizeX64::word) + place(0x66); placeRex(lhs.base, rhs); place(size == SizeX64::byte ? code8 : code); @@ -1417,6 +1443,13 @@ void AssemblyBuilderX64::placeImm8(int32_t imm) LUAU_ASSERT(!"Invalid immediate value"); } +void AssemblyBuilderX64::placeImm16(int16_t imm) +{ + uint8_t* pos = codePos; + LUAU_ASSERT(pos + sizeof(imm) < codeEnd); + codePos = writeu16(pos, imm); +} + void AssemblyBuilderX64::placeImm32(int32_t imm) { uint8_t* pos = codePos; diff --git a/CodeGen/src/ByteUtils.h b/CodeGen/src/ByteUtils.h index 70e27097..2c70ef6c 100644 --- a/CodeGen/src/ByteUtils.h +++ b/CodeGen/src/ByteUtils.h @@ -15,6 +15,16 @@ inline uint8_t* writeu8(uint8_t* target, uint8_t value) return target + sizeof(value); } +inline uint8_t* writeu16(uint8_t* target, uint16_t value) +{ +#if defined(LUAU_BIG_ENDIAN) + value = htole16(value); +#endif + + memcpy(target, &value, sizeof(value)); + return target + sizeof(value); +} + inline uint8_t* writeu32(uint8_t* target, uint32_t value) { #if defined(LUAU_BIG_ENDIAN) diff --git a/CodeGen/src/CodeGenLower.h b/CodeGen/src/CodeGenLower.h index 8fcd832f..484d2dab 100644 --- a/CodeGen/src/CodeGenLower.h +++ b/CodeGen/src/CodeGenLower.h @@ -50,6 +50,13 @@ inline void gatherFunctions(std::vector& results, Proto* proto, unsigned gatherFunctions(results, proto->p[i], flags); } +inline unsigned getInstructionCount(const std::vector& instructions, IrCmd cmd) +{ + return unsigned(std::count_if(instructions.begin(), instructions.end(), [&cmd](const IrInst& inst) { + return inst.cmd == cmd; + })); +} + template inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& function, const std::vector& sortedBlocks, int bytecodeid, AssemblyOptions options) @@ -269,7 +276,25 @@ inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers& constPropInBlockChains(ir, useValueNumbering); if (!FFlag::DebugCodegenOptSize) + { + double startTime = 0.0; + unsigned constPropInstructionCount = 0; + + if (stats) + { + constPropInstructionCount = getInstructionCount(ir.function.instructions, IrCmd::SUBSTITUTE); + startTime = lua_clock(); + } + createLinearBlocks(ir, useValueNumbering); + + if (stats) + { + stats->blockLinearizationStats.timeSeconds += lua_clock() - startTime; + constPropInstructionCount = getInstructionCount(ir.function.instructions, IrCmd::SUBSTITUTE) - constPropInstructionCount; + stats->blockLinearizationStats.constPropInstructionCount += constPropInstructionCount; + } + } } std::vector sortedBlocks = getSortedBlockOrder(ir.function); diff --git a/CodeGen/src/CodeGenUtils.cpp b/CodeGen/src/CodeGenUtils.cpp index 3cdd20b3..9306ae4c 100644 --- a/CodeGen/src/CodeGenUtils.cpp +++ b/CodeGen/src/CodeGenUtils.cpp @@ -531,50 +531,6 @@ const Instruction* executeSETTABLEKS(lua_State* L, const Instruction* pc, StkId } } -const Instruction* executeNEWCLOSURE(lua_State* L, const Instruction* pc, StkId base, TValue* k) -{ - [[maybe_unused]] Closure* cl = clvalue(L->ci->func); - Instruction insn = *pc++; - StkId ra = VM_REG(LUAU_INSN_A(insn)); - - Proto* pv = cl->l.p->p[LUAU_INSN_D(insn)]; - LUAU_ASSERT(unsigned(LUAU_INSN_D(insn)) < unsigned(cl->l.p->sizep)); - - VM_PROTECT_PC(); // luaF_newLclosure may fail due to OOM - - // note: we save closure to stack early in case the code below wants to capture it by value - Closure* ncl = luaF_newLclosure(L, pv->nups, cl->env, pv); - setclvalue(L, ra, ncl); - - for (int ui = 0; ui < pv->nups; ++ui) - { - Instruction uinsn = *pc++; - LUAU_ASSERT(LUAU_INSN_OP(uinsn) == LOP_CAPTURE); - - switch (LUAU_INSN_A(uinsn)) - { - case LCT_VAL: - setobj(L, &ncl->l.uprefs[ui], VM_REG(LUAU_INSN_B(uinsn))); - break; - - case LCT_REF: - setupvalue(L, &ncl->l.uprefs[ui], luaF_findupval(L, VM_REG(LUAU_INSN_B(uinsn)))); - break; - - case LCT_UPVAL: - setobj(L, &ncl->l.uprefs[ui], VM_UV(LUAU_INSN_B(uinsn))); - break; - - default: - LUAU_ASSERT(!"Unknown upvalue capture type"); - LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks - } - } - - VM_PROTECT(luaC_checkGC(L)); - return pc; -} - const Instruction* executeNAMECALL(lua_State* L, const Instruction* pc, StkId base, TValue* k) { [[maybe_unused]] Closure* cl = clvalue(L->ci->func); @@ -587,43 +543,19 @@ const Instruction* executeNAMECALL(lua_State* L, const Instruction* pc, StkId ba if (ttistable(rb)) { - Table* h = hvalue(rb); - // note: we can't use nodemask8 here because we need to query the main position of the table, and 8-bit nodemask8 only works - // for predictive lookups - LuaNode* n = &h->node[tsvalue(kv)->hash & (sizenode(h) - 1)]; + // note: lvmexecute.cpp version of NAMECALL has two fast paths, but both fast paths are inlined into IR + // as such, if we get here we can just use the generic path which makes the fallback path a little faster - const TValue* mt = 0; - const LuaNode* mtn = 0; - - // fast-path: key is in the table in expected slot - if (ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv) && !ttisnil(gval(n))) - { - // note: order of copies allows rb to alias ra+1 or ra - setobj2s(L, ra + 1, rb); - setobj2s(L, ra, gval(n)); - } - // fast-path: key is absent from the base, table has an __index table, and it has the result in the expected slot - else if (gnext(n) == 0 && (mt = fasttm(L, hvalue(rb)->metatable, TM_INDEX)) && ttistable(mt) && - (mtn = &hvalue(mt)->node[LUAU_INSN_C(insn) & hvalue(mt)->nodemask8]) && ttisstring(gkey(mtn)) && tsvalue(gkey(mtn)) == tsvalue(kv) && - !ttisnil(gval(mtn))) - { - // note: order of copies allows rb to alias ra+1 or ra - setobj2s(L, ra + 1, rb); - setobj2s(L, ra, gval(mtn)); - } - else - { - // slow-path: handles full table lookup - setobj2s(L, ra + 1, rb); - L->cachedslot = LUAU_INSN_C(insn); - VM_PROTECT(luaV_gettable(L, rb, kv, ra)); - // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ - VM_PATCH_C(pc - 2, L->cachedslot); - // recompute ra since stack might have been reallocated - ra = VM_REG(LUAU_INSN_A(insn)); - if (ttisnil(ra)) - luaG_methoderror(L, ra + 1, tsvalue(kv)); - } + // slow-path: handles full table lookup + setobj2s(L, ra + 1, rb); + L->cachedslot = LUAU_INSN_C(insn); + VM_PROTECT(luaV_gettable(L, rb, kv, ra)); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + // recompute ra since stack might have been reallocated + ra = VM_REG(LUAU_INSN_A(insn)); + if (ttisnil(ra)) + luaG_methoderror(L, ra + 1, tsvalue(kv)); } else { diff --git a/CodeGen/src/CodeGenUtils.h b/CodeGen/src/CodeGenUtils.h index 7075e348..515a81f0 100644 --- a/CodeGen/src/CodeGenUtils.h +++ b/CodeGen/src/CodeGenUtils.h @@ -25,7 +25,6 @@ const Instruction* executeGETGLOBAL(lua_State* L, const Instruction* pc, StkId b const Instruction* executeSETGLOBAL(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeGETTABLEKS(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeSETTABLEKS(lua_State* L, const Instruction* pc, StkId base, TValue* k); -const Instruction* executeNEWCLOSURE(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeNAMECALL(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeSETLIST(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeFORGPREP(lua_State* L, const Instruction* pc, StkId base, TValue* k); diff --git a/CodeGen/src/IrDump.cpp b/CodeGen/src/IrDump.cpp index 483e3e00..7893d076 100644 --- a/CodeGen/src/IrDump.cpp +++ b/CodeGen/src/IrDump.cpp @@ -235,6 +235,8 @@ const char* getCmdName(IrCmd cmd) return "CHECK_NODE_NO_NEXT"; case IrCmd::CHECK_NODE_VALUE: return "CHECK_NODE_VALUE"; + case IrCmd::CHECK_BUFFER_LEN: + return "CHECK_BUFFER_LEN"; case IrCmd::INTERRUPT: return "INTERRUPT"; case IrCmd::CHECK_GC: @@ -309,6 +311,8 @@ const char* getCmdName(IrCmd cmd) return "BITCOUNTLZ_UINT"; case IrCmd::BITCOUNTRZ_UINT: return "BITCOUNTRZ_UINT"; + case IrCmd::BYTESWAP_UINT: + return "BYTESWAP_UINT"; case IrCmd::INVOKE_LIBM: return "INVOKE_LIBM"; case IrCmd::GET_TYPE: @@ -317,6 +321,30 @@ const char* getCmdName(IrCmd cmd) return "GET_TYPEOF"; case IrCmd::FINDUPVAL: return "FINDUPVAL"; + case IrCmd::BUFFER_READI8: + return "BUFFER_READI8"; + case IrCmd::BUFFER_READU8: + return "BUFFER_READU8"; + case IrCmd::BUFFER_WRITEI8: + return "BUFFER_WRITEI8"; + case IrCmd::BUFFER_READI16: + return "BUFFER_READI16"; + case IrCmd::BUFFER_READU16: + return "BUFFER_READU16"; + case IrCmd::BUFFER_WRITEI16: + return "BUFFER_WRITEI16"; + case IrCmd::BUFFER_READI32: + return "BUFFER_READI32"; + case IrCmd::BUFFER_WRITEI32: + return "BUFFER_WRITEI32"; + case IrCmd::BUFFER_READF32: + return "BUFFER_READF32"; + case IrCmd::BUFFER_WRITEF32: + return "BUFFER_WRITEF32"; + case IrCmd::BUFFER_READF64: + return "BUFFER_READF64"; + case IrCmd::BUFFER_WRITEF64: + return "BUFFER_WRITEF64"; } LUAU_UNREACHABLE(); diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 26a3b887..42450d3c 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -135,13 +135,9 @@ static void checkObjectBarrierConditions(AssemblyBuilderA64& build, RegisterA64 if (ratag == -1 || !isGCO(ratag)) { if (ra.kind == IrOpKind::VmReg) - { addr = mem(rBase, vmRegOp(ra) * sizeof(TValue) + offsetof(TValue, tt)); - } else if (ra.kind == IrOpKind::VmConst) - { emitAddOffset(build, temp, rConstants, vmConstOp(ra) * sizeof(TValue) + offsetof(TValue, tt)); - } build.ldr(tempw, addr); build.cmp(tempw, LUA_TSTRING); @@ -154,13 +150,10 @@ static void checkObjectBarrierConditions(AssemblyBuilderA64& build, RegisterA64 // iswhite(gcvalue(ra)) if (ra.kind == IrOpKind::VmReg) - { addr = mem(rBase, vmRegOp(ra) * sizeof(TValue) + offsetof(TValue, value)); - } else if (ra.kind == IrOpKind::VmConst) - { emitAddOffset(build, temp, rConstants, vmConstOp(ra) * sizeof(TValue) + offsetof(TValue, value)); - } + build.ldr(temp, addr); build.ldrb(tempw, mem(temp, offsetof(GCheader, marked))); build.tst(tempw, bit2mask(WHITE0BIT, WHITE1BIT)); @@ -240,6 +233,14 @@ static bool emitBuiltin( } } +static uint64_t getDoubleBits(double value) +{ + uint64_t result; + static_assert(sizeof(result) == sizeof(value), "Expecting double to be 64-bit"); + memcpy(&result, &value, sizeof(value)); + return result; +} + IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, IrFunction& function, LoweringStats* stats) : build(build) , helpers(helpers) @@ -309,7 +310,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) if (inst.b.kind == IrOpKind::Inst) { - build.add(inst.regA64, inst.regA64, regOp(inst.b), kTValueSizeLog2); + build.add(inst.regA64, inst.regA64, regOp(inst.b), kTValueSizeLog2); // implicit uxtw } else if (inst.b.kind == IrOpKind::Constant) { @@ -409,9 +410,16 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } case IrCmd::STORE_DOUBLE: { - RegisterA64 temp = tempDouble(inst.b); AddressA64 addr = tempAddr(inst.a, offsetof(TValue, value)); - build.str(temp, addr); + if (inst.b.kind == IrOpKind::Constant && getDoubleBits(doubleOp(inst.b)) == 0) + { + build.str(xzr, addr); + } + else + { + RegisterA64 temp = tempDouble(inst.b); + build.str(temp, addr); + } break; } case IrCmd::STORE_INT: @@ -816,11 +824,12 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) { RegisterA64 index = tempDouble(inst.a); RegisterA64 limit = tempDouble(inst.b); + RegisterA64 step = tempDouble(inst.c); Label direct; // step > 0 - build.fcmpz(tempDouble(inst.c)); + build.fcmpz(step); build.b(getConditionFP(IrCondition::Greater), direct); // !(limit <= index) @@ -974,6 +983,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) { inst.regA64 = regs.allocReg(KindA64::w, index); RegisterA64 temp = tempDouble(inst.a); + // note: we don't use fcvtzu for consistency with C++ code build.fcvtzs(castReg(KindA64::x, inst.regA64), temp); break; } @@ -989,7 +999,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) else if (inst.b.kind == IrOpKind::Inst) { build.add(temp, rBase, uint16_t(vmRegOp(inst.a) * sizeof(TValue))); - build.add(temp, temp, regOp(inst.b), kTValueSizeLog2); + build.add(temp, temp, regOp(inst.b), kTValueSizeLog2); // implicit uxtw build.str(temp, mem(rState, offsetof(lua_State, top))); } else @@ -1372,6 +1382,63 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) finalizeTargetLabel(inst.b, fresh); break; } + case IrCmd::CHECK_BUFFER_LEN: + { + int accessSize = intOp(inst.c); + LUAU_ASSERT(accessSize > 0 && accessSize <= int(AssemblyBuilderA64::kMaxImmediate)); + + Label fresh; // used when guard aborts execution or jumps to a VM exit + Label& target = getTargetLabel(inst.d, fresh); + + RegisterA64 temp = regs.allocTemp(KindA64::w); + build.ldr(temp, mem(regOp(inst.a), offsetof(Buffer, len))); + + if (inst.b.kind == IrOpKind::Inst) + { + if (accessSize == 1) + { + // fails if offset >= len + build.cmp(temp, regOp(inst.b)); + build.b(ConditionA64::UnsignedLessEqual, target); + } + else + { + // fails if offset + size >= len; we compute it as len - offset <= size + RegisterA64 tempx = castReg(KindA64::x, temp); + build.sub(tempx, tempx, regOp(inst.b)); // implicit uxtw + build.cmp(tempx, uint16_t(accessSize)); + build.b(ConditionA64::LessEqual, target); // note: this is a signed 64-bit comparison so that out of bounds offset fails + } + } + else if (inst.b.kind == IrOpKind::Constant) + { + int offset = intOp(inst.b); + + // Constant folding can take care of it, but for safety we avoid overflow/underflow cases here + if (offset < 0 || unsigned(offset) + unsigned(accessSize) >= unsigned(INT_MAX)) + { + build.b(target); + } + else if (offset + accessSize <= int(AssemblyBuilderA64::kMaxImmediate)) + { + build.cmp(temp, uint16_t(offset + accessSize)); + build.b(ConditionA64::UnsignedLessEqual, target); + } + else + { + RegisterA64 temp2 = regs.allocTemp(KindA64::w); + build.mov(temp2, offset + accessSize); + build.cmp(temp, temp2); + build.b(ConditionA64::UnsignedLessEqual, target); + } + } + else + { + LUAU_ASSERT(!"Unsupported instruction form"); + } + finalizeTargetLabel(inst.d, fresh); + break; + } case IrCmd::INTERRUPT: { regs.spill(build, index); @@ -1912,6 +1979,13 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.clz(inst.regA64, inst.regA64); break; } + case IrCmd::BYTESWAP_UINT: + { + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.a}); + RegisterA64 temp = tempUint(inst.a); + build.rev(inst.regA64, temp); + break; + } case IrCmd::INVOKE_LIBM: { if (inst.c.kind != IrOpKind::None) @@ -1960,7 +2034,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) LUAU_ASSERT(sizeof(TString*) == 8); if (inst.a.kind == IrOpKind::Inst) - build.add(inst.regA64, rGlobalState, regOp(inst.a), 3); + build.add(inst.regA64, rGlobalState, regOp(inst.a), 3); // implicit uxtw else if (inst.a.kind == IrOpKind::Constant) build.add(inst.regA64, rGlobalState, uint16_t(tagOp(inst.a)) * 8); else @@ -1993,6 +2067,118 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) break; } + case IrCmd::BUFFER_READI8: + { + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.b}); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.ldrsb(inst.regA64, addr); + break; + } + + case IrCmd::BUFFER_READU8: + { + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.b}); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.ldrb(inst.regA64, addr); + break; + } + + case IrCmd::BUFFER_WRITEI8: + { + RegisterA64 temp = tempInt(inst.c); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.strb(temp, addr); + break; + } + + case IrCmd::BUFFER_READI16: + { + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.b}); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.ldrsh(inst.regA64, addr); + break; + } + + case IrCmd::BUFFER_READU16: + { + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.b}); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.ldrh(inst.regA64, addr); + break; + } + + case IrCmd::BUFFER_WRITEI16: + { + RegisterA64 temp = tempInt(inst.c); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.strh(temp, addr); + break; + } + + case IrCmd::BUFFER_READI32: + { + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.b}); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.ldr(inst.regA64, addr); + break; + } + + case IrCmd::BUFFER_WRITEI32: + { + RegisterA64 temp = tempInt(inst.c); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.str(temp, addr); + break; + } + + case IrCmd::BUFFER_READF32: + { + inst.regA64 = regs.allocReg(KindA64::d, index); + RegisterA64 temp = castReg(KindA64::s, inst.regA64); // safe to alias a fresh register + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.ldr(temp, addr); + build.fcvt(inst.regA64, temp); + break; + } + + case IrCmd::BUFFER_WRITEF32: + { + RegisterA64 temp1 = tempDouble(inst.c); + RegisterA64 temp2 = regs.allocTemp(KindA64::s); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.fcvt(temp2, temp1); + build.str(temp2, addr); + break; + } + + case IrCmd::BUFFER_READF64: + { + inst.regA64 = regs.allocReg(KindA64::d, index); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.ldr(inst.regA64, addr); + break; + } + + case IrCmd::BUFFER_WRITEF64: + { + RegisterA64 temp = tempDouble(inst.c); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b); + + build.str(temp, addr); + break; + } + // To handle unsupported instructions, add "case IrCmd::OP" and make sure to set error = true! } @@ -2119,9 +2305,7 @@ RegisterA64 IrLoweringA64::tempDouble(IrOp op) RegisterA64 temp1 = regs.allocTemp(KindA64::x); RegisterA64 temp2 = regs.allocTemp(KindA64::d); - uint64_t vali; - static_assert(sizeof(vali) == sizeof(val), "Expecting double to be 64-bit"); - memcpy(&vali, &val, sizeof(val)); + uint64_t vali = getDoubleBits(val); if ((vali << 16) == 0) { @@ -2217,6 +2401,35 @@ AddressA64 IrLoweringA64::tempAddr(IrOp op, int offset) } } +AddressA64 IrLoweringA64::tempAddrBuffer(IrOp bufferOp, IrOp indexOp) +{ + if (indexOp.kind == IrOpKind::Inst) + { + RegisterA64 temp = regs.allocTemp(KindA64::x); + build.add(temp, regOp(bufferOp), regOp(indexOp)); // implicit uxtw + return mem(temp, offsetof(Buffer, data)); + } + else if (indexOp.kind == IrOpKind::Constant) + { + // Since the resulting address may be used to load any size, including 1 byte, from an unaligned offset, we are limited by unscaled encoding + if (unsigned(intOp(indexOp)) + offsetof(Buffer, data) <= 255) + return mem(regOp(bufferOp), int(intOp(indexOp) + offsetof(Buffer, data))); + + // indexOp can only be negative in dead code (since offsets are checked); this avoids assertion in emitAddOffset + if (intOp(indexOp) < 0) + return mem(regOp(bufferOp), offsetof(Buffer, data)); + + RegisterA64 temp = regs.allocTemp(KindA64::x); + emitAddOffset(build, temp, regOp(bufferOp), size_t(intOp(indexOp))); + return mem(temp, offsetof(Buffer, data)); + } + else + { + LUAU_ASSERT(!"Unsupported instruction form"); + return noreg; + } +} + RegisterA64 IrLoweringA64::regOp(IrOp op) { IrInst& inst = function.instOp(op); diff --git a/CodeGen/src/IrLoweringA64.h b/CodeGen/src/IrLoweringA64.h index 46f41021..5fb7f2b8 100644 --- a/CodeGen/src/IrLoweringA64.h +++ b/CodeGen/src/IrLoweringA64.h @@ -44,6 +44,7 @@ struct IrLoweringA64 RegisterA64 tempInt(IrOp op); RegisterA64 tempUint(IrOp op); AddressA64 tempAddr(IrOp op, int offset); + AddressA64 tempAddrBuffer(IrOp bufferOp, IrOp indexOp); // May emit restore instructions RegisterA64 regOp(IrOp op); diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index b9ff4f1f..f7572a6c 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -219,22 +219,26 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.mov(luauRegValue(vmRegOp(inst.a)), regOp(inst.b)); break; case IrCmd::STORE_DOUBLE: + { + OperandX64 valueLhs = inst.a.kind == IrOpKind::Inst ? qword[regOp(inst.a) + offsetof(TValue, value)] : luauRegValue(vmRegOp(inst.a)); + if (inst.b.kind == IrOpKind::Constant) { ScopedRegX64 tmp{regs, SizeX64::xmmword}; build.vmovsd(tmp.reg, build.f64(doubleOp(inst.b))); - build.vmovsd(luauRegValue(vmRegOp(inst.a)), tmp.reg); + build.vmovsd(valueLhs, tmp.reg); } else if (inst.b.kind == IrOpKind::Inst) { - build.vmovsd(luauRegValue(vmRegOp(inst.a)), regOp(inst.b)); + build.vmovsd(valueLhs, regOp(inst.b)); } else { LUAU_ASSERT(!"Unsupported instruction form"); } break; + } case IrCmd::STORE_INT: if (inst.b.kind == IrOpKind::Constant) build.mov(luauRegValueInt(vmRegOp(inst.a)), intOp(inst.b)); @@ -822,7 +826,19 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) case IrCmd::UINT_TO_NUM: inst.regX64 = regs.allocReg(SizeX64::xmmword, index); - build.vcvtsi2sd(inst.regX64, inst.regX64, qwordReg(regOp(inst.a))); + // AVX has no uint->double conversion; the source must come from UINT op and they all should clear top 32 bits so we can usually + // use 64-bit reg; the one exception is NUM_TO_UINT which doesn't clear top bits + if (IrCmd source = function.instOp(inst.a).cmd; source == IrCmd::NUM_TO_UINT) + { + ScopedRegX64 tmp{regs, SizeX64::dword}; + build.mov(tmp.reg, regOp(inst.a)); + build.vcvtsi2sd(inst.regX64, inst.regX64, qwordReg(tmp.reg)); + } + else + { + LUAU_ASSERT(source != IrCmd::SUBSTITUTE); // we don't process substitutions + build.vcvtsi2sd(inst.regX64, inst.regX64, qwordReg(regOp(inst.a))); + } break; case IrCmd::NUM_TO_INT: inst.regX64 = regs.allocReg(SizeX64::dword, index); @@ -1157,6 +1173,64 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) jumpOrAbortOnUndef(ConditionX64::Equal, inst.b, next); break; } + case IrCmd::CHECK_BUFFER_LEN: + { + int accessSize = intOp(inst.c); + LUAU_ASSERT(accessSize > 0); + + if (inst.b.kind == IrOpKind::Inst) + { + if (accessSize == 1) + { + // Simpler check for a single byte access + build.cmp(dword[regOp(inst.a) + offsetof(Buffer, len)], regOp(inst.b)); + jumpOrAbortOnUndef(ConditionX64::BelowEqual, inst.d, next); + } + else + { + ScopedRegX64 tmp1{regs, SizeX64::qword}; + ScopedRegX64 tmp2{regs, SizeX64::dword}; + + // To perform the bounds check using a single branch, we take index that is limited to 32 bit int + // Access size is then added using a 64 bit addition + // This will make sure that addition will not wrap around for values like 0xffffffff + + if (IrCmd source = function.instOp(inst.b).cmd; source == IrCmd::NUM_TO_INT) + { + // When previous operation is a conversion to an integer (common case), it is guaranteed to have high register bits cleared + build.lea(tmp1.reg, addr[qwordReg(regOp(inst.b)) + accessSize]); + } + else + { + // When the source of the index is unknown, it could contain garbage in the high bits, so we zero-extend it explicitly + build.mov(dwordReg(tmp1.reg), regOp(inst.b)); + build.add(tmp1.reg, accessSize); + } + + build.mov(tmp2.reg, dword[regOp(inst.a) + offsetof(Buffer, len)]); + build.cmp(qwordReg(tmp2.reg), tmp1.reg); + + jumpOrAbortOnUndef(ConditionX64::Below, inst.d, next); + } + } + else if (inst.b.kind == IrOpKind::Constant) + { + int offset = intOp(inst.b); + + // Constant folding can take care of it, but for safety we avoid overflow/underflow cases here + if (offset < 0 || unsigned(offset) + unsigned(accessSize) >= unsigned(INT_MAX)) + jumpOrAbortOnUndef(inst.d, next); + else + build.cmp(dword[regOp(inst.a) + offsetof(Buffer, len)], offset + accessSize); + + jumpOrAbortOnUndef(ConditionX64::Below, inst.d, next); + } + else + { + LUAU_ASSERT(!"Unsupported instruction form"); + } + break; + } case IrCmd::INTERRUPT: { unsigned pcpos = uintOp(inst.a); @@ -1633,6 +1707,16 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.setLabel(exit); break; } + case IrCmd::BYTESWAP_UINT: + { + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a}); + + if (inst.a.kind != IrOpKind::Inst || inst.regX64 != regOp(inst.a)) + build.mov(inst.regX64, memRegUintOp(inst.a)); + + build.bswap(inst.regX64); + break; + } case IrCmd::INVOKE_LIBM: { IrCallWrapperX64 callWrap(regs, build, index); @@ -1689,6 +1773,93 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) break; } + case IrCmd::BUFFER_READI8: + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a, inst.b}); + + build.movsx(inst.regX64, byte[bufferAddrOp(inst.a, inst.b)]); + break; + + case IrCmd::BUFFER_READU8: + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a, inst.b}); + + build.movzx(inst.regX64, byte[bufferAddrOp(inst.a, inst.b)]); + break; + + case IrCmd::BUFFER_WRITEI8: + { + OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); + + build.mov(byte[bufferAddrOp(inst.a, inst.b)], value); + break; + } + + case IrCmd::BUFFER_READI16: + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a, inst.b}); + + build.movsx(inst.regX64, word[bufferAddrOp(inst.a, inst.b)]); + break; + + case IrCmd::BUFFER_READU16: + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a, inst.b}); + + build.movzx(inst.regX64, word[bufferAddrOp(inst.a, inst.b)]); + break; + + case IrCmd::BUFFER_WRITEI16: + { + OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); + + build.mov(word[bufferAddrOp(inst.a, inst.b)], value); + break; + } + + case IrCmd::BUFFER_READI32: + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a, inst.b}); + + build.mov(inst.regX64, dword[bufferAddrOp(inst.a, inst.b)]); + break; + + case IrCmd::BUFFER_WRITEI32: + { + OperandX64 value = inst.c.kind == IrOpKind::Inst ? regOp(inst.c) : OperandX64(intOp(inst.c)); + + build.mov(dword[bufferAddrOp(inst.a, inst.b)], value); + break; + } + + case IrCmd::BUFFER_READF32: + inst.regX64 = regs.allocReg(SizeX64::xmmword, index); + + build.vcvtss2sd(inst.regX64, inst.regX64, dword[bufferAddrOp(inst.a, inst.b)]); + break; + + case IrCmd::BUFFER_WRITEF32: + storeDoubleAsFloat(dword[bufferAddrOp(inst.a, inst.b)], inst.c); + break; + + case IrCmd::BUFFER_READF64: + inst.regX64 = regs.allocReg(SizeX64::xmmword, index); + + build.vmovsd(inst.regX64, qword[bufferAddrOp(inst.a, inst.b)]); + break; + + case IrCmd::BUFFER_WRITEF64: + if (inst.c.kind == IrOpKind::Constant) + { + ScopedRegX64 tmp{regs, SizeX64::xmmword}; + build.vmovsd(tmp.reg, build.f64(doubleOp(inst.c))); + build.vmovsd(qword[bufferAddrOp(inst.a, inst.b)], tmp.reg); + } + else if (inst.c.kind == IrOpKind::Inst) + { + build.vmovsd(qword[bufferAddrOp(inst.a, inst.b)], regOp(inst.c)); + } + else + { + LUAU_ASSERT(!"Unsupported instruction form"); + } + break; + // Pseudo instructions case IrCmd::NOP: case IrCmd::SUBSTITUTE: @@ -1900,6 +2071,17 @@ RegisterX64 IrLoweringX64::regOp(IrOp op) return inst.regX64; } +OperandX64 IrLoweringX64::bufferAddrOp(IrOp bufferOp, IrOp indexOp) +{ + if (indexOp.kind == IrOpKind::Inst) + return regOp(bufferOp) + qwordReg(regOp(indexOp)) + offsetof(Buffer, data); + else if (indexOp.kind == IrOpKind::Constant) + return regOp(bufferOp) + intOp(indexOp) + offsetof(Buffer, data); + + LUAU_ASSERT(!"Unsupported instruction form"); + return noreg; +} + IrConst IrLoweringX64::constOp(IrOp op) const { return function.constOp(op); diff --git a/CodeGen/src/IrLoweringX64.h b/CodeGen/src/IrLoweringX64.h index 920ad002..5f12b303 100644 --- a/CodeGen/src/IrLoweringX64.h +++ b/CodeGen/src/IrLoweringX64.h @@ -50,6 +50,7 @@ struct IrLoweringX64 OperandX64 memRegUintOp(IrOp op); OperandX64 memRegTagOp(IrOp op); RegisterX64 regOp(IrOp op); + OperandX64 bufferAddrOp(IrOp bufferOp, IrOp indexOp); IrConst constOp(IrOp op) const; uint8_t tagOp(IrOp op) const; diff --git a/CodeGen/src/IrTranslateBuiltins.cpp b/CodeGen/src/IrTranslateBuiltins.cpp index 3b6b5def..6aec01a5 100644 --- a/CodeGen/src/IrTranslateBuiltins.cpp +++ b/CodeGen/src/IrTranslateBuiltins.cpp @@ -8,6 +8,9 @@ #include +LUAU_FASTFLAGVARIABLE(LuauBufferTranslateIr, false) +LUAU_FASTFLAGVARIABLE(LuauImproveInsertIr, false) + // TODO: when nresults is less than our actual result count, we can skip computing/writing unused results static const int kMinMaxUnrolledParams = 5; @@ -150,13 +153,12 @@ static BuiltinImplResult translateBuiltinMathDegRad(IrBuilder& build, IrCmd cmd, return {BuiltinImplType::Full, 1}; } -static BuiltinImplResult translateBuiltinMathLog( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) +static BuiltinImplResult translateBuiltinMathLog(IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) { if (nparams < 1 || nresults > 1) return {BuiltinImplType::None, -1}; - int libmId = bfid; + int libmId = LBF_MATH_LOG; std::optional denom; if (nparams != 1) @@ -298,7 +300,7 @@ static BuiltinImplResult translateBuiltinTypeof(IrBuilder& build, int nparams, i } static BuiltinImplResult translateBuiltinBit32BinaryOp( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) + IrBuilder& build, IrCmd cmd, bool btest, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) { if (nparams < 2 || nparams > kBit32BinaryOpUnrolledParams || nresults > 1) return {BuiltinImplType::None, -1}; @@ -315,17 +317,6 @@ static BuiltinImplResult translateBuiltinBit32BinaryOp( IrOp vaui = build.inst(IrCmd::NUM_TO_UINT, va); IrOp vbui = build.inst(IrCmd::NUM_TO_UINT, vb); - - IrCmd cmd = IrCmd::NOP; - if (bfid == LBF_BIT32_BAND || bfid == LBF_BIT32_BTEST) - cmd = IrCmd::BITAND_UINT; - else if (bfid == LBF_BIT32_BXOR) - cmd = IrCmd::BITXOR_UINT; - else if (bfid == LBF_BIT32_BOR) - cmd = IrCmd::BITOR_UINT; - - LUAU_ASSERT(cmd != IrCmd::NOP); - IrOp res = build.inst(cmd, vaui, vbui); for (int i = 3; i <= nparams; ++i) @@ -336,7 +327,7 @@ static BuiltinImplResult translateBuiltinBit32BinaryOp( res = build.inst(cmd, res, arg); } - if (bfid == LBF_BIT32_BTEST) + if (btest) { IrOp falsey = build.block(IrBlockKind::Internal); IrOp truthy = build.block(IrBlockKind::Internal); @@ -351,7 +342,6 @@ static BuiltinImplResult translateBuiltinBit32BinaryOp( build.inst(IrCmd::STORE_INT, build.vmReg(ra), build.constInt(1)); build.inst(IrCmd::JUMP, exit); - build.beginBlock(exit); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TBOOLEAN)); } @@ -367,8 +357,7 @@ static BuiltinImplResult translateBuiltinBit32BinaryOp( return {BuiltinImplType::Full, 1}; } -static BuiltinImplResult translateBuiltinBit32Bnot( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) +static BuiltinImplResult translateBuiltinBit32Bnot(IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) { if (nparams < 1 || nresults > 1) return {BuiltinImplType::None, -1}; @@ -389,7 +378,7 @@ static BuiltinImplResult translateBuiltinBit32Bnot( } static BuiltinImplResult translateBuiltinBit32Shift( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, IrOp fallback, int pcpos) + IrBuilder& build, IrCmd cmd, int nparams, int ra, int arg, IrOp args, int nresults, IrOp fallback, int pcpos) { if (nparams < 2 || nresults > 1) return {BuiltinImplType::None, -1}; @@ -418,16 +407,6 @@ static BuiltinImplResult translateBuiltinBit32Shift( build.beginBlock(block); } - IrCmd cmd = IrCmd::NOP; - if (bfid == LBF_BIT32_LSHIFT) - cmd = IrCmd::BITLSHIFT_UINT; - else if (bfid == LBF_BIT32_RSHIFT) - cmd = IrCmd::BITRSHIFT_UINT; - else if (bfid == LBF_BIT32_ARSHIFT) - cmd = IrCmd::BITARSHIFT_UINT; - - LUAU_ASSERT(cmd != IrCmd::NOP); - IrOp shift = build.inst(cmd, vaui, vbi); IrOp value = build.inst(IrCmd::UINT_TO_NUM, shift); @@ -439,8 +418,7 @@ static BuiltinImplResult translateBuiltinBit32Shift( return {BuiltinImplType::UsesFallback, 1}; } -static BuiltinImplResult translateBuiltinBit32Rotate( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) +static BuiltinImplResult translateBuiltinBit32Rotate(IrBuilder& build, IrCmd cmd, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) { if (nparams < 2 || nresults > 1) return {BuiltinImplType::None, -1}; @@ -454,7 +432,6 @@ static BuiltinImplResult translateBuiltinBit32Rotate( IrOp vaui = build.inst(IrCmd::NUM_TO_UINT, va); IrOp vbi = build.inst(IrCmd::NUM_TO_INT, vb); - IrCmd cmd = (bfid == LBF_BIT32_LROTATE) ? IrCmd::BITLROTATE_UINT : IrCmd::BITRROTATE_UINT; IrOp shift = build.inst(cmd, vaui, vbi); IrOp value = build.inst(IrCmd::UINT_TO_NUM, shift); @@ -467,7 +444,7 @@ static BuiltinImplResult translateBuiltinBit32Rotate( } static BuiltinImplResult translateBuiltinBit32Extract( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, IrOp fallback, int pcpos) + IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, IrOp fallback, int pcpos) { if (nparams < 2 || nresults > 1) return {BuiltinImplType::None, -1}; @@ -547,8 +524,7 @@ static BuiltinImplResult translateBuiltinBit32Extract( return {BuiltinImplType::UsesFallback, 1}; } -static BuiltinImplResult translateBuiltinBit32ExtractK( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) +static BuiltinImplResult translateBuiltinBit32ExtractK(IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) { if (nparams < 2 || nresults > 1) return {BuiltinImplType::None, -1}; @@ -583,8 +559,7 @@ static BuiltinImplResult translateBuiltinBit32ExtractK( return {BuiltinImplType::Full, 1}; } -static BuiltinImplResult translateBuiltinBit32Countz( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) +static BuiltinImplResult translateBuiltinBit32Unary(IrBuilder& build, IrCmd cmd, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) { if (nparams < 1 || nresults > 1) return {BuiltinImplType::None, -1}; @@ -594,7 +569,6 @@ static BuiltinImplResult translateBuiltinBit32Countz( IrOp vaui = build.inst(IrCmd::NUM_TO_UINT, va); - IrCmd cmd = (bfid == LBF_BIT32_COUNTLZ) ? IrCmd::BITCOUNTLZ_UINT : IrCmd::BITCOUNTRZ_UINT; IrOp bin = build.inst(cmd, vaui); IrOp value = build.inst(IrCmd::UINT_TO_NUM, bin); @@ -608,7 +582,7 @@ static BuiltinImplResult translateBuiltinBit32Countz( } static BuiltinImplResult translateBuiltinBit32Replace( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, IrOp fallback, int pcpos) + IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, IrOp fallback, int pcpos) { if (nparams < 3 || nresults > 1) return {BuiltinImplType::None, -1}; @@ -632,7 +606,6 @@ static BuiltinImplResult translateBuiltinBit32Replace( build.inst(IrCmd::JUMP_CMP_INT, f, build.constInt(32), build.cond(IrCondition::UnsignedGreaterEqual), fallback, block); build.beginBlock(block); - // TODO: this can be optimized using a bit-select instruction (btr on x86) IrOp m = build.constInt(1); IrOp shift = build.inst(IrCmd::BITLSHIFT_UINT, m, f); IrOp not_ = build.inst(IrCmd::BITNOT_UINT, shift); @@ -718,10 +691,35 @@ static BuiltinImplResult translateBuiltinTableInsert(IrBuilder& build, int npara IrOp setnum = build.inst(IrCmd::TABLE_SETNUM, table, pos); - IrOp va = build.inst(IrCmd::LOAD_TVALUE, args); - build.inst(IrCmd::STORE_TVALUE, setnum, va); + if (FFlag::LuauImproveInsertIr) + { + if (args.kind == IrOpKind::Constant) + { + LUAU_ASSERT(build.function.constOp(args).kind == IrConstKind::Double); - build.inst(IrCmd::BARRIER_TABLE_FORWARD, table, args, build.undef()); + // No barrier necessary since numbers aren't collectable + build.inst(IrCmd::STORE_DOUBLE, setnum, args); + build.inst(IrCmd::STORE_TAG, setnum, build.constTag(LUA_TNUMBER)); + } + else + { + IrOp va = build.inst(IrCmd::LOAD_TVALUE, args); + build.inst(IrCmd::STORE_TVALUE, setnum, va); + + // Compiler only generates FASTCALL*K for source-level constants, so dynamic imports are not affected + LUAU_ASSERT(build.function.proto); + IrOp argstag = args.kind == IrOpKind::VmConst ? build.constTag(build.function.proto->k[vmConstOp(args)].tt) : build.undef(); + + build.inst(IrCmd::BARRIER_TABLE_FORWARD, table, args, argstag); + } + } + else + { + IrOp va = build.inst(IrCmd::LOAD_TVALUE, args); + build.inst(IrCmd::STORE_TVALUE, setnum, va); + + build.inst(IrCmd::BARRIER_TABLE_FORWARD, table, args, build.undef()); + } return {BuiltinImplType::Full, 0}; } @@ -743,6 +741,59 @@ static BuiltinImplResult translateBuiltinStringLen(IrBuilder& build, int nparams return {BuiltinImplType::Full, 1}; } +static void translateBufferArgsAndCheckBounds(IrBuilder& build, int nparams, int arg, IrOp args, int size, int pcpos, IrOp& buf, IrOp& intIndex) +{ + build.loadAndCheckTag(build.vmReg(arg), LUA_TBUFFER, build.vmExit(pcpos)); + builtinCheckDouble(build, args, pcpos); + + if (nparams == 3) + builtinCheckDouble(build, build.vmReg(vmRegOp(args) + 1), pcpos); + + buf = build.inst(IrCmd::LOAD_POINTER, build.vmReg(arg)); + + IrOp numIndex = builtinLoadDouble(build, args); + intIndex = build.inst(IrCmd::NUM_TO_INT, numIndex); + + build.inst(IrCmd::CHECK_BUFFER_LEN, buf, intIndex, build.constInt(size), build.vmExit(pcpos)); +} + +static BuiltinImplResult translateBuiltinBufferRead( + IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos, IrCmd readCmd, int size, IrCmd convCmd) +{ + if (!FFlag::LuauBufferTranslateIr) + return {BuiltinImplType::None, -1}; + + if (nparams < 2 || nresults > 1) + return {BuiltinImplType::None, -1}; + + IrOp buf, intIndex; + translateBufferArgsAndCheckBounds(build, nparams, arg, args, size, pcpos, buf, intIndex); + + IrOp result = build.inst(readCmd, buf, intIndex); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), convCmd == IrCmd::NOP ? result : build.inst(convCmd, result)); + build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); + + return {BuiltinImplType::Full, 1}; +} + +static BuiltinImplResult translateBuiltinBufferWrite( + IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos, IrCmd writeCmd, int size, IrCmd convCmd) +{ + if (!FFlag::LuauBufferTranslateIr) + return {BuiltinImplType::None, -1}; + + if (nparams < 3 || nresults > 0) + return {BuiltinImplType::None, -1}; + + IrOp buf, intIndex; + translateBufferArgsAndCheckBounds(build, nparams, arg, args, size, pcpos, buf, intIndex); + + IrOp numValue = builtinLoadDouble(build, build.vmReg(vmRegOp(args) + 1)); + build.inst(writeCmd, buf, intIndex, convCmd == IrCmd::NOP ? numValue : build.inst(convCmd, numValue)); + + return {BuiltinImplType::Full, 0}; +} + BuiltinImplResult translateBuiltin(IrBuilder& build, int bfid, int ra, int arg, IrOp args, int nparams, int nresults, IrOp fallback, int pcpos) { // Builtins are not allowed to handle variadic arguments @@ -758,7 +809,7 @@ BuiltinImplResult translateBuiltin(IrBuilder& build, int bfid, int ra, int arg, case LBF_MATH_RAD: return translateBuiltinMathDegRad(build, IrCmd::MUL_NUM, nparams, ra, arg, args, nresults, pcpos); case LBF_MATH_LOG: - return translateBuiltinMathLog(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); + return translateBuiltinMathLog(build, nparams, ra, arg, args, nresults, pcpos); case LBF_MATH_MIN: return translateBuiltinMathMinMax(build, IrCmd::MIN_NUM, nparams, ra, arg, args, nresults, pcpos); case LBF_MATH_MAX: @@ -798,28 +849,35 @@ BuiltinImplResult translateBuiltin(IrBuilder& build, int bfid, int ra, int arg, case LBF_MATH_MODF: return translateBuiltinNumberTo2Number(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_BAND: + return translateBuiltinBit32BinaryOp(build, IrCmd::BITAND_UINT, /* btest= */ false, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_BOR: + return translateBuiltinBit32BinaryOp(build, IrCmd::BITOR_UINT, /* btest= */ false, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_BXOR: + return translateBuiltinBit32BinaryOp(build, IrCmd::BITXOR_UINT, /* btest= */ false, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_BTEST: - return translateBuiltinBit32BinaryOp(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); + return translateBuiltinBit32BinaryOp(build, IrCmd::BITAND_UINT, /* btest= */ true, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_BNOT: - return translateBuiltinBit32Bnot(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); + return translateBuiltinBit32Bnot(build, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_LSHIFT: + return translateBuiltinBit32Shift(build, IrCmd::BITLSHIFT_UINT, nparams, ra, arg, args, nresults, fallback, pcpos); case LBF_BIT32_RSHIFT: + return translateBuiltinBit32Shift(build, IrCmd::BITRSHIFT_UINT, nparams, ra, arg, args, nresults, fallback, pcpos); case LBF_BIT32_ARSHIFT: - return translateBuiltinBit32Shift(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, fallback, pcpos); + return translateBuiltinBit32Shift(build, IrCmd::BITARSHIFT_UINT, nparams, ra, arg, args, nresults, fallback, pcpos); case LBF_BIT32_LROTATE: + return translateBuiltinBit32Rotate(build, IrCmd::BITLROTATE_UINT, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_RROTATE: - return translateBuiltinBit32Rotate(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); + return translateBuiltinBit32Rotate(build, IrCmd::BITRROTATE_UINT, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_EXTRACT: - return translateBuiltinBit32Extract(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, fallback, pcpos); + return translateBuiltinBit32Extract(build, nparams, ra, arg, args, nresults, fallback, pcpos); case LBF_BIT32_EXTRACTK: - return translateBuiltinBit32ExtractK(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); + return translateBuiltinBit32ExtractK(build, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_COUNTLZ: + return translateBuiltinBit32Unary(build, IrCmd::BITCOUNTLZ_UINT, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_COUNTRZ: - return translateBuiltinBit32Countz(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); + return translateBuiltinBit32Unary(build, IrCmd::BITCOUNTRZ_UINT, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_REPLACE: - return translateBuiltinBit32Replace(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, fallback, pcpos); + return translateBuiltinBit32Replace(build, nparams, ra, arg, args, nresults, fallback, pcpos); case LBF_TYPE: return translateBuiltinType(build, nparams, ra, arg, args, nresults); case LBF_TYPEOF: @@ -830,6 +888,34 @@ BuiltinImplResult translateBuiltin(IrBuilder& build, int bfid, int ra, int arg, return translateBuiltinTableInsert(build, nparams, ra, arg, args, nresults, pcpos); case LBF_STRING_LEN: return translateBuiltinStringLen(build, nparams, ra, arg, args, nresults, pcpos); + case LBF_BIT32_BYTESWAP: + return translateBuiltinBit32Unary(build, IrCmd::BYTESWAP_UINT, nparams, ra, arg, args, nresults, pcpos); + case LBF_BUFFER_READI8: + return translateBuiltinBufferRead(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_READI8, 1, IrCmd::INT_TO_NUM); + case LBF_BUFFER_READU8: + return translateBuiltinBufferRead(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_READU8, 1, IrCmd::INT_TO_NUM); + case LBF_BUFFER_WRITEU8: + return translateBuiltinBufferWrite(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_WRITEI8, 1, IrCmd::NUM_TO_UINT); + case LBF_BUFFER_READI16: + return translateBuiltinBufferRead(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_READI16, 2, IrCmd::INT_TO_NUM); + case LBF_BUFFER_READU16: + return translateBuiltinBufferRead(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_READU16, 2, IrCmd::INT_TO_NUM); + case LBF_BUFFER_WRITEU16: + return translateBuiltinBufferWrite(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_WRITEI16, 2, IrCmd::NUM_TO_UINT); + case LBF_BUFFER_READI32: + return translateBuiltinBufferRead(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_READI32, 4, IrCmd::INT_TO_NUM); + case LBF_BUFFER_READU32: + return translateBuiltinBufferRead(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_READI32, 4, IrCmd::UINT_TO_NUM); + case LBF_BUFFER_WRITEU32: + return translateBuiltinBufferWrite(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_WRITEI32, 4, IrCmd::NUM_TO_UINT); + case LBF_BUFFER_READF32: + return translateBuiltinBufferRead(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_READF32, 4, IrCmd::NOP); + case LBF_BUFFER_WRITEF32: + return translateBuiltinBufferWrite(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_WRITEF32, 4, IrCmd::NOP); + case LBF_BUFFER_READF64: + return translateBuiltinBufferRead(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_READF64, 8, IrCmd::NOP); + case LBF_BUFFER_WRITEF64: + return translateBuiltinBufferWrite(build, nparams, ra, arg, args, nresults, pcpos, IrCmd::BUFFER_WRITEF64, 8, IrCmd::NOP); default: return {BuiltinImplType::None, -1}; } diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index 763a8478..dff7002d 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -12,9 +12,8 @@ #include "lstate.h" #include "ltm.h" -LUAU_FASTFLAG(LuauReduceStackSpills) -LUAU_FASTFLAGVARIABLE(LuauInlineArrConstOffset, false) LUAU_FASTFLAGVARIABLE(LuauLowerAltLoopForn, false) +LUAU_FASTFLAG(LuauImproveInsertIr) namespace Luau { @@ -562,9 +561,10 @@ IrOp translateFastCallN(IrBuilder& build, const Instruction* pc, int pcpos, bool IrOp builtinArgs = args; - if (customArgs.kind == IrOpKind::VmConst && bfid != LBF_TABLE_INSERT) + if (customArgs.kind == IrOpKind::VmConst && (FFlag::LuauImproveInsertIr || bfid != LBF_TABLE_INSERT)) { - TValue protok = build.function.proto->k[customArgs.index]; + LUAU_ASSERT(build.function.proto); + TValue protok = build.function.proto->k[vmConstOp(customArgs)]; if (protok.tt == LUA_TNUMBER) builtinArgs = build.constDouble(protok.value.n); @@ -921,20 +921,10 @@ void translateInstGetTableN(IrBuilder& build, const Instruction* pc, int pcpos) build.inst(IrCmd::CHECK_ARRAY_SIZE, vb, build.constInt(c), fallback); build.inst(IrCmd::CHECK_NO_METATABLE, vb, fallback); - if (FFlag::LuauInlineArrConstOffset) - { - IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(0)); + IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(0)); - IrOp arrElTval = build.inst(IrCmd::LOAD_TVALUE, arrEl, build.constInt(c * sizeof(TValue))); - build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), arrElTval); - } - else - { - IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(c)); - - IrOp arrElTval = build.inst(IrCmd::LOAD_TVALUE, arrEl); - build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), arrElTval); - } + IrOp arrElTval = build.inst(IrCmd::LOAD_TVALUE, arrEl, build.constInt(c * sizeof(TValue))); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), arrElTval); IrOp next = build.blockAtInst(pcpos + 1); FallbackStreamScope scope(build, fallback, next); @@ -961,20 +951,10 @@ void translateInstSetTableN(IrBuilder& build, const Instruction* pc, int pcpos) build.inst(IrCmd::CHECK_NO_METATABLE, vb, fallback); build.inst(IrCmd::CHECK_READONLY, vb, fallback); - if (FFlag::LuauInlineArrConstOffset) - { - IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(0)); + IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(0)); - IrOp tva = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(ra)); - build.inst(IrCmd::STORE_TVALUE, arrEl, tva, build.constInt(c * sizeof(TValue))); - } - else - { - IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(c)); - - IrOp tva = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(ra)); - build.inst(IrCmd::STORE_TVALUE, arrEl, tva); - } + IrOp tva = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(ra)); + build.inst(IrCmd::STORE_TVALUE, arrEl, tva, build.constInt(c * sizeof(TValue))); build.inst(IrCmd::BARRIER_TABLE_FORWARD, vb, build.vmReg(ra), build.undef()); @@ -1376,74 +1356,37 @@ void translateInstNewClosure(IrBuilder& build, const Instruction* pc, int pcpos) Instruction uinsn = pc[ui + 1]; LUAU_ASSERT(LUAU_INSN_OP(uinsn) == LOP_CAPTURE); - if (FFlag::LuauReduceStackSpills) + switch (LUAU_INSN_A(uinsn)) { - switch (LUAU_INSN_A(uinsn)) - { - case LCT_VAL: - { - IrOp src = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(LUAU_INSN_B(uinsn))); - IrOp dst = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, ncl, build.vmUpvalue(ui)); - build.inst(IrCmd::STORE_TVALUE, dst, src); - break; - } - - case LCT_REF: - { - IrOp src = build.inst(IrCmd::FINDUPVAL, build.vmReg(LUAU_INSN_B(uinsn))); - IrOp dst = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, ncl, build.vmUpvalue(ui)); - build.inst(IrCmd::STORE_POINTER, dst, src); - build.inst(IrCmd::STORE_TAG, dst, build.constTag(LUA_TUPVAL)); - break; - } - - case LCT_UPVAL: - { - IrOp src = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, build.undef(), build.vmUpvalue(LUAU_INSN_B(uinsn))); - IrOp dst = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, ncl, build.vmUpvalue(ui)); - IrOp load = build.inst(IrCmd::LOAD_TVALUE, src); - build.inst(IrCmd::STORE_TVALUE, dst, load); - break; - } - - default: - LUAU_ASSERT(!"Unknown upvalue capture type"); - LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks - } - } - else + case LCT_VAL: { + IrOp src = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(LUAU_INSN_B(uinsn))); IrOp dst = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, ncl, build.vmUpvalue(ui)); + build.inst(IrCmd::STORE_TVALUE, dst, src); + break; + } - switch (LUAU_INSN_A(uinsn)) - { - case LCT_VAL: - { - IrOp src = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(LUAU_INSN_B(uinsn))); - build.inst(IrCmd::STORE_TVALUE, dst, src); - break; - } + case LCT_REF: + { + IrOp src = build.inst(IrCmd::FINDUPVAL, build.vmReg(LUAU_INSN_B(uinsn))); + IrOp dst = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, ncl, build.vmUpvalue(ui)); + build.inst(IrCmd::STORE_POINTER, dst, src); + build.inst(IrCmd::STORE_TAG, dst, build.constTag(LUA_TUPVAL)); + break; + } - case LCT_REF: - { - IrOp src = build.inst(IrCmd::FINDUPVAL, build.vmReg(LUAU_INSN_B(uinsn))); - build.inst(IrCmd::STORE_POINTER, dst, src); - build.inst(IrCmd::STORE_TAG, dst, build.constTag(LUA_TUPVAL)); - break; - } + case LCT_UPVAL: + { + IrOp src = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, build.undef(), build.vmUpvalue(LUAU_INSN_B(uinsn))); + IrOp dst = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, ncl, build.vmUpvalue(ui)); + IrOp load = build.inst(IrCmd::LOAD_TVALUE, src); + build.inst(IrCmd::STORE_TVALUE, dst, load); + break; + } - case LCT_UPVAL: - { - IrOp src = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, build.undef(), build.vmUpvalue(LUAU_INSN_B(uinsn))); - IrOp load = build.inst(IrCmd::LOAD_TVALUE, src); - build.inst(IrCmd::STORE_TVALUE, dst, load); - break; - } - - default: - LUAU_ASSERT(!"Unknown upvalue capture type"); - LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks - } + default: + LUAU_ASSERT(!"Unknown upvalue capture type"); + LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks } } diff --git a/CodeGen/src/IrUtils.cpp b/CodeGen/src/IrUtils.cpp index ba46dbc9..15cd9426 100644 --- a/CodeGen/src/IrUtils.cpp +++ b/CodeGen/src/IrUtils.cpp @@ -122,6 +122,7 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::CHECK_SLOT_MATCH: case IrCmd::CHECK_NODE_NO_NEXT: case IrCmd::CHECK_NODE_VALUE: + case IrCmd::CHECK_BUFFER_LEN: case IrCmd::INTERRUPT: case IrCmd::CHECK_GC: case IrCmd::BARRIER_OBJ: @@ -163,6 +164,7 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::BITRROTATE_UINT: case IrCmd::BITCOUNTLZ_UINT: case IrCmd::BITCOUNTRZ_UINT: + case IrCmd::BYTESWAP_UINT: return IrValueKind::Int; case IrCmd::INVOKE_LIBM: return IrValueKind::Double; @@ -171,6 +173,21 @@ IrValueKind getCmdValueKind(IrCmd cmd) return IrValueKind::Pointer; case IrCmd::FINDUPVAL: return IrValueKind::Pointer; + case IrCmd::BUFFER_READI8: + case IrCmd::BUFFER_READU8: + case IrCmd::BUFFER_READI16: + case IrCmd::BUFFER_READU16: + case IrCmd::BUFFER_READI32: + return IrValueKind::Int; + case IrCmd::BUFFER_WRITEI8: + case IrCmd::BUFFER_WRITEI16: + case IrCmd::BUFFER_WRITEI32: + case IrCmd::BUFFER_WRITEF32: + case IrCmd::BUFFER_WRITEF64: + return IrValueKind::None; + case IrCmd::BUFFER_READF32: + case IrCmd::BUFFER_READF64: + return IrValueKind::Double; } LUAU_UNREACHABLE(); diff --git a/CodeGen/src/IrValueLocationTracking.cpp b/CodeGen/src/IrValueLocationTracking.cpp index 20dee34a..b17be682 100644 --- a/CodeGen/src/IrValueLocationTracking.cpp +++ b/CodeGen/src/IrValueLocationTracking.cpp @@ -3,8 +3,6 @@ #include "Luau/IrUtils.h" -LUAU_FASTFLAGVARIABLE(LuauReduceStackSpills, false) - namespace Luau { namespace CodeGen @@ -198,7 +196,7 @@ void IrValueLocationTracking::invalidateRestoreOp(IrOp location, bool skipValueI IrInst& inst = function.instructions[instIdx]; // If we are only modifying the tag, we can avoid invalidating tracked location of values - if (FFlag::LuauReduceStackSpills && skipValueInvalidation) + if (skipValueInvalidation) { switch (getCmdValueKind(inst.cmd)) { diff --git a/CodeGen/src/NativeState.cpp b/CodeGen/src/NativeState.cpp index 7b2f068b..a161987d 100644 --- a/CodeGen/src/NativeState.cpp +++ b/CodeGen/src/NativeState.cpp @@ -103,7 +103,6 @@ void initFunctions(NativeState& data) data.context.executeGETTABLEKS = executeGETTABLEKS; data.context.executeSETTABLEKS = executeSETTABLEKS; - data.context.executeNEWCLOSURE = executeNEWCLOSURE; data.context.executeNAMECALL = executeNAMECALL; data.context.executeFORGPREP = executeFORGPREP; data.context.executeGETVARARGSMultRet = executeGETVARARGSMultRet; diff --git a/CodeGen/src/NativeState.h b/CodeGen/src/NativeState.h index f0b8561c..7670482d 100644 --- a/CodeGen/src/NativeState.h +++ b/CodeGen/src/NativeState.h @@ -94,7 +94,6 @@ struct NativeContext const Instruction* (*executeSETGLOBAL)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeGETTABLEKS)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeSETTABLEKS)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; - const Instruction* (*executeNEWCLOSURE)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeNAMECALL)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeSETLIST)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeFORGPREP)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 3315ec96..8d0f829a 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -15,8 +15,6 @@ LUAU_FASTINTVARIABLE(LuauCodeGenMinLinearBlockPath, 3) LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64) LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false) -LUAU_FASTFLAGVARIABLE(LuauReuseHashSlots2, false) -LUAU_FASTFLAGVARIABLE(LuauMergeTagLoads, false) LUAU_FASTFLAGVARIABLE(LuauReuseArrSlots2, false) LUAU_FASTFLAG(LuauLowerAltLoopForn) @@ -546,10 +544,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& } else if (inst.a.kind == IrOpKind::VmReg) { - if (FFlag::LuauMergeTagLoads) - state.substituteOrRecordVmRegLoad(inst); - else - state.createRegLink(index, inst.a); + state.substituteOrRecordVmRegLoad(inst); } break; case IrCmd::LOAD_POINTER: @@ -762,7 +757,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& else replace(function, block, index, {IrCmd::JUMP, inst.d}); } - else if (FFlag::LuauMergeTagLoads && inst.a == inst.b) + else if (inst.a == inst.b) { replace(function, block, index, {IrCmd::JUMP, inst.c}); } @@ -920,6 +915,22 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& state.inSafeEnv = true; } break; + case IrCmd::CHECK_BUFFER_LEN: + // TODO: remove duplicate checks and extend earlier check bound when possible + break; + case IrCmd::BUFFER_READI8: + case IrCmd::BUFFER_READU8: + case IrCmd::BUFFER_WRITEI8: + case IrCmd::BUFFER_READI16: + case IrCmd::BUFFER_READU16: + case IrCmd::BUFFER_WRITEI16: + case IrCmd::BUFFER_READI32: + case IrCmd::BUFFER_WRITEI32: + case IrCmd::BUFFER_READF32: + case IrCmd::BUFFER_WRITEF32: + case IrCmd::BUFFER_READF64: + case IrCmd::BUFFER_WRITEF64: + break; case IrCmd::CHECK_GC: // It is enough to perform a GC check once in a block if (state.checkedGc) @@ -971,9 +982,6 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& state.getArrAddrCache.push_back(index); break; case IrCmd::GET_SLOT_NODE_ADDR: - if (!FFlag::LuauReuseHashSlots2) - break; - for (uint32_t prevIdx : state.getSlotNodeCache) { const IrInst& prev = function.instructions[prevIdx]; @@ -1126,9 +1134,6 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& break; } case IrCmd::CHECK_SLOT_MATCH: - if (!FFlag::LuauReuseHashSlots2) - break; - for (uint32_t prevIdx : state.checkSlotMatchCache) { const IrInst& prev = function.instructions[prevIdx]; @@ -1168,6 +1173,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& case IrCmd::BITLROTATE_UINT: case IrCmd::BITCOUNTLZ_UINT: case IrCmd::BITCOUNTRZ_UINT: + case IrCmd::BYTESWAP_UINT: case IrCmd::INVOKE_LIBM: case IrCmd::GET_TYPE: case IrCmd::GET_TYPEOF: diff --git a/Common/include/Luau/DenseHash.h b/Common/include/Luau/DenseHash.h index 72aa6ec5..067a9d7a 100644 --- a/Common/include/Luau/DenseHash.h +++ b/Common/include/Luau/DenseHash.h @@ -539,6 +539,25 @@ public: { return impl.end(); } + + bool operator==(const DenseHashSet& other) + { + if (size() != other.size()) + return false; + + for (const Key& k : *this) + { + if (!other.contains(k)) + return false; + } + + return true; + } + + bool operator!=(const DenseHashSet& other) + { + return !(*this == other); + } }; // This is a faster alternative of unordered_map, but it does not implement the same interface (i.e. it does not support erasing and has diff --git a/Compiler/include/Luau/BytecodeBuilder.h b/Compiler/include/Luau/BytecodeBuilder.h index 36f23537..8345fb85 100644 --- a/Compiler/include/Luau/BytecodeBuilder.h +++ b/Compiler/include/Luau/BytecodeBuilder.h @@ -84,6 +84,7 @@ public: void pushDebugUpval(StringRef name); size_t getInstructionCount() const; + size_t getTotalInstructionCount() const; uint32_t getDebugPC() const; void addDebugRemark(const char* format, ...) LUAU_PRINTF_ATTR(2, 3); @@ -237,6 +238,7 @@ private: uint32_t currentFunction = ~0u; uint32_t mainFunction = ~0u; + size_t totalInstructionCount = 0; std::vector insns; std::vector lines; std::vector constants; diff --git a/Compiler/src/BytecodeBuilder.cpp b/Compiler/src/BytecodeBuilder.cpp index 69b05e53..774ea97c 100644 --- a/Compiler/src/BytecodeBuilder.cpp +++ b/Compiler/src/BytecodeBuilder.cpp @@ -7,7 +7,6 @@ #include #include -LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauVectorLiterals) namespace Luau @@ -269,6 +268,7 @@ void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues, uin currentFunction = ~0u; + totalInstructionCount += insns.size(); insns.clear(); lines.clear(); constants.clear(); @@ -580,6 +580,11 @@ size_t BytecodeBuilder::getInstructionCount() const return insns.size(); } +size_t BytecodeBuilder::getTotalInstructionCount() const +{ + return totalInstructionCount; +} + uint32_t BytecodeBuilder::getDebugPC() const { return uint32_t(insns.size()); @@ -1325,8 +1330,6 @@ void BytecodeBuilder::validateInstructions() const case LOP_IDIV: case LOP_MOD: case LOP_POW: - LUAU_ASSERT(FFlag::LuauFloorDivision || op != LOP_IDIV); - VREG(LUAU_INSN_A(insn)); VREG(LUAU_INSN_B(insn)); VREG(LUAU_INSN_C(insn)); @@ -1339,8 +1342,6 @@ void BytecodeBuilder::validateInstructions() const case LOP_IDIVK: case LOP_MODK: case LOP_POWK: - LUAU_ASSERT(FFlag::LuauFloorDivision || op != LOP_IDIVK); - VREG(LUAU_INSN_A(insn)); VREG(LUAU_INSN_B(insn)); VCONST(LUAU_INSN_C(insn), Number); @@ -1911,8 +1912,6 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, break; case LOP_IDIV: - LUAU_ASSERT(FFlag::LuauFloorDivision); - formatAppend(result, "IDIV R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); break; @@ -1949,8 +1948,6 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, break; case LOP_IDIVK: - LUAU_ASSERT(FFlag::LuauFloorDivision); - formatAppend(result, "IDIVK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); dumpConstant(result, LUAU_INSN_C(insn)); result.append("]\n"); diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 7bb70201..bb5d8da8 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -26,9 +26,8 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) -LUAU_FASTFLAG(LuauFloorDivision) -LUAU_FASTFLAGVARIABLE(LuauCompileFixContinueValidation2, false) -LUAU_FASTFLAGVARIABLE(LuauCompileIfElseAndOr, false) +LUAU_FASTFLAGVARIABLE(LuauCompileSideEffects, false) +LUAU_FASTFLAGVARIABLE(LuauCompileDeadIf, false) namespace Luau { @@ -261,7 +260,7 @@ struct Compiler if (bytecode.getInstructionCount() > kMaxInstructionCount) CompileError::raise(func->location, "Exceeded function instruction limit; split the function into parts to compile"); - // since top-level code only executes once, it can be marked as cold if it has no loops (top-level code with loops might be profitable to compile natively) + // top-level code only executes once so it can be marked as cold if it has no loops; code with loops might be profitable to compile natively if (func->functionDepth == 0 && !hasLoops) protoflags |= LPF_NATIVE_COLD; @@ -645,10 +644,7 @@ struct Compiler // evaluate extra expressions for side effects for (size_t i = func->args.size; i < expr->args.size; ++i) - { - RegScope rsi(this); - compileExprAuto(expr->args.data[i], rsi); - } + compileExprSide(expr->args.data[i]); // apply all evaluated arguments to the compiler state // note: locals use current startpc for debug info, although some of them have been computed earlier; this is similar to compileStatLocal @@ -1039,8 +1035,6 @@ struct Compiler return k ? LOP_DIVK : LOP_DIV; case AstExprBinary::FloorDiv: - LUAU_ASSERT(FFlag::LuauFloorDivision); - return k ? LOP_IDIVK : LOP_IDIV; case AstExprBinary::Mod: @@ -1512,8 +1506,6 @@ struct Compiler case AstExprBinary::Mod: case AstExprBinary::Pow: { - LUAU_ASSERT(FFlag::LuauFloorDivision || expr->op != AstExprBinary::FloorDiv); - int32_t rc = getConstantNumber(expr->right); if (rc >= 0 && rc <= 255) @@ -1612,18 +1604,15 @@ struct Compiler } else { - if (FFlag::LuauCompileIfElseAndOr) + // Optimization: convert some if..then..else expressions into and/or when the other side has no side effects and is very cheap to compute + // if v then v else e => v or e + // if v then e else v => v and e + if (int creg = getExprLocalReg(expr->condition); creg >= 0) { - // Optimization: convert some if..then..else expressions into and/or when the other side has no side effects and is very cheap to compute - // if v then v else e => v or e - // if v then e else v => v and e - if (int creg = getExprLocalReg(expr->condition); creg >= 0) - { - if (creg == getExprLocalReg(expr->trueExpr) && (getExprLocalReg(expr->falseExpr) >= 0 || isConstant(expr->falseExpr))) - return compileExprIfElseAndOr(/* and_= */ false, uint8_t(creg), expr->falseExpr, target); - else if (creg == getExprLocalReg(expr->falseExpr) && (getExprLocalReg(expr->trueExpr) >= 0 || isConstant(expr->trueExpr))) - return compileExprIfElseAndOr(/* and_= */ true, uint8_t(creg), expr->trueExpr, target); - } + if (creg == getExprLocalReg(expr->trueExpr) && (getExprLocalReg(expr->falseExpr) >= 0 || isConstant(expr->falseExpr))) + return compileExprIfElseAndOr(/* and_= */ false, uint8_t(creg), expr->falseExpr, target); + else if (creg == getExprLocalReg(expr->falseExpr) && (getExprLocalReg(expr->trueExpr) >= 0 || isConstant(expr->trueExpr))) + return compileExprIfElseAndOr(/* and_= */ true, uint8_t(creg), expr->trueExpr, target); } std::vector elseJump; @@ -2238,6 +2227,23 @@ struct Compiler return reg; } + void compileExprSide(AstExpr* node) + { + if (FFlag::LuauCompileSideEffects) + { + // Optimization: some expressions never carry side effects so we don't need to emit any code + if (node->is() || node->is() || node->is() || node->is() || isConstant(node)) + return; + + // note: the remark is omitted for calls as it's fairly noisy due to inlining + if (!node->is()) + bytecode.addDebugRemark("expression only compiled for side effects"); + } + + RegScope rsi(this); + compileExprAuto(node, rsi); + } + // 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 @@ -2286,10 +2292,7 @@ struct Compiler // evaluate extra expressions for side effects for (size_t i = targetCount; i < list.size; ++i) - { - RegScope rsi(this); - compileExprAuto(list.data[i], rsi); - } + compileExprSide(list.data[i]); } else if (list.size > 0) { @@ -2524,6 +2527,18 @@ struct Compiler return; } + // Optimization: condition is always false but isn't a constant => we only need the else body and condition's side effects + if (FFlag::LuauCompileDeadIf) + { + if (AstExprBinary* cand = stat->condition->as(); cand && cand->op == AstExprBinary::And && isConstantFalse(cand->right)) + { + compileExprSide(cand->left); + if (stat->elsebody) + compileStat(stat->elsebody); + return; + } + } + // Optimization: body is a "break" statement with no "else" => we can directly break out of the loop in "then" case if (!stat->elsebody && isStatBreak(stat->thenbody) && !areLocalsCaptured(loops.back().localOffset)) { @@ -2541,14 +2556,9 @@ struct Compiler // Optimization: body is a "continue" statement with no "else" => we can directly continue in "then" case if (!stat->elsebody && continueStatement != nullptr && !areLocalsCaptured(loops.back().localOffsetContinue)) { - if (FFlag::LuauCompileFixContinueValidation2) - { - // track continue statement for repeat..until validation (validateContinueUntil) - if (!loops.back().continueUsed) - loops.back().continueUsed = continueStatement; - } - else if (loops.back().untilCondition) - validateContinueUntil(continueStatement, loops.back().untilCondition); + // track continue statement for repeat..until validation (validateContinueUntil) + if (!loops.back().continueUsed) + loops.back().continueUsed = continueStatement; // fallthrough = proceed with the loop body as usual std::vector elseJump; @@ -2609,7 +2619,7 @@ struct Compiler size_t oldJumps = loopJumps.size(); size_t oldLocals = localStack.size(); - loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); hasLoops = true; size_t loopLabel = bytecode.emitLabel(); @@ -2645,7 +2655,7 @@ struct Compiler size_t oldJumps = loopJumps.size(); size_t oldLocals = localStack.size(); - loops.push_back({oldLocals, oldLocals, stat->condition, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); hasLoops = true; size_t loopLabel = bytecode.emitLabel(); @@ -2668,9 +2678,9 @@ struct Compiler // expression that continue will jump to. loops.back().localOffsetContinue = localStack.size(); - // if continue was called from this statement, then any local defined after this in the loop body should not be accessed by until condition + // if continue was called from this statement, any local defined after this in the loop body should not be accessed by until condition // it is sufficient to check this condition once, as if this holds for the first continue, it must hold for all subsequent continues. - if (FFlag::LuauCompileFixContinueValidation2 && loops.back().continueUsed && !continueValidated) + if (loops.back().continueUsed && !continueValidated) { validateContinueUntil(loops.back().continueUsed, stat->condition, body, i + 1); continueValidated = true; @@ -2892,7 +2902,7 @@ struct Compiler size_t oldLocals = localStack.size(); size_t oldJumps = loopJumps.size(); - loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); for (int iv = 0; iv < tripCount; ++iv) { @@ -2943,7 +2953,7 @@ struct Compiler size_t oldLocals = localStack.size(); size_t oldJumps = loopJumps.size(); - loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); hasLoops = true; // register layout: limit, step, index @@ -3008,7 +3018,7 @@ struct Compiler size_t oldLocals = localStack.size(); size_t oldJumps = loopJumps.size(); - loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); hasLoops = true; // register layout: generator, state, index, variables... @@ -3258,10 +3268,7 @@ struct Compiler // compute expressions with side effects for (size_t i = stat->vars.size; i < stat->values.size; ++i) - { - RegScope rsi(this); - compileExprAuto(stat->values.data[i], rsi); - } + compileExprSide(stat->values.data[i]); // 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 @@ -3304,8 +3311,6 @@ struct Compiler case AstExprBinary::Mod: case AstExprBinary::Pow: { - LUAU_ASSERT(FFlag::LuauFloorDivision || stat->op != AstExprBinary::FloorDiv); - if (var.kind != LValue::Kind_Local) compileLValueUse(var, target, /* set= */ false); @@ -3420,14 +3425,9 @@ struct Compiler { LUAU_ASSERT(!loops.empty()); - if (FFlag::LuauCompileFixContinueValidation2) - { - // track continue statement for repeat..until validation (validateContinueUntil) - if (!loops.back().continueUsed) - loops.back().continueUsed = stat; - } - else if (loops.back().untilCondition) - validateContinueUntil(stat, loops.back().untilCondition); + // track continue statement for repeat..until validation (validateContinueUntil) + if (!loops.back().continueUsed) + loops.back().continueUsed = stat; // before continuing, we need to close all local variables that were captured in closures since loop start // normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here @@ -3458,8 +3458,7 @@ struct Compiler } else { - RegScope rs(this); - compileExprAuto(stat->expr, rs); + compileExprSide(stat->expr); } } else if (AstStatLocal* stat = node->as()) @@ -3510,21 +3509,8 @@ struct Compiler } } - void validateContinueUntil(AstStat* cont, AstExpr* condition) - { - LUAU_ASSERT(!FFlag::LuauCompileFixContinueValidation2); - UndefinedLocalVisitor visitor(this); - condition->visit(&visitor); - - if (visitor.undef) - CompileError::raise(condition->location, - "Local %s used in the repeat..until condition is undefined because continue statement on line %d jumps over it", - visitor.undef->name.value, cont->location.begin.line + 1); - } - void validateContinueUntil(AstStat* cont, AstExpr* condition, AstStatBlock* body, size_t start) { - LUAU_ASSERT(FFlag::LuauCompileFixContinueValidation2); UndefinedLocalVisitor visitor(this); for (size_t i = start; i < body->body.size; ++i) @@ -3770,18 +3756,8 @@ struct Compiler void check(AstLocal* local) { - if (FFlag::LuauCompileFixContinueValidation2) - { - if (!undef && locals.contains(local)) - undef = local; - } - else - { - Local& l = self->locals[local]; - - if (!l.allocated && !undef) - undef = local; - } + if (!undef && locals.contains(local)) + undef = local; } bool visit(AstExprLocal* node) override @@ -3926,9 +3902,6 @@ struct Compiler size_t localOffset; size_t localOffsetContinue; - // TODO: Remove with LuauCompileFixContinueValidation2 - AstExpr* untilCondition; - AstStatContinue* continueUsed; }; diff --git a/Config/src/Config.cpp b/Config/src/Config.cpp index 8e9802cf..97d86b62 100644 --- a/Config/src/Config.cpp +++ b/Config/src/Config.cpp @@ -4,7 +4,6 @@ #include "Luau/Lexer.h" #include "Luau/StringUtils.h" -LUAU_FASTFLAG(LuauFloorDivision) namespace Luau { @@ -113,24 +112,8 @@ static void next(Lexer& lexer) lexer.next(); // skip C-style comments as Lexer only understands Lua-style comments atm - - if (FFlag::LuauFloorDivision) - { - while (lexer.current().type == Luau::Lexeme::FloorDiv) - lexer.nextline(); - } - else - { - while (lexer.current().type == '/') - { - Lexeme peek = lexer.lookahead(); - - if (peek.type != '/' || peek.location.begin != lexer.current().location.end) - break; - - lexer.nextline(); - } - } + while (lexer.current().type == Luau::Lexeme::FloorDiv) + lexer.nextline(); } static Error fail(Lexer& lexer, const char* message) diff --git a/Makefile b/Makefile index 2e5e9791..9e97633f 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ MAKEFLAGS+=-r -j8 COMMA=, +CMAKE_PATH=cmake + config=debug protobuf=system @@ -101,7 +103,6 @@ ifeq ($(config),analyze) endif ifeq ($(config),fuzz) - CXX=clang++ # our fuzzing infra relies on llvm fuzzer CXXFLAGS+=-fsanitize=address,fuzzer -Ibuild/libprotobuf-mutator -O2 LDFLAGS+=-fsanitize=address,fuzzer LPROTOBUF=-lprotobuf @@ -252,12 +253,13 @@ fuzz/luau.pb.cpp: fuzz/luau.proto build/libprotobuf-mutator $(BUILD)/fuzz/proto.cpp.o: fuzz/luau.pb.cpp $(BUILD)/fuzz/protoprint.cpp.o: fuzz/luau.pb.cpp +$(BUILD)/fuzz/prototest.cpp.o: fuzz/luau.pb.cpp build/libprotobuf-mutator: git clone https://github.com/google/libprotobuf-mutator build/libprotobuf-mutator git -C build/libprotobuf-mutator checkout 212a7be1eb08e7f9c79732d2aab9b2097085d936 - CXX= cmake -S build/libprotobuf-mutator -B build/libprotobuf-mutator $(DPROTOBUF) - make -C build/libprotobuf-mutator -j8 + $(CMAKE_PATH) -DCMAKE_CXX_COMPILER=$(CMAKE_CXX) -DCMAKE_C_COMPILER=$(CMAKE_CC) -DCMAKE_CXX_COMPILER_LAUNCHER=$(CMAKE_PROXY) -S build/libprotobuf-mutator -B build/libprotobuf-mutator $(DPROTOBUF) + $(MAKE) -C build/libprotobuf-mutator # picks up include dependencies for all object files -include $(OBJECTS:.o=.d) diff --git a/README.md b/README.md index 500f53d2..8efe7a2c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Luau is an embeddable language, but it also comes with two command-line tools by `luau` is a command-line REPL and can also run input files. Note that REPL runs in a sandboxed environment and as such doesn't have access to the underlying file system except for ability to `require` modules. -`luau-analyze` is a command-line type checker and linter; given a set of input files, it produces errors/warnings according to the file configuration, which can be customized by using `--!` comments in the files or [`.luaurc`](https://github.com/luau-lang/luau/blob/master/rfcs/config-luaurc.md) files. For details please refer to [type checking]( https://luau-lang.org/typecheck) and [linting](https://luau-lang.org/lint) documentation. +`luau-analyze` is a command-line type checker and linter; given a set of input files, it produces errors/warnings according to the file configuration, which can be customized by using `--!` comments in the files or [`.luaurc`](https://github.com/luau-lang/rfcs/blob/master/docs/config-luaurc.md) files. For details please refer to [type checking]( https://luau-lang.org/typecheck) and [linting](https://luau-lang.org/lint) documentation. # Installation diff --git a/Sources.cmake b/Sources.cmake index 2604514e..929c99a4 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -156,7 +156,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Cancellation.h Analysis/include/Luau/Clone.h Analysis/include/Luau/Constraint.h - Analysis/include/Luau/ConstraintGraphBuilder.h + Analysis/include/Luau/ConstraintGenerator.h Analysis/include/Luau/ConstraintSolver.h Analysis/include/Luau/ControlFlow.h Analysis/include/Luau/DataFlowGraph.h @@ -185,6 +185,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Refinement.h Analysis/include/Luau/RequireTracer.h Analysis/include/Luau/Scope.h + Analysis/include/Luau/Set.h Analysis/include/Luau/Simplify.h Analysis/include/Luau/Substitution.h Analysis/include/Luau/Subtyping.h @@ -223,7 +224,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/BuiltinDefinitions.cpp Analysis/src/Clone.cpp Analysis/src/Constraint.cpp - Analysis/src/ConstraintGraphBuilder.cpp + Analysis/src/ConstraintGenerator.cpp Analysis/src/ConstraintSolver.cpp Analysis/src/DataFlowGraph.cpp Analysis/src/DcrLogger.cpp @@ -385,8 +386,8 @@ if(TARGET Luau.UnitTest) tests/CodeAllocator.test.cpp tests/Compiler.test.cpp tests/Config.test.cpp - tests/ConstraintGraphBuilderFixture.cpp - tests/ConstraintGraphBuilderFixture.h + tests/ConstraintGeneratorFixture.cpp + tests/ConstraintGeneratorFixture.h tests/ConstraintSolver.test.cpp tests/CostModel.test.cpp tests/DataFlowGraph.test.cpp @@ -419,6 +420,7 @@ if(TARGET Luau.UnitTest) tests/RuntimeLimits.test.cpp tests/ScopedFlags.h tests/Simplify.test.cpp + tests/Set.test.cpp tests/StringUtils.test.cpp tests/Subtyping.test.cpp tests/Symbol.test.cpp diff --git a/VM/src/lbuflib.cpp b/VM/src/lbuflib.cpp index 51ed5dac..3fb6e166 100644 --- a/VM/src/lbuflib.cpp +++ b/VM/src/lbuflib.cpp @@ -10,6 +10,8 @@ #include +LUAU_FASTFLAGVARIABLE(LuauBufferBetterMsg, false) + // while C API returns 'size_t' for binary compatibility in case of future extensions, // in the current implementation, length and offset are limited to 31 bits // because offset is limited to an integer, a single 64bit comparison can be used and will not overflow @@ -36,8 +38,15 @@ static int buffer_create(lua_State* L) { int size = luaL_checkinteger(L, 1); - if (size < 0) - luaL_error(L, "size cannot be negative"); + if (FFlag::LuauBufferBetterMsg) + { + luaL_argcheck(L, size >= 0, 1, "size"); + } + else + { + if (size < 0) + luaL_error(L, "invalid size"); + } lua_newbuffer(L, size); return 1; @@ -165,8 +174,15 @@ static int buffer_readstring(lua_State* L) int offset = luaL_checkinteger(L, 2); int size = luaL_checkinteger(L, 3); - if (size < 0) - luaL_error(L, "size cannot be negative"); + if (FFlag::LuauBufferBetterMsg) + { + luaL_argcheck(L, size >= 0, 3, "size"); + } + else + { + if (size < 0) + luaL_error(L, "invalid size"); + } if (isoutofbounds(offset, len, unsigned(size))) luaL_error(L, "buffer access out of bounds"); @@ -184,8 +200,15 @@ static int buffer_writestring(lua_State* L) const char* val = luaL_checklstring(L, 3, &size); int count = luaL_optinteger(L, 4, int(size)); - if (count < 0) - luaL_error(L, "count cannot be negative"); + if (FFlag::LuauBufferBetterMsg) + { + luaL_argcheck(L, count >= 0, 4, "count"); + } + else + { + if (count < 0) + luaL_error(L, "invalid count"); + } if (size_t(count) > size) luaL_error(L, "string length overflow"); diff --git a/VM/src/lbuiltins.cpp b/VM/src/lbuiltins.cpp index e5e2bac6..e28bb169 100644 --- a/VM/src/lbuiltins.cpp +++ b/VM/src/lbuiltins.cpp @@ -1353,7 +1353,7 @@ static int luauF_readinteger(lua_State* L, StkId res, TValue* arg0, int nresults return -1; T val; - memcpy(&val, (char*)bufvalue(arg0)->data + offset, sizeof(T)); + memcpy(&val, (char*)bufvalue(arg0)->data + unsigned(offset), sizeof(T)); setnvalue(res, double(val)); return 1; } @@ -1378,7 +1378,7 @@ static int luauF_writeinteger(lua_State* L, StkId res, TValue* arg0, int nresult luai_num2unsigned(value, incoming); T val = T(value); - memcpy((char*)bufvalue(arg0)->data + offset, &val, sizeof(T)); + memcpy((char*)bufvalue(arg0)->data + unsigned(offset), &val, sizeof(T)); return 0; } #endif @@ -1398,7 +1398,12 @@ static int luauF_readfp(lua_State* L, StkId res, TValue* arg0, int nresults, Stk return -1; T val; - memcpy(&val, (char*)bufvalue(arg0)->data + offset, sizeof(T)); +#ifdef _MSC_VER + // avoid memcpy path on MSVC because it results in integer stack copy + floating-point ops on stack + val = *(T*)((char*)bufvalue(arg0)->data + unsigned(offset)); +#else + memcpy(&val, (char*)bufvalue(arg0)->data + unsigned(offset), sizeof(T)); +#endif setnvalue(res, double(val)); return 1; } @@ -1419,7 +1424,12 @@ static int luauF_writefp(lua_State* L, StkId res, TValue* arg0, int nresults, St return -1; T val = T(nvalue(args + 1)); - memcpy((char*)bufvalue(arg0)->data + offset, &val, sizeof(T)); +#ifdef _MSC_VER + // avoid memcpy path on MSVC because it results in integer stack copy + floating-point ops on stack + *(T*)((char*)bufvalue(arg0)->data + unsigned(offset)) = val; +#else + memcpy((char*)bufvalue(arg0)->data + unsigned(offset), &val, sizeof(T)); +#endif return 0; } #endif diff --git a/VM/src/ldo.cpp b/VM/src/ldo.cpp index 6729f155..d13e98f3 100644 --- a/VM/src/ldo.cpp +++ b/VM/src/ldo.cpp @@ -17,8 +17,6 @@ #include -LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauHandlerClose, false) - /* ** {====================================================== ** Error-recovery functions @@ -409,7 +407,7 @@ static void resume_handle(lua_State* L, void* ud) L->ci = restoreci(L, old_ci); // close eventual pending closures; this means it's now safe to restore stack - luaF_close(L, DFFlag::LuauHandlerClose ? L->ci->base : L->base); + luaF_close(L, L->ci->base); // finish cont call and restore stack to previous ci top luau_poscall(L, L->top - n); diff --git a/VM/src/loslib.cpp b/VM/src/loslib.cpp index 1dbd34c6..a3365558 100644 --- a/VM/src/loslib.cpp +++ b/VM/src/loslib.cpp @@ -9,8 +9,6 @@ #define LUA_STRFTIMEOPTIONS "aAbBcdHIjmMpSUwWxXyYzZ%" -LUAU_FASTFLAGVARIABLE(LuauOsTimegm, false) - #if defined(_WIN32) static tm* gmtime_r(const time_t* timep, tm* result) { @@ -21,19 +19,10 @@ static tm* localtime_r(const time_t* timep, tm* result) { return localtime_s(result, timep) == 0 ? result : NULL; } - -static time_t timegm(struct tm* timep) -{ - LUAU_ASSERT(!FFlag::LuauOsTimegm); - - return _mkgmtime(timep); -} #endif static time_t os_timegm(struct tm* timep) { - LUAU_ASSERT(FFlag::LuauOsTimegm); - // Julian day number calculation int day = timep->tm_mday; int month = timep->tm_mon + 1; @@ -206,10 +195,7 @@ static int os_time(lua_State* L) ts.tm_isdst = getboolfield(L, "isdst"); // Note: upstream Lua uses mktime() here which assumes input is local time, but we prefer UTC for consistency - if (FFlag::LuauOsTimegm) - t = os_timegm(&ts); - else - t = timegm(&ts); + t = os_timegm(&ts); } if (t == (time_t)(-1)) lua_pushnil(L); diff --git a/VM/src/lutf8lib.cpp b/VM/src/lutf8lib.cpp index 4887b5e8..ef99b94f 100644 --- a/VM/src/lutf8lib.cpp +++ b/VM/src/lutf8lib.cpp @@ -8,6 +8,8 @@ #define iscont(p) ((*(p)&0xC0) == 0x80) +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauStricterUtf8, false) + // from strlib // translate a relative string position: negative means back from end static int u_posrelat(int pos, size_t len) @@ -45,6 +47,8 @@ static const char* utf8_decode(const char* o, int* val) res |= ((c & 0x7F) << (count * 5)); // add first byte if (count > 3 || res > MAXUNICODE || res <= limits[count]) return NULL; // invalid byte sequence + if (DFFlag::LuauStricterUtf8 && unsigned(res - 0xD800) < 0x800) + return NULL; // surrogate s += count; // skip continuation bytes read } if (val) diff --git a/VM/src/lvmexecute.cpp b/VM/src/lvmexecute.cpp index 5eecc2ac..451433ee 100644 --- a/VM/src/lvmexecute.cpp +++ b/VM/src/lvmexecute.cpp @@ -135,8 +135,6 @@ // Does VM support native execution via ExecutionCallbacks? We mostly assume it does but keep the define to make it easy to quantify the cost. #define VM_HAS_NATIVE 1 -void (*lua_iter_call_telemetry)(lua_State* L, int gtt, int stt, int itt) = NULL; - LUAU_NOINLINE void luau_callhook(lua_State* L, lua_Hook hook, void* userdata) { ptrdiff_t base = savestack(L, L->base); @@ -2293,10 +2291,6 @@ reentry: { // table or userdata with __call, will be called during FORGLOOP // TODO: we might be able to stop supporting this depending on whether it's used in practice - void (*telemetrycb)(lua_State * L, int gtt, int stt, int itt) = lua_iter_call_telemetry; - - if (telemetrycb) - telemetrycb(L, ttype(ra), ttype(ra + 1), ttype(ra + 2)); } else if (ttistable(ra)) { diff --git a/bench/tests/pcmmix.lua b/bench/tests/pcmmix.lua new file mode 100644 index 00000000..a1760f67 --- /dev/null +++ b/bench/tests/pcmmix.lua @@ -0,0 +1,33 @@ +local bench = script and require(script.Parent.bench_support) or require("bench_support") + +local samples = 100_000 + +-- create two 16-bit stereo pcm audio buffers +local ch1 = buffer.create(samples * 2 * 2) +local ch2 = buffer.create(samples * 2 * 2) + +-- just init with random data +for i = 0, samples * 2 - 1 do + buffer.writei16(ch1, i * 2, math.random(-32768, 32767)) + buffer.writei16(ch2, i * 2, math.random(-32768, 32767)) +end + +function test() + local mix = buffer.create(samples * 2 * 2) + + for i = 0, samples - 1 do + local s1l = buffer.readi16(ch1, i * 4) + local s1r = buffer.readi16(ch1, i * 4 + 2) + + local s2l = buffer.readi16(ch2, i * 4) + local s2r = buffer.readi16(ch2, i * 4 + 2) + + local combinedl = s1l + s2l - s1l * s2l / 32768 + local combinedr = s1r + s2r - s1r * s2r / 32768 + + buffer.writei16(mix, i * 4, combinedl) + buffer.writei16(mix, i * 4 + 2, combinedr) + end +end + +bench.runCode(test, "pcmmix") diff --git a/bench/tests/sha256.lua b/bench/tests/sha256.lua index 0e4227a3..a01e801e 100644 --- a/bench/tests/sha256.lua +++ b/bench/tests/sha256.lua @@ -132,7 +132,8 @@ function test() local ts0 = os.clock() for i = 1, 100 do - sha256(input) + local res = sha256(input) + assert(res == "45849646c50337988ccc877d23fcc0de50d1df7490fdc3b9333aed0de8ab492a") end local ts1 = os.clock() diff --git a/fuzz/proto.cpp b/fuzz/proto.cpp index 63c7618f..ba6fb4c8 100644 --- a/fuzz/proto.cpp +++ b/fuzz/proto.cpp @@ -20,25 +20,32 @@ #include "lualib.h" #include +#include + +static bool getEnvParam(const char* name, bool def) +{ + char* val = getenv(name); + if (val == nullptr) + return def; + else + return strcmp(val, "0") != 0; +} // Select components to fuzz -const bool kFuzzCompiler = true; -const bool kFuzzLinter = true; -const bool kFuzzTypeck = true; -const bool kFuzzVM = true; -const bool kFuzzTranspile = true; -const bool kFuzzCodegenVM = true; -const bool kFuzzCodegenAssembly = true; +const bool kFuzzCompiler = getEnvParam("LUAU_FUZZ_COMPILER", true); +const bool kFuzzLinter = getEnvParam("LUAU_FUZZ_LINTER", true); +const bool kFuzzTypeck = getEnvParam("LUAU_FUZZ_TYPE_CHECK", true); +const bool kFuzzVM = getEnvParam("LUAU_FUZZ_VM", true); +const bool kFuzzTranspile = getEnvParam("LUAU_FUZZ_TRANSPILE", true); +const bool kFuzzCodegenVM = getEnvParam("LUAU_FUZZ_CODEGEN_VM", true); +const bool kFuzzCodegenAssembly = getEnvParam("LUAU_FUZZ_CODEGEN_ASM", true); +const bool kFuzzUseNewSolver = getEnvParam("LUAU_FUZZ_NEW_SOLVER", false); // Should we generate type annotations? -const bool kFuzzTypes = true; +const bool kFuzzTypes = getEnvParam("LUAU_FUZZ_GEN_TYPES", true); const Luau::CodeGen::AssemblyOptions::Target kFuzzCodegenTarget = Luau::CodeGen::AssemblyOptions::A64; -static_assert(!(kFuzzVM && !kFuzzCompiler), "VM requires the compiler!"); -static_assert(!(kFuzzCodegenVM && !kFuzzCompiler), "Codegen requires the compiler!"); -static_assert(!(kFuzzCodegenAssembly && !kFuzzCompiler), "Codegen requires the compiler!"); - std::vector protoprint(const luau::ModuleSet& stat, bool types); LUAU_FASTINT(LuauTypeInferRecursionLimit) @@ -49,6 +56,7 @@ LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauAbortingChecks) +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) std::chrono::milliseconds kInterruptTimeout(10); std::chrono::time_point interruptDeadline; @@ -218,6 +226,13 @@ static std::vector debugsources; DEFINE_PROTO_FUZZER(const luau::ModuleSet& message) { + if (!kFuzzCompiler && (kFuzzCodegenAssembly || kFuzzCodegenVM || kFuzzVM)) + { + printf("Compiler is required in order to fuzz codegen or the VM\n"); + LUAU_ASSERT(false); + return; + } + FInt::LuauTypeInferRecursionLimit.value = 100; FInt::LuauTypeInferTypePackLoopLimit.value = 100; FInt::LuauCheckRecursionLimit.value = 100; @@ -231,6 +246,7 @@ DEFINE_PROTO_FUZZER(const luau::ModuleSet& message) FFlag::DebugLuauFreezeArena.value = true; FFlag::DebugLuauAbortingChecks.value = true; + FFlag::DebugLuauDeferredConstraintResolution.value = kFuzzUseNewSolver; std::vector sources = protoprint(message, kFuzzTypes); diff --git a/tests/AssemblyBuilderX64.test.cpp b/tests/AssemblyBuilderX64.test.cpp index ccf1ca17..c55de91a 100644 --- a/tests/AssemblyBuilderX64.test.cpp +++ b/tests/AssemblyBuilderX64.test.cpp @@ -208,6 +208,11 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfMov") SINGLE_COMPARE(mov(byte[rsi], al), 0x88, 0x06); SINGLE_COMPARE(mov(byte[rsi], dil), 0x48, 0x88, 0x3e); SINGLE_COMPARE(mov(byte[rsi], r10b), 0x4c, 0x88, 0x16); + SINGLE_COMPARE(mov(wordReg(ebx), 0x3a3d), 0x66, 0xbb, 0x3d, 0x3a); + SINGLE_COMPARE(mov(word[rsi], 0x3a3d), 0x66, 0xc7, 0x06, 0x3d, 0x3a); + SINGLE_COMPARE(mov(word[rsi], wordReg(eax)), 0x66, 0x89, 0x06); + SINGLE_COMPARE(mov(word[rsi], wordReg(edi)), 0x66, 0x89, 0x3e); + SINGLE_COMPARE(mov(word[rsi], wordReg(r10)), 0x66, 0x44, 0x89, 0x16); } TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfMovExtended") @@ -531,6 +536,8 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXConversionInstructionForms") SINGLE_COMPARE(vcvtsi2sd(xmm6, xmm11, qword[rcx + rdx]), 0xc4, 0xe1, 0xa3, 0x2a, 0x34, 0x11); SINGLE_COMPARE(vcvtsd2ss(xmm5, xmm10, xmm11), 0xc4, 0xc1, 0x2b, 0x5a, 0xeb); SINGLE_COMPARE(vcvtsd2ss(xmm6, xmm11, qword[rcx + rdx]), 0xc4, 0xe1, 0xa3, 0x5a, 0x34, 0x11); + SINGLE_COMPARE(vcvtss2sd(xmm3, xmm8, xmm12), 0xc4, 0xc1, 0x3a, 0x5a, 0xdc); + SINGLE_COMPARE(vcvtss2sd(xmm4, xmm9, dword[rcx + rsi]), 0xc4, 0xe1, 0x32, 0x5a, 0x24, 0x31); } TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXTernaryInstructionForms") diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index 112c58e9..f2d559cc 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -1147,33 +1147,27 @@ L0: RETURN R1 1 TEST_CASE("AndOrFoldLeft") { // constant folding and/or expression is possible even if just the left hand is constant - CHECK_EQ("\n" + compileFunction0("local a = false if a and b then b() end"), R"( -RETURN R0 0 + CHECK_EQ("\n" + compileFunction0("local a = false return a and b"), R"( +LOADB R0 0 +RETURN R0 1 )"); - CHECK_EQ("\n" + compileFunction0("local a = true if a or b then b() end"), R"( -GETIMPORT R0 1 [b] -CALL R0 0 0 -RETURN R0 0 + CHECK_EQ("\n" + compileFunction0("local a = true return a or b"), R"( +LOADB R0 1 +RETURN R0 1 )"); - // however, if right hand side is constant we can't constant fold the entire expression - // (note that we don't need to evaluate the right hand side, but we do need a branch) - CHECK_EQ("\n" + compileFunction0("local a = false if b and a then b() end"), R"( -GETIMPORT R0 1 [b] -JUMPIFNOT R0 L0 -RETURN R0 0 -GETIMPORT R0 1 [b] -CALL R0 0 0 -L0: RETURN R0 0 + // if right hand side is constant we can't constant fold the entire expression + CHECK_EQ("\n" + compileFunction0("local a = false return b and a"), R"( +GETIMPORT R1 2 [b] +ANDK R0 R1 K0 [false] +RETURN R0 1 )"); - CHECK_EQ("\n" + compileFunction0("local a = true if b or a then b() end"), R"( -GETIMPORT R0 1 [b] -JUMPIF R0 L0 -L0: GETIMPORT R0 1 [b] -CALL R0 0 0 -RETURN R0 0 + CHECK_EQ("\n" + compileFunction0("local a = true return b or a"), R"( +GETIMPORT R1 2 [b] +ORK R0 R1 K0 [true] +RETURN R0 1 )"); } @@ -1914,8 +1908,6 @@ RETURN R0 0 TEST_CASE("LoopContinueIgnoresImplicitConstant") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; - // this used to crash the compiler :( CHECK_EQ("\n" + compileFunction0(R"( local _ @@ -1931,8 +1923,6 @@ RETURN R0 0 TEST_CASE("LoopContinueIgnoresExplicitConstant") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; - // Constants do not allocate locals and 'continue' validation should skip them if their lifetime already started CHECK_EQ("\n" + compileFunction0(R"( local c = true @@ -1948,8 +1938,6 @@ RETURN R0 0 TEST_CASE("LoopContinueRespectsExplicitConstant") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; - // If local lifetime hasn't started, even if it's a constant that will not receive an allocation, it cannot be jumped over try { @@ -1974,8 +1962,6 @@ until c TEST_CASE("LoopContinueIgnoresImplicitConstantAfterInline") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; - // Inlining might also replace some locals with constants instead of allocating them CHECK_EQ("\n" + compileFunction(R"( local function inline(f) @@ -1999,7 +1985,6 @@ RETURN R0 0 TEST_CASE("LoopContinueCorrectlyHandlesImplicitConstantAfterUnroll") { - ScopedFastFlag sff{"LuauCompileFixContinueValidation2", true}; ScopedFastInt sfi("LuauCompileLoopUnrollThreshold", 200); // access to implicit constant that depends on the unrolled loop constant is still invalid even though we can constant-propagate it @@ -2015,7 +2000,8 @@ for i = 1, 2 do local x = i == 1 or a until f(x) end -)", 0, 2); +)", + 0, 2); CHECK(!"Expected CompileError"); } @@ -7625,8 +7611,6 @@ L0: RETURN R0 2 TEST_CASE("IfThenElseAndOr") { - ScopedFastFlag sff("LuauCompileIfElseAndOr", true); - // if v then v else k can be optimized to ORK CHECK_EQ("\n" + compileFunction0(R"( local x = ... @@ -7722,4 +7706,127 @@ RETURN R1 1 )"); } +TEST_CASE("SideEffects") +{ + ScopedFastFlag sff("LuauCompileSideEffects", true); + + // we do not evaluate expressions in some cases when we know they can't carry side effects + CHECK_EQ("\n" + compileFunction0(R"( +local x = 5, print +local y = 5, 42 +local z = 5, table.find -- considered side effecting because of metamethods +)"), + R"( +LOADN R0 5 +LOADN R1 5 +LOADN R2 5 +GETIMPORT R3 2 [table.find] +RETURN R0 0 +)"); + + // this also applies to returns in cases where a function gets inlined + CHECK_EQ("\n" + compileFunction(R"( +local function test1() + return 42 +end + +local function test2() + return print +end + +local function test3() + return function() print(test3) end +end + +local function test4() + return table.find -- considered side effecting because of metamethods +end + +test1() +test2() +test3() +test4() +)", + 5, 2), + R"( +DUPCLOSURE R0 K0 ['test1'] +DUPCLOSURE R1 K1 ['test2'] +DUPCLOSURE R2 K2 ['test3'] +CAPTURE VAL R2 +DUPCLOSURE R3 K3 ['test4'] +GETIMPORT R4 6 [table.find] +RETURN R0 0 +)"); +} + +TEST_CASE("IfElimination") +{ + ScopedFastFlag sff1("LuauCompileDeadIf", true); + ScopedFastFlag sff2("LuauCompileSideEffects", true); + + // if the left hand side of a condition is constant, it constant folds and we don't emit the branch + CHECK_EQ("\n" + compileFunction0("local a = false if a and b then b() end"), R"( +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction0("local a = true if a or b then b() end"), R"( +GETIMPORT R0 1 [b] +CALL R0 0 0 +RETURN R0 0 +)"); + + // of course this keeps the other branch if present + CHECK_EQ("\n" + compileFunction0("local a = false if a and b then b() else return 42 end"), R"( +LOADN R0 42 +RETURN R0 1 +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction0("local a = true if a or b then b() else return 42 end"), R"( +GETIMPORT R0 1 [b] +CALL R0 0 0 +RETURN R0 0 +)"); + + // if the right hand side is constant, the condition doesn't constant fold but we still could eliminate one of the branches for 'a and K' + CHECK_EQ("\n" + compileFunction0("local a = false if b and a then return 1 end"), R"( +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction0("local a = false if b and a then return 1 else return 2 end"), R"( +LOADN R0 2 +RETURN R0 1 +)"); + + // of course if the right hand side of 'and' is 'true', we still need to actually evaluate the left hand side + CHECK_EQ("\n" + compileFunction0("local a = true if b and a then return 1 end"), R"( +GETIMPORT R0 1 [b] +JUMPIFNOT R0 L0 +LOADN R0 1 +RETURN R0 1 +L0: RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction0("local a = true if b and a then return 1 else return 2 end"), R"( +GETIMPORT R0 1 [b] +JUMPIFNOT R0 L0 +LOADN R0 1 +RETURN R0 1 +L0: LOADN R0 2 +RETURN R0 1 +)"); + + // also even if we eliminate the branch, we still need to compute side effects + CHECK_EQ("\n" + compileFunction0("local a = false if b.test and a then return 1 end"), R"( +GETIMPORT R0 2 [b.test] +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction0("local a = false if b.test and a then return 1 else return 2 end"), R"( +GETIMPORT R0 2 [b.test] +LOADN R0 2 +RETURN R0 1 +)"); +} + TEST_SUITE_END(); diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index b7f77711..968a55be 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -24,8 +24,6 @@ extern bool verbose; extern bool codegen; extern int optimizationLevel; -LUAU_FASTFLAG(LuauFloorDivision); - static lua_CompileOptions defaultOptions() { lua_CompileOptions copts = {}; @@ -288,13 +286,13 @@ TEST_CASE("Assert") TEST_CASE("Basic") { - ScopedFastFlag sffs{"LuauFloorDivision", true}; - runConformance("basic.lua"); } TEST_CASE("Buffers") { + ScopedFastFlag luauBufferBetterMsg{"LuauBufferBetterMsg", true}; + runConformance("buffers.lua"); } @@ -379,7 +377,6 @@ TEST_CASE("Errors") TEST_CASE("Events") { - ScopedFastFlag sffs{"LuauFloorDivision", true}; runConformance("events.lua"); } @@ -416,6 +413,7 @@ TEST_CASE("Bitwise") TEST_CASE("UTF8") { + ScopedFastFlag sff("LuauStricterUtf8", true); runConformance("utf8.lua"); } @@ -435,8 +433,6 @@ static int cxxthrow(lua_State* L) TEST_CASE("PCall") { - ScopedFastFlag sff("LuauHandlerClose", true); - runConformance( "pcall.lua", [](lua_State* L) { @@ -464,8 +460,6 @@ TEST_CASE("Pack") TEST_CASE("Vector") { - ScopedFastFlag sffs{"LuauFloorDivision", true}; - lua_CompileOptions copts = defaultOptions(); copts.vectorCtor = "vector"; @@ -523,6 +517,10 @@ static void populateRTTI(lua_State* L, Luau::TypeId type) lua_pushstring(L, "thread"); break; + case Luau::PrimitiveType::Buffer: + lua_pushstring(L, "buffer"); + break; + default: LUAU_ASSERT(!"Unknown primitive type"); } @@ -1698,9 +1696,6 @@ static void pushInt64(lua_State* L, int64_t value) TEST_CASE("Userdata") { - - ScopedFastFlag sffs{"LuauFloorDivision", true}; - runConformance("userdata.lua", [](lua_State* L) { // create metatable with all the metamethods lua_newtable(L); diff --git a/tests/ConstraintGraphBuilderFixture.cpp b/tests/ConstraintGeneratorFixture.cpp similarity index 62% rename from tests/ConstraintGraphBuilderFixture.cpp rename to tests/ConstraintGeneratorFixture.cpp index 293c26ff..dc1aea80 100644 --- a/tests/ConstraintGraphBuilderFixture.cpp +++ b/tests/ConstraintGeneratorFixture.cpp @@ -1,10 +1,10 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "ConstraintGraphBuilderFixture.h" +#include "ConstraintGeneratorFixture.h" namespace Luau { -ConstraintGraphBuilderFixture::ConstraintGraphBuilderFixture() +ConstraintGeneratorFixture::ConstraintGeneratorFixture() : Fixture() , mainModule(new Module) , forceTheFlag{"DebugLuauDeferredConstraintResolution", true} @@ -15,18 +15,18 @@ ConstraintGraphBuilderFixture::ConstraintGraphBuilderFixture() BlockedTypePack::nextIndex = 0; } -void ConstraintGraphBuilderFixture::generateConstraints(const std::string& code) +void ConstraintGeneratorFixture::generateConstraints(const std::string& code) { AstStatBlock* root = parse(code); dfg = std::make_unique(DataFlowGraphBuilder::build(root, NotNull{&ice})); - cgb = std::make_unique(mainModule, NotNull{&normalizer}, NotNull(&moduleResolver), builtinTypes, NotNull(&ice), + cg = std::make_unique(mainModule, NotNull{&normalizer}, NotNull(&moduleResolver), builtinTypes, NotNull(&ice), frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, &logger, NotNull{dfg.get()}, std::vector()); - cgb->visitModuleRoot(root); - rootScope = cgb->rootScope; - constraints = Luau::borrowConstraints(cgb->constraints); + cg->visitModuleRoot(root); + rootScope = cg->rootScope; + constraints = Luau::borrowConstraints(cg->constraints); } -void ConstraintGraphBuilderFixture::solve(const std::string& code) +void ConstraintGeneratorFixture::solve(const std::string& code) { generateConstraints(code); ConstraintSolver cs{NotNull{&normalizer}, NotNull{rootScope}, constraints, "MainModule", NotNull(&moduleResolver), {}, &logger, {}}; diff --git a/tests/ConstraintGraphBuilderFixture.h b/tests/ConstraintGeneratorFixture.h similarity index 81% rename from tests/ConstraintGraphBuilderFixture.h rename to tests/ConstraintGeneratorFixture.h index 5e7fedab..ff362be1 100644 --- a/tests/ConstraintGraphBuilderFixture.h +++ b/tests/ConstraintGeneratorFixture.h @@ -1,7 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/ConstraintSolver.h" #include "Luau/DcrLogger.h" #include "Luau/TypeArena.h" @@ -13,7 +13,7 @@ namespace Luau { -struct ConstraintGraphBuilderFixture : Fixture +struct ConstraintGeneratorFixture : Fixture { TypeArena arena; ModulePtr mainModule; @@ -22,14 +22,14 @@ struct ConstraintGraphBuilderFixture : Fixture Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; std::unique_ptr dfg; - std::unique_ptr cgb; + std::unique_ptr cg; Scope* rootScope = nullptr; std::vector> constraints; ScopedFastFlag forceTheFlag; - ConstraintGraphBuilderFixture(); + ConstraintGeneratorFixture(); void generateConstraints(const std::string& code); void solve(const std::string& code); diff --git a/tests/ConstraintSolver.test.cpp b/tests/ConstraintSolver.test.cpp index 32cb3cda..204d4d14 100644 --- a/tests/ConstraintSolver.test.cpp +++ b/tests/ConstraintSolver.test.cpp @@ -1,6 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "ConstraintGraphBuilderFixture.h" +#include "ConstraintGeneratorFixture.h" #include "Fixture.h" #include "doctest.h" @@ -17,7 +17,7 @@ static TypeId requireBinding(Scope* scope, const char* name) TEST_SUITE_BEGIN("ConstraintSolver"); -TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "hello") +TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "hello") { solve(R"( local a = 55 @@ -29,7 +29,7 @@ TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "hello") CHECK("number" == toString(bType)); } -TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "generic_function") +TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "generic_function") { solve(R"( local function id(a) @@ -42,7 +42,7 @@ TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "generic_function") CHECK("(a) -> a" == toString(idType)); } -TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "proper_let_generalization") +TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "proper_let_generalization") { solve(R"( local function a(c) diff --git a/tests/DataFlowGraph.test.cpp b/tests/DataFlowGraph.test.cpp index cd91039b..e957316e 100644 --- a/tests/DataFlowGraph.test.cpp +++ b/tests/DataFlowGraph.test.cpp @@ -92,4 +92,229 @@ TEST_CASE_FIXTURE(DataFlowGraphFixture, "independent_locals") REQUIRE(x != y); } +TEST_CASE_FIXTURE(DataFlowGraphFixture, "phi") +{ + dfg(R"( + local x + + if a then + x = true + end + + local y = x + )"); + + DefId y = getDef(); + + const Phi* phi = get(y); + CHECK(phi); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_local_not_owned_by_while") +{ + dfg(R"( + local x + + while cond() do + x = true + end + + local y = x + )"); + + DefId x0 = graph->getDef(query(module)->vars.data[0]); + DefId x1 = getDef(); // x = true + DefId x2 = getDef(); // local y = x + + CHECK(x0 == x1); + CHECK(x1 == x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_local_owned_by_while") +{ + dfg(R"( + while cond() do + local x + x = true + x = 5 + end + )"); + + DefId x0 = graph->getDef(query(module)->vars.data[0]); + DefId x1 = getDef(); // x = true + DefId x2 = getDef(); // x = 5 + + CHECK(x0 != x1); + CHECK(x1 != x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_local_not_owned_by_repeat") +{ + dfg(R"( + local x + + repeat + x = true + until cond() + + local y = x + )"); + + DefId x0 = graph->getDef(query(module)->vars.data[0]); + DefId x1 = getDef(); // x = true + DefId x2 = getDef(); // local y = x + + CHECK(x0 == x1); + CHECK(x1 == x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_local_owned_by_repeat") +{ + dfg(R"( + repeat + local x + x = true + x = 5 + until cond() + )"); + + DefId x0 = graph->getDef(query(module)->vars.data[0]); + DefId x1 = getDef(); // x = true + DefId x2 = getDef(); // x = 5 + + CHECK(x0 != x1); + CHECK(x1 != x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_local_not_owned_by_for") +{ + dfg(R"( + local x + + for i = 0, 5 do + x = true + end + + local y = x + )"); + + DefId x0 = graph->getDef(query(module)->vars.data[0]); + DefId x1 = getDef(); // x = true + DefId x2 = getDef(); // local y = x + + CHECK(x0 == x1); + CHECK(x1 == x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_local_owned_by_for") +{ + dfg(R"( + for i = 0, 5 do + local x + x = true + x = 5 + end + )"); + + DefId x0 = graph->getDef(query(module)->vars.data[0]); + DefId x1 = getDef(); // x = true + DefId x2 = getDef(); // x = 5 + + CHECK(x0 != x1); + CHECK(x1 != x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_local_not_owned_by_for_in") +{ + dfg(R"( + local x + + for i, v in t do + x = true + end + + local y = x + )"); + + DefId x0 = graph->getDef(query(module)->vars.data[0]); + DefId x1 = getDef(); // x = true + DefId x2 = getDef(); // local y = x + + CHECK(x0 == x1); + CHECK(x1 == x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_local_owned_by_for_in") +{ + dfg(R"( + for i, v in t do + local x + x = true + x = 5 + end + )"); + + DefId x0 = graph->getDef(query(module)->vars.data[0]); + DefId x1 = getDef(); // x = true + DefId x2 = getDef(); // x = 5 + + CHECK(x0 != x1); + CHECK(x1 != x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_preexisting_property_not_owned_by_while") +{ + dfg(R"( + local t = {} + t.x = 5 + + while cond() do + t.x = true + end + + local y = t.x + )"); + + DefId x1 = getDef(); // t.x = 5 + DefId x2 = getDef(); // t.x = true + DefId x3 = getDef(); // local y = t.x + + CHECK(x1 == x2); + CHECK(x2 == x3); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_non_preexisting_property_not_owned_by_while") +{ + dfg(R"( + local t = {} + + while cond() do + t.x = true + end + + local y = t.x + )"); + + DefId x1 = getDef(); // t.x = true + DefId x2 = getDef(); // local y = t.x + + CHECK(x1 == x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_property_of_table_owned_by_while") +{ + dfg(R"( + while cond() do + local t = {} + t.x = true + t.x = 5 + end + )"); + + DefId x1 = getDef(); // t.x = true + DefId x2 = getDef(); // t.x = 5 + + CHECK(x1 != x2); +} + TEST_SUITE_END(); diff --git a/tests/Differ.test.cpp b/tests/Differ.test.cpp index fded0715..6b7a6558 100644 --- a/tests/Differ.test.cpp +++ b/tests/Differ.test.cpp @@ -154,7 +154,7 @@ TEST_CASE_FIXTURE(DifferFixture, "left_cyclic_table_right_table_property_wrong") local function id(x: a): a return x end - + -- Remove name from cyclic table local foo = id({}) foo.foo = foo @@ -172,7 +172,7 @@ TEST_CASE_FIXTURE(DifferFixture, "right_cyclic_table_left_table_missing_property local function id(x: a): a return x end - + -- Remove name from cyclic table local foo = id({}) foo.foo = foo @@ -190,7 +190,7 @@ TEST_CASE_FIXTURE(DifferFixture, "right_cyclic_table_left_table_property_wrong") local function id(x: a): a return x end - + -- Remove name from cyclic table local foo = id({}) foo.foo = foo @@ -208,7 +208,7 @@ TEST_CASE_FIXTURE(DifferFixture, "equal_table_two_cyclic_tables_are_not_differen local function id(x: a): a return x end - + -- Remove name from cyclic table local foo = id({}) foo.foo = foo @@ -226,7 +226,7 @@ TEST_CASE_FIXTURE(DifferFixture, "equal_table_two_shifted_circles_are_not_differ local function id(x: a): a return x end - + -- Remove name from cyclic table local foo = id({}) foo.foo = id({}) @@ -254,7 +254,7 @@ TEST_CASE_FIXTURE(DifferFixture, "table_left_circle_right_measuring_tape") local function id(x: a): a return x end - + -- Remove name from cyclic table local foo = id({}) foo.foo = id({}) @@ -281,7 +281,7 @@ TEST_CASE_FIXTURE(DifferFixture, "equal_table_measuring_tapes") local function id(x: a): a return x end - + -- Remove name from cyclic table local foo = id({}) foo.foo = id({}) @@ -305,7 +305,7 @@ TEST_CASE_FIXTURE(DifferFixture, "equal_table_A_B_C") local function id(x: a): a return x end - + -- Remove name from cyclic table local foo = id({}) foo.foo = id({}) @@ -774,7 +774,7 @@ TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "negation") if typeof(almostBar.x.y) ~= "number" then almostFoo = almostBar end - + )"); LUAU_REQUIRE_NO_ERRORS(result); diff --git a/tests/Error.test.cpp b/tests/Error.test.cpp index 0a71794f..a1869e88 100644 --- a/tests/Error.test.cpp +++ b/tests/Error.test.cpp @@ -17,7 +17,7 @@ TEST_CASE("TypeError_code_should_return_nonzero_code") TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_names_show_instead_of_tables") { frontend.options.retainFullTypeGraphs = false; - ScopedFastFlag sff{"LuauStacklessTypeClone2", true}; + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; CheckResult result = check(R"( --!strict local Account = {} diff --git a/tests/Fixture.h b/tests/Fixture.h index fa3dfb06..87073b2b 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -99,6 +99,8 @@ struct Fixture ScopedFastFlag sff_DebugLuauFreezeArena; + ScopedFastFlag luauBufferTypeck{"LuauBufferTypeck", true}; + TestFileResolver fileResolver; TestConfigResolver configResolver; NullModuleResolver moduleResolver; @@ -185,17 +187,9 @@ struct DifferFixtureGeneric : BaseFixture void compareNe(TypeId left, std::optional symbolLeft, TypeId right, std::optional symbolRight, const std::string& expectedMessage, bool multiLine) { - std::string diffMessage; - try - { - DifferResult diffRes = diffWithSymbols(left, right, symbolLeft, symbolRight); - REQUIRE_MESSAGE(diffRes.diffError.has_value(), "Differ did not report type error, even though types are unequal"); - diffMessage = diffRes.diffError->toString(multiLine); - } - catch (const InternalCompilerError& e) - { - REQUIRE_MESSAGE(false, ("InternalCompilerError: " + e.message)); - } + DifferResult diffRes = diffWithSymbols(left, right, symbolLeft, symbolRight); + REQUIRE_MESSAGE(diffRes.diffError.has_value(), "Differ did not report type error, even though types are unequal"); + std::string diffMessage = diffRes.diffError->toString(multiLine); CHECK_EQ(expectedMessage, diffMessage); } @@ -216,15 +210,10 @@ struct DifferFixtureGeneric : BaseFixture void compareEq(TypeId left, TypeId right) { - try - { - DifferResult diffRes = diff(left, right); - CHECK_MESSAGE(!diffRes.diffError.has_value(), diffRes.diffError->toString()); - } - catch (const InternalCompilerError& e) - { - REQUIRE_MESSAGE(false, ("InternalCompilerError: " + e.message)); - } + DifferResult diffRes = diff(left, right); + CHECK(!diffRes.diffError); + if (diffRes.diffError) + INFO(diffRes.diffError->toString()); } void compareTypesEq(const std::string& leftSymbol, const std::string& rightSymbol) diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 61333422..3c584c48 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -1238,7 +1238,7 @@ TEST_CASE_FIXTURE(FrontendFixture, "parse_only") REQUIRE(frontend.sourceNodes.count("game/Gui/Modules/B")); auto node = frontend.sourceNodes["game/Gui/Modules/B"]; - CHECK_EQ(node->requireSet.count("game/Gui/Modules/A"), 1); + CHECK(node->requireSet.contains("game/Gui/Modules/A")); REQUIRE_EQ(node->requireLocations.size(), 1); CHECK_EQ(node->requireLocations[0].second, Luau::Location(Position(2, 18), Position(2, 36))); diff --git a/tests/IostreamOptional.h b/tests/IostreamOptional.h index e0756bad..51122f38 100644 --- a/tests/IostreamOptional.h +++ b/tests/IostreamOptional.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/DenseHash.h" #include #include @@ -21,4 +22,40 @@ auto operator<<(std::ostream& lhs, const std::optional& t) -> decltype(lhs << return lhs << "none"; } +template +auto operator<<(std::ostream& lhs, const std::vector& t) -> decltype(lhs << t[0]) +{ + lhs << "{ "; + bool first = true; + for (const T& element : t) + { + if (first) + first = false; + else + lhs << ", "; + + lhs << element; + } + + return lhs << " }"; +} + +template +auto operator<<(std::ostream& lhs, const Luau::DenseHashSet& set) -> decltype(lhs << *set.begin()) +{ + lhs << "{ "; + bool first = true; + for (const K& element : set) + { + if (first) + first = false; + else + lhs << ", "; + + lhs << element; + } + + return lhs << " }"; +} + } // namespace std diff --git a/tests/IrBuilder.test.cpp b/tests/IrBuilder.test.cpp index 4388c400..1b7242e2 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -1933,8 +1933,6 @@ bb_0: TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecks") { - ScopedFastFlag luauReuseHashSlots{"LuauReuseHashSlots2", true}; - IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); @@ -1991,8 +1989,6 @@ bb_fallback_1: TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecksAvoidNil") { - ScopedFastFlag luauReuseHashSlots{"LuauReuseHashSlots2", true}; - IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); @@ -3074,8 +3070,6 @@ bb_0: TEST_CASE_FIXTURE(IrBuilderFixture, "TagSelfEqualityCheckRemoval") { - ScopedFastFlag luauMergeTagLoads{"LuauMergeTagLoads", true}; - IrOp entry = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); @@ -3106,3 +3100,37 @@ bb_1: } TEST_SUITE_END(); + +TEST_SUITE_BEGIN("Dump"); + +TEST_CASE_FIXTURE(IrBuilderFixture, "ToDot") +{ + IrOp entry = build.block(IrBlockKind::Internal); + IrOp a = build.block(IrBlockKind::Internal); + IrOp b = build.block(IrBlockKind::Internal); + IrOp exit = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::JUMP_EQ_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(tnumber), a, b); + + build.beginBlock(a); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); + build.inst(IrCmd::JUMP, exit); + + build.beginBlock(b); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); + build.inst(IrCmd::JUMP, exit); + + build.beginBlock(exit); + build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(2)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + + // note: we don't validate the output of these to avoid test churn when formatting changes; we run these to make sure they don't assert/crash + toDot(build.function, /* includeInst= */ true); + toDotCfg(build.function); + toDotDjGraph(build.function); +} + +TEST_SUITE_END(); diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index 9907d7a1..7f6431d9 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -1517,8 +1517,6 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiFenv") { - ScopedFastFlag sff("LuauLintDeprecatedFenv", true); - LintResult result = lint(R"( local f, g, h = ... @@ -1591,8 +1589,6 @@ table.create(42, {} :: {}) TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperationsIndexer") { - ScopedFastFlag sff("LuauLintTableIndexer", true); - LintResult result = lint(R"( local t1 = {} -- ok: empty local t2 = {1, 2} -- ok: array @@ -1690,8 +1686,8 @@ TEST_CASE_FIXTURE(Fixture, "DuplicateConditionsExpr") LintResult result = lint(R"( local correct, opaque = ... -if correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls")}) then -elseif correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls")}) then +if correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls", `string {opaque}`)}) then +elseif correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls", `string {opaque}`)}) then elseif correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls", false)}) then end )"); @@ -1827,8 +1823,71 @@ local _ = 0x10000000000000000 )"); REQUIRE(2 == result.warnings.size()); - CHECK_EQ(result.warnings[0].text, "Binary number literal exceeded available precision and has been truncated to 2^64"); - CHECK_EQ(result.warnings[1].text, "Hexadecimal number literal exceeded available precision and has been truncated to 2^64"); + CHECK_EQ(result.warnings[0].text, "Binary number literal exceeded available precision and was truncated to 2^64"); + CHECK_EQ(result.warnings[1].text, "Hexadecimal number literal exceeded available precision and was truncated to 2^64"); +} + +TEST_CASE_FIXTURE(Fixture, "IntegerParsingDecimalImprecise") +{ + ScopedFastFlag sff("LuauParseImpreciseNumber", true); + + LintResult result = lint(R"( +local _ = 10000000000000000000000000000000000000000000000000000000000000000 +local _ = 10000000000000001 +local _ = -10000000000000001 + +-- 10^16 = 2^16 * 5^16, 5^16 only requires 38 bits +local _ = 10000000000000000 +local _ = -10000000000000000 + +-- smallest possible number that is parsed imprecisely +local _ = 9007199254740993 +local _ = -9007199254740993 + +-- note that numbers before and after parse precisely (number after is even => 1 more mantissa bit) +local _ = 9007199254740992 +local _ = 9007199254740994 + +-- large powers of two should work as well (this is 2^63) +local _ = -9223372036854775808 +)"); + + REQUIRE(5 == result.warnings.size()); + CHECK_EQ(result.warnings[0].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[0].location.begin.line, 1); + CHECK_EQ(result.warnings[1].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[1].location.begin.line, 2); + CHECK_EQ(result.warnings[2].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[2].location.begin.line, 3); + CHECK_EQ(result.warnings[3].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[3].location.begin.line, 10); + CHECK_EQ(result.warnings[4].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[4].location.begin.line, 11); +} + +TEST_CASE_FIXTURE(Fixture, "IntegerParsingHexImprecise") +{ + ScopedFastFlag sff("LuauParseImpreciseNumber", true); + + LintResult result = lint(R"( +local _ = 0x1234567812345678 + +-- smallest possible number that is parsed imprecisely +local _ = 0x20000000000001 + +-- note that numbers before and after parse precisely (number after is even => 1 more mantissa bit) +local _ = 0x20000000000000 +local _ = 0x20000000000002 + +-- large powers of two should work as well (this is 2^63) +local _ = 0x80000000000000 +)"); + + REQUIRE(2 == result.warnings.size()); + CHECK_EQ(result.warnings[0].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[0].location.begin.line, 1); + CHECK_EQ(result.warnings[1].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[1].location.begin.line, 4); } TEST_CASE_FIXTURE(Fixture, "ComparisonPrecedence") diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index 208db5d8..c596b5b8 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -14,7 +14,7 @@ using namespace Luau; LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); -LUAU_FASTFLAG(LuauStacklessTypeClone2) +LUAU_FASTFLAG(LuauStacklessTypeClone3) TEST_SUITE_BEGIN("ModuleTests"); @@ -336,7 +336,7 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit") int limit = 400; #endif - ScopedFastFlag sff{"LuauStacklessTypeClone2", false}; + ScopedFastFlag sff{"LuauStacklessTypeClone3", false}; ScopedFastInt luauTypeCloneRecursionLimit{"LuauTypeCloneRecursionLimit", limit}; TypeArena src; @@ -360,7 +360,7 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit") TEST_CASE_FIXTURE(Fixture, "clone_iteration_limit") { - ScopedFastFlag sff{"LuauStacklessTypeClone2", true}; + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; ScopedFastInt sfi{"LuauTypeCloneIterationLimit", 500}; TypeArena src; @@ -390,8 +390,6 @@ TEST_CASE_FIXTURE(Fixture, "clone_iteration_limit") // they are. TEST_CASE_FIXTURE(Fixture, "clone_cyclic_union") { - ScopedFastFlag sff{"LuauCloneCyclicUnions", true}; - TypeArena src; TypeId u = src.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}}); @@ -417,10 +415,6 @@ TEST_CASE_FIXTURE(Fixture, "clone_cyclic_union") TEST_CASE_FIXTURE(Fixture, "any_persistance_does_not_leak") { - ScopedFastFlag flags[] = { - {"LuauOccursIsntAlwaysFailure", true}, - }; - fileResolver.source["Module/A"] = R"( export type A = B type B = A @@ -534,4 +528,36 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "clone_table_bound_to_table_bound_to_table") REQUIRE(!tableA->boundTo); } +TEST_CASE_FIXTURE(BuiltinsFixture, "clone_a_bound_type_to_a_persistent_type") +{ + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; + + TypeArena arena; + + TypeId boundTo = arena.addType(BoundType{builtinTypes->numberType}); + REQUIRE(builtinTypes->numberType->persistent); + + TypeArena dest; + CloneState state{builtinTypes}; + TypeId res = clone(boundTo, dest, state); + + REQUIRE(res == follow(boundTo)); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "clone_a_bound_typepack_to_a_persistent_typepack") +{ + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; + + TypeArena arena; + + TypePackId boundTo = arena.addTypePack(BoundTypePack{builtinTypes->neverTypePack}); + REQUIRE(builtinTypes->neverTypePack->persistent); + + TypeArena dest; + CloneState state{builtinTypes}; + TypePackId res = clone(boundTo, dest, state); + + REQUIRE(res == follow(boundTo)); +} + TEST_SUITE_END(); diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index ee818022..54e77532 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -31,6 +31,9 @@ struct IsSubtypeFixture : Fixture bool isConsistentSubtype(TypeId a, TypeId b) { + // any test that is testing isConsistentSubtype is testing the old solver exclusively! + ScopedFastFlag noDcr{"DebugLuauDeferredConstraintResolution", false}; + Location location; ModulePtr module = getMainModule(); REQUIRE(module); @@ -169,7 +172,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "table_with_union_prop") TypeId a = requireType("a"); TypeId b = requireType("b"); - CHECK(isSubtype(a, b)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(!isSubtype(a, b)); // table properties are invariant + else + CHECK(isSubtype(a, b)); CHECK(!isSubtype(b, a)); } @@ -187,7 +193,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "table_with_any_prop") TypeId a = requireType("a"); TypeId b = requireType("b"); - CHECK(isSubtype(a, b)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(!isSubtype(a, b)); // table properties are invariant + else + CHECK(isSubtype(a, b)); CHECK(!isSubtype(b, a)); CHECK(isConsistentSubtype(b, a)); } @@ -249,7 +258,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "tables") TypeId c = requireType("c"); TypeId d = requireType("d"); - CHECK(isSubtype(a, b)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(!isSubtype(a, b)); // table properties are invariant + else + CHECK(isSubtype(a, b)); CHECK(!isSubtype(b, a)); CHECK(isConsistentSubtype(b, a)); @@ -259,7 +271,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "tables") CHECK(isSubtype(d, a)); CHECK(!isSubtype(a, d)); - CHECK(isSubtype(d, b)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(!isSubtype(d, b)); // table properties are invariant + else + CHECK(isSubtype(d, b)); CHECK(!isSubtype(b, d)); } @@ -695,7 +710,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "union_function_and_top_function") TEST_CASE_FIXTURE(NormalizeFixture, "negated_function_is_anything_except_a_function") { - CHECK("(boolean | class | number | string | table | thread)?" == toString(normal(R"( + CHECK("(boolean | buffer | class | number | string | table | thread)?" == toString(normal(R"( Not )"))); } @@ -705,10 +720,22 @@ TEST_CASE_FIXTURE(NormalizeFixture, "specific_functions_cannot_be_negated") CHECK(nullptr == toNormalizedType("Not<(boolean) -> boolean>")); } +TEST_CASE_FIXTURE(NormalizeFixture, "trivial_intersection_inhabited") +{ + // this test was used to fix a bug in normalization when working with intersections/unions of the same type. + + TypeId a = arena.addType(FunctionType{builtinTypes->emptyTypePack, builtinTypes->anyTypePack, std::nullopt, false}); + TypeId c = arena.addType(IntersectionType{{a, a}}); + + const NormalizedType* n = normalizer.normalize(c); + REQUIRE(n); + + CHECK(normalizer.isInhabited(n)); +} + TEST_CASE_FIXTURE(NormalizeFixture, "bare_negated_boolean") { - // TODO: We don't yet have a way to say number | string | thread | nil | Class | Table | Function - CHECK("(class | function | number | string | table | thread)?" == toString(normal(R"( + CHECK("(buffer | class | function | number | string | table | thread)?" == toString(normal(R"( Not )"))); } @@ -821,8 +848,6 @@ TEST_CASE_FIXTURE(NormalizeFixture, "recurring_intersection") TEST_CASE_FIXTURE(NormalizeFixture, "cyclic_union") { - ScopedFastFlag sff{"LuauNormalizeCyclicUnions", true}; - // T where T = any & (number | T) TypeId t = arena.addType(BlockedType{}); TypeId u = arena.addType(UnionType{{builtinTypes->numberType, t}}); @@ -843,11 +868,11 @@ TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_classes") { createSomeClasses(&frontend); CHECK("(Parent & ~Child) | Unrelated" == toString(normal("(Parent & Not) | Unrelated"))); - CHECK("((class & ~Child) | boolean | function | number | string | table | thread)?" == toString(normal("Not"))); + CHECK("((class & ~Child) | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not"))); CHECK("Child" == toString(normal("Not & Child"))); - CHECK("((class & ~Parent) | Child | boolean | function | number | string | table | thread)?" == toString(normal("Not | Child"))); - CHECK("(boolean | function | number | string | table | thread)?" == toString(normal("Not"))); - CHECK("(Parent | Unrelated | boolean | function | number | string | table | thread)?" == + CHECK("((class & ~Parent) | Child | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not | Child"))); + CHECK("(boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not"))); + CHECK("(Parent | Unrelated | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not & Not & Not>"))); } @@ -876,7 +901,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "top_table_type") TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_tables") { CHECK(nullptr == toNormalizedType("Not<{}>")); - CHECK("(boolean | class | function | number | string | thread)?" == toString(normal("Not"))); + CHECK("(boolean | buffer | class | function | number | string | thread)?" == toString(normal("Not"))); CHECK("table" == toString(normal("Not>"))); } @@ -906,4 +931,12 @@ TEST_CASE_FIXTURE(NormalizeFixture, "normalize_is_exactly_number") CHECK(!unionIntersection->isExactlyNumber()); } +TEST_CASE_FIXTURE(NormalizeFixture, "normalize_unknown") +{ + auto nt = toNormalizedType("Not | Not"); + CHECK(nt); + CHECK(nt->isUnknown()); + CHECK(toString(normalizer.typeFromNormal(*nt)) == "unknown"); +} + TEST_SUITE_END(); diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index 0ee135a1..30147402 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -1337,7 +1337,6 @@ TEST_CASE_FIXTURE(Fixture, "parse_error_with_too_many_nested_type_group") TEST_CASE_FIXTURE(Fixture, "can_parse_complex_unions_successfully") { ScopedFastInt sfis[] = {{"LuauRecursionLimit", 10}, {"LuauTypeLengthLimit", 10}}; - ScopedFastFlag sff{"LuauBetterTypeUnionLimits", true}; parse(R"( local f: @@ -1959,8 +1958,6 @@ TEST_CASE_FIXTURE(Fixture, "class_method_properties") TEST_CASE_FIXTURE(Fixture, "class_indexer") { - ScopedFastFlag LuauParseDeclareClassIndexer("LuauParseDeclareClassIndexer", true); - AstStatBlock* stat = parseEx(R"( declare class Foo prop: boolean diff --git a/tests/Set.test.cpp b/tests/Set.test.cpp new file mode 100644 index 00000000..4476452a --- /dev/null +++ b/tests/Set.test.cpp @@ -0,0 +1,63 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Set.h" + +#include "doctest.h" + +TEST_SUITE_BEGIN("SetTests"); + +TEST_CASE("empty_set_size_0") +{ + Luau::Set s1{0}; + CHECK(s1.size() == 0); + CHECK(s1.empty()); +} + +TEST_CASE("insertion_works_and_increases_size") +{ + Luau::Set s1{0}; + CHECK(s1.size() == 0); + CHECK(s1.empty()); + + s1.insert(1); + CHECK(s1.contains(1)); + CHECK(s1.size() == 1); + + s1.insert(2); + CHECK(s1.contains(2)); + CHECK(s1.size() == 2); +} + +TEST_CASE("clear_resets_size") +{ + Luau::Set s1{0}; + s1.insert(1); + s1.insert(2); + REQUIRE(s1.size() == 2); + + s1.clear(); + CHECK(s1.size() == 0); + CHECK(s1.empty()); +} + +TEST_CASE("erase_works_and_decreases_size") +{ + Luau::Set s1{0}; + s1.insert(1); + s1.insert(2); + CHECK(s1.size() == 2); + CHECK(s1.contains(1)); + CHECK(s1.contains(2)); + + s1.erase(1); + CHECK(s1.size() == 1); + CHECK(!s1.contains(1)); + CHECK(s1.contains(2)); + + s1.erase(2); + CHECK(s1.size() == 0); + CHECK(s1.empty()); + CHECK(!s1.contains(1)); + CHECK(!s1.contains(2)); +} + +TEST_SUITE_END(); diff --git a/tests/Simplify.test.cpp b/tests/Simplify.test.cpp index cb333562..0c222f29 100644 --- a/tests/Simplify.test.cpp +++ b/tests/Simplify.test.cpp @@ -32,7 +32,6 @@ struct SimplifyFixture : Fixture const TypeId stringTy = builtinTypes->stringType; const TypeId booleanTy = builtinTypes->booleanType; const TypeId nilTy = builtinTypes->nilType; - const TypeId threadTy = builtinTypes->threadType; const TypeId classTy = builtinTypes->classType; diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index 9dc193d8..d7120bca 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -1,15 +1,17 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypePath.h" -#include "doctest.h" -#include "Fixture.h" -#include "RegisterCallbacks.h" #include "Luau/Normalize.h" #include "Luau/Subtyping.h" #include "Luau/Type.h" #include "Luau/TypePack.h" +#include "doctest.h" +#include "Fixture.h" +#include "RegisterCallbacks.h" +#include + using namespace Luau; namespace Luau @@ -857,6 +859,15 @@ TEST_CASE_FIXTURE(SubtypeFixture, "Child & ~GrandchildOne numberType); } +TEST_CASE_FIXTURE(SubtypeFixture, "semantic_subtyping_disj") +{ + TypeId subTy = builtinTypes->unknownType; + TypeId superTy = join(negate(builtinTypes->numberType), negate(builtinTypes->stringType)); + SubtypingResult result = isSubtype(subTy, superTy); + CHECK(result.isSubtype); +} + + TEST_CASE_FIXTURE(SubtypeFixture, "t1 where t1 = {trim: (t1) -> string} <: t2 where t2 = {trim: (t2) -> string}") { TypeId t1 = cyclicTable([&](TypeId ty, TableType* tt) { @@ -1094,6 +1105,20 @@ TEST_SUITE_END(); TEST_SUITE_BEGIN("Subtyping.Subpaths"); +bool operator==(const DenseHashSet& set, const std::vector& items) +{ + if (items.size() != set.size()) + return false; + + for (const SubtypingReasoning& r : items) + { + if (!set.contains(r)) + return false; + } + + return true; +} + TEST_CASE_FIXTURE(SubtypeFixture, "table_property") { TypeId subTy = tbl({{"X", builtinTypes->numberType}}); @@ -1101,10 +1126,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_property") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == SubtypingReasoning{ + CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")), - }); + }}); } TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers") @@ -1114,18 +1139,14 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == SubtypingReasoning{ - /* subPath */ Path(TypePath::TypeField::IndexLookup), - /* superPath */ Path(TypePath::TypeField::IndexLookup), - }); - - subTy = idx(builtinTypes->stringType, builtinTypes->stringType); - result = isSubtype(subTy, superTy); - CHECK(!result.isSubtype); - CHECK(result.reasoning == SubtypingReasoning{ - /* subPath */ Path(TypePath::TypeField::IndexResult), - /* superPath */ Path(TypePath::TypeField::IndexResult), - }); + CHECK(result.reasoning == std::vector{SubtypingReasoning{ + /* subPath */ Path(TypePath::TypeField::IndexLookup), + /* superPath */ Path(TypePath::TypeField::IndexLookup), + }, + SubtypingReasoning{ + /* subPath */ Path(TypePath::TypeField::IndexResult), + /* superPath */ Path(TypePath::TypeField::IndexResult), + }}); } TEST_CASE_FIXTURE(SubtypeFixture, "fn_arguments") @@ -1135,10 +1156,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "fn_arguments") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == SubtypingReasoning{ + CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().args().index(0).build(), /* superPath */ TypePath::PathBuilder().args().index(0).build(), - }); + }}); } TEST_CASE_FIXTURE(SubtypeFixture, "fn_arguments_tail") @@ -1148,10 +1169,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "fn_arguments_tail") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == SubtypingReasoning{ + CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().args().tail().variadic().build(), /* superPath */ TypePath::PathBuilder().args().tail().variadic().build(), - }); + }}); } TEST_CASE_FIXTURE(SubtypeFixture, "fn_rets") @@ -1161,10 +1182,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "fn_rets") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == SubtypingReasoning{ + CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().rets().index(0).build(), /* superPath */ TypePath::PathBuilder().rets().index(0).build(), - }); + }}); } TEST_CASE_FIXTURE(SubtypeFixture, "fn_rets_tail") @@ -1174,10 +1195,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "fn_rets_tail") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == SubtypingReasoning{ + CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().rets().tail().variadic().build(), /* superPath */ TypePath::PathBuilder().rets().tail().variadic().build(), - }); + }}); } TEST_CASE_FIXTURE(SubtypeFixture, "nested_table_properties") @@ -1187,10 +1208,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "nested_table_properties") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == SubtypingReasoning{ + CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), /* superPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), - }); + }}); } TEST_CASE_FIXTURE(SubtypeFixture, "string_table_mt") @@ -1204,10 +1225,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "string_table_mt") // the string metatable. That means subtyping will see that the entire // metatable is empty, and abort there, without looking at the metatable // properties (because there aren't any). - CHECK(result.reasoning == SubtypingReasoning{ + CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().mt().prop("__index").build(), /* superPath */ TypePath::kEmpty, - }); + }}); } TEST_CASE_FIXTURE(SubtypeFixture, "negation") @@ -1217,9 +1238,22 @@ TEST_CASE_FIXTURE(SubtypeFixture, "negation") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == SubtypingReasoning{ + CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::kEmpty, /* superPath */ Path(TypePath::TypeField::Negated), + }}); +} + +TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings") +{ + TypeId subTy = tbl({{"X", builtinTypes->stringType}, {"Y", builtinTypes->numberType}}); + TypeId superTy = tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->stringType}}); + + SubtypingResult result = isSubtype(subTy, superTy); + CHECK(!result.isSubtype); + CHECK(result.reasoning == std::vector{ + SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X"))}, + SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y"))}, }); } diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index 6c667ee6..00c3c737 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -937,22 +937,10 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch") )"); //clang-format off std::string expected = - (FFlag::DebugLuauDeferredConstraintResolution) ? -R"(Type - '{| a: number, b: string, c: {| d: string |} |}' -could not be converted into - '{ a: number, b: string, c: { d: number } }' -caused by: - Property 'c' is not compatible. -Type - '{| d: string |}' -could not be converted into - '{ d: number }' -caused by: - Property 'd' is not compatible. -Type 'string' could not be converted into 'number' in an invariant context)" - : -R"(Type + (FFlag::DebugLuauDeferredConstraintResolution) + ? R"(Type pack '{| a: number, b: string, c: {| d: string |} |}' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0]["c"]["d"], string is not a subtype of number)" + : + R"(Type '{ a: number, b: string, c: { d: string } }' could not be converted into '{| a: number, b: string, c: {| d: number |} |}' diff --git a/tests/Transpiler.test.cpp b/tests/Transpiler.test.cpp index ae7a925c..871d984e 100644 --- a/tests/Transpiler.test.cpp +++ b/tests/Transpiler.test.cpp @@ -531,8 +531,6 @@ until c TEST_CASE_FIXTURE(Fixture, "transpile_compound_assignment") { - ScopedFastFlag sffs{"LuauFloorDivision", true}; - std::string code = R"( local a = 1 a += 2 diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 3f6d90fa..199b1b22 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -185,7 +185,6 @@ TEST_CASE_FIXTURE(Fixture, "mutually_recursive_aliases") LUAU_REQUIRE_NO_ERRORS(result); } -#if 0 TEST_CASE_FIXTURE(Fixture, "generic_aliases") { ScopedFastFlag sff[] = { @@ -200,7 +199,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases") LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = - R"(Type 'bad' could not be converted into 'T'; type bad["v"] (string) is not a subtype of T["v"] (number))"; + R"(Type 'bad' could not be converted into 'T'; at ["v"], string is not a subtype of number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK_EQ(expected, toString(result.errors[0])); } @@ -220,12 +219,11 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = - R"(Type 'bad' could not be converted into 'U'; type bad["t"]["v"] (string) is not a subtype of U["t"]["v"] (number))"; + R"(Type 'bad' could not be converted into 'U'; at ["t"]["v"], string is not a subtype of number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK_EQ(expected, toString(result.errors[0])); } -#endif TEST_CASE_FIXTURE(Fixture, "mutually_recursive_generic_aliases") { @@ -1037,4 +1035,27 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "alias_expands_to_bare_reference_to_imported_ LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "table_types_record_the_property_locations") +{ + CheckResult result = check(R"( + type Table = { + create: () -> () + } + + local x: Table + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + auto ty = requireTypeAlias("Table"); + + auto ttv = Luau::get(follow(ty)); + REQUIRE(ttv); + + auto propIt = ttv->props.find("create"); + REQUIRE(propIt != ttv->props.end()); + + CHECK_EQ(propIt->second.location, std::nullopt); + CHECK_EQ(propIt->second.typeLocation, Location({2, 12}, {2, 18})); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.annotations.test.cpp b/tests/TypeInfer.annotations.test.cpp index 03534413..99c0b088 100644 --- a/tests/TypeInfer.annotations.test.cpp +++ b/tests/TypeInfer.annotations.test.cpp @@ -541,10 +541,6 @@ TEST_CASE_FIXTURE(Fixture, "typeof_expr") TEST_CASE_FIXTURE(Fixture, "corecursive_types_error_on_tight_loop") { - ScopedFastFlag flags[] = { - {"LuauOccursIsntAlwaysFailure", true}, - }; - CheckResult result = check(R"( type A = B type B = A diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 2c68ae94..1e6150a3 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -484,6 +484,16 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "thread_is_a_type") CHECK("thread" == toString(requireType("co"))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "buffer_is_a_type") +{ + CheckResult result = check(R"( + local b = buffer.create(10) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("buffer" == toString(requireType("b"))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "coroutine_resume_anything_goes") { CheckResult result = check(R"( diff --git a/tests/TypeInfer.cfa.test.cpp b/tests/TypeInfer.cfa.test.cpp index 19700d2c..07652c15 100644 --- a/tests/TypeInfer.cfa.test.cpp +++ b/tests/TypeInfer.cfa.test.cpp @@ -934,7 +934,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_ { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; - // In CGB, we walk the block to prototype aliases. We then visit the block in-order, which will resolve the prototype to a real type. + // In CG, we walk the block to prototype aliases. We then visit the block in-order, which will resolve the prototype to a real type. // That second walk assumes that the name occurs in the same `Scope` that the prototype walk had. If we arbitrarily change scope midway // through, we'd invoke UB. CheckResult result = check(R"( diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 94eec961..a6d3fa5c 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -426,8 +426,6 @@ TEST_CASE_FIXTURE(ClassFixture, "unions_of_intersections_of_classes") TEST_CASE_FIXTURE(ClassFixture, "index_instance_property") { - ScopedFastFlag luauAllowIndexClassParameters{"LuauAllowIndexClassParameters", true}; - CheckResult result = check(R"( local function execute(object: BaseClass, name: string) print(object[name]) @@ -440,8 +438,6 @@ TEST_CASE_FIXTURE(ClassFixture, "index_instance_property") TEST_CASE_FIXTURE(ClassFixture, "index_instance_property_nonstrict") { - ScopedFastFlag luauAllowIndexClassParameters{"LuauAllowIndexClassParameters", true}; - CheckResult result = check(R"( --!nonstrict diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index 615b81d6..86f619fd 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -397,8 +397,6 @@ TEST_CASE_FIXTURE(Fixture, "class_definition_string_props") TEST_CASE_FIXTURE(Fixture, "class_definition_indexer") { - ScopedFastFlag LuauParseDeclareClassIndexer("LuauParseDeclareClassIndexer", true); - loadDefinition(R"( declare class Foo [number]: string diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 1e9b8ad9..da207c49 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -2137,7 +2137,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_before_num_or_str") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == "Type pack 'string' could not be converted into 'number'; at [0], string is not a subtype of number"); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + CHECK_EQ("() -> number", toString(requireType("num_or_str"))); } @@ -2158,7 +2162,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_after_num_or_str") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == "Type pack 'string' could not be converted into 'number'; at [0], string is not a subtype of number"); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); CHECK_EQ("() -> number", toString(requireType("num_or_str"))); } diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 18b8ab8a..6892c78f 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -976,7 +976,6 @@ local y = x["Bar"] LUAU_REQUIRE_NO_ERRORS(result); } -#if 0 TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_degenerate_intersections") { ScopedFastFlag dcr{"DebugLuauDeferredConstraintResolution", true}; @@ -1026,6 +1025,5 @@ TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_more_realistic_intersections") LUAU_REQUIRE_ERRORS(result); } -#endif TEST_SUITE_END(); diff --git a/tests/TypeInfer.oop.test.cpp b/tests/TypeInfer.oop.test.cpp index a332dee2..7fe4a2c3 100644 --- a/tests/TypeInfer.oop.test.cpp +++ b/tests/TypeInfer.oop.test.cpp @@ -415,7 +415,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "promise_type_error_too_complex" * doctest::t // TODO: LTI changes to function call resolution have rendered this test impossibly slow // shared self should fix it, but there may be other mitigations possible as well REQUIRE(!FFlag::DebugLuauDeferredConstraintResolution); - ScopedFastFlag sff{"LuauStacklessTypeClone2", true}; + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; frontend.options.retainFullTypeGraphs = false; diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index 75f1ce03..ba3c8216 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -17,6 +17,7 @@ using namespace Luau; LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) +LUAU_FASTFLAG(LuauRemoveBadRelationalOperatorWarning) TEST_SUITE_BEGIN("TypeInferOperators"); @@ -147,8 +148,6 @@ TEST_CASE_FIXTURE(Fixture, "some_primitive_binary_ops") TEST_CASE_FIXTURE(Fixture, "floor_division_binary_op") { - ScopedFastFlag sffs{"LuauFloorDivision", true}; - CheckResult result = check(R"( local a = 4 // 8 local b = -4 // 9 @@ -768,6 +767,13 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato local foo = a < b )"); + // If DCR is off and the flag to remove this check in the old solver is on, the expected behavior is no errors. + if (!FFlag::DebugLuauDeferredConstraintResolution && FFlag::LuauRemoveBadRelationalOperatorWarning) + { + LUAU_REQUIRE_NO_ERRORS(result); + return; + } + LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::DebugLuauDeferredConstraintResolution) @@ -786,8 +792,6 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato TEST_CASE_FIXTURE(Fixture, "cli_38355_recursive_union") { - ScopedFastFlag sff{"LuauOccursIsntAlwaysFailure", true}; - CheckResult result = check(R"( --!strict local _ @@ -1028,8 +1032,6 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_division") TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_floor_division") { - ScopedFastFlag floorDiv{"LuauFloorDivision", true}; - CheckResult result = check(Mode::Strict, R"( local function f(x, y) return x // y @@ -1452,4 +1454,24 @@ end LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "compare_singleton_string_to_string") +{ + CheckResult result = check(R"( + local function test(a: string, b: string) + if a == "Pet" and b == "Pet" then + return true + elseif a ~= b then + return a < b + else + return false + end + end +)"); + + if (FFlag::LuauRemoveBadRelationalOperatorWarning) + LUAU_REQUIRE_NO_ERRORS(result); + else + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 55bec5af..cba1f37e 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -1540,6 +1540,23 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_thread") CHECK_EQ("number", toString(requireTypeAtPosition({5, 28}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "refine_buffer") +{ + CheckResult result = check(R"( + local function f(x: number | buffer) + if typeof(x) == "buffer" then + local foo = x + else + local foo = x + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("buffer", toString(requireTypeAtPosition({3, 28}))); + CHECK_EQ("number", toString(requireTypeAtPosition({5, 28}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "falsiness_of_TruthyPredicate_narrows_into_nil") { CheckResult result = check(R"( diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 97ee15e1..1bc6b380 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -350,7 +350,6 @@ Table type 'a' not compatible with type 'Bad' because the former is missing fiel CHECK_EQ(expected, toString(result.errors[0])); } -#if 0 TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") { ScopedFastFlag sff[] = { @@ -368,11 +367,11 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expectedError = - "Type 'a' could not be converted into 'Err | Ok'; type a (a) is not a subtype of Err | Ok[1] (Err)"; + "Type 'a' could not be converted into 'Err | Ok'; type a (a) is not a subtype of Err | Ok[1] (Err)" + "\n\ttype a[\"success\"] (false) is not a subtype of Err | Ok[0][\"success\"] (true)"; CHECK(toString(result.errors[0]) == expectedError); } -#endif TEST_CASE_FIXTURE(Fixture, "if_then_else_expression_singleton_options") { diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index d5ae004c..8e32a6a7 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -8,6 +8,7 @@ #include "Fixture.h" +#include "ScopedFlags.h" #include "doctest.h" #include @@ -43,7 +44,10 @@ TEST_CASE_FIXTURE(Fixture, "basic") TEST_CASE_FIXTURE(Fixture, "augment_table") { - CheckResult result = check("local t = {} t.foo = 'bar'"); + CheckResult result = check(R"( + local t = {} + t.foo = 'bar' + )"); LUAU_REQUIRE_NO_ERRORS(result); const TableType* tType = get(requireType("t")); @@ -70,6 +74,35 @@ TEST_CASE_FIXTURE(Fixture, "augment_nested_table") CHECK("{ p: { foo: string } }" == toString(requireType("t"), {true})); } +TEST_CASE_FIXTURE(Fixture, "assign_key_at_index_expr") +{ + CheckResult result = check(R"( + function f(t: {[string]: number}) + t["hello"] = 1 + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // We had a bug where we forgot to record the astType of this particular node. + CHECK("string" == toString(requireTypeAtPosition({2, 19}))); +} + +TEST_CASE_FIXTURE(Fixture, "index_expression_is_checked_against_the_indexer_type") +{ + CheckResult result = check(R"( + function f(t: {[boolean]: number}) + t["hello"] = 15 + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_MESSAGE(get(result.errors[0]), "Expected CannotExtendTable but got " << toString(result.errors[0])); + else + CHECK(get(result.errors[0])); +} + TEST_CASE_FIXTURE(Fixture, "cannot_augment_sealed_table") { CheckResult result = check(R"( @@ -3452,13 +3485,22 @@ TEST_CASE_FIXTURE(Fixture, "a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_ end )"); - const std::string expected = - R"(Type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' could not be converted into 'string' + LUAU_REQUIRE_ERROR_COUNT(1, result); + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == + "Type pack 't1 where t1 = { absolutely_no_scalar_has_this_method: (t1) -> (unknown, a...) }' could not be converted into 'string'; at " + "[0], t1 where t1 = { absolutely_no_scalar_has_this_method: (t1) -> (unknown, a...) } is not a subtype of string"); + else + { + const std::string expected = + R"(Type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' could not be converted into 'string' caused by: The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } + CHECK_EQ("(t1) -> string where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}", toString(requireType("f"))); } @@ -3883,4 +3925,30 @@ TEST_CASE_FIXTURE(Fixture, "simple_method_definition") CHECK_EQ("{| m: (a) -> number |}", toString(getMainModule()->returnType, ToStringOptions{true})); } +TEST_CASE_FIXTURE(Fixture, "identify_all_problematic_table_fields") +{ + ScopedFastFlag sff_DebugLuauDeferredConstraintResolution{"DebugLuauDeferredConstraintResolution", true}; + + CheckResult result = check(R"( + type T = { + a: number, + b: string, + c: boolean, + } + + local a: T = { + a = "foo", + b = false, + c = 123, + } + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not a subtype of number" + "\n\tat [\"b\"], boolean is not a subtype of string" + "\n\tat [\"c\"], number is not a subtype of boolean"; + CHECK(toString(result.errors[0]) == expected); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index a2a28ae5..5af34930 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -2,6 +2,7 @@ #include "Luau/AstQuery.h" #include "Luau/BuiltinDefinitions.h" +#include "Luau/Frontend.h" #include "Luau/Scope.h" #include "Luau/TypeInfer.h" #include "Luau/Type.h" @@ -1261,8 +1262,6 @@ local b = typeof(foo) ~= 'nil' TEST_CASE_FIXTURE(Fixture, "occurs_isnt_always_failure") { - ScopedFastFlag sff{"LuauOccursIsntAlwaysFailure", true}; - CheckResult result = check(R"( function f(x, c) -- x : X local y = if c then x else nil -- y : X? @@ -1441,6 +1440,32 @@ TEST_CASE_FIXTURE(Fixture, "promote_tail_type_packs") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "lti_must_record_contributing_locations") +{ + ScopedFastFlag sff_DebugLuauDeferredConstraintResolution{"DebugLuauDeferredConstraintResolution", true}; + + CheckResult result = check(R"( + local function f(a) + if math.random() > 0.5 then + math.abs(a) + else + string.len(a) + end + end + )"); + + // We inspect the actual errors in other tests; this test verifies that we + // actually recorded breadcrumbs for a. + LUAU_REQUIRE_ERROR_COUNT(3, result); + TypeId fnTy = requireType("f"); + const FunctionType* fn = get(fnTy); + REQUIRE(fn); + + TypeId argTy = *first(fn->argTypes); + std::vector> locations = getMainModule()->upperBoundContributors[argTy]; + CHECK(locations.size() == 2); +} + /* * CLI-49876 * @@ -1453,8 +1478,6 @@ TEST_CASE_FIXTURE(Fixture, "promote_tail_type_packs") */ TEST_CASE_FIXTURE(BuiltinsFixture, "be_sure_to_use_active_txnlog_when_evaluating_a_variadic_overload") { - ScopedFastFlag sff{"LuauVariadicOverloadFix", true}; - CheckResult result = check(R"( local function concat(target: {T}, ...: {T} | T): {T} return (nil :: any) :: {T} diff --git a/tests/TypeInfer.tryUnify.test.cpp b/tests/TypeInfer.tryUnify.test.cpp index cc925cab..9f523ce3 100644 --- a/tests/TypeInfer.tryUnify.test.cpp +++ b/tests/TypeInfer.tryUnify.test.cpp @@ -15,6 +15,9 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) struct TryUnifyFixture : Fixture { + // Cannot use `TryUnifyFixture` under DCR. + ScopedFastFlag noDcr{"DebugLuauDeferredConstraintResolution", false}; + TypeArena arena; ScopePtr globalScope{new Scope{arena.addTypePack({TypeId{}})}}; InternalErrorReporter iceHandler; @@ -139,7 +142,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_tables_are_preserved") CHECK_NE(*getMutable(&tableOne)->props["foo"].type(), *getMutable(&tableTwo)->props["foo"].type()); } -TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_never") +TEST_CASE_FIXTURE(Fixture, "uninhabited_intersection_sub_never") { CheckResult result = check(R"( function f(arg : string & number) : never @@ -149,7 +152,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_never") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_anything") +TEST_CASE_FIXTURE(Fixture, "uninhabited_intersection_sub_anything") { CheckResult result = check(R"( function f(arg : string & number) : boolean @@ -159,7 +162,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_anything") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_never") +TEST_CASE_FIXTURE(Fixture, "uninhabited_table_sub_never") { CheckResult result = check(R"( function f(arg : { prop : string & number }) : never @@ -169,7 +172,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_never") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_anything") +TEST_CASE_FIXTURE(Fixture, "uninhabited_table_sub_anything") { CheckResult result = check(R"( function f(arg : { prop : string & number }) : boolean @@ -179,9 +182,11 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_anything") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(TryUnifyFixture, "members_of_failed_typepack_unification_are_unified_with_errorType") +TEST_CASE_FIXTURE(Fixture, "members_of_failed_typepack_unification_are_unified_with_errorType") { - ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true}; + ScopedFastFlag sff[] = { + {"LuauAlwaysCommitInferencesOfFunctionCalls", true}, + }; CheckResult result = check(R"( function f(arg: number) end @@ -196,9 +201,11 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "members_of_failed_typepack_unification_are_u CHECK_EQ("*error-type*", toString(requireType("b"))); } -TEST_CASE_FIXTURE(TryUnifyFixture, "result_of_failed_typepack_unification_is_constrained") +TEST_CASE_FIXTURE(Fixture, "result_of_failed_typepack_unification_is_constrained") { - ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true}; + ScopedFastFlag sff[] = { + {"LuauAlwaysCommitInferencesOfFunctionCalls", true}, + }; CheckResult result = check(R"( function f(arg: number) return arg end @@ -214,7 +221,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "result_of_failed_typepack_unification_is_con CHECK_EQ("number", toString(requireType("c"))); } -TEST_CASE_FIXTURE(TryUnifyFixture, "typepack_unification_should_trim_free_tails") +TEST_CASE_FIXTURE(Fixture, "typepack_unification_should_trim_free_tails") { CheckResult result = check(R"( --!strict @@ -254,7 +261,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "variadic_tails_respect_progress") CHECK(state.errors.empty()); } -TEST_CASE_FIXTURE(TryUnifyFixture, "variadics_should_use_reversed_properly") +TEST_CASE_FIXTURE(Fixture, "variadics_should_use_reversed_properly") { CheckResult result = check(R"( --!strict @@ -373,22 +380,12 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table state.log.commit(); REQUIRE_EQ(state.errors.size(), 1); - // clang-format off - const std::string expected = - (FFlag::DebugLuauDeferredConstraintResolution) ? -R"(Type - '{ @metatable { __index: { foo: string } }, {| |} }' -could not be converted into - '{- foo: number -}' -caused by: - Type 'number' could not be converted into 'string')" : -R"(Type + const std::string expected = R"(Type '{ @metatable {| __index: {| foo: string |} |}, { } }' could not be converted into '{- foo: number -}' caused by: Type 'number' could not be converted into 'string')"; - // clang-format on CHECK_EQ(expected, toString(state.errors[0])); } diff --git a/tests/TypeInfer.typePacks.cpp b/tests/TypeInfer.typePacks.cpp index 8efa8303..8aa42653 100644 --- a/tests/TypeInfer.typePacks.cpp +++ b/tests/TypeInfer.typePacks.cpp @@ -247,6 +247,7 @@ TEST_CASE_FIXTURE(Fixture, "variadic_pack_syntax") CHECK_EQ(toString(requireType("foo")), "(...number) -> ()"); } +#if 0 TEST_CASE_FIXTURE(Fixture, "type_pack_hidden_free_tail_infinite_growth") { CheckResult result = check(R"( @@ -263,6 +264,7 @@ end LUAU_REQUIRE_ERRORS(result); } +#endif TEST_CASE_FIXTURE(Fixture, "variadic_argument_tail") { @@ -1044,7 +1046,11 @@ TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments_free") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'boolean'"); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == + "Type pack '...number' could not be converted into 'boolean'; type ...number.tail() (...number) is not a subtype of boolean (boolean)"); + else + CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'boolean'"); } TEST_CASE_FIXTURE(BuiltinsFixture, "type_packs_with_tails_in_vararg_adjustment") diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index c15d5c0f..cee36832 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -101,7 +101,6 @@ TEST_CASE_FIXTURE(TypeStateFixture, "refine_a_local_and_then_assign_it") LUAU_REQUIRE_NO_ERRORS(result); } -#endif TEST_CASE_FIXTURE(TypeStateFixture, "assign_a_local_and_then_refine_it") { @@ -118,6 +117,7 @@ TEST_CASE_FIXTURE(TypeStateFixture, "assign_a_local_and_then_refine_it") LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK("Type 'string' could not be converted into 'never'" == toString(result.errors[0])); } +#endif TEST_CASE_FIXTURE(TypeStateFixture, "recursive_local_function") { @@ -197,4 +197,81 @@ TEST_CASE_FIXTURE(TypeStateFixture, "parameter_x_was_constrained_by_two_types") CHECK("(nil) -> number?" == toString(requireType("f"))); } +TEST_CASE_FIXTURE(TypeStateFixture, "parameter_x_is_some_type_or_optional_then_assigned_with_alternate_value") +{ + CheckResult result = check(R"( + local function f(x: number?) + x = x or 5 + return x + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("(number?) -> number" == toString(requireType("f"))); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "local_assigned_in_either_branches_that_falls_through") +{ + CheckResult result = check(R"( + local x = nil + if math.random() > 0.5 then + x = 5 + else + x = "hello" + end + local y = x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("number | string" == toString(requireType("y"))); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "local_assigned_in_only_one_branch_that_falls_through") +{ + CheckResult result = check(R"( + local x = nil + if math.random() > 0.5 then + x = 5 + end + local y = x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("number?" == toString(requireType("y"))); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_and_else_branch_also_assigns_but_is_met_with_return") +{ + CheckResult result = check(R"( + local x = nil + if math.random() > 0.5 then + x = 5 + else + x = "hello" + return + end + local y = x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("number?" == toString(requireType("y"))); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_but_is_met_with_return_and_else_branch_assigns") +{ + CheckResult result = check(R"( + local x = nil + if math.random() > 0.5 then + x = 5 + return + else + x = "hello" + end + local y = x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("string?" == toString(requireType("y"))); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.unknownnever.test.cpp b/tests/TypeInfer.unknownnever.test.cpp index fc7f9707..9077167e 100644 --- a/tests/TypeInfer.unknownnever.test.cpp +++ b/tests/TypeInfer.unknownnever.test.cpp @@ -341,4 +341,49 @@ TEST_CASE_FIXTURE(Fixture, "compare_never") CHECK_EQ("(nil, number) -> boolean", toString(requireType("cmp"))); } +TEST_CASE_FIXTURE(Fixture, "lti_error_at_declaration_for_never_normalizations") +{ + ScopedFastFlag sff_DebugLuauDeferredConstraintResolution{"DebugLuauDeferredConstraintResolution", true}; + + CheckResult result = check(R"( + local function num(x: number) end + local function str(x: string) end + local function cond(): boolean return false end + + local function f(a) + if cond() then + num(a) + else + str(a) + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(3, result); + CHECK(toString(result.errors[0]) == "Parameter 'a' has been reduced to never. This function is not callable with any possible value."); + CHECK(toString(result.errors[1]) == "Parameter 'a' is required to be a subtype of 'number' here."); + CHECK(toString(result.errors[2]) == "Parameter 'a' is required to be a subtype of 'string' here."); +} + +TEST_CASE_FIXTURE(Fixture, "lti_permit_explicit_never_annotation") +{ + ScopedFastFlag sff_DebugLuauDeferredConstraintResolution{"DebugLuauDeferredConstraintResolution", true}; + + CheckResult result = check(R"( + local function num(x: number) end + local function str(x: string) end + local function cond(): boolean return false end + + local function f(a: never) + if cond() then + num(a) + else + str(a) + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypePath.test.cpp b/tests/TypePath.test.cpp index 53127c3d..5d4a49bf 100644 --- a/tests/TypePath.test.cpp +++ b/tests/TypePath.test.cpp @@ -93,7 +93,6 @@ TEST_SUITE_BEGIN("TypePathTraversal"); LUAU_REQUIRE_NO_ERRORS(result); \ } while (false); -#if 0 TEST_CASE_FIXTURE(Fixture, "empty_traversal") { CHECK(traverseForType(builtinTypes->numberType, kEmpty, builtinTypes) == builtinTypes->numberType); @@ -475,7 +474,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "complex_chains") CHECK(*result == builtinTypes->falseType); } } -#endif TEST_SUITE_END(); // TypePathTraversal diff --git a/tests/conformance/bitwise.lua b/tests/conformance/bitwise.lua index 281ad274..f394dc5b 100644 --- a/tests/conformance/bitwise.lua +++ b/tests/conformance/bitwise.lua @@ -140,6 +140,11 @@ assert(bit32.byteswap(0x10203040) == 0x40302010) assert(bit32.byteswap(0) == 0) assert(bit32.byteswap(-1) == 0xffffffff) +-- bit32.bor(n, 0) must clear top bits +-- we check this obscuring the constant through a global to make sure this gets evaluated fully +high32 = 0x42_1234_5678 +assert(bit32.bor(high32, 0) == 0x1234_5678) + --[[ This test verifies a fix in luauF_replace() where if the 4th parameter was not a number, but the first three are numbers, it will diff --git a/tests/conformance/buffers.lua b/tests/conformance/buffers.lua index a6b951ea..1cf996da 100644 --- a/tests/conformance/buffers.lua +++ b/tests/conformance/buffers.lua @@ -34,6 +34,8 @@ local function simple_byte_reads() local x = buffer.readi8(b, 14) + buffer.readi8(b, 13) assert(x == 7) + + buffer.writei8(b, 16, x) end simple_byte_reads() @@ -71,6 +73,11 @@ local function simple_float_reinterpret() buffer.writef32(b, 10, 2.75197) local magic = buffer.readi32(b, 10) assert(magic == 0x40302047) + + buffer.writef32(b, 10, one) + local magic2 = buffer.readi32(b, 10) + + assert(magic2 == 0x3f800000) end simple_float_reinterpret() @@ -89,6 +96,13 @@ local function simple_double_reinterpret() assert(magic1 == 0x40302010) assert(magic2 == 0x3ff70050) + + buffer.writef64(b, 10, one) + local magic3 = buffer.readi32(b, 10) + local magic4 = buffer.readi32(b, 14) + + assert(magic3 == 0x00000000) + assert(magic4 == 0x3ff00000) end simple_double_reinterpret() @@ -149,8 +163,8 @@ simple_copy_ops() -- bounds checking local function createchecks() - assert(ecall(function() buffer.create(-1) end) == "size cannot be negative") - assert(ecall(function() buffer.create(-1000000) end) == "size cannot be negative") + assert(ecall(function() buffer.create(-1) end) == "invalid argument #1 to 'create' (size)") + assert(ecall(function() buffer.create(-1000000) end) == "invalid argument #1 to 'create' (size)") end createchecks() @@ -177,6 +191,7 @@ local function boundchecks() assert(ecall(function() buffer.readi16(b, 0x7ffffffe) end) == "buffer access out of bounds") assert(ecall(function() buffer.readi16(b, 0x7ffffffd) end) == "buffer access out of bounds") assert(ecall(function() buffer.readi16(b, 0x80000000) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readi16(b, 0x0fffffff) end) == "buffer access out of bounds") call(function() buffer.writei16(b, 1022, 0) end) assert(ecall(function() buffer.writei16(b, 1023, 0) end) == "buffer access out of bounds") @@ -219,7 +234,7 @@ local function boundchecks() -- string assert(call(function() return buffer.readstring(b, 1016, 8) end) == "\0\0\0\0\0\0\0\0") assert(ecall(function() buffer.readstring(b, 1017, 8) end) == "buffer access out of bounds") - assert(ecall(function() buffer.readstring(b, -1, -8) end) == "size cannot be negative") + assert(ecall(function() buffer.readstring(b, -1, -8) end) == "invalid argument #3 to 'readstring' (size)") assert(ecall(function() buffer.readstring(b, -100000, 8) end) == "buffer access out of bounds") assert(ecall(function() buffer.readstring(b, -100000, 8) end) == "buffer access out of bounds") @@ -227,7 +242,7 @@ local function boundchecks() assert(ecall(function() buffer.writestring(b, 1017, "abcdefgh") end) == "buffer access out of bounds") assert(ecall(function() buffer.writestring(b, -1, "abcdefgh") end) == "buffer access out of bounds") assert(ecall(function() buffer.writestring(b, -100000, "abcdefgh") end) == "buffer access out of bounds") - assert(ecall(function() buffer.writestring(b, 100, "abcd", -5) end) == "count cannot be negative") + assert(ecall(function() buffer.writestring(b, 100, "abcd", -5) end) == "invalid argument #4 to 'writestring' (count)") assert(ecall(function() buffer.writestring(b, 100, "abcd", 50) end) == "string length overflow") -- copy @@ -374,6 +389,60 @@ end boundcheckssmall() +local function boundcheckssmallnonconst(zero, one, minus1, minus2, minus4, minus7, minus8) + local b = buffer.create(1) + + assert(call(function() return buffer.readi8(b, 0) end) == 0) + assert(ecall(function() buffer.readi8(b, one) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readi8(b, minus1) end) == "buffer access out of bounds") + + call(function() buffer.writei8(b, 0, 0) end) + assert(ecall(function() buffer.writei8(b, one, 0) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writei8(b, minus1, 0) end) == "buffer access out of bounds") + + -- i16 + assert(ecall(function() buffer.readi16(b, zero) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readi16(b, minus1) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readi16(b, minus2) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writei16(b, zero, 0) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writei16(b, minus1, 0) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writei16(b, minus2, 0) end) == "buffer access out of bounds") + + -- i32 + assert(ecall(function() buffer.readi32(b, zero) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readi32(b, minus1) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readi32(b, minus4) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writei32(b, zero, 0) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writei32(b, minus1, 0) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writei32(b, minus4, 0) end) == "buffer access out of bounds") + + -- f32 + assert(ecall(function() buffer.readf32(b, zero) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readf32(b, minus1) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readf32(b, minus4) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writef32(b, zero, 0) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writef32(b, minus1, 0) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writef32(b, minus4, 0) end) == "buffer access out of bounds") + + -- f64 + assert(ecall(function() buffer.readf64(b, zero) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readf64(b, minus1) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readf64(b, minus8) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writef64(b, zero, 0) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writef64(b, minus1, 0) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writef64(b, minus7, 0) end) == "buffer access out of bounds") + + -- string + assert(ecall(function() buffer.readstring(b, zero, 8) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readstring(b, minus1, 8) end) == "buffer access out of bounds") + assert(ecall(function() buffer.readstring(b, minus8, 8) end) == "buffer access out of bounds") + assert(ecall(function() buffer.writestring(b, zero, "abcdefgh") end) == "buffer access out of bounds") + assert(ecall(function() buffer.writestring(b, minus1, "abcdefgh") end) == "buffer access out of bounds") + assert(ecall(function() buffer.writestring(b, minus7, "abcdefgh") end) == "buffer access out of bounds") +end + +boundcheckssmallnonconst(0, 1, -1, -2, -4, -7, -8) + local function boundchecksempty() local b = buffer.create(0) -- useless, but probably more generic @@ -505,14 +574,21 @@ end fill() -local function misc() +local function misc(t16) local b = buffer.create(1000) assert(select('#', buffer.writei32(b, 10, 40)) == 0) assert(select('#', buffer.writef32(b, 20, 40.0)) == 0) + + -- some extra operation to place '#t16' into a linear block + t16[1] = 10 + t16[15] = 20 + + buffer.writei32(b, #t16, 10) + assert(buffer.readi32(b, 16) == 10) end -misc() +misc(table.create(16, 0)) local function testslowcalls() getfenv() @@ -527,12 +603,13 @@ local function testslowcalls() boundchecks() boundchecksnonconst(1024, -1, -100000, 0x7fffffff) boundcheckssmall() + boundcheckssmallnonconst(0, 1, -1, -2, -4, -7, -8) boundchecksempty() intuint() intuinttricky() fromtostring() fill() - misc() + misc(table.create(16, 0)) end testslowcalls() diff --git a/tests/conformance/math.lua b/tests/conformance/math.lua index d285df78..9262f4ea 100644 --- a/tests/conformance/math.lua +++ b/tests/conformance/math.lua @@ -61,6 +61,7 @@ assert(1111111111111111-1111111111111110== 1000.00e-03) -- 1234567890123456 assert(1.1 == '1.'+'.1') assert('1111111111111111'-'1111111111111110' == tonumber" +0.001e+3 \n\t") +assert(10000000000000001 == 10000000000000000) function eq (a,b,limit) if not limit then limit = 10E-10 end diff --git a/tests/conformance/utf8.lua b/tests/conformance/utf8.lua index bfd7a1ac..c86e90eb 100644 --- a/tests/conformance/utf8.lua +++ b/tests/conformance/utf8.lua @@ -15,20 +15,33 @@ end local justone = "^" .. utf8.charpattern .. "$" +-- 't' is the list of codepoints of 's' +local function checksyntax (s, t) + -- creates a string "return '\u{t[1]}...\u{t[n]}'" + local ts = {"return '"} + for i = 1, #t do ts[i + 1] = string.format("\\u{%x}", t[i]) end + ts[#t + 2] = "'" + ts = table.concat(ts) + -- its execution should result in 's' + assert(assert(loadstring(ts))() == s) +end + assert(not utf8.offset("alo", 5)) assert(not utf8.offset("alo", -4)) -- 'check' makes several tests over the validity of string 's'. -- 't' is the list of codepoints of 's'. -local function check (s, t, nonstrict) - local l = utf8.len(s, 1, -1, nonstrict) +local function check (s, t) + local l = utf8.len(s, 1, -1) assert(#t == l and len(s) == l) assert(utf8.char(table.unpack(t)) == s) -- 't' and 's' are equivalent assert(utf8.offset(s, 0) == 1) + checksyntax(s, t) + -- creates new table with all codepoints of 's' - local t1 = {utf8.codepoint(s, 1, -1, nonstrict)} + local t1 = {utf8.codepoint(s, 1, -1)} assert(#t == #t1) for i = 1, #t do assert(t[i] == t1[i]) end -- 't' is equal to 't1' @@ -38,25 +51,25 @@ local function check (s, t, nonstrict) assert(string.find(string.sub(s, pi, pi1 - 1), justone)) assert(utf8.offset(s, -1, pi1) == pi) assert(utf8.offset(s, i - l - 1) == pi) - assert(pi1 - pi == #utf8.char(utf8.codepoint(s, pi, pi, nonstrict))) + assert(pi1 - pi == #utf8.char(utf8.codepoint(s, pi, pi))) for j = pi, pi1 - 1 do assert(utf8.offset(s, 0, j) == pi) end for j = pi + 1, pi1 - 1 do assert(not utf8.len(s, j)) end - assert(utf8.len(s, pi, pi, nonstrict) == 1) - assert(utf8.len(s, pi, pi1 - 1, nonstrict) == 1) - assert(utf8.len(s, pi, -1, nonstrict) == l - i + 1) - assert(utf8.len(s, pi1, -1, nonstrict) == l - i) - assert(utf8.len(s, 1, pi, nonstrict) == i) + assert(utf8.len(s, pi, pi) == 1) + assert(utf8.len(s, pi, pi1 - 1) == 1) + assert(utf8.len(s, pi, -1) == l - i + 1) + assert(utf8.len(s, pi1, -1) == l - i) + assert(utf8.len(s, 1, pi) == i) end local i = 0 - for p, c in utf8.codes(s, nonstrict) do + for p, c in utf8.codes(s) do i = i + 1 assert(c == t[i] and p == utf8.offset(s, i)) - assert(utf8.codepoint(s, p, p, nonstrict) == c) + assert(utf8.codepoint(s, p, p) == c) end assert(i == #t) @@ -80,9 +93,15 @@ do -- error indication in utf8.len assert(not a and b == p) end check("abc\xE3def", 4) - check("汉字\x80", #("汉字") + 1) check("\xF4\x9F\xBF", 1) check("\xF4\x9F\xBF\xBF", 1) + -- spurious continuation bytes + check("汉字\x80", #("汉字") + 1) + check("\x80hello", 1) + check("hel\x80lo", 4) + check("汉字\xBF", #("汉字") + 1) + check("\xBFhello", 1) + check("hel\xBFlo", 4) end -- errors in utf8.codes @@ -94,7 +113,17 @@ do end) end errorcodes("ab\xff") - -- errorcodes("\u{110000}") + errorcodes("\244\144\128\128") -- "\u{110000}" in Lua 5.4 + errorcodes("in\x80valid") + errorcodes("\xbfinvalid") + errorcodes("αλφ\xBFα") + + -- calling interation function with invalid arguments + local f = utf8.codes("") + assert(f("", 2) == nil) + assert(f("", -1) == nil) + assert(f("", math.mininteger) == nil) + end -- error in initial position for offset @@ -131,16 +160,16 @@ do -- surrogates assert(utf8.codepoint("\u{D7FF}") == 0xD800 - 1) assert(utf8.codepoint("\u{E000}") == 0xDFFF + 1) - assert(utf8.codepoint("\u{D800}", 1, 1, true) == 0xD800) - assert(utf8.codepoint("\u{DFFF}", 1, 1, true) == 0xDFFF) - -- assert(utf8.codepoint("\u{7FFFFFFF}", 1, 1, true) == 0x7FFFFFFF) + assert(pcall(utf8.codepoint, "\u{D800}") == false) -- allowed in Luau 5.4 when called with lax=true + assert(pcall(utf8.codepoint, "\u{DFFF}") == false) -- allowed in Luau 5.4 when called with lax=true + assert(pcall(utf8.codepoint, "\253\191\191\191\191\191") == false) -- 0x7FFFFFFF in Lua 5.4 when called with lax=true end assert(utf8.char() == "") assert(utf8.char(0, 97, 98, 99, 1) == "\0abc\1") assert(utf8.codepoint(utf8.char(0x10FFFF)) == 0x10FFFF) --- assert(utf8.codepoint(utf8.char(0x7FFFFFFF), 1, 1, true) == 2147483647) +assert(pcall(utf8.char, 0x7FFFFFFF) == false) -- valid in Lua 5.4 checkerror("value out of range", utf8.char, 0x7FFFFFFF + 1) checkerror("value out of range", utf8.char, -1) @@ -154,8 +183,8 @@ end invalid("\xF4\x9F\xBF\xBF") -- surrogates --- invalid("\u{D800}") --- invalid("\u{DFFF}") +invalid("\u{D800}") +invalid("\u{DFFF}") -- overlong sequences invalid("\xC0\x80") -- zero @@ -182,7 +211,7 @@ s = "\0 \x7F\z s = string.gsub(s, " ", "") check(s, {0,0x7F, 0x80,0x7FF, 0x800,0xFFFF, 0x10000,0x10FFFF}) -x = "日本語a-4\0éó" +local x = "日本語a-4\0éó" check(x, {26085, 26412, 35486, 97, 45, 52, 0, 233, 243}) diff --git a/tests/main.cpp b/tests/main.cpp index 96a6525f..fa6d61b5 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -28,6 +28,7 @@ #endif #include +#include // Indicates if verbose output is enabled; can be overridden via --verbose // Currently, this enables output from 'print', but other verbose output could be enabled eventually. @@ -98,8 +99,6 @@ static int testAssertionHandler(const char* expr, const char* file, int line, co return 1; } - - struct BoostLikeReporter : doctest::IReporter { const doctest::TestCaseData* currentTest = nullptr; @@ -180,6 +179,70 @@ struct BoostLikeReporter : doctest::IReporter void test_case_skipped(const doctest::TestCaseData&) override {} }; +struct TeamCityReporter : doctest::IReporter +{ + const doctest::TestCaseData* currentTest = nullptr; + + TeamCityReporter(const doctest::ContextOptions& in) {} + + void report_query(const doctest::QueryData&) override {} + + void test_run_start() override {} + + void test_run_end(const doctest::TestRunStats& /*in*/) override {} + + void test_case_start(const doctest::TestCaseData& in) override + { + currentTest = ∈ + printf("##teamcity[testStarted name='%s: %s' captureStandardOutput='true']\n", in.m_test_suite, in.m_name); + } + + // called when a test case is reentered because of unfinished subcases + void test_case_reenter(const doctest::TestCaseData& /*in*/) override {} + + void test_case_end(const doctest::CurrentTestCaseStats& in) override + { + printf("##teamcity[testMetadata testName='%s: %s' name='total_asserts' type='number' value='%d']\n", currentTest->m_test_suite, currentTest->m_name, in.numAssertsCurrentTest); + printf("##teamcity[testMetadata testName='%s: %s' name='failed_asserts' type='number' value='%d']\n", currentTest->m_test_suite, currentTest->m_name, in.numAssertsFailedCurrentTest); + printf("##teamcity[testMetadata testName='%s: %s' name='runtime' type='number' value='%f']\n", currentTest->m_test_suite, currentTest->m_name, in.seconds); + + if (!in.testCaseSuccess) + printf("##teamcity[testFailed name='%s: %s']\n", currentTest->m_test_suite, currentTest->m_name); + + printf("##teamcity[testFinished name='%s: %s']\n", currentTest->m_test_suite, currentTest->m_name); + } + + void test_case_exception(const doctest::TestCaseException& in) override { + printf("##teamcity[testFailed name='%s: %s' message='Unhandled exception' details='%s']\n", currentTest->m_test_suite, currentTest->m_name, in.error_string.c_str()); + } + + void subcase_start(const doctest::SubcaseSignature& /*in*/) override {} + void subcase_end() override {} + + void log_assert(const doctest::AssertData& ad) override { + if(!ad.m_failed) + return; + + if (ad.m_decomp.size()) + fprintf(stderr, "%s(%d): ERROR: %s (%s)\n", ad.m_file, ad.m_line, ad.m_expr, ad.m_decomp.c_str()); + else + fprintf(stderr, "%s(%d): ERROR: %s\n", ad.m_file, ad.m_line, ad.m_expr); + } + + void log_message(const doctest::MessageData& md) override { + const char* severity = (md.m_severity & doctest::assertType::is_warn) ? "WARNING" : "ERROR"; + bool isError = md.m_severity & (doctest::assertType::is_require | doctest::assertType::is_check); + fprintf(isError ? stderr : stdout, "%s(%d): %s: %s\n", md.m_file, md.m_line, severity, md.m_string.c_str()); + } + + void test_case_skipped(const doctest::TestCaseData& in) override + { + printf("##teamcity[testIgnored name='%s: %s' captureStandardOutput='false']\n", in.m_test_suite, in.m_name); + } +}; + +REGISTER_REPORTER("teamcity", 1, TeamCityReporter); + template using FValueResult = std::pair; @@ -274,8 +337,6 @@ int main(int argc, char** argv) Luau::assertHandler() = testAssertionHandler; - - doctest::registerReporter("boost", 0, true); doctest::Context context; diff --git a/tools/faillist.txt b/tools/faillist.txt index 46454bbc..c421f2ce 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -8,7 +8,6 @@ AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg AutocompleteTest.autocomplete_interpolated_string_as_singleton -AutocompleteTest.autocomplete_oop_implicit_self AutocompleteTest.autocomplete_response_perf1 AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.autocomplete_string_singleton_escape @@ -230,6 +229,7 @@ IntersectionTypes.table_intersection_write_sealed IntersectionTypes.table_intersection_write_sealed_indirect IntersectionTypes.table_write_sealed_indirect IntersectionTypes.union_saturate_overloaded_functions +isSubtype.any_is_unknown_union_error Linter.DeprecatedApiFenv Linter.FormatStringTyped Linter.TableOperationsIndexer @@ -312,7 +312,7 @@ TableTests.accidentally_checked_prop_in_opposite_branch TableTests.any_when_indexing_into_an_unsealed_table_with_no_indexer_in_nonstrict_mode TableTests.array_factory_function TableTests.call_method -TableTests.cannot_change_type_of_unsealed_table_prop +TableTests.call_method_with_explicit_self_argument TableTests.casting_tables_with_props_into_table_with_indexer2 TableTests.casting_tables_with_props_into_table_with_indexer3 TableTests.casting_tables_with_props_into_table_with_indexer4 @@ -333,7 +333,6 @@ TableTests.cyclic_shifted_tables TableTests.disallow_indexing_into_an_unsealed_table_with_no_indexer_in_strict_mode TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar TableTests.dont_extend_unsealed_tables_in_rvalue_position -TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index 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 @@ -349,6 +348,7 @@ TableTests.expected_indexer_value_type_extra_2 TableTests.explicitly_typed_table TableTests.explicitly_typed_table_error TableTests.explicitly_typed_table_with_indexer +TableTests.fuzz_table_unify_instantiated_table_with_prop_realloc TableTests.generalize_table_argument TableTests.generic_table_instantiation_potential_regression TableTests.indexer_mismatch @@ -379,7 +379,6 @@ TableTests.ok_to_set_nil_even_on_non_lvalue_base_expr TableTests.okay_to_add_property_to_unsealed_tables_by_assignment TableTests.okay_to_add_property_to_unsealed_tables_by_function_call TableTests.only_ascribe_synthetic_names_at_module_scope -TableTests.oop_indexer_works TableTests.oop_polymorphic TableTests.open_table_unification_2 TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table @@ -424,6 +423,8 @@ TableTests.unification_of_unions_in_a_self_referential_type TableTests.unifying_tables_shouldnt_uaf1 TableTests.used_colon_instead_of_dot TableTests.used_dot_instead_of_colon +TableTests.used_dot_instead_of_colon_but_correctly +TableTests.when_augmenting_an_unsealed_table_with_an_indexer_apply_the_correct_scope_to_the_indexer_type TableTests.wrong_assign_does_hit_indexer ToDot.function ToString.exhaustive_toString_of_cyclic_table @@ -431,7 +432,6 @@ ToString.free_types ToString.named_metatable_toStringNamedFunction ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics ToString.primitive -ToString.tostring_error_mismatch ToString.tostring_unsee_ttv_if_array ToString.toStringDetailed2 ToString.toStringErrorPack @@ -458,6 +458,7 @@ TypeAliases.mutually_recursive_types_swapsies_not_ok TypeAliases.recursive_types_restriction_not_ok TypeAliases.report_shadowed_aliases TypeAliases.saturate_to_first_type_pack +TypeAliases.table_types_record_the_property_locations TypeAliases.type_alias_local_mutation TypeAliases.type_alias_local_rename TypeAliases.type_alias_locations @@ -557,7 +558,6 @@ TypeInferFunctions.function_is_supertype_of_concrete_functions TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer TypeInferFunctions.generic_packs_are_not_variadic TypeInferFunctions.higher_order_function_2 -TypeInferFunctions.higher_order_function_3 TypeInferFunctions.higher_order_function_4 TypeInferFunctions.improved_function_arg_mismatch_error_nonstrict TypeInferFunctions.improved_function_arg_mismatch_errors @@ -576,8 +576,6 @@ TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count TypeInferFunctions.luau_subtyping_is_np_hard TypeInferFunctions.no_lossy_function_type -TypeInferFunctions.num_is_solved_after_num_or_str -TypeInferFunctions.num_is_solved_before_num_or_str TypeInferFunctions.occurs_check_failure_in_function_return_type TypeInferFunctions.other_things_are_not_related_to_function TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible @@ -597,9 +595,6 @@ TypeInferFunctions.vararg_function_is_quantified TypeInferLoops.cli_68448_iterators_need_not_accept_nil TypeInferLoops.dcr_iteration_explore_raycast_minimization TypeInferLoops.dcr_iteration_fragmented_keys -TypeInferLoops.dcr_iteration_minimized_fragmented_keys_1 -TypeInferLoops.dcr_iteration_minimized_fragmented_keys_2 -TypeInferLoops.dcr_iteration_minimized_fragmented_keys_3 TypeInferLoops.dcr_iteration_on_never_gives_never TypeInferLoops.dcr_xpath_candidates TypeInferLoops.for_in_loop @@ -647,7 +642,6 @@ TypeInferOOP.methods_are_topologically_sorted TypeInferOOP.object_constructor_can_refer_to_method_of_self TypeInferOOP.promise_type_error_too_complex TypeInferOOP.react_style_oo -TypeInferOOP.table_oop TypeInferOperators.add_type_family_works TypeInferOperators.and_binexps_dont_unify TypeInferOperators.cli_38355_recursive_union @@ -693,8 +687,6 @@ TypePackTests.type_alias_default_type_errors TypePackTests.type_alias_type_packs_import TypePackTests.type_packs_with_tails_in_vararg_adjustment TypePackTests.unify_variadic_tails_in_arguments -TypePackTests.unify_variadic_tails_in_arguments_free -TypePackTests.variadic_argument_tail TypeSingletons.enums_using_singletons_mismatch TypeSingletons.error_detailed_tagged_union_mismatch_bool TypeSingletons.error_detailed_tagged_union_mismatch_string diff --git a/tools/fuzz/fuzzer-postprocess.py b/tools/fuzz/fuzzer-postprocess.py new file mode 100644 index 00000000..742e47fe --- /dev/null +++ b/tools/fuzz/fuzzer-postprocess.py @@ -0,0 +1,168 @@ +#!/usr/bin/python3 +# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +import argparse +import jinja2 +import multiprocessing +import os +import shutil +import subprocess +import sys +import tempfile + + +class CrashReport: + def __init__(self, args, crash_id): + self.id = crash_id + self.args = args + self.crash_root = os.path.join(args.output_directory, crash_id) + + def trace(self) -> str: + with open(os.path.join(self.crash_root, "trace.txt"), "r") as trace_file: + return trace_file.read() + + def modules(self) -> str: + with open(os.path.join(self.crash_root, "modules.txt"), "r") as modules_file: + return modules_file.read() + + def artifact_link(self) -> str: + return f"{self.args.artifact_root}/{self.id}/minimized_reproducer" + + +class MetaValue: + def __init__(self, name, value): + self.name = name + self.value = value + self.link = None + + +def minimize_crash(args, reproducer): + print( + f"Minimizing reproducer {os.path.basename(reproducer)} for {args.minimize_for} seconds.") + + reproducer_absolute = os.path.abspath(reproducer) + + with tempfile.TemporaryDirectory(prefix="fuzzer_minimize") as workdir: + print(f"Working in temporary directory {workdir}.") + artifact = os.path.join(workdir, os.path.basename(reproducer)) + minimize_result = subprocess.run([args.executable, "-detect_leaks=0", "-minimize_crash=1", + f"-exact_artifact_path={artifact}", f"-max_total_time={args.minimize_for}", reproducer_absolute], cwd=workdir, stdout=sys.stdout if args.verbose else subprocess.DEVNULL, stderr=sys.stderr if args.verbose else subprocess.DEVNULL) + if minimize_result.returncode != 0: + print( + f"Minimize process exited with code {minimize_result.returncode}; minimization failed.") + + if os.path.exists(artifact): + print( + f"Minimized {os.path.basename(reproducer)} from {os.path.getsize(reproducer)} bytes to {os.path.getsize(artifact)}.") + with open(artifact, "r") as handle: + return handle.read() + + print(f"Unable to minimize.") + with open(reproducer, "r") as handle: + return handle.read() + + +def process_crash(args, reproducer): + crash_id = os.path.basename(reproducer) + crash_output = os.path.join(args.output_directory, crash_id) + print(f"Processing reproducer {crash_id}.") + + print(f"Output will be stored in {crash_output}.") + if os.path.exists(crash_output): + print(f"Contents of {crash_output} will be discarded.") + shutil.rmtree(crash_output, ignore_errors=True) + + os.makedirs(crash_output) + shutil.copyfile(reproducer, os.path.join( + crash_output, "original_reproducer")) + minimized_reproducer = minimize_crash(args, reproducer) + with open(os.path.join(crash_output, "minimized_reproducer"), "w") as repro_file: + repro_file.write(minimized_reproducer) + + trace_result = subprocess.run([args.executable, os.path.join( + crash_output, "minimized_reproducer")], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + trace_text = trace_result.stdout + + with open(os.path.join(crash_output, "trace.txt"), "w") as trace_file: + trace_file.write(trace_text) + + modules_result = subprocess.run([args.prototest, os.path.join( + crash_output, "minimized_reproducer")], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + modules_text = modules_result.stdout + + module_index_of = modules_text.index("Module") + modules_text = modules_text[module_index_of:] + + with open(os.path.join(crash_output, "modules.txt"), "w") as modules_file: + modules_file.write(modules_text) + + return CrashReport(args, crash_id) + + +def process_crashes(args): + crash_names = os.listdir(args.source_directory) + with multiprocessing.Pool(args.workers) as pool: + crashes = [(args, os.path.join(args.source_directory, c)) for c in crash_names] + crashes = pool.starmap(process_crash, crashes) + print(f"Processed {len(crashes)} crashes.") + return crashes + + +def generate_report(crashes, meta): + env = jinja2.Environment( + loader=jinja2.PackageLoader("fuzzer-postprocess"), + autoescape=jinja2.select_autoescape() + ) + + template = env.get_template("index.html") + with open("fuzz-report.html", "w") as report_file: + report_file.write(template.render( + crashes=crashes, + meta=meta, + )) + + +def __main__(): + parser = argparse.ArgumentParser() + parser.add_argument("--source_directory", required=True) + parser.add_argument("--output_directory", required=True) + parser.add_argument("--executable", required=True) + parser.add_argument("--prototest", required=True) + parser.add_argument("--minimize_for", required=True) + parser.add_argument("--artifact_root", required=True) + parser.add_argument("--verbose", "-v", action="store_true") + parser.add_argument("--workers", action="store", type=int, default=4) + meta_group = parser.add_argument_group( + "metadata", description="Report metadata to attach.") + meta_group.add_argument("--meta.values", nargs="*", + help="Any metadata to attach, in the form name=value. Multiple values may be specified.", dest="metadata_values", default=[]) + meta_group.add_argument("--meta.urls", nargs="*", + help="URLs to attach to metadata, in the form name=url. Multiple values may be specified. A value must also be specified with --meta.values.", dest="metadata_urls", default=[]) + args = parser.parse_args() + + meta_values = dict() + for pair in args.metadata_values: + components = pair.split("=", 1) + name = components[0] + value = components[1] + + meta_values[name] = MetaValue(name, value) + + for pair in args.metadata_urls: + components = pair.split("=", 1) + name = components[0] + url = components[1] + + if name in meta_values: + meta_values[name].link = url + else: + print(f"Metadata {name} has URL {url} but no value specified.") + + meta_values = sorted(list(meta_values.values()), key=lambda x: x.name) + + crashes = process_crashes(args) + generate_report(crashes, meta_values) + + +if __name__ == "__main__": + __main__() diff --git a/tools/fuzz/fuzzfilter.py b/tools/fuzz/fuzzfilter.py new file mode 100644 index 00000000..18e715fe --- /dev/null +++ b/tools/fuzz/fuzzfilter.py @@ -0,0 +1,103 @@ +#!/usr/bin/python3 +# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +# Given a fuzzer binary and a list of crashing programs, this tool collects unique crash reasons and prints reproducers. + +import argparse +import multiprocessing +import os +import re +import subprocess +import sys + + +class Reproducer: + def __init__(self, file, reason, fingerprint): + self.file = file + self.reason = reason + self.fingerprint = fingerprint + + +def get_crash_reason(binary, file, remove_passing): + res = subprocess.run( + [binary, file], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + + if res.returncode == 0: + if remove_passing: + print(f"Warning: {binary} {file} returned 0; removing from result set.", file=sys.stderr) + os.remove(file) + else: + print(f"Warning: {binary} {file} returned 0", file=sys.stderr) + + return None + + err = res.stderr.decode("utf-8") + + if (pos := err.find("ERROR: AddressSanitizer:")) != -1: + return err[pos:] + + if (pos := err.find("ERROR: libFuzzer:")) != -1: + return err[pos:] + + print(f"Warning: {binary} {file} returned unrecognized error {err}", file=sys.stderr) + return None + + +def get_crash_fingerprint(reason): + # Due to ASLR addresses are different every time, so we filter them out + reason = re.sub(r"0x[0-9a-f]+", "0xXXXX", reason) + return reason + + +parser = argparse.ArgumentParser() +parser.add_argument("binary") +parser.add_argument("files", action="append", default=[]) +parser.add_argument("--remove-duplicates", action="store_true") +parser.add_argument("--remove-passing", action="store_true") +parser.add_argument("--workers", action="store", default=1, type=int) +parser.add_argument("--verbose", "-v", action="count", default=0, dest="verbosity") + +args = parser.parse_args() + +def process_file(file): + reason = get_crash_reason(args.binary, file, args.remove_passing) + if reason is None: + return None + + fingerprint = get_crash_fingerprint(reason) + return Reproducer(file, reason, fingerprint) + + +filter_targets = [] +if len(args.files) == 1: + for root, dirs, files in os.walk(args.files[0]): + for file in files: + filter_targets.append(os.path.join(root, file)) +else: + filter_targets = args.files + +with multiprocessing.Pool(processes = args.workers) as pool: + print(f"Processing {len(filter_targets)} reproducers across {args.workers} workers.") + reproducers = [r for r in pool.map(process_file, filter_targets) if r is not None] + + seen = set() + for index, reproducer in enumerate(reproducers): + if reproducer.fingerprint in seen: + if sys.stdout.isatty(): + print("-\|/"[index % 4], end="\r") + + if args.remove_duplicates: + if args.verbosity >= 1: + print(f"Removing duplicate reducer {reproducer.file}.") + os.remove(reproducer.file) + + continue + + seen.add(reproducer.fingerprint) + if args.verbosity >= 2: + print(f"Reproducer: {args.binary} {reproducer.file}") + print(f"Output: {reproducer.reason}") + + print(f"Total unique crashes: {len(seen)}") + if args.remove_duplicates: + print(f"Duplicate reproducers have been removed.") diff --git a/tools/fuzz/requirements.txt b/tools/fuzz/requirements.txt new file mode 100644 index 00000000..0f591a2b --- /dev/null +++ b/tools/fuzz/requirements.txt @@ -0,0 +1,2 @@ +Jinja2==3.1.2 +MarkupSafe==2.1.3 diff --git a/tools/fuzz/templates/index.html b/tools/fuzz/templates/index.html new file mode 100644 index 00000000..85f74c10 --- /dev/null +++ b/tools/fuzz/templates/index.html @@ -0,0 +1,130 @@ + + + + + + + Luau Fuzzer Report + + + +
+
+

Fuzzer Report

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