From c2ba1058c35315a71e6f4b3fdf5b415a9a45bc80 Mon Sep 17 00:00:00 2001 From: Alexander McCord <11488393+alexmccord@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:10:07 -0800 Subject: [PATCH 1/6] Sync to upstream/release/603 (#1097) # What's changed? - Record the location of properties for table types (closes #802) - Implement stricter UTF-8 validations as per the RFC (https://github.com/luau-lang/rfcs/pull/1) - Implement `buffer` as a new type in both the old and new solvers. - Changed errors produced by some `buffer` builtins to be a bit more generic to avoid platform-dependent error messages. - Fixed a bug where `Unifier` would copy some persistent types, tripping some internal assertions. - Type checking rules on relational operators is now a little bit more lax. - Improve dead code elimination for some `if` statements with complex always-false conditions ## New type solver - Dataflow analysis now generates phi nodes on exit of branches. - Dataflow analysis avoids producing a new definition for locals or properties that are not owned by that loop. - If a function parameter has been constrained to `never`, report errors at all uses of that parameter within that function. - Switch to using the new `Luau::Set` to replace `std::unordered_set` to alleviate some poor allocation characteristics which was negatively affecting overall performance. - Subtyping can now report many failing reasons instead of just the first one that we happened to find during the test. - Subtyping now also report reasons for type pack mismatches. - When visiting `if` statements or expressions, the resulting context are the common terms in both branches. ## Native codegen - Implement support for `buffer` builtins to its IR for x64 and A64. - Optimized `table.insert` by not inserting a table barrier if it is fastcalled with a constant. ## Internal Contributors Co-authored-by: Aaron Weiss Co-authored-by: Alexander McCord Co-authored-by: Andy Friesen Co-authored-by: Arseny Kapoulkine Co-authored-by: Aviral Goel Co-authored-by: Lily Brown Co-authored-by: Vyacheslav Egorov --- Analysis/include/Luau/ConstraintGenerator.h | 2 + Analysis/include/Luau/ConstraintSolver.h | 10 +- Analysis/include/Luau/DataFlowGraph.h | 55 +-- Analysis/include/Luau/Def.h | 3 +- Analysis/include/Luau/Error.h | 1 + Analysis/include/Luau/Frontend.h | 4 +- Analysis/include/Luau/Module.h | 2 + Analysis/include/Luau/Normalize.h | 17 +- Analysis/include/Luau/Scope.h | 2 +- Analysis/include/Luau/Set.h | 105 ++++++ Analysis/include/Luau/Simplify.h | 5 +- Analysis/include/Luau/Subtyping.h | 17 +- Analysis/include/Luau/Type.h | 15 +- Analysis/include/Luau/TypeInfer.h | 1 + Analysis/include/Luau/TypePath.h | 13 +- Analysis/include/Luau/Unifier2.h | 3 +- Analysis/src/AstJsonEncoder.cpp | 2 - Analysis/src/Clone.cpp | 35 +- Analysis/src/ConstraintGenerator.cpp | 111 ++++-- Analysis/src/ConstraintSolver.cpp | 36 +- Analysis/src/DataFlowGraph.cpp | 290 +++++++++++---- Analysis/src/Def.cpp | 32 +- Analysis/src/EmbeddedBuiltinDefinitions.cpp | 37 +- Analysis/src/Error.cpp | 7 +- Analysis/src/Frontend.cpp | 18 +- Analysis/src/GlobalTypes.cpp | 3 + Analysis/src/Linter.cpp | 7 +- Analysis/src/NonStrictTypeChecker.cpp | 383 +++++++++++++++----- Analysis/src/Normalize.cpp | 80 ++-- Analysis/src/Scope.cpp | 22 +- Analysis/src/Simplify.cpp | 3 +- Analysis/src/Subtyping.cpp | 86 +++-- Analysis/src/ToString.cpp | 3 + Analysis/src/Transpiler.cpp | 5 - Analysis/src/Type.cpp | 9 + Analysis/src/TypeAttach.cpp | 6 +- Analysis/src/TypeChecker2.cpp | 122 +++++-- Analysis/src/TypeFamily.cpp | 14 +- Analysis/src/TypeInfer.cpp | 44 ++- Analysis/src/TypePath.cpp | 53 ++- Analysis/src/Unifier.cpp | 27 +- Analysis/src/Unifier2.cpp | 7 +- Ast/src/Ast.cpp | 2 - Ast/src/Lexer.cpp | 42 +-- Ast/src/Parser.cpp | 33 +- CodeGen/include/Luau/AssemblyBuilderX64.h | 3 +- CodeGen/include/Luau/IrData.h | 77 +++- CodeGen/include/Luau/IrUtils.h | 8 + CodeGen/src/AssemblyBuilderX64.cpp | 35 +- CodeGen/src/ByteUtils.h | 10 + CodeGen/src/IrDump.cpp | 26 ++ CodeGen/src/IrLoweringA64.cpp | 240 +++++++++++- CodeGen/src/IrLoweringA64.h | 1 + CodeGen/src/IrLoweringX64.cpp | 164 ++++++++- CodeGen/src/IrLoweringX64.h | 1 + CodeGen/src/IrTranslateBuiltins.cpp | 186 +++++++--- CodeGen/src/IrTranslation.cpp | 127 ++----- CodeGen/src/IrUtils.cpp | 16 + CodeGen/src/IrValueLocationTracking.cpp | 4 +- CodeGen/src/OptimizeConstProp.cpp | 31 +- Common/include/Luau/DenseHash.h | 19 + Compiler/src/BytecodeBuilder.cpp | 9 - Compiler/src/Compiler.cpp | 80 ++-- Config/src/Config.cpp | 21 +- Makefile | 8 +- Sources.cmake | 2 + VM/src/lbuflib.cpp | 35 +- VM/src/loslib.cpp | 16 +- VM/src/lutf8lib.cpp | 4 + VM/src/lvmexecute.cpp | 6 - bench/tests/pcmmix.lua | 33 ++ fuzz/proto.cpp | 40 +- tests/AssemblyBuilderX64.test.cpp | 7 + tests/Compiler.test.cpp | 164 +++++++-- tests/Conformance.test.cpp | 17 +- tests/DataFlowGraph.test.cpp | 225 ++++++++++++ tests/Differ.test.cpp | 18 +- tests/Fixture.h | 29 +- tests/Frontend.test.cpp | 2 +- tests/IostreamOptional.h | 37 ++ tests/IrBuilder.test.cpp | 8 +- tests/Linter.test.cpp | 6 +- tests/Module.test.cpp | 6 - tests/Normalize.test.cpp | 17 +- tests/Parser.test.cpp | 3 - tests/Set.test.cpp | 63 ++++ tests/Simplify.test.cpp | 1 - tests/Subtyping.test.cpp | 85 +++-- tests/ToString.test.cpp | 20 +- tests/Transpiler.test.cpp | 2 - tests/TypeInfer.aliases.test.cpp | 2 +- tests/TypeInfer.annotations.test.cpp | 4 - tests/TypeInfer.builtins.test.cpp | 10 + tests/TypeInfer.definitions.test.cpp | 2 - tests/TypeInfer.functions.test.cpp | 11 +- tests/TypeInfer.operators.test.cpp | 34 +- tests/TypeInfer.refinements.test.cpp | 17 + tests/TypeInfer.singletons.test.cpp | 3 +- tests/TypeInfer.tables.test.cpp | 44 ++- tests/TypeInfer.test.cpp | 29 +- tests/TypeInfer.typePacks.cpp | 8 +- tests/TypeInfer.typestates.test.cpp | 79 +++- tests/TypeInfer.unknownnever.test.cpp | 45 +++ tests/conformance/buffers.lua | 91 ++++- tests/conformance/utf8.lua | 8 +- tests/main.cpp | 4 - tools/faillist.txt | 9 +- tools/fuzz/fuzzer-postprocess.py | 168 +++++++++ tools/fuzz/fuzzfilter.py | 103 ++++++ tools/fuzz/requirements.txt | 2 + tools/fuzz/templates/index.html | 130 +++++++ tools/fuzzfilter.py | 47 --- tools/lldb_formatters.py | 140 +++---- 113 files changed, 3600 insertions(+), 1076 deletions(-) create mode 100644 Analysis/include/Luau/Set.h create mode 100644 bench/tests/pcmmix.lua create mode 100644 tests/Set.test.cpp create mode 100644 tools/fuzz/fuzzer-postprocess.py create mode 100644 tools/fuzz/fuzzfilter.py create mode 100644 tools/fuzz/requirements.txt create mode 100644 tools/fuzz/templates/index.html delete mode 100644 tools/fuzzfilter.py diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index 088ae4c2..aab31c40 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -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. 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 f752f022..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" @@ -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 54a4dc61..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 @@ -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. @@ -299,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; @@ -359,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); @@ -381,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 959ec49a..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,8 +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; @@ -740,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); @@ -798,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; @@ -966,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() { @@ -993,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/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/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/Clone.cpp b/Analysis/src/Clone.cpp index a0e76987..1b97bb89 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -12,7 +12,6 @@ LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) -LUAU_FASTFLAGVARIABLE(LuauCloneCyclicUnions, false) LUAU_FASTFLAGVARIABLE(LuauStacklessTypeClone3, false) LUAU_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000) @@ -782,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) diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index e5652549..15e64c92 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -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 { @@ -206,6 +205,66 @@ ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent) return scope; } +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()}; @@ -393,7 +452,7 @@ void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location locat 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) @@ -811,10 +870,10 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f 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()) { @@ -880,7 +939,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f 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) @@ -918,7 +977,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatBlock* bloc 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; } @@ -1000,6 +1063,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatIf* ifState 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)) @@ -1098,7 +1166,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"; } ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareClass* declaredClass) @@ -1140,7 +1208,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareClas scope->exportedTypeBindings[className] = TypeFun{{}, classTy}; - if (FFlag::LuauParseDeclareClassIndexer && declaredClass->indexer) + if (declaredClass->indexer) { RecursionCounter counter{&recursionCount}; @@ -1645,12 +1713,12 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) // 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) { @@ -1676,11 +1744,11 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* globa /* 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 @@ -1698,7 +1766,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* in 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; @@ -1721,7 +1789,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* in 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; @@ -2063,6 +2131,8 @@ std::tuple ConstraintGenerator::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") @@ -2152,18 +2222,11 @@ std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, As */ std::optional annotatedTy = scope->lookup(local->local); if (annotatedTy) - { addConstraint(scope, local->location, SubtypeConstraint{assignedTy, *annotatedTy}); - return 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 ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy) 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 3f78f3a6..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}); @@ -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}; @@ -636,9 +780,14 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId i } // 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) @@ -651,18 +800,28 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId } // 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 feea40c4..710e3699 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -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(); @@ -1285,6 +1286,7 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vectorscopes = 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 4aef48c3..ea35a6a1 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -14,6 +14,8 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) +LUAU_FASTFLAG(LuauBufferTypeck) + namespace Luau { @@ -1105,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") @@ -2215,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); } 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 c21f7f32..9f14b355 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -8,6 +8,7 @@ #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" @@ -18,10 +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 { @@ -268,6 +269,7 @@ NormalizedType::NormalizedType(NotNull builtinTypes) , numbers(builtinTypes->neverType) , strings{NormalizedStringType::never} , threads(builtinTypes->neverType) + , buffers(builtinTypes->neverType) { } @@ -310,13 +312,13 @@ bool NormalizedType::isUnknown() const 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 @@ -373,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(); @@ -393,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) @@ -430,7 +438,7 @@ bool Normalizer::isInhabited(TypeId ty) return *result; } - bool result = isInhabited(ty, {}); + bool result = isInhabited(ty, {nullptr}); if (cacheInhabitance) cachedIsInhabited[ty] = result; @@ -438,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); @@ -492,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); @@ -628,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) @@ -748,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)); @@ -774,7 +796,7 @@ 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()) @@ -795,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)) @@ -818,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(); @@ -1503,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; @@ -1531,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()) @@ -1559,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) { @@ -1620,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(); @@ -1739,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 @@ -1828,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; @@ -2621,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();) { @@ -2658,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); @@ -2699,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()) @@ -2779,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); @@ -2793,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) @@ -2963,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) 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 6e386e68..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. @@ -632,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 2f98e85a..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)) @@ -926,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 bd637453..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,29 +2443,65 @@ struct TypeChecker2 } } - void explainError(TypeId subTy, TypeId superTy, Location location, const SubtypingResult& r) + template + std::optional explainReasonings(TID subTy, TID superTy, Location location, const SubtypingResult& r) { - if (!r.reasoning) - return reportError(TypeMismatch{superTy, subTy}, location); + if (r.reasoning.empty()) + return std::nullopt; - std::optional subLeaf = traverse(subTy, r.reasoning->subPath, builtinTypes); - std::optional superLeaf = traverse(superTy, r.reasoning->superPath, builtinTypes); + std::vector reasons; + for (const SubtypingReasoning& reasoning : r.reasoning) + { + if (reasoning.subPath.empty() && reasoning.superPath.empty()) + continue; - if (!subLeaf || !superLeaf) - ice->ice("Subtyping test returned a reasoning with an invalid path", location); + std::optional subLeaf = traverse(subTy, reasoning.subPath, builtinTypes); + std::optional superLeaf = traverse(superTy, reasoning.superPath, builtinTypes); - 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); + if (!subLeaf || !superLeaf) + ice->ice("Subtyping test returned a reasoning with an invalid path", location); - std::string reason; + 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); - if (r.reasoning->subPath == r.reasoning->superPath) - reason = "at " + toString(r.reasoning->subPath) + ", " + toString(*subLeaf) + " is not a subtype of " + toString(*superLeaf); - else - reason = "type " + toString(subTy) + toString(r.reasoning->subPath) + " (" + toString(*subLeaf) + ") is not a subtype of " + - toString(superTy) + toString(r.reasoning->superPath) + " (" + toString(*superLeaf) + ")"; + 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) + ")"; - reportError(TypeMismatch{superTy, subTy, reason}, location); + 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) @@ -2459,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; } @@ -2507,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) @@ -2568,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 1cc32bdc..0ffe40df 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -35,12 +35,11 @@ LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure) LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false) LUAU_FASTFLAGVARIABLE(LuauLoopControlFlowAnalysis, false) LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false) -LUAU_FASTFLAG(LuauParseDeclareClassIndexer) -LUAU_FASTFLAG(LuauFloorDivision); +LUAU_FASTFLAG(LuauBufferTypeck) +LUAU_FASTFLAGVARIABLE(LuauRemoveBadRelationalOperatorWarning, false) namespace Luau { @@ -202,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 @@ -222,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) @@ -1626,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 @@ -1762,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); @@ -2560,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"; @@ -2763,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; @@ -3058,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; @@ -6016,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 0940ea92..93f8a851 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -19,10 +19,10 @@ LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, 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 { @@ -2873,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)) @@ -2931,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; @@ -2963,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; @@ -2993,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/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/Parser.cpp b/Ast/src/Parser.cpp index 3871ea62..510e5f0e 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -16,14 +16,9 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) // Warning: If you are introducing new syntax, ensure that it is behind a separate // flag so that we don't break production games by reverting syntax changes. // See docs/SyntaxChanges.md for an explanation. -LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false) LUAU_FASTFLAGVARIABLE(LuauClipExtraHasEndProps, false) -LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauCheckedFunctionSyntax) -LUAU_FASTFLAGVARIABLE(LuauBetterTypeUnionLimits, false) -LUAU_FASTFLAGVARIABLE(LuauBetterTypeRecLimits, false) - LUAU_FASTFLAGVARIABLE(LuauParseImpreciseNumber, false) namespace Luau @@ -926,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(); @@ -946,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) { @@ -1546,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; } @@ -1556,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; @@ -1568,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; } @@ -1581,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"); } @@ -1609,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; @@ -1632,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; @@ -1841,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 == '^') @@ -1883,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) 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/IrData.h b/CodeGen/include/Luau/IrData.h index 9beee0ac..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 @@ -621,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/IrDump.cpp b/CodeGen/src/IrDump.cpp index fd015b3a..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: @@ -319,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 98beb513..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); @@ -1967,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 @@ -2000,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! } @@ -2126,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) { @@ -2224,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 df7488a9..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)); @@ -1169,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); @@ -1711,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: @@ -1922,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 b7d66ae6..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 translateBuiltinBit32Unary( - IrBuilder& build, IrCmd cmd, 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}; @@ -607,7 +582,7 @@ static BuiltinImplResult translateBuiltinBit32Unary( } 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}; @@ -631,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); @@ -717,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}; } @@ -742,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 @@ -757,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: @@ -797,29 +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 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: @@ -832,6 +890,32 @@ BuiltinImplResult translateBuiltin(IrBuilder& build, int bfid, int ra, int arg, 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 5e606481..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: @@ -172,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/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index a37b810d..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]; 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/src/BytecodeBuilder.cpp b/Compiler/src/BytecodeBuilder.cpp index 7f545282..296cf4ef 100644 --- a/Compiler/src/BytecodeBuilder.cpp +++ b/Compiler/src/BytecodeBuilder.cpp @@ -7,7 +7,6 @@ #include #include -LUAU_FASTFLAG(LuauFloorDivision) namespace Luau { @@ -1283,8 +1282,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)); @@ -1297,8 +1294,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); @@ -1866,8 +1861,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; @@ -1904,8 +1897,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 c685ffbd..8722fe8d 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -26,8 +26,8 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) -LUAU_FASTFLAG(LuauFloorDivision) -LUAU_FASTFLAGVARIABLE(LuauCompileIfElseAndOr, false) +LUAU_FASTFLAGVARIABLE(LuauCompileSideEffects, false) +LUAU_FASTFLAGVARIABLE(LuauCompileDeadIf, false) namespace Luau { @@ -260,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; @@ -644,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 @@ -1038,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: @@ -1496,8 +1491,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) @@ -1596,18 +1589,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; @@ -2215,6 +2205,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 @@ -2263,10 +2270,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) { @@ -2501,6 +2505,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)) { @@ -2640,7 +2656,7 @@ 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 (loops.back().continueUsed && !continueValidated) { @@ -3230,10 +3246,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 @@ -3276,8 +3289,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); @@ -3425,8 +3436,7 @@ struct Compiler } else { - RegScope rs(this); - compileExprAuto(stat->expr, rs); + compileExprSide(stat->expr); } } else if (AstStatLocal* stat = node->as()) 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/Sources.cmake b/Sources.cmake index fb883d7d..929c99a4 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -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 @@ -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/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/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 35dfd230..922a3505 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -1142,33 +1142,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 )"); } @@ -2001,7 +1995,8 @@ for i = 1, 2 do local x = i == 1 or a until f(x) end -)", 0, 2); +)", + 0, 2); CHECK(!"Expected CompileError"); } @@ -7594,8 +7589,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 = ... @@ -7691,4 +7684,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 2a2017a6..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"); } @@ -462,8 +460,6 @@ TEST_CASE("Pack") TEST_CASE("Vector") { - ScopedFastFlag sffs{"LuauFloorDivision", true}; - lua_CompileOptions copts = defaultOptions(); copts.vectorCtor = "vector"; @@ -521,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"); } @@ -1696,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/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/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 bd69cb13..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); @@ -3133,7 +3127,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "ToDot") updateUseCounts(build.function); computeCfgInfo(build.function); - // note: we don't validate the output of these to avoid test churn when dot formatting changes, but we run these to make sure they don't assert/crash + // 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); diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index f71d92ad..7f6431d9 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -1686,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 )"); @@ -1880,7 +1880,7 @@ local _ = 0x20000000000000 local _ = 0x20000000000002 -- large powers of two should work as well (this is 2^63) -local _ = -9223372036854775808 +local _ = 0x80000000000000 )"); REQUIRE(2 == result.warnings.size()); diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index c966968f..c596b5b8 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -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 diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 30ec3316..54e77532 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -710,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 )"))); } @@ -735,8 +735,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "trivial_intersection_inhabited") 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 )"))); } @@ -849,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}}); @@ -871,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>"))); } @@ -904,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>"))); } 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 57455244..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 @@ -1103,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}}); @@ -1110,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") @@ -1123,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") @@ -1144,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") @@ -1157,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") @@ -1170,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") @@ -1183,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") @@ -1196,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") @@ -1213,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") @@ -1226,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 53772349..199b1b22 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -1048,7 +1048,7 @@ TEST_CASE_FIXTURE(Fixture, "table_types_record_the_property_locations") LUAU_REQUIRE_NO_ERRORS(result); auto ty = requireTypeAlias("Table"); - auto ttv = Luau::get(ty); + auto ttv = Luau::get(follow(ty)); REQUIRE(ttv); auto propIt = ttv->props.find("create"); 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.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.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 23314535..1bc6b380 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -367,7 +367,8 @@ 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); } diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index b0a8b98d..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 @@ -3484,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"))); } @@ -3915,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 a1d5e95a..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 * 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/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/utf8.lua b/tests/conformance/utf8.lua index 3314216f..c86e90eb 100644 --- a/tests/conformance/utf8.lua +++ b/tests/conformance/utf8.lua @@ -160,8 +160,8 @@ do -- surrogates assert(utf8.codepoint("\u{D7FF}") == 0xD800 - 1) assert(utf8.codepoint("\u{E000}") == 0xDFFF + 1) - assert(utf8.codepoint("\u{D800}", 1, 1) == 0xD800) -- TODO: this is an error in Lua 5.4 - assert(utf8.codepoint("\u{DFFF}", 1, 1) == 0xDFFF) -- TODO: this is an error in Lua 5.4 + 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 @@ -183,8 +183,8 @@ end invalid("\xF4\x9F\xBF\xBF") -- surrogates --- invalid("\u{D800}") TODO: this is an error in Lua 5.4 --- invalid("\u{DFFF}") TODO: this is an error in Lua 5.4 +invalid("\u{D800}") +invalid("\u{DFFF}") -- overlong sequences invalid("\xC0\x80") -- zero diff --git a/tests/main.cpp b/tests/main.cpp index 57d4ee7c..fa6d61b5 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -99,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; @@ -339,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 ca8112a1..c421f2ce 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -8,6 +8,7 @@ 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_response_perf1 AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.autocomplete_string_singleton_escape AutocompleteTest.autocomplete_string_singletons @@ -333,6 +334,7 @@ TableTests.disallow_indexing_into_an_unsealed_table_with_no_indexer_in_strict_mo TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar TableTests.dont_extend_unsealed_tables_in_rvalue_position TableTests.dont_leak_free_table_props +TableTests.dont_quantify_table_that_belongs_to_outer_scope TableTests.dont_seal_an_unsealed_table_by_passing_it_to_a_function_that_takes_a_sealed_table TableTests.dont_suggest_exact_match_keys TableTests.error_detailed_indexer_key @@ -346,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 @@ -429,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 @@ -456,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 @@ -573,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 @@ -593,6 +594,7 @@ TypeInferFunctions.too_many_return_values_no_function 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_on_never_gives_never TypeInferLoops.dcr_xpath_candidates TypeInferLoops.for_in_loop @@ -685,7 +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 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) From 4b2af900c28598a18221e4798b72b16010ce7f2a Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Sun, 12 Nov 2023 22:48:41 +0100 Subject: [PATCH 2/6] Fix link to `.luaurc` info in README (#1101) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 64d2d0fb76609947dd89ff93f4e68aa4e6e327b3 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 13 Nov 2023 07:41:15 -0800 Subject: [PATCH 3/6] Update README.md Use rfcs.luau-lang.org for the .luaurc link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8efe7a2c..ba337585 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/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. +`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://rfcs.luau-lang.org/config-luaurc) files. For details please refer to [type checking]( https://luau-lang.org/typecheck) and [linting](https://luau-lang.org/lint) documentation. # Installation From d2059dd50ddd3005ee3f4a3a7141d424fa83f2e2 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 15 Nov 2023 06:30:40 -0800 Subject: [PATCH 4/6] Update codecov.yml Make status checks informational --- .github/codecov.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/codecov.yml b/.github/codecov.yml index b5f7ec42..7e0dee17 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -2,3 +2,6 @@ comment: false coverage: status: patch: false + project: + default: + informational: true From 0492ecffdfae8c838067494f5761627b5563cb5a Mon Sep 17 00:00:00 2001 From: Kostadin Date: Thu, 16 Nov 2023 21:51:16 +0200 Subject: [PATCH 5/6] Add #include to fix building with gcc 14 (#1104) With gcc 14 some C++ Standard Library headers have been changed to no longer include other headers that were used internally by the library. In luau's case it is the `` header. --- Analysis/src/ConstraintSolver.cpp | 1 + Analysis/src/Instantiation.cpp | 2 ++ CodeGen/src/IrAnalysis.cpp | 1 + tests/RuntimeLimits.test.cpp | 2 ++ 4 files changed, 6 insertions(+) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index c056a150..482997c4 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -17,6 +17,7 @@ #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" #include "Luau/VisitType.h" +#include #include LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); diff --git a/Analysis/src/Instantiation.cpp b/Analysis/src/Instantiation.cpp index e74ece06..235786a8 100644 --- a/Analysis/src/Instantiation.cpp +++ b/Analysis/src/Instantiation.cpp @@ -7,6 +7,8 @@ #include "Luau/TypeArena.h" #include "Luau/TypeCheckLimits.h" +#include + LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) namespace Luau diff --git a/CodeGen/src/IrAnalysis.cpp b/CodeGen/src/IrAnalysis.cpp index 63f48ed4..e848970a 100644 --- a/CodeGen/src/IrAnalysis.cpp +++ b/CodeGen/src/IrAnalysis.cpp @@ -8,6 +8,7 @@ #include "lobject.h" +#include #include #include diff --git a/tests/RuntimeLimits.test.cpp b/tests/RuntimeLimits.test.cpp index 3ea60ee7..3bf06333 100644 --- a/tests/RuntimeLimits.test.cpp +++ b/tests/RuntimeLimits.test.cpp @@ -13,6 +13,8 @@ #include "doctest.h" +#include + using namespace Luau; struct LimitFixture : BuiltinsFixture From 298cd70154dfa83dac8b4ee02acd77e48448fea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petri=20H=C3=A4kkinen?= Date: Fri, 17 Nov 2023 14:54:32 +0200 Subject: [PATCH 6/6] Optimize vector literals by storing them in the constant table (#1096) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this optimization, built-in vector constructor calls with 3/4 arguments are detected by the compiler and turned into vector constants when the arguments are constant numbers. Requires optimization level 2 because built-ins are not folded otherwise by the compiler. Bytecode version is bumped because of the new constant type, but old bytecode versions can still be loaded. The following synthetic benchmark shows ~6.6x improvement. ``` local v for i = 1, 10000000 do v = vector(1, 2, 3) v = vector(1, 2, 3) v = vector(1, 2, 3) v = vector(1, 2, 3) v = vector(1, 2, 3) v = vector(1, 2, 3) v = vector(1, 2, 3) v = vector(1, 2, 3) v = vector(1, 2, 3) v = vector(1, 2, 3) end ``` Also tried a more real world scenario and could see a few percent improvement. Added a new fast flag LuauVectorLiterals for enabling the feature. --------- Co-authored-by: Petri Häkkinen Co-authored-by: vegorov-rbx <75688451+vegorov-rbx@users.noreply.github.com> Co-authored-by: Arseny Kapoulkine --- Common/include/Luau/Bytecode.h | 6 +- Compiler/include/Luau/BytecodeBuilder.h | 9 ++- Compiler/src/BuiltinFolding.cpp | 28 ++++++++ Compiler/src/BytecodeBuilder.cpp | 89 ++++++++++++++++++++----- Compiler/src/Compiler.cpp | 22 ++++++ Compiler/src/ConstantFolding.cpp | 7 ++ Compiler/src/ConstantFolding.h | 2 + VM/src/lvmload.cpp | 11 +++ tests/Compiler.test.cpp | 42 +++++++++++- 9 files changed, 196 insertions(+), 20 deletions(-) diff --git a/Common/include/Luau/Bytecode.h b/Common/include/Luau/Bytecode.h index 8096eec5..36dfabdb 100644 --- a/Common/include/Luau/Bytecode.h +++ b/Common/include/Luau/Bytecode.h @@ -45,6 +45,7 @@ // Version 2: Adds Proto::linedefined. Supported until 0.544. // Version 3: Adds FORGPREP/JUMPXEQK* and enhances AUX encoding for FORGLOOP. Removes FORGLOOP_NEXT/INEXT and JUMPIFEQK/JUMPIFNOTEQK. Currently supported. // Version 4: Adds Proto::flags, typeinfo, and floor division opcodes IDIV/IDIVK. Currently supported. +// Version 5: Adds vector constants. Currently supported. // Bytecode opcode, part of the instruction header enum LuauOpcode @@ -70,7 +71,7 @@ enum LuauOpcode // D: value (-32768..32767) LOP_LOADN, - // LOADK: sets register to an entry from the constant table from the proto (number/string) + // LOADK: sets register to an entry from the constant table from the proto (number/vector/string) // A: target register // D: constant table index (0..32767) LOP_LOADK, @@ -426,7 +427,7 @@ enum LuauBytecodeTag { // Bytecode version; runtime supports [MIN, MAX], compiler emits TARGET by default but may emit a higher version when flags are enabled LBC_VERSION_MIN = 3, - LBC_VERSION_MAX = 4, + LBC_VERSION_MAX = 5, LBC_VERSION_TARGET = 4, // Type encoding version LBC_TYPE_VERSION = 1, @@ -438,6 +439,7 @@ enum LuauBytecodeTag LBC_CONSTANT_IMPORT, LBC_CONSTANT_TABLE, LBC_CONSTANT_CLOSURE, + LBC_CONSTANT_VECTOR, }; // Type table tags diff --git a/Compiler/include/Luau/BytecodeBuilder.h b/Compiler/include/Luau/BytecodeBuilder.h index 2d86e412..99bac8ad 100644 --- a/Compiler/include/Luau/BytecodeBuilder.h +++ b/Compiler/include/Luau/BytecodeBuilder.h @@ -54,6 +54,7 @@ public: int32_t addConstantNil(); int32_t addConstantBoolean(bool value); int32_t addConstantNumber(double value); + int32_t addConstantVector(float x, float y, float z, float w); int32_t addConstantString(StringRef value); int32_t addImport(uint32_t iid); int32_t addConstantTable(const TableShape& shape); @@ -146,6 +147,7 @@ private: Type_Nil, Type_Boolean, Type_Number, + Type_Vector, Type_String, Type_Import, Type_Table, @@ -157,6 +159,7 @@ private: { bool valueBoolean; double valueNumber; + float valueVector[4]; unsigned int valueString; // index into string table uint32_t valueImport; // 10-10-10-2 encoded import id uint32_t valueTable; // index into tableShapes[] @@ -167,12 +170,14 @@ private: struct ConstantKey { Constant::Type type; - // Note: this stores value* from Constant; when type is Number_Double, this stores the same bits as double does but in uint64_t. + // Note: this stores value* from Constant; when type is Type_Number, this stores the same bits as double does but in uint64_t. + // For Type_Vector, x and y are stored in 'value' and z and w are stored in 'extra'. uint64_t value; + uint64_t extra = 0; bool operator==(const ConstantKey& key) const { - return type == key.type && value == key.value; + return type == key.type && value == key.value && extra == key.extra; } }; diff --git a/Compiler/src/BuiltinFolding.cpp b/Compiler/src/BuiltinFolding.cpp index 8fa4b7c7..692915c0 100644 --- a/Compiler/src/BuiltinFolding.cpp +++ b/Compiler/src/BuiltinFolding.cpp @@ -5,6 +5,8 @@ #include +LUAU_FASTFLAGVARIABLE(LuauVectorLiterals, false) + namespace Luau { namespace Compile @@ -32,6 +34,16 @@ static Constant cnum(double v) return res; } +static Constant cvector(double x, double y, double z, double w) +{ + Constant res = {Constant::Type_Vector}; + res.valueVector[0] = (float)x; + res.valueVector[1] = (float)y; + res.valueVector[2] = (float)z; + res.valueVector[3] = (float)w; + return res; +} + static Constant cstring(const char* v) { Constant res = {Constant::Type_String}; @@ -55,6 +67,9 @@ static Constant ctype(const Constant& c) case Constant::Type_Number: return cstring("number"); + case Constant::Type_Vector: + return cstring("vector"); + case Constant::Type_String: return cstring("string"); @@ -456,6 +471,19 @@ Constant foldBuiltin(int bfid, const Constant* args, size_t count) if (count == 1 && args[0].type == Constant::Type_Number) return cnum(round(args[0].valueNumber)); break; + + case LBF_VECTOR: + if (FFlag::LuauVectorLiterals && count >= 3 && + args[0].type == Constant::Type_Number && + args[1].type == Constant::Type_Number && + args[2].type == Constant::Type_Number) + { + if (count == 3) + return cvector(args[0].valueNumber, args[1].valueNumber, args[2].valueNumber, 0.0); + else if (count == 4 && args[3].type == Constant::Type_Number) + return cvector(args[0].valueNumber, args[1].valueNumber, args[2].valueNumber, args[3].valueNumber); + } + break; } return cvar(); diff --git a/Compiler/src/BytecodeBuilder.cpp b/Compiler/src/BytecodeBuilder.cpp index 296cf4ef..8dc7b88e 100644 --- a/Compiler/src/BytecodeBuilder.cpp +++ b/Compiler/src/BytecodeBuilder.cpp @@ -7,6 +7,7 @@ #include #include +LUAU_FASTFLAG(LuauVectorLiterals) namespace Luau { @@ -41,6 +42,11 @@ static void writeInt(std::string& ss, int value) ss.append(reinterpret_cast(&value), sizeof(value)); } +static void writeFloat(std::string& ss, float value) +{ + ss.append(reinterpret_cast(&value), sizeof(value)); +} + static void writeDouble(std::string& ss, double value) { ss.append(reinterpret_cast(&value), sizeof(value)); @@ -146,23 +152,43 @@ size_t BytecodeBuilder::StringRefHash::operator()(const StringRef& v) const size_t BytecodeBuilder::ConstantKeyHash::operator()(const ConstantKey& key) const { - // finalizer from MurmurHash64B - const uint32_t m = 0x5bd1e995; + if (key.type == Constant::Type_Vector) + { + uint32_t i[4]; + static_assert(sizeof(key.value) + sizeof(key.extra) == sizeof(i), "Expecting vector to have four 32-bit components"); + memcpy(i, &key.value, sizeof(i)); - uint32_t h1 = uint32_t(key.value); - uint32_t h2 = uint32_t(key.value >> 32) ^ (key.type * m); + // scramble bits to make sure that integer coordinates have entropy in lower bits + i[0] ^= i[0] >> 17; + i[1] ^= i[1] >> 17; + i[2] ^= i[2] >> 17; + i[3] ^= i[3] >> 17; - h1 ^= h2 >> 18; - h1 *= m; - h2 ^= h1 >> 22; - h2 *= m; - h1 ^= h2 >> 17; - h1 *= m; - h2 ^= h1 >> 19; - h2 *= m; + // Optimized Spatial Hashing for Collision Detection of Deformable Objects + uint32_t h = (i[0] * 73856093) ^ (i[1] * 19349663) ^ (i[2] * 83492791) ^ (i[3] * 39916801); - // ... truncated to 32-bit output (normally hash is equal to (uint64_t(h1) << 32) | h2, but we only really need the lower 32-bit half) - return size_t(h2); + return size_t(h); + } + else + { + // finalizer from MurmurHash64B + const uint32_t m = 0x5bd1e995; + + uint32_t h1 = uint32_t(key.value); + uint32_t h2 = uint32_t(key.value >> 32) ^ (key.type * m); + + h1 ^= h2 >> 18; + h1 *= m; + h2 ^= h1 >> 22; + h2 *= m; + h1 ^= h2 >> 17; + h1 *= m; + h2 ^= h1 >> 19; + h2 *= m; + + // ... truncated to 32-bit output (normally hash is equal to (uint64_t(h1) << 32) | h2, but we only really need the lower 32-bit half) + return size_t(h2); + } } size_t BytecodeBuilder::TableShapeHash::operator()(const TableShape& v) const @@ -330,6 +356,24 @@ int32_t BytecodeBuilder::addConstantNumber(double value) return addConstant(k, c); } +int32_t BytecodeBuilder::addConstantVector(float x, float y, float z, float w) +{ + Constant c = {Constant::Type_Vector}; + c.valueVector[0] = x; + c.valueVector[1] = y; + c.valueVector[2] = z; + c.valueVector[3] = w; + + ConstantKey k = {Constant::Type_Vector}; + static_assert(sizeof(k.value) == sizeof(x) + sizeof(y) && sizeof(k.extra) == sizeof(z) + sizeof(w), "Expecting vector to have four 32-bit components"); + memcpy(&k.value, &x, sizeof(x)); + memcpy((char*)&k.value + sizeof(x), &y, sizeof(y)); + memcpy(&k.extra, &z, sizeof(z)); + memcpy((char*)&k.extra + sizeof(z), &w, sizeof(w)); + + return addConstant(k, c); +} + int32_t BytecodeBuilder::addConstantString(StringRef value) { unsigned int index = addStringTableEntry(value); @@ -647,6 +691,14 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags) writeDouble(ss, c.valueNumber); break; + case Constant::Type_Vector: + writeByte(ss, LBC_CONSTANT_VECTOR); + writeFloat(ss, c.valueVector[0]); + writeFloat(ss, c.valueVector[1]); + writeFloat(ss, c.valueVector[2]); + writeFloat(ss, c.valueVector[3]); + break; + case Constant::Type_String: writeByte(ss, LBC_CONSTANT_STRING); writeVarInt(ss, c.valueString); @@ -1071,7 +1123,7 @@ std::string BytecodeBuilder::getError(const std::string& message) uint8_t BytecodeBuilder::getVersion() { // This function usually returns LBC_VERSION_TARGET but may sometimes return a higher number (within LBC_VERSION_MIN/MAX) under fast flags - return LBC_VERSION_TARGET; + return (FFlag::LuauVectorLiterals ? 5 : LBC_VERSION_TARGET); } uint8_t BytecodeBuilder::getTypeEncodingVersion() @@ -1635,6 +1687,13 @@ void BytecodeBuilder::dumpConstant(std::string& result, int k) const case Constant::Type_Number: formatAppend(result, "%.17g", data.valueNumber); break; + case Constant::Type_Vector: + // 3-vectors is the most common configuration, so truncate to three components if possible + if (data.valueVector[3] == 0.0) + formatAppend(result, "%.9g, %.9g, %.9g", data.valueVector[0], data.valueVector[1], data.valueVector[2]); + else + formatAppend(result, "%.9g, %.9g, %.9g, %.9g", data.valueVector[0], data.valueVector[1], data.valueVector[2], data.valueVector[3]); + break; case Constant::Type_String: { const StringRef& str = debugStrings[data.valueString - 1]; diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 8722fe8d..0a5463a2 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -1094,6 +1094,13 @@ struct Compiler return cv && cv->type != Constant::Type_Unknown && !cv->isTruthful(); } + bool isConstantVector(AstExpr* node) + { + const Constant* cv = constants.find(node); + + return cv && cv->type == Constant::Type_Vector; + } + Constant getConstant(AstExpr* node) { const Constant* cv = constants.find(node); @@ -1117,6 +1124,10 @@ struct Compiler std::swap(left, right); } + // disable fast path for vectors because supporting it would require a new opcode + if (operandIsConstant && isConstantVector(right)) + operandIsConstant = false; + uint8_t rl = compileExprAuto(left, rs); if (isEq && operandIsConstant) @@ -1226,6 +1237,10 @@ struct Compiler cid = bytecode.addConstantNumber(c->valueNumber); break; + case Constant::Type_Vector: + cid = bytecode.addConstantVector(c->valueVector[0], c->valueVector[1], c->valueVector[2], c->valueVector[3]); + break; + case Constant::Type_String: cid = bytecode.addConstantString(sref(c->getString())); break; @@ -2052,6 +2067,13 @@ struct Compiler } break; + case Constant::Type_Vector: + { + int32_t cid = bytecode.addConstantVector(cv->valueVector[0], cv->valueVector[1], cv->valueVector[2], cv->valueVector[3]); + emitLoadK(target, cid); + } + break; + case Constant::Type_String: { int32_t cid = bytecode.addConstantString(sref(cv->getString())); diff --git a/Compiler/src/ConstantFolding.cpp b/Compiler/src/ConstantFolding.cpp index 5b098a11..5eb2ac3b 100644 --- a/Compiler/src/ConstantFolding.cpp +++ b/Compiler/src/ConstantFolding.cpp @@ -26,6 +26,13 @@ static bool constantsEqual(const Constant& la, const Constant& ra) case Constant::Type_Number: return ra.type == Constant::Type_Number && la.valueNumber == ra.valueNumber; + case Constant::Type_Vector: + return ra.type == Constant::Type_Vector && + la.valueVector[0] == ra.valueVector[0] && + la.valueVector[1] == ra.valueVector[1] && + la.valueVector[2] == ra.valueVector[2] && + la.valueVector[3] == ra.valueVector[3]; + case Constant::Type_String: return ra.type == Constant::Type_String && la.stringLength == ra.stringLength && memcmp(la.valueString, ra.valueString, la.stringLength) == 0; diff --git a/Compiler/src/ConstantFolding.h b/Compiler/src/ConstantFolding.h index f0798ea2..b22b0bf0 100644 --- a/Compiler/src/ConstantFolding.h +++ b/Compiler/src/ConstantFolding.h @@ -16,6 +16,7 @@ struct Constant Type_Nil, Type_Boolean, Type_Number, + Type_Vector, Type_String, }; @@ -26,6 +27,7 @@ struct Constant { bool valueBoolean; double valueNumber; + float valueVector[4]; const char* valueString = nullptr; // length stored in stringLength }; diff --git a/VM/src/lvmload.cpp b/VM/src/lvmload.cpp index 365aa5d3..5f1ff40a 100644 --- a/VM/src/lvmload.cpp +++ b/VM/src/lvmload.cpp @@ -287,6 +287,17 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size break; } + case LBC_CONSTANT_VECTOR: + { + float x = read(data, size, offset); + float y = read(data, size, offset); + float z = read(data, size, offset); + float w = read(data, size, offset); + (void)w; + setvvalue(&p->k[j], x, y, z, w); + break; + } + case LBC_CONSTANT_STRING: { TString* v = readString(strings, data, size, offset); diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index 922a3505..c605a364 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -17,12 +17,17 @@ std::string rep(const std::string& s, size_t n); using namespace Luau; -static std::string compileFunction(const char* source, uint32_t id, int optimizationLevel = 1) +static std::string compileFunction(const char* source, uint32_t id, int optimizationLevel = 1, bool enableVectors = false) { Luau::BytecodeBuilder bcb; bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code); Luau::CompileOptions options; options.optimizationLevel = optimizationLevel; + if (enableVectors) + { + options.vectorLib = "Vector3"; + options.vectorCtor = "new"; + } Luau::compileOrThrow(bcb, source, options); return bcb.dumpFunction(id); @@ -4475,6 +4480,41 @@ L0: RETURN R0 -1 )"); } +TEST_CASE("VectorLiterals") +{ + ScopedFastFlag sff("LuauVectorLiterals", true); + + CHECK_EQ("\n" + compileFunction("return Vector3.new(1, 2, 3)", 0, 2, /*enableVectors*/ true), R"( +LOADK R0 K0 [1, 2, 3] +RETURN R0 1 +)"); + + CHECK_EQ("\n" + compileFunction("print(Vector3.new(1, 2, 3))", 0, 2, /*enableVectors*/ true), R"( +GETIMPORT R0 1 [print] +LOADK R1 K2 [1, 2, 3] +CALL R0 1 0 +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction("print(Vector3.new(1, 2, 3, 4))", 0, 2, /*enableVectors*/ true), R"( +GETIMPORT R0 1 [print] +LOADK R1 K2 [1, 2, 3, 4] +CALL R0 1 0 +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction("return Vector3.new(0, 0, 0), Vector3.new(-0, 0, 0)", 0, 2, /*enableVectors*/ true), R"( +LOADK R0 K0 [0, 0, 0] +LOADK R1 K1 [-0, 0, 0] +RETURN R0 2 +)"); + + CHECK_EQ("\n" + compileFunction("return type(Vector3.new(0, 0, 0))", 0, 2, /*enableVectors*/ true), R"( +LOADK R0 K0 ['vector'] +RETURN R0 1 +)"); +} + TEST_CASE("TypeAssertion") { // validate that type assertions work with the compiler and that the code inside type assertion isn't evaluated